116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
import { resolve } from "node:path";
|
|
import { parseArgs } from "node:util";
|
|
import { loadConfig, resolveConfigPath } from "./config.js";
|
|
import { Daemon } from "./daemon.js";
|
|
import { install, status, uninstall } from "./launchd.js";
|
|
import { Logger } from "./logger.js";
|
|
|
|
export const COMMAND_NAME = "sf-server";
|
|
|
|
export const USAGE = `Usage: sf-server [options]
|
|
|
|
Alias: sf-daemon
|
|
|
|
Options:
|
|
--config <path> Path to YAML config file (default: ~/.sf/daemon.yaml)
|
|
--verbose Lower log level to debug AND mirror entries to stderr
|
|
--start <path> Start an autonomous SF session for this project path
|
|
--command <text> Command to send for --start (default: /sf autonomous)
|
|
--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<void> {
|
|
const { values } = parseArgs({
|
|
options: {
|
|
config: { type: "string", short: "c" },
|
|
verbose: { type: "boolean", short: "v", default: false },
|
|
start: { type: "string" },
|
|
command: { type: "string" },
|
|
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);
|
|
|
|
// --verbose lowers log level to debug AND mirrors to stderr. Without
|
|
// this, debug entries are dropped before reaching the verbose stderr
|
|
// mirror — making --verbose silently useless on a default-info config.
|
|
const logger = new Logger({
|
|
filePath: config.log.file,
|
|
level: values.verbose ? "debug" : config.log.level,
|
|
verbose: values.verbose,
|
|
});
|
|
|
|
const daemon = new Daemon(config, logger);
|
|
await daemon.start();
|
|
|
|
if (values.start !== undefined) {
|
|
const projectDir =
|
|
values.start.trim() === "" ? process.cwd() : values.start;
|
|
const sessionId = await daemon.getSessionManager().startSession({
|
|
projectDir,
|
|
...(values.command ? { command: values.command } : {}),
|
|
});
|
|
logger.info("batch session started", { sessionId, projectDir });
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|