javi-forge 1.35.1 → 1.37.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 +5 -0
- package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +97 -29
- package/assets/claude-hooks/manifest.json +1 -1
- package/ci-local/README.md +11 -6
- package/ci-local/ci-local.ps1 +34 -0
- package/ci-local/ci-local.sh +19 -1
- package/ci-local/install.ps1 +23 -0
- package/ci-local/install.sh +18 -1
- package/dist/cli/dispatch/hooks.d.ts +9 -2
- package/dist/cli/dispatch/hooks.js +22 -8
- package/dist/cli/dispatch/simple-renderers.js +7 -0
- package/dist/commands/claude-hooks.js +2 -0
- package/dist/commands/codex-hooks.d.ts +26 -0
- package/dist/commands/codex-hooks.js +106 -0
- package/dist/commands/init.d.ts +9 -1
- package/dist/commands/init.js +10 -6
- package/dist/lib/__fixtures__/claude-hook-ownership.d.ts +6 -0
- package/dist/lib/__fixtures__/claude-hook-ownership.js +7 -1
- package/dist/lib/agent-adapter.d.ts +56 -0
- package/dist/lib/agent-adapter.js +88 -0
- package/dist/lib/claude-hook-manager.d.ts +17 -1
- package/dist/lib/claude-hook-manager.js +20 -4
- package/dist/lib/claude-hook-settings.d.ts +3 -3
- package/dist/lib/claude-hook-settings.js +3 -3
- package/dist/lib/codex-hook-manager.d.ts +178 -0
- package/dist/lib/codex-hook-manager.js +546 -0
- package/dist/lib/platform-support.d.ts +19 -0
- package/dist/lib/platform-support.js +22 -0
- package/dist/lib/secure-fs-transaction.d.ts +25 -5
- package/dist/lib/secure-fs-transaction.js +44 -19
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `javi-forge hooks <install|doctor|repair> codex` — console-only renderer for
|
|
3
|
+
* the Codex PreToolUse guard library (agent-agnostic slice 2). It wires the
|
|
4
|
+
* install/doctor/repair lib fns to human output + exit codes; it adds NO new
|
|
5
|
+
* security logic and never touches `runTransaction`/secure-fs directly.
|
|
6
|
+
*
|
|
7
|
+
* Doctor exit code follows the effective-execution verdict (runnable → 0,
|
|
8
|
+
* blocked → 1, inconclusive → 2), independent of any component health — the same
|
|
9
|
+
* honest-execution contract as the Claude renderer. The UNTRUSTED state is a
|
|
10
|
+
* `blocked` verdict (an untrusted Codex hook is silently skipped), so a fresh
|
|
11
|
+
* install correctly reports blocked until the user grants trust.
|
|
12
|
+
*/
|
|
13
|
+
import { doctorCodexPreToolUse, installCodexPreToolUse, repairCodexPreToolUse, } from "../lib/codex-hook-manager.js";
|
|
14
|
+
function renderWarnings(warnings, log) {
|
|
15
|
+
if (warnings.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
log("warnings:");
|
|
18
|
+
for (const w of warnings)
|
|
19
|
+
log(` ${w}`);
|
|
20
|
+
}
|
|
21
|
+
function renderMutation(verb, result, log, logError) {
|
|
22
|
+
if (result.ok) {
|
|
23
|
+
log(`${verb} codex: ok`);
|
|
24
|
+
if (result.changed.length > 0) {
|
|
25
|
+
log("changed:");
|
|
26
|
+
for (const p of result.changed)
|
|
27
|
+
log(` ${p}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
log("changed: nothing (already up to date)");
|
|
31
|
+
}
|
|
32
|
+
log(`trust: ${result.report.trust.state}`);
|
|
33
|
+
if (result.report.trust.state === "untrusted") {
|
|
34
|
+
log(` → ${result.report.trust.grantCommand}`);
|
|
35
|
+
}
|
|
36
|
+
renderWarnings(result.warnings, log);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
logError(`${verb} codex: refused`);
|
|
40
|
+
for (const e of result.errors)
|
|
41
|
+
logError(` ${e}`);
|
|
42
|
+
renderWarnings(result.warnings, log);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
function renderDoctor(report, log) {
|
|
46
|
+
log(`doctor codex: ${report.healthy ? "healthy" : "unhealthy"}`);
|
|
47
|
+
if (report.platformSupport)
|
|
48
|
+
log(` platform-support: ${report.platformSupport.state} — ${report.platformSupport.guidance}`);
|
|
49
|
+
log(` hooks.json: ${report.hooksJson.state}`);
|
|
50
|
+
log(` config: [features] hooks=${report.config.featuresHooks} (readable: ${report.config.readable})`);
|
|
51
|
+
log(` trust: ${report.trust.state}`);
|
|
52
|
+
log(` asset: ${report.asset.state}`);
|
|
53
|
+
log(` node: ${report.node.version ?? "unavailable"} (min-satisfied: ${report.node.satisfiesMinimum})`);
|
|
54
|
+
const onPath = report.nodeOnPath;
|
|
55
|
+
const onPathDetail = onPath.status === "resolved"
|
|
56
|
+
? ` ${onPath.version}`
|
|
57
|
+
: onPath.status === "unknown"
|
|
58
|
+
? ` — ${onPath.detail}`
|
|
59
|
+
: "";
|
|
60
|
+
log(` node-on-PATH: ${onPath.status}${onPathDetail} (heuristic: this process' PATH)`);
|
|
61
|
+
log(` execution: ${report.execution.status}`);
|
|
62
|
+
if (report.execution.blockers.length > 0) {
|
|
63
|
+
log(" blockers:");
|
|
64
|
+
for (const b of report.execution.blockers)
|
|
65
|
+
log(` - ${b}`);
|
|
66
|
+
}
|
|
67
|
+
if (report.execution.unknownSources.length > 0) {
|
|
68
|
+
log(" unknown-sources:");
|
|
69
|
+
for (const u of report.execution.unknownSources)
|
|
70
|
+
log(` - ${u}`);
|
|
71
|
+
}
|
|
72
|
+
if (report.execution.residual.length > 0) {
|
|
73
|
+
log(" execution-residual:");
|
|
74
|
+
for (const r of report.execution.residual)
|
|
75
|
+
log(` - ${r}`);
|
|
76
|
+
}
|
|
77
|
+
if (report.remediation.length > 0) {
|
|
78
|
+
log(" remediation:");
|
|
79
|
+
for (const r of report.remediation)
|
|
80
|
+
log(` - ${r}`);
|
|
81
|
+
}
|
|
82
|
+
if (report.execution.status === "blocked")
|
|
83
|
+
return 1;
|
|
84
|
+
if (report.execution.status === "inconclusive")
|
|
85
|
+
return 2;
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
export async function runCodexHookCommand(sub, _cwd, opts, deps = {}) {
|
|
89
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
90
|
+
const logError = deps.logError ?? ((m) => console.error(m));
|
|
91
|
+
const install = deps.install ?? installCodexPreToolUse;
|
|
92
|
+
const doctor = deps.doctor ?? doctorCodexPreToolUse;
|
|
93
|
+
const repair = deps.repair ?? repairCodexPreToolUse;
|
|
94
|
+
// Codex config is user-global (~/.codex). `cwd` is accepted for CLI symmetry
|
|
95
|
+
// with the Claude command; the home dir is what the manager operates on.
|
|
96
|
+
const home = deps.homeDir;
|
|
97
|
+
if (sub === "install") {
|
|
98
|
+
return renderMutation("install", await install(home), log, logError);
|
|
99
|
+
}
|
|
100
|
+
if (sub === "repair") {
|
|
101
|
+
const result = await repair(home, { force: opts.force === true });
|
|
102
|
+
return renderMutation("repair", result, log, logError);
|
|
103
|
+
}
|
|
104
|
+
return renderDoctor(await doctor(home), log);
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=codex-hooks.js.map
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PlatformSupport } from "../lib/platform-support.js";
|
|
1
2
|
import type { InitOptions } from "../types/index.js";
|
|
2
3
|
import type { StepCallback } from "./init/types.js";
|
|
3
4
|
/**
|
|
@@ -6,5 +7,12 @@ import type { StepCallback } from "./init/types.js";
|
|
|
6
7
|
*
|
|
7
8
|
* Pure dispatch: every step lives in src/commands/init/steps/.
|
|
8
9
|
*/
|
|
9
|
-
export
|
|
10
|
+
export type InitProjectResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
refusalCode: string;
|
|
15
|
+
platformSupport: PlatformSupport;
|
|
16
|
+
};
|
|
17
|
+
export declare function initProject(options: InitOptions, onStep: StepCallback): Promise<InitProjectResult>;
|
|
10
18
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/commands/init.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ensureDirExists } from "../lib/common.js";
|
|
2
|
+
import { resolvePlatformSupport, } from "../lib/platform-support.js";
|
|
2
3
|
import { stepAgentSkills } from "./init/steps/agent-skills.js";
|
|
3
4
|
import { stepAISync } from "./init/steps/ai-sync.js";
|
|
4
5
|
import { stepCITemplate, stepDependabot } from "./init/steps/ci.js";
|
|
@@ -15,13 +16,15 @@ import { stepMemory } from "./init/steps/memory.js";
|
|
|
15
16
|
import { stepMock } from "./init/steps/mock.js";
|
|
16
17
|
import { stepSDD } from "./init/steps/sdd.js";
|
|
17
18
|
import { stepSecurityHooks } from "./init/steps/security.js";
|
|
18
|
-
/**
|
|
19
|
-
* Main init orchestrator: bootstraps a project with CI, git hooks,
|
|
20
|
-
* memory module, AI config sync, SDD, ghagga, and friends.
|
|
21
|
-
*
|
|
22
|
-
* Pure dispatch: every step lives in src/commands/init/steps/.
|
|
23
|
-
*/
|
|
24
19
|
export async function initProject(options, onStep) {
|
|
20
|
+
const platformSupport = resolvePlatformSupport(process.platform);
|
|
21
|
+
if (platformSupport) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
refusalCode: platformSupport.refusalCode,
|
|
25
|
+
platformSupport,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
25
28
|
const { projectDir, dryRun } = options;
|
|
26
29
|
// Ensure project directory exists before any steps
|
|
27
30
|
if (!dryRun && projectDir) {
|
|
@@ -47,5 +50,6 @@ export async function initProject(options, onStep) {
|
|
|
47
50
|
await stepLocalAi(ctx);
|
|
48
51
|
await stepAgentSkills(ctx);
|
|
49
52
|
await stepManifest(ctx);
|
|
53
|
+
return { ok: true };
|
|
50
54
|
}
|
|
51
55
|
//# sourceMappingURL=init.js.map
|
|
@@ -24,6 +24,12 @@ export declare const LEGACY_FILE_SHA256 = "b4638222ecddc2daac6ec3339596d853a6269
|
|
|
24
24
|
export declare const MANAGED_MATCHER = "Bash|PowerShell|Read|Write|Edit";
|
|
25
25
|
/** Single project-local MJS argument, kept as a literal placeholder path. */
|
|
26
26
|
export declare const MANAGED_ASSET_ARG = "${CLAUDE_PROJECT_DIR}/.claude/hooks/javi-forge-skillguard-pre-tool-use.mjs";
|
|
27
|
+
/**
|
|
28
|
+
* The agent selector the install writer appends after the asset path. The runtime
|
|
29
|
+
* fails closed without it (no agent config = cannot know what to protect), so it is
|
|
30
|
+
* part of the managed command shape and the canonical settings identity.
|
|
31
|
+
*/
|
|
32
|
+
export declare const MANAGED_AGENT_ARG = "--agent=claude";
|
|
27
33
|
/** Exact asset filename under `.claude/hooks/`. */
|
|
28
34
|
export declare const ASSET_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
|
|
29
35
|
/** Exact first-line comment marking the managed asset. */
|
|
@@ -25,6 +25,12 @@ export const LEGACY_FILE_SHA256 = "b4638222ecddc2daac6ec3339596d853a626906bbd123
|
|
|
25
25
|
export const MANAGED_MATCHER = "Bash|PowerShell|Read|Write|Edit";
|
|
26
26
|
/** Single project-local MJS argument, kept as a literal placeholder path. */
|
|
27
27
|
export const MANAGED_ASSET_ARG = "${CLAUDE_PROJECT_DIR}/.claude/hooks/javi-forge-skillguard-pre-tool-use.mjs";
|
|
28
|
+
/**
|
|
29
|
+
* The agent selector the install writer appends after the asset path. The runtime
|
|
30
|
+
* fails closed without it (no agent config = cannot know what to protect), so it is
|
|
31
|
+
* part of the managed command shape and the canonical settings identity.
|
|
32
|
+
*/
|
|
33
|
+
export const MANAGED_AGENT_ARG = "--agent=claude";
|
|
28
34
|
/** Exact asset filename under `.claude/hooks/`. */
|
|
29
35
|
export const ASSET_NAME = "javi-forge-skillguard-pre-tool-use.mjs";
|
|
30
36
|
/** Exact first-line comment marking the managed asset. */
|
|
@@ -44,7 +50,7 @@ export function managedHandler(assetSha = SAMPLE_ASSET_SHA256) {
|
|
|
44
50
|
return {
|
|
45
51
|
type: "command",
|
|
46
52
|
command: "node",
|
|
47
|
-
args: [MANAGED_ASSET_ARG],
|
|
53
|
+
args: [MANAGED_ASSET_ARG, MANAGED_AGENT_ARG],
|
|
48
54
|
timeout: 30,
|
|
49
55
|
statusMessage: managedStatusMessage(assetSha),
|
|
50
56
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent adapter registry (agent-agnostic slice 2). One descriptor per host
|
|
3
|
+
* (`claude`, `codex`) capturing the per-agent facts the SkillGuard installer and
|
|
4
|
+
* doctor need: config paths, the protected managed set, the project-root source,
|
|
5
|
+
* the settings-schema validator (SHARED — both hosts use the identical hooks
|
|
6
|
+
* schema), the managed marker, the deny protocol, and the trust model.
|
|
7
|
+
*
|
|
8
|
+
* This descriptor is additive: Codex is routed through it, while Claude keeps its
|
|
9
|
+
* existing, byte-identical runtime path (the descriptor only DESCRIBES Claude; it
|
|
10
|
+
* does not change how the Claude command executes). The CLI dispatch decides the
|
|
11
|
+
* valid `hooks <sub> <agent>` target set through `isAgentId` (backed by the
|
|
12
|
+
* `AGENT_ADAPTERS` keys) — this registry is the single source of agent truth, and
|
|
13
|
+
* a new agent added here cannot be silently dropped: the dispatch's command-loader
|
|
14
|
+
* map is typed `Record<AgentId, …>`, so omitting a loader is a compile error.
|
|
15
|
+
*/
|
|
16
|
+
import { validateSettingsShape } from "./claude-hook-settings.js";
|
|
17
|
+
export type AgentId = "claude" | "codex";
|
|
18
|
+
export type TrustState = "trusted" | "untrusted" | "unknown";
|
|
19
|
+
export interface TrustDescriptor {
|
|
20
|
+
/** Detect whether the installed hook is TRUSTED from the host config text. */
|
|
21
|
+
detect(configText: string, hooksFile: string): TrustState;
|
|
22
|
+
/** The exact step a human runs to grant trust (no non-interactive path exists). */
|
|
23
|
+
grantCommand(hooksFile: string): string;
|
|
24
|
+
}
|
|
25
|
+
export interface AgentAdapter {
|
|
26
|
+
id: AgentId;
|
|
27
|
+
/** Resolve the two host config files from a base dir (project root or home). */
|
|
28
|
+
configPaths(baseDir: string): {
|
|
29
|
+
hooksFile: string;
|
|
30
|
+
settingsFile: string;
|
|
31
|
+
};
|
|
32
|
+
/** Relative protected paths the guard refuses writes to (from AGENT_CONFIGS). */
|
|
33
|
+
managedSet: readonly string[];
|
|
34
|
+
/** Project-root source: an env var, or null → the envelope `cwd` (Codex). */
|
|
35
|
+
projectDir: {
|
|
36
|
+
envVar: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** SHARED settings-schema validator — the hooks container shape is identical. */
|
|
39
|
+
settingsSchema: typeof validateSettingsShape;
|
|
40
|
+
/** The managed asset marker for this host. */
|
|
41
|
+
marker: string;
|
|
42
|
+
/** Deny protocol emitted by the shared `.mjs` — the same on both hosts. */
|
|
43
|
+
emitDeny: "exit2+stderr";
|
|
44
|
+
/** Hook-trust model, or null when the host has none (Claude). */
|
|
45
|
+
trust: TrustDescriptor | null;
|
|
46
|
+
}
|
|
47
|
+
export declare const claudeAdapter: AgentAdapter;
|
|
48
|
+
export declare const codexAdapter: AgentAdapter;
|
|
49
|
+
export declare const AGENT_ADAPTERS: Record<AgentId, AgentAdapter>;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a raw CLI token names a known agent adapter. Backed by the
|
|
52
|
+
* `AGENT_ADAPTERS` registry so there is ONE source deciding valid targets: adding
|
|
53
|
+
* an adapter to the registry automatically widens the accepted CLI set.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isAgentId(value: unknown): value is AgentId;
|
|
56
|
+
//# sourceMappingURL=agent-adapter.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent adapter registry (agent-agnostic slice 2). One descriptor per host
|
|
3
|
+
* (`claude`, `codex`) capturing the per-agent facts the SkillGuard installer and
|
|
4
|
+
* doctor need: config paths, the protected managed set, the project-root source,
|
|
5
|
+
* the settings-schema validator (SHARED — both hosts use the identical hooks
|
|
6
|
+
* schema), the managed marker, the deny protocol, and the trust model.
|
|
7
|
+
*
|
|
8
|
+
* This descriptor is additive: Codex is routed through it, while Claude keeps its
|
|
9
|
+
* existing, byte-identical runtime path (the descriptor only DESCRIBES Claude; it
|
|
10
|
+
* does not change how the Claude command executes). The CLI dispatch decides the
|
|
11
|
+
* valid `hooks <sub> <agent>` target set through `isAgentId` (backed by the
|
|
12
|
+
* `AGENT_ADAPTERS` keys) — this registry is the single source of agent truth, and
|
|
13
|
+
* a new agent added here cannot be silently dropped: the dispatch's command-loader
|
|
14
|
+
* map is typed `Record<AgentId, …>`, so omitting a loader is a compile error.
|
|
15
|
+
*/
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { ASSET_NAME } from "./__fixtures__/claude-hook-ownership.js";
|
|
18
|
+
import { validateSettingsShape } from "./claude-hook-settings.js";
|
|
19
|
+
import { codexConfigPaths, codexTrustGrantCommand, hasCodexTrustEntry, } from "./codex-hook-manager.js";
|
|
20
|
+
const CLAUDE_MANAGED_SET = [
|
|
21
|
+
".claude/settings.json",
|
|
22
|
+
".claude/settings.local.json",
|
|
23
|
+
".claude/CLAUDE.md",
|
|
24
|
+
"CLAUDE.md",
|
|
25
|
+
".javi-forge/ci.yaml",
|
|
26
|
+
".claude/hooks/",
|
|
27
|
+
".claude/agents/",
|
|
28
|
+
".claude/skills/",
|
|
29
|
+
];
|
|
30
|
+
const CODEX_MANAGED_SET = [
|
|
31
|
+
".codex/hooks.json",
|
|
32
|
+
".claude/settings.json",
|
|
33
|
+
".claude/settings.local.json",
|
|
34
|
+
".claude/CLAUDE.md",
|
|
35
|
+
"CLAUDE.md",
|
|
36
|
+
".javi-forge/ci.yaml",
|
|
37
|
+
".claude/hooks/",
|
|
38
|
+
".claude/agents/",
|
|
39
|
+
".claude/skills/",
|
|
40
|
+
];
|
|
41
|
+
export const claudeAdapter = {
|
|
42
|
+
id: "claude",
|
|
43
|
+
configPaths(projectDir) {
|
|
44
|
+
return {
|
|
45
|
+
hooksFile: path.join(projectDir, ".claude", "hooks", ASSET_NAME),
|
|
46
|
+
settingsFile: path.join(projectDir, ".claude", "settings.json"),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
managedSet: CLAUDE_MANAGED_SET,
|
|
50
|
+
projectDir: { envVar: "CLAUDE_PROJECT_DIR" },
|
|
51
|
+
settingsSchema: validateSettingsShape,
|
|
52
|
+
marker: "// javi-forge-managed: claude-pretooluse v1",
|
|
53
|
+
emitDeny: "exit2+stderr",
|
|
54
|
+
trust: null,
|
|
55
|
+
};
|
|
56
|
+
export const codexAdapter = {
|
|
57
|
+
id: "codex",
|
|
58
|
+
configPaths(homeDir) {
|
|
59
|
+
const paths = codexConfigPaths(homeDir);
|
|
60
|
+
return { hooksFile: paths.hooksFile, settingsFile: paths.configFile };
|
|
61
|
+
},
|
|
62
|
+
managedSet: CODEX_MANAGED_SET,
|
|
63
|
+
projectDir: { envVar: null },
|
|
64
|
+
settingsSchema: validateSettingsShape,
|
|
65
|
+
marker: "// javi-forge-managed: codex-pretooluse v1",
|
|
66
|
+
emitDeny: "exit2+stderr",
|
|
67
|
+
trust: {
|
|
68
|
+
detect(configText, hooksFile) {
|
|
69
|
+
return hasCodexTrustEntry(configText, hooksFile)
|
|
70
|
+
? "trusted"
|
|
71
|
+
: "untrusted";
|
|
72
|
+
},
|
|
73
|
+
grantCommand: codexTrustGrantCommand,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
export const AGENT_ADAPTERS = {
|
|
77
|
+
claude: claudeAdapter,
|
|
78
|
+
codex: codexAdapter,
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Whether a raw CLI token names a known agent adapter. Backed by the
|
|
82
|
+
* `AGENT_ADAPTERS` registry so there is ONE source deciding valid targets: adding
|
|
83
|
+
* an adapter to the registry automatically widens the accepted CLI set.
|
|
84
|
+
*/
|
|
85
|
+
export function isAgentId(value) {
|
|
86
|
+
return typeof value === "string" && Object.hasOwn(AGENT_ADAPTERS, value);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=agent-adapter.js.map
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Slice-3 seams — Slice 3 GROWS this file, it does not relocate this code.
|
|
9
9
|
*/
|
|
10
10
|
import { type ClaudeHookComponentState, type SettingsClassification, type SettingsIdentityManifest } from "./claude-hook-settings.js";
|
|
11
|
+
import { type PlatformSupport } from "./platform-support.js";
|
|
11
12
|
import { type AclCapability, type SpawnFn } from "./secure-fs-posix.js";
|
|
12
13
|
import { type PlatformSecureFs } from "./secure-fs-transaction.js";
|
|
13
14
|
declare const COVERAGE: readonly ["Bash", "PowerShell", "Read", "Write", "Edit"];
|
|
@@ -29,6 +30,7 @@ export interface ClaudeHookAssetClassification {
|
|
|
29
30
|
}
|
|
30
31
|
export interface ClaudeHookDoctorReport {
|
|
31
32
|
healthy: boolean;
|
|
33
|
+
platformSupport?: PlatformSupport;
|
|
32
34
|
settings: {
|
|
33
35
|
state: ClaudeHookComponentState;
|
|
34
36
|
version?: number;
|
|
@@ -207,8 +209,9 @@ export declare function doctorClaudePreToolUse(projectDir: string, options?: {
|
|
|
207
209
|
execution?: ExecutionProbeEnv;
|
|
208
210
|
/** Injectable read-only ACL capability probe (defaults to the real one). */
|
|
209
211
|
aclProbe?: () => Promise<AclCapability>;
|
|
212
|
+
platform?: NodeJS.Platform;
|
|
210
213
|
}): Promise<ClaudeHookDoctorReport>;
|
|
211
|
-
|
|
214
|
+
interface ClaudeHookMutationResultWithReport {
|
|
212
215
|
ok: boolean;
|
|
213
216
|
changed: string[];
|
|
214
217
|
backups: string[];
|
|
@@ -220,7 +223,19 @@ export interface ClaudeHookMutationResult {
|
|
|
220
223
|
* which is strictly worse than an exec-form guard that may not resolve.
|
|
221
224
|
*/
|
|
222
225
|
warnings: string[];
|
|
226
|
+
lifecycleRefusal?: never;
|
|
227
|
+
}
|
|
228
|
+
interface ClaudeHookLifecycleRefusal {
|
|
229
|
+
ok: false;
|
|
230
|
+
changed: string[];
|
|
231
|
+
backups: string[];
|
|
232
|
+
errors: string[];
|
|
233
|
+
warnings: string[];
|
|
234
|
+
/** The lifecycle gate refused before any doctor or installed-state probe. */
|
|
235
|
+
lifecycleRefusal: PlatformSupport;
|
|
236
|
+
report?: never;
|
|
223
237
|
}
|
|
238
|
+
export type ClaudeHookMutationResult = ClaudeHookMutationResultWithReport | ClaudeHookLifecycleRefusal;
|
|
224
239
|
/** Injectable deps so tests drive `_run` with a fake `PlatformSecureFs`. */
|
|
225
240
|
export interface ClaudeHookRunDeps {
|
|
226
241
|
secureFs?: PlatformSecureFs | null;
|
|
@@ -236,6 +251,7 @@ export interface ClaudeHookRunDeps {
|
|
|
236
251
|
aclProbe?: () => Promise<AclCapability>;
|
|
237
252
|
/** Injectable node-on-PATH heuristic, shared by the warning and the report. */
|
|
238
253
|
nodeProbe?: () => Promise<NodeOnPathProbe>;
|
|
254
|
+
doctor?: typeof doctorClaudePreToolUse;
|
|
239
255
|
}
|
|
240
256
|
/** Internal deps-taking entry; tests drive it with a fake `PlatformSecureFs`. */
|
|
241
257
|
export declare function _run(projectDir: string, mode: "install" | "repair", options: {
|
|
@@ -14,7 +14,8 @@ import os from "node:os";
|
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { CLAUDE_HOOK_ASSETS_DIR } from "../constants.js";
|
|
16
16
|
import { ASSET_MANAGED_MARKER, ASSET_NAME, } from "./__fixtures__/claude-hook-ownership.js";
|
|
17
|
-
import { buildManagedContainer, classifySettingsEntry, isPlainObject, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, planForceReplace, planLegacyCohortExcision, planManagedClaudeHookMerge, scanExecutionFlags, } from "./claude-hook-settings.js";
|
|
17
|
+
import { buildManagedContainer, classifySettingsEntry, isPlainObject, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, planForceReplace, planLegacyCohortExcision, planManagedClaudeHookMerge, scanExecutionFlags, } from "./claude-hook-settings.js";
|
|
18
|
+
import { resolvePlatformSupport, } from "./platform-support.js";
|
|
18
19
|
import { safeReadFile } from "./safe-read.js";
|
|
19
20
|
import { ACL_DETAIL, probeAclCapability, selectSecureFs, } from "./secure-fs-posix.js";
|
|
20
21
|
import { runTransaction, } from "./secure-fs-transaction.js";
|
|
@@ -174,8 +175,9 @@ function settingsSignals(value, classification, currentAssetSha) {
|
|
|
174
175
|
const commandShapeExact = handler.type === "command" &&
|
|
175
176
|
handler.command === "node" &&
|
|
176
177
|
Array.isArray(args) &&
|
|
177
|
-
args.length ===
|
|
178
|
+
args.length === 2 &&
|
|
178
179
|
args[0] === MANAGED_ASSET_ARG &&
|
|
180
|
+
args[1] === MANAGED_AGENT_ARG &&
|
|
179
181
|
handler.timeout === 30;
|
|
180
182
|
let assetSettingsConsistent = false;
|
|
181
183
|
if (typeof handler.statusMessage === "string" &&
|
|
@@ -536,7 +538,9 @@ export async function doctorClaudePreToolUse(projectDir, options) {
|
|
|
536
538
|
execution.blockers.some((blocker) => blocker.startsWith("guard:"))) {
|
|
537
539
|
remediation.add(aclRemediation);
|
|
538
540
|
}
|
|
541
|
+
const platformSupport = resolvePlatformSupport(options?.platform ?? process.platform);
|
|
539
542
|
return {
|
|
543
|
+
...(platformSupport ? { platformSupport } : {}),
|
|
540
544
|
healthy,
|
|
541
545
|
settings: {
|
|
542
546
|
state: settings.state,
|
|
@@ -711,8 +715,20 @@ function serializeSettings(container) {
|
|
|
711
715
|
}
|
|
712
716
|
/** Internal deps-taking entry; tests drive it with a fake `PlatformSecureFs`. */
|
|
713
717
|
export async function _run(projectDir, mode, options, deps) {
|
|
714
|
-
const manifest = deps.manifest ?? (await readManifest());
|
|
715
718
|
const platform = deps.platform ?? process.platform;
|
|
719
|
+
const platformSupport = resolvePlatformSupport(platform);
|
|
720
|
+
if (platformSupport) {
|
|
721
|
+
return {
|
|
722
|
+
ok: false,
|
|
723
|
+
changed: [],
|
|
724
|
+
backups: [],
|
|
725
|
+
errors: [platformSupport.refusalCode],
|
|
726
|
+
warnings: [platformSupport.guidance],
|
|
727
|
+
lifecycleRefusal: platformSupport,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const doctorFn = deps.doctor ?? doctorClaudePreToolUse;
|
|
731
|
+
const manifest = deps.manifest ?? (await readManifest());
|
|
716
732
|
const secureFs = deps.secureFs !== undefined ? deps.secureFs : selectSecureFs(platform);
|
|
717
733
|
const clock = deps.clock ?? (() => new Date());
|
|
718
734
|
const nonce = deps.nonce ?? (() => randomBytes(4).toString("hex"));
|
|
@@ -724,7 +740,7 @@ export async function _run(projectDir, mode, options, deps) {
|
|
|
724
740
|
// report, so the run never spawns `node --version` twice and the warning, the
|
|
725
741
|
// report row and the verdict can never disagree about the same PATH.
|
|
726
742
|
const nodeOnPath = await (deps.nodeProbe ?? probeNodeOnPath)();
|
|
727
|
-
const doctor = () =>
|
|
743
|
+
const doctor = () => doctorFn(projectDir, {
|
|
728
744
|
manifest,
|
|
729
745
|
aclProbe: deps.aclProbe,
|
|
730
746
|
execution: { nodeProbe: async () => nodeOnPath },
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* removal/merge PLANNING only. Identity is always recomputed from observed
|
|
9
9
|
* structure; a marker only claims ownership, it never proves it.
|
|
10
10
|
*/
|
|
11
|
-
import { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX } from "./__fixtures__/claude-hook-ownership.js";
|
|
12
|
-
export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
|
|
11
|
+
import { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX } from "./__fixtures__/claude-hook-ownership.js";
|
|
12
|
+
export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
|
|
13
13
|
/** The nine independent component states (identical for asset and settings). */
|
|
14
14
|
export type ClaudeHookComponentState = "absent" | "managed-current" | "released-outdated" | "exact-legacy" | "edited-managed" | "foreign" | "symlink" | "non-regular" | "malformed";
|
|
15
15
|
/** A released settings-entry identity: version plus placeholder-normalized hash. */
|
|
@@ -149,7 +149,7 @@ export declare function planManagedClaudeHookMerge(parsed: unknown, currentAsset
|
|
|
149
149
|
export interface ManagedHandler {
|
|
150
150
|
type: "command";
|
|
151
151
|
command: "node";
|
|
152
|
-
args: [string];
|
|
152
|
+
args: [string, string];
|
|
153
153
|
timeout: number;
|
|
154
154
|
statusMessage: string;
|
|
155
155
|
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* structure; a marker only claims ownership, it never proves it.
|
|
10
10
|
*/
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
|
-
import { ASSET_SHA_PLACEHOLDER, LEGACY_COHORT, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, } from "./__fixtures__/claude-hook-ownership.js";
|
|
13
|
-
export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
|
|
12
|
+
import { ASSET_SHA_PLACEHOLDER, LEGACY_COHORT, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, } from "./__fixtures__/claude-hook-ownership.js";
|
|
13
|
+
export { ASSET_SHA_PLACEHOLDER, LEGACY_FILE_SHA256, MANAGED_AGENT_ARG, MANAGED_ASSET_ARG, MANAGED_MATCHER, MANAGED_STATUS_PREFIX, };
|
|
14
14
|
// Shape helpers
|
|
15
15
|
export function isPlainObject(value) {
|
|
16
16
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -443,7 +443,7 @@ export function buildManagedContainer(currentAssetSha) {
|
|
|
443
443
|
{
|
|
444
444
|
type: "command",
|
|
445
445
|
command: "node",
|
|
446
|
-
args: [MANAGED_ASSET_ARG],
|
|
446
|
+
args: [MANAGED_ASSET_ARG, MANAGED_AGENT_ARG],
|
|
447
447
|
timeout: MANAGED_TIMEOUT,
|
|
448
448
|
statusMessage: `${MANAGED_STATUS_PREFIX}${currentAssetSha}`,
|
|
449
449
|
},
|