diff --git a/package.json b/package.json index 3fb300cd0..495e912d7 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,8 @@ "build:native:dev": "node rust-engine/scripts/build.js --dev", "dev": "node scripts/dev.js", "sf": "node scripts/dev-cli.js", - "sf:server": "node packages/daemon/dist/cli.js", + "sf:server": "node scripts/dev-server.js", + "sf:server:dist": "node packages/daemon/dist/cli.js", "sf:web": "npm run build:pi && npm run copy-resources && node scripts/build-web-if-stale.cjs && node scripts/dev-cli.js --web", "sf:web:stop": "node scripts/dev-cli.js web stop", "sf:web:stop:all": "node scripts/dev-cli.js web stop all", diff --git a/packages/daemon/src/cli-dev.ts b/packages/daemon/src/cli-dev.ts new file mode 100644 index 000000000..1a33df6d7 --- /dev/null +++ b/packages/daemon/src/cli-dev.ts @@ -0,0 +1,3 @@ +import { handleFatalError, main } from './cli-main.js'; + +main().catch(handleFatalError); diff --git a/packages/daemon/src/cli-main.ts b/packages/daemon/src/cli-main.ts new file mode 100644 index 000000000..6e3101956 --- /dev/null +++ b/packages/daemon/src/cli-main.ts @@ -0,0 +1,97 @@ +import { parseArgs } from 'node:util'; +import { resolve } from 'node:path'; +import { resolveConfigPath, loadConfig } from './config.js'; +import { Logger } from './logger.js'; +import { Daemon } from './daemon.js'; +import { install, uninstall, status } from './launchd.js'; + +export const COMMAND_NAME = 'sf-server'; + +export const USAGE = `Usage: sf-server [options] + +Alias: sf-daemon + +Options: + --config Path to YAML config file (default: ~/.sf/daemon.yaml) + --verbose Print log entries to stderr in addition to the log file + --install Install the launchd LaunchAgent (auto-starts on login) + --uninstall Uninstall the launchd LaunchAgent + --status Show launchd agent status (registered, PID, exit code) + --help Show this help message and exit +`; + +export async function main(): Promise { + const { values } = parseArgs({ + options: { + config: { type: 'string', short: 'c' }, + verbose: { type: 'boolean', short: 'v', default: false }, + install: { type: 'boolean', default: false }, + uninstall: { type: 'boolean', default: false }, + status: { type: 'boolean', default: false }, + help: { type: 'boolean', short: 'h', default: false }, + }, + strict: true, + }); + + if (values.help) { + process.stdout.write(USAGE); + process.exit(0); + } + + // --- launchd commands (dispatch before Daemon creation) --- + + if (values.install) { + const configPath = resolveConfigPath(values.config); + const scriptPath = resolve(import.meta.dirname, 'cli.js'); + + install({ + nodePath: process.execPath, + scriptPath, + configPath, + }); + process.stdout.write(`${COMMAND_NAME}: launchd agent installed and loaded.\n`); + process.exit(0); + } + + if (values.uninstall) { + uninstall(); + process.stdout.write(`${COMMAND_NAME}: launchd agent uninstalled.\n`); + process.exit(0); + } + + if (values.status) { + const result = status(); + if (!result.registered) { + process.stdout.write(`${COMMAND_NAME}: not registered with launchd.\n`); + } else if (result.pid != null) { + process.stdout.write( + `${COMMAND_NAME}: running (PID ${result.pid}, last exit status: ${result.lastExitStatus ?? 'n/a'})\n`, + ); + } else { + process.stdout.write( + `${COMMAND_NAME}: registered but not running (last exit status: ${result.lastExitStatus ?? 'n/a'})\n`, + ); + } + process.exit(0); + } + + // --- normal daemon start --- + + const configPath = resolveConfigPath(values.config); + const config = loadConfig(configPath); + + const logger = new Logger({ + filePath: config.log.file, + level: config.log.level, + verbose: values.verbose, + }); + + const daemon = new Daemon(config, logger); + await daemon.start(); +} + +export function handleFatalError(err: unknown): never { + const msg = err instanceof Error ? err.message : String(err); + process.stderr.write(`${COMMAND_NAME}: fatal: ${msg}\n`); + process.exit(1); +} diff --git a/packages/daemon/src/cli.ts b/packages/daemon/src/cli.ts index 27bc19646..f4be73b9f 100644 --- a/packages/daemon/src/cli.ts +++ b/packages/daemon/src/cli.ts @@ -1,98 +1,4 @@ #!/usr/bin/env node -import { parseArgs } from 'node:util'; -import { resolve } from 'node:path'; -import { resolveConfigPath, loadConfig } from './config.js'; -import { Logger } from './logger.js'; -import { Daemon } from './daemon.js'; -import { install, uninstall, status } from './launchd.js'; +import { handleFatalError, main } from './cli-main.js'; -const COMMAND_NAME = 'sf-server'; - -const USAGE = `Usage: sf-server [options] - -Alias: sf-daemon - -Options: - --config Path to YAML config file (default: ~/.sf/daemon.yaml) - --verbose Print log entries to stderr in addition to the log file - --install Install the launchd LaunchAgent (auto-starts on login) - --uninstall Uninstall the launchd LaunchAgent - --status Show launchd agent status (registered, PID, exit code) - --help Show this help message and exit -`; - -async function main(): Promise { - const { values } = parseArgs({ - options: { - config: { type: 'string', short: 'c' }, - verbose: { type: 'boolean', short: 'v', default: false }, - install: { type: 'boolean', default: false }, - uninstall: { type: 'boolean', default: false }, - status: { type: 'boolean', default: false }, - help: { type: 'boolean', short: 'h', default: false }, - }, - strict: true, - }); - - if (values.help) { - process.stdout.write(USAGE); - process.exit(0); - } - - // --- launchd commands (dispatch before Daemon creation) --- - - if (values.install) { - const configPath = resolveConfigPath(values.config); - const scriptPath = resolve(import.meta.dirname, 'cli.js'); - - install({ - nodePath: process.execPath, - scriptPath, - configPath, - }); - process.stdout.write(`${COMMAND_NAME}: launchd agent installed and loaded.\n`); - process.exit(0); - } - - if (values.uninstall) { - uninstall(); - process.stdout.write(`${COMMAND_NAME}: launchd agent uninstalled.\n`); - process.exit(0); - } - - if (values.status) { - const result = status(); - if (!result.registered) { - process.stdout.write(`${COMMAND_NAME}: not registered with launchd.\n`); - } else if (result.pid != null) { - process.stdout.write( - `${COMMAND_NAME}: running (PID ${result.pid}, last exit status: ${result.lastExitStatus ?? 'n/a'})\n`, - ); - } else { - process.stdout.write( - `${COMMAND_NAME}: registered but not running (last exit status: ${result.lastExitStatus ?? 'n/a'})\n`, - ); - } - process.exit(0); - } - - // --- normal daemon start --- - - const configPath = resolveConfigPath(values.config); - const config = loadConfig(configPath); - - const logger = new Logger({ - filePath: config.log.file, - level: config.log.level, - verbose: values.verbose, - }); - - const daemon = new Daemon(config, logger); - await daemon.start(); -} - -main().catch((err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); - process.stderr.write(`${COMMAND_NAME}: fatal: ${msg}\n`); - process.exit(1); -}); +main().catch(handleFatalError); diff --git a/scripts/dev-server.js b/scripts/dev-server.js new file mode 100644 index 000000000..d7d5f6a74 --- /dev/null +++ b/scripts/dev-server.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +import { spawn, spawnSync } from "node:child_process"; +import { resolve } from "node:path"; + +const __dirname = import.meta.dirname; +const root = resolve(__dirname, ".."); +const sourceBinPath = resolve(root, "bin", "sf-from-source"); +const ensureResourcesPath = resolve( + root, + "scripts", + "ensure-source-resources.cjs", +); +const daemonCliPath = resolve(root, "packages", "daemon", "src", "cli-dev.ts"); +const resolveTsPath = resolve( + root, + "src", + "resources", + "extensions", + "sf", + "tests", + "resolve-ts.mjs", +); + +const resourceBuild = spawnSync(process.execPath, [ensureResourcesPath], { + cwd: root, + stdio: "inherit", + env: process.env, +}); + +if (resourceBuild.status !== 0) { + process.exit(resourceBuild.status ?? 1); +} + +const child = spawn( + process.execPath, + [ + "--import", + resolveTsPath, + "--experimental-strip-types", + "--no-warnings", + daemonCliPath, + ...process.argv.slice(2), + ], + { + cwd: process.cwd(), + stdio: "inherit", + env: { + ...process.env, + SF_SOURCE_ROOT: process.env.SF_SOURCE_ROOT || root, + SF_RUNTIME_SOURCE_ROOT: process.env.SF_RUNTIME_SOURCE_ROOT || root, + SF_BIN_PATH: process.env.SF_BIN_PATH || resolve(root, "dist", "loader.js"), + SF_CLI_PATH: process.env.SF_CLI_PATH || sourceBinPath, + }, + }, +); + +child.on("error", (error) => { + console.error( + `[forge] Failed to launch local dev server: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); +}); + +child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 0); +});