javi-forge 1.37.0 → 1.38.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 -5
- package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +13 -11
- package/assets/claude-hooks/manifest.json +35 -1
- package/assets/hooks/manifest.json +6 -2
- package/assets/hooks/pre-push +206 -8
- package/ci-local/README.md +3 -6
- package/ci-local/ci-local.ps1 +36 -10
- package/ci-local/ci-local.sh +26 -9
- package/ci-local/install.ps1 +36 -14
- package/ci-local/install.sh +27 -10
- package/ci-local/lib/common.sh +3 -7
- package/ci-local/uninstall.ps1 +50 -0
- package/ci-local/uninstall.sh +38 -3
- package/dist/cli/bootstrap.d.ts +12 -0
- package/dist/cli/bootstrap.js +12 -0
- package/dist/cli/dispatch/simple-renderers.d.ts +8 -1
- package/dist/cli/dispatch/simple-renderers.js +11 -2
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +85 -0
- package/dist/commands/claude-hooks.d.ts +1 -0
- package/dist/commands/claude-hooks.js +12 -3
- package/dist/commands/codex-hooks.d.ts +1 -0
- package/dist/commands/codex-hooks.js +12 -3
- package/dist/commands/doctor.d.ts +16 -1
- package/dist/commands/doctor.js +54 -35
- package/dist/commands/init.d.ts +4 -1
- package/dist/commands/init.js +2 -2
- package/dist/index.js +4 -81
- package/dist/lib/claude-hook-manager.d.ts +11 -6
- package/dist/lib/claude-hook-manager.js +28 -21
- package/dist/lib/codex-hook-manager.d.ts +10 -4
- package/dist/lib/codex-hook-manager.js +44 -13
- package/dist/lib/platform-support.d.ts +18 -14
- package/dist/lib/platform-support.js +19 -17
- package/dist/lib/secure-fs-posix.d.ts +8 -13
- package/dist/lib/secure-fs-posix.js +11 -52
- package/dist/lib/secure-fs-transaction.d.ts +2 -3
- package/dist/types/index.d.ts +7 -2
- package/dist/ui/Doctor.d.ts +2 -0
- package/dist/ui/Doctor.js +13 -0
- package/lib/common.sh +3 -7
- package/package.json +7 -2
package/dist/commands/doctor.js
CHANGED
|
@@ -4,40 +4,45 @@ import { FORGE_ROOT, MODULES_DIR, PLUGINS_DIR, TEMPLATES_DIR, } from "../constan
|
|
|
4
4
|
import { detectStack } from "../lib/common.js";
|
|
5
5
|
import { refreshContextDir } from "../lib/context.js";
|
|
6
6
|
import { execFileAsync } from "../lib/exec.js";
|
|
7
|
+
import { resolvePlatformSupport } from "../lib/platform-support.js";
|
|
7
8
|
import { listInstalledPlugins } from "../lib/plugin.js";
|
|
8
|
-
/** Resolve a binary name to its full path, returns null if not found */
|
|
9
|
-
async function which(bin) {
|
|
9
|
+
/** Resolve a binary name to its first full path, returns null if not found. */
|
|
10
|
+
async function which(bin, exec, platform) {
|
|
10
11
|
try {
|
|
11
|
-
const
|
|
12
|
-
|
|
12
|
+
const command = platform === "win32" ? "where.exe" : "which";
|
|
13
|
+
const { stdout } = await exec(command, [bin]);
|
|
14
|
+
return (stdout
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.map((entry) => entry.trim())
|
|
17
|
+
.find(Boolean) ?? null);
|
|
13
18
|
}
|
|
14
19
|
catch {
|
|
15
20
|
return null;
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
/** Read the forge manifest from a project directory */
|
|
19
|
-
async function readManifest(projectDir) {
|
|
24
|
+
async function readManifest(projectDir, filesystem) {
|
|
20
25
|
const manifestPath = path.join(projectDir, ".javi-forge", "manifest.json");
|
|
21
|
-
if (!(await
|
|
26
|
+
if (!(await filesystem.pathExists(manifestPath)))
|
|
22
27
|
return null;
|
|
23
28
|
try {
|
|
24
|
-
return (await
|
|
29
|
+
return (await filesystem.readJson(manifestPath));
|
|
25
30
|
}
|
|
26
31
|
catch {
|
|
27
32
|
return null;
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
/** Count entries in a directory */
|
|
31
|
-
async function countDir(dir) {
|
|
32
|
-
if (!(await
|
|
36
|
+
async function countDir(dir, filesystem) {
|
|
37
|
+
if (!(await filesystem.pathExists(dir)))
|
|
33
38
|
return 0;
|
|
34
|
-
const entries = await
|
|
39
|
+
const entries = await filesystem.readdir(dir);
|
|
35
40
|
return entries.filter((e) => !e.startsWith(".")).length;
|
|
36
41
|
}
|
|
37
42
|
/** Read a single git config value, "" when unset or on any error. */
|
|
38
|
-
async function gitConfigValue(cwd, key) {
|
|
43
|
+
async function gitConfigValue(cwd, key, exec) {
|
|
39
44
|
try {
|
|
40
|
-
const { stdout } = await
|
|
45
|
+
const { stdout } = await exec("git", ["config", "--get", key], {
|
|
41
46
|
cwd,
|
|
42
47
|
});
|
|
43
48
|
return stdout.trim();
|
|
@@ -70,9 +75,9 @@ export function parseRemote(url) {
|
|
|
70
75
|
* a recommendation, never a gate (a hook enforcing it was trivially bypassed
|
|
71
76
|
* with `--no-verify`).
|
|
72
77
|
*/
|
|
73
|
-
async function commitSigningCheck(cwd) {
|
|
74
|
-
const gpgSign = await gitConfigValue(cwd, "commit.gpgsign");
|
|
75
|
-
const signingKey = await gitConfigValue(cwd, "user.signingkey");
|
|
78
|
+
async function commitSigningCheck(cwd, exec) {
|
|
79
|
+
const gpgSign = await gitConfigValue(cwd, "commit.gpgsign", exec);
|
|
80
|
+
const signingKey = await gitConfigValue(cwd, "user.signingkey", exec);
|
|
76
81
|
if (gpgSign === "true" && signingKey) {
|
|
77
82
|
return {
|
|
78
83
|
label: "Commit signing",
|
|
@@ -93,8 +98,8 @@ async function commitSigningCheck(cwd) {
|
|
|
93
98
|
* probe or protection is a non-alarming `skip` (advisory, not a failure).
|
|
94
99
|
* GitLab or no `gh` → `skip` with a note to verify in the forge UI.
|
|
95
100
|
*/
|
|
96
|
-
async function branchProtectionCheck(cwd) {
|
|
97
|
-
const remote = parseRemote(await gitConfigValue(cwd, "remote.origin.url"));
|
|
101
|
+
async function branchProtectionCheck(cwd, exec, platform) {
|
|
102
|
+
const remote = parseRemote(await gitConfigValue(cwd, "remote.origin.url", exec));
|
|
98
103
|
const label = "Branch protection";
|
|
99
104
|
if (!remote || remote.host === "other") {
|
|
100
105
|
return {
|
|
@@ -103,7 +108,7 @@ async function branchProtectionCheck(cwd) {
|
|
|
103
108
|
detail: "no GitHub/GitLab origin — verify branch protection in the forge UI",
|
|
104
109
|
};
|
|
105
110
|
}
|
|
106
|
-
if (remote.host === "gitlab" || !(await which("gh"))) {
|
|
111
|
+
if (remote.host === "gitlab" || !(await which("gh", exec, platform))) {
|
|
107
112
|
return {
|
|
108
113
|
label,
|
|
109
114
|
status: "skip",
|
|
@@ -113,14 +118,14 @@ async function branchProtectionCheck(cwd) {
|
|
|
113
118
|
// gh + GitHub: resolve the default branch, then probe protection.
|
|
114
119
|
let branch = "main";
|
|
115
120
|
try {
|
|
116
|
-
const { stdout } = await
|
|
121
|
+
const { stdout } = await exec("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd });
|
|
117
122
|
branch = stdout.trim().replace(/^origin\//, "") || "main";
|
|
118
123
|
}
|
|
119
124
|
catch {
|
|
120
125
|
/* fall back to main */
|
|
121
126
|
}
|
|
122
127
|
try {
|
|
123
|
-
await
|
|
128
|
+
await exec("gh", [
|
|
124
129
|
"api",
|
|
125
130
|
`repos/${remote.owner}/${remote.repo}/branches/${branch}/protection`,
|
|
126
131
|
]);
|
|
@@ -141,8 +146,22 @@ async function branchProtectionCheck(cwd) {
|
|
|
141
146
|
/**
|
|
142
147
|
* Run comprehensive health checks for the project and framework.
|
|
143
148
|
*/
|
|
144
|
-
export async function runDoctor(projectDir) {
|
|
145
|
-
const
|
|
149
|
+
export async function runDoctor(projectDir, deps = {}) {
|
|
150
|
+
const platform = deps.platform ?? process.platform;
|
|
151
|
+
const platformSupport = resolvePlatformSupport(platform);
|
|
152
|
+
if (platformSupport) {
|
|
153
|
+
return {
|
|
154
|
+
state: "unsupported-platform",
|
|
155
|
+
guidance: platformSupport.guidance,
|
|
156
|
+
sections: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const cwd = projectDir ?? (deps.cwd ?? process.cwd)();
|
|
160
|
+
const filesystem = deps.filesystem ?? fs;
|
|
161
|
+
const exec = deps.exec ?? execFileAsync;
|
|
162
|
+
const stackDetector = deps.stackDetector ?? detectStack;
|
|
163
|
+
const pluginLister = deps.pluginLister ?? listInstalledPlugins;
|
|
164
|
+
const contextRefresher = deps.contextRefresher ?? refreshContextDir;
|
|
146
165
|
const sections = [];
|
|
147
166
|
// ── 1. System Tools ────────────────────────────────────────────────────────
|
|
148
167
|
const toolChecks = [];
|
|
@@ -154,12 +173,12 @@ export async function runDoctor(projectDir) {
|
|
|
154
173
|
{ name: "pnpm", label: "pnpm" },
|
|
155
174
|
];
|
|
156
175
|
for (const tool of tools) {
|
|
157
|
-
const bin = await which(tool.name);
|
|
176
|
+
const bin = await which(tool.name, exec, platform);
|
|
158
177
|
if (bin) {
|
|
159
178
|
// Try to get version
|
|
160
179
|
let version = "";
|
|
161
180
|
try {
|
|
162
|
-
const { stdout } = await
|
|
181
|
+
const { stdout } = await exec(tool.name, ["--version"]);
|
|
163
182
|
version = stdout.trim().split("\n")[0] ?? "";
|
|
164
183
|
}
|
|
165
184
|
catch {
|
|
@@ -184,8 +203,8 @@ export async function runDoctor(projectDir) {
|
|
|
184
203
|
// L4/L6 (commit signing) and L5 (branch protection) are advisories, NOT hooks
|
|
185
204
|
// (hook-consolidation D9): doctor reports, nothing blocks.
|
|
186
205
|
const securityChecks = [
|
|
187
|
-
await commitSigningCheck(cwd),
|
|
188
|
-
await branchProtectionCheck(cwd),
|
|
206
|
+
await commitSigningCheck(cwd, exec),
|
|
207
|
+
await branchProtectionCheck(cwd, exec, platform),
|
|
189
208
|
];
|
|
190
209
|
sections.push({ title: "Security", checks: securityChecks });
|
|
191
210
|
// ── 2. Framework Structure ─────────────────────────────────────────────────
|
|
@@ -197,8 +216,8 @@ export async function runDoctor(projectDir) {
|
|
|
197
216
|
{ path: path.join(FORGE_ROOT, "ci-local"), label: "ci-local/" },
|
|
198
217
|
];
|
|
199
218
|
for (const dir of expectedDirs) {
|
|
200
|
-
if (await
|
|
201
|
-
const count = await countDir(dir.path);
|
|
219
|
+
if (await filesystem.pathExists(dir.path)) {
|
|
220
|
+
const count = await countDir(dir.path, filesystem);
|
|
202
221
|
structureChecks.push({
|
|
203
222
|
label: dir.label,
|
|
204
223
|
status: "ok",
|
|
@@ -216,7 +235,7 @@ export async function runDoctor(projectDir) {
|
|
|
216
235
|
sections.push({ title: "Framework Structure", checks: structureChecks });
|
|
217
236
|
// ── 3. Stack Detection ─────────────────────────────────────────────────────
|
|
218
237
|
const stackChecks = [];
|
|
219
|
-
const detection = await
|
|
238
|
+
const detection = await stackDetector(cwd);
|
|
220
239
|
if (detection) {
|
|
221
240
|
stackChecks.push({
|
|
222
241
|
label: "Detected stack",
|
|
@@ -234,7 +253,7 @@ export async function runDoctor(projectDir) {
|
|
|
234
253
|
sections.push({ title: "Stack Detection", checks: stackChecks });
|
|
235
254
|
// ── 4. Project Manifest ────────────────────────────────────────────────────
|
|
236
255
|
const manifestChecks = [];
|
|
237
|
-
const manifest = await readManifest(cwd);
|
|
256
|
+
const manifest = await readManifest(cwd, filesystem);
|
|
238
257
|
if (manifest) {
|
|
239
258
|
manifestChecks.push({
|
|
240
259
|
label: "Forge manifest",
|
|
@@ -267,7 +286,7 @@ export async function runDoctor(projectDir) {
|
|
|
267
286
|
const moduleNames = ["engram", "obsidian-brain", "memory-simple", "ghagga"];
|
|
268
287
|
for (const mod of moduleNames) {
|
|
269
288
|
const modPath = path.join(cwd, ".javi-forge", "modules", mod);
|
|
270
|
-
if (await
|
|
289
|
+
if (await filesystem.pathExists(modPath)) {
|
|
271
290
|
moduleChecks.push({ label: mod, status: "ok", detail: "installed" });
|
|
272
291
|
}
|
|
273
292
|
else {
|
|
@@ -281,9 +300,9 @@ export async function runDoctor(projectDir) {
|
|
|
281
300
|
sections.push({ title: "Installed Modules", checks: moduleChecks });
|
|
282
301
|
// ── 6. Plugins ─────────────────────────────────────────────────────────────
|
|
283
302
|
const pluginChecks = [];
|
|
284
|
-
const pluginsDirExists = await
|
|
303
|
+
const pluginsDirExists = await filesystem.pathExists(PLUGINS_DIR);
|
|
285
304
|
if (pluginsDirExists) {
|
|
286
|
-
const plugins = await
|
|
305
|
+
const plugins = await pluginLister();
|
|
287
306
|
if (plugins.length > 0) {
|
|
288
307
|
for (const plugin of plugins) {
|
|
289
308
|
pluginChecks.push({
|
|
@@ -312,7 +331,7 @@ export async function runDoctor(projectDir) {
|
|
|
312
331
|
// ── 7. Context Directory Refresh ───────────────────────────────────────────
|
|
313
332
|
const contextChecks = [];
|
|
314
333
|
try {
|
|
315
|
-
const result = await
|
|
334
|
+
const result = await contextRefresher(cwd);
|
|
316
335
|
if (result) {
|
|
317
336
|
contextChecks.push({
|
|
318
337
|
label: ".context/ refresh",
|
|
@@ -345,6 +364,6 @@ export async function runDoctor(projectDir) {
|
|
|
345
364
|
});
|
|
346
365
|
}
|
|
347
366
|
sections.push({ title: "Context Directory", checks: contextChecks });
|
|
348
|
-
return { sections };
|
|
367
|
+
return { state: "supported", sections };
|
|
349
368
|
}
|
|
350
369
|
//# sourceMappingURL=doctor.js.map
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -14,5 +14,8 @@ export type InitProjectResult = {
|
|
|
14
14
|
refusalCode: string;
|
|
15
15
|
platformSupport: PlatformSupport;
|
|
16
16
|
};
|
|
17
|
-
export
|
|
17
|
+
export interface InitProjectDeps {
|
|
18
|
+
platform?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function initProject(options: InitOptions, onStep: StepCallback, deps?: InitProjectDeps): Promise<InitProjectResult>;
|
|
18
21
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/commands/init.js
CHANGED
|
@@ -16,8 +16,8 @@ import { stepMemory } from "./init/steps/memory.js";
|
|
|
16
16
|
import { stepMock } from "./init/steps/mock.js";
|
|
17
17
|
import { stepSDD } from "./init/steps/sdd.js";
|
|
18
18
|
import { stepSecurityHooks } from "./init/steps/security.js";
|
|
19
|
-
export async function initProject(options, onStep) {
|
|
20
|
-
const platformSupport = resolvePlatformSupport(process.platform);
|
|
19
|
+
export async function initProject(options, onStep, deps = {}) {
|
|
20
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
21
21
|
if (platformSupport) {
|
|
22
22
|
return {
|
|
23
23
|
ok: false,
|
package/dist/index.js
CHANGED
|
@@ -1,84 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { handleSecurity } from "./cli/dispatch/security.js";
|
|
7
|
-
import { handleAnalyze, handleDoctor, handleInitDefault, handleLlmsTxt, handlePlugin, } from "./cli/dispatch/simple-renderers.js";
|
|
8
|
-
import { handleSkillPublish } from "./cli/dispatch/skill-publish.js";
|
|
9
|
-
import { handleSkillsCmd } from "./cli/dispatch/skills-cmd.js";
|
|
10
|
-
import { handleTdd } from "./cli/dispatch/tdd.js";
|
|
11
|
-
import { handleWorkflow } from "./cli/dispatch/workflow.js";
|
|
12
|
-
import { FLAGS_SCHEMA, HELP_TEXT } from "./cli/help.js";
|
|
13
|
-
import { createInkStdin, detectCI, setupUpdateNotifier, } from "./cli/runtime.js";
|
|
14
|
-
// Check for updates in background (non-blocking, cached 24h)
|
|
15
|
-
const _require = createRequire(import.meta.url);
|
|
16
|
-
const pkg = _require("../package.json");
|
|
17
|
-
setupUpdateNotifier(pkg);
|
|
18
|
-
const cli = meow(HELP_TEXT, {
|
|
19
|
-
importMeta: import.meta,
|
|
20
|
-
flags: FLAGS_SCHEMA,
|
|
21
|
-
// Handle --help manually so `ci --help` can show ci-specific usage instead
|
|
22
|
-
// of the global banner (meow's autoHelp would print + exit before dispatch).
|
|
23
|
-
autoHelp: false,
|
|
24
|
-
});
|
|
25
|
-
const subcommand = cli.input[0] ?? "init";
|
|
26
|
-
// Global --help: every command except `ci` and `hooks` shows the global banner
|
|
27
|
-
// here. Those two own their per-command help inside their handlers.
|
|
28
|
-
if (cli.flags.help && subcommand !== "ci" && subcommand !== "hooks") {
|
|
29
|
-
console.log(HELP_TEXT);
|
|
30
|
-
process.exit(0);
|
|
31
|
-
}
|
|
32
|
-
const isCI = detectCI(cli.flags);
|
|
33
|
-
const inkStdin = createInkStdin();
|
|
34
|
-
switch (subcommand) {
|
|
35
|
-
case "tdd": {
|
|
36
|
-
await handleTdd(cli);
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
case "ci": {
|
|
40
|
-
await handleCi(cli, { inkStdin, isCI });
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
case "hooks": {
|
|
44
|
-
await handleHooks(cli);
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
case "doctor": {
|
|
48
|
-
handleDoctor(cli, { inkStdin, isCI });
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
case "analyze": {
|
|
52
|
-
handleAnalyze(cli, { inkStdin, isCI });
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
case "workflow": {
|
|
56
|
-
await handleWorkflow(cli);
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
case "llms-txt": {
|
|
60
|
-
handleLlmsTxt(cli, { inkStdin, isCI });
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
case "plugin": {
|
|
64
|
-
handlePlugin(cli, { inkStdin, isCI });
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
case "skills": {
|
|
68
|
-
await handleSkillsCmd(cli, { inkStdin, isCI });
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
case "skill": {
|
|
72
|
-
await handleSkillPublish(cli);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
case "security": {
|
|
76
|
-
await handleSecurity(cli);
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
default: {
|
|
80
|
-
handleInitDefault(cli, { inkStdin, isCI });
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
2
|
+
import { bootstrapCli } from "./cli/bootstrap.js";
|
|
3
|
+
const bootstrapResult = await bootstrapCli(process.platform, () => import("./cli/main.js"), (message) => console.error(message));
|
|
4
|
+
if (bootstrapResult.state === "unsupported-platform") {
|
|
5
|
+
process.exitCode = bootstrapResult.exitCode;
|
|
83
6
|
}
|
|
84
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -30,7 +30,6 @@ export interface ClaudeHookAssetClassification {
|
|
|
30
30
|
}
|
|
31
31
|
export interface ClaudeHookDoctorReport {
|
|
32
32
|
healthy: boolean;
|
|
33
|
-
platformSupport?: PlatformSupport;
|
|
34
33
|
settings: {
|
|
35
34
|
state: ClaudeHookComponentState;
|
|
36
35
|
version?: number;
|
|
@@ -73,6 +72,12 @@ export interface ClaudeHookDoctorReport {
|
|
|
73
72
|
*/
|
|
74
73
|
nodeOnPath: NodeOnPathProbe;
|
|
75
74
|
}
|
|
75
|
+
export interface ClaudeHookDoctorRefusal {
|
|
76
|
+
state: "unsupported-platform";
|
|
77
|
+
healthy: false;
|
|
78
|
+
platformSupport: PlatformSupport;
|
|
79
|
+
}
|
|
80
|
+
export type ClaudeHookDoctorResult = ClaudeHookDoctorReport | ClaudeHookDoctorRefusal;
|
|
76
81
|
/**
|
|
77
82
|
* Classify the asset into one of nine states from observed bytes only. Never
|
|
78
83
|
* trusts a claimed hash: the full-file SHA is always recomputed and compared to
|
|
@@ -158,8 +163,8 @@ export interface ManagedSettingsPaths {
|
|
|
158
163
|
file: string;
|
|
159
164
|
dropInDir: string;
|
|
160
165
|
}
|
|
161
|
-
/** Static managed-settings locations
|
|
162
|
-
export declare function resolveManagedSettingsPaths(platform: NodeJS.Platform): ManagedSettingsPaths;
|
|
166
|
+
/** Static managed-settings locations for supported hosts only (no fs). */
|
|
167
|
+
export declare function resolveManagedSettingsPaths(platform: NodeJS.Platform): ManagedSettingsPaths | null;
|
|
163
168
|
/**
|
|
164
169
|
* Probe one settings source path for the two documented hook-neutralizing
|
|
165
170
|
* flags. Fail-closed: only a genuinely-absent path or a cleanly-parsed source
|
|
@@ -210,7 +215,7 @@ export declare function doctorClaudePreToolUse(projectDir: string, options?: {
|
|
|
210
215
|
/** Injectable read-only ACL capability probe (defaults to the real one). */
|
|
211
216
|
aclProbe?: () => Promise<AclCapability>;
|
|
212
217
|
platform?: NodeJS.Platform;
|
|
213
|
-
}): Promise<
|
|
218
|
+
}): Promise<ClaudeHookDoctorResult>;
|
|
214
219
|
interface ClaudeHookMutationResultWithReport {
|
|
215
220
|
ok: boolean;
|
|
216
221
|
changed: string[];
|
|
@@ -257,9 +262,9 @@ export interface ClaudeHookRunDeps {
|
|
|
257
262
|
export declare function _run(projectDir: string, mode: "install" | "repair", options: {
|
|
258
263
|
force?: boolean;
|
|
259
264
|
}, deps: ClaudeHookRunDeps): Promise<ClaudeHookMutationResult>;
|
|
260
|
-
export declare function installClaudePreToolUse(projectDir: string): Promise<ClaudeHookMutationResult>;
|
|
265
|
+
export declare function installClaudePreToolUse(projectDir: string, deps?: ClaudeHookRunDeps): Promise<ClaudeHookMutationResult>;
|
|
261
266
|
export declare function repairClaudePreToolUse(projectDir: string, options?: {
|
|
262
267
|
force?: boolean;
|
|
263
|
-
}): Promise<ClaudeHookMutationResult>;
|
|
268
|
+
}, deps?: ClaudeHookRunDeps): Promise<ClaudeHookMutationResult>;
|
|
264
269
|
export {};
|
|
265
270
|
//# sourceMappingURL=claude-hook-manager.d.ts.map
|
|
@@ -258,15 +258,8 @@ export async function probeNodeOnPath(spawn = defaultNodeSpawn) {
|
|
|
258
258
|
}
|
|
259
259
|
return { status: "resolved", version: banner, major: Number(match[1]) };
|
|
260
260
|
}
|
|
261
|
-
/** Static managed-settings locations
|
|
261
|
+
/** Static managed-settings locations for supported hosts only (no fs). */
|
|
262
262
|
export function resolveManagedSettingsPaths(platform) {
|
|
263
|
-
if (platform === "darwin") {
|
|
264
|
-
const base = "/Library/Application Support/ClaudeCode";
|
|
265
|
-
return {
|
|
266
|
-
file: `${base}/managed-settings.json`,
|
|
267
|
-
dropInDir: `${base}/managed-settings.d`,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
263
|
if (platform === "win32") {
|
|
271
264
|
const base = "C:\\Program Files\\ClaudeCode";
|
|
272
265
|
return {
|
|
@@ -274,6 +267,8 @@ export function resolveManagedSettingsPaths(platform) {
|
|
|
274
267
|
dropInDir: `${base}\\managed-settings.d`,
|
|
275
268
|
};
|
|
276
269
|
}
|
|
270
|
+
if (platform !== "linux")
|
|
271
|
+
return null;
|
|
277
272
|
const base = "/etc/claude-code";
|
|
278
273
|
return {
|
|
279
274
|
file: `${base}/managed-settings.json`,
|
|
@@ -379,10 +374,10 @@ export async function probeExecution(projectDir, componentStates, env = {}) {
|
|
|
379
374
|
const homeDir = env.homeDir ?? os.homedir();
|
|
380
375
|
const processEnv = env.env ?? process.env;
|
|
381
376
|
const resolved = resolveManagedSettingsPaths(platform);
|
|
382
|
-
const managedFile = env.managedFile !== undefined ? env.managedFile : resolved
|
|
377
|
+
const managedFile = env.managedFile !== undefined ? env.managedFile : resolved?.file;
|
|
383
378
|
const dropInDir = env.managedDropInDir !== undefined
|
|
384
379
|
? env.managedDropInDir
|
|
385
|
-
: resolved
|
|
380
|
+
: resolved?.dropInDir;
|
|
386
381
|
const specs = [
|
|
387
382
|
{
|
|
388
383
|
label: "project",
|
|
@@ -485,6 +480,14 @@ export async function probeExecution(projectDir, componentStates, env = {}) {
|
|
|
485
480
|
* The `execution` verdict is INDEPENDENT of `healthy`.
|
|
486
481
|
*/
|
|
487
482
|
export async function doctorClaudePreToolUse(projectDir, options) {
|
|
483
|
+
const platformSupport = resolvePlatformSupport(options?.platform ?? process.platform);
|
|
484
|
+
if (platformSupport) {
|
|
485
|
+
return {
|
|
486
|
+
state: "unsupported-platform",
|
|
487
|
+
healthy: false,
|
|
488
|
+
platformSupport,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
488
491
|
const manifest = options?.manifest ?? (await readManifest());
|
|
489
492
|
const currentAssetSha = manifest.asset.sha256;
|
|
490
493
|
const asset = await classifyAssetState(path.join(projectDir, ".claude", "hooks", ASSET_NAME), manifest);
|
|
@@ -538,9 +541,7 @@ export async function doctorClaudePreToolUse(projectDir, options) {
|
|
|
538
541
|
execution.blockers.some((blocker) => blocker.startsWith("guard:"))) {
|
|
539
542
|
remediation.add(aclRemediation);
|
|
540
543
|
}
|
|
541
|
-
const platformSupport = resolvePlatformSupport(options?.platform ?? process.platform);
|
|
542
544
|
return {
|
|
543
|
-
...(platformSupport ? { platformSupport } : {}),
|
|
544
545
|
healthy,
|
|
545
546
|
settings: {
|
|
546
547
|
state: settings.state,
|
|
@@ -740,11 +741,17 @@ export async function _run(projectDir, mode, options, deps) {
|
|
|
740
741
|
// report, so the run never spawns `node --version` twice and the warning, the
|
|
741
742
|
// report row and the verdict can never disagree about the same PATH.
|
|
742
743
|
const nodeOnPath = await (deps.nodeProbe ?? probeNodeOnPath)();
|
|
743
|
-
const doctor = () =>
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
744
|
+
const doctor = async () => {
|
|
745
|
+
const result = await doctorFn(projectDir, {
|
|
746
|
+
manifest,
|
|
747
|
+
aclProbe: deps.aclProbe,
|
|
748
|
+
execution: { nodeProbe: async () => nodeOnPath },
|
|
749
|
+
});
|
|
750
|
+
if ("state" in result) {
|
|
751
|
+
throw new Error("supported Claude lifecycle received unsupported doctor result");
|
|
752
|
+
}
|
|
753
|
+
return result;
|
|
754
|
+
};
|
|
748
755
|
// Non-blocking runtime notice, computed once and carried by EVERY outcome
|
|
749
756
|
// (success, no-op and refusal alike) — it never gates the mutation.
|
|
750
757
|
const warnings = nodeOnPathWarnings(nodeOnPath);
|
|
@@ -832,10 +839,10 @@ export async function _run(projectDir, mode, options, deps) {
|
|
|
832
839
|
report: await doctor(),
|
|
833
840
|
};
|
|
834
841
|
}
|
|
835
|
-
export function installClaudePreToolUse(projectDir) {
|
|
836
|
-
return _run(projectDir, "install", {},
|
|
842
|
+
export function installClaudePreToolUse(projectDir, deps = {}) {
|
|
843
|
+
return _run(projectDir, "install", {}, deps);
|
|
837
844
|
}
|
|
838
|
-
export function repairClaudePreToolUse(projectDir, options) {
|
|
839
|
-
return _run(projectDir, "repair", options ?? {},
|
|
845
|
+
export function repairClaudePreToolUse(projectDir, options, deps = {}) {
|
|
846
|
+
return _run(projectDir, "repair", options ?? {}, deps);
|
|
840
847
|
}
|
|
841
848
|
//# sourceMappingURL=claude-hook-manager.js.map
|
|
@@ -102,7 +102,6 @@ export declare function classifyCodexHooksJson(value: unknown, expectedCommand:
|
|
|
102
102
|
export type CodexTrustState = "trusted" | "untrusted";
|
|
103
103
|
export interface CodexHookDoctorReport {
|
|
104
104
|
healthy: boolean;
|
|
105
|
-
platformSupport?: PlatformSupport;
|
|
106
105
|
hooksJson: {
|
|
107
106
|
state: ClaudeHookComponentState;
|
|
108
107
|
detail?: string;
|
|
@@ -128,6 +127,12 @@ export interface CodexHookDoctorReport {
|
|
|
128
127
|
};
|
|
129
128
|
remediation: string[];
|
|
130
129
|
}
|
|
130
|
+
export interface CodexHookDoctorRefusal {
|
|
131
|
+
state: "unsupported-platform";
|
|
132
|
+
healthy: false;
|
|
133
|
+
platformSupport: PlatformSupport;
|
|
134
|
+
}
|
|
135
|
+
export type CodexHookDoctorResult = CodexHookDoctorReport | CodexHookDoctorRefusal;
|
|
131
136
|
export interface CodexDoctorOptions {
|
|
132
137
|
platform?: NodeJS.Platform;
|
|
133
138
|
manifest?: Manifest;
|
|
@@ -135,7 +140,7 @@ export interface CodexDoctorOptions {
|
|
|
135
140
|
nodeVersion?: string;
|
|
136
141
|
nodeProbe?: () => Promise<NodeOnPathProbe>;
|
|
137
142
|
}
|
|
138
|
-
export declare function doctorCodexPreToolUse(homeDir?: string, options?: CodexDoctorOptions): Promise<
|
|
143
|
+
export declare function doctorCodexPreToolUse(homeDir?: string, options?: CodexDoctorOptions): Promise<CodexHookDoctorResult>;
|
|
139
144
|
interface CodexHookMutationResultWithReport {
|
|
140
145
|
ok: boolean;
|
|
141
146
|
changed: string[];
|
|
@@ -158,6 +163,7 @@ interface CodexHookLifecycleRefusal {
|
|
|
158
163
|
}
|
|
159
164
|
export type CodexHookMutationResult = CodexHookMutationResultWithReport | CodexHookLifecycleRefusal;
|
|
160
165
|
export interface CodexHookRunDeps {
|
|
166
|
+
homeDirProvider?: () => string;
|
|
161
167
|
secureFs?: PlatformSecureFs | null;
|
|
162
168
|
clock?: () => Date;
|
|
163
169
|
nonce?: () => string;
|
|
@@ -171,8 +177,8 @@ export interface CodexHookRunDeps {
|
|
|
171
177
|
export declare function _runCodex(homeDir: string, _mode: "install" | "repair", _options: {
|
|
172
178
|
force?: boolean;
|
|
173
179
|
}, deps: CodexHookRunDeps): Promise<CodexHookMutationResult>;
|
|
174
|
-
export declare function installCodexPreToolUse(homeDir?: string): Promise<CodexHookMutationResult>;
|
|
180
|
+
export declare function installCodexPreToolUse(homeDir?: string, deps?: CodexHookRunDeps): Promise<CodexHookMutationResult>;
|
|
175
181
|
export declare function repairCodexPreToolUse(homeDir?: string, options?: {
|
|
176
182
|
force?: boolean;
|
|
177
|
-
}): Promise<CodexHookMutationResult>;
|
|
183
|
+
}, deps?: CodexHookRunDeps): Promise<CodexHookMutationResult>;
|
|
178
184
|
//# sourceMappingURL=codex-hook-manager.d.ts.map
|
|
@@ -295,10 +295,19 @@ async function readManifest() {
|
|
|
295
295
|
throw new Error(`unreadable claude-hooks manifest: ${read.reason}`);
|
|
296
296
|
return JSON.parse(read.content);
|
|
297
297
|
}
|
|
298
|
-
export async function doctorCodexPreToolUse(homeDir
|
|
298
|
+
export async function doctorCodexPreToolUse(homeDir, options = {}) {
|
|
299
|
+
const platformSupport = resolvePlatformSupport(options.platform ?? process.platform);
|
|
300
|
+
if (platformSupport) {
|
|
301
|
+
return {
|
|
302
|
+
state: "unsupported-platform",
|
|
303
|
+
healthy: false,
|
|
304
|
+
platformSupport,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const resolvedHomeDir = homeDir ?? os.homedir();
|
|
299
308
|
const manifest = options.manifest ?? (await readManifest());
|
|
300
309
|
const assetPath = options.assetPath ?? SHIPPED_CODEX_ASSET;
|
|
301
|
-
const { hooksFile, configFile } = codexConfigPaths(
|
|
310
|
+
const { hooksFile, configFile } = codexConfigPaths(resolvedHomeDir);
|
|
302
311
|
const expectedCommand = expectedCodexCommand(assetPath);
|
|
303
312
|
// hooks.json registration.
|
|
304
313
|
const hooksRead = await readText(hooksFile);
|
|
@@ -373,9 +382,7 @@ export async function doctorCodexPreToolUse(homeDir = os.homedir(), options = {}
|
|
|
373
382
|
}
|
|
374
383
|
if (!node.satisfiesMinimum)
|
|
375
384
|
remediation.push("install Node 22 or newer");
|
|
376
|
-
const platformSupport = resolvePlatformSupport(options.platform ?? process.platform);
|
|
377
385
|
return {
|
|
378
|
-
...(platformSupport ? { platformSupport } : {}),
|
|
379
386
|
healthy: status === "runnable",
|
|
380
387
|
hooksJson,
|
|
381
388
|
config: { featuresHooks, readable: configReadable },
|
|
@@ -421,11 +428,17 @@ export async function _runCodex(homeDir, _mode, _options, deps) {
|
|
|
421
428
|
const { codexDir, hooksFile, configFile } = codexConfigPaths(homeDir);
|
|
422
429
|
const expectedCommand = expectedCodexCommand(assetPath);
|
|
423
430
|
const nodeOnPath = await (deps.nodeProbe ?? probeNodeOnPath)();
|
|
424
|
-
const doctor = () =>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
431
|
+
const doctor = async () => {
|
|
432
|
+
const result = await doctorFn(homeDir, {
|
|
433
|
+
manifest,
|
|
434
|
+
assetPath,
|
|
435
|
+
nodeProbe: async () => nodeOnPath,
|
|
436
|
+
});
|
|
437
|
+
if ("state" in result) {
|
|
438
|
+
throw new Error("supported Codex lifecycle received unsupported doctor result");
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
};
|
|
429
442
|
if (!secureFs) {
|
|
430
443
|
return {
|
|
431
444
|
ok: false,
|
|
@@ -537,10 +550,28 @@ export async function _runCodex(homeDir, _mode, _options, deps) {
|
|
|
537
550
|
report: await doctor(),
|
|
538
551
|
};
|
|
539
552
|
}
|
|
540
|
-
|
|
541
|
-
return
|
|
553
|
+
function unsupportedCodexLifecycle(platformSupport) {
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
changed: [],
|
|
557
|
+
backups: [],
|
|
558
|
+
errors: [platformSupport.refusalCode],
|
|
559
|
+
warnings: [platformSupport.guidance],
|
|
560
|
+
lifecycleRefusal: platformSupport,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
export function installCodexPreToolUse(homeDir, deps = {}) {
|
|
564
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
565
|
+
if (platformSupport)
|
|
566
|
+
return Promise.resolve(unsupportedCodexLifecycle(platformSupport));
|
|
567
|
+
const resolvedHomeDir = homeDir ?? (deps.homeDirProvider ?? os.homedir)();
|
|
568
|
+
return _runCodex(resolvedHomeDir, "install", {}, deps);
|
|
542
569
|
}
|
|
543
|
-
export function repairCodexPreToolUse(homeDir =
|
|
544
|
-
|
|
570
|
+
export function repairCodexPreToolUse(homeDir, options, deps = {}) {
|
|
571
|
+
const platformSupport = resolvePlatformSupport(deps.platform ?? process.platform);
|
|
572
|
+
if (platformSupport)
|
|
573
|
+
return Promise.resolve(unsupportedCodexLifecycle(platformSupport));
|
|
574
|
+
const resolvedHomeDir = homeDir ?? (deps.homeDirProvider ?? os.homedir)();
|
|
575
|
+
return _runCodex(resolvedHomeDir, "repair", options ?? {}, deps);
|
|
545
576
|
}
|
|
546
577
|
//# sourceMappingURL=codex-hook-manager.js.map
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
readonly
|
|
3
|
-
|
|
4
|
-
export declare const LIFECYCLE_SUPPORT: {
|
|
5
|
-
readonly UNSUPPORTED: "unsupported";
|
|
6
|
-
};
|
|
7
|
-
export declare const PLATFORM_REFUSAL: {
|
|
8
|
-
readonly MACOS_LIFECYCLE_UNSUPPORTED: "macos-lifecycle-unsupported";
|
|
1
|
+
export declare const HOST_SUPPORT_STATE: {
|
|
2
|
+
readonly SUPPORTED: "supported";
|
|
3
|
+
readonly UNSUPPORTED: "unsupported-platform";
|
|
9
4
|
};
|
|
5
|
+
export declare const HOST_SUPPORT_GUIDANCE = "javi-forge supports Linux and Windows only.";
|
|
6
|
+
export type SupportedHostPlatform = "linux" | "win32";
|
|
7
|
+
export interface SupportedHost {
|
|
8
|
+
state: typeof HOST_SUPPORT_STATE.SUPPORTED;
|
|
9
|
+
platform: SupportedHostPlatform;
|
|
10
|
+
}
|
|
10
11
|
export interface PlatformSupport {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
refusalCode: typeof PLATFORM_REFUSAL.MACOS_LIFECYCLE_UNSUPPORTED;
|
|
15
|
-
guidance: string;
|
|
12
|
+
state: typeof HOST_SUPPORT_STATE.UNSUPPORTED;
|
|
13
|
+
refusalCode: typeof HOST_SUPPORT_STATE.UNSUPPORTED;
|
|
14
|
+
guidance: typeof HOST_SUPPORT_GUIDANCE;
|
|
16
15
|
}
|
|
17
|
-
export
|
|
16
|
+
export type HostSupportResult = SupportedHost | PlatformSupport;
|
|
17
|
+
/**
|
|
18
|
+
* Classifies every host value without aliases or fallthrough.
|
|
19
|
+
*/
|
|
20
|
+
export declare function classifyHostPlatform(platform: string): HostSupportResult;
|
|
21
|
+
/** Returns an unsupported result for legacy callers, otherwise undefined. */
|
|
18
22
|
export declare function resolvePlatformSupport(platform: string): PlatformSupport | undefined;
|
|
19
23
|
//# sourceMappingURL=platform-support.d.ts.map
|