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
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "lint:fix": "biome check --write src/",
22
22
  "format": "biome format --write src/",
23
23
  "validate": "pnpm typecheck && pnpm typecheck:test && pnpm lint && pnpm test",
24
- "package:check": "npm pack --dry-run --json --cache /tmp/javi-forge-npm-pack-cache > /tmp/javi-forge-pack.json && node scripts/verify-package-contents.mjs /tmp/javi-forge-pack.json"
24
+ "package:check": "node scripts/verify-package-contents.mjs"
25
25
  },
26
26
  "files": [
27
27
  "dist/**/*.js",
@@ -31,6 +31,7 @@
31
31
  "assets/",
32
32
  "!assets/**/*.test.sh",
33
33
  "ci-local/",
34
+ "!ci-local/tests/**",
34
35
  "modules/",
35
36
  "templates/",
36
37
  "workflows/",
@@ -45,6 +46,10 @@
45
46
  "type": "git",
46
47
  "url": "git+https://github.com/JNZader/javi-forge.git"
47
48
  },
49
+ "os": [
50
+ "linux",
51
+ "win32"
52
+ ],
48
53
  "engines": {
49
54
  "node": ">=22"
50
55
  },