javi-forge 1.37.1 → 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 (40) 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/ci-local/README.md +3 -6
  5. package/ci-local/ci-local.ps1 +36 -10
  6. package/ci-local/ci-local.sh +26 -9
  7. package/ci-local/install.ps1 +36 -14
  8. package/ci-local/install.sh +27 -10
  9. package/ci-local/lib/common.sh +3 -7
  10. package/ci-local/uninstall.ps1 +50 -0
  11. package/ci-local/uninstall.sh +38 -3
  12. package/dist/cli/bootstrap.d.ts +12 -0
  13. package/dist/cli/bootstrap.js +12 -0
  14. package/dist/cli/dispatch/simple-renderers.d.ts +8 -1
  15. package/dist/cli/dispatch/simple-renderers.js +11 -2
  16. package/dist/cli/main.d.ts +2 -0
  17. package/dist/cli/main.js +85 -0
  18. package/dist/commands/claude-hooks.d.ts +1 -0
  19. package/dist/commands/claude-hooks.js +12 -3
  20. package/dist/commands/codex-hooks.d.ts +1 -0
  21. package/dist/commands/codex-hooks.js +12 -3
  22. package/dist/commands/doctor.d.ts +16 -1
  23. package/dist/commands/doctor.js +54 -35
  24. package/dist/commands/init.d.ts +4 -1
  25. package/dist/commands/init.js +2 -2
  26. package/dist/index.js +4 -81
  27. package/dist/lib/claude-hook-manager.d.ts +11 -6
  28. package/dist/lib/claude-hook-manager.js +28 -21
  29. package/dist/lib/codex-hook-manager.d.ts +10 -4
  30. package/dist/lib/codex-hook-manager.js +44 -13
  31. package/dist/lib/platform-support.d.ts +18 -14
  32. package/dist/lib/platform-support.js +19 -17
  33. package/dist/lib/secure-fs-posix.d.ts +8 -13
  34. package/dist/lib/secure-fs-posix.js +11 -52
  35. package/dist/lib/secure-fs-transaction.d.ts +2 -3
  36. package/dist/types/index.d.ts +7 -2
  37. package/dist/ui/Doctor.d.ts +2 -0
  38. package/dist/ui/Doctor.js +13 -0
  39. package/lib/common.sh +3 -7
  40. package/package.json +7 -2
@@ -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
@@ -1,22 +1,24 @@
1
- export const PLATFORM_SUPPORT_STATE = {
2
- MACOS_DEPRECATED: "macos-deprecated",
1
+ export const HOST_SUPPORT_STATE = {
2
+ SUPPORTED: "supported",
3
+ UNSUPPORTED: "unsupported-platform",
3
4
  };
4
- export const LIFECYCLE_SUPPORT = {
5
- UNSUPPORTED: "unsupported",
6
- };
7
- export const PLATFORM_REFUSAL = {
8
- MACOS_LIFECYCLE_UNSUPPORTED: "macos-lifecycle-unsupported",
9
- };
10
- export 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.";
11
- export function resolvePlatformSupport(platform) {
12
- if (platform !== "darwin")
13
- return undefined;
5
+ export const HOST_SUPPORT_GUIDANCE = "javi-forge supports Linux and Windows only.";
6
+ /**
7
+ * Classifies every host value without aliases or fallthrough.
8
+ */
9
+ export function classifyHostPlatform(platform) {
10
+ if (platform === "linux" || platform === "win32") {
11
+ return { state: HOST_SUPPORT_STATE.SUPPORTED, platform };
12
+ }
14
13
  return {
15
- platform: "darwin",
16
- state: PLATFORM_SUPPORT_STATE.MACOS_DEPRECATED,
17
- lifecycle: LIFECYCLE_SUPPORT.UNSUPPORTED,
18
- refusalCode: PLATFORM_REFUSAL.MACOS_LIFECYCLE_UNSUPPORTED,
19
- guidance: MACOS_DEPRECATION_GUIDANCE,
14
+ state: HOST_SUPPORT_STATE.UNSUPPORTED,
15
+ refusalCode: HOST_SUPPORT_STATE.UNSUPPORTED,
16
+ guidance: HOST_SUPPORT_GUIDANCE,
20
17
  };
21
18
  }
19
+ /** Returns an unsupported result for legacy callers, otherwise undefined. */
20
+ export function resolvePlatformSupport(platform) {
21
+ const result = classifyHostPlatform(platform);
22
+ return result.state === HOST_SUPPORT_STATE.UNSUPPORTED ? result : undefined;
23
+ }
22
24
  //# sourceMappingURL=platform-support.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * POSIX `PlatformSecureFs` adapters for the SkillGuard transactional installer
3
- * (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux,
4
- * `/bin/ls -lde` on macOS) and the ONLY place `os`/`fs` ownership bits are
3
+ * (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux)
4
+ * and the ONLY place `os`/`fs` ownership bits are interpreted.
5
5
  * interpreted. Everything fails closed: a missing/erroring/timed-out ACL tool,
6
6
  * an extended/named/mask/default/inherited ACL entry, a foreign-owned or
7
7
  * group/other-writable directory, or any inconclusive result refuses rather than
@@ -38,7 +38,7 @@ export interface PosixAclAdapter {
38
38
  * mask) `w`, OR any named-group with effective `w`. Everything else (base
39
39
  * entries, a lone `mask::`, effective-non-write named entries, x-only, trusted
40
40
  * named users, `default:*`) proceeds. Same fail-closed spawn edges as
41
- * `proveClean`. On darwin this is the no-op alias of `proveClean` (deferred).
41
+ * `proveClean`.
42
42
  */
43
43
  proveNoEndangeringAcl(target: string): Promise<SecureResult<void>>;
44
44
  }
@@ -51,13 +51,8 @@ export declare const ACL_DETAIL: {
51
51
  readonly getfaclAbsent: "getfacl absent";
52
52
  readonly getfaclTimeout: "getfacl timeout";
53
53
  readonly extendedAclEntry: "extended ACL entry";
54
- readonly macosLsAbsent: "/bin/ls absent";
55
- readonly macosLsTimeout: "ls timeout";
56
- readonly macosAclFlag: "ACL present (+ flag)";
57
- readonly macosAceListed: "ACE listed";
58
54
  };
59
55
  export declare function createLinuxAclAdapter(spawn?: SpawnFn, stat?: StatFn): PosixAclAdapter;
60
- export declare function createMacosAclAdapter(spawn?: SpawnFn): PosixAclAdapter;
61
56
  /**
62
57
  * Whether the host's ACL adapter is RESOLVABLE — an install-time capability
63
58
  * question, deliberately separate from the per-target prover above. It never
@@ -65,10 +60,10 @@ export declare function createMacosAclAdapter(spawn?: SpawnFn): PosixAclAdapter;
65
60
  */
66
61
  export type AclCapability = {
67
62
  status: "available";
68
- tool: "getfacl" | "/bin/ls";
63
+ tool: "getfacl";
69
64
  } | {
70
65
  status: "absent";
71
- tool: "getfacl" | "/bin/ls";
66
+ tool: "getfacl";
72
67
  } | {
73
68
  status: "unknown";
74
69
  tool: string;
@@ -89,9 +84,9 @@ export type AclCapability = {
89
84
  export declare function probeAclCapability(spawn?: SpawnFn, platform?: NodeJS.Platform): Promise<AclCapability>;
90
85
  export declare function createPosixSecureFs(acl: PosixAclAdapter): PlatformSecureFs;
91
86
  /**
92
- * Select the secure filesystem for the host platform. Linux uses the `getfacl`
93
- * adapter, macOS uses `/bin/ls -lde`, and win32 uses the digest-bound PowerShell
94
- * helper over a lazily-spawned session (Slice 3b). Every other platform returns
87
+ * Select the secure filesystem for supported hosts. Linux uses the `getfacl`
88
+ * adapter and win32 uses the digest-bound PowerShell helper over a lazily-spawned
89
+ * session (Slice 3b). Every other platform returns
95
90
  * `null` so the manager refuses with `windows-secure-object-unavailable` and
96
91
  * mutates nothing.
97
92
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * POSIX `PlatformSecureFs` adapters for the SkillGuard transactional installer
3
- * (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux,
4
- * `/bin/ls -lde` on macOS) and the ONLY place `os`/`fs` ownership bits are
3
+ * (Slice 3a). This is the ONLY place a shell tool runs (`getfacl` on Linux)
4
+ * and the ONLY place `os`/`fs` ownership bits are interpreted.
5
5
  * interpreted. Everything fails closed: a missing/erroring/timed-out ACL tool,
6
6
  * an extended/named/mask/default/inherited ACL entry, a foreign-owned or
7
7
  * group/other-writable directory, or any inconclusive result refuses rather than
@@ -57,10 +57,6 @@ export const ACL_DETAIL = {
57
57
  getfaclAbsent: "getfacl absent",
58
58
  getfaclTimeout: "getfacl timeout",
59
59
  extendedAclEntry: "extended ACL entry",
60
- macosLsAbsent: "/bin/ls absent",
61
- macosLsTimeout: "ls timeout",
62
- macosAclFlag: "ACL present (+ flag)",
63
- macosAceListed: "ACE listed",
64
60
  };
65
61
  // --- Linux getfacl adapter (Algorithm D) -------------------------------------
66
62
  const LINUX_BASE_ENTRY = /^(user|group|other)::/;
@@ -208,39 +204,6 @@ export function createLinuxAclAdapter(spawn = defaultSpawn, stat = defaultStat)
208
204
  },
209
205
  };
210
206
  }
211
- // --- macOS /bin/ls -lde adapter (Algorithm E) --------------------------------
212
- const MACOS_ACE_LINE = /^\s*\d+:\s/;
213
- export function createMacosAclAdapter(spawn = defaultSpawn) {
214
- const adapter = {
215
- async proveClean(target) {
216
- const res = await spawn("/bin/ls", ["-lde", "--", target]);
217
- if (res.spawnError)
218
- return refuse("unsupported-posix-acl", ACL_DETAIL.macosLsAbsent);
219
- if (res.timedOut)
220
- return refuse("unsupported-posix-acl", ACL_DETAIL.macosLsTimeout);
221
- if (res.code !== 0) {
222
- return refuse("unsupported-posix-acl", `ls exit ${res.code}`);
223
- }
224
- const lines = res.stdout.split("\n");
225
- const modeLine = lines[0] ?? "";
226
- if (modeLine[10] === "+") {
227
- return refuse("unsupported-posix-acl", ACL_DETAIL.macosAclFlag);
228
- }
229
- if (lines.some((line) => MACOS_ACE_LINE.test(line))) {
230
- return refuse("unsupported-posix-acl", ACL_DETAIL.macosAceListed);
231
- }
232
- return ok();
233
- },
234
- // DEFERRED (user: Linux only): darwin ancestors stay STRICT = status-quo
235
- // over-refusal, not the reported Linux bug. `/bin/ls -lde` ACE text carries
236
- // no numeric mask to compute effective rights from, so the path-endangering
237
- // narrowing is a documented follow-up. Alias to the strict proof.
238
- proveNoEndangeringAcl(target) {
239
- return adapter.proveClean(target);
240
- },
241
- };
242
- return adapter;
243
- }
244
207
  /**
245
208
  * Probe the ACL adapter READ-ONLY: it resolves and runs a version/list argv and
246
209
  * inspects nothing on disk. It creates, modifies and removes nothing, and its
@@ -254,29 +217,27 @@ export async function probeAclCapability(spawn = defaultSpawn, platform = proces
254
217
  if (platform === "win32") {
255
218
  return { status: "not-applicable", tool: "windows-secure-object" };
256
219
  }
257
- if (platform !== "linux" && platform !== "darwin") {
220
+ if (platform !== "linux") {
258
221
  return {
259
222
  status: "unknown",
260
223
  tool: platform,
261
224
  detail: `no POSIX ACL adapter for platform ${platform}`,
262
225
  };
263
226
  }
264
- const tool = platform === "linux" ? "getfacl" : "/bin/ls";
265
- const args = platform === "linux" ? ["--version"] : ["-ld", "/"];
227
+ const tool = "getfacl";
228
+ const args = ["--version"];
266
229
  const res = await spawn(tool, args);
267
230
  if (res.spawnError)
268
231
  return { status: "absent", tool };
269
232
  if (res.timedOut) {
270
- // The argv differs per platform (`getfacl --version` on linux, `/bin/ls -ld /`
271
- // on darwin), so the detail names the probe, not a hardcoded flag.
272
233
  return { status: "unknown", tool, detail: `${tool} probe timeout` };
273
234
  }
274
235
  if (res.code !== 0) {
275
236
  return { status: "unknown", tool, detail: `${tool} exit ${res.code}` };
276
237
  }
277
- // Linux only: a zero exit whose banner does not name getfacl is a foreign
278
- // binary on PATH, not proof of the adapter — report ignorance, not success.
279
- if (platform === "linux" && !res.stdout.toLowerCase().includes("getfacl")) {
238
+ // A zero exit whose banner does not name getfacl is a foreign binary on PATH,
239
+ // not proof of the adapter — report ignorance, not success.
240
+ if (!res.stdout.toLowerCase().includes("getfacl")) {
280
241
  return {
281
242
  status: "unknown",
282
243
  tool,
@@ -504,9 +465,9 @@ export function createPosixSecureFs(acl) {
504
465
  return secureFs;
505
466
  }
506
467
  /**
507
- * Select the secure filesystem for the host platform. Linux uses the `getfacl`
508
- * adapter, macOS uses `/bin/ls -lde`, and win32 uses the digest-bound PowerShell
509
- * helper over a lazily-spawned session (Slice 3b). Every other platform returns
468
+ * Select the secure filesystem for supported hosts. Linux uses the `getfacl`
469
+ * adapter and win32 uses the digest-bound PowerShell helper over a lazily-spawned
470
+ * session (Slice 3b). Every other platform returns
510
471
  * `null` so the manager refuses with `windows-secure-object-unavailable` and
511
472
  * mutates nothing.
512
473
  *
@@ -521,8 +482,6 @@ export function createPosixSecureFs(acl) {
521
482
  export function selectSecureFs(platform = process.platform) {
522
483
  if (platform === "linux")
523
484
  return createPosixSecureFs(createLinuxAclAdapter());
524
- if (platform === "darwin")
525
- return createPosixSecureFs(createMacosAclAdapter());
526
485
  if (platform === "win32")
527
486
  return createWindowsSecureFs(createPs1Session());
528
487
  return null;
@@ -74,9 +74,8 @@ export interface PlatformSecureFs {
74
74
  * ones (a lone mask, an effective-non-write or x-only or trusted named entry, a
75
75
  * `default:*`). Called by the engine on ANCESTOR (non-managed) controlling dirs
76
76
  * only; managed containers keep the strict proof. Selection is by the
77
- * managed-containers role, NEVER by `process.platform`. On win32 this mirrors
78
- * the ratified Predicate A (already lenient on ancestors); on darwin it is the
79
- * strict no-op alias (deferred).
77
+ * managed-containers role, NEVER by `process.platform`. Supported adapters
78
+ * implement the same role contract without host branching in this engine.
80
79
  */
81
80
  proveNoEndangeringAcl(target: string): Promise<SecureResult<void>>;
82
81
  /** Create ONE child directory exclusively at mode, reopen+verify, return its handle. */
@@ -69,9 +69,14 @@ export interface DoctorSection {
69
69
  title: string;
70
70
  checks: DoctorCheck[];
71
71
  }
72
- export interface DoctorResult {
72
+ export type DoctorResult = {
73
+ state: "supported";
73
74
  sections: DoctorSection[];
74
- }
75
+ } | {
76
+ state: "unsupported-platform";
77
+ guidance: string;
78
+ sections: [];
79
+ };
75
80
  export interface PluginManifest {
76
81
  name: string;
77
82
  version: string;
@@ -1,3 +1,5 @@
1
1
  import React from "react";
2
+ import type { DoctorResult } from "../types/index.js";
3
+ export declare function unsupportedDoctorMessage(result: DoctorResult): string | undefined;
2
4
  export default function Doctor(): React.JSX.Element;
3
5
  //# sourceMappingURL=Doctor.d.ts.map
package/dist/ui/Doctor.js CHANGED
@@ -15,6 +15,11 @@ const STATUS_COLOR = {
15
15
  fail: theme.error,
16
16
  skip: theme.muted,
17
17
  };
18
+ export function unsupportedDoctorMessage(result) {
19
+ return result.state === "unsupported-platform"
20
+ ? `unsupported-platform: ${result.guidance}`
21
+ : undefined;
22
+ }
18
23
  export default function Doctor() {
19
24
  const { exit } = useApp();
20
25
  const isCI = useCIMode();
@@ -52,6 +57,14 @@ export default function Doctor() {
52
57
  if (input.toLowerCase() === "q" || key.return || key.escape)
53
58
  exit();
54
59
  }, { isActive: !isCI });
60
+ const unsupportedMessage = result
61
+ ? unsupportedDoctorMessage(result)
62
+ : undefined;
63
+ if (unsupportedMessage) {
64
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
65
+ React.createElement(Header, { subtitle: "doctor" }),
66
+ React.createElement(Text, { color: theme.error }, unsupportedMessage)));
67
+ }
55
68
  // Compute health score
56
69
  const allChecks = result?.sections.flatMap((s) => s.checks) ?? [];
57
70
  const passed = allChecks.filter((c) => c.status === "ok").length;
package/lib/common.sh CHANGED
@@ -39,16 +39,12 @@ log_info() { echo -e " ${CYAN}[INFO]${NC} $1"; }
39
39
  log_step() { echo -e "${YELLOW}$1${NC}"; }
40
40
 
41
41
  # =============================================================================
42
- # sed_inplace - Portable sed -i (works on both GNU and BSD/macOS sed)
42
+ # sed_inplace - GNU sed -i for supported Linux/WSL hosts
43
43
  # =============================================================================
44
44
  # Usage: sed_inplace "s/foo/bar/" file.txt
45
45
  # =============================================================================
46
46
  sed_inplace() {
47
- if sed --version 2>/dev/null | grep -q GNU; then
48
- sed -i "$@"
49
- else
50
- sed -i '' "$@"
51
- fi
47
+ sed -i "$@"
52
48
  }
53
49
 
54
50
  # =============================================================================
@@ -130,7 +126,7 @@ detect_stack() {
130
126
  STACK_TYPE="java-gradle"
131
127
  BUILD_TOOL="gradle"
132
128
 
133
- # Detect Java version from build files (compatible with macOS and Linux)
129
+ # Detect Java version from build files
134
130
  if [[ -f "$project_dir/build.gradle.kts" ]]; then
135
131
  JAVA_VERSION=$(grep -E 'languageVersion\s*=\s*JavaLanguageVersion\.of\(' "$project_dir/build.gradle.kts" 2>/dev/null | grep -o '[0-9]\+' | head -1 || echo "21")
136
132
  elif [[ -f "$project_dir/build.gradle" ]]; then