diff --git a/src/service/launchd.ts b/src/service/launchd.ts index d0f8e76..693193d 100644 --- a/src/service/launchd.ts +++ b/src/service/launchd.ts @@ -1,4 +1,4 @@ -import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'node:fs' +import { writeFileSync, unlinkSync, existsSync, mkdirSync, chmodSync } from 'node:fs' import { join, resolve } from 'node:path' import { homedir } from 'node:os' import { execSync } from 'node:child_process' @@ -6,26 +6,54 @@ import { logger } from '../logger.js' const LABEL = 'com.sftp-proxy' const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`) -const LOG_DIR = join(homedir(), '.config', 'sftp-proxy') +const CONFIG_DIR = join(homedir(), '.config', 'sftp-proxy') +const LOG_DIR = CONFIG_DIR + +/** Path to the wrapper script that macOS shows as the process name */ +const WRAPPER_PATH = join(CONFIG_DIR, 'sftp-proxy') /** * Resolves the absolute paths needed to launch sftp-proxy. * All paths are resolved at install time so the plist is self-contained * regardless of what working directory launchd starts from. + * + * Uses the compiled JS build (`dist/bin/sftp-proxy.js`) with plain `node` + * so there's no dev-dependency (tsx) requirement at runtime. */ -function resolveLaunchPaths(): { projectDir: string; tsxBin: string; entryScript: string } { +function resolveLaunchPaths(): { projectDir: string; nodeBin: string; entryScript: string } { const projectDir = resolve(process.cwd()) - const tsxBin = resolve(projectDir, 'node_modules', '.bin', 'tsx') - const entryScript = resolve(projectDir, 'bin', 'sftp-proxy.ts') + const entryScript = resolve(projectDir, 'dist', 'bin', 'sftp-proxy.js') - if (!existsSync(tsxBin)) { - throw new Error(`tsx not found at ${tsxBin} — run npm install first`) - } if (!existsSync(entryScript)) { - throw new Error(`Entry script not found at ${entryScript}`) + throw new Error( + `Compiled entry not found at ${entryScript} — run "npm run build" first` + ) } - return { projectDir, tsxBin, entryScript } + let nodeBin: string + try { + nodeBin = execSync('which node', { encoding: 'utf-8' }).trim() + } catch { + nodeBin = '/usr/local/bin/node' + } + + return { projectDir, nodeBin, entryScript } +} + +/** + * Creates a small shell wrapper script named "sftp-proxy" so macOS shows + * that name in System Settings > Login Items instead of "Node.js Foundation". + * The wrapper simply exec's node with the compiled entry point. + */ +function createWrapper(nodeBin: string, entryScript: string): void { + const script = [ + '#!/bin/bash', + `exec "${nodeBin}" "${entryScript}" "$@"`, + '', + ].join('\n') + + writeFileSync(WRAPPER_PATH, script, { encoding: 'utf-8' }) + chmodSync(WRAPPER_PATH, 0o755) } /** @@ -34,7 +62,7 @@ function resolveLaunchPaths(): { projectDir: string; tsxBin: string; entryScript * so Node can find node_modules and the config file. */ function generatePlist(): string { - const { projectDir, tsxBin, entryScript } = resolveLaunchPaths() + const { projectDir } = resolveLaunchPaths() return ` @@ -44,8 +72,7 @@ function generatePlist(): string { ${LABEL} ProgramArguments - ${tsxBin} - ${entryScript} + ${WRAPPER_PATH} start WorkingDirectory @@ -73,7 +100,11 @@ function generatePlist(): string { */ export function install(): void { mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true }) - mkdirSync(LOG_DIR, { recursive: true }) + mkdirSync(CONFIG_DIR, { recursive: true }) + + const { nodeBin, entryScript } = resolveLaunchPaths() + createWrapper(nodeBin, entryScript) + logger.info(`Wrote wrapper script to ${WRAPPER_PATH}`) const plist = generatePlist() writeFileSync(PLIST_PATH, plist, { encoding: 'utf-8' }) @@ -106,6 +137,11 @@ export function uninstall(): void { unlinkSync(PLIST_PATH) logger.info(`Removed plist at ${PLIST_PATH}`) } + + if (existsSync(WRAPPER_PATH)) { + unlinkSync(WRAPPER_PATH) + logger.info(`Removed wrapper at ${WRAPPER_PATH}`) + } } /**