@scopebond/agent 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"selfcheck.d.ts","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,OAAO,EAAE,KAAK,cAAc,EAClC,MAAM,iBAAiB,CAAC;AAGzB,MAAM,WAAW,aAAa;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AAE3E,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3F;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,GAAG,SAAa,GAAG,aAAa,EAAE,CAgB5H;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EACnF,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3E,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CAAC,CAuBzG"}
1
+ {"version":3,"file":"selfcheck.d.ts","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,OAAO,EAAE,KAAK,cAAc,EAClC,MAAM,iBAAiB,CAAC;AAGzB,MAAM,WAAW,aAAa;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE;AAE3E,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3F;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,GAAG,SAAa,GAAG,aAAa,EAAE,CAmB5H;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EACnF,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3E,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CAAC,CAuBzG"}
package/dist/selfcheck.js CHANGED
@@ -3,10 +3,11 @@
3
3
  // then signs the day with the key it enrolled with and sends everything to the workspace, which
4
4
  // verifies the signature. A pass means the whole path works, end to end; a failure names what broke.
5
5
  import { existsSync } from "node:fs";
6
+ import { homedir } from "node:os";
6
7
  import { join } from "node:path";
7
8
  import { canonical } from "@scopebond/gateway";
8
9
  import { loadOrCreateAttester } from "@scopebond/gateway/node";
9
- import { configuredHookCommands, hookCommandResolves, hookVersion, isScopebondHookCommand, queueStatus, readDeliveryState, userHarnessFile, } from "@scopebond/hook";
10
+ import { configuredHookCommands, duplicateHooks, hookCommandResolves, hookVersion, isScopebondHookCommand, queueStatus, readDeliveryState, userHarnessFile, } from "@scopebond/hook";
10
11
  import { autostartHealth } from "./autostart.js";
11
12
  export function selfCheckProof(credentialId, gatewayId, day) {
12
13
  return canonical({ type: "scopebond:self-check", version: 1, credential_id: credentialId, gateway_id: gatewayId, day });
@@ -17,6 +18,9 @@ export function localChecks(dir, connection, harnesses, now = Date.now()) {
17
18
  checks.push({ id: "hook_entry", ok: entries.length > 0, ...(entries.length ? {} : { detail: "no agent setting holds the Scopebond hook" }) });
18
19
  const broken = entries.filter((c) => !hookCommandResolves(c));
19
20
  checks.push({ id: "hook_starts", ok: entries.length > 0 && broken.length === 0, ...(broken.length ? { detail: `${broken.length} hook command(s) cannot start` } : {}) });
21
+ // SB302: the hook twice in the user settings, or an enabled plugin beside them, signs and sends every action twice.
22
+ const dupes = harnesses.flatMap((h) => duplicateHooks(h, homedir()) ? [h] : []);
23
+ checks.push({ id: "hook_duplicates", ok: dupes.length === 0, ...(dupes.length ? { detail: `the Scopebond hook runs more than once per action for ${dupes.join(", ")} (fix: ${process.platform === "win32" ? "npx.cmd" : "npx"} -y @scopebond/hook@${hookVersion()} dedupe)` } : {}) });
20
24
  const autostart = autostartHealth(dir);
21
25
  checks.push({ id: "autostart", ok: autostart.ok, ...(autostart.ok ? {} : { detail: autostart.detail }) });
22
26
  const { pending, oldest } = queueStatus(dir);
@@ -1 +1 @@
1
- {"version":3,"file":"selfcheck.js","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,mGAAmG;AACnG,gGAAgG;AAChG,qGAAqG;AAErG,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EACL,sBAAsB,EAAE,mBAAmB,EAAE,WAAW,EAAE,sBAAsB,EAAE,WAAW,EAAE,iBAAiB,EAAE,eAAe,GAElI,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAIjD,MAAM,UAAU,cAAc,CAAC,YAAoB,EAAE,SAAiB,EAAE,GAAW;IACjF,OAAO,SAAS,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,CAAC,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;AAC1H,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,UAA0B,EAAE,SAAoB,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACzG,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9H,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,2CAA2C,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9I,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,+BAA+B,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACzK,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1G,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,IAAI,MAAM,KAAK,IAAI,IAAI,GAAG,GAAG,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC9E,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,4BAA4B,IAAI,IAAI,CAAC,MAAO,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACjN,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,GAAG,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACpF,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,0BAA0B,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACjK,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAW,EAAE,UAA0B,EAAE,SAAoB,EAAE,YAAoB,EACnF,UAA0E,EAAE;IAE5E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3G,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrD,MAAM,EAAE,QAAQ,EAAE,GAAG,oBAAoB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,aAAa,EAAE,UAAU,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5G,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;YACnG,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;YAC3F,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,UAAU,CAAC,UAAU,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YACjG,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;SAC3G,CAAC,CAAC;QACH,oHAAoH;QACpH,gDAAgD;QAChD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAAC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,CAAC;QACvK,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,kBAAkB,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QAC/G,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAuE,CAAC;QACnG,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC;IACzI,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,0BAA2B,KAAe,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACvI,CAAC;AACH,CAAC","sourcesContent":["// The daily end-to-end self-check (SB347). The agent checks this computer's side (the hook entry\n// is present and starts, autostart works, nothing is stuck, the connection is not about to lapse),\n// then signs the day with the key it enrolled with and sends everything to the workspace, which\n// verifies the signature. A pass means the whole path works, end to end; a failure names what broke.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { canonical } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport {\n configuredHookCommands, hookCommandResolves, hookVersion, isScopebondHookCommand, queueStatus, readDeliveryState, userHarnessFile,\n type Harness, type HookConnection,\n} from \"@scopebond/hook\";\nimport { autostartHealth } from \"./autostart.js\";\n\nexport interface SelfCheckItem { id: string; ok: boolean; detail?: string }\n\nexport function selfCheckProof(credentialId: string, gatewayId: string, day: string): string {\n return canonical({ type: \"scopebond:self-check\", version: 1, credential_id: credentialId, gateway_id: gatewayId, day });\n}\n\nexport function localChecks(dir: string, connection: HookConnection, harnesses: Harness[], now = Date.now()): SelfCheckItem[] {\n const checks: SelfCheckItem[] = [];\n const entries = harnesses.flatMap((h) => configuredHookCommands(userHarnessFile(h)).filter((c) => isScopebondHookCommand(c)));\n checks.push({ id: \"hook_entry\", ok: entries.length > 0, ...(entries.length ? {} : { detail: \"no agent setting holds the Scopebond hook\" }) });\n const broken = entries.filter((c) => !hookCommandResolves(c));\n checks.push({ id: \"hook_starts\", ok: entries.length > 0 && broken.length === 0, ...(broken.length ? { detail: `${broken.length} hook command(s) cannot start` } : {}) });\n const autostart = autostartHealth(dir);\n checks.push({ id: \"autostart\", ok: autostart.ok, ...(autostart.ok ? {} : { detail: autostart.detail }) });\n const { pending, oldest } = queueStatus(dir);\n const state = readDeliveryState(dir);\n const stuck = pending > 0 && oldest !== null && now - oldest > 60 * 60 * 1000;\n checks.push({ id: \"queue\", ok: !stuck, ...(stuck ? { detail: `${pending} record(s) waiting since ${new Date(oldest!).toISOString()}${state.last_error ? ` (${state.last_error.slice(0, 120)})` : \"\"}` } : {}) });\n const expires = Date.parse(connection.expires_at ?? \"\");\n const lapsing = Number.isFinite(expires) && expires - now < 7 * 24 * 60 * 60 * 1000;\n checks.push({ id: \"credential\", ok: !lapsing, ...(lapsing ? { detail: `the connection expires ${new Date(expires).toISOString()} and was not renewed` } : {}) });\n return checks;\n}\n\nexport async function runSelfCheck(\n dir: string, connection: HookConnection, harnesses: Harness[], agentVersion: string,\n options: { fetchImpl?: typeof fetch; now?: number; timeoutMs?: number } = {},\n): Promise<{ ok: boolean; signature_verified: boolean; failed: string[]; checks: SelfCheckItem[] } | null> {\n const now = options.now ?? Date.now();\n const checks = localChecks(dir, connection, harnesses, now);\n const keyFile = join(dir, \"attester.key\");\n if (!existsSync(keyFile)) return { ok: false, signature_verified: false, failed: [\"signing_key\"], checks };\n const day = new Date(now).toISOString().slice(0, 10);\n const { attester } = loadOrCreateAttester({ file: keyFile });\n const signature = await attester.sign(selfCheckProof(connection.credential_id, connection.gateway_id, day));\n try {\n const res = await (options.fetchImpl ?? fetch)(new URL(\"/v1/self-check\", connection.url).toString(), {\n method: \"POST\", redirect: \"error\", signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n headers: { authorization: `Bearer ${connection.credential}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ signature, day, checks, hook_version: hookVersion(), agent_version: agentVersion }),\n });\n // A workspace that does not offer the self-check yet answers 404: the computer's own checks still count; the signed\n // workspace part is simply not available there.\n if (res.status === 404) { const failed = checks.filter((c) => !c.ok).map((c) => c.id); return { ok: failed.length === 0, signature_verified: false, failed, checks }; }\n if (!res.ok) return { ok: false, signature_verified: false, failed: [`workspace_http_${res.status}`], checks };\n const body = await res.json() as { ok?: boolean; signature_verified?: boolean; failed?: string[] };\n return { ok: !!body.ok, signature_verified: !!body.signature_verified, failed: Array.isArray(body.failed) ? body.failed : [], checks };\n } catch (error) {\n return { ok: false, signature_verified: false, failed: [`workspace_unreachable: ${(error as Error).message.slice(0, 80)}`], checks };\n }\n}\n"]}
1
+ {"version":3,"file":"selfcheck.js","sourceRoot":"","sources":["../src/selfcheck.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,mGAAmG;AACnG,gGAAgG;AAChG,qGAAqG;AAErG,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EACL,sBAAsB,EAAE,cAAc,EAAE,mBAAmB,EAAE,WAAW,EAAE,sBAAsB,EAAE,WAAW,EAAE,iBAAiB,EAAE,eAAe,GAElJ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAIjD,MAAM,UAAU,cAAc,CAAC,YAAoB,EAAE,SAAiB,EAAE,GAAW;IACjF,OAAO,SAAS,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,CAAC,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;AAC1H,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,UAA0B,EAAE,SAAoB,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACzG,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9H,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,2CAA2C,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9I,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,+BAA+B,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACzK,oHAAoH;IACpH,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAChF,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,iBAAiB,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,yDAAyD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,uBAAuB,WAAW,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACvR,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1G,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,IAAI,MAAM,KAAK,IAAI,IAAI,GAAG,GAAG,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC9E,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,4BAA4B,IAAI,IAAI,CAAC,MAAO,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACjN,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,GAAG,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACpF,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,0BAA0B,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACjK,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAW,EAAE,UAA0B,EAAE,SAAoB,EAAE,YAAoB,EACnF,UAA0E,EAAE;IAE5E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3G,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrD,MAAM,EAAE,QAAQ,EAAE,GAAG,oBAAoB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,aAAa,EAAE,UAAU,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5G,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;YACnG,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;YAC3F,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,UAAU,CAAC,UAAU,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YACjG,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;SAC3G,CAAC,CAAC;QACH,oHAAoH;QACpH,gDAAgD;QAChD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAAC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,CAAC;QACvK,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,kBAAkB,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QAC/G,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAuE,CAAC;QACnG,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC;IACzI,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,0BAA2B,KAAe,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACvI,CAAC;AACH,CAAC","sourcesContent":["// The daily end-to-end self-check (SB347). The agent checks this computer's side (the hook entry\n// is present and starts, autostart works, nothing is stuck, the connection is not about to lapse),\n// then signs the day with the key it enrolled with and sends everything to the workspace, which\n// verifies the signature. A pass means the whole path works, end to end; a failure names what broke.\n\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { canonical } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport {\n configuredHookCommands, duplicateHooks, hookCommandResolves, hookVersion, isScopebondHookCommand, queueStatus, readDeliveryState, userHarnessFile,\n type Harness, type HookConnection,\n} from \"@scopebond/hook\";\nimport { autostartHealth } from \"./autostart.js\";\n\nexport interface SelfCheckItem { id: string; ok: boolean; detail?: string }\n\nexport function selfCheckProof(credentialId: string, gatewayId: string, day: string): string {\n return canonical({ type: \"scopebond:self-check\", version: 1, credential_id: credentialId, gateway_id: gatewayId, day });\n}\n\nexport function localChecks(dir: string, connection: HookConnection, harnesses: Harness[], now = Date.now()): SelfCheckItem[] {\n const checks: SelfCheckItem[] = [];\n const entries = harnesses.flatMap((h) => configuredHookCommands(userHarnessFile(h)).filter((c) => isScopebondHookCommand(c)));\n checks.push({ id: \"hook_entry\", ok: entries.length > 0, ...(entries.length ? {} : { detail: \"no agent setting holds the Scopebond hook\" }) });\n const broken = entries.filter((c) => !hookCommandResolves(c));\n checks.push({ id: \"hook_starts\", ok: entries.length > 0 && broken.length === 0, ...(broken.length ? { detail: `${broken.length} hook command(s) cannot start` } : {}) });\n // SB302: the hook twice in the user settings, or an enabled plugin beside them, signs and sends every action twice.\n const dupes = harnesses.flatMap((h) => duplicateHooks(h, homedir()) ? [h] : []);\n checks.push({ id: \"hook_duplicates\", ok: dupes.length === 0, ...(dupes.length ? { detail: `the Scopebond hook runs more than once per action for ${dupes.join(\", \")} (fix: ${process.platform === \"win32\" ? \"npx.cmd\" : \"npx\"} -y @scopebond/hook@${hookVersion()} dedupe)` } : {}) });\n const autostart = autostartHealth(dir);\n checks.push({ id: \"autostart\", ok: autostart.ok, ...(autostart.ok ? {} : { detail: autostart.detail }) });\n const { pending, oldest } = queueStatus(dir);\n const state = readDeliveryState(dir);\n const stuck = pending > 0 && oldest !== null && now - oldest > 60 * 60 * 1000;\n checks.push({ id: \"queue\", ok: !stuck, ...(stuck ? { detail: `${pending} record(s) waiting since ${new Date(oldest!).toISOString()}${state.last_error ? ` (${state.last_error.slice(0, 120)})` : \"\"}` } : {}) });\n const expires = Date.parse(connection.expires_at ?? \"\");\n const lapsing = Number.isFinite(expires) && expires - now < 7 * 24 * 60 * 60 * 1000;\n checks.push({ id: \"credential\", ok: !lapsing, ...(lapsing ? { detail: `the connection expires ${new Date(expires).toISOString()} and was not renewed` } : {}) });\n return checks;\n}\n\nexport async function runSelfCheck(\n dir: string, connection: HookConnection, harnesses: Harness[], agentVersion: string,\n options: { fetchImpl?: typeof fetch; now?: number; timeoutMs?: number } = {},\n): Promise<{ ok: boolean; signature_verified: boolean; failed: string[]; checks: SelfCheckItem[] } | null> {\n const now = options.now ?? Date.now();\n const checks = localChecks(dir, connection, harnesses, now);\n const keyFile = join(dir, \"attester.key\");\n if (!existsSync(keyFile)) return { ok: false, signature_verified: false, failed: [\"signing_key\"], checks };\n const day = new Date(now).toISOString().slice(0, 10);\n const { attester } = loadOrCreateAttester({ file: keyFile });\n const signature = await attester.sign(selfCheckProof(connection.credential_id, connection.gateway_id, day));\n try {\n const res = await (options.fetchImpl ?? fetch)(new URL(\"/v1/self-check\", connection.url).toString(), {\n method: \"POST\", redirect: \"error\", signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n headers: { authorization: `Bearer ${connection.credential}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ signature, day, checks, hook_version: hookVersion(), agent_version: agentVersion }),\n });\n // A workspace that does not offer the self-check yet answers 404: the computer's own checks still count; the signed\n // workspace part is simply not available there.\n if (res.status === 404) { const failed = checks.filter((c) => !c.ok).map((c) => c.id); return { ok: failed.length === 0, signature_verified: false, failed, checks }; }\n if (!res.ok) return { ok: false, signature_verified: false, failed: [`workspace_http_${res.status}`], checks };\n const body = await res.json() as { ok?: boolean; signature_verified?: boolean; failed?: string[] };\n return { ok: !!body.ok, signature_verified: !!body.signature_verified, failed: Array.isArray(body.failed) ? body.failed : [], checks };\n } catch (error) {\n return { ok: false, signature_verified: false, failed: [`workspace_unreachable: ${(error as Error).message.slice(0, 80)}`], checks };\n }\n}\n"]}
package/dist/service.d.ts CHANGED
@@ -17,6 +17,8 @@ export interface ServiceOptions {
17
17
  prompter?: Prompter;
18
18
  /** The Windows tray icon (default on Windows; SCOPEBOND_AGENT_TRAY=off turns it off). */
19
19
  tray?: boolean;
20
+ /** Called once the agent has stopped because `stop` (or `autostart off`) asked it to; the CLI exits. */
21
+ onStopped?: () => void;
20
22
  }
21
23
  export interface Service {
22
24
  stop(): Promise<void>;
@@ -45,5 +47,16 @@ export declare function repairHookEntries(harnesses?: Harness[]): Array<{
45
47
  /** Start a fresh agent (the updated one) that waits for this process to exit, through the
46
48
  * autostart launcher when there is one so it finds Node the same way sign-in does. */
47
49
  export declare function spawnReplacement(dir: string): void;
50
+ export declare const AGENT_LOCK = "agent.lock";
51
+ /** How long a lock whose agent never answered on its local channel still counts as an agent starting up. */
52
+ export declare const AGENT_LOCK_STARTING_MS = 60000;
53
+ /** Take the one-agent-per-home lock: created exclusively, or taken over when the agent named in it is
54
+ * gone. The caller has already found no agent answering on the local channel, so a lock older than
55
+ * AGENT_LOCK_STARTING_MS is left over even when its process id is alive: Windows reuses process ids
56
+ * soon after a restart, and a sign-in after a reboot must not find "already running". A newer lock
57
+ * is an agent still starting (or one whose id is not written yet): this start gives way.
58
+ * Returns the lock file, or null while another agent holds it. */
59
+ export declare function acquireAgentLock(dir: string, now?: number): string | null;
60
+ export declare function releaseAgentLock(file: string): void;
48
61
  export declare function startService(options: ServiceOptions): Promise<Service>;
49
62
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiF,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC9H,OAAO,EAAmE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAG/G,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAA6C,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAKvF,eAAO,MAAM,aAAa,QAA4B,CAAC;AAKvD,eAAO,MAAM,aAAa,8BAA8B,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzF,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,yFAAyF;IACzF,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACjC,WAAW,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;IACpD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;sFACsF;AACtF,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,OAAO,EAAwB,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAOvH;AAED;uFACuF;AACvF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CASlD;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAsH5E"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AASA,OAAO,EAAoE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AACjH,OAAO,EAAmE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAG/G,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAA6C,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAKvF,eAAO,MAAM,aAAa,QAA4B,CAAC;AAKvD,eAAO,MAAM,aAAa,8BAA8B,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzF,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,yFAAyF;IACzF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,wGAAwG;IACxG,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACjC,WAAW,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;IACpD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;sFACsF;AACtF,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,OAAO,EAAwB,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAOvH;AAED;uFACuF;AACvF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CASlD;AAED,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,4GAA4G;AAC5G,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C;;;;;mEAKmE;AACnE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,MAAM,GAAG,IAAI,CAmB7E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAiI5E"}
package/dist/service.js CHANGED
@@ -3,9 +3,10 @@
3
3
  // second one finds the first answering and exits. Every six hours it asks the workspace which
4
4
  // versions to run, and once a day it runs the end-to-end self-check.
5
5
  import { spawn } from "node:child_process";
6
- import { existsSync, realpathSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
- import { hookCommand, hookVersion, loadConnection, userHarnessFile, writeHarnessConfig } from "@scopebond/hook";
9
+ import { hookVersion, loadConnection, userHarnessFile, writeHarnessConfig } from "@scopebond/hook";
9
10
  import { computerStatus, expectedHarnesses, missingHookEntries, runCycle } from "./agent.js";
10
11
  import { launcherPath } from "./autostart.js";
11
12
  import { callAgent, startControl } from "./ipc.js";
@@ -14,7 +15,7 @@ import { flushReasons, queueReason } from "./override-reasons.js";
14
15
  import { parseQuestion, serialized, systemPrompter } from "./prompt.js";
15
16
  import { healthOf } from "./health.js";
16
17
  import { notifyChange, startTray } from "./tray.js";
17
- import { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries } from "./update.js";
18
+ import { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries, maintainedHookCommand } from "./update.js";
18
19
  export const AGENT_VERSION = `agent/${agentVersion()}`;
19
20
  const INTERVAL_MS = 60_000;
20
21
  const MAX_BACKOFF_MS = 15 * 60_000;
@@ -26,7 +27,7 @@ export const AFTER_PID_ENV = "SCOPEBOND_AGENT_AFTER_PID";
26
27
  export function repairHookEntries(harnesses = expectedHarnesses()) {
27
28
  const repaired = [];
28
29
  for (const harness of missingHookEntries(harnesses)) {
29
- const file = writeHarnessConfig(userHarnessFile(harness), harness, hookCommand(harness));
30
+ const file = writeHarnessConfig(userHarnessFile(harness), harness, maintainedHookCommand(harness));
30
31
  repaired.push({ harness, file });
31
32
  }
32
33
  return repaired;
@@ -43,10 +44,64 @@ export function spawnReplacement(dir) {
43
44
  const child = spawn(command, [...args], { env, detached: true, stdio: "ignore", windowsHide: true });
44
45
  child.unref();
45
46
  }
47
+ export const AGENT_LOCK = "agent.lock";
48
+ /** How long a lock whose agent never answered on its local channel still counts as an agent starting up. */
49
+ export const AGENT_LOCK_STARTING_MS = 60_000;
50
+ /** Take the one-agent-per-home lock: created exclusively, or taken over when the agent named in it is
51
+ * gone. The caller has already found no agent answering on the local channel, so a lock older than
52
+ * AGENT_LOCK_STARTING_MS is left over even when its process id is alive: Windows reuses process ids
53
+ * soon after a restart, and a sign-in after a reboot must not find "already running". A newer lock
54
+ * is an agent still starting (or one whose id is not written yet): this start gives way.
55
+ * Returns the lock file, or null while another agent holds it. */
56
+ export function acquireAgentLock(dir, now = Date.now()) {
57
+ const file = join(dir, AGENT_LOCK);
58
+ mkdirSync(dir, { recursive: true });
59
+ const mine = `${process.pid} ${now}`;
60
+ try {
61
+ writeFileSync(file, mine, { flag: "wx" });
62
+ return file;
63
+ }
64
+ catch { /* held, or left behind */ }
65
+ let text = "";
66
+ let written = now;
67
+ try {
68
+ text = readFileSync(file, "utf8").trim();
69
+ written = statSync(file).mtimeMs;
70
+ }
71
+ catch { /* removed meanwhile */ }
72
+ const [pidText, atText] = text.split(/\s+/);
73
+ const holder = Number(pidText);
74
+ const at = Number(atText);
75
+ const since = Number.isFinite(at) && at > 0 ? at : written;
76
+ const fresh = now - since < AGENT_LOCK_STARTING_MS;
77
+ if (holder !== process.pid && fresh) {
78
+ if (!(Number.isInteger(holder) && holder > 0))
79
+ return null; // being written by an agent starting this moment
80
+ try {
81
+ process.kill(holder, 0);
82
+ return null;
83
+ }
84
+ catch { /* that agent is gone: take the lock over */ }
85
+ }
86
+ writeFileSync(file, mine);
87
+ return file;
88
+ }
89
+ export function releaseAgentLock(file) {
90
+ try {
91
+ if (readFileSync(file, "utf8").trim().split(/\s+/)[0] === String(process.pid))
92
+ rmSync(file, { force: true });
93
+ }
94
+ catch { /* already gone */ }
95
+ }
46
96
  export async function startService(options) {
47
97
  const log = options.log ?? ((line) => console.log(`${new Date().toISOString()} ${line}`));
48
98
  if (await callAgent(options.dir, "GET", "/status", undefined, 2_000))
49
99
  throw new Error("a Scopebond Agent is already running for this computer");
100
+ // Two agents started within moments of each other (a slow first start, then a second start) both see
101
+ // nobody answering yet: the lock file decides, so the second exits instead of orphaning the first.
102
+ const lock = acquireAgentLock(options.dir);
103
+ if (!lock)
104
+ throw new Error("a Scopebond Agent is already running for this computer");
50
105
  const interval = options.intervalMs ?? INTERVAL_MS;
51
106
  let last = null;
52
107
  let lastMaintenance = null;
@@ -150,6 +205,12 @@ export async function startService(options) {
150
205
  return { repaired };
151
206
  },
152
207
  "POST /maintain": async () => ({ maintenance: await maintain(true) }),
208
+ // `scopebond-agent stop` and `autostart off`: answer first, then stop, so the caller hears back.
209
+ "POST /stop": () => {
210
+ log("stopping: asked to by this computer's user");
211
+ setTimeout(() => { void stop().finally(() => options.onStopped?.()); }, 50);
212
+ return { stopping: true };
213
+ },
153
214
  // Warn mode: the hook asks; only the window answers. The caller learns the answer, never decides it.
154
215
  "POST /override": async (body) => {
155
216
  const question = parseQuestion(body);
@@ -178,6 +239,7 @@ export async function startService(options) {
178
239
  clearInterval(maintenanceTimer);
179
240
  await running?.catch(() => undefined);
180
241
  await control.close();
242
+ releaseAgentLock(lock);
181
243
  };
182
244
  log(`Scopebond Agent ${agentVersion()} running for ${options.dir} (control on 127.0.0.1:${control.endpoint.port})`);
183
245
  await cycle();
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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","sourcesContent":["// The resident loop: one cycle now, then one every interval, sooner after a change is asked for,\n// backing off while the workspace is unreachable. Exactly one agent serves a Scopebond home: a\n// second one finds the first answering and exits. Every six hours it asks the workspace which\n// versions to run, and once a day it runs the end-to-end self-check.\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { hookCommand, hookVersion, loadConnection, userHarnessFile, writeHarnessConfig, type Harness } from \"@scopebond/hook\";\nimport { computerStatus, expectedHarnesses, missingHookEntries, runCycle, type CycleResult } from \"./agent.js\";\nimport { launcherPath } from \"./autostart.js\";\nimport { callAgent, startControl } from \"./ipc.js\";\nimport { runSelfCheck } from \"./selfcheck.js\";\nimport { flushReasons, queueReason } from \"./override-reasons.js\";\nimport { parseQuestion, serialized, systemPrompter, type Prompter } from \"./prompt.js\";\nimport { healthOf, type HealthLevel } from \"./health.js\";\nimport { notifyChange, startTray } from \"./tray.js\";\nimport { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries } from \"./update.js\";\n\nexport const AGENT_VERSION = `agent/${agentVersion()}`;\nconst INTERVAL_MS = 60_000;\nconst MAX_BACKOFF_MS = 15 * 60_000;\nconst UPDATE_EVERY_MS = 6 * 60 * 60 * 1000;\nconst SELF_CHECK_EVERY_MS = 24 * 60 * 60 * 1000;\nexport const AFTER_PID_ENV = \"SCOPEBOND_AGENT_AFTER_PID\";\n\nexport interface ServiceOptions {\n dir: string; intervalMs?: number; fetchImpl?: typeof fetch; log?: (line: string) => void;\n /** Off in tests: the update check and self-check run only when asked. */\n maintenance?: boolean;\n /** Called after a successful update instead of restarting (tests). */\n onUpdated?: (version: string) => void;\n /** Shows the Scopebond window for an override (tests replace it). */\n prompter?: Prompter;\n /** The Windows tray icon (default on Windows; SCOPEBOND_AGENT_TRAY=off turns it off). */\n tray?: boolean;\n}\n\nexport interface Service {\n stop(): Promise<void>;\n cycleNow(): Promise<CycleResult>;\n maintainNow(): Promise<MaintenanceResult>;\n port: number;\n}\n\nexport interface MaintenanceResult {\n at: number;\n policy: \"recommended\" | \"hold\" | \"unknown\";\n updatedTo: string | null;\n hookEntries: Array<{ harness: Harness; file: string; reason: string }>;\n selfCheck: Awaited<ReturnType<typeof runSelfCheck>>;\n error: string | null;\n}\n\n/** Put the Scopebond hook back into agent settings that lost it. A visible change: each one is\n * logged and returned, and only settings of agents this computer has are touched. */\nexport function repairHookEntries(harnesses: Harness[] = expectedHarnesses()): Array<{ harness: Harness; file: string }> {\n const repaired: Array<{ harness: Harness; file: string }> = [];\n for (const harness of missingHookEntries(harnesses)) {\n const file = writeHarnessConfig(userHarnessFile(harness), harness, hookCommand(harness));\n repaired.push({ harness, file });\n }\n return repaired;\n}\n\n/** Start a fresh agent (the updated one) that waits for this process to exit, through the\n * autostart launcher when there is one so it finds Node the same way sign-in does. */\nexport function spawnReplacement(dir: string): void {\n const launcher = launcherPath(dir);\n const cli = realpathSync(fileURLToPath(new URL(\"./cli.js\", import.meta.url)));\n const env = { ...process.env, [AFTER_PID_ENV]: String(process.pid) };\n const [command, args] = existsSync(launcher)\n ? process.platform === \"win32\" ? [\"cmd.exe\", [\"/d\", \"/c\", launcher]] as const : [\"/bin/sh\", [launcher]] as const\n : [process.execPath, [cli, \"run\"]] as const;\n const child = spawn(command, [...args], { env, detached: true, stdio: \"ignore\", windowsHide: true });\n child.unref();\n}\n\nexport async function startService(options: ServiceOptions): Promise<Service> {\n const log = options.log ?? ((line: string) => console.log(`${new Date().toISOString()} ${line}`));\n if (await callAgent(options.dir, \"GET\", \"/status\", undefined, 2_000)) throw new Error(\"a Scopebond Agent is already running for this computer\");\n const interval = options.intervalMs ?? INTERVAL_MS;\n let last: CycleResult | null = null;\n let lastMaintenance: MaintenanceResult | null = null;\n let failures = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let maintenanceTimer: ReturnType<typeof setInterval> | null = null;\n let running: Promise<CycleResult> | null = null;\n let stopped = false;\n let lastSelfCheckAt = 0;\n\n const prompt = serialized(options.prompter ?? systemPrompter);\n let shownLevel: HealthLevel | null = null;\n const health = () => healthOf(computerStatus(options.dir), lastMaintenance?.selfCheck ?? null);\n const noticeHealth = () => { const h = health(); notifyChange(shownLevel, h); shownLevel = h.level; };\n const sendReasons = async () => {\n const connection = loadConnection(options.dir);\n if (!connection) return;\n try { const sent = await flushReasons(options.dir, connection, options.fetchImpl); if (sent) log(`sent ${sent} override reason(s) to the workspace`); }\n catch { /* they wait for the next cycle */ }\n };\n const cycle = async (): Promise<CycleResult> => {\n if (running) return running;\n running = runCycle({ dir: options.dir, fetchImpl: options.fetchImpl }).then(async (result) => {\n await sendReasons();\n try { noticeHealth(); } catch { /* status is best effort */ }\n last = result;\n failures = result.deliveryError ? failures + 1 : 0;\n if (result.delivered) log(`delivered ${result.delivered} record(s); ${result.pending} waiting`);\n if (result.deliveryError) log(`delivery problem: ${result.deliveryError}`);\n return result;\n }).finally(() => { running = null; });\n return running;\n };\n\n const maintain = async (forceSelfCheck = false): Promise<MaintenanceResult> => {\n const result: MaintenanceResult = { at: Date.now(), policy: \"unknown\", updatedTo: null, hookEntries: [], selfCheck: null, error: null };\n try {\n const connection = loadConnection(options.dir);\n const harnesses = expectedHarnesses();\n const target = connection ? await fetchClientVersion(connection, options.fetchImpl) : null;\n if (target) result.policy = target.policy;\n const current = agentVersion();\n if (target?.policy === \"recommended\" && target.agent && compareVersions(target.agent, current) > 0) {\n log(`updating the Scopebond Agent ${current} -> ${target.agent}`);\n const installed = await installAgent(target.agent);\n if (installed.ok) {\n result.updatedTo = target.agent;\n lastMaintenance = result;\n if (options.onUpdated) options.onUpdated(target.agent);\n else { spawnReplacement(options.dir); setTimeout(() => { void stop().finally(() => process.exit(0)); }, 500); }\n return result;\n }\n result.error = `update to ${target.agent} failed: ${installed.output.slice(-300)}`;\n log(result.error);\n }\n // Hook entries: under \"recommended\" they move to the newest hook named or carried; under \"hold\"\n // only broken ones are repaired, at the version they already name or the one the agent carries.\n const hookTarget = target?.policy === \"recommended\" && target.hook && compareVersions(target.hook, hookVersion()) > 0 ? target.hook : hookVersion();\n result.hookEntries = maintainHookEntries(harnesses, hookTarget, target?.policy !== \"hold\");\n for (const change of result.hookEntries) log(`${change.reason}: ${change.file}`);\n if (connection && (forceSelfCheck || Date.now() - lastSelfCheckAt >= SELF_CHECK_EVERY_MS)) {\n result.selfCheck = await runSelfCheck(options.dir, connection, harnesses, current, { fetchImpl: options.fetchImpl });\n lastSelfCheckAt = Date.now();\n log(`self-check ${result.selfCheck?.ok ? \"passed\" : `failed: ${result.selfCheck?.failed.join(\", \") ?? \"no result\"}`}`);\n }\n } catch (error) { result.error = (error as Error).message; }\n lastMaintenance = result;\n return result;\n };\n\n const schedule = () => {\n if (stopped) return;\n const delay = failures ? Math.min(MAX_BACKOFF_MS, interval * 2 ** Math.min(failures, 6)) : interval;\n timer = setTimeout(() => { void cycle().finally(schedule); }, delay);\n };\n\n const control = await startControl(options.dir, `${AGENT_VERSION} hook/${hookVersion()}`, {\n \"GET /status\": () => ({ ...computerStatus(options.dir), health: health(), agent: { pid: process.pid, version: AGENT_VERSION, last_cycle: last, last_maintenance: lastMaintenance } }),\n \"POST /flush\": async () => ({ cycle: await cycle() }),\n \"POST /repair\": () => {\n const repaired = repairHookEntries();\n for (const r of repaired) log(`repaired the Scopebond hook entry in ${r.file}`);\n return { repaired };\n },\n \"POST /maintain\": async () => ({ maintenance: await maintain(true) }),\n // Warn mode: the hook asks; only the window answers. The caller learns the answer, never decides it.\n \"POST /override\": async (body) => {\n const question = parseQuestion(body);\n if (!question) return { decision: \"unavailable\" };\n log(`asking whether to allow \"${question.title}\"`);\n const answer = await prompt(question);\n log(`override ${answer.decision === \"allow\" ? \"given\" : answer.decision === \"deny\" ? \"not given\" : \"not available\"} for \"${question.title}\"`);\n if (answer.decision === \"allow\" && answer.reason) { queueReason(options.dir, question.action_id, answer.reason); void sendReasons(); }\n return answer;\n },\n });\n const tray = (options.tray ?? true) && process.env.SCOPEBOND_AGENT_TRAY !== \"off\" ? startTray(options.dir) : null;\n const stop = async () => {\n stopped = true;\n try { tray?.kill(); } catch { /* already gone */ }\n if (timer) clearTimeout(timer);\n if (maintenanceTimer) clearInterval(maintenanceTimer);\n await running?.catch(() => undefined);\n await control.close();\n };\n log(`Scopebond Agent ${agentVersion()} running for ${options.dir} (control on 127.0.0.1:${control.endpoint.port})`);\n await cycle();\n schedule();\n if (options.maintenance !== false) {\n // Shortly after start (so a fresh sign-in is not slowed), then every six hours.\n setTimeout(() => { if (!stopped) void maintain(); }, 2 * 60_000).unref?.();\n maintenanceTimer = setInterval(() => { if (!stopped) void maintain(); }, UPDATE_EVERY_MS);\n maintenanceTimer.unref?.();\n }\n return { port: control.endpoint.port, cycleNow: cycle, maintainNow: () => maintain(true), stop };\n}\n"]}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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","sourcesContent":["// The resident loop: one cycle now, then one every interval, sooner after a change is asked for,\n// backing off while the workspace is unreachable. Exactly one agent serves a Scopebond home: a\n// second one finds the first answering and exits. Every six hours it asks the workspace which\n// versions to run, and once a day it runs the end-to-end self-check.\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { hookVersion, loadConnection, userHarnessFile, writeHarnessConfig, type Harness } from \"@scopebond/hook\";\nimport { computerStatus, expectedHarnesses, missingHookEntries, runCycle, type CycleResult } from \"./agent.js\";\nimport { launcherPath } from \"./autostart.js\";\nimport { callAgent, startControl } from \"./ipc.js\";\nimport { runSelfCheck } from \"./selfcheck.js\";\nimport { flushReasons, queueReason } from \"./override-reasons.js\";\nimport { parseQuestion, serialized, systemPrompter, type Prompter } from \"./prompt.js\";\nimport { healthOf, type HealthLevel } from \"./health.js\";\nimport { notifyChange, startTray } from \"./tray.js\";\nimport { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries, maintainedHookCommand } from \"./update.js\";\n\nexport const AGENT_VERSION = `agent/${agentVersion()}`;\nconst INTERVAL_MS = 60_000;\nconst MAX_BACKOFF_MS = 15 * 60_000;\nconst UPDATE_EVERY_MS = 6 * 60 * 60 * 1000;\nconst SELF_CHECK_EVERY_MS = 24 * 60 * 60 * 1000;\nexport const AFTER_PID_ENV = \"SCOPEBOND_AGENT_AFTER_PID\";\n\nexport interface ServiceOptions {\n dir: string; intervalMs?: number; fetchImpl?: typeof fetch; log?: (line: string) => void;\n /** Off in tests: the update check and self-check run only when asked. */\n maintenance?: boolean;\n /** Called after a successful update instead of restarting (tests). */\n onUpdated?: (version: string) => void;\n /** Shows the Scopebond window for an override (tests replace it). */\n prompter?: Prompter;\n /** The Windows tray icon (default on Windows; SCOPEBOND_AGENT_TRAY=off turns it off). */\n tray?: boolean;\n /** Called once the agent has stopped because `stop` (or `autostart off`) asked it to; the CLI exits. */\n onStopped?: () => void;\n}\n\nexport interface Service {\n stop(): Promise<void>;\n cycleNow(): Promise<CycleResult>;\n maintainNow(): Promise<MaintenanceResult>;\n port: number;\n}\n\nexport interface MaintenanceResult {\n at: number;\n policy: \"recommended\" | \"hold\" | \"unknown\";\n updatedTo: string | null;\n hookEntries: Array<{ harness: Harness; file: string; reason: string }>;\n selfCheck: Awaited<ReturnType<typeof runSelfCheck>>;\n error: string | null;\n}\n\n/** Put the Scopebond hook back into agent settings that lost it. A visible change: each one is\n * logged and returned, and only settings of agents this computer has are touched. */\nexport function repairHookEntries(harnesses: Harness[] = expectedHarnesses()): Array<{ harness: Harness; file: string }> {\n const repaired: Array<{ harness: Harness; file: string }> = [];\n for (const harness of missingHookEntries(harnesses)) {\n const file = writeHarnessConfig(userHarnessFile(harness), harness, maintainedHookCommand(harness));\n repaired.push({ harness, file });\n }\n return repaired;\n}\n\n/** Start a fresh agent (the updated one) that waits for this process to exit, through the\n * autostart launcher when there is one so it finds Node the same way sign-in does. */\nexport function spawnReplacement(dir: string): void {\n const launcher = launcherPath(dir);\n const cli = realpathSync(fileURLToPath(new URL(\"./cli.js\", import.meta.url)));\n const env = { ...process.env, [AFTER_PID_ENV]: String(process.pid) };\n const [command, args] = existsSync(launcher)\n ? process.platform === \"win32\" ? [\"cmd.exe\", [\"/d\", \"/c\", launcher]] as const : [\"/bin/sh\", [launcher]] as const\n : [process.execPath, [cli, \"run\"]] as const;\n const child = spawn(command, [...args], { env, detached: true, stdio: \"ignore\", windowsHide: true });\n child.unref();\n}\n\nexport const AGENT_LOCK = \"agent.lock\";\n\n/** How long a lock whose agent never answered on its local channel still counts as an agent starting up. */\nexport const AGENT_LOCK_STARTING_MS = 60_000;\n\n/** Take the one-agent-per-home lock: created exclusively, or taken over when the agent named in it is\n * gone. The caller has already found no agent answering on the local channel, so a lock older than\n * AGENT_LOCK_STARTING_MS is left over even when its process id is alive: Windows reuses process ids\n * soon after a restart, and a sign-in after a reboot must not find \"already running\". A newer lock\n * is an agent still starting (or one whose id is not written yet): this start gives way.\n * Returns the lock file, or null while another agent holds it. */\nexport function acquireAgentLock(dir: string, now = Date.now()): string | null {\n const file = join(dir, AGENT_LOCK);\n mkdirSync(dir, { recursive: true });\n const mine = `${process.pid} ${now}`;\n try { writeFileSync(file, mine, { flag: \"wx\" }); return file; } catch { /* held, or left behind */ }\n let text = \"\";\n let written = now;\n try { text = readFileSync(file, \"utf8\").trim(); written = statSync(file).mtimeMs; } catch { /* removed meanwhile */ }\n const [pidText, atText] = text.split(/\\s+/);\n const holder = Number(pidText);\n const at = Number(atText);\n const since = Number.isFinite(at) && at > 0 ? at : written;\n const fresh = now - since < AGENT_LOCK_STARTING_MS;\n if (holder !== process.pid && fresh) {\n if (!(Number.isInteger(holder) && holder > 0)) return null; // being written by an agent starting this moment\n try { process.kill(holder, 0); return null; } catch { /* that agent is gone: take the lock over */ }\n }\n writeFileSync(file, mine);\n return file;\n}\n\nexport function releaseAgentLock(file: string): void {\n try { if (readFileSync(file, \"utf8\").trim().split(/\\s+/)[0] === String(process.pid)) rmSync(file, { force: true }); } catch { /* already gone */ }\n}\n\nexport async function startService(options: ServiceOptions): Promise<Service> {\n const log = options.log ?? ((line: string) => console.log(`${new Date().toISOString()} ${line}`));\n if (await callAgent(options.dir, \"GET\", \"/status\", undefined, 2_000)) throw new Error(\"a Scopebond Agent is already running for this computer\");\n // Two agents started within moments of each other (a slow first start, then a second start) both see\n // nobody answering yet: the lock file decides, so the second exits instead of orphaning the first.\n const lock = acquireAgentLock(options.dir);\n if (!lock) throw new Error(\"a Scopebond Agent is already running for this computer\");\n const interval = options.intervalMs ?? INTERVAL_MS;\n let last: CycleResult | null = null;\n let lastMaintenance: MaintenanceResult | null = null;\n let failures = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let maintenanceTimer: ReturnType<typeof setInterval> | null = null;\n let running: Promise<CycleResult> | null = null;\n let stopped = false;\n let lastSelfCheckAt = 0;\n\n const prompt = serialized(options.prompter ?? systemPrompter);\n let shownLevel: HealthLevel | null = null;\n const health = () => healthOf(computerStatus(options.dir), lastMaintenance?.selfCheck ?? null);\n const noticeHealth = () => { const h = health(); notifyChange(shownLevel, h); shownLevel = h.level; };\n const sendReasons = async () => {\n const connection = loadConnection(options.dir);\n if (!connection) return;\n try { const sent = await flushReasons(options.dir, connection, options.fetchImpl); if (sent) log(`sent ${sent} override reason(s) to the workspace`); }\n catch { /* they wait for the next cycle */ }\n };\n const cycle = async (): Promise<CycleResult> => {\n if (running) return running;\n running = runCycle({ dir: options.dir, fetchImpl: options.fetchImpl }).then(async (result) => {\n await sendReasons();\n try { noticeHealth(); } catch { /* status is best effort */ }\n last = result;\n failures = result.deliveryError ? failures + 1 : 0;\n if (result.delivered) log(`delivered ${result.delivered} record(s); ${result.pending} waiting`);\n if (result.deliveryError) log(`delivery problem: ${result.deliveryError}`);\n return result;\n }).finally(() => { running = null; });\n return running;\n };\n\n const maintain = async (forceSelfCheck = false): Promise<MaintenanceResult> => {\n const result: MaintenanceResult = { at: Date.now(), policy: \"unknown\", updatedTo: null, hookEntries: [], selfCheck: null, error: null };\n try {\n const connection = loadConnection(options.dir);\n const harnesses = expectedHarnesses();\n const target = connection ? await fetchClientVersion(connection, options.fetchImpl) : null;\n if (target) result.policy = target.policy;\n const current = agentVersion();\n if (target?.policy === \"recommended\" && target.agent && compareVersions(target.agent, current) > 0) {\n log(`updating the Scopebond Agent ${current} -> ${target.agent}`);\n const installed = await installAgent(target.agent);\n if (installed.ok) {\n result.updatedTo = target.agent;\n lastMaintenance = result;\n if (options.onUpdated) options.onUpdated(target.agent);\n else { spawnReplacement(options.dir); setTimeout(() => { void stop().finally(() => process.exit(0)); }, 500); }\n return result;\n }\n result.error = `update to ${target.agent} failed: ${installed.output.slice(-300)}`;\n log(result.error);\n }\n // Hook entries: under \"recommended\" they move to the newest hook named or carried; under \"hold\"\n // only broken ones are repaired, at the version they already name or the one the agent carries.\n const hookTarget = target?.policy === \"recommended\" && target.hook && compareVersions(target.hook, hookVersion()) > 0 ? target.hook : hookVersion();\n result.hookEntries = maintainHookEntries(harnesses, hookTarget, target?.policy !== \"hold\");\n for (const change of result.hookEntries) log(`${change.reason}: ${change.file}`);\n if (connection && (forceSelfCheck || Date.now() - lastSelfCheckAt >= SELF_CHECK_EVERY_MS)) {\n result.selfCheck = await runSelfCheck(options.dir, connection, harnesses, current, { fetchImpl: options.fetchImpl });\n lastSelfCheckAt = Date.now();\n log(`self-check ${result.selfCheck?.ok ? \"passed\" : `failed: ${result.selfCheck?.failed.join(\", \") ?? \"no result\"}`}`);\n }\n } catch (error) { result.error = (error as Error).message; }\n lastMaintenance = result;\n return result;\n };\n\n const schedule = () => {\n if (stopped) return;\n const delay = failures ? Math.min(MAX_BACKOFF_MS, interval * 2 ** Math.min(failures, 6)) : interval;\n timer = setTimeout(() => { void cycle().finally(schedule); }, delay);\n };\n\n const control = await startControl(options.dir, `${AGENT_VERSION} hook/${hookVersion()}`, {\n \"GET /status\": () => ({ ...computerStatus(options.dir), health: health(), agent: { pid: process.pid, version: AGENT_VERSION, last_cycle: last, last_maintenance: lastMaintenance } }),\n \"POST /flush\": async () => ({ cycle: await cycle() }),\n \"POST /repair\": () => {\n const repaired = repairHookEntries();\n for (const r of repaired) log(`repaired the Scopebond hook entry in ${r.file}`);\n return { repaired };\n },\n \"POST /maintain\": async () => ({ maintenance: await maintain(true) }),\n // `scopebond-agent stop` and `autostart off`: answer first, then stop, so the caller hears back.\n \"POST /stop\": () => {\n log(\"stopping: asked to by this computer's user\");\n setTimeout(() => { void stop().finally(() => options.onStopped?.()); }, 50);\n return { stopping: true };\n },\n // Warn mode: the hook asks; only the window answers. The caller learns the answer, never decides it.\n \"POST /override\": async (body) => {\n const question = parseQuestion(body);\n if (!question) return { decision: \"unavailable\" };\n log(`asking whether to allow \"${question.title}\"`);\n const answer = await prompt(question);\n log(`override ${answer.decision === \"allow\" ? \"given\" : answer.decision === \"deny\" ? \"not given\" : \"not available\"} for \"${question.title}\"`);\n if (answer.decision === \"allow\" && answer.reason) { queueReason(options.dir, question.action_id, answer.reason); void sendReasons(); }\n return answer;\n },\n });\n const tray = (options.tray ?? true) && process.env.SCOPEBOND_AGENT_TRAY !== \"off\" ? startTray(options.dir) : null;\n const stop = async () => {\n stopped = true;\n try { tray?.kill(); } catch { /* already gone */ }\n if (timer) clearTimeout(timer);\n if (maintenanceTimer) clearInterval(maintenanceTimer);\n await running?.catch(() => undefined);\n await control.close();\n releaseAgentLock(lock);\n };\n log(`Scopebond Agent ${agentVersion()} running for ${options.dir} (control on 127.0.0.1:${control.endpoint.port})`);\n await cycle();\n schedule();\n if (options.maintenance !== false) {\n // Shortly after start (so a fresh sign-in is not slowed), then every six hours.\n setTimeout(() => { if (!stopped) void maintain(); }, 2 * 60_000).unref?.();\n maintenanceTimer = setInterval(() => { if (!stopped) void maintain(); }, UPDATE_EVERY_MS);\n maintenanceTimer.unref?.();\n }\n return { port: control.endpoint.port, cycleNow: cycle, maintainNow: () => maintain(true), stop };\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import { type Harness } from "@scopebond/hook";
2
+ export type SetupStep = "login" | "install_agent" | "autostart";
3
+ export interface SetupState {
4
+ /** The workspace origin this computer's user-level connection points at, if any. */
5
+ connectedTo: string | null;
6
+ /** The workspace refused that connection's credential (it must sign in again). */
7
+ credentialRefused: boolean;
8
+ /** The agent version installed for this user with npm, if any. */
9
+ installedVersion: string | null;
10
+ /** Autostart is on and its launcher exists. */
11
+ autostartOk: boolean;
12
+ }
13
+ /** What a setup run has to do, given what is already true. Pure, so a second run is provably a no-op
14
+ * (or a repair) rather than a second sign-in or install. */
15
+ export declare function setupPlan(state: SetupState, origin: string, version: string, relogin?: boolean): SetupStep[];
16
+ export declare function nodeSupported(version?: string): boolean;
17
+ /** The folder npm puts global commands in: the prefix itself on Windows, <prefix>/bin elsewhere. */
18
+ export declare function globalBinDir(prefix: string, platform?: NodeJS.Platform): string;
19
+ /** Whether `dir` is on PATH (case-insensitive on Windows, ignoring a trailing separator). */
20
+ export declare function onPath(dir: string, pathEnv?: string, platform?: NodeJS.Platform): boolean;
21
+ /** The PowerShell line that adds a folder to this user's PATH for new windows. */
22
+ export declare function addToPathCommand(dir: string): string;
23
+ export interface SetupOptions {
24
+ dir: string;
25
+ origin: string;
26
+ harness: Harness;
27
+ relogin: boolean;
28
+ version: string;
29
+ me: string;
30
+ }
31
+ export declare function runSetup(o: SetupOptions): Promise<number>;
32
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAYA,OAAO,EAAsD,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAMnG,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,WAAW,CAAC;AAEhE,MAAM,WAAW,UAAU;IACzB,oFAAoF;IACpF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kFAAkF;IAClF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,kEAAkE;IAClE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,+CAA+C;IAC/C,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;6DAC6D;AAC7D,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,UAAQ,GAAG,SAAS,EAAE,CAO1G;AAED,wBAAgB,aAAa,CAAC,OAAO,SAAwB,GAAG,OAAO,CAGtE;AAED,oGAAoG;AACpG,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,CAEjG;AAED,6FAA6F;AAC7F,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,SAA6C,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,OAAO,CAQ/I;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AA6BD,MAAM,WAAW,YAAY;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AAE9H,wBAAsB,QAAQ,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAuE/D"}
package/dist/setup.js ADDED
@@ -0,0 +1,182 @@
1
+ // `scopebond-agent setup <workspace-url>`: one command from nothing to a connected computer.
2
+ // It checks Node, signs this person in with a code (which puts the hook in the user-level agent
3
+ // settings), installs the agent for this user so autostart has a stable path, turns autostart on
4
+ // and ends with `status`. Run again, it repairs what is missing and duplicates nothing: an
5
+ // existing connection to the same workspace is kept, the same agent version is not reinstalled,
6
+ // and the hook entry and autostart are put back only where they are gone.
7
+ import { spawn, spawnSync } from "node:child_process";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { createRequire } from "node:module";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ import { loadConnection, nodeTooOldLines, readDeliveryState } from "@scopebond/hook";
13
+ import { autostartHealth, enableAutostart, startNow } from "./autostart.js";
14
+ import { callAgent } from "./ipc.js";
15
+ import { compareVersions, ownNpm } from "./update.js";
16
+ import { repairHookEntries } from "./service.js";
17
+ /** What a setup run has to do, given what is already true. Pure, so a second run is provably a no-op
18
+ * (or a repair) rather than a second sign-in or install. */
19
+ export function setupPlan(state, origin, version, relogin = false) {
20
+ const steps = [];
21
+ if (relogin || state.connectedTo !== origin || state.credentialRefused)
22
+ steps.push("login");
23
+ const install = state.installedVersion === null || compareVersions(state.installedVersion, version) < 0;
24
+ if (install)
25
+ steps.push("install_agent");
26
+ if (install || !state.autostartOk)
27
+ steps.push("autostart");
28
+ return steps;
29
+ }
30
+ export function nodeSupported(version = process.versions.node) {
31
+ const [major, minor] = version.split(".").map(Number);
32
+ return major > 22 || (major === 22 && minor >= 13);
33
+ }
34
+ /** The folder npm puts global commands in: the prefix itself on Windows, <prefix>/bin elsewhere. */
35
+ export function globalBinDir(prefix, platform = process.platform) {
36
+ return platform === "win32" ? prefix : join(prefix, "bin");
37
+ }
38
+ /** Whether `dir` is on PATH (case-insensitive on Windows, ignoring a trailing separator). */
39
+ export function onPath(dir, pathEnv = process.env.PATH ?? process.env.Path ?? "", platform = process.platform) {
40
+ const norm = (p) => {
41
+ let t = p.trim();
42
+ while (t.endsWith("/") || t.endsWith("\\"))
43
+ t = t.slice(0, -1); // a loop, not a regex: PATH entries are untrusted input
44
+ return platform === "win32" ? t.toLowerCase().replace(/\//g, "\\") : t;
45
+ };
46
+ const target = norm(dir);
47
+ return pathEnv.split(platform === "win32" ? ";" : ":").some((p) => p && norm(p) === target);
48
+ }
49
+ /** The PowerShell line that adds a folder to this user's PATH for new windows. */
50
+ export function addToPathCommand(dir) {
51
+ return `[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";${dir}", "User")`;
52
+ }
53
+ function npm(args) {
54
+ const win = process.platform === "win32";
55
+ // This Node's own npm, without a shell; otherwise npm on PATH (a .cmd on Windows, started through a shell, each argument quoted).
56
+ const own = ownNpm();
57
+ const r = own
58
+ ? spawnSync(own[0], [...own[1], ...args], { encoding: "utf8", windowsHide: true, timeout: 5 * 60_000 })
59
+ : spawnSync(win ? "npm.cmd" : "npm", win ? args.map((a) => `"${a}"`) : args, { encoding: "utf8", shell: win, windowsHide: true, timeout: 5 * 60_000 });
60
+ return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}` };
61
+ }
62
+ function globalAgent() {
63
+ const prefix = npm(["prefix", "-g"]);
64
+ const root = npm(["root", "-g"]);
65
+ if (!root.ok)
66
+ return { prefix: null, cli: null, version: null };
67
+ const dir = join(root.out.trim().split(/\r?\n/).pop() ?? "", "@scopebond", "agent");
68
+ const cli = join(dir, "dist", "cli.js");
69
+ let version = null;
70
+ try {
71
+ version = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version ?? null;
72
+ }
73
+ catch { /* not installed */ }
74
+ return { prefix: prefix.ok ? prefix.out.trim().split(/\r?\n/).pop() ?? null : null, cli: existsSync(cli) ? cli : null, version };
75
+ }
76
+ /** The bundled hook's CLI, which this agent depends on at an exact version. */
77
+ function hookCli() {
78
+ const index = createRequire(import.meta.url).resolve("@scopebond/hook");
79
+ return join(dirname(index), "cli.js");
80
+ }
81
+ export async function runSetup(o) {
82
+ const say = (line) => console.log(line);
83
+ if (!nodeSupported()) {
84
+ for (const line of nodeTooOldLines(process.versions.node))
85
+ console.error(line);
86
+ return 1;
87
+ }
88
+ const connection = loadConnection(o.dir);
89
+ let connectedTo = null;
90
+ try {
91
+ connectedTo = connection ? new URL(connection.url).origin : null;
92
+ }
93
+ catch {
94
+ connectedTo = null;
95
+ }
96
+ const global = globalAgent();
97
+ const state = {
98
+ connectedTo,
99
+ credentialRefused: readDeliveryState(o.dir).invalid_since != null,
100
+ installedVersion: global.version,
101
+ autostartOk: autostartHealth(o.dir).ok,
102
+ };
103
+ const steps = setupPlan(state, o.origin, o.version, o.relogin);
104
+ say(`Setting up Scopebond for this user (${steps.length ? steps.join(", ").replace(/_/g, " ") : "nothing missing"}).`);
105
+ // 1. Sign in with a code. The hook's own login prints the link and code and waits for approval.
106
+ if (steps.includes("login")) {
107
+ const flag = o.harness === "claude" ? "--claude" : `--${o.harness}`;
108
+ // From the home folder, so a project's own setup in the current folder can never take the sign-in;
109
+ // and a failed sign-in names this setup command, which also installs and starts the agent.
110
+ const again = `${process.platform === "win32" ? "npx.cmd" : "npx"} -y @scopebond/agent@${o.version} setup ${o.origin}${o.harness === "claude" ? "" : ` --${o.harness}`}`;
111
+ const r = spawnSync(process.execPath, [hookCli(), "login", o.origin, flag], { stdio: "inherit", cwd: homedir(), env: { ...process.env, SCOPEBOND_HOME: o.dir, SCOPEBOND_RETRY_COMMAND: again } });
112
+ if (r.status !== 0) {
113
+ console.error("Setup stopped: the sign-in did not finish. Run the same command again for a new code.");
114
+ return 1;
115
+ }
116
+ }
117
+ else {
118
+ say(`✓ Already connected to ${o.origin} (to sign in again anyway, add --relogin).`);
119
+ for (const fixed of repairHookEntries())
120
+ say(`✓ Put the Scopebond hook back in ${fixed.file}`);
121
+ }
122
+ // 2. The agent, installed for this user so autostart points at a path npm will not clear.
123
+ let cli = global.cli;
124
+ if (steps.includes("install_agent")) {
125
+ const spec = process.env.SCOPEBOND_AGENT_INSTALL_SPEC || `@scopebond/agent@${o.version}`;
126
+ say(`Installing the Scopebond Agent for this user: ${process.platform === "win32" ? "npm.cmd" : "npm"} install -g ${spec}`);
127
+ const installed = npm(["install", "-g", spec, "--no-fund", "--no-audit"]);
128
+ if (!installed.ok) {
129
+ console.error(`Setup stopped: npm could not install the agent.\n${installed.out.slice(-1500)}`);
130
+ return 1;
131
+ }
132
+ cli = globalAgent().cli;
133
+ if (!cli) {
134
+ console.error("Setup stopped: npm reported success but the agent is not in npm's global folder.");
135
+ return 1;
136
+ }
137
+ }
138
+ else
139
+ say(`✓ The Scopebond Agent ${global.version} is installed for this user.`);
140
+ const bin = global.prefix ? globalBinDir(global.prefix) : null;
141
+ if (bin && !onPath(bin)) {
142
+ say(`Note: npm's global folder ${bin} is not on PATH, so new terminals will not find ${o.me}.`);
143
+ say(process.platform === "win32"
144
+ ? ` Add it for your user in PowerShell, then open a new window: ${addToPathCommand(bin)}
145
+ (or: Start, "Edit environment variables for your account", Path, New)`
146
+ : ` Add it to PATH in your shell profile: export PATH="${bin}:$PATH"`);
147
+ }
148
+ // 3. Autostart, and the agent running now.
149
+ if (steps.includes("autostart") && cli) {
150
+ say(enableAutostart(o.dir, cli));
151
+ }
152
+ else
153
+ say("✓ The agent already starts with your sign-in.");
154
+ if (cli && !await callAgent(o.dir, "GET", "/status", undefined, 2_000)) {
155
+ const up = async (tries) => {
156
+ for (let i = 0; i < tries; i++) {
157
+ if (await callAgent(o.dir, "GET", "/status", undefined, 1_000))
158
+ return true;
159
+ await new Promise((r) => setTimeout(r, 500));
160
+ }
161
+ return false;
162
+ };
163
+ if (process.platform === "win32") {
164
+ // The headless console first; where it does not start, the launcher through a hidden cmd.exe.
165
+ if (!(startNow(o.dir, "win32", 0) && await up(12))) {
166
+ startNow(o.dir, "win32", 1);
167
+ await up(18);
168
+ }
169
+ }
170
+ else {
171
+ // No systemd user session (a container, WSL without systemd): run it detached until the next sign-in.
172
+ if (!await up(10) && process.platform === "linux")
173
+ spawn(process.execPath, [cli, "run"], { detached: true, stdio: "ignore", env: { ...process.env, SCOPEBOND_HOME: o.dir } }).unref();
174
+ await up(20);
175
+ }
176
+ }
177
+ // 4. Status, from the installed agent.
178
+ say("");
179
+ const status = spawnSync(process.execPath, [cli ?? process.argv[1], "status"], { stdio: "inherit", env: { ...process.env, SCOPEBOND_HOME: o.dir } });
180
+ return status.status === 0 ? 0 : 1;
181
+ }
182
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"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","sourcesContent":["// `scopebond-agent setup <workspace-url>`: one command from nothing to a connected computer.\n// It checks Node, signs this person in with a code (which puts the hook in the user-level agent\n// settings), installs the agent for this user so autostart has a stable path, turns autostart on\n// and ends with `status`. Run again, it repairs what is missing and duplicates nothing: an\n// existing connection to the same workspace is kept, the same agent version is not reinstalled,\n// and the hook entry and autostart are put back only where they are gone.\n\nimport { spawn, spawnSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { loadConnection, nodeTooOldLines, readDeliveryState, type Harness } from \"@scopebond/hook\";\nimport { autostartHealth, enableAutostart, startNow } from \"./autostart.js\";\nimport { callAgent } from \"./ipc.js\";\nimport { compareVersions, ownNpm } from \"./update.js\";\nimport { repairHookEntries } from \"./service.js\";\n\nexport type SetupStep = \"login\" | \"install_agent\" | \"autostart\";\n\nexport interface SetupState {\n /** The workspace origin this computer's user-level connection points at, if any. */\n connectedTo: string | null;\n /** The workspace refused that connection's credential (it must sign in again). */\n credentialRefused: boolean;\n /** The agent version installed for this user with npm, if any. */\n installedVersion: string | null;\n /** Autostart is on and its launcher exists. */\n autostartOk: boolean;\n}\n\n/** What a setup run has to do, given what is already true. Pure, so a second run is provably a no-op\n * (or a repair) rather than a second sign-in or install. */\nexport function setupPlan(state: SetupState, origin: string, version: string, relogin = false): SetupStep[] {\n const steps: SetupStep[] = [];\n if (relogin || state.connectedTo !== origin || state.credentialRefused) steps.push(\"login\");\n const install = state.installedVersion === null || compareVersions(state.installedVersion, version) < 0;\n if (install) steps.push(\"install_agent\");\n if (install || !state.autostartOk) steps.push(\"autostart\");\n return steps;\n}\n\nexport function nodeSupported(version = process.versions.node): boolean {\n const [major, minor] = version.split(\".\").map(Number);\n return major > 22 || (major === 22 && minor >= 13);\n}\n\n/** The folder npm puts global commands in: the prefix itself on Windows, <prefix>/bin elsewhere. */\nexport function globalBinDir(prefix: string, platform: NodeJS.Platform = process.platform): string {\n return platform === \"win32\" ? prefix : join(prefix, \"bin\");\n}\n\n/** Whether `dir` is on PATH (case-insensitive on Windows, ignoring a trailing separator). */\nexport function onPath(dir: string, pathEnv = process.env.PATH ?? process.env.Path ?? \"\", platform: NodeJS.Platform = process.platform): boolean {\n const norm = (p: string) => {\n let t = p.trim();\n while (t.endsWith(\"/\") || t.endsWith(\"\\\\\")) t = t.slice(0, -1); // a loop, not a regex: PATH entries are untrusted input\n return platform === \"win32\" ? t.toLowerCase().replace(/\\//g, \"\\\\\") : t;\n };\n const target = norm(dir);\n return pathEnv.split(platform === \"win32\" ? \";\" : \":\").some((p) => p && norm(p) === target);\n}\n\n/** The PowerShell line that adds a folder to this user's PATH for new windows. */\nexport function addToPathCommand(dir: string): string {\n return `[Environment]::SetEnvironmentVariable(\"Path\", [Environment]::GetEnvironmentVariable(\"Path\", \"User\") + \";${dir}\", \"User\")`;\n}\n\nfunction npm(args: string[]): { ok: boolean; out: string } {\n const win = process.platform === \"win32\";\n // This Node's own npm, without a shell; otherwise npm on PATH (a .cmd on Windows, started through a shell, each argument quoted).\n const own = ownNpm();\n const r = own\n ? spawnSync(own[0], [...own[1], ...args], { encoding: \"utf8\", windowsHide: true, timeout: 5 * 60_000 })\n : spawnSync(win ? \"npm.cmd\" : \"npm\", win ? args.map((a) => `\"${a}\"`) : args, { encoding: \"utf8\", shell: win, windowsHide: true, timeout: 5 * 60_000 });\n return { ok: r.status === 0, out: `${r.stdout ?? \"\"}${r.stderr ?? \"\"}` };\n}\n\nfunction globalAgent(): { prefix: string | null; cli: string | null; version: string | null } {\n const prefix = npm([\"prefix\", \"-g\"]);\n const root = npm([\"root\", \"-g\"]);\n if (!root.ok) return { prefix: null, cli: null, version: null };\n const dir = join(root.out.trim().split(/\\r?\\n/).pop() ?? \"\", \"@scopebond\", \"agent\");\n const cli = join(dir, \"dist\", \"cli.js\");\n let version: string | null = null;\n try { version = (JSON.parse(readFileSync(join(dir, \"package.json\"), \"utf8\")) as { version?: string }).version ?? null; } catch { /* not installed */ }\n return { prefix: prefix.ok ? prefix.out.trim().split(/\\r?\\n/).pop() ?? null : null, cli: existsSync(cli) ? cli : null, version };\n}\n\n/** The bundled hook's CLI, which this agent depends on at an exact version. */\nfunction hookCli(): string {\n const index = createRequire(import.meta.url).resolve(\"@scopebond/hook\");\n return join(dirname(index), \"cli.js\");\n}\n\nexport interface SetupOptions { dir: string; origin: string; harness: Harness; relogin: boolean; version: string; me: string }\n\nexport async function runSetup(o: SetupOptions): Promise<number> {\n const say = (line: string) => console.log(line);\n if (!nodeSupported()) { for (const line of nodeTooOldLines(process.versions.node)) console.error(line); return 1; }\n const connection = loadConnection(o.dir);\n let connectedTo: string | null = null;\n try { connectedTo = connection ? new URL(connection.url).origin : null; } catch { connectedTo = null; }\n const global = globalAgent();\n const state: SetupState = {\n connectedTo,\n credentialRefused: readDeliveryState(o.dir).invalid_since != null,\n installedVersion: global.version,\n autostartOk: autostartHealth(o.dir).ok,\n };\n const steps = setupPlan(state, o.origin, o.version, o.relogin);\n say(`Setting up Scopebond for this user (${steps.length ? steps.join(\", \").replace(/_/g, \" \") : \"nothing missing\"}).`);\n\n // 1. Sign in with a code. The hook's own login prints the link and code and waits for approval.\n if (steps.includes(\"login\")) {\n const flag = o.harness === \"claude\" ? \"--claude\" : `--${o.harness}`;\n // From the home folder, so a project's own setup in the current folder can never take the sign-in;\n // and a failed sign-in names this setup command, which also installs and starts the agent.\n const again = `${process.platform === \"win32\" ? \"npx.cmd\" : \"npx\"} -y @scopebond/agent@${o.version} setup ${o.origin}${o.harness === \"claude\" ? \"\" : ` --${o.harness}`}`;\n const r = spawnSync(process.execPath, [hookCli(), \"login\", o.origin, flag], { stdio: \"inherit\", cwd: homedir(), env: { ...process.env, SCOPEBOND_HOME: o.dir, SCOPEBOND_RETRY_COMMAND: again } });\n if (r.status !== 0) { console.error(\"Setup stopped: the sign-in did not finish. Run the same command again for a new code.\"); return 1; }\n } else {\n say(`✓ Already connected to ${o.origin} (to sign in again anyway, add --relogin).`);\n for (const fixed of repairHookEntries()) say(`✓ Put the Scopebond hook back in ${fixed.file}`);\n }\n\n // 2. The agent, installed for this user so autostart points at a path npm will not clear.\n let cli = global.cli;\n if (steps.includes(\"install_agent\")) {\n const spec = process.env.SCOPEBOND_AGENT_INSTALL_SPEC || `@scopebond/agent@${o.version}`;\n say(`Installing the Scopebond Agent for this user: ${process.platform === \"win32\" ? \"npm.cmd\" : \"npm\"} install -g ${spec}`);\n const installed = npm([\"install\", \"-g\", spec, \"--no-fund\", \"--no-audit\"]);\n if (!installed.ok) { console.error(`Setup stopped: npm could not install the agent.\\n${installed.out.slice(-1500)}`); return 1; }\n cli = globalAgent().cli;\n if (!cli) { console.error(\"Setup stopped: npm reported success but the agent is not in npm's global folder.\"); return 1; }\n } else say(`✓ The Scopebond Agent ${global.version} is installed for this user.`);\n const bin = global.prefix ? globalBinDir(global.prefix) : null;\n if (bin && !onPath(bin)) {\n say(`Note: npm's global folder ${bin} is not on PATH, so new terminals will not find ${o.me}.`);\n say(process.platform === \"win32\"\n ? ` Add it for your user in PowerShell, then open a new window: ${addToPathCommand(bin)}\n (or: Start, \"Edit environment variables for your account\", Path, New)`\n : ` Add it to PATH in your shell profile: export PATH=\"${bin}:$PATH\"`);\n }\n\n // 3. Autostart, and the agent running now.\n if (steps.includes(\"autostart\") && cli) {\n say(enableAutostart(o.dir, cli));\n } else say(\"✓ The agent already starts with your sign-in.\");\n if (cli && !await callAgent(o.dir, \"GET\", \"/status\", undefined, 2_000)) {\n const up = async (tries: number) => {\n for (let i = 0; i < tries; i++) { if (await callAgent(o.dir, \"GET\", \"/status\", undefined, 1_000)) return true; await new Promise((r) => setTimeout(r, 500)); }\n return false;\n };\n if (process.platform === \"win32\") {\n // The headless console first; where it does not start, the launcher through a hidden cmd.exe.\n if (!(startNow(o.dir, \"win32\", 0) && await up(12))) { startNow(o.dir, \"win32\", 1); await up(18); }\n } else {\n // No systemd user session (a container, WSL without systemd): run it detached until the next sign-in.\n if (!await up(10) && process.platform === \"linux\") spawn(process.execPath, [cli, \"run\"], { detached: true, stdio: \"ignore\", env: { ...process.env, SCOPEBOND_HOME: o.dir } }).unref();\n await up(20);\n }\n }\n\n // 4. Status, from the installed agent.\n say(\"\");\n const status = spawnSync(process.execPath, [cli ?? process.argv[1], \"status\"], { stdio: \"inherit\", env: { ...process.env, SCOPEBOND_HOME: o.dir } });\n return status.status === 0 ? 0 : 1;\n}\n"]}