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
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
+ }
@@ -14,8 +14,8 @@
14
14
  ]
15
15
  },
16
16
  "pre-push": {
17
- "version": 3,
18
- "sha256": "c396387a15c3c448d524f68597a993f6b4341ae06143d2d5907ad914823d0ad0",
17
+ "version": 4,
18
+ "sha256": "ffe1b5dd0931617df3c9fea37947dac84dffd11c490c39590459e861b404148d",
19
19
  "historical": [
20
20
  {
21
21
  "sha256": "7de58640aeef33085a49f31f1d9d0c8bacde0069d6d3265ae41aa8d3cd14d7a5",
@@ -28,6 +28,10 @@
28
28
  {
29
29
  "sha256": "c396387a15c3c448d524f68597a993f6b4341ae06143d2d5907ad914823d0ad0",
30
30
  "firstCommit": "0779d746b23ad614a13181d60f1fe3aa034e91dd"
31
+ },
32
+ {
33
+ "sha256": "ffe1b5dd0931617df3c9fea37947dac84dffd11c490c39590459e861b404148d",
34
+ "firstCommit": "6b5799d9d9e8c528fad9dc1f7b641afbd3a60637"
31
35
  }
32
36
  ]
33
37
  },
@@ -4,13 +4,211 @@
4
4
  # Default: quick native CI gate (setup + lint + compile + gates — no tests, no coverage).
5
5
  # Requires javi-forge on PATH (npx fallback; offline npx fails -> hook fails closed).
6
6
  # To skip: git push --no-verify
7
- set -e
8
- if command -v javi-forge >/dev/null 2>&1; then
9
- javi-forge hooks run pre-push
10
- else
11
- npx javi-forge hooks run pre-push
12
- fi || {
7
+ fail_pre_push() {
8
+ message=$1
9
+ status=${2:-1}
13
10
  echo ""
14
- echo "pre-push FAILED - fix the issues above."
15
- exit 1
11
+ echo "pre-push FAILED - $message"
12
+ exit "$status"
13
+ }
14
+
15
+ hash_required_file() {
16
+ target=$1
17
+ if [ -L "$target" ] || [ ! -f "$target" ]; then
18
+ return 1
19
+ fi
20
+ git hash-object --no-filters "$target"
21
+ }
22
+
23
+ hash_optional_file() {
24
+ target=$1
25
+ if [ -L "$target" ]; then
26
+ return 1
27
+ fi
28
+ if [ -f "$target" ]; then
29
+ git hash-object --no-filters "$target"
30
+ return $?
31
+ fi
32
+ if [ -e "$target" ]; then
33
+ return 1
34
+ fi
35
+ printf '%s\n' absent
16
36
  }
37
+
38
+ # Git exports repository-local targeting variables to hooks. Preserve every
39
+ # path needed for this repository before clearing the complete Git-owned set.
40
+ if ! repository_root=$(git rev-parse --show-toplevel) || [ -z "$repository_root" ]; then
41
+ fail_pre_push "Could not resolve the repository root."
42
+ fi
43
+ if ! cd "$repository_root"; then
44
+ fail_pre_push "Could not return to the repository root."
45
+ fi
46
+ if ! git_dir=$(git rev-parse --absolute-git-dir) || [ -z "$git_dir" ]; then
47
+ fail_pre_push "Could not resolve the worktree Git directory."
48
+ fi
49
+ if ! common_dir=$(git rev-parse --path-format=absolute --git-common-dir) || [ -z "$common_dir" ]; then
50
+ fail_pre_push "Could not resolve the common Git directory."
51
+ fi
52
+ if ! index_path=$(git rev-parse --path-format=absolute --git-path index) || [ -z "$index_path" ]; then
53
+ fail_pre_push "Could not resolve the worktree index."
54
+ fi
55
+ if ! worktree_config_path=$(git rev-parse --path-format=absolute --git-path config.worktree) || [ -z "$worktree_config_path" ]; then
56
+ fail_pre_push "Could not resolve the worktree-local Git config."
57
+ fi
58
+
59
+ if ! git_local_env_vars=$(git rev-parse --local-env-vars); then
60
+ fail_pre_push "Could not discover Git-local environment variables."
61
+ fi
62
+
63
+ git_local_env_count=0
64
+ invalid_git_local_env_var=
65
+ saved_ifs=$IFS
66
+ IFS='
67
+ '
68
+ for git_local_env_var in $git_local_env_vars; do
69
+ git_local_env_count=$((git_local_env_count + 1))
70
+ case "$git_local_env_var" in
71
+ GIT_*) ;;
72
+ *) invalid_git_local_env_var=$git_local_env_var ;;
73
+ esac
74
+ case "$git_local_env_var" in
75
+ *[!A-Za-z0-9_]*) invalid_git_local_env_var=$git_local_env_var ;;
76
+ esac
77
+ if [ -n "$invalid_git_local_env_var" ]; then
78
+ break
79
+ fi
80
+ done
81
+ IFS=$saved_ifs
82
+
83
+ if [ "$git_local_env_count" -eq 0 ]; then
84
+ fail_pre_push "Git returned no Git-local environment variables."
85
+ fi
86
+ if [ -n "$invalid_git_local_env_var" ]; then
87
+ fail_pre_push "Invalid Git-local environment variable name: $invalid_git_local_env_var"
88
+ fi
89
+
90
+ # A bounded canary protects the metadata proven vulnerable in the incident.
91
+ common_config_path="$common_dir/config"
92
+ head_path="$git_dir/HEAD"
93
+ packed_refs_path="$common_dir/packed-refs"
94
+
95
+ if ! common_config_before=$(hash_required_file "$common_config_path"); then
96
+ fail_pre_push "Could not snapshot the common Git config."
97
+ fi
98
+ if ! head_before=$(hash_required_file "$head_path"); then
99
+ fail_pre_push "Could not snapshot HEAD."
100
+ fi
101
+ if ! index_before=$(hash_required_file "$index_path"); then
102
+ fail_pre_push "Could not snapshot the worktree index."
103
+ fi
104
+ if ! worktree_config_before=$(hash_optional_file "$worktree_config_path"); then
105
+ fail_pre_push "Could not snapshot the worktree-local Git config."
106
+ fi
107
+ if ! packed_refs_before=$(hash_optional_file "$packed_refs_path"); then
108
+ fail_pre_push "Could not snapshot packed refs."
109
+ fi
110
+
111
+ attached_ref=
112
+ ref_path=
113
+ ref_before=detached
114
+ if attached_ref=$(git symbolic-ref -q HEAD); then
115
+ if ! ref_path=$(git rev-parse --path-format=absolute --git-path "$attached_ref") || [ -z "$ref_path" ]; then
116
+ fail_pre_push "Could not resolve the attached HEAD ref."
117
+ fi
118
+ if ! ref_before=$(hash_optional_file "$ref_path"); then
119
+ fail_pre_push "Could not snapshot the attached HEAD ref."
120
+ fi
121
+ if ! ref_oid_before=$(git rev-parse --verify "$attached_ref"); then
122
+ fail_pre_push "Could not resolve the attached HEAD ref value."
123
+ fi
124
+ else
125
+ symbolic_ref_status=$?
126
+ if [ "$symbolic_ref_status" -ne 1 ]; then
127
+ fail_pre_push "Could not determine whether HEAD is attached."
128
+ fi
129
+ if ! ref_oid_before=$(git rev-parse --verify HEAD); then
130
+ fail_pre_push "Could not resolve detached HEAD."
131
+ fi
132
+ fi
133
+
134
+ # The scrub lives in a subshell: the hook retains its own targeting context for
135
+ # the post-test canary, while the dispatcher and every descendant start clean.
136
+ if (
137
+ scrub_ifs=$IFS
138
+ IFS='
139
+ '
140
+ for git_local_env_var in $git_local_env_vars; do
141
+ if ! unset "$git_local_env_var"; then
142
+ echo "pre-push: could not unset $git_local_env_var"
143
+ exit 1
144
+ fi
145
+ if declare -p "$git_local_env_var" >/dev/null 2>&1; then
146
+ echo "pre-push: $git_local_env_var remained set after unset"
147
+ exit 1
148
+ fi
149
+ done
150
+ IFS=$scrub_ifs
151
+
152
+ if ! cd "$repository_root"; then
153
+ echo "pre-push: could not return to the repository root after scrubbing"
154
+ exit 1
155
+ fi
156
+
157
+ if command -v javi-forge >/dev/null 2>&1; then
158
+ javi-forge hooks run pre-push
159
+ else
160
+ npx javi-forge hooks run pre-push
161
+ fi
162
+ ); then
163
+ test_status=0
164
+ else
165
+ test_status=$?
166
+ fi
167
+
168
+ if ! cd "$repository_root"; then
169
+ fail_pre_push "Could not return to the repository root after tests."
170
+ fi
171
+
172
+ if ! common_config_after=$(hash_required_file "$common_config_path"); then
173
+ fail_pre_push "Could not verify the common Git config after tests."
174
+ fi
175
+ if ! head_after=$(hash_required_file "$head_path"); then
176
+ fail_pre_push "Could not verify HEAD after tests."
177
+ fi
178
+ if ! index_after=$(hash_required_file "$index_path"); then
179
+ fail_pre_push "Could not verify the worktree index after tests."
180
+ fi
181
+ if ! worktree_config_after=$(hash_optional_file "$worktree_config_path"); then
182
+ fail_pre_push "Could not verify the worktree-local Git config after tests."
183
+ fi
184
+ if ! packed_refs_after=$(hash_optional_file "$packed_refs_path"); then
185
+ fail_pre_push "Could not verify packed refs after tests."
186
+ fi
187
+
188
+ ref_after=detached
189
+ if [ -n "$attached_ref" ]; then
190
+ if ! ref_after=$(hash_optional_file "$ref_path"); then
191
+ fail_pre_push "Could not verify the attached HEAD ref after tests."
192
+ fi
193
+ if ! ref_oid_after=$(git rev-parse --verify "$attached_ref"); then
194
+ fail_pre_push "Could not resolve the attached HEAD ref value after tests."
195
+ fi
196
+ else
197
+ if ! ref_oid_after=$(git rev-parse --verify HEAD); then
198
+ fail_pre_push "Could not resolve detached HEAD after tests."
199
+ fi
200
+ fi
201
+
202
+ if [ "$common_config_before" != "$common_config_after" ] ||
203
+ [ "$head_before" != "$head_after" ] ||
204
+ [ "$index_before" != "$index_after" ] ||
205
+ [ "$worktree_config_before" != "$worktree_config_after" ] ||
206
+ [ "$packed_refs_before" != "$packed_refs_after" ] ||
207
+ [ "$ref_before" != "$ref_after" ] ||
208
+ [ "$ref_oid_before" != "$ref_oid_after" ]; then
209
+ fail_pre_push "Git metadata changed during pre-push tests."
210
+ fi
211
+
212
+ if [ "$test_status" -ne 0 ]; then
213
+ fail_pre_push "fix the issues above." "$test_status"
214
+ fi
@@ -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