Improved background service
This commit is contained in:
@@ -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 { join, resolve } from 'node:path'
|
||||||
import { homedir } from 'node:os'
|
import { homedir } from 'node:os'
|
||||||
import { execSync } from 'node:child_process'
|
import { execSync } from 'node:child_process'
|
||||||
@@ -6,26 +6,54 @@ import { logger } from '../logger.js'
|
|||||||
|
|
||||||
const LABEL = 'com.sftp-proxy'
|
const LABEL = 'com.sftp-proxy'
|
||||||
const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`)
|
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.
|
* Resolves the absolute paths needed to launch sftp-proxy.
|
||||||
* All paths are resolved at install time so the plist is self-contained
|
* All paths are resolved at install time so the plist is self-contained
|
||||||
* regardless of what working directory launchd starts from.
|
* 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 projectDir = resolve(process.cwd())
|
||||||
const tsxBin = resolve(projectDir, 'node_modules', '.bin', 'tsx')
|
const entryScript = resolve(projectDir, 'dist', 'bin', 'sftp-proxy.js')
|
||||||
const entryScript = resolve(projectDir, 'bin', 'sftp-proxy.ts')
|
|
||||||
|
|
||||||
if (!existsSync(tsxBin)) {
|
|
||||||
throw new Error(`tsx not found at ${tsxBin} — run npm install first`)
|
|
||||||
}
|
|
||||||
if (!existsSync(entryScript)) {
|
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.
|
* so Node can find node_modules and the config file.
|
||||||
*/
|
*/
|
||||||
function generatePlist(): string {
|
function generatePlist(): string {
|
||||||
const { projectDir, tsxBin, entryScript } = resolveLaunchPaths()
|
const { projectDir } = resolveLaunchPaths()
|
||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
@@ -44,8 +72,7 @@ function generatePlist(): string {
|
|||||||
<string>${LABEL}</string>
|
<string>${LABEL}</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>${tsxBin}</string>
|
<string>${WRAPPER_PATH}</string>
|
||||||
<string>${entryScript}</string>
|
|
||||||
<string>start</string>
|
<string>start</string>
|
||||||
</array>
|
</array>
|
||||||
<key>WorkingDirectory</key>
|
<key>WorkingDirectory</key>
|
||||||
@@ -73,7 +100,11 @@ function generatePlist(): string {
|
|||||||
*/
|
*/
|
||||||
export function install(): void {
|
export function install(): void {
|
||||||
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true })
|
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()
|
const plist = generatePlist()
|
||||||
writeFileSync(PLIST_PATH, plist, { encoding: 'utf-8' })
|
writeFileSync(PLIST_PATH, plist, { encoding: 'utf-8' })
|
||||||
@@ -106,6 +137,11 @@ export function uninstall(): void {
|
|||||||
unlinkSync(PLIST_PATH)
|
unlinkSync(PLIST_PATH)
|
||||||
logger.info(`Removed plist at ${PLIST_PATH}`)
|
logger.info(`Removed plist at ${PLIST_PATH}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existsSync(WRAPPER_PATH)) {
|
||||||
|
unlinkSync(WRAPPER_PATH)
|
||||||
|
logger.info(`Removed wrapper at ${WRAPPER_PATH}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user