diff --git a/docs/dev/drafts/M053-per-repo-supervisor.md b/docs/dev/drafts/M053-per-repo-supervisor.md index 9487703fc..235e4116d 100644 --- a/docs/dev/drafts/M053-per-repo-supervisor.md +++ b/docs/dev/drafts/M053-per-repo-supervisor.md @@ -43,10 +43,10 @@ belong behind a future federation requirement, not M053. ## Registry Sync -`sf-server` owns swarm registry refresh. It scans configured +`sf-daemon` owns swarm registry refresh. It scans configured `projects.scan_roots` from `~/.sf/daemon.yaml` and atomically rewrites `~/.sf/swarms.json` on startup and every `swarms.refresh_ms` while the server is -running. Operators can run a one-shot refresh with `sf-server --sync-swarms`. +running. Operators can run a one-shot refresh with `sf-daemon --sync-swarms`. This replaces the old script/watchdog shape. There is no repo-local enrollment script and no nested meta-supervisor. The registry is a server-owned read model: @@ -77,8 +77,8 @@ M053 is done when: - The non-TUI status query writes the versioned atomic status projection. - Web has a Swarms view backed by `/api/swarms`. - The web reader survives missing/corrupt projections per repo. -- `sf-server` auto-syncs `~/.sf/swarms.json` from configured scan roots and can - run the same refresh once with `sf-server --sync-swarms`. +- `sf-daemon` auto-syncs `~/.sf/swarms.json` from configured scan roots and can + run the same refresh once with `sf-daemon --sync-swarms`. - Legacy script/watchdog entrypoints are removed from the normal lifecycle. - Linux server/package code can create a user-level systemd worker for `sf autonomous`. diff --git a/package-lock.json b/package-lock.json index e8574a726..600b63c5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,8 +69,7 @@ "bin": { "sf": "dist/loader.js", "sf-cli": "dist/loader.js", - "sf-daemon": "packages/daemon/dist/cli.js", - "sf-server": "packages/daemon/dist/cli.js" + "sf-daemon": "packages/daemon/dist/cli.js" }, "devDependencies": { "@biomejs/biome": "^2.4.15", @@ -17542,8 +17541,7 @@ "zod": "^4.4.3" }, "bin": { - "sf-daemon": "dist/cli.js", - "sf-server": "dist/cli.js" + "sf-daemon": "dist/cli.js" }, "devDependencies": { "@types/node": "^25.6.2", diff --git a/package.json b/package.json index fcedc7f1a..1ec5eba6b 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ "bin": { "sf": "dist/loader.js", "sf-cli": "dist/loader.js", - "sf-daemon": "packages/daemon/dist/cli.js", - "sf-server": "packages/daemon/dist/cli.js" + "sf-daemon": "packages/daemon/dist/cli.js" }, "files": [ "dist", @@ -79,10 +78,12 @@ "build:native:dev": "node rust-engine/scripts/build.js --dev", "dev": "node scripts/dev.js", "sf": "node scripts/dev-cli.js", - "sf-dev": "node scripts/dev-server.js --verbose --start .", + "sf-dev": "node scripts/dev-cli.js server", "sf:dev": "npm run sf-dev", - "sf:server": "node scripts/dev-server.js", - "sf:server:dist": "node packages/daemon/dist/cli.js", + "sf:server": "node scripts/dev-cli.js server", + "sf:server:dist": "node dist/loader.js server", + "sf:daemon": "node scripts/dev-server.js", + "sf:daemon:dist": "node packages/daemon/dist/cli.js", "postinstall": "node scripts/link-workspace-packages.cjs && node scripts/ensure-workspace-builds.cjs && node scripts/postinstall.js", "pi:install-global": "node scripts/install-pi-global.js", "pi:uninstall-global": "node scripts/uninstall-pi-global.js", diff --git a/packages/daemon/package.json b/packages/daemon/package.json index b37e0f38d..c1db9008f 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -21,8 +21,7 @@ } }, "bin": { - "sf-daemon": "./dist/cli.js", - "sf-server": "./dist/cli.js" + "sf-daemon": "./dist/cli.js" }, "scripts": { "build": "tsgo", diff --git a/packages/daemon/src/cli-main.ts b/packages/daemon/src/cli-main.ts index 8e3726d5c..2f89b49f8 100644 --- a/packages/daemon/src/cli-main.ts +++ b/packages/daemon/src/cli-main.ts @@ -8,17 +8,15 @@ import { Logger } from "./logger.js"; import { scanForProjects } from "./project-scanner.js"; import { syncSwarmRegistryFromProjects } from "./swarm-registry.js"; -export const COMMAND_NAME = "sf-server"; +export const COMMAND_NAME = "sf-daemon"; -export const USAGE = `Usage: sf-server [options] - -Alias: sf-daemon +export const USAGE = `Usage: sf-daemon [options] Options: --config Path to YAML config file (default: ~/.sf/daemon.yaml) --verbose Lower log level to debug AND mirror entries to stderr --sync-swarms One-shot SF server swarm registry refresh, then exit - Normal sf-server runs auto-sync by default; set swarms.auto_start=true + Normal sf-daemon runs auto-sync by default; set swarms.auto_start=true to also start missing clients during daemon sync --start Start an autonomous SF session for this project path --command Command to send for --start (default: /sf autonomous) diff --git a/packages/daemon/src/daemon.test.ts b/packages/daemon/src/daemon.test.ts index b9c472f02..feadd0235 100644 --- a/packages/daemon/src/daemon.test.ts +++ b/packages/daemon/src/daemon.test.ts @@ -661,8 +661,7 @@ describe("CLI integration", () => { [resolveCliPath()!, "--help"], { encoding: "utf-8", timeout: 5000 }, ); - assert.ok(result.includes("Usage: sf-server")); - assert.ok(result.includes("Alias: sf-daemon")); + assert.ok(result.includes("Usage: sf-daemon")); assert.ok(result.includes("--config")); assert.ok(result.includes("--verbose")); assert.ok(result.includes("--start")); diff --git a/packages/daemon/src/daemon.ts b/packages/daemon/src/daemon.ts index 4d34bea14..3b1f3f5e5 100644 --- a/packages/daemon/src/daemon.ts +++ b/packages/daemon/src/daemon.ts @@ -253,7 +253,7 @@ export class Daemon { /** * Start missing daemon-managed autonomous clients for discovered repos. * - * Purpose: let sf-server reconcile both the SF server swarm registry and the + * Purpose: let sf-daemon reconcile both the SF server swarm registry and the * corresponding per-repo autonomous clients from one configured scan root. * * Consumer: syncSwarms() when `swarms.auto_start` is enabled. diff --git a/packages/daemon/src/swarm-registry.ts b/packages/daemon/src/swarm-registry.ts index 7ffe0a5f9..7dad92221 100644 --- a/packages/daemon/src/swarm-registry.ts +++ b/packages/daemon/src/swarm-registry.ts @@ -30,7 +30,7 @@ export interface SwarmRegistryEntry { } /** - * Stores the full swarm registry document written by sf-server. + * Stores the full swarm registry document written by sf-daemon. * * Purpose: version the SF server's cross-repo discovery file so future schema * changes can degrade cleanly instead of breaking the dashboard. @@ -49,7 +49,7 @@ export interface SwarmRegistry { * Purpose: let the daemon log and CLI report what the server scan changed * without exposing the full registry document. * - * Consumer: Daemon.syncSwarms() and `sf-server --sync-swarms`. + * Consumer: Daemon.syncSwarms() and `sf-daemon --sync-swarms`. */ export interface SyncSwarmRegistryResult { registryPath: string; diff --git a/packages/daemon/src/systemd.test.ts b/packages/daemon/src/systemd.test.ts index e4c7d6165..49ef89ac8 100644 --- a/packages/daemon/src/systemd.test.ts +++ b/packages/daemon/src/systemd.test.ts @@ -1,10 +1,5 @@ import assert from "node:assert/strict"; -import { - existsSync, - mkdtempSync, - rmSync, - writeFileSync, -} from "node:fs"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, it } from "vitest"; @@ -100,13 +95,13 @@ describe("generateUnit", () => { // ---------- getServicePath ---------- describe("getServicePath", () => { - it("returns ~/.config/systemd/user/sf-server.service", () => { + it("returns ~/.config/systemd/user/sf-daemon.service", () => { const expected = resolve( homedir(), ".config", "systemd", "user", - "sf-server.service", + "sf-daemon.service", ); assert.equal(getServicePath(), expected); }); @@ -114,7 +109,7 @@ describe("getServicePath", () => { it("honors SF_SYSTEMD_USER_DIR for tests and sandboxed installs", () => { const dir = fakeSystemdDir(); - assert.equal(getServicePath(), join(dir, "sf-server.service")); + assert.equal(getServicePath(), join(dir, "sf-daemon.service")); }); }); @@ -147,7 +142,7 @@ describe("install", () => { it("disables existing unit before reinstalling", () => { const tmpDir = fakeSystemdDir(); - writeFileSync(join(tmpDir, "sf-server.service"), "[Service]\n", "utf-8"); + writeFileSync(join(tmpDir, "sf-daemon.service"), "[Service]\n", "utf-8"); const calls: string[] = []; const mockRun: RunCommandFn = (cmd: string) => { @@ -157,9 +152,9 @@ describe("install", () => { install(baseUnitOpts(), mockRun); - assert.ok(calls.some((c) => c.includes("disable --now sf-server.service"))); + assert.ok(calls.some((c) => c.includes("disable --now sf-daemon.service"))); assert.ok(calls.some((c) => c.includes("daemon-reload"))); - assert.ok(calls.some((c) => c.includes("enable --now sf-server.service"))); + assert.ok(calls.some((c) => c.includes("enable --now sf-daemon.service"))); }); }); @@ -180,10 +175,10 @@ describe("uninstall", () => { it("handles already-stopped service gracefully", () => { const dir = fakeSystemdDir(); - writeFileSync(join(dir, "sf-server.service"), "[Service]\n", "utf-8"); + writeFileSync(join(dir, "sf-daemon.service"), "[Service]\n", "utf-8"); const mockRun: RunCommandFn = (cmd: string) => { if (cmd.includes("disable --now")) { - throw new Error("Failed to stop sf-server.service: Unit not loaded."); + throw new Error("Failed to stop sf-daemon.service: Unit not loaded."); } return ""; }; @@ -194,7 +189,7 @@ describe("uninstall", () => { it("calls daemon-reload after removing unit file (when file exists)", () => { const dir = fakeSystemdDir(); - writeFileSync(join(dir, "sf-server.service"), "[Service]\n", "utf-8"); + writeFileSync(join(dir, "sf-daemon.service"), "[Service]\n", "utf-8"); const calls: string[] = []; const mockRun: RunCommandFn = (cmd: string) => { calls.push(cmd); @@ -202,7 +197,7 @@ describe("uninstall", () => { }; uninstall(mockRun); - assert.ok(calls.some((c) => c.includes("disable --now sf-server.service"))); + assert.ok(calls.some((c) => c.includes("disable --now sf-daemon.service"))); assert.ok(calls.some((c) => c.includes("daemon-reload"))); }); }); @@ -286,7 +281,7 @@ describe("status", () => { status(mockRun); assert.equal(cmds.length, 1); - assert.ok(cmds[0].includes("systemctl --user show sf-server.service")); + assert.ok(cmds[0].includes("systemctl --user show sf-daemon.service")); assert.ok(cmds[0].includes("MainPID")); assert.ok(cmds[0].includes("ExecMainStatus")); assert.ok(cmds[0].includes("LoadState")); diff --git a/packages/daemon/src/systemd.ts b/packages/daemon/src/systemd.ts index b948b1c7b..d476c2819 100644 --- a/packages/daemon/src/systemd.ts +++ b/packages/daemon/src/systemd.ts @@ -27,7 +27,7 @@ export type RunCommandFn = (cmd: string) => string; // --------------- constants --------------- -const SERVICE_NAME = "sf-server.service"; +const SERVICE_NAME = "sf-daemon.service"; // --------------- helpers --------------- @@ -41,14 +41,14 @@ export function getServicePath(): string { // --------------- unit file generation --------------- -/** Generate a systemd user unit file for the SF server. */ +/** Generate a systemd user unit file for the SF daemon supervisor. */ export function generateUnit(opts: SystemdUnitOptions): string { const home = homedir(); const stdoutPath = resolve(home, ".sf", "daemon-stdout.log"); const stderrPath = resolve(home, ".sf", "daemon-stderr.log"); return `[Unit] -Description=SF Server (singularity-forge operator entrypoint) +Description=SF Daemon (singularity-forge background supervisor) After=network.target [Service] @@ -127,7 +127,7 @@ export function uninstall(runCommand: RunCommandFn = defaultRunCommand): void { * Returns structured information about registration, PID, and last exit code. * * Parses output from: - * systemctl --user show sf-server.service --property MainPID,ExecMainStatus,LoadState + * systemctl --user show sf-daemon.service --property MainPID,ExecMainStatus,LoadState */ export function status( runCommand: RunCommandFn = defaultRunCommand, diff --git a/scripts/validate-pack.js b/scripts/validate-pack.js index 845b117a5..3b4324ee3 100644 --- a/scripts/validate-pack.js +++ b/scripts/validate-pack.js @@ -245,7 +245,7 @@ try { process.exit(1); } - console.log("==> Running installed daemon binary (sf-server --help)..."); + console.log("==> Running installed daemon binary (sf-daemon --help)..."); try { const helpOutput = execFileSync( process.execPath, @@ -258,12 +258,12 @@ try { maxBuffer: DEFAULT_MAX_BUFFER, }, ); - if (!helpOutput.includes("Usage: sf-server")) { - console.log("ERROR: sf-server --help returned unexpected output."); + if (!helpOutput.includes("Usage: sf-daemon")) { + console.log("ERROR: sf-daemon --help returned unexpected output."); process.exit(1); } } catch (err) { - console.log("ERROR: Running sf-server --help failed after install."); + console.log("ERROR: Running sf-daemon --help failed after install."); if (err.stdout) console.log(err.stdout); if (err.stderr) console.log(err.stderr); process.exit(1); diff --git a/src/help-text.ts b/src/help-text.ts index a9746ca36..c7c304dda 100644 --- a/src/help-text.ts +++ b/src/help-text.ts @@ -45,7 +45,7 @@ const SUBCOMMAND_HELP: Record = { "Start the full SF operator server for a project.", "", "The server owns the browser surface and daemon-backed project operations.", - "sf-server keeps the swarm registry synced from configured scan roots by", + "sf-daemon keeps the swarm registry synced from configured scan roots by", "default. Set swarms.auto_start=true to also start missing per-repo", "autonomous clients.", "", diff --git a/src/tests/integration/web-mode-cli.test.ts b/src/tests/integration/web-mode-cli.test.ts index d0d5300cb..f4a7ff779 100644 --- a/src/tests/integration/web-mode-cli.test.ts +++ b/src/tests/integration/web-mode-cli.test.ts @@ -35,9 +35,17 @@ test("package hooks declare a concrete staged web host", () => { "npm --prefix web run build && npm run stage:web-host", ); assert.equal(rootPackage.scripts["sf"], "node scripts/dev-cli.js"); - assert.equal(rootPackage.scripts["sf:server"], "node scripts/dev-server.js"); + assert.equal( + rootPackage.scripts["sf:server"], + "node scripts/dev-cli.js server", + ); assert.equal( rootPackage.scripts["sf:server:dist"], + "node dist/loader.js server", + ); + assert.equal(rootPackage.scripts["sf:daemon"], "node scripts/dev-server.js"); + assert.equal( + rootPackage.scripts["sf:daemon:dist"], "node packages/daemon/dist/cli.js", ); assert.equal(rootPackage.scripts["sf:web"], undefined); diff --git a/src/tests/sf-web-launcher-contract.test.ts b/src/tests/sf-web-launcher-contract.test.ts index 72d06456b..4074254e2 100644 --- a/src/tests/sf-web-launcher-contract.test.ts +++ b/src/tests/sf-web-launcher-contract.test.ts @@ -8,13 +8,18 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { scripts?: Record; }; -test("sf:server starts the daemon-backed development server", () => { +test("sf:server starts the source web server surface", () => { const script = packageJson.scripts?.["sf:server"]; assert.ok(script, "package.json must define a sf:server script"); assert.match( script, + /node scripts\/dev-cli\.js server/, + "sf:server must run the real sf server entrypoint", + ); + assert.match( + packageJson.scripts?.["sf:daemon"] ?? "", /node scripts\/dev-server\.js/, - "sf:server must run the daemon-backed server entrypoint", + "sf:daemon must own the background daemon dev entrypoint", ); assert.equal(packageJson.scripts?.["sf:web"], undefined); });