@scopebond/hook 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +260 -49
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +35 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +74 -6
- package/dist/init.js.map +1 -1
- package/dist/install.d.ts +29 -4
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +109 -6
- package/dist/install.js.map +1 -1
- package/dist/minimize.d.ts +11 -3
- package/dist/minimize.d.ts.map +1 -1
- package/dist/minimize.js +35 -6
- package/dist/minimize.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
export { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
|
|
5
5
|
export { createHookRuntime, starterPolicy, upgradeStarterPolicy } from "./runtime.js";
|
|
6
6
|
export { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE, } from "./rules.js";
|
|
7
|
-
export { scaffold, harnessSnippet, installHarness } from "./init.js";
|
|
8
|
-
export { userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, isScopebondHookCommand, harnessEntryMatches, } from "./install.js";
|
|
7
|
+
export { scaffold, harnessSnippet, installHarness, placeHook } from "./init.js";
|
|
8
|
+
export { userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, isScopebondHookCommand, harnessEntryMatches, localHarnessFile, isMachineSpecificCommand, gitShareState, excludeFromGit, pruneHarnessEntries, } from "./install.js";
|
|
9
9
|
export { explainDeny, describeAction, findClause } from "./explain.js";
|
|
10
10
|
export { ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath, } from "./runtime-install.js";
|
|
11
11
|
export { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from "./cloud.js";
|
|
12
|
-
export { scrubSecrets, scrubParam, redactCommand, digest, sha256 } from "./minimize.js";
|
|
12
|
+
export { scrubSecrets, scrubParam, redactCommand, digest, sha256, keyedDigest, useDigestKey, loadOrCreateDigestKey } from "./minimize.js";
|
|
13
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,4EAA4E;AAC5E,gDAAgD;AAEhD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEtF,OAAO,EACL,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,GAC/F,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,4EAA4E;AAC5E,gDAAgD;AAEhD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEtF,OAAO,EACL,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,GAC/F,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEhF,OAAO,EACL,QAAQ,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EACxG,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,SAAS,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EACpG,aAAa,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAC7E,sBAAsB,EAAE,mBAAmB,EAC3C,gBAAgB,EAAE,wBAAwB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,GAC/F,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,EACL,oBAAoB,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,GACpG,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAExG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC","sourcesContent":["// @scopebond/hook — the Claude Code, Cursor + Codex connector. The library surface is\n// the deterministic mapper and the check-only runtime; the `scopebond-hook`\n// binary is the thin harness adapter over them.\n\nexport { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from \"./map.js\";\nexport type { Mapped, NormalizedIntent } from \"./map.js\";\nexport { createHookRuntime, starterPolicy, upgradeStarterPolicy } from \"./runtime.js\";\nexport type { RuntimeConfig, Decision } from \"./runtime.js\";\nexport {\n compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE,\n} from \"./rules.js\";\nexport type { RuleSet, PathRule } from \"./rules.js\";\nexport { scaffold, harnessSnippet, installHarness, placeHook } from \"./init.js\";\nexport type { HookPlacement } from \"./init.js\";\nexport {\n userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig,\n cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome,\n readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile,\n harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves,\n isScopebondHookCommand, harnessEntryMatches,\n localHarnessFile, isMachineSpecificCommand, gitShareState, excludeFromGit, pruneHarnessEntries,\n} from \"./install.js\";\nexport type { Harness, HarnessScopes, GitShare } from \"./install.js\";\nexport { explainDeny, describeAction, findClause } from \"./explain.js\";\nexport type { ExplainDenyInput, ExplainIntent, ExplainPolicy, ExplainClause } from \"./explain.js\";\nexport {\n ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath,\n} from \"./runtime-install.js\";\nexport type { PinResult } from \"./runtime-install.js\";\nexport { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from \"./cloud.js\";\nexport type { HookConnection } from \"./cloud.js\";\nexport { scrubSecrets, scrubParam, redactCommand, digest, sha256, keyedDigest, useDigestKey, loadOrCreateDigestKey } from \"./minimize.js\";\n"]}
|
package/dist/init.d.ts
CHANGED
|
@@ -5,14 +5,45 @@ export declare function scaffold(dir: string, opts?: {
|
|
|
5
5
|
policyPath: string;
|
|
6
6
|
rulesPath: string;
|
|
7
7
|
};
|
|
8
|
+
/** Where `init` put the hook, and why — for the lines it prints. */
|
|
9
|
+
export interface HookPlacement {
|
|
10
|
+
/** The config file written. */
|
|
11
|
+
file: string;
|
|
12
|
+
/** The command written into it. */
|
|
13
|
+
command: string;
|
|
14
|
+
/** `shared`: the project config a team commits (portable command only).
|
|
15
|
+
* `personal`: a file only this clone uses (the pinned, machine-specific command). */
|
|
16
|
+
scope: "shared" | "personal";
|
|
17
|
+
/** One line for the user when the placement differs from what they might expect. */
|
|
18
|
+
note?: string;
|
|
19
|
+
/** Machine-specific entries moved out of a shared config file (a repair). */
|
|
20
|
+
repaired: number;
|
|
21
|
+
}
|
|
22
|
+
/** Place the hook for a project so that nothing machine-specific reaches a file git
|
|
23
|
+
* shares. A pinned command (`"<node>" "<…/cli.js>" claude`) is fast but names paths that
|
|
24
|
+
* exist only here; in a committed `.claude/settings.json` it cannot start on a
|
|
25
|
+
* teammate's machine, and an agent treats a hook that cannot start as a non-blocking
|
|
26
|
+
* error — so the agent there would run unchecked, while the file says it is governed.
|
|
27
|
+
*
|
|
28
|
+
* - `shared` (or no pinned command): the portable `npx` form in the project config.
|
|
29
|
+
* - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;
|
|
30
|
+
* any machine-specific entry an older `init` wrote into `settings.json` is removed.
|
|
31
|
+
* - Cursor / Codex, pinned: they have no personal file, so the project file is used
|
|
32
|
+
* only if git does not already track it (then excluded for this clone); a tracked
|
|
33
|
+
* file gets the portable form. */
|
|
34
|
+
export declare function placeHook(harness: "claude" | "cursor" | "codex", cwd: string, pinned: string | undefined, opts?: {
|
|
35
|
+
shared?: boolean;
|
|
36
|
+
}): HookPlacement;
|
|
8
37
|
/** Install the hook into the agent's own config file, merging with anything already
|
|
9
38
|
* there (idempotent — safe to run twice). Returns the file it wrote. This is what
|
|
10
39
|
* lets `connect` finish setup without the user hand-editing JSON.
|
|
11
|
-
* `cwd` defaults to the current directory (the project root the user runs it in).
|
|
40
|
+
* `cwd` defaults to the current directory (the project root the user runs it in).
|
|
41
|
+
* Writes exactly where it is told; `placeHook` decides where a command may go. */
|
|
12
42
|
export declare function installHarness(harness: "claude" | "cursor" | "codex", cwd?: string,
|
|
13
|
-
/** The command to run per tool call. Defaults to the portable `npx` form
|
|
14
|
-
|
|
15
|
-
|
|
43
|
+
/** The command to run per tool call. Defaults to the portable `npx` form. */
|
|
44
|
+
command?: string,
|
|
45
|
+
/** The file to write; defaults to the shared project config. */
|
|
46
|
+
target?: string): string;
|
|
16
47
|
/** The harness configuration snippet to install the hook by hand. Defaults to the
|
|
17
48
|
* version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so
|
|
18
49
|
* the printed snippet matches what `init` would have written. */
|
package/dist/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAgBA,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAyBjI;AAED,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB;0FACsF;IACtF,KAAK,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC7B,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;qCAWqC;AACrC,wBAAgB,SAAS,CACvB,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EACtC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9B,aAAa,CAiDf;AAED;;;;mFAImF;AACnF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EACtC,GAAG,GAAE,MAAsB;AAC3B,6EAA6E;AAC7E,OAAO,CAAC,EAAE,MAAM;AAChB,gEAAgE;AAChE,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAmCR;AAED;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA6B/F"}
|
package/dist/init.js
CHANGED
|
@@ -6,8 +6,9 @@ import { dirname, join } from "node:path";
|
|
|
6
6
|
import { loadOrCreateAttester } from "@scopebond/gateway/node";
|
|
7
7
|
import { createSigner } from "@scopebond/sdk";
|
|
8
8
|
import { compile, defaultRules, loadRules, saveRules, rulesPath } from "./rules.js";
|
|
9
|
-
import { readHarnessConfig, projectHarnessFile, harnessEntryMatches, backupHarnessConfig } from "./install.js";
|
|
9
|
+
import { readHarnessConfig, projectHarnessFile, localHarnessFile, harnessEntryMatches, backupHarnessConfig, gitShareState, excludeFromGit, pruneHarnessEntries, configuredHookCommands, isMachineSpecificCommand, } from "./install.js";
|
|
10
10
|
import { hookCommand } from "./version.js";
|
|
11
|
+
import { loadOrCreateDigestKey } from "./minimize.js";
|
|
11
12
|
export function scaffold(dir, opts = {}) {
|
|
12
13
|
mkdirSync(dir, { recursive: true });
|
|
13
14
|
const keyPath = join(dir, "agent.key");
|
|
@@ -20,6 +21,8 @@ export function scaffold(dir, opts = {}) {
|
|
|
20
21
|
// Machine signing key (agent) + gateway countersigning key (attester). Reused if present.
|
|
21
22
|
loadOrCreateAttester({ file: keyPath });
|
|
22
23
|
loadOrCreateAttester({ file: attesterPath });
|
|
24
|
+
// The per-machine key for keyed command and argument digests (see minimize.ts).
|
|
25
|
+
loadOrCreateDigestKey(dir);
|
|
23
26
|
const agent = createSigner({ privateKeyPem: readFileSync(keyPath, "utf8") });
|
|
24
27
|
// The editable rule set beside the compiled policy, so "edit the limits" means editing a
|
|
25
28
|
// readable list rather than a 700-character lookahead. `compile(defaultRules())` produces
|
|
@@ -34,16 +37,81 @@ export function scaffold(dir, opts = {}) {
|
|
|
34
37
|
}
|
|
35
38
|
return { agentKid: agent.kid, policyPath, rulesPath: rulesFile };
|
|
36
39
|
}
|
|
40
|
+
/** Place the hook for a project so that nothing machine-specific reaches a file git
|
|
41
|
+
* shares. A pinned command (`"<node>" "<…/cli.js>" claude`) is fast but names paths that
|
|
42
|
+
* exist only here; in a committed `.claude/settings.json` it cannot start on a
|
|
43
|
+
* teammate's machine, and an agent treats a hook that cannot start as a non-blocking
|
|
44
|
+
* error — so the agent there would run unchecked, while the file says it is governed.
|
|
45
|
+
*
|
|
46
|
+
* - `shared` (or no pinned command): the portable `npx` form in the project config.
|
|
47
|
+
* - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;
|
|
48
|
+
* any machine-specific entry an older `init` wrote into `settings.json` is removed.
|
|
49
|
+
* - Cursor / Codex, pinned: they have no personal file, so the project file is used
|
|
50
|
+
* only if git does not already track it (then excluded for this clone); a tracked
|
|
51
|
+
* file gets the portable form. */
|
|
52
|
+
export function placeHook(harness, cwd, pinned, opts = {}) {
|
|
53
|
+
const shared = projectHarnessFile(harness, cwd);
|
|
54
|
+
const portable = hookCommand(harness);
|
|
55
|
+
const local = localHarnessFile(harness, cwd);
|
|
56
|
+
if (opts.shared || !pinned) {
|
|
57
|
+
const file = installHarness(harness, cwd, portable);
|
|
58
|
+
// One hook per project: a personal copy beside a shared one would check every action twice.
|
|
59
|
+
if (local)
|
|
60
|
+
pruneHarnessEntries(local, () => true);
|
|
61
|
+
return { file, command: portable, scope: "shared", repaired: 0 };
|
|
62
|
+
}
|
|
63
|
+
if (local) {
|
|
64
|
+
const state = gitShareState(local);
|
|
65
|
+
if (state === "tracked") {
|
|
66
|
+
const file = installHarness(harness, cwd, portable);
|
|
67
|
+
return { file, command: portable, scope: "shared", repaired: 0,
|
|
68
|
+
note: `${local} is committed to git, so the portable command was used (it starts on any machine)` };
|
|
69
|
+
}
|
|
70
|
+
// An existing portable hook in the shared file is the team's; leave it rather than
|
|
71
|
+
// running two hooks, and say how to get the fast one.
|
|
72
|
+
if (configuredHookCommands(shared).some((c) => !isMachineSpecificCommand(c))) {
|
|
73
|
+
return { file: shared, command: portable, scope: "shared", repaired: 0,
|
|
74
|
+
note: `kept the shared hook already in ${shared}; remove it there to pin a faster one on this machine` };
|
|
75
|
+
}
|
|
76
|
+
// A personal file git would still add is not personal: if it cannot be excluded,
|
|
77
|
+
// fall back to the portable command rather than leave a pinned path to be committed.
|
|
78
|
+
if (state === "untracked" && !excludeFromGit(local)) {
|
|
79
|
+
const file = installHarness(harness, cwd, portable);
|
|
80
|
+
pruneHarnessEntries(local, () => true);
|
|
81
|
+
pruneHarnessEntries(shared, isMachineSpecificCommand);
|
|
82
|
+
return { file, command: portable, scope: "shared", repaired: 0,
|
|
83
|
+
note: `${local} could not be kept out of git, so the portable command was used` };
|
|
84
|
+
}
|
|
85
|
+
const file = installHarness(harness, cwd, pinned, local);
|
|
86
|
+
const repaired = pruneHarnessEntries(shared, isMachineSpecificCommand);
|
|
87
|
+
return { file, command: pinned, scope: "personal", repaired };
|
|
88
|
+
}
|
|
89
|
+
const state = gitShareState(shared);
|
|
90
|
+
if (state === "tracked") {
|
|
91
|
+
const file = installHarness(harness, cwd, portable);
|
|
92
|
+
return { file, command: portable, scope: "shared", repaired: 0,
|
|
93
|
+
note: `${shared} is committed to git, so the portable command was used (it starts on any machine)` };
|
|
94
|
+
}
|
|
95
|
+
if (state === "untracked" && !excludeFromGit(shared)) {
|
|
96
|
+
const file = installHarness(harness, cwd, portable);
|
|
97
|
+
return { file, command: portable, scope: "shared", repaired: 0,
|
|
98
|
+
note: `${shared} could not be kept out of git, so the portable command was used` };
|
|
99
|
+
}
|
|
100
|
+
const file = installHarness(harness, cwd, pinned);
|
|
101
|
+
return { file, command: pinned, scope: "personal", repaired: 0 };
|
|
102
|
+
}
|
|
37
103
|
/** Install the hook into the agent's own config file, merging with anything already
|
|
38
104
|
* there (idempotent — safe to run twice). Returns the file it wrote. This is what
|
|
39
105
|
* lets `connect` finish setup without the user hand-editing JSON.
|
|
40
|
-
* `cwd` defaults to the current directory (the project root the user runs it in).
|
|
106
|
+
* `cwd` defaults to the current directory (the project root the user runs it in).
|
|
107
|
+
* Writes exactly where it is told; `placeHook` decides where a command may go. */
|
|
41
108
|
export function installHarness(harness, cwd = process.cwd(),
|
|
42
|
-
/** The command to run per tool call. Defaults to the portable `npx` form
|
|
43
|
-
|
|
44
|
-
|
|
109
|
+
/** The command to run per tool call. Defaults to the portable `npx` form. */
|
|
110
|
+
command,
|
|
111
|
+
/** The file to write; defaults to the shared project config. */
|
|
112
|
+
target) {
|
|
45
113
|
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
46
|
-
const file = projectHarnessFile(harness, cwd);
|
|
114
|
+
const file = target ?? projectHarnessFile(harness, cwd);
|
|
47
115
|
const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object
|
|
48
116
|
backupHarnessConfig(file); // keep the user's original beside it before the first change
|
|
49
117
|
mkdirSync(dirname(file), { recursive: true });
|
package/dist/init.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["// `scopebond-hook init` — scaffold the local enrollment: a machine signing key, a\n// gateway countersigning key and a starter policy. Keys and receipts stay on the\n// machine; no credentials are handled.\n\nimport { mkdirSync, writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport { compile, defaultRules, loadRules, saveRules, rulesPath } from \"./rules.js\";\nimport { readHarnessConfig, projectHarnessFile, harnessEntryMatches, backupHarnessConfig } from \"./install.js\";\nimport { hookCommand } from \"./version.js\";\n\nexport function scaffold(dir: string, opts: { force?: boolean } = {}): { agentKid: string; policyPath: string; rulesPath: string } {\n mkdirSync(dir, { recursive: true });\n const keyPath = join(dir, \"agent.key\");\n const attesterPath = join(dir, \"attester.key\");\n const policyPath = join(dir, \"policy.json\");\n // Never let the signing keys, the Cloud credential or the local log be committed.\n const gitignorePath = join(dir, \".gitignore\");\n if (!existsSync(gitignorePath)) writeFileSync(gitignorePath, \"*\\n\");\n // Machine signing key (agent) + gateway countersigning key (attester). Reused if present.\n loadOrCreateAttester({ file: keyPath });\n loadOrCreateAttester({ file: attesterPath });\n const agent = createSigner({ privateKeyPem: readFileSync(keyPath, \"utf8\") });\n // The editable rule set beside the compiled policy, so \"edit the limits\" means editing a\n // readable list rather than a 700-character lookahead. `compile(defaultRules())` produces\n // the starter policy's exact patterns — `rules.test.mjs` pins that — so writing either\n // form enforces the same thing.\n const rulesFile = rulesPath(dir);\n if (!existsSync(rulesFile) || opts.force) saveRules(dir, defaultRules());\n if (!existsSync(policyPath) || opts.force) {\n const rules = loadRules(dir) ?? defaultRules();\n writeFileSync(policyPath, JSON.stringify(compile(rules, agent.kid), null, 2) + \"\\n\");\n }\n return { agentKid: agent.kid, policyPath, rulesPath: rulesFile };\n}\n\n/** Install the hook into the agent's own config file, merging with anything already\n * there (idempotent — safe to run twice). Returns the file it wrote. This is what\n * lets `connect` finish setup without the user hand-editing JSON.\n * `cwd` defaults to the current directory (the project root the user runs it in). */\nexport function installHarness(\n harness: \"claude\" | \"cursor\" | \"codex\",\n cwd: string = process.cwd(),\n /** The command to run per tool call. Defaults to the portable `npx` form; `init`\n * passes a pinned absolute path when it has one, which is ~7x faster to start. */\n command?: string,\n): string {\n const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n const file = projectHarnessFile(harness, cwd);\n const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object\n backupHarnessConfig(file); // keep the user's original beside it before the first change\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n const forHarness = (h: \"claude\" | \"cursor\" | \"codex\"): string =>\n command ? command.replace(/\\b(claude|cursor|codex)$/, h) : hookCommand(h);\n const cursorCmd = forHarness(\"cursor\");\n const claudeCmd = forHarness(\"claude\");\n const codexCmd = forHarness(\"codex\");\n // Match any existing Scopebond hook entry — npx, pinned absolute (either path\n // separator) or a legacy bare `scopebond-hook` — so re-running replaces the entry\n // instead of appending a second one. One shared matcher with `install`: a private\n // copy here missed the pinned Windows form and duplicated the hook.\n const entryMatches = harnessEntryMatches;\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 existing = list.findIndex(entryMatches);\n if (existing >= 0) list[existing] = { command: cursorCmd }; else list.push({ command: cursorCmd });\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 existing = list.findIndex(entryMatches);\n // Codex matchers are regular expressions. No matcher means all supported tools.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command: codexCmd, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command: claudeCmd }] };\n if (existing >= 0) list[existing] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** The harness configuration snippet to install the hook by hand. Defaults to the\n * version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so\n * the printed snippet matches what `init` would have written. */\nexport function harnessSnippet(harness: \"claude\" | \"cursor\" | \"codex\", command?: string): string {\n const cmdFor = (h: \"claude\" | \"cursor\" | \"codex\"): string =>\n command ? command.replace(/\\b(claude|cursor|codex)$/, h) : hookCommand(h);\n if (harness === \"cursor\") {\n const cmd = cmdFor(\"cursor\");\n return JSON.stringify({\n version: 1,\n hooks: {\n beforeShellExecution: [{ command: cmd }],\n beforeMCPExecution: [{ command: cmd }],\n beforeReadFile: [{ command: cmd }],\n afterFileEdit: [{ command: cmd }],\n },\n }, null, 2);\n }\n if (harness === \"codex\") {\n return JSON.stringify({\n hooks: {\n PreToolUse: [{\n hooks: [{ type: \"command\", command: cmdFor(\"codex\"), timeout: 30, statusMessage: \"Checking this action with Scopebond\" }],\n }],\n },\n }, null, 2);\n }\n return JSON.stringify({\n hooks: {\n PreToolUse: [{ matcher: \"*\", hooks: [{ type: \"command\", command: cmdFor(\"claude\") }] }],\n },\n }, null, 2);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["// `scopebond-hook init` — scaffold the local enrollment: a machine signing key, a\n// gateway countersigning key and a starter policy. Keys and receipts stay on the\n// machine; no credentials are handled.\n\nimport { mkdirSync, writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport { compile, defaultRules, loadRules, saveRules, rulesPath } from \"./rules.js\";\nimport {\n readHarnessConfig, projectHarnessFile, localHarnessFile, harnessEntryMatches, backupHarnessConfig,\n gitShareState, excludeFromGit, pruneHarnessEntries, configuredHookCommands, isMachineSpecificCommand,\n} from \"./install.js\";\nimport { hookCommand } from \"./version.js\";\nimport { loadOrCreateDigestKey } from \"./minimize.js\";\n\nexport function scaffold(dir: string, opts: { force?: boolean } = {}): { agentKid: string; policyPath: string; rulesPath: string } {\n mkdirSync(dir, { recursive: true });\n const keyPath = join(dir, \"agent.key\");\n const attesterPath = join(dir, \"attester.key\");\n const policyPath = join(dir, \"policy.json\");\n // Never let the signing keys, the Cloud credential or the local log be committed.\n const gitignorePath = join(dir, \".gitignore\");\n if (!existsSync(gitignorePath)) writeFileSync(gitignorePath, \"*\\n\");\n // Machine signing key (agent) + gateway countersigning key (attester). Reused if present.\n loadOrCreateAttester({ file: keyPath });\n loadOrCreateAttester({ file: attesterPath });\n // The per-machine key for keyed command and argument digests (see minimize.ts).\n loadOrCreateDigestKey(dir);\n const agent = createSigner({ privateKeyPem: readFileSync(keyPath, \"utf8\") });\n // The editable rule set beside the compiled policy, so \"edit the limits\" means editing a\n // readable list rather than a 700-character lookahead. `compile(defaultRules())` produces\n // the starter policy's exact patterns — `rules.test.mjs` pins that — so writing either\n // form enforces the same thing.\n const rulesFile = rulesPath(dir);\n if (!existsSync(rulesFile) || opts.force) saveRules(dir, defaultRules());\n if (!existsSync(policyPath) || opts.force) {\n const rules = loadRules(dir) ?? defaultRules();\n writeFileSync(policyPath, JSON.stringify(compile(rules, agent.kid), null, 2) + \"\\n\");\n }\n return { agentKid: agent.kid, policyPath, rulesPath: rulesFile };\n}\n\n/** Where `init` put the hook, and why — for the lines it prints. */\nexport interface HookPlacement {\n /** The config file written. */\n file: string;\n /** The command written into it. */\n command: string;\n /** `shared`: the project config a team commits (portable command only).\n * `personal`: a file only this clone uses (the pinned, machine-specific command). */\n scope: \"shared\" | \"personal\";\n /** One line for the user when the placement differs from what they might expect. */\n note?: string;\n /** Machine-specific entries moved out of a shared config file (a repair). */\n repaired: number;\n}\n\n/** Place the hook for a project so that nothing machine-specific reaches a file git\n * shares. A pinned command (`\"<node>\" \"<…/cli.js>\" claude`) is fast but names paths that\n * exist only here; in a committed `.claude/settings.json` it cannot start on a\n * teammate's machine, and an agent treats a hook that cannot start as a non-blocking\n * error — so the agent there would run unchecked, while the file says it is governed.\n *\n * - `shared` (or no pinned command): the portable `npx` form in the project config.\n * - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;\n * any machine-specific entry an older `init` wrote into `settings.json` is removed.\n * - Cursor / Codex, pinned: they have no personal file, so the project file is used\n * only if git does not already track it (then excluded for this clone); a tracked\n * file gets the portable form. */\nexport function placeHook(\n harness: \"claude\" | \"cursor\" | \"codex\",\n cwd: string,\n pinned: string | undefined,\n opts: { shared?: boolean } = {},\n): HookPlacement {\n const shared = projectHarnessFile(harness, cwd);\n const portable = hookCommand(harness);\n const local = localHarnessFile(harness, cwd);\n if (opts.shared || !pinned) {\n const file = installHarness(harness, cwd, portable);\n // One hook per project: a personal copy beside a shared one would check every action twice.\n if (local) pruneHarnessEntries(local, () => true);\n return { file, command: portable, scope: \"shared\", repaired: 0 };\n }\n if (local) {\n const state = gitShareState(local);\n if (state === \"tracked\") {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${local} is committed to git, so the portable command was used (it starts on any machine)` };\n }\n // An existing portable hook in the shared file is the team's; leave it rather than\n // running two hooks, and say how to get the fast one.\n if (configuredHookCommands(shared).some((c) => !isMachineSpecificCommand(c))) {\n return { file: shared, command: portable, scope: \"shared\", repaired: 0,\n note: `kept the shared hook already in ${shared}; remove it there to pin a faster one on this machine` };\n }\n // A personal file git would still add is not personal: if it cannot be excluded,\n // fall back to the portable command rather than leave a pinned path to be committed.\n if (state === \"untracked\" && !excludeFromGit(local)) {\n const file = installHarness(harness, cwd, portable);\n pruneHarnessEntries(local, () => true);\n pruneHarnessEntries(shared, isMachineSpecificCommand);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${local} could not be kept out of git, so the portable command was used` };\n }\n const file = installHarness(harness, cwd, pinned, local);\n const repaired = pruneHarnessEntries(shared, isMachineSpecificCommand);\n return { file, command: pinned, scope: \"personal\", repaired };\n }\n const state = gitShareState(shared);\n if (state === \"tracked\") {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${shared} is committed to git, so the portable command was used (it starts on any machine)` };\n }\n if (state === \"untracked\" && !excludeFromGit(shared)) {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${shared} could not be kept out of git, so the portable command was used` };\n }\n const file = installHarness(harness, cwd, pinned);\n return { file, command: pinned, scope: \"personal\", repaired: 0 };\n}\n\n/** Install the hook into the agent's own config file, merging with anything already\n * there (idempotent — safe to run twice). Returns the file it wrote. This is what\n * lets `connect` finish setup without the user hand-editing JSON.\n * `cwd` defaults to the current directory (the project root the user runs it in).\n * Writes exactly where it is told; `placeHook` decides where a command may go. */\nexport function installHarness(\n harness: \"claude\" | \"cursor\" | \"codex\",\n cwd: string = process.cwd(),\n /** The command to run per tool call. Defaults to the portable `npx` form. */\n command?: string,\n /** The file to write; defaults to the shared project config. */\n target?: string,\n): string {\n const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n const file = target ?? projectHarnessFile(harness, cwd);\n const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object\n backupHarnessConfig(file); // keep the user's original beside it before the first change\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n const forHarness = (h: \"claude\" | \"cursor\" | \"codex\"): string =>\n command ? command.replace(/\\b(claude|cursor|codex)$/, h) : hookCommand(h);\n const cursorCmd = forHarness(\"cursor\");\n const claudeCmd = forHarness(\"claude\");\n const codexCmd = forHarness(\"codex\");\n // Match any existing Scopebond hook entry — npx, pinned absolute (either path\n // separator) or a legacy bare `scopebond-hook` — so re-running replaces the entry\n // instead of appending a second one. One shared matcher with `install`: a private\n // copy here missed the pinned Windows form and duplicated the hook.\n const entryMatches = harnessEntryMatches;\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 existing = list.findIndex(entryMatches);\n if (existing >= 0) list[existing] = { command: cursorCmd }; else list.push({ command: cursorCmd });\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 existing = list.findIndex(entryMatches);\n // Codex matchers are regular expressions. No matcher means all supported tools.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command: codexCmd, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command: claudeCmd }] };\n if (existing >= 0) list[existing] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** The harness configuration snippet to install the hook by hand. Defaults to the\n * version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so\n * the printed snippet matches what `init` would have written. */\nexport function harnessSnippet(harness: \"claude\" | \"cursor\" | \"codex\", command?: string): string {\n const cmdFor = (h: \"claude\" | \"cursor\" | \"codex\"): string =>\n command ? command.replace(/\\b(claude|cursor|codex)$/, h) : hookCommand(h);\n if (harness === \"cursor\") {\n const cmd = cmdFor(\"cursor\");\n return JSON.stringify({\n version: 1,\n hooks: {\n beforeShellExecution: [{ command: cmd }],\n beforeMCPExecution: [{ command: cmd }],\n beforeReadFile: [{ command: cmd }],\n afterFileEdit: [{ command: cmd }],\n },\n }, null, 2);\n }\n if (harness === \"codex\") {\n return JSON.stringify({\n hooks: {\n PreToolUse: [{\n hooks: [{ type: \"command\", command: cmdFor(\"codex\"), timeout: 30, statusMessage: \"Checking this action with Scopebond\" }],\n }],\n },\n }, null, 2);\n }\n return JSON.stringify({\n hooks: {\n PreToolUse: [{ matcher: \"*\", hooks: [{ type: \"command\", command: cmdFor(\"claude\") }] }],\n },\n }, null, 2);\n}\n"]}
|
package/dist/install.d.ts
CHANGED
|
@@ -7,17 +7,42 @@ export declare function userHarnessFile(harness: Harness): string;
|
|
|
7
7
|
/** The project-level agent config file for a harness — what `init` writes, so the
|
|
8
8
|
* hook travels with the repository rather than the machine. */
|
|
9
9
|
export declare function projectHarnessFile(harness: Harness, cwd?: string): string;
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
/** The personal, per-machine project config for a harness, or null when the harness
|
|
11
|
+
* has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and
|
|
12
|
+
* merges the hooks of both; it is the place for anything that names a path on this
|
|
13
|
+
* machine. Cursor and Codex have no such file. */
|
|
14
|
+
export declare function localHarnessFile(harness: Harness, cwd?: string): string | null;
|
|
15
|
+
/** Which config files actually carry a Scopebond hook for this harness. There are three
|
|
16
|
+
* places it can live — the shared project config, the personal project config (Claude
|
|
17
|
+
* Code's `settings.local.json`, where `init` pins the fast command) and the user config
|
|
18
|
+
* `install` writes — and `status`/`doctor` must look at all of them: checking only the
|
|
19
|
+
* user one told everyone who ran `init` that their install had failed. */
|
|
14
20
|
export interface HarnessScopes {
|
|
15
21
|
project: string | null;
|
|
22
|
+
local: string | null;
|
|
16
23
|
user: string | null;
|
|
17
24
|
}
|
|
18
25
|
export declare function harnessScopes(harness: Harness, cwd?: string): HarnessScopes;
|
|
19
26
|
/** One word for where a harness is wired, for a status line. */
|
|
20
27
|
export declare function harnessScopeLabel(scopes: HarnessScopes): string;
|
|
28
|
+
/** Whether a hook command names a path that exists only on this machine — the pinned
|
|
29
|
+
* `"<node>" "<…/cli.js>" claude` form — rather than the portable `npx` form. */
|
|
30
|
+
export declare function isMachineSpecificCommand(command: string): boolean;
|
|
31
|
+
/** How git treats a config file: committed (`tracked`), kept out (`ignored`), neither
|
|
32
|
+
* yet (`untracked`, so a `git add .` would commit it), or not in a repository / no git
|
|
33
|
+
* on this machine (`none`). A file git shares must never carry a machine-specific
|
|
34
|
+
* command: on a teammate's machine the path does not exist, the command cannot start,
|
|
35
|
+
* and Claude Code, Cursor and Codex all treat a hook that cannot start as a non-blocking
|
|
36
|
+
* error — the agent runs with no check at all. */
|
|
37
|
+
export type GitShare = "tracked" | "ignored" | "untracked" | "none";
|
|
38
|
+
export declare function gitShareState(file: string): GitShare;
|
|
39
|
+
/** Keep a file out of git for this clone only, through `.git/info/exclude` — never the
|
|
40
|
+
* repository's own `.gitignore`, which is the team's file. Returns true when the file is
|
|
41
|
+
* ignored afterwards. */
|
|
42
|
+
export declare function excludeFromGit(file: string): boolean;
|
|
43
|
+
/** Remove the Scopebond entries whose command `remove` selects, leaving every other
|
|
44
|
+
* setting alone. Returns the number of entries removed. */
|
|
45
|
+
export declare function pruneHarnessEntries(file: string, remove: (command: string) => boolean): number;
|
|
21
46
|
/** The file in the user home that pins trusted project policies: absolute project
|
|
22
47
|
* config dir → SHA-256 of its policy.json. It lives under the protected home, so the
|
|
23
48
|
* governed agent cannot write it. */
|
package/dist/install.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
package/dist/install.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// user-level install exists, a project-local `.scopebond` is used only if the user
|
|
6
6
|
// trusted that exact policy (`scopebond trust`, or `init` in the project): a cloned
|
|
7
7
|
// repository, or an agent inside it, must not be able to swap in its own policy.
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
8
9
|
import { createHash } from "node:crypto";
|
|
9
10
|
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, copyFileSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
13
|
/** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */
|
|
13
14
|
export function userHome() {
|
|
14
15
|
return process.env.SCOPEBOND_HOME ?? join(homedir(), ".scopebond");
|
|
@@ -31,23 +32,125 @@ export function projectHarnessFile(harness, cwd = process.cwd()) {
|
|
|
31
32
|
return join(cwd, ".codex", "hooks.json");
|
|
32
33
|
return join(cwd, ".claude", "settings.json");
|
|
33
34
|
}
|
|
35
|
+
/** The personal, per-machine project config for a harness, or null when the harness
|
|
36
|
+
* has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and
|
|
37
|
+
* merges the hooks of both; it is the place for anything that names a path on this
|
|
38
|
+
* machine. Cursor and Codex have no such file. */
|
|
39
|
+
export function localHarnessFile(harness, cwd = process.cwd()) {
|
|
40
|
+
return harness === "claude" ? join(cwd, ".claude", "settings.local.json") : null;
|
|
41
|
+
}
|
|
34
42
|
export function harnessScopes(harness, cwd = process.cwd()) {
|
|
35
43
|
const project = projectHarnessFile(harness, cwd);
|
|
44
|
+
const local = localHarnessFile(harness, cwd);
|
|
36
45
|
const user = userHarnessFile(harness);
|
|
37
46
|
return {
|
|
38
47
|
project: isHarnessConfigured(project) ? project : null,
|
|
48
|
+
local: local && isHarnessConfigured(local) ? local : null,
|
|
39
49
|
user: isHarnessConfigured(user) ? user : null,
|
|
40
50
|
};
|
|
41
51
|
}
|
|
42
52
|
/** One word for where a harness is wired, for a status line. */
|
|
43
53
|
export function harnessScopeLabel(scopes) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
const where = [
|
|
55
|
+
scopes.project ? "project" : "",
|
|
56
|
+
scopes.local ? "this machine" : "",
|
|
57
|
+
scopes.user ? "user" : "",
|
|
58
|
+
].filter(Boolean);
|
|
59
|
+
if (where.length === 0)
|
|
60
|
+
return "";
|
|
61
|
+
if (where.length === 1 && scopes.project)
|
|
47
62
|
return "configured (this project)";
|
|
48
|
-
if (scopes.
|
|
63
|
+
if (where.length === 1 && scopes.local)
|
|
64
|
+
return "configured (this project, this machine only)";
|
|
65
|
+
if (where.length === 1 && scopes.user)
|
|
49
66
|
return "configured (user-level)";
|
|
50
|
-
return ""
|
|
67
|
+
return `configured (${where.join(" + ")})`;
|
|
68
|
+
}
|
|
69
|
+
/** Whether a hook command names a path that exists only on this machine — the pinned
|
|
70
|
+
* `"<node>" "<…/cli.js>" claude` form — rather than the portable `npx` form. */
|
|
71
|
+
export function isMachineSpecificCommand(command) {
|
|
72
|
+
if (/(^|\s)npx(\.cmd)?\s/.test(command))
|
|
73
|
+
return false;
|
|
74
|
+
const first = (command.match(/"[^"]+"|\S+/) ?? [""])[0].replace(/^"|"$/g, "");
|
|
75
|
+
return isAbsolute(first) || /^[A-Za-z]:[\\/]/.test(first);
|
|
76
|
+
}
|
|
77
|
+
export function gitShareState(file) {
|
|
78
|
+
const cwd = dirname(file);
|
|
79
|
+
const git = (args) => {
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(cwd, { recursive: true });
|
|
82
|
+
execFileSync("git", args, { cwd, stdio: "ignore", timeout: 5000 });
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const status = error.status;
|
|
87
|
+
return typeof status === "number" ? status : -1;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
if (git(["rev-parse", "--is-inside-work-tree"]) !== 0)
|
|
91
|
+
return "none";
|
|
92
|
+
if (git(["ls-files", "--error-unmatch", "--", file]) === 0)
|
|
93
|
+
return "tracked";
|
|
94
|
+
return git(["check-ignore", "-q", "--", file]) === 0 ? "ignored" : "untracked";
|
|
95
|
+
}
|
|
96
|
+
/** Keep a file out of git for this clone only, through `.git/info/exclude` — never the
|
|
97
|
+
* repository's own `.gitignore`, which is the team's file. Returns true when the file is
|
|
98
|
+
* ignored afterwards. */
|
|
99
|
+
export function excludeFromGit(file) {
|
|
100
|
+
try {
|
|
101
|
+
const cwd = dirname(file);
|
|
102
|
+
mkdirSync(cwd, { recursive: true });
|
|
103
|
+
// The entry is built from git's own view of where this directory sits in the work
|
|
104
|
+
// tree (`--show-prefix`), not from comparing file-system paths: the same directory
|
|
105
|
+
// can be spelled two ways (a Windows 8.3 short name, a symlinked temp dir), and a
|
|
106
|
+
// path computed across the two spellings excludes the wrong file.
|
|
107
|
+
const prefix = execFileSync("git", ["rev-parse", "--show-prefix"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
108
|
+
const exclude = resolve(cwd, execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], { cwd, encoding: "utf8", timeout: 5000 }).trim());
|
|
109
|
+
const entry = "/" + prefix + basename(file);
|
|
110
|
+
const existing = existsSync(exclude) ? readFileSync(exclude, "utf8") : "";
|
|
111
|
+
if (!existing.split(/\r?\n/).includes(entry)) {
|
|
112
|
+
mkdirSync(dirname(exclude), { recursive: true });
|
|
113
|
+
writeFileSync(exclude, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${entry}\n`);
|
|
114
|
+
}
|
|
115
|
+
return gitShareState(file) === "ignored";
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Remove the Scopebond entries whose command `remove` selects, leaving every other
|
|
122
|
+
* setting alone. Returns the number of entries removed. */
|
|
123
|
+
export function pruneHarnessEntries(file, remove) {
|
|
124
|
+
if (!existsSync(file))
|
|
125
|
+
return 0;
|
|
126
|
+
let config;
|
|
127
|
+
try {
|
|
128
|
+
config = readHarnessConfig(file);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const hooks = isRecord(config.hooks) ? config.hooks : null;
|
|
134
|
+
if (!hooks)
|
|
135
|
+
return 0;
|
|
136
|
+
let removed = 0;
|
|
137
|
+
const commandsOf = (e) => {
|
|
138
|
+
if (!isRecord(e))
|
|
139
|
+
return [];
|
|
140
|
+
const own = isScopebond(e.command) ? [String(e.command)] : [];
|
|
141
|
+
const nested = Array.isArray(e.hooks) ? e.hooks.flatMap(commandsOf) : [];
|
|
142
|
+
return [...own, ...nested];
|
|
143
|
+
};
|
|
144
|
+
for (const [event, value] of Object.entries(hooks)) {
|
|
145
|
+
if (!Array.isArray(value))
|
|
146
|
+
continue;
|
|
147
|
+
const kept = value.filter((e) => !(entryMatches(e) && commandsOf(e).some(remove)));
|
|
148
|
+
removed += value.length - kept.length;
|
|
149
|
+
hooks[event] = kept;
|
|
150
|
+
}
|
|
151
|
+
if (removed > 0)
|
|
152
|
+
writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
|
|
153
|
+
return removed;
|
|
51
154
|
}
|
|
52
155
|
/** The file in the user home that pins trusted project policies: absolute project
|
|
53
156
|
* config dir → SHA-256 of its policy.json. It lives under the protected home, so the
|