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.
Files changed (42) hide show
  1. package/README.md +5 -5
  2. package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +13 -11
  3. package/assets/claude-hooks/manifest.json +35 -1
  4. package/assets/hooks/manifest.json +6 -2
  5. package/assets/hooks/pre-push +206 -8
  6. package/ci-local/README.md +3 -6
  7. package/ci-local/ci-local.ps1 +36 -10
  8. package/ci-local/ci-local.sh +26 -9
  9. package/ci-local/install.ps1 +36 -14
  10. package/ci-local/install.sh +27 -10
  11. package/ci-local/lib/common.sh +3 -7
  12. package/ci-local/uninstall.ps1 +50 -0
  13. package/ci-local/uninstall.sh +38 -3
  14. package/dist/cli/bootstrap.d.ts +12 -0
  15. package/dist/cli/bootstrap.js +12 -0
  16. package/dist/cli/dispatch/simple-renderers.d.ts +8 -1
  17. package/dist/cli/dispatch/simple-renderers.js +11 -2
  18. package/dist/cli/main.d.ts +2 -0
  19. package/dist/cli/main.js +85 -0
  20. package/dist/commands/claude-hooks.d.ts +1 -0
  21. package/dist/commands/claude-hooks.js +12 -3
  22. package/dist/commands/codex-hooks.d.ts +1 -0
  23. package/dist/commands/codex-hooks.js +12 -3
  24. package/dist/commands/doctor.d.ts +16 -1
  25. package/dist/commands/doctor.js +54 -35
  26. package/dist/commands/init.d.ts +4 -1
  27. package/dist/commands/init.js +2 -2
  28. package/dist/index.js +4 -81
  29. package/dist/lib/claude-hook-manager.d.ts +11 -6
  30. package/dist/lib/claude-hook-manager.js +28 -21
  31. package/dist/lib/codex-hook-manager.d.ts +10 -4
  32. package/dist/lib/codex-hook-manager.js +44 -13
  33. package/dist/lib/platform-support.d.ts +18 -14
  34. package/dist/lib/platform-support.js +19 -17
  35. package/dist/lib/secure-fs-posix.d.ts +8 -13
  36. package/dist/lib/secure-fs-posix.js +11 -52
  37. package/dist/lib/secure-fs-transaction.d.ts +2 -3
  38. package/dist/types/index.d.ts +7 -2
  39. package/dist/ui/Doctor.d.ts +2 -0
  40. package/dist/ui/Doctor.js +13 -0
  41. package/lib/common.sh +3 -7
  42. package/package.json +7 -2
@@ -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 { stdout } = await execFileAsync("which", [bin]);
12
- return stdout.trim() || null;
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 fs.pathExists(manifestPath)))
26
+ if (!(await filesystem.pathExists(manifestPath)))
22
27
  return null;
23
28
  try {
24
- return (await fs.readJson(manifestPath));
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 fs.pathExists(dir)))
36
+ async function countDir(dir, filesystem) {
37
+ if (!(await filesystem.pathExists(dir)))
33
38
  return 0;
34
- const entries = await fs.readdir(dir);
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 execFileAsync("git", ["config", "--get", key], {
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 execFileAsync("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd });
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 execFileAsync("gh", [
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 cwd = projectDir ?? process.cwd();
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 execFileAsync(tool.name, ["--version"]);
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 fs.pathExists(dir.path)) {
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 detectStack(cwd);
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 fs.pathExists(modPath)) {
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 fs.pathExists(PLUGINS_DIR);
303
+ const pluginsDirExists = await filesystem.pathExists(PLUGINS_DIR);
285
304
  if (pluginsDirExists) {
286
- const plugins = await listInstalledPlugins();
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 refreshContextDir(cwd);
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
@@ -14,5 +14,8 @@ export type InitProjectResult = {
14
14
  refusalCode: string;
15
15
  platformSupport: PlatformSupport;
16
16
  };
17
- export declare function initProject(options: InitOptions, onStep: StepCallback): Promise<InitProjectResult>;
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
@@ -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 { createRequire } from "node:module";
3
- import meow from "meow";
4
- import { handleCi } from "./cli/dispatch/ci.js";
5
- import { handleHooks } from "./cli/dispatch/hooks.js";
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 per OS (no fs). WSL reports `linux`. */
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<ClaudeHookDoctorReport>;
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 per OS (no fs). WSL reports `linux`. */
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.file;
377
+ const managedFile = env.managedFile !== undefined ? env.managedFile : resolved?.file;
383
378
  const dropInDir = env.managedDropInDir !== undefined
384
379
  ? env.managedDropInDir
385
- : resolved.dropInDir;
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 = () => doctorFn(projectDir, {
744
- manifest,
745
- aclProbe: deps.aclProbe,
746
- execution: { nodeProbe: async () => nodeOnPath },
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<CodexHookDoctorReport>;
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 = os.homedir(), options = {}) {
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(homeDir);
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 = () => doctorFn(homeDir, {
425
- manifest,
426
- assetPath,
427
- nodeProbe: async () => nodeOnPath,
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
- export function installCodexPreToolUse(homeDir = os.homedir()) {
541
- return _runCodex(homeDir, "install", {}, {});
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 = os.homedir(), options) {
544
- return _runCodex(homeDir, "repair", options ?? {}, {});
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 PLATFORM_SUPPORT_STATE: {
2
- readonly MACOS_DEPRECATED: "macos-deprecated";
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
- platform: "darwin";
12
- state: typeof PLATFORM_SUPPORT_STATE.MACOS_DEPRECATED;
13
- lifecycle: typeof LIFECYCLE_SUPPORT.UNSUPPORTED;
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 declare const MACOS_DEPRECATION_GUIDANCE = "macOS is deprecated and unsupported for install, repair, and init; pin a supported release or migrate. Existing installed guards are not removed; Darwin removal is planned for 2.0.";
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