@scopebond/hook 0.11.0 → 0.12.0
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.
- package/README.md +14 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +59 -9
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +6 -1
- package/dist/cloud.js.map +1 -1
- package/dist/delivery-report.d.ts +16 -0
- package/dist/delivery-report.d.ts.map +1 -0
- package/dist/delivery-report.js +51 -0
- package/dist/delivery-report.js.map +1 -0
- package/dist/delivery-state.d.ts +30 -0
- package/dist/delivery-state.d.ts.map +1 -0
- package/dist/delivery-state.js +86 -0
- package/dist/delivery-state.js.map +1 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +8 -2
- package/dist/install.js.map +1 -1
- package/dist/policy-sync.d.ts.map +1 -1
- package/dist/policy-sync.js +2 -0
- package/dist/policy-sync.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +10 -2
- package/dist/runtime.js.map +1 -1
- package/dist/version.d.ts +4 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +5 -5
- package/dist/version.js.map +1 -1
- package/package.json +3 -3
package/dist/cloud.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,qBAAqB,EAAE,KAAK,qBAAqB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAC9F,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAwB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,qBAAqB,EAAE,KAAK,qBAAqB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAC9F,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAwB,MAAM,yBAAyB,CAAC;AAGlF;8EAC8E;AAC9E,MAAM,WAAW,cAAe,SAAQ,qBAAqB;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ;;2EAEuE;IACvE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;6CAEyC;IACzC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;gGAC4F;IAC5F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;8CAE8C;AAC9C,wBAAgB,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,YAAY,CAAC,GAAG,MAAM,CAExF;AAYD,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAiC,CAAC;AAE/E,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAQjE;AAED;;;;;;;;;qEASqE;AACrE,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,GAChF,OAAO,CAAC,cAAc,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyBvE;AAED;yEACyE;AACzE,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAmC,CAAC;AAEjF,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAMlE;AAED;oFACoF;AACpF,wBAAgB,cAAc,CAC5B,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,GAC9F;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAQ7E;AAED;kFACkF;AAClF,wBAAsB,YAAY,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAK3F"}
|
package/dist/cloud.js
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { CloudEnrollmentError, completeCloudEnrollment, createCloudExporter, withCloudExporter, } from "@scopebond/gateway";
|
|
9
9
|
import { SqliteCloudOutbox, loadOrCreateAttester } from "@scopebond/gateway/node";
|
|
10
|
+
import { LOSSLESS_OUTBOX } from "./delivery-report.js";
|
|
10
11
|
/** The address records, observations and recovery go to: the enrollment's `ingest_url` when it
|
|
11
12
|
* is a valid HTTPS origin (or localhost in development), otherwise the workspace URL. Sign-in,
|
|
12
13
|
* policy and the portal always use `url`. */
|
|
@@ -98,7 +99,11 @@ export function retireAttesterKey(dir, kid) {
|
|
|
98
99
|
/** Wrap a receipt store so every stored receipt is enqueued to a durable outbox and
|
|
99
100
|
* exported to Cloud. Returns the wrapped store and the exporter (flush + stop). */
|
|
100
101
|
export function attachExporter(outboxDbPath, connection, store, fetchImpl) {
|
|
101
|
-
|
|
102
|
+
// Lossless (SB275): no cap and no expiry. A record leaves the queue only when the workspace
|
|
103
|
+
// accepts it, or when a key change makes it undeliverable (`recover` then sends it). The
|
|
104
|
+
// gateway's defaults (10,000 records, 64 MiB, 7 days) dropped the newest records once a long
|
|
105
|
+
// outage filled the queue.
|
|
106
|
+
const outbox = new SqliteCloudOutbox(outboxDbPath, LOSSLESS_OUTBOX);
|
|
102
107
|
const exporter = createCloudExporter({ url: ingestUrl(connection), credential: connection.credential, outbox, fetch: fetchImpl });
|
|
103
108
|
return { store: withCloudExporter(store, exporter), exporter, outbox };
|
|
104
109
|
}
|
package/dist/cloud.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud.js","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"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","sourcesContent":["// scopebond-hook → Scopebond Cloud: enroll the machine's countersigning key with a\n// workspace and auto-export signed receipts to the hosted portal, so a connected\n// hook starts monitoring automatically. Reuses the gateway's enrollment and bounded\n// durable exporter (D40 — no new transport). The machine credential and the complete\n// receipt log stay local-first; export is best-effort and never blocks a tool call.\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport {\n CloudEnrollmentError, completeCloudEnrollment, createCloudExporter, withCloudExporter,\n type CloudEnrollmentBundle, type CloudEnrollmentResult, type CloudExporter, type ReceiptStore,\n} from \"@scopebond/gateway\";\nimport { SqliteCloudOutbox, loadOrCreateAttester } from \"@scopebond/gateway/node\";\n\n/** The persisted connection between this machine and a Cloud workspace. Holds the\n * scoped machine credential; treat cloud.json as a secret (written 0600). */\nexport interface HookConnection extends CloudEnrollmentResult {\n url: string;\n /** The workspace's installation id for this machine. An enrollment answer names it\n * `installation_id`, or (older answers) as `gateway_id`, which is the same identifier;\n * observations use `gateway_id` when the explicit field is absent. */\n installation_id?: string;\n /** The installation generation the enrollment created. Only the workspace can say what it\n * is, so there is no fallback: without it the hook reports observations unsupported\n * rather than guessing a generation. */\n installation_generation?: number;\n /** Where this machine sends its records: the workspace's regional ingest address, named by\n * the enrollment answer. Absent from older answers and connections; `url` is used then. */\n ingest_url?: string;\n}\n\n/** The address records, observations and recovery go to: the enrollment's `ingest_url` when it\n * is a valid HTTPS origin (or localhost in development), otherwise the workspace URL. Sign-in,\n * policy and the portal always use `url`. */\nexport function ingestUrl(connection: Pick<HookConnection, \"url\" | \"ingest_url\">): string {\n return safeIngestOrigin(connection.ingest_url) ?? connection.url;\n}\n\nfunction safeIngestOrigin(value: unknown): string | null {\n if (typeof value !== \"string\" || value.length > 2048) return null;\n try {\n const parsed = new URL(value);\n const local = parsed.hostname === \"localhost\" || parsed.hostname === \"127.0.0.1\";\n if ((parsed.protocol !== \"https:\" && !(local && parsed.protocol === \"http:\")) || parsed.username || parsed.password) return null;\n return parsed.origin;\n } catch { return null; }\n}\n\nexport const connectionPath = (dir: string): string => join(dir, \"cloud.json\");\n\n/** Read the persisted connection, or null when the hook is not connected to Cloud. */\nexport function loadConnection(dir: string): HookConnection | null {\n const path = connectionPath(dir);\n if (!existsSync(path)) return null;\n try {\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<HookConnection>;\n if (typeof parsed.url === \"string\" && typeof parsed.credential === \"string\") return parsed as HookConnection;\n } catch { /* fall through */ }\n return null;\n}\n\n/** Enroll the machine's attester with a workspace using the portal's one-use handoff,\n * then persist the scoped machine credential. The attester whose possession is proved\n * here is the same key the hook countersigns receipts with, so ingest accepts them.\n *\n * A workspace refuses a key it already knows — one revoked when the computer was\n * replaced or disconnected, or one already enrolled elsewhere — without spending the\n * token. Reconnecting then replaces the key and enrolls again with the same token, so a\n * second `login` always works; the old key is kept under retired-keys/, and queued\n * receipts it signed leave the delivery queue (they stay in the local log for\n * `recover`), because the new connection can never deliver them. */\nexport async function connectCloud(\n dir: string, url: string, bundle: CloudEnrollmentBundle, fetchImpl?: typeof fetch,\n): Promise<HookConnection & { rotatedFrom?: string; setAside?: number }> {\n let { attester } = loadOrCreateAttester({ file: join(dir, \"attester.key\") });\n const { attester: agent } = loadOrCreateAttester({ file: join(dir, \"agent.key\") });\n let rotatedFrom: string | undefined;\n let result;\n try {\n result = await completeCloudEnrollment({ url, bundle, attester, agent, fetch: fetchImpl });\n } catch (error) {\n if (!(error instanceof CloudEnrollmentError) || error.code !== \"gateway_key_conflict\") throw error;\n rotatedFrom = attester.kid;\n retireAttesterKey(dir, attester.kid);\n attester = loadOrCreateAttester({ file: join(dir, \"attester.key\") }).attester;\n result = await completeCloudEnrollment({ url, bundle, attester, agent, fetch: fetchImpl });\n }\n const { ingest_url: offered, ...enrolled } = result as typeof result & { ingest_url?: unknown };\n const ingest = safeIngestOrigin(offered);\n const connection: HookConnection = { url, ...enrolled, ...(ingest ? { ingest_url: ingest } : {}) };\n writeFileSync(connectionPath(dir), JSON.stringify(connection, null, 2) + \"\\n\", { mode: 0o600 });\n let setAside = 0;\n const outboxPath = join(dir, \"receipts.db.cloud-outbox.db\");\n if (existsSync(outboxPath)) {\n const outbox = new SqliteCloudOutbox(outboxPath);\n try { setAside = outbox.discardNotSignedBy(attester.kid); } finally { outbox.close(); }\n }\n return { ...connection, ...(rotatedFrom ? { rotatedFrom } : {}), ...(setAside ? { setAside } : {}) };\n}\n\n/** Where a replaced countersigning key is kept: receipts it signed stay verifiable, and\n * `recover` can still deliver the ones the workspace never received. */\nexport const retiredKeysDir = (dir: string): string => join(dir, \"retired-keys\");\n\n/** Move the current countersigning key aside so the next load creates a fresh one. */\nexport function retireAttesterKey(dir: string, kid: string): string {\n const target = join(retiredKeysDir(dir), `${kid.replace(/[^A-Za-z0-9_-]/g, \"-\")}.key`);\n const destination = existsSync(target) ? `${target}.${Date.now()}` : target;\n mkdirSync(retiredKeysDir(dir), { recursive: true });\n renameSync(join(dir, \"attester.key\"), destination);\n return destination;\n}\n\n/** Wrap a receipt store so every stored receipt is enqueued to a durable outbox and\n * exported to Cloud. Returns the wrapped store and the exporter (flush + stop). */\nexport function attachExporter(\n outboxDbPath: string, connection: HookConnection, store: ReceiptStore, fetchImpl?: typeof fetch,\n): { store: ReceiptStore; exporter: CloudExporter; outbox: SqliteCloudOutbox } {\n const outbox = new SqliteCloudOutbox(outboxDbPath);\n const exporter = createCloudExporter({ url: ingestUrl(connection), credential: connection.credential, outbox, fetch: fetchImpl });\n return { store: withCloudExporter(store, exporter), exporter, outbox };\n}\n\n/** Attempt delivery with a bounded timeout so a per-invocation hook never hangs the\n * agent; undelivered receipts stay in the durable outbox and flush next time. */\nexport async function flushBounded(exporter: CloudExporter, timeoutMs = 3000): Promise<void> {\n await Promise.race([\n exporter.flush().catch(() => {}),\n new Promise<void>((resolve) => setTimeout(resolve, Math.max(0, timeoutMs)).unref?.()),\n ]);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cloud.js","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"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","sourcesContent":["// scopebond-hook → Scopebond Cloud: enroll the machine's countersigning key with a\n// workspace and auto-export signed receipts to the hosted portal, so a connected\n// hook starts monitoring automatically. Reuses the gateway's enrollment and bounded\n// durable exporter (D40 — no new transport). The machine credential and the complete\n// receipt log stay local-first; export is best-effort and never blocks a tool call.\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport {\n CloudEnrollmentError, completeCloudEnrollment, createCloudExporter, withCloudExporter,\n type CloudEnrollmentBundle, type CloudEnrollmentResult, type CloudExporter, type ReceiptStore,\n} from \"@scopebond/gateway\";\nimport { SqliteCloudOutbox, loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { LOSSLESS_OUTBOX } from \"./delivery-report.js\";\n\n/** The persisted connection between this machine and a Cloud workspace. Holds the\n * scoped machine credential; treat cloud.json as a secret (written 0600). */\nexport interface HookConnection extends CloudEnrollmentResult {\n url: string;\n /** The workspace's installation id for this machine. An enrollment answer names it\n * `installation_id`, or (older answers) as `gateway_id`, which is the same identifier;\n * observations use `gateway_id` when the explicit field is absent. */\n installation_id?: string;\n /** The installation generation the enrollment created. Only the workspace can say what it\n * is, so there is no fallback: without it the hook reports observations unsupported\n * rather than guessing a generation. */\n installation_generation?: number;\n /** Where this machine sends its records: the workspace's regional ingest address, named by\n * the enrollment answer. Absent from older answers and connections; `url` is used then. */\n ingest_url?: string;\n}\n\n/** The address records, observations and recovery go to: the enrollment's `ingest_url` when it\n * is a valid HTTPS origin (or localhost in development), otherwise the workspace URL. Sign-in,\n * policy and the portal always use `url`. */\nexport function ingestUrl(connection: Pick<HookConnection, \"url\" | \"ingest_url\">): string {\n return safeIngestOrigin(connection.ingest_url) ?? connection.url;\n}\n\nfunction safeIngestOrigin(value: unknown): string | null {\n if (typeof value !== \"string\" || value.length > 2048) return null;\n try {\n const parsed = new URL(value);\n const local = parsed.hostname === \"localhost\" || parsed.hostname === \"127.0.0.1\";\n if ((parsed.protocol !== \"https:\" && !(local && parsed.protocol === \"http:\")) || parsed.username || parsed.password) return null;\n return parsed.origin;\n } catch { return null; }\n}\n\nexport const connectionPath = (dir: string): string => join(dir, \"cloud.json\");\n\n/** Read the persisted connection, or null when the hook is not connected to Cloud. */\nexport function loadConnection(dir: string): HookConnection | null {\n const path = connectionPath(dir);\n if (!existsSync(path)) return null;\n try {\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<HookConnection>;\n if (typeof parsed.url === \"string\" && typeof parsed.credential === \"string\") return parsed as HookConnection;\n } catch { /* fall through */ }\n return null;\n}\n\n/** Enroll the machine's attester with a workspace using the portal's one-use handoff,\n * then persist the scoped machine credential. The attester whose possession is proved\n * here is the same key the hook countersigns receipts with, so ingest accepts them.\n *\n * A workspace refuses a key it already knows — one revoked when the computer was\n * replaced or disconnected, or one already enrolled elsewhere — without spending the\n * token. Reconnecting then replaces the key and enrolls again with the same token, so a\n * second `login` always works; the old key is kept under retired-keys/, and queued\n * receipts it signed leave the delivery queue (they stay in the local log for\n * `recover`), because the new connection can never deliver them. */\nexport async function connectCloud(\n dir: string, url: string, bundle: CloudEnrollmentBundle, fetchImpl?: typeof fetch,\n): Promise<HookConnection & { rotatedFrom?: string; setAside?: number }> {\n let { attester } = loadOrCreateAttester({ file: join(dir, \"attester.key\") });\n const { attester: agent } = loadOrCreateAttester({ file: join(dir, \"agent.key\") });\n let rotatedFrom: string | undefined;\n let result;\n try {\n result = await completeCloudEnrollment({ url, bundle, attester, agent, fetch: fetchImpl });\n } catch (error) {\n if (!(error instanceof CloudEnrollmentError) || error.code !== \"gateway_key_conflict\") throw error;\n rotatedFrom = attester.kid;\n retireAttesterKey(dir, attester.kid);\n attester = loadOrCreateAttester({ file: join(dir, \"attester.key\") }).attester;\n result = await completeCloudEnrollment({ url, bundle, attester, agent, fetch: fetchImpl });\n }\n const { ingest_url: offered, ...enrolled } = result as typeof result & { ingest_url?: unknown };\n const ingest = safeIngestOrigin(offered);\n const connection: HookConnection = { url, ...enrolled, ...(ingest ? { ingest_url: ingest } : {}) };\n writeFileSync(connectionPath(dir), JSON.stringify(connection, null, 2) + \"\\n\", { mode: 0o600 });\n let setAside = 0;\n const outboxPath = join(dir, \"receipts.db.cloud-outbox.db\");\n if (existsSync(outboxPath)) {\n const outbox = new SqliteCloudOutbox(outboxPath);\n try { setAside = outbox.discardNotSignedBy(attester.kid); } finally { outbox.close(); }\n }\n return { ...connection, ...(rotatedFrom ? { rotatedFrom } : {}), ...(setAside ? { setAside } : {}) };\n}\n\n/** Where a replaced countersigning key is kept: receipts it signed stay verifiable, and\n * `recover` can still deliver the ones the workspace never received. */\nexport const retiredKeysDir = (dir: string): string => join(dir, \"retired-keys\");\n\n/** Move the current countersigning key aside so the next load creates a fresh one. */\nexport function retireAttesterKey(dir: string, kid: string): string {\n const target = join(retiredKeysDir(dir), `${kid.replace(/[^A-Za-z0-9_-]/g, \"-\")}.key`);\n const destination = existsSync(target) ? `${target}.${Date.now()}` : target;\n mkdirSync(retiredKeysDir(dir), { recursive: true });\n renameSync(join(dir, \"attester.key\"), destination);\n return destination;\n}\n\n/** Wrap a receipt store so every stored receipt is enqueued to a durable outbox and\n * exported to Cloud. Returns the wrapped store and the exporter (flush + stop). */\nexport function attachExporter(\n outboxDbPath: string, connection: HookConnection, store: ReceiptStore, fetchImpl?: typeof fetch,\n): { store: ReceiptStore; exporter: CloudExporter; outbox: SqliteCloudOutbox } {\n // Lossless (SB275): no cap and no expiry. A record leaves the queue only when the workspace\n // accepts it, or when a key change makes it undeliverable (`recover` then sends it). The\n // gateway's defaults (10,000 records, 64 MiB, 7 days) dropped the newest records once a long\n // outage filled the queue.\n const outbox = new SqliteCloudOutbox(outboxDbPath, LOSSLESS_OUTBOX);\n const exporter = createCloudExporter({ url: ingestUrl(connection), credential: connection.credential, outbox, fetch: fetchImpl });\n return { store: withCloudExporter(store, exporter), exporter, outbox };\n}\n\n/** Attempt delivery with a bounded timeout so a per-invocation hook never hangs the\n * agent; undelivered receipts stay in the durable outbox and flush next time. */\nexport async function flushBounded(exporter: CloudExporter, timeoutMs = 3000): Promise<void> {\n await Promise.race([\n exporter.flush().catch(() => {}),\n new Promise<void>((resolve) => setTimeout(resolve, Math.max(0, timeoutMs)).unref?.()),\n ]);\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HookConnection } from "./cloud.js";
|
|
2
|
+
export declare const OUTBOX_FILE = "receipts.db.cloud-outbox.db";
|
|
3
|
+
/** The queue opened without limits, so reading it never expires or drops anything. */
|
|
4
|
+
export declare const LOSSLESS_OUTBOX: {
|
|
5
|
+
readonly maxPending: number;
|
|
6
|
+
readonly maxBytes: number;
|
|
7
|
+
readonly maxAgeMs: number;
|
|
8
|
+
};
|
|
9
|
+
export interface DeliveryReport {
|
|
10
|
+
lines: string[];
|
|
11
|
+
problems: string[];
|
|
12
|
+
/** The one command that fixes a refused connection, when the connection was refused. */
|
|
13
|
+
fix: string | null;
|
|
14
|
+
}
|
|
15
|
+
export declare function describeDelivery(dir: string, connection: Pick<HookConnection, "url">, now?: number): DeliveryReport;
|
|
16
|
+
//# sourceMappingURL=delivery-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-report.d.ts","sourceRoot":"","sources":["../src/delivery-report.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIjD,eAAO,MAAM,WAAW,gCAAgC,CAAC;AAEzD,sFAAsF;AACtF,eAAO,MAAM,eAAe;;;;CAAyH,CAAC;AAEtJ,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wFAAwF;IACxF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,EAAE,GAAG,SAAa,GAAG,cAAc,CAmCvH"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SB273/SB274: what delivery to the workspace looks like from this computer — the persisted
|
|
2
|
+
// delivery state plus the queue — in the lines `status` prints and the problems `doctor` reports.
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { SqliteCloudOutbox } from "@scopebond/gateway/node";
|
|
6
|
+
import { ago, readDeliveryState } from "./delivery-state.js";
|
|
7
|
+
import { cliCommand } from "./version.js";
|
|
8
|
+
export const OUTBOX_FILE = "receipts.db.cloud-outbox.db";
|
|
9
|
+
/** The queue opened without limits, so reading it never expires or drops anything. */
|
|
10
|
+
export const LOSSLESS_OUTBOX = { maxPending: Number.MAX_SAFE_INTEGER, maxBytes: Number.MAX_SAFE_INTEGER, maxAgeMs: Number.MAX_SAFE_INTEGER };
|
|
11
|
+
export function describeDelivery(dir, connection, now = Date.now()) {
|
|
12
|
+
const state = readDeliveryState(dir);
|
|
13
|
+
const lines = [];
|
|
14
|
+
const problems = [];
|
|
15
|
+
let pending = 0;
|
|
16
|
+
let oldest = null;
|
|
17
|
+
const outboxPath = join(dir, OUTBOX_FILE);
|
|
18
|
+
if (existsSync(outboxPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const outbox = new SqliteCloudOutbox(outboxPath, LOSSLESS_OUTBOX);
|
|
21
|
+
try {
|
|
22
|
+
const status = outbox.status();
|
|
23
|
+
pending = status.pending;
|
|
24
|
+
oldest = status.oldestEnqueuedAt;
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
outbox.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch { /* an unreadable queue leaves the count at zero */ }
|
|
31
|
+
}
|
|
32
|
+
let fix = null;
|
|
33
|
+
if (state.invalid_since !== null) {
|
|
34
|
+
fix = cliCommand(`login ${connection.url}`);
|
|
35
|
+
lines.push(`NOT DELIVERING since ${new Date(state.invalid_since).toISOString()}: the workspace refused this computer's connection (it was revoked, replaced or removed).`);
|
|
36
|
+
lines.push(`fix: run ${fix}`);
|
|
37
|
+
problems.push(`the workspace no longer accepts this computer's connection; run ${fix}`);
|
|
38
|
+
}
|
|
39
|
+
const tried = state.last_attempt_at !== null ? `; last tried ${ago(state.last_attempt_at, now)}` : "";
|
|
40
|
+
lines.push(`last delivered ${ago(state.last_success_at, now)}${tried}`);
|
|
41
|
+
const age = oldest !== null ? `, oldest from ${ago(oldest, now)}` : "";
|
|
42
|
+
lines.push(`waiting to send ${pending} record(s)${age}`);
|
|
43
|
+
if (state.last_error && state.invalid_since === null) {
|
|
44
|
+
lines.push(`last problem ${state.last_error}`);
|
|
45
|
+
if (pending > 0 && oldest !== null && now - oldest > 60 * 60 * 1000) {
|
|
46
|
+
problems.push(`${pending} record(s) have waited more than an hour to send (${state.last_error}); they stay queued and send once the workspace accepts them`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { lines, problems, fix };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=delivery-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-report.js","sourceRoot":"","sources":["../src/delivery-report.ts"],"names":[],"mappings":"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","sourcesContent":["// SB273/SB274: what delivery to the workspace looks like from this computer — the persisted\n// delivery state plus the queue — in the lines `status` prints and the problems `doctor` reports.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { SqliteCloudOutbox } from \"@scopebond/gateway/node\";\nimport type { HookConnection } from \"./cloud.js\";\nimport { ago, readDeliveryState } from \"./delivery-state.js\";\nimport { cliCommand } from \"./version.js\";\n\nexport const OUTBOX_FILE = \"receipts.db.cloud-outbox.db\";\n\n/** The queue opened without limits, so reading it never expires or drops anything. */\nexport const LOSSLESS_OUTBOX = { maxPending: Number.MAX_SAFE_INTEGER, maxBytes: Number.MAX_SAFE_INTEGER, maxAgeMs: Number.MAX_SAFE_INTEGER } as const;\n\nexport interface DeliveryReport {\n lines: string[];\n problems: string[];\n /** The one command that fixes a refused connection, when the connection was refused. */\n fix: string | null;\n}\n\nexport function describeDelivery(dir: string, connection: Pick<HookConnection, \"url\">, now = Date.now()): DeliveryReport {\n const state = readDeliveryState(dir);\n const lines: string[] = [];\n const problems: string[] = [];\n let pending = 0;\n let oldest: number | null = null;\n const outboxPath = join(dir, OUTBOX_FILE);\n if (existsSync(outboxPath)) {\n try {\n const outbox = new SqliteCloudOutbox(outboxPath, LOSSLESS_OUTBOX);\n try {\n const status = outbox.status();\n pending = status.pending;\n oldest = status.oldestEnqueuedAt;\n } finally { outbox.close(); }\n } catch { /* an unreadable queue leaves the count at zero */ }\n }\n let fix: string | null = null;\n if (state.invalid_since !== null) {\n fix = cliCommand(`login ${connection.url}`);\n lines.push(`NOT DELIVERING since ${new Date(state.invalid_since).toISOString()}: the workspace refused this computer's connection (it was revoked, replaced or removed).`);\n lines.push(`fix: run ${fix}`);\n problems.push(`the workspace no longer accepts this computer's connection; run ${fix}`);\n }\n const tried = state.last_attempt_at !== null ? `; last tried ${ago(state.last_attempt_at, now)}` : \"\";\n lines.push(`last delivered ${ago(state.last_success_at, now)}${tried}`);\n const age = oldest !== null ? `, oldest from ${ago(oldest, now)}` : \"\";\n lines.push(`waiting to send ${pending} record(s)${age}`);\n if (state.last_error && state.invalid_since === null) {\n lines.push(`last problem ${state.last_error}`);\n if (pending > 0 && oldest !== null && now - oldest > 60 * 60 * 1000) {\n problems.push(`${pending} record(s) have waited more than an hour to send (${state.last_error}); they stay queued and send once the workspace accepts them`);\n }\n }\n return { lines, problems, fix };\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CloudExporterStatus } from "@scopebond/gateway";
|
|
2
|
+
export declare const DELIVERY_STATE_FILE = "delivery.json";
|
|
3
|
+
export interface DeliveryState {
|
|
4
|
+
/** When this computer last tried to deliver records (ms since epoch). */
|
|
5
|
+
last_attempt_at: number | null;
|
|
6
|
+
/** When the workspace last accepted records from this computer. */
|
|
7
|
+
last_success_at: number | null;
|
|
8
|
+
/** The last delivery failure, in plain words, and its HTTP status when there was one. */
|
|
9
|
+
last_error: string | null;
|
|
10
|
+
last_status: number | null;
|
|
11
|
+
/** Set when the workspace refused this computer's credential (HTTP 401): the connection
|
|
12
|
+
* was revoked, replaced or removed, and only signing in again fixes it. */
|
|
13
|
+
invalid_since: number | null;
|
|
14
|
+
/** What saw the refusal: record delivery or the rules check. */
|
|
15
|
+
invalid_source: "delivery" | "rules" | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function readDeliveryState(dir: string): DeliveryState;
|
|
18
|
+
/** Merge and save. Never throws: delivery state is diagnostic and must not affect a decision. */
|
|
19
|
+
export declare function writeDeliveryState(dir: string, patch: Partial<DeliveryState>): DeliveryState;
|
|
20
|
+
/** The HTTP status in an exporter error ("ingest failed: HTTP 401"), if any. */
|
|
21
|
+
export declare function httpStatusOf(message: string | null | undefined): number | null;
|
|
22
|
+
/** Record the outcome of one delivery attempt from the exporter's in-process status.
|
|
23
|
+
* `before` is the exporter's last success time before this attempt, so an attempt with
|
|
24
|
+
* nothing to send does not count as a delivery. */
|
|
25
|
+
export declare function recordDeliveryAttempt(dir: string, status: Pick<CloudExporterStatus, "lastSuccessAt" | "lastError" | "pending">, at: number, before?: number | null): DeliveryState;
|
|
26
|
+
/** The rules check saw a 401 (or a working connection again). */
|
|
27
|
+
export declare function recordRulesCredential(dir: string, valid: boolean, at: number): void;
|
|
28
|
+
/** "3 minutes ago", "2 hours ago", "4 days ago". */
|
|
29
|
+
export declare function ago(ms: number | null, now?: number): string;
|
|
30
|
+
//# sourceMappingURL=delivery-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-state.d.ts","sourceRoot":"","sources":["../src/delivery-state.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mEAAmE;IACnE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yFAAyF;IACzF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;gFAC4E;IAC5E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gEAAgE;IAChE,cAAc,EAAE,UAAU,GAAG,OAAO,GAAG,IAAI,CAAC;CAC7C;AAMD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAO5D;AAED,iGAAiG;AACjG,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAS5F;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAG9E;AAED;;oDAEoD;AACpD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,eAAe,GAAG,WAAW,GAAG,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,GAAG,IAAW,GAAG,aAAa,CAgBxL;AAED,iEAAiE;AACjE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAOnF;AAED,oDAAoD;AACpD,wBAAgB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CASvE"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Delivery state that outlives the process. The hook runs once per tool call, so the
|
|
2
|
+
// exporter's own counters (last success, last error) vanish when it exits; without this
|
|
3
|
+
// file `status` could only say "connected" while every upload was being refused. Written
|
|
4
|
+
// after each delivery attempt and by policy sync; read by `status` and `doctor`.
|
|
5
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
export const DELIVERY_STATE_FILE = "delivery.json";
|
|
8
|
+
const EMPTY = {
|
|
9
|
+
last_attempt_at: null, last_success_at: null, last_error: null, last_status: null, invalid_since: null, invalid_source: null,
|
|
10
|
+
};
|
|
11
|
+
export function readDeliveryState(dir) {
|
|
12
|
+
const file = join(dir, DELIVERY_STATE_FILE);
|
|
13
|
+
if (!existsSync(file))
|
|
14
|
+
return { ...EMPTY };
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
17
|
+
return { ...EMPTY, ...parsed };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { ...EMPTY };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Merge and save. Never throws: delivery state is diagnostic and must not affect a decision. */
|
|
24
|
+
export function writeDeliveryState(dir, patch) {
|
|
25
|
+
const next = { ...readDeliveryState(dir), ...patch };
|
|
26
|
+
try {
|
|
27
|
+
const file = join(dir, DELIVERY_STATE_FILE);
|
|
28
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
29
|
+
writeFileSync(temp, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
|
|
30
|
+
renameSync(temp, file);
|
|
31
|
+
}
|
|
32
|
+
catch { /* best effort */ }
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
/** The HTTP status in an exporter error ("ingest failed: HTTP 401"), if any. */
|
|
36
|
+
export function httpStatusOf(message) {
|
|
37
|
+
const match = /HTTP (\d{3})/.exec(message ?? "");
|
|
38
|
+
return match ? Number(match[1]) : null;
|
|
39
|
+
}
|
|
40
|
+
/** Record the outcome of one delivery attempt from the exporter's in-process status.
|
|
41
|
+
* `before` is the exporter's last success time before this attempt, so an attempt with
|
|
42
|
+
* nothing to send does not count as a delivery. */
|
|
43
|
+
export function recordDeliveryAttempt(dir, status, at, before = null) {
|
|
44
|
+
const patch = { last_attempt_at: at };
|
|
45
|
+
if (status.lastError) {
|
|
46
|
+
const code = httpStatusOf(status.lastError);
|
|
47
|
+
patch.last_error = status.lastError;
|
|
48
|
+
patch.last_status = code;
|
|
49
|
+
if (code === 401) {
|
|
50
|
+
const current = readDeliveryState(dir);
|
|
51
|
+
patch.invalid_since = current.invalid_since ?? at;
|
|
52
|
+
patch.invalid_source = current.invalid_source ?? "delivery";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (status.lastSuccessAt !== null && status.lastSuccessAt !== before) {
|
|
56
|
+
// Accepted: the connection works, whatever an earlier run saw.
|
|
57
|
+
Object.assign(patch, { last_success_at: status.lastSuccessAt, last_error: null, last_status: null, invalid_since: null, invalid_source: null });
|
|
58
|
+
}
|
|
59
|
+
return writeDeliveryState(dir, patch);
|
|
60
|
+
}
|
|
61
|
+
/** The rules check saw a 401 (or a working connection again). */
|
|
62
|
+
export function recordRulesCredential(dir, valid, at) {
|
|
63
|
+
const current = readDeliveryState(dir);
|
|
64
|
+
if (valid) {
|
|
65
|
+
if (current.invalid_since !== null && current.invalid_source === "rules")
|
|
66
|
+
writeDeliveryState(dir, { invalid_since: null, invalid_source: null });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
writeDeliveryState(dir, { invalid_since: current.invalid_since ?? at, invalid_source: current.invalid_source ?? "rules" });
|
|
70
|
+
}
|
|
71
|
+
/** "3 minutes ago", "2 hours ago", "4 days ago". */
|
|
72
|
+
export function ago(ms, now = Date.now()) {
|
|
73
|
+
if (ms === null)
|
|
74
|
+
return "never";
|
|
75
|
+
const seconds = Math.max(0, Math.round((now - ms) / 1000));
|
|
76
|
+
if (seconds < 90)
|
|
77
|
+
return `${seconds} second${seconds === 1 ? "" : "s"} ago`;
|
|
78
|
+
const minutes = Math.round(seconds / 60);
|
|
79
|
+
if (minutes < 90)
|
|
80
|
+
return `${minutes} minutes ago`;
|
|
81
|
+
const hours = Math.round(minutes / 60);
|
|
82
|
+
if (hours < 36)
|
|
83
|
+
return `${hours} hours ago`;
|
|
84
|
+
return `${Math.round(hours / 24)} days ago`;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=delivery-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-state.js","sourceRoot":"","sources":["../src/delivery-state.ts"],"names":[],"mappings":"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","sourcesContent":["// Delivery state that outlives the process. The hook runs once per tool call, so the\n// exporter's own counters (last success, last error) vanish when it exits; without this\n// file `status` could only say \"connected\" while every upload was being refused. Written\n// after each delivery attempt and by policy sync; read by `status` and `doctor`.\n\nimport { existsSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { CloudExporterStatus } from \"@scopebond/gateway\";\n\nexport const DELIVERY_STATE_FILE = \"delivery.json\";\n\nexport interface DeliveryState {\n /** When this computer last tried to deliver records (ms since epoch). */\n last_attempt_at: number | null;\n /** When the workspace last accepted records from this computer. */\n last_success_at: number | null;\n /** The last delivery failure, in plain words, and its HTTP status when there was one. */\n last_error: string | null;\n last_status: number | null;\n /** Set when the workspace refused this computer's credential (HTTP 401): the connection\n * was revoked, replaced or removed, and only signing in again fixes it. */\n invalid_since: number | null;\n /** What saw the refusal: record delivery or the rules check. */\n invalid_source: \"delivery\" | \"rules\" | null;\n}\n\nconst EMPTY: DeliveryState = {\n last_attempt_at: null, last_success_at: null, last_error: null, last_status: null, invalid_since: null, invalid_source: null,\n};\n\nexport function readDeliveryState(dir: string): DeliveryState {\n const file = join(dir, DELIVERY_STATE_FILE);\n if (!existsSync(file)) return { ...EMPTY };\n try {\n const parsed = JSON.parse(readFileSync(file, \"utf8\")) as Partial<DeliveryState>;\n return { ...EMPTY, ...parsed };\n } catch { return { ...EMPTY }; }\n}\n\n/** Merge and save. Never throws: delivery state is diagnostic and must not affect a decision. */\nexport function writeDeliveryState(dir: string, patch: Partial<DeliveryState>): DeliveryState {\n const next = { ...readDeliveryState(dir), ...patch };\n try {\n const file = join(dir, DELIVERY_STATE_FILE);\n const temp = `${file}.${process.pid}.tmp`;\n writeFileSync(temp, JSON.stringify(next, null, 2) + \"\\n\", { mode: 0o600 });\n renameSync(temp, file);\n } catch { /* best effort */ }\n return next;\n}\n\n/** The HTTP status in an exporter error (\"ingest failed: HTTP 401\"), if any. */\nexport function httpStatusOf(message: string | null | undefined): number | null {\n const match = /HTTP (\\d{3})/.exec(message ?? \"\");\n return match ? Number(match[1]) : null;\n}\n\n/** Record the outcome of one delivery attempt from the exporter's in-process status.\n * `before` is the exporter's last success time before this attempt, so an attempt with\n * nothing to send does not count as a delivery. */\nexport function recordDeliveryAttempt(dir: string, status: Pick<CloudExporterStatus, \"lastSuccessAt\" | \"lastError\" | \"pending\">, at: number, before: number | null = null): DeliveryState {\n const patch: Partial<DeliveryState> = { last_attempt_at: at };\n if (status.lastError) {\n const code = httpStatusOf(status.lastError);\n patch.last_error = status.lastError;\n patch.last_status = code;\n if (code === 401) {\n const current = readDeliveryState(dir);\n patch.invalid_since = current.invalid_since ?? at;\n patch.invalid_source = current.invalid_source ?? \"delivery\";\n }\n } else if (status.lastSuccessAt !== null && status.lastSuccessAt !== before) {\n // Accepted: the connection works, whatever an earlier run saw.\n Object.assign(patch, { last_success_at: status.lastSuccessAt, last_error: null, last_status: null, invalid_since: null, invalid_source: null });\n }\n return writeDeliveryState(dir, patch);\n}\n\n/** The rules check saw a 401 (or a working connection again). */\nexport function recordRulesCredential(dir: string, valid: boolean, at: number): void {\n const current = readDeliveryState(dir);\n if (valid) {\n if (current.invalid_since !== null && current.invalid_source === \"rules\") writeDeliveryState(dir, { invalid_since: null, invalid_source: null });\n return;\n }\n writeDeliveryState(dir, { invalid_since: current.invalid_since ?? at, invalid_source: current.invalid_source ?? \"rules\" });\n}\n\n/** \"3 minutes ago\", \"2 hours ago\", \"4 days ago\". */\nexport function ago(ms: number | null, now: number = Date.now()): string {\n if (ms === null) return \"never\";\n const seconds = Math.max(0, Math.round((now - ms) / 1000));\n if (seconds < 90) return `${seconds} second${seconds === 1 ? \"\" : \"s\"} ago`;\n const minutes = Math.round(seconds / 60);\n if (minutes < 90) return `${minutes} minutes ago`;\n const hours = Math.round(minutes / 60);\n if (hours < 36) return `${hours} hours ago`;\n return `${Math.round(hours / 24)} days ago`;\n}\n"]}
|
package/dist/install.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD,gFAAgF;AAChF,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;qEACqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAIxD;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAIxF;AAED;;;mDAGmD;AACnD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAE7F;AAED;;;;2EAI2E;AAC3E,MAAM,WAAW,aAAa;IAAG,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AAEpG,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,aAAa,CAS1F;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAW/D;AAED;iFACiF;AACjF,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAIjE;AAED;;;;;mDAKmD;AACnD,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;AAEpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAepD;AAED;;0BAE0B;AAC1B,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAkBpD;AAED;4DAC4D;AAC5D,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,GAAG,MAAM,CAqB9F;AAED;;sCAEsC;AACtC,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAYD;uFACuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED;;;;;iFAKiF;AACjF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAavE;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIpF;AAyBD,eAAO,MAAM,sBAAsB,GAAI,KAAK,OAAO,KAAG,OAgBrD,CAAC;AAGF;kDACkD;AAClD,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,KAAG,OACkF,CAAC;AAGpI;;iFAEiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAED;;;;;;;4EAO4E;AAC5E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED;4FAC4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
package/dist/install.js
CHANGED
|
@@ -318,8 +318,7 @@ export function backupHarnessConfig(file) {
|
|
|
318
318
|
* Shared by the project installer (npx command) and the user installer (absolute path). */
|
|
319
319
|
export function writeHarnessConfig(file, harness, command) {
|
|
320
320
|
const config = readHarnessConfig(file);
|
|
321
|
-
|
|
322
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
321
|
+
const before = existsSync(file) ? JSON.stringify(config) : null;
|
|
323
322
|
const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});
|
|
324
323
|
if (harness === "cursor") {
|
|
325
324
|
config.version = config.version ?? 1;
|
|
@@ -346,6 +345,13 @@ export function writeHarnessConfig(file, harness, command) {
|
|
|
346
345
|
else
|
|
347
346
|
list.push(entry);
|
|
348
347
|
}
|
|
348
|
+
// SB276: a settings file that already holds exactly this entry is left byte-for-byte
|
|
349
|
+
// untouched — no rewrite, no backup. A running agent session watches this file, and
|
|
350
|
+
// rewriting it (even with the same content) is what disturbed a live session on re-login.
|
|
351
|
+
if (before !== null && JSON.stringify(config) === before)
|
|
352
|
+
return file;
|
|
353
|
+
backupHarnessConfig(file);
|
|
354
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
349
355
|
writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
|
|
350
356
|
return file;
|
|
351
357
|
}
|
package/dist/install.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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","sourcesContent":["// SB112 — the user-level installer. One install per developer machine (not per repo):\n// keys and a starter policy live in a user-level home (~/.scopebond, override\n// SCOPEBOND_HOME), and the hook is registered by absolute path in the user-level agent\n// config (~/.claude/settings.json, ~/.cursor/hooks.json, ~/.codex/hooks.json). When a\n// user-level install exists, a project-local `.scopebond` is used only if the user\n// trusted that exact policy (`scopebond trust`, or `init` in the project): a cloned\n// repository, or an agent inside it, must not be able to swap in its own policy.\n\nimport { execFileSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, copyFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, resolve } from \"node:path\";\n\nexport type Harness = \"claude\" | \"cursor\" | \"codex\";\n\n/** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */\nexport function userHome(): string {\n return process.env.SCOPEBOND_HOME ?? join(homedir(), \".scopebond\");\n}\n\n/** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)\n * roots it; SCOPEBOND_HOME does not move the agent's own config. */\nexport function userHarnessFile(harness: Harness): string {\n if (harness === \"cursor\") return join(homedir(), \".cursor\", \"hooks.json\");\n if (harness === \"codex\") return join(homedir(), \".codex\", \"hooks.json\");\n return join(homedir(), \".claude\", \"settings.json\");\n}\n\n/** The project-level agent config file for a harness — what `init` writes, so the\n * hook travels with the repository rather than the machine. */\nexport function projectHarnessFile(harness: Harness, cwd: string = process.cwd()): string {\n if (harness === \"cursor\") return join(cwd, \".cursor\", \"hooks.json\");\n if (harness === \"codex\") return join(cwd, \".codex\", \"hooks.json\");\n return join(cwd, \".claude\", \"settings.json\");\n}\n\n/** The personal, per-machine project config for a harness, or null when the harness\n * has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and\n * merges the hooks of both; it is the place for anything that names a path on this\n * machine. Cursor and Codex have no such file. */\nexport function localHarnessFile(harness: Harness, cwd: string = process.cwd()): string | null {\n return harness === \"claude\" ? join(cwd, \".claude\", \"settings.local.json\") : null;\n}\n\n/** Which config files actually carry a Scopebond hook for this harness. There are three\n * places it can live — the shared project config, the personal project config (Claude\n * Code's `settings.local.json`, where `init` pins the fast command) and the user config\n * `install` writes — and `status`/`doctor` must look at all of them: checking only the\n * user one told everyone who ran `init` that their install had failed. */\nexport interface HarnessScopes { project: string | null; local: string | null; user: string | null }\n\nexport function harnessScopes(harness: Harness, cwd: string = process.cwd()): HarnessScopes {\n const project = projectHarnessFile(harness, cwd);\n const local = localHarnessFile(harness, cwd);\n const user = userHarnessFile(harness);\n return {\n project: isHarnessConfigured(project) ? project : null,\n local: local && isHarnessConfigured(local) ? local : null,\n user: isHarnessConfigured(user) ? user : null,\n };\n}\n\n/** One word for where a harness is wired, for a status line. */\nexport function harnessScopeLabel(scopes: HarnessScopes): string {\n const where = [\n scopes.project ? \"project\" : \"\",\n scopes.local ? \"this machine\" : \"\",\n scopes.user ? \"user\" : \"\",\n ].filter(Boolean);\n if (where.length === 0) return \"\";\n if (where.length === 1 && scopes.project) return \"configured (this project)\";\n if (where.length === 1 && scopes.local) return \"configured (this project, this machine only)\";\n if (where.length === 1 && scopes.user) return \"configured (user-level)\";\n return `configured (${where.join(\" + \")})`;\n}\n\n/** Whether a hook command names a path that exists only on this machine — the pinned\n * `\"<node>\" \"<…/cli.js>\" claude` form — rather than the portable `npx` form. */\nexport function isMachineSpecificCommand(command: string): boolean {\n if (/(^|\\s)npx(\\.cmd)?\\s/.test(command)) return false;\n const first = (command.match(/\"[^\"]+\"|\\S+/) ?? [\"\"])[0].replace(/^\"|\"$/g, \"\");\n return isAbsolute(first) || /^[A-Za-z]:[\\\\/]/.test(first);\n}\n\n/** How git treats a config file: committed (`tracked`), kept out (`ignored`), neither\n * yet (`untracked`, so a `git add .` would commit it), or not in a repository / no git\n * on this machine (`none`). A file git shares must never carry a machine-specific\n * command: on a teammate's machine the path does not exist, the command cannot start,\n * and Claude Code, Cursor and Codex all treat a hook that cannot start as a non-blocking\n * error — the agent runs with no check at all. */\nexport type GitShare = \"tracked\" | \"ignored\" | \"untracked\" | \"none\";\n\nexport function gitShareState(file: string): GitShare {\n const cwd = dirname(file);\n const git = (args: string[]): number => {\n try {\n mkdirSync(cwd, { recursive: true });\n execFileSync(\"git\", args, { cwd, stdio: \"ignore\", timeout: 5000 });\n return 0;\n } catch (error) {\n const status = (error as { status?: number | null }).status;\n return typeof status === \"number\" ? status : -1;\n }\n };\n if (git([\"rev-parse\", \"--is-inside-work-tree\"]) !== 0) return \"none\";\n if (git([\"ls-files\", \"--error-unmatch\", \"--\", file]) === 0) return \"tracked\";\n return git([\"check-ignore\", \"-q\", \"--\", file]) === 0 ? \"ignored\" : \"untracked\";\n}\n\n/** Keep a file out of git for this clone only, through `.git/info/exclude` — never the\n * repository's own `.gitignore`, which is the team's file. Returns true when the file is\n * ignored afterwards. */\nexport function excludeFromGit(file: string): boolean {\n try {\n const cwd = dirname(file);\n mkdirSync(cwd, { recursive: true });\n // The entry is built from git's own view of where this directory sits in the work\n // tree (`--show-prefix`), not from comparing file-system paths: the same directory\n // can be spelled two ways (a Windows 8.3 short name, a symlinked temp dir), and a\n // path computed across the two spellings excludes the wrong file.\n const prefix = execFileSync(\"git\", [\"rev-parse\", \"--show-prefix\"], { cwd, encoding: \"utf8\", timeout: 5000 }).trim();\n const exclude = resolve(cwd, execFileSync(\"git\", [\"rev-parse\", \"--git-path\", \"info/exclude\"], { cwd, encoding: \"utf8\", timeout: 5000 }).trim());\n const entry = \"/\" + prefix + basename(file);\n const existing = existsSync(exclude) ? readFileSync(exclude, \"utf8\") : \"\";\n if (!existing.split(/\\r?\\n/).includes(entry)) {\n mkdirSync(dirname(exclude), { recursive: true });\n writeFileSync(exclude, `${existing}${existing && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\"}${entry}\\n`);\n }\n return gitShareState(file) === \"ignored\";\n } catch { return false; }\n}\n\n/** Remove the Scopebond entries whose command `remove` selects, leaving every other\n * setting alone. Returns the number of entries removed. */\nexport function pruneHarnessEntries(file: string, remove: (command: string) => boolean): number {\n if (!existsSync(file)) return 0;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return 0; }\n const hooks = isRecord(config.hooks) ? config.hooks : null;\n if (!hooks) return 0;\n let removed = 0;\n const commandsOf = (e: unknown): string[] => {\n if (!isRecord(e)) return [];\n const own = isScopebond(e.command) ? [String(e.command)] : [];\n const nested = Array.isArray(e.hooks) ? e.hooks.flatMap(commandsOf) : [];\n return [...own, ...nested];\n };\n for (const [event, value] of Object.entries(hooks)) {\n if (!Array.isArray(value)) continue;\n const kept = value.filter((e) => !(entryMatches(e) && commandsOf(e).some(remove)));\n removed += value.length - kept.length;\n (hooks as Record<string, unknown[]>)[event] = kept;\n }\n if (removed > 0) writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return removed;\n}\n\n/** The file in the user home that pins trusted project policies: absolute project\n * config dir → SHA-256 of its policy.json. It lives under the protected home, so the\n * governed agent cannot write it. */\nexport function trustedProjectsFile(): string {\n return join(userHome(), \"trusted-projects.json\");\n}\n\nfunction policyDigest(dir: string): string | null {\n try { return createHash(\"sha256\").update(readFileSync(join(dir, \"policy.json\"))).digest(\"hex\"); } catch { return null; }\n}\n\nfunction readTrusted(): Record<string, string> {\n try { const p = JSON.parse(readFileSync(trustedProjectsFile(), \"utf8\")); return isRecord(p) ? (p as Record<string, string>) : {}; } catch { return {}; }\n}\n\nconst trustKey = (dir: string): string => resolve(dir).replace(/\\\\/g, \"/\").toLowerCase();\n\n/** Pin the project's current policy as trusted. Editing the policy afterwards un-trusts\n * it until the user trusts it again, so a rewrite by the agent never takes effect. */\nexport function trustProjectPolicy(dir: string): string {\n const digest = policyDigest(dir);\n if (!digest) throw new Error(`no policy at ${join(dir, \"policy.json\")}`);\n const trusted = readTrusted();\n trusted[trustKey(dir)] = digest;\n mkdirSync(userHome(), { recursive: true });\n writeFileSync(trustedProjectsFile(), JSON.stringify(trusted, null, 2) + \"\\n\");\n return digest;\n}\n\n/** Whether the project's policy is exactly the one the user trusted. */\nexport function isTrustedProject(dir: string): boolean {\n const digest = policyDigest(dir);\n return !!digest && readTrusted()[trustKey(dir)] === digest;\n}\n\n/** Resolve the config dir for a hook event, most specific first:\n * explicit override → the payload's project → $CLAUDE_PROJECT_DIR → the user home.\n * A project dir counts only when it has been scaffolded (a policy.json), so a bare\n * working directory never shadows the user-level install — and, once a user-level\n * install exists, only when its policy is trusted (`isTrustedProject`). Otherwise the\n * user's own policy governs: a repository cannot bring a weaker one with it. */\nexport function resolveConfigDir(payloadCwd: string | undefined): string {\n if (process.env.SCOPEBOND_HOOK_DIR) return process.env.SCOPEBOND_HOOK_DIR;\n const candidates: string[] = [];\n if (payloadCwd) candidates.push(join(payloadCwd, \".scopebond\"));\n if (process.env.CLAUDE_PROJECT_DIR) candidates.push(join(process.env.CLAUDE_PROJECT_DIR, \".scopebond\"));\n const home = userHome();\n const homeInstalled = existsSync(join(home, \"policy.json\"));\n for (const dir of candidates) {\n if (existsSync(join(dir, \"policy.json\")) && (!homeInstalled || isTrustedProject(dir))) return dir;\n }\n if (homeInstalled) return home;\n // Nothing scaffolded yet: prefer the payload project, else the user home.\n return candidates[0] ?? home;\n}\n\n/** A project policy present but ignored because it is not trusted (for status/doctor). */\nexport function untrustedProjectPolicy(payloadCwd: string | undefined): string | null {\n if (process.env.SCOPEBOND_HOOK_DIR || !existsSync(join(userHome(), \"policy.json\"))) return null;\n const dirs = [payloadCwd, process.env.CLAUDE_PROJECT_DIR].filter(Boolean).map((d) => join(d as string, \".scopebond\"));\n return dirs.find((d) => existsSync(join(d, \"policy.json\")) && !isTrustedProject(d)) ?? null;\n}\n\nconst isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** Whether a harness command string is one of ours. This is the single matcher for\n * every form the installer has ever written, and it is what makes re-running `init`\n * or `install` replace the entry instead of appending a second one:\n * - `npx -y @scopebond/hook@0.6.0 claude` (portable)\n * - `\"<node>\" \"<…/@scopebond/hook/dist/cli.js>\" claude` (pinned, POSIX)\n * - `\"<node>\" \"<…\\@scopebond\\hook\\dist\\cli.js>\" claude` (pinned, Windows)\n * - `scopebond-hook claude` (legacy global binary)\n * The separator class matters: a Windows pinned path spells the scope\n * `@scopebond\\hook`, and matching only `@scopebond/hook` let a second `init`\n * install a duplicate hook — which would double-check every tool call.\n *\n * Written with substring and token tests rather than one regular expression: the\n * previous `(^|[\"\\s])scopebond([\"'\\s]).*\\b(claude|cursor|codex)\\b` backtracked\n * polynomially on a long run of `\\tscopebond\\t` (CodeQL js/polynomial-redos). The\n * input is a command out of a config file rather than a request, so this was not\n * reachable by an attacker — but a linear check is also the clearer one. */\nconst HARNESS_SUBCOMMANDS = new Set([\"claude\", \"cursor\", \"codex\"]);\n/** Long enough for any real command; a bound keeps the work linear and small. */\nconst MAX_COMMAND = 4096;\nconst unquote = (token: string): string => token.replace(/^[\"']+|[\"']+$/g, \"\");\n\nexport const isScopebondHookCommand = (cmd: unknown): boolean => {\n if (typeof cmd !== \"string\" || cmd.length === 0 || cmd.length > MAX_COMMAND) return false;\n const lower = cmd.toLowerCase();\n // The package itself, in either path spelling (`@scopebond/hook`, `@scopebond\\hook`).\n if (lower.includes(\"@scopebond/hook\") || lower.includes(\"@scopebond\\\\hook\")) return true;\n // The legacy global binary, as a whole token.\n if (/(^|[\\s\"'])scopebond-hook([\\s\"']|$)/.test(cmd)) return true;\n // An absolute or bare form whose last argument is one of our harness subcommands:\n // `<node> <…/cli.js> claude`, `scopebond cursor`.\n const tokens = cmd.split(/\\s+/).filter(Boolean);\n const last = unquote(tokens[tokens.length - 1] ?? \"\");\n if (!HARNESS_SUBCOMMANDS.has(last)) return false;\n return tokens.some((token) => {\n const t = unquote(token).toLowerCase();\n return t.includes(\"scopebond\") || /cli\\.(m|c)?js$/.test(t);\n });\n};\nconst isScopebond = isScopebondHookCommand;\n\n/** Whether a harness hook entry (either shape: a bare command, or a group with a\n * nested `hooks` array) is a Scopebond entry. */\nexport const harnessEntryMatches = (e: unknown): boolean =>\n isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));\nconst entryMatches = harnessEntryMatches;\n\n/** Read an agent's JSON config for merging. A missing file is an empty config; a file\n * that exists but does not parse as a JSON object is an error — rewriting it would\n * silently delete the user's other settings, so the installer stops instead. */\nexport function readHarnessConfig(file: string): Record<string, unknown> {\n if (!existsSync(file)) return {};\n const text = readFileSync(file, \"utf8\");\n if (!text.trim()) return {};\n let parsed: unknown;\n try { parsed = JSON.parse(text); } catch (error) {\n throw new Error(`${file} is not valid JSON (${(error as Error).message}); fix or move it, then run this again — it was left unchanged`);\n }\n if (!isRecord(parsed)) throw new Error(`${file} is not a JSON object; fix or move it, then run this again — it was left unchanged`);\n return parsed;\n}\n\n/** Copy an agent config aside before changing it, once. Re-running an installer must not\n * overwrite the backup with the already-modified file, or the original is lost on the\n * second run — so an existing backup is left alone. Returns the backup path when one was\n * written.\n *\n * This exists because these installers rewrite files the user did not create and may not\n * know are involved (`~/.claude/settings.json` holds their theme, plugins and\n * permissions), and the undo was previously \"hope the merge was right\". */\nexport function backupHarnessConfig(file: string): string | null {\n if (!existsSync(file)) return null;\n const backup = `${file}.scopebond-backup`;\n if (existsSync(backup)) return null;\n try { copyFileSync(file, backup); return backup; } catch { return null; }\n}\n\n/** Write (idempotently) a harness hook entry pointing at an explicit command string.\n * Shared by the project installer (npx command) and the user installer (absolute path). */\nexport function writeHarnessConfig(file: string, harness: Harness, command: string): string {\n const config = readHarnessConfig(file);\n backupHarnessConfig(file);\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n if (harness === \"cursor\") {\n config.version = config.version ?? 1;\n for (const event of [\"beforeShellExecution\", \"beforeMCPExecution\", \"beforeReadFile\", \"afterFileEdit\"]) {\n const list = Array.isArray((hooks as Record<string, unknown>)[event]) ? (hooks as Record<string, unknown[]>)[event] : ((hooks as Record<string, unknown[]>)[event] = []);\n const at = list.findIndex(entryMatches);\n const entry = { command: command.replace(/\\bclaude$/, \"cursor\") };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n } else {\n const list = Array.isArray((hooks as Record<string, unknown>).PreToolUse) ? (hooks as Record<string, unknown[]>).PreToolUse : ((hooks as Record<string, unknown[]>).PreToolUse = []);\n const at = list.findIndex(entryMatches);\n // Codex matchers are regular expressions; omitting the matcher means every tool.\n // A literal \"*\" is not a valid regular expression and causes Codex to skip the group.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command }] };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** Remove any Scopebond hook entry from a user-level harness config, leaving the rest\n * intact. Returns true if the file existed. */\nexport function removeHarnessConfig(file: string): boolean {\n if (!existsSync(file)) return false;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return true; } // unreadable: leave it untouched\n const hooks = isRecord(config.hooks) ? config.hooks : {};\n for (const [event, value] of Object.entries(hooks)) {\n if (Array.isArray(value)) (hooks as Record<string, unknown[]>)[event] = value.filter((e) => !entryMatches(e));\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return true;\n}\n\n/** Detect a Cursor install by its user config directory. */\nexport function cursorDetected(): boolean {\n return existsSync(join(homedir(), \".cursor\"));\n}\n\n/** Detect a Codex install by its user config directory. */\nexport function codexDetected(): boolean {\n return existsSync(join(homedir(), \".codex\"));\n}\n\n/** The absolute command a harness entry runs: the current Node executable and the\n * absolute path to this CLI, so it does not depend on PATH.\n *\n * Both paths are always quoted on Windows, even without a space in them. A harness\n * runs this string through a shell, and that shell may be bash (Git Bash, WSL, a\n * dev container), where an unquoted Windows path loses every backslash and the\n * hook dies with MODULE_NOT_FOUND. Inside double quotes bash leaves a backslash\n * alone unless it precedes $ ` \" \\ or a newline — none of which occur in a Windows\n * path — and cmd.exe and PowerShell accept quoted paths too. */\nexport function absoluteHookCommand(cliPath: string, harness: Harness): string {\n const quote = (s: string) => (process.platform === \"win32\" || /\\s/.test(s) ? `\"${s}\"` : s);\n return `${quote(process.execPath)} ${quote(cliPath)} ${harness}`;\n}\n\nexport function isHarnessConfigured(file: string): boolean {\n if (!existsSync(file)) return false;\n try {\n const p = JSON.parse(readFileSync(file, \"utf8\"));\n if (!isRecord(p) || !isRecord(p.hooks)) return false;\n return Object.values(p.hooks).some((v) => Array.isArray(v) && v.some(entryMatches));\n } catch { return false; }\n}\n\n/** Every Scopebond command string a harness config currently runs. `doctor` uses it to\n * check that a pinned command still resolves: a hook entry that cannot start is worse\n * than none, because a harness can read the failure as \"no hook\". */\nexport function configuredHookCommands(file: string): string[] {\n if (!existsSync(file)) return [];\n const found: string[] = [];\n const collect = (entry: unknown): void => {\n if (!isRecord(entry)) return;\n if (isScopebond(entry.command)) found.push(String(entry.command));\n if (Array.isArray(entry.hooks)) for (const h of entry.hooks) collect(h);\n };\n try {\n const p = JSON.parse(readFileSync(file, \"utf8\"));\n if (!isRecord(p) || !isRecord(p.hooks)) return [];\n for (const list of Object.values(p.hooks)) if (Array.isArray(list)) for (const e of list) collect(e);\n } catch { return []; }\n return found;\n}\n\n/** Whether a configured hook command can actually start. An `npx` form always can\n * (npm resolves it); a pinned command names two absolute paths — the Node binary and\n * the CLI — and either can go away: a Node version manager switching versions, or the\n * Scopebond home being cleared. Both are exactly what this check exists to catch. */\nexport function hookCommandResolves(command: string): boolean {\n if (/(^|\\s)npx(\\.cmd)?\\s/.test(command)) return true;\n // `\"<node>\" \"<cli.js>\" claude`: check every token that names a file on disk.\n const tokens = command.match(/\"[^\"]+\"|\\S+/g) ?? [];\n let checked = 0;\n for (const raw of tokens) {\n const candidate = raw.replace(/^\"|\"$/g, \"\");\n if (!/\\.(m|c)?js$|\\.exe$/i.test(candidate)) continue;\n checked += 1;\n if (!existsSync(candidate)) return false;\n }\n // A bare `node cli.js` form on POSIX has no .exe to check; the .js still counted.\n return checked > 0 || !/[\\\\/]/.test(command.split(/\\s+/)[0] ?? \"\");\n}\n\n/** Remove the user-level home (keys, policy, receipts). */\nexport function purgeHome(): void {\n rmSync(userHome(), { recursive: true, force: true });\n}\n\n/** The Claude Code events the observation emitters listen to, beyond PreToolUse. */\nexport const LIFECYCLE_EVENTS = [\"SessionStart\", \"SessionEnd\", \"PostToolUse\", \"PostToolUseFailure\"] as const;\n\n/** Add (idempotently) the session lifecycle and after-action hook entries to a Claude Code\n * config file, running the same Scopebond command that already handles PreToolUse: the\n * command tells the events apart. Only Claude Code is wired: its lifecycle events are the\n * ones the hook's tested mapping covers. Nothing else in the file changes. */\nexport function wireLifecycleHooks(file: string, command: string): string {\n const config = readHarnessConfig(file);\n backupHarnessConfig(file);\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n for (const event of LIFECYCLE_EVENTS) {\n const list = Array.isArray((hooks as Record<string, unknown>)[event]) ? (hooks as Record<string, unknown[]>)[event] : ((hooks as Record<string, unknown[]>)[event] = []);\n const at = list.findIndex(entryMatches);\n const entry = event.startsWith(\"Session\")\n ? { hooks: [{ type: \"command\", command }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command }] };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** Remove only the lifecycle entries `wireLifecycleHooks` added, leaving PreToolUse and every\n * other setting alone. Returns the number removed. */\nexport function unwireLifecycleHooks(file: string): number {\n if (!existsSync(file)) return 0;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return 0; }\n const hooks = isRecord(config.hooks) ? config.hooks : null;\n if (!hooks) return 0;\n let removed = 0;\n for (const event of LIFECYCLE_EVENTS) {\n const value = (hooks as Record<string, unknown>)[event];\n if (!Array.isArray(value)) continue;\n const kept = value.filter((e) => !entryMatches(e));\n removed += value.length - kept.length;\n if (kept.length) (hooks as Record<string, unknown[]>)[event] = kept; else delete (hooks as Record<string, unknown>)[event];\n }\n if (removed > 0) writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return removed;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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","sourcesContent":["// SB112 — the user-level installer. One install per developer machine (not per repo):\n// keys and a starter policy live in a user-level home (~/.scopebond, override\n// SCOPEBOND_HOME), and the hook is registered by absolute path in the user-level agent\n// config (~/.claude/settings.json, ~/.cursor/hooks.json, ~/.codex/hooks.json). When a\n// user-level install exists, a project-local `.scopebond` is used only if the user\n// trusted that exact policy (`scopebond trust`, or `init` in the project): a cloned\n// repository, or an agent inside it, must not be able to swap in its own policy.\n\nimport { execFileSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, copyFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, resolve } from \"node:path\";\n\nexport type Harness = \"claude\" | \"cursor\" | \"codex\";\n\n/** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */\nexport function userHome(): string {\n return process.env.SCOPEBOND_HOME ?? join(homedir(), \".scopebond\");\n}\n\n/** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)\n * roots it; SCOPEBOND_HOME does not move the agent's own config. */\nexport function userHarnessFile(harness: Harness): string {\n if (harness === \"cursor\") return join(homedir(), \".cursor\", \"hooks.json\");\n if (harness === \"codex\") return join(homedir(), \".codex\", \"hooks.json\");\n return join(homedir(), \".claude\", \"settings.json\");\n}\n\n/** The project-level agent config file for a harness — what `init` writes, so the\n * hook travels with the repository rather than the machine. */\nexport function projectHarnessFile(harness: Harness, cwd: string = process.cwd()): string {\n if (harness === \"cursor\") return join(cwd, \".cursor\", \"hooks.json\");\n if (harness === \"codex\") return join(cwd, \".codex\", \"hooks.json\");\n return join(cwd, \".claude\", \"settings.json\");\n}\n\n/** The personal, per-machine project config for a harness, or null when the harness\n * has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and\n * merges the hooks of both; it is the place for anything that names a path on this\n * machine. Cursor and Codex have no such file. */\nexport function localHarnessFile(harness: Harness, cwd: string = process.cwd()): string | null {\n return harness === \"claude\" ? join(cwd, \".claude\", \"settings.local.json\") : null;\n}\n\n/** Which config files actually carry a Scopebond hook for this harness. There are three\n * places it can live — the shared project config, the personal project config (Claude\n * Code's `settings.local.json`, where `init` pins the fast command) and the user config\n * `install` writes — and `status`/`doctor` must look at all of them: checking only the\n * user one told everyone who ran `init` that their install had failed. */\nexport interface HarnessScopes { project: string | null; local: string | null; user: string | null }\n\nexport function harnessScopes(harness: Harness, cwd: string = process.cwd()): HarnessScopes {\n const project = projectHarnessFile(harness, cwd);\n const local = localHarnessFile(harness, cwd);\n const user = userHarnessFile(harness);\n return {\n project: isHarnessConfigured(project) ? project : null,\n local: local && isHarnessConfigured(local) ? local : null,\n user: isHarnessConfigured(user) ? user : null,\n };\n}\n\n/** One word for where a harness is wired, for a status line. */\nexport function harnessScopeLabel(scopes: HarnessScopes): string {\n const where = [\n scopes.project ? \"project\" : \"\",\n scopes.local ? \"this machine\" : \"\",\n scopes.user ? \"user\" : \"\",\n ].filter(Boolean);\n if (where.length === 0) return \"\";\n if (where.length === 1 && scopes.project) return \"configured (this project)\";\n if (where.length === 1 && scopes.local) return \"configured (this project, this machine only)\";\n if (where.length === 1 && scopes.user) return \"configured (user-level)\";\n return `configured (${where.join(\" + \")})`;\n}\n\n/** Whether a hook command names a path that exists only on this machine — the pinned\n * `\"<node>\" \"<…/cli.js>\" claude` form — rather than the portable `npx` form. */\nexport function isMachineSpecificCommand(command: string): boolean {\n if (/(^|\\s)npx(\\.cmd)?\\s/.test(command)) return false;\n const first = (command.match(/\"[^\"]+\"|\\S+/) ?? [\"\"])[0].replace(/^\"|\"$/g, \"\");\n return isAbsolute(first) || /^[A-Za-z]:[\\\\/]/.test(first);\n}\n\n/** How git treats a config file: committed (`tracked`), kept out (`ignored`), neither\n * yet (`untracked`, so a `git add .` would commit it), or not in a repository / no git\n * on this machine (`none`). A file git shares must never carry a machine-specific\n * command: on a teammate's machine the path does not exist, the command cannot start,\n * and Claude Code, Cursor and Codex all treat a hook that cannot start as a non-blocking\n * error — the agent runs with no check at all. */\nexport type GitShare = \"tracked\" | \"ignored\" | \"untracked\" | \"none\";\n\nexport function gitShareState(file: string): GitShare {\n const cwd = dirname(file);\n const git = (args: string[]): number => {\n try {\n mkdirSync(cwd, { recursive: true });\n execFileSync(\"git\", args, { cwd, stdio: \"ignore\", timeout: 5000 });\n return 0;\n } catch (error) {\n const status = (error as { status?: number | null }).status;\n return typeof status === \"number\" ? status : -1;\n }\n };\n if (git([\"rev-parse\", \"--is-inside-work-tree\"]) !== 0) return \"none\";\n if (git([\"ls-files\", \"--error-unmatch\", \"--\", file]) === 0) return \"tracked\";\n return git([\"check-ignore\", \"-q\", \"--\", file]) === 0 ? \"ignored\" : \"untracked\";\n}\n\n/** Keep a file out of git for this clone only, through `.git/info/exclude` — never the\n * repository's own `.gitignore`, which is the team's file. Returns true when the file is\n * ignored afterwards. */\nexport function excludeFromGit(file: string): boolean {\n try {\n const cwd = dirname(file);\n mkdirSync(cwd, { recursive: true });\n // The entry is built from git's own view of where this directory sits in the work\n // tree (`--show-prefix`), not from comparing file-system paths: the same directory\n // can be spelled two ways (a Windows 8.3 short name, a symlinked temp dir), and a\n // path computed across the two spellings excludes the wrong file.\n const prefix = execFileSync(\"git\", [\"rev-parse\", \"--show-prefix\"], { cwd, encoding: \"utf8\", timeout: 5000 }).trim();\n const exclude = resolve(cwd, execFileSync(\"git\", [\"rev-parse\", \"--git-path\", \"info/exclude\"], { cwd, encoding: \"utf8\", timeout: 5000 }).trim());\n const entry = \"/\" + prefix + basename(file);\n const existing = existsSync(exclude) ? readFileSync(exclude, \"utf8\") : \"\";\n if (!existing.split(/\\r?\\n/).includes(entry)) {\n mkdirSync(dirname(exclude), { recursive: true });\n writeFileSync(exclude, `${existing}${existing && !existing.endsWith(\"\\n\") ? \"\\n\" : \"\"}${entry}\\n`);\n }\n return gitShareState(file) === \"ignored\";\n } catch { return false; }\n}\n\n/** Remove the Scopebond entries whose command `remove` selects, leaving every other\n * setting alone. Returns the number of entries removed. */\nexport function pruneHarnessEntries(file: string, remove: (command: string) => boolean): number {\n if (!existsSync(file)) return 0;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return 0; }\n const hooks = isRecord(config.hooks) ? config.hooks : null;\n if (!hooks) return 0;\n let removed = 0;\n const commandsOf = (e: unknown): string[] => {\n if (!isRecord(e)) return [];\n const own = isScopebond(e.command) ? [String(e.command)] : [];\n const nested = Array.isArray(e.hooks) ? e.hooks.flatMap(commandsOf) : [];\n return [...own, ...nested];\n };\n for (const [event, value] of Object.entries(hooks)) {\n if (!Array.isArray(value)) continue;\n const kept = value.filter((e) => !(entryMatches(e) && commandsOf(e).some(remove)));\n removed += value.length - kept.length;\n (hooks as Record<string, unknown[]>)[event] = kept;\n }\n if (removed > 0) writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return removed;\n}\n\n/** The file in the user home that pins trusted project policies: absolute project\n * config dir → SHA-256 of its policy.json. It lives under the protected home, so the\n * governed agent cannot write it. */\nexport function trustedProjectsFile(): string {\n return join(userHome(), \"trusted-projects.json\");\n}\n\nfunction policyDigest(dir: string): string | null {\n try { return createHash(\"sha256\").update(readFileSync(join(dir, \"policy.json\"))).digest(\"hex\"); } catch { return null; }\n}\n\nfunction readTrusted(): Record<string, string> {\n try { const p = JSON.parse(readFileSync(trustedProjectsFile(), \"utf8\")); return isRecord(p) ? (p as Record<string, string>) : {}; } catch { return {}; }\n}\n\nconst trustKey = (dir: string): string => resolve(dir).replace(/\\\\/g, \"/\").toLowerCase();\n\n/** Pin the project's current policy as trusted. Editing the policy afterwards un-trusts\n * it until the user trusts it again, so a rewrite by the agent never takes effect. */\nexport function trustProjectPolicy(dir: string): string {\n const digest = policyDigest(dir);\n if (!digest) throw new Error(`no policy at ${join(dir, \"policy.json\")}`);\n const trusted = readTrusted();\n trusted[trustKey(dir)] = digest;\n mkdirSync(userHome(), { recursive: true });\n writeFileSync(trustedProjectsFile(), JSON.stringify(trusted, null, 2) + \"\\n\");\n return digest;\n}\n\n/** Whether the project's policy is exactly the one the user trusted. */\nexport function isTrustedProject(dir: string): boolean {\n const digest = policyDigest(dir);\n return !!digest && readTrusted()[trustKey(dir)] === digest;\n}\n\n/** Resolve the config dir for a hook event, most specific first:\n * explicit override → the payload's project → $CLAUDE_PROJECT_DIR → the user home.\n * A project dir counts only when it has been scaffolded (a policy.json), so a bare\n * working directory never shadows the user-level install — and, once a user-level\n * install exists, only when its policy is trusted (`isTrustedProject`). Otherwise the\n * user's own policy governs: a repository cannot bring a weaker one with it. */\nexport function resolveConfigDir(payloadCwd: string | undefined): string {\n if (process.env.SCOPEBOND_HOOK_DIR) return process.env.SCOPEBOND_HOOK_DIR;\n const candidates: string[] = [];\n if (payloadCwd) candidates.push(join(payloadCwd, \".scopebond\"));\n if (process.env.CLAUDE_PROJECT_DIR) candidates.push(join(process.env.CLAUDE_PROJECT_DIR, \".scopebond\"));\n const home = userHome();\n const homeInstalled = existsSync(join(home, \"policy.json\"));\n for (const dir of candidates) {\n if (existsSync(join(dir, \"policy.json\")) && (!homeInstalled || isTrustedProject(dir))) return dir;\n }\n if (homeInstalled) return home;\n // Nothing scaffolded yet: prefer the payload project, else the user home.\n return candidates[0] ?? home;\n}\n\n/** A project policy present but ignored because it is not trusted (for status/doctor). */\nexport function untrustedProjectPolicy(payloadCwd: string | undefined): string | null {\n if (process.env.SCOPEBOND_HOOK_DIR || !existsSync(join(userHome(), \"policy.json\"))) return null;\n const dirs = [payloadCwd, process.env.CLAUDE_PROJECT_DIR].filter(Boolean).map((d) => join(d as string, \".scopebond\"));\n return dirs.find((d) => existsSync(join(d, \"policy.json\")) && !isTrustedProject(d)) ?? null;\n}\n\nconst isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** Whether a harness command string is one of ours. This is the single matcher for\n * every form the installer has ever written, and it is what makes re-running `init`\n * or `install` replace the entry instead of appending a second one:\n * - `npx -y @scopebond/hook@0.6.0 claude` (portable)\n * - `\"<node>\" \"<…/@scopebond/hook/dist/cli.js>\" claude` (pinned, POSIX)\n * - `\"<node>\" \"<…\\@scopebond\\hook\\dist\\cli.js>\" claude` (pinned, Windows)\n * - `scopebond-hook claude` (legacy global binary)\n * The separator class matters: a Windows pinned path spells the scope\n * `@scopebond\\hook`, and matching only `@scopebond/hook` let a second `init`\n * install a duplicate hook — which would double-check every tool call.\n *\n * Written with substring and token tests rather than one regular expression: the\n * previous `(^|[\"\\s])scopebond([\"'\\s]).*\\b(claude|cursor|codex)\\b` backtracked\n * polynomially on a long run of `\\tscopebond\\t` (CodeQL js/polynomial-redos). The\n * input is a command out of a config file rather than a request, so this was not\n * reachable by an attacker — but a linear check is also the clearer one. */\nconst HARNESS_SUBCOMMANDS = new Set([\"claude\", \"cursor\", \"codex\"]);\n/** Long enough for any real command; a bound keeps the work linear and small. */\nconst MAX_COMMAND = 4096;\nconst unquote = (token: string): string => token.replace(/^[\"']+|[\"']+$/g, \"\");\n\nexport const isScopebondHookCommand = (cmd: unknown): boolean => {\n if (typeof cmd !== \"string\" || cmd.length === 0 || cmd.length > MAX_COMMAND) return false;\n const lower = cmd.toLowerCase();\n // The package itself, in either path spelling (`@scopebond/hook`, `@scopebond\\hook`).\n if (lower.includes(\"@scopebond/hook\") || lower.includes(\"@scopebond\\\\hook\")) return true;\n // The legacy global binary, as a whole token.\n if (/(^|[\\s\"'])scopebond-hook([\\s\"']|$)/.test(cmd)) return true;\n // An absolute or bare form whose last argument is one of our harness subcommands:\n // `<node> <…/cli.js> claude`, `scopebond cursor`.\n const tokens = cmd.split(/\\s+/).filter(Boolean);\n const last = unquote(tokens[tokens.length - 1] ?? \"\");\n if (!HARNESS_SUBCOMMANDS.has(last)) return false;\n return tokens.some((token) => {\n const t = unquote(token).toLowerCase();\n return t.includes(\"scopebond\") || /cli\\.(m|c)?js$/.test(t);\n });\n};\nconst isScopebond = isScopebondHookCommand;\n\n/** Whether a harness hook entry (either shape: a bare command, or a group with a\n * nested `hooks` array) is a Scopebond entry. */\nexport const harnessEntryMatches = (e: unknown): boolean =>\n isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));\nconst entryMatches = harnessEntryMatches;\n\n/** Read an agent's JSON config for merging. A missing file is an empty config; a file\n * that exists but does not parse as a JSON object is an error — rewriting it would\n * silently delete the user's other settings, so the installer stops instead. */\nexport function readHarnessConfig(file: string): Record<string, unknown> {\n if (!existsSync(file)) return {};\n const text = readFileSync(file, \"utf8\");\n if (!text.trim()) return {};\n let parsed: unknown;\n try { parsed = JSON.parse(text); } catch (error) {\n throw new Error(`${file} is not valid JSON (${(error as Error).message}); fix or move it, then run this again — it was left unchanged`);\n }\n if (!isRecord(parsed)) throw new Error(`${file} is not a JSON object; fix or move it, then run this again — it was left unchanged`);\n return parsed;\n}\n\n/** Copy an agent config aside before changing it, once. Re-running an installer must not\n * overwrite the backup with the already-modified file, or the original is lost on the\n * second run — so an existing backup is left alone. Returns the backup path when one was\n * written.\n *\n * This exists because these installers rewrite files the user did not create and may not\n * know are involved (`~/.claude/settings.json` holds their theme, plugins and\n * permissions), and the undo was previously \"hope the merge was right\". */\nexport function backupHarnessConfig(file: string): string | null {\n if (!existsSync(file)) return null;\n const backup = `${file}.scopebond-backup`;\n if (existsSync(backup)) return null;\n try { copyFileSync(file, backup); return backup; } catch { return null; }\n}\n\n/** Write (idempotently) a harness hook entry pointing at an explicit command string.\n * Shared by the project installer (npx command) and the user installer (absolute path). */\nexport function writeHarnessConfig(file: string, harness: Harness, command: string): string {\n const config = readHarnessConfig(file);\n const before = existsSync(file) ? JSON.stringify(config) : null;\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n if (harness === \"cursor\") {\n config.version = config.version ?? 1;\n for (const event of [\"beforeShellExecution\", \"beforeMCPExecution\", \"beforeReadFile\", \"afterFileEdit\"]) {\n const list = Array.isArray((hooks as Record<string, unknown>)[event]) ? (hooks as Record<string, unknown[]>)[event] : ((hooks as Record<string, unknown[]>)[event] = []);\n const at = list.findIndex(entryMatches);\n const entry = { command: command.replace(/\\bclaude$/, \"cursor\") };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n } else {\n const list = Array.isArray((hooks as Record<string, unknown>).PreToolUse) ? (hooks as Record<string, unknown[]>).PreToolUse : ((hooks as Record<string, unknown[]>).PreToolUse = []);\n const at = list.findIndex(entryMatches);\n // Codex matchers are regular expressions; omitting the matcher means every tool.\n // A literal \"*\" is not a valid regular expression and causes Codex to skip the group.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command }] };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n // SB276: a settings file that already holds exactly this entry is left byte-for-byte\n // untouched — no rewrite, no backup. A running agent session watches this file, and\n // rewriting it (even with the same content) is what disturbed a live session on re-login.\n if (before !== null && JSON.stringify(config) === before) return file;\n backupHarnessConfig(file);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** Remove any Scopebond hook entry from a user-level harness config, leaving the rest\n * intact. Returns true if the file existed. */\nexport function removeHarnessConfig(file: string): boolean {\n if (!existsSync(file)) return false;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return true; } // unreadable: leave it untouched\n const hooks = isRecord(config.hooks) ? config.hooks : {};\n for (const [event, value] of Object.entries(hooks)) {\n if (Array.isArray(value)) (hooks as Record<string, unknown[]>)[event] = value.filter((e) => !entryMatches(e));\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return true;\n}\n\n/** Detect a Cursor install by its user config directory. */\nexport function cursorDetected(): boolean {\n return existsSync(join(homedir(), \".cursor\"));\n}\n\n/** Detect a Codex install by its user config directory. */\nexport function codexDetected(): boolean {\n return existsSync(join(homedir(), \".codex\"));\n}\n\n/** The absolute command a harness entry runs: the current Node executable and the\n * absolute path to this CLI, so it does not depend on PATH.\n *\n * Both paths are always quoted on Windows, even without a space in them. A harness\n * runs this string through a shell, and that shell may be bash (Git Bash, WSL, a\n * dev container), where an unquoted Windows path loses every backslash and the\n * hook dies with MODULE_NOT_FOUND. Inside double quotes bash leaves a backslash\n * alone unless it precedes $ ` \" \\ or a newline — none of which occur in a Windows\n * path — and cmd.exe and PowerShell accept quoted paths too. */\nexport function absoluteHookCommand(cliPath: string, harness: Harness): string {\n const quote = (s: string) => (process.platform === \"win32\" || /\\s/.test(s) ? `\"${s}\"` : s);\n return `${quote(process.execPath)} ${quote(cliPath)} ${harness}`;\n}\n\nexport function isHarnessConfigured(file: string): boolean {\n if (!existsSync(file)) return false;\n try {\n const p = JSON.parse(readFileSync(file, \"utf8\"));\n if (!isRecord(p) || !isRecord(p.hooks)) return false;\n return Object.values(p.hooks).some((v) => Array.isArray(v) && v.some(entryMatches));\n } catch { return false; }\n}\n\n/** Every Scopebond command string a harness config currently runs. `doctor` uses it to\n * check that a pinned command still resolves: a hook entry that cannot start is worse\n * than none, because a harness can read the failure as \"no hook\". */\nexport function configuredHookCommands(file: string): string[] {\n if (!existsSync(file)) return [];\n const found: string[] = [];\n const collect = (entry: unknown): void => {\n if (!isRecord(entry)) return;\n if (isScopebond(entry.command)) found.push(String(entry.command));\n if (Array.isArray(entry.hooks)) for (const h of entry.hooks) collect(h);\n };\n try {\n const p = JSON.parse(readFileSync(file, \"utf8\"));\n if (!isRecord(p) || !isRecord(p.hooks)) return [];\n for (const list of Object.values(p.hooks)) if (Array.isArray(list)) for (const e of list) collect(e);\n } catch { return []; }\n return found;\n}\n\n/** Whether a configured hook command can actually start. An `npx` form always can\n * (npm resolves it); a pinned command names two absolute paths — the Node binary and\n * the CLI — and either can go away: a Node version manager switching versions, or the\n * Scopebond home being cleared. Both are exactly what this check exists to catch. */\nexport function hookCommandResolves(command: string): boolean {\n if (/(^|\\s)npx(\\.cmd)?\\s/.test(command)) return true;\n // `\"<node>\" \"<cli.js>\" claude`: check every token that names a file on disk.\n const tokens = command.match(/\"[^\"]+\"|\\S+/g) ?? [];\n let checked = 0;\n for (const raw of tokens) {\n const candidate = raw.replace(/^\"|\"$/g, \"\");\n if (!/\\.(m|c)?js$|\\.exe$/i.test(candidate)) continue;\n checked += 1;\n if (!existsSync(candidate)) return false;\n }\n // A bare `node cli.js` form on POSIX has no .exe to check; the .js still counted.\n return checked > 0 || !/[\\\\/]/.test(command.split(/\\s+/)[0] ?? \"\");\n}\n\n/** Remove the user-level home (keys, policy, receipts). */\nexport function purgeHome(): void {\n rmSync(userHome(), { recursive: true, force: true });\n}\n\n/** The Claude Code events the observation emitters listen to, beyond PreToolUse. */\nexport const LIFECYCLE_EVENTS = [\"SessionStart\", \"SessionEnd\", \"PostToolUse\", \"PostToolUseFailure\"] as const;\n\n/** Add (idempotently) the session lifecycle and after-action hook entries to a Claude Code\n * config file, running the same Scopebond command that already handles PreToolUse: the\n * command tells the events apart. Only Claude Code is wired: its lifecycle events are the\n * ones the hook's tested mapping covers. Nothing else in the file changes. */\nexport function wireLifecycleHooks(file: string, command: string): string {\n const config = readHarnessConfig(file);\n backupHarnessConfig(file);\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n for (const event of LIFECYCLE_EVENTS) {\n const list = Array.isArray((hooks as Record<string, unknown>)[event]) ? (hooks as Record<string, unknown[]>)[event] : ((hooks as Record<string, unknown[]>)[event] = []);\n const at = list.findIndex(entryMatches);\n const entry = event.startsWith(\"Session\")\n ? { hooks: [{ type: \"command\", command }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command }] };\n if (at >= 0) list[at] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** Remove only the lifecycle entries `wireLifecycleHooks` added, leaving PreToolUse and every\n * other setting alone. Returns the number removed. */\nexport function unwireLifecycleHooks(file: string): number {\n if (!existsSync(file)) return 0;\n let config: Record<string, unknown>;\n try { config = readHarnessConfig(file); } catch { return 0; }\n const hooks = isRecord(config.hooks) ? config.hooks : null;\n if (!hooks) return 0;\n let removed = 0;\n for (const event of LIFECYCLE_EVENTS) {\n const value = (hooks as Record<string, unknown>)[event];\n if (!Array.isArray(value)) continue;\n const kept = value.filter((e) => !entryMatches(e));\n removed += value.length - kept.length;\n if (kept.length) (hooks as Record<string, unknown[]>)[event] = kept; else delete (hooks as Record<string, unknown>)[event];\n }\n if (removed > 0) writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return removed;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-sync.d.ts","sourceRoot":"","sources":["../src/policy-sync.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"policy-sync.d.ts","sourceRoot":"","sources":["../src/policy-sync.ts"],"names":[],"mappings":"AAoBA,OAAO,EAC2F,KAAK,aAAa,EACnH,MAAM,cAAc,CAAC;AAEtB,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAI9C,+GAA+G;AAC/G,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAGtC,MAAM,MAAM,WAAW,GACnB;IAAE,KAAK,EAAE,eAAe,CAAA;CAAE,GAC1B;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACxC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrF;IAAE,KAAK,EAAE,cAAc,CAAA;CAAE,GACzB;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAC3D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,yGAAyG;IACzG,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CA+FxF;AAED;;;yEAGyE;AACzE,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,WAAW,EAAE,QAAQ,SAAmE,EAAE,GAAG,SAAa,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAyBvM;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEjD"}
|