javi-forge 1.37.1 → 1.38.1

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 (41) 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/lib/secure-fs-transaction.js +4 -2
  37. package/dist/types/index.d.ts +7 -2
  38. package/dist/ui/Doctor.d.ts +2 -0
  39. package/dist/ui/Doctor.js +13 -0
  40. package/lib/common.sh +3 -7
  41. package/package.json +10 -4
package/README.md CHANGED
@@ -13,6 +13,10 @@ npx javi-forge init
13
13
 
14
14
  An interactive TUI guides you through project setup: pick a stack, CI provider, memory module, and get a production-ready project structure in seconds.
15
15
 
16
+ ## Platform Support
17
+
18
+ javi-forge 2.0 supports Linux and Windows only. npm package eligibility enforces this boundary. Source or manual execution on every other host returns generic `unsupported-platform` guidance before probes or mutation; there is no legacy-host migration or compatibility mode.
19
+
16
20
  ## What It Creates
17
21
 
18
22
  `javi-forge init` bootstraps a complete project with 13 sequential steps:
@@ -373,14 +377,10 @@ npx javi-forge doctor
373
377
  - **Installed Modules** — engram, obsidian-brain, memory-simple, ghagga
374
378
 
375
379
 
376
- ## macOS CI-Local support
377
-
378
- macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate your workflow. Existing installed guards are not removed in 1.x; Darwin code removal is planned separately for 2.0.
379
-
380
380
  ## Requirements
381
381
 
382
382
  - **Node.js** >= 22 (required by ink 7; previous versions ran on >= 18)
383
- - **Linux only** — the `acl` package (provides `getfacl`) is required to install or repair the Claude PreToolUse guard: the transactional installer proves every controlling directory carries no extended ACL, and refuses fail-closed when `getfacl` is unresolvable. Install with `apt install acl`, `apk add acl`, or `dnf install acl` (slim container images usually omit it). An already-installed guard keeps working without it — `javi-forge hooks doctor claude` reports the capability as its own row.
383
+ - **Linux guard installation/repair** — the `acl` package (provides `getfacl`) is required to install or repair the Claude PreToolUse guard: the transactional installer proves every controlling directory carries no endangering ACL, and refuses fail-closed when `getfacl` is unresolvable. Install with `apt install acl`, `apk add acl`, or `dnf install acl` (slim container images usually omit it). An already-installed guard keeps working without it — `javi-forge hooks doctor claude` reports the capability as its own row.
384
384
  - **`node` on `PATH`** — Claude Code spawns the guard in exec form, so `node` must resolve on the `PATH` Claude Code itself uses, not only inside javi-forge.
385
385
 
386
386
  ## Ecosystem
@@ -8,7 +8,8 @@ import { fileURLToPath } from "node:url";
8
8
  export const MANAGED_MARKER = "// javi-forge-managed: claude-pretooluse v1";
9
9
  export const INPUT_LIMIT_BYTES = 1_048_576;
10
10
  export const SUPPORTED_TOOLS = Object.freeze(["Bash", "PowerShell", "Read", "Write", "Edit"]);
11
- export const POLICY_REGISTRY = Object.freeze({ schemaVersion: 1, policyVersion: 1, diagnosticsMaxBytes: 240 });
11
+ export const POLICY_REGISTRY = Object.freeze({ schemaVersion: 1, policyVersion: 2, diagnosticsMaxBytes: 240 });
12
+ export function resolvePlatformSupport(platform = process.platform) { return platform === "linux" || platform === "win32" ? { supported: true } : { supported: false, reason: "unsupported-platform" }; }
12
13
  const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
13
14
  // Per-agent adapter config (S0 core-extraction): every agent-specific input the guard needs
14
15
  // (the isManaged protected-path set, the project-dir source, the managed marker) is resolved by
@@ -57,7 +58,7 @@ function lexicalNormalize(input, platform) {
57
58
  else parts.push(part);
58
59
  }
59
60
  value = `${root}${root.endsWith("/") || parts.length === 0 ? "" : "/"}${parts.join("/")}`;
60
- if (windows || platform === "darwin") value = value.normalize("NFC").toLowerCase();
61
+ if (windows) value = value.normalize("NFC").toLowerCase();
61
62
  return value;
62
63
  }
63
64
  function nativeRealpath(input) {
@@ -109,7 +110,7 @@ function policyPathKeys(input, options = {}) {
109
110
  function isAbsolutePolicyPath(value) { return POSIX_ABSOLUTE.test(value) || WINDOWS_DRIVE.test(value) || WINDOWS_UNC.test(value) || /^\\(?:\\\?|\?\?|\\\.)\\/i.test(value); }
110
111
  // Home-relative credential files, matched as a path SUFFIX (never an expanded
111
112
  // literal home) so one rule holds for every user, container and HOME. Literals
112
- // stay lowercase because lexicalNormalize folds keys on darwin/win32.
113
+ // stay lowercase because lexicalNormalize folds keys on win32.
113
114
  const SENSITIVE_PATH_SUFFIXES = Object.freeze(["/.aws/credentials", "/.kube/config", "/.config/gcloud/application_default_credentials.json", "/.docker/config.json", "/.config/gh/hosts.yml"]);
114
115
  // Absolute system secrets: the shadow suite holds every local account's password
115
116
  // hash, and passwd/vipw leave the "-" backup beside it holding the same secret.
@@ -132,17 +133,16 @@ export function isSensitivePolicyKey(key, platform = process.platform) {
132
133
  if (SENSITIVE_ABSOLUTE_KEYS.includes(key) || PROC_ENVIRON_KEY.test(key)) return true;
133
134
  if (SENSITIVE_PATH_SUFFIXES.some((suffix) => key.endsWith(suffix))) return true;
134
135
  if (SENSITIVE_DIRECTORY_SUFFIXES.some((directory) => key.endsWith(directory) || key.includes(`${directory}/`))) return true;
135
- return platform === "win32" || platform === "darwin" ? basename.toLowerCase() === "serviceaccountkey.json" : basename === "serviceAccountKey.json";
136
+ return platform === "win32" ? basename.toLowerCase() === "serviceaccountkey.json" : basename === "serviceAccountKey.json";
136
137
  }
137
138
  function isManaged(key, managedSet = CLAUDE_MANAGED_SET, projectRoot = PROJECT_ROOT) {
138
139
  const project = canonicalizePolicyPath(projectRoot);
139
140
  if (!key.startsWith(`${project}/`) && key !== project) return false;
140
141
  const relative = key.slice(project.length + 1);
141
- // On case-insensitive platforms lexicalNormalize folds the key to lowercase,
142
- // so the mixed-case CLAUDE.md literals must be matched case-insensitively too
143
- // (the other literals are already lowercase). Otherwise CLAUDE.md and
144
- // .claude/CLAUDE.md lose managed-config protection on macOS/Windows.
145
- const foldedClaudeMd = (process.platform === "win32" || process.platform === "darwin") && managedSet.caseFoldExact.includes(relative);
142
+ // On win32 lexicalNormalize folds the key to lowercase, so the mixed-case
143
+ // CLAUDE.md literals must be matched case-insensitively too. Otherwise
144
+ // CLAUDE.md and .claude/CLAUDE.md lose managed-config protection.
145
+ const foldedClaudeMd = process.platform === "win32" && managedSet.caseFoldExact.includes(relative);
146
146
  return foldedClaudeMd || managedSet.exact.includes(relative) || managedSet.prefixes.some((prefix) => relative.startsWith(prefix));
147
147
  }
148
148
  function evaluateFile(toolName, filePath, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT) {
@@ -259,7 +259,7 @@ export function normalizeLiteralUtilityIdentity(rawToken) {
259
259
  // Lexical-only: split on "/" without path/realpath/PATH/alias resolution.
260
260
  // Basename compare is case-insensitive and host-independent so inherited
261
261
  // deny families still fire for CHMOD/ENV/BASE64 on case-insensitive
262
- // filesystems, mirroring the darwin/win32 path folding lexicalNormalize
262
+ // filesystems, mirroring the win32 path folding lexicalNormalize
263
263
  // already applies. The canonical lowercase utility feeds profile lookup.
264
264
  const token = typeof rawToken === "string" ? rawToken : "";
265
265
  const literal = token.length > 0 && !NON_LITERAL_IDENTITY_MARKERS.test(token);
@@ -837,6 +837,7 @@ function diagnostic(error) {
837
837
  "invalid-event": "input does not match the supported event schema",
838
838
  "invalid-config": "missing or unknown --agent selector (expected --agent=<id>)",
839
839
  "oversized-input": "stdin exceeds 1048576 bytes",
840
+ "unsupported-platform": "Linux and Windows are supported hosts",
840
841
  "missing-policy": "embedded policy registry is unavailable",
841
842
  "internal-error": "policy evaluation could not complete",
842
843
  };
@@ -897,10 +898,11 @@ export function readBoundedStdin(stream = process.stdin) {
897
898
  }
898
899
  export async function main() {
899
900
  try {
901
+ if (!resolvePlatformSupport().supported) throw new Error("unsupported-platform");
900
902
  const config = resolveAgentConfig(process.argv);
901
903
  const fault = process.argv.find((arg) => arg.startsWith("--javi-forge-test-fault="))?.split("=")[1];
902
904
  if (fault === "missing-policy") throw new Error("missing-policy");
903
- if (POLICY_REGISTRY.schemaVersion !== 1 || POLICY_REGISTRY.policyVersion !== 1) throw new Error("missing-policy");
905
+ if (POLICY_REGISTRY.schemaVersion !== 1 || POLICY_REGISTRY.policyVersion !== 2) throw new Error("missing-policy");
904
906
  const input = await readBoundedStdin();
905
907
  let parsed;
906
908
  try {
@@ -1 +1,35 @@
1
- {"schemaVersion":1,"asset":{"name":"javi-forge-skillguard-pre-tool-use.mjs","version":1,"policyVersion":1,"sha256":"9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e","historical":["78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc","5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425","0c9aa8fa26b389f4892782f83104f0792c2b71ebca39c18c02706e2185e22b40","3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4","54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa"]},"settingsEntries":{"current":{"version":1,"canonicalSha256":"b1341803cd076091edfcb473494514df1a23bd33372374ca1de6b75258e52e06"},"historical":[{"version":1,"canonicalSha256":"038c59a91bf8967f6908afed74c465f1e7030254e11e4f8738975d6d708424d4"}]},"installerHelpers":{"windowsSecureObject":{"name":"javi-forge-windows-secure-object.ps1","sha256":"2289ef6ac6b039ec74dc3ea0894413e243ff9bea963f04008a356b3838f9b8dd"}}}
1
+ {
2
+ "schemaVersion": 1,
3
+ "asset": {
4
+ "name": "javi-forge-skillguard-pre-tool-use.mjs",
5
+ "version": 1,
6
+ "policyVersion": 2,
7
+ "sha256": "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1",
8
+ "historical": [
9
+ "78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
10
+ "5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
11
+ "0c9aa8fa26b389f4892782f83104f0792c2b71ebca39c18c02706e2185e22b40",
12
+ "3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4",
13
+ "54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa",
14
+ "9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e"
15
+ ]
16
+ },
17
+ "settingsEntries": {
18
+ "current": {
19
+ "version": 1,
20
+ "canonicalSha256": "b1341803cd076091edfcb473494514df1a23bd33372374ca1de6b75258e52e06"
21
+ },
22
+ "historical": [
23
+ {
24
+ "version": 1,
25
+ "canonicalSha256": "038c59a91bf8967f6908afed74c465f1e7030254e11e4f8738975d6d708424d4"
26
+ }
27
+ ]
28
+ },
29
+ "installerHelpers": {
30
+ "windowsSecureObject": {
31
+ "name": "javi-forge-windows-secure-object.ps1",
32
+ "sha256": "2289ef6ac6b039ec74dc3ea0894413e243ff9bea963f04008a356b3838f9b8dd"
33
+ }
34
+ }
35
+ }
@@ -39,21 +39,18 @@ cd /path/to/new-project
39
39
  .\.ci-local\install.ps1 # Windows (PowerShell 7+)
40
40
  ```
41
41
 
42
- > **Importante:** CI-Local depende de `lib/common.sh` (Linux/WSL) y/o `lib/common.psm1` (Windows). Copiá ambos junto con el directorio `ci-local/`. macOS está deprecated y se rechaza para instalaciones o arranques nuevos.
42
+ > **Importante:** CI-Local depende de `lib/common.sh` (Linux/WSL) y/o `lib/common.psm1` (Windows). Copiá ambos junto con el directorio `ci-local/`.
43
43
 
44
44
  El installer (ambas variantes) falla con mensaje claro si `javi-forge` no está en el PATH.
45
45
 
46
46
 
47
- ## macOS deprecation
47
+ ## Plataformas compatibles
48
48
 
49
- macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate your workflow. Existing installed guards are not removed in 1.x; Darwin code removal is planned separately for 2.0.
50
-
51
- ### Soporte cross-platform
49
+ CI-Local soporta Linux/WSL y Windows nativo. Cualquier otro host recibe `unsupported-platform` antes de ejecutar comandos, probes, instalación o mutaciones. No existe un modo de compatibilidad ni un flujo de migración para hosts no soportados.
52
50
 
53
51
  | Plataforma | Installer | Runner | Hooks | Requisitos |
54
52
  |---|---|---|---|---|
55
53
  | Linux | `install.sh` | `ci-local.sh` | bash | bash, perl, docker (opcional) |
56
- | macOS | Unsupported: pin a supported release or migrate | Refused before startup | Existing installed guards retained | No new install or startup |
57
54
  | WSL | `install.sh` | `ci-local.sh` | bash | bash, perl, docker (opcional) |
58
55
  | Windows nativo | `install.ps1` | `ci-local.ps1` | bash | PowerShell 7+, Git for Windows (MSYS2 bash), docker (opcional) |
59
56
 
@@ -27,8 +27,8 @@ function Invoke-CiLocalMain {
27
27
  [ref]$ExitCode
28
28
  )
29
29
 
30
- if ($Platform -eq 'Darwin') {
31
- Write-Host 'macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate. Existing installed guards are not removed; Darwin code removal is planned separately for 2.0.'
30
+ if ($Platform -notin @('Linux', 'Windows')) {
31
+ Write-Host 'unsupported-platform: javi-forge supports Linux and Windows only.'
32
32
  if ($PSBoundParameters.ContainsKey('ExitCode')) {
33
33
  $ExitCode.Value = 1
34
34
  return
@@ -36,15 +36,22 @@ function Invoke-CiLocalMain {
36
36
  return 1
37
37
  }
38
38
 
39
- Invoke-CiLocalStartupBody -Platform $Platform
39
+ $bodyExitCode = 0
40
+ & ${function:Invoke-CiLocalStartupBody} -Platform $Platform -ExitCode ([ref]$bodyExitCode)
40
41
  if ($PSBoundParameters.ContainsKey('ExitCode')) {
41
- $ExitCode.Value = 0
42
+ $ExitCode.Value = $bodyExitCode
43
+ } elseif ($bodyExitCode -ne 0) {
44
+ return $bodyExitCode
42
45
  }
43
46
  }
44
47
 
45
48
  function Invoke-CiLocalStartupBody {
46
- param([string]$Platform)
49
+ param(
50
+ [string]$Platform,
51
+ [ref]$ExitCode
52
+ )
47
53
 
54
+ $ExitCode.Value = 0
48
55
  # See install.ps1 for the rationale behind 7.2 minimum (ResolveLinkTarget).
49
56
  if ($PSVersionTable.PSEdition -ne 'Core' -or $PSVersionTable.PSVersion -lt [Version]'7.2') {
50
57
  Write-Host 'ERROR: ci-local.ps1 requires PowerShell 7.2+ (pwsh).' -ForegroundColor Red
@@ -291,12 +298,15 @@ function Confirm-DockerImage {
291
298
  $buildArgs += @('--build-arg', "JAVA_VERSION=$($Cfg.JavaVersion)")
292
299
  }
293
300
  & docker build @buildArgs -f $dockerfile -t $imageName (Join-Path $ScriptDir 'docker')
294
- if ($LASTEXITCODE -ne 0) { throw 'docker build failed' }
301
+ if ($LASTEXITCODE -ne 0) {
302
+ $ExitCode.Value = $LASTEXITCODE
303
+ return
304
+ }
295
305
  }
296
306
  }
297
307
 
298
308
  # Convert a host path to the form expected by docker run -v.
299
- # Linux/macOS: /path/to/x -> /path/to/x (pass through)
309
+ # Linux/WSL: /path/to/x -> /path/to/x (pass through)
300
310
  # Windows: C:\Users\foo\proj -> /c/Users/foo/proj (Docker Desktop format)
301
311
  # WSL inside Windows is the bash side and never reaches this code.
302
312
  function ConvertTo-DockerHostPath {
@@ -348,7 +358,10 @@ function Invoke-InCi {
348
358
  -v "${hostMount}:/home/runner/work" `
349
359
  -e CI=true `
350
360
  $imageName timeout $timeout bash -c $Cmd
351
- if ($LASTEXITCODE -ne 0) { throw "step failed with exit $LASTEXITCODE" }
361
+ if ($LASTEXITCODE -ne 0) {
362
+ $ExitCode.Value = $LASTEXITCODE
363
+ return
364
+ }
352
365
  }
353
366
 
354
367
  # ─── Main ─────────────────────────────────────────────────────────────
@@ -385,21 +398,25 @@ switch ($Mode) {
385
398
 
386
399
  'quick' {
387
400
  Confirm-DockerImage -Cfg $cfg
401
+ if ($ExitCode.Value -ne 0) { return }
388
402
  Write-Host ''
389
403
  Write-Host 'Running quick check...' -ForegroundColor Yellow
390
404
 
391
405
  if ($cfg.LintCmd) {
392
406
  Write-Host "Lint: $($cfg.LintCmd)" -ForegroundColor Cyan
393
407
  Invoke-InCi -Cfg $cfg -Cmd "cd /home/runner/work && $($cfg.LintCmd)"
408
+ if ($ExitCode.Value -ne 0) { return }
394
409
  }
395
410
  if ($cfg.CompileCmd) {
396
411
  Write-Host "Compile: $($cfg.CompileCmd)" -ForegroundColor Cyan
397
412
  Invoke-InCi -Cfg $cfg -Cmd "cd /home/runner/work && $($cfg.CompileCmd)" -RunUser 'root'
413
+ if ($ExitCode.Value -ne 0) { return }
398
414
  }
399
415
  }
400
416
 
401
417
  'shell' {
402
418
  Confirm-DockerImage -Cfg $cfg
419
+ if ($ExitCode.Value -ne 0) { return }
403
420
  Write-Host ''
404
421
  Write-Host 'Opening shell in CI environment...' -ForegroundColor Yellow
405
422
  $imageName = Get-ImageName -StackType $cfg.StackType
@@ -408,11 +425,16 @@ switch ($Mode) {
408
425
  -v "${hostMount}:/home/runner/work" `
409
426
  -e CI=true `
410
427
  $imageName 'cd /home/runner/work && bash'
428
+ if ($LASTEXITCODE -ne 0) {
429
+ $ExitCode.Value = $LASTEXITCODE
430
+ return
431
+ }
411
432
  }
412
433
 
413
434
  Default {
414
435
  # 'full'
415
436
  Confirm-DockerImage -Cfg $cfg
437
+ if ($ExitCode.Value -ne 0) { return }
416
438
  Write-Host ''
417
439
  Write-Host 'Running full CI simulation...' -ForegroundColor Yellow
418
440
 
@@ -429,6 +451,7 @@ switch ($Mode) {
429
451
  Write-Host "Step $step/$total`: Lint" -ForegroundColor Yellow
430
452
  Write-Host " $($cfg.LintCmd)" -ForegroundColor Cyan
431
453
  Invoke-InCi -Cfg $cfg -Cmd "cd /home/runner/work && $($cfg.LintCmd)"
454
+ if ($ExitCode.Value -ne 0) { return }
432
455
  $step++
433
456
  }
434
457
 
@@ -437,6 +460,7 @@ switch ($Mode) {
437
460
  Write-Host "Step $step/$total`: Compile" -ForegroundColor Yellow
438
461
  Write-Host " $($cfg.CompileCmd)" -ForegroundColor Cyan
439
462
  Invoke-InCi -Cfg $cfg -Cmd "cd /home/runner/work && $($cfg.CompileCmd)" -RunUser 'root'
463
+ if ($ExitCode.Value -ne 0) { return }
440
464
  $step++
441
465
  }
442
466
 
@@ -445,6 +469,7 @@ switch ($Mode) {
445
469
  Write-Host "Step $step/$total`: Test" -ForegroundColor Yellow
446
470
  Write-Host " $($cfg.TestCmd)" -ForegroundColor Cyan
447
471
  Invoke-InCi -Cfg $cfg -Cmd "cd /home/runner/work && $($cfg.TestCmd)"
472
+ if ($ExitCode.Value -ne 0) { return }
448
473
  $step++
449
474
  }
450
475
 
@@ -455,7 +480,8 @@ switch ($Mode) {
455
480
  & ghagga review --plain --exit-on-issues
456
481
  if ($LASTEXITCODE -ne 0) {
457
482
  Write-Host 'GHAGGA review found issues!' -ForegroundColor Red
458
- exit 1
483
+ $ExitCode.Value = $LASTEXITCODE
484
+ return
459
485
  }
460
486
  }
461
487
  }
@@ -468,7 +494,7 @@ Write-Host ''
468
494
  }
469
495
 
470
496
  if ($MyInvocation.InvocationName -ne '.') {
471
- $platform = if ($IsMacOS) { 'Darwin' } else { 'Windows' }
497
+ $platform = if ($IsWindows) { 'Windows' } elseif ($IsLinux) { 'Linux' } else { 'unsupported' }
472
498
  $exitCode = 0
473
499
  & ${function:Invoke-CiLocalMain} -Platform $platform -ExitCode ([ref]$exitCode)
474
500
  if ($exitCode -ne 0) {
@@ -11,23 +11,40 @@
11
11
  # ./ci-local.sh detect # Mostrar stack detectado
12
12
  # =============================================================================
13
13
 
14
- function ci_local_main() {
14
+
15
+ CI_LOCAL_ENTRYPOINT_SOURCE="${BASH_SOURCE[0]}"
16
+ if [[ "$CI_LOCAL_ENTRYPOINT_SOURCE" == /* ]]; then
17
+ CI_LOCAL_ENTRYPOINT_PATH="$CI_LOCAL_ENTRYPOINT_SOURCE"
18
+ else
19
+ CI_LOCAL_ENTRYPOINT_PATH="$PWD/$CI_LOCAL_ENTRYPOINT_SOURCE"
20
+ fi
21
+ CI_LOCAL_ENTRYPOINT_DIR="${CI_LOCAL_ENTRYPOINT_PATH%/*}"
22
+
23
+ function ci_local_normalize_platform() {
15
24
  local platform="${1:?platform required}"
16
- shift
17
25
 
18
- if [ "$platform" = "Darwin" ]; then
19
- printf '%s\n' 'macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate. Existing installed guards are not removed; Darwin code removal is planned separately for 2.0.'
20
- return 1
21
- fi
26
+ case "$platform" in
27
+ MINGW*_NT-*|MSYS*|CYGWIN*) printf '%s\n' 'Windows' ;;
28
+ *) printf '%s\n' "$platform" ;;
29
+ esac
30
+ }
31
+
32
+ function ci_local_main() {
33
+ local platform
34
+ platform="$(ci_local_normalize_platform "${1:?platform required}")"
35
+ shift
22
36
 
23
- ci_local_startup_body "$@"
37
+ case "$platform" in
38
+ Linux|Windows) ci_local_startup_body "$@" ;;
39
+ *) printf '%s\n' 'unsupported-platform: javi-forge supports Linux and Windows only.'; return 1 ;;
40
+ esac
24
41
  }
25
42
 
26
43
  function ci_local_startup_body() {
27
44
  set -e
28
45
 
29
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
30
- PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
46
+ SCRIPT_DIR="$CI_LOCAL_ENTRYPOINT_DIR"
47
+ PROJECT_DIR="${SCRIPT_DIR%/*}"
31
48
 
32
49
  # Require realpath: load-bearing for the lib symlink check below.
33
50
  if ! command -v realpath >/dev/null 2>&1; then
@@ -7,7 +7,7 @@
7
7
  # .\install.ps1
8
8
  #
9
9
  # Behaviour MUST match install.sh. Diff against the bash version on every
10
- # change. Test on real Windows + WSL + macOS pwsh before tagging a release.
10
+ # change. Test on supported Windows and WSL hosts before tagging a release.
11
11
  # =============================================================================
12
12
 
13
13
  #Requires -Version 7.2
@@ -15,19 +15,36 @@
15
15
  param()
16
16
 
17
17
  function Invoke-CiLocalMain {
18
- param([Parameter(Mandatory)][string]$Platform)
19
-
20
- if ($Platform -eq 'Darwin') {
21
- Write-Host 'macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate. Existing installed guards are not removed; Darwin code removal is planned separately for 2.0.'
18
+ param(
19
+ [Parameter(Mandatory)][string]$Platform,
20
+ [ref]$ExitCode
21
+ )
22
+
23
+ if ($Platform -notin @('Linux', 'Windows')) {
24
+ Write-Host 'unsupported-platform: javi-forge supports Linux and Windows only.'
25
+ if ($PSBoundParameters.ContainsKey('ExitCode')) {
26
+ $ExitCode.Value = 1
27
+ return
28
+ }
22
29
  return 1
23
30
  }
24
31
 
25
- Invoke-CiLocalStartupBody -Platform $Platform
32
+ $bodyExitCode = 0
33
+ & ${function:Invoke-CiLocalStartupBody} -Platform $Platform -ExitCode ([ref]$bodyExitCode)
34
+ if ($PSBoundParameters.ContainsKey('ExitCode')) {
35
+ $ExitCode.Value = $bodyExitCode
36
+ } elseif ($bodyExitCode -ne 0) {
37
+ return $bodyExitCode
38
+ }
26
39
  }
27
40
 
28
41
  function Invoke-CiLocalStartupBody {
29
- param([string]$Platform)
42
+ param(
43
+ [string]$Platform,
44
+ [ref]$ExitCode
45
+ )
30
46
 
47
+ $ExitCode.Value = 0
31
48
  # Defense-in-depth: #Requires is parsed by some hosts AFTER the script body.
32
49
  # 7.2 is the minimum because FileSystemInfo.ResolveLinkTarget($true) is .NET 6+,
33
50
  # which shipped with pwsh 7.2. Earlier pwsh 7.x runs on .NET 5 and the method
@@ -159,9 +176,11 @@ try {
159
176
 
160
177
  $jfVersionOk = $true
161
178
  $jfVersionOutput = ''
179
+ $jfVersionExitCode = 1
162
180
  try {
163
181
  $jfVersionOutput = (& javi-forge --version 2>&1) -join "`n"
164
- if ($LASTEXITCODE -ne 0) { $jfVersionOk = $false }
182
+ $jfVersionExitCode = $LASTEXITCODE
183
+ if ($jfVersionExitCode -ne 0) { $jfVersionOk = $false }
165
184
  } catch {
166
185
  $jfVersionOk = $false
167
186
  $jfVersionOutput = $_.Exception.Message
@@ -177,7 +196,8 @@ try {
177
196
  Write-Host "ERROR: javi-forge found at $jfPath but '--version' failed" -ForegroundColor Red
178
197
  Write-Host "Output: $jfVersionOutput" -ForegroundColor Yellow
179
198
  Write-Host 'Reinstall: npm install -g javi-forge' -ForegroundColor Cyan
180
- exit 1
199
+ $ExitCode.Value = $jfVersionExitCode
200
+ return
181
201
  }
182
202
 
183
203
  # Reject UNC / remote paths outright — a javi-forge symlinked to
@@ -297,12 +317,13 @@ try {
297
317
  & git config core.hooksPath $hooksRel
298
318
  if ($LASTEXITCODE -ne 0) {
299
319
  Write-Host 'ERROR: failed to set core.hooksPath. Are you inside a git repo?' -ForegroundColor Red
300
- exit 1
320
+ $ExitCode.Value = $LASTEXITCODE
321
+ return
301
322
  }
302
323
  Write-Host "hooksPath = $hooksRel" -ForegroundColor Green
303
324
 
304
325
  # Make hooks executable. Different OS, different model:
305
- # - macOS / Linux / WSL: chmod 0755 — POSIX bits, what git expects.
326
+ # - Linux / WSL: chmod 0755 — POSIX bits, what git expects.
306
327
  # - Windows native NTFS: chmod is a no-op; lock the ACL via icacls so
307
328
  # only the current user can rewrite the hooks between commits.
308
329
  if (Test-CommandExists -Name 'chmod') {
@@ -382,9 +403,10 @@ try {
382
403
  }
383
404
 
384
405
  if ($MyInvocation.InvocationName -ne '.') {
385
- $platform = if ($IsMacOS) { 'Darwin' } else { 'Windows' }
386
- $exitCode = & ${function:Invoke-CiLocalMain} -Platform $platform
387
- if ($exitCode -is [int] -and $exitCode -ne 0) {
406
+ $platform = if ($IsWindows) { 'Windows' } elseif ($IsLinux) { 'Linux' } else { 'unsupported' }
407
+ $exitCode = 0
408
+ & ${function:Invoke-CiLocalMain} -Platform $platform -ExitCode ([ref]$exitCode)
409
+ if ($exitCode -ne 0) {
388
410
  exit $exitCode
389
411
  }
390
412
  }
@@ -3,22 +3,40 @@
3
3
  # CI-LOCAL: Installation Script
4
4
  # =============================================================================
5
5
 
6
- function ci_local_main() {
6
+
7
+ CI_LOCAL_ENTRYPOINT_SOURCE="${BASH_SOURCE[0]}"
8
+ if [[ "$CI_LOCAL_ENTRYPOINT_SOURCE" == /* ]]; then
9
+ CI_LOCAL_ENTRYPOINT_PATH="$CI_LOCAL_ENTRYPOINT_SOURCE"
10
+ else
11
+ CI_LOCAL_ENTRYPOINT_PATH="$PWD/$CI_LOCAL_ENTRYPOINT_SOURCE"
12
+ fi
13
+ CI_LOCAL_ENTRYPOINT_DIR="${CI_LOCAL_ENTRYPOINT_PATH%/*}"
14
+
15
+ function ci_local_normalize_platform() {
7
16
  local platform="${1:?platform required}"
8
17
 
9
- if [ "$platform" = "Darwin" ]; then
10
- printf '%s\n' 'macOS is deprecated and unsupported for new CI-Local install/startup. Pin a supported release or migrate. Existing installed guards are not removed; Darwin code removal is planned separately for 2.0.'
11
- return 1
12
- fi
18
+ case "$platform" in
19
+ MINGW*_NT-*|MSYS*|CYGWIN*) printf '%s\n' 'Windows' ;;
20
+ *) printf '%s\n' "$platform" ;;
21
+ esac
22
+ }
13
23
 
14
- ci_local_startup_body "$platform"
24
+ function ci_local_main() {
25
+ local platform
26
+ platform="$(ci_local_normalize_platform "${1:?platform required}")"
27
+ shift
28
+
29
+ case "$platform" in
30
+ Linux|Windows) ci_local_startup_body "$@" ;;
31
+ *) printf '%s\n' 'unsupported-platform: javi-forge supports Linux and Windows only.'; return 1 ;;
32
+ esac
15
33
  }
16
34
 
17
35
  function ci_local_startup_body() {
18
36
  set -e
19
37
 
20
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21
- PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
38
+ SCRIPT_DIR="$CI_LOCAL_ENTRYPOINT_DIR"
39
+ PROJECT_DIR="${SCRIPT_DIR%/*}"
22
40
 
23
41
  # ─── Required tools (fail-closed) ──────────────────────────────────
24
42
  # realpath is load-bearing for the symlink-traversal checks below. If it's
@@ -27,7 +45,6 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
27
45
  if ! command -v realpath >/dev/null 2>&1; then
28
46
  printf 'ERROR: realpath is required for ci-local install (symlink checks).\n' >&2
29
47
  printf ' Linux/WSL: install coreutils\n' >&2
30
- printf ' macOS: brew install coreutils, then add gnubin to PATH\n' >&2
31
48
  exit 1
32
49
  fi
33
50
 
@@ -208,5 +225,5 @@ echo -e ""
208
225
  }
209
226
 
210
227
  if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
211
- ci_local_main "$(/usr/bin/uname -s)"
228
+ ci_local_main "$(/usr/bin/uname -s)" "$@"
212
229
  fi
@@ -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
  # =============================================================================
@@ -100,7 +96,7 @@ detect_stack() {
100
96
  STACK_TYPE="java-gradle"
101
97
  BUILD_TOOL="gradle"
102
98
 
103
- # Detect Java version from build files (compatible with macOS and Linux)
99
+ # Detect Java version from build files
104
100
  if [[ -f "$project_dir/build.gradle.kts" ]]; then
105
101
  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")
106
102
  elif [[ -f "$project_dir/build.gradle" ]]; then
@@ -16,6 +16,42 @@ param(
16
16
  [switch]$Purge
17
17
  )
18
18
 
19
+
20
+ function Invoke-CiLocalMain {
21
+ param(
22
+ [Parameter(Mandatory)][string]$Platform,
23
+ [switch]$RestoreBackups,
24
+ [switch]$Purge,
25
+ [ref]$ExitCode
26
+ )
27
+
28
+ if ($Platform -notin @('Linux', 'Windows')) {
29
+ Write-Host 'unsupported-platform: javi-forge supports Linux and Windows only.'
30
+ if ($PSBoundParameters.ContainsKey('ExitCode')) {
31
+ $ExitCode.Value = 1
32
+ return
33
+ }
34
+ return 1
35
+ }
36
+
37
+ $bodyExitCode = 0
38
+ & ${function:Invoke-CiLocalStartupBody} -Platform $Platform -RestoreBackups:$RestoreBackups -Purge:$Purge -ExitCode ([ref]$bodyExitCode)
39
+ if ($PSBoundParameters.ContainsKey('ExitCode')) {
40
+ $ExitCode.Value = $bodyExitCode
41
+ } elseif ($bodyExitCode -ne 0) {
42
+ return $bodyExitCode
43
+ }
44
+ }
45
+
46
+ function Invoke-CiLocalStartupBody {
47
+ param(
48
+ [Parameter(Mandatory)][string]$Platform,
49
+ [switch]$RestoreBackups,
50
+ [switch]$Purge,
51
+ [ref]$ExitCode
52
+ )
53
+
54
+ $ExitCode.Value = 0
19
55
  Set-StrictMode -Version Latest
20
56
  $ErrorActionPreference = 'Stop'
21
57
 
@@ -32,6 +68,10 @@ try {
32
68
  $current = (& git config --get core.hooksPath 2>$null) -join ''
33
69
  if ($current) {
34
70
  & git config --unset core.hooksPath
71
+ if ($LASTEXITCODE -ne 0) {
72
+ $ExitCode.Value = $LASTEXITCODE
73
+ return
74
+ }
35
75
  Write-Host "Removed hooksPath = $current" -ForegroundColor Green
36
76
  } else {
37
77
  Write-Host 'No core.hooksPath was set; nothing to remove' -ForegroundColor Yellow
@@ -83,3 +123,13 @@ try {
83
123
  } finally {
84
124
  Pop-Location
85
125
  }
126
+ }
127
+
128
+ if ($MyInvocation.InvocationName -ne '.') {
129
+ $platform = if ($IsWindows) { 'Windows' } elseif ($IsLinux) { 'Linux' } else { 'unsupported' }
130
+ $exitCode = 0
131
+ & ${function:Invoke-CiLocalMain} -Platform $platform -RestoreBackups:$RestoreBackups -Purge:$Purge -ExitCode ([ref]$exitCode)
132
+ if ($exitCode -ne 0) {
133
+ exit $exitCode
134
+ }
135
+ }