refactor(sf): separate daemon from server identity
This commit is contained in:
parent
187d736930
commit
3adcb833ed
14 changed files with 57 additions and 54 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
11
package.json
11
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",
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@
|
|||
}
|
||||
},
|
||||
"bin": {
|
||||
"sf-daemon": "./dist/cli.js",
|
||||
"sf-server": "./dist/cli.js"
|
||||
"sf-daemon": "./dist/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsgo",
|
||||
|
|
|
|||
|
|
@ -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> 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 <path> Start an autonomous SF session for this project path
|
||||
--command <text> Command to send for --start (default: /sf autonomous)
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const SUBCOMMAND_HELP: Record<string, string> = {
|
|||
"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.",
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -8,13 +8,18 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
|||
scripts?: Record<string, string>;
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue