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.
- package/README.md +5 -5
- package/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +13 -11
- package/assets/claude-hooks/manifest.json +35 -1
- package/assets/hooks/manifest.json +6 -2
- package/assets/hooks/pre-push +206 -8
- package/ci-local/README.md +3 -6
- package/ci-local/ci-local.ps1 +36 -10
- package/ci-local/ci-local.sh +26 -9
- package/ci-local/install.ps1 +36 -14
- package/ci-local/install.sh +27 -10
- package/ci-local/lib/common.sh +3 -7
- package/ci-local/uninstall.ps1 +50 -0
- package/ci-local/uninstall.sh +38 -3
- package/dist/cli/bootstrap.d.ts +12 -0
- package/dist/cli/bootstrap.js +12 -0
- package/dist/cli/dispatch/simple-renderers.d.ts +8 -1
- package/dist/cli/dispatch/simple-renderers.js +11 -2
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +85 -0
- package/dist/commands/claude-hooks.d.ts +1 -0
- package/dist/commands/claude-hooks.js +12 -3
- package/dist/commands/codex-hooks.d.ts +1 -0
- package/dist/commands/codex-hooks.js +12 -3
- package/dist/commands/doctor.d.ts +16 -1
- package/dist/commands/doctor.js +54 -35
- package/dist/commands/init.d.ts +4 -1
- package/dist/commands/init.js +2 -2
- package/dist/index.js +4 -81
- package/dist/lib/claude-hook-manager.d.ts +11 -6
- package/dist/lib/claude-hook-manager.js +28 -21
- package/dist/lib/codex-hook-manager.d.ts +10 -4
- package/dist/lib/codex-hook-manager.js +44 -13
- package/dist/lib/platform-support.d.ts +18 -14
- package/dist/lib/platform-support.js +19 -17
- package/dist/lib/secure-fs-posix.d.ts +8 -13
- package/dist/lib/secure-fs-posix.js +11 -52
- package/dist/lib/secure-fs-transaction.d.ts +2 -3
- package/dist/types/index.d.ts +7 -2
- package/dist/ui/Doctor.d.ts +2 -0
- package/dist/ui/Doctor.js +13 -0
- package/lib/common.sh +3 -7
- 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
|
|
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:
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
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
|
|
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 !==
|
|
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
|
-
{
|
|
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":
|
|
18
|
-
"sha256": "
|
|
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
|
},
|
package/assets/hooks/pre-push
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 -
|
|
15
|
-
exit
|
|
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
|
package/ci-local/README.md
CHANGED
|
@@ -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/`.
|
|
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
|
-
##
|
|
47
|
+
## Plataformas compatibles
|
|
48
48
|
|
|
49
|
-
|
|
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
|
|
package/ci-local/ci-local.ps1
CHANGED
|
@@ -27,8 +27,8 @@ function Invoke-CiLocalMain {
|
|
|
27
27
|
[ref]$ExitCode
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
if ($Platform -
|
|
31
|
-
Write-Host '
|
|
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
|
-
|
|
39
|
+
$bodyExitCode = 0
|
|
40
|
+
& ${function:Invoke-CiLocalStartupBody} -Platform $Platform -ExitCode ([ref]$bodyExitCode)
|
|
40
41
|
if ($PSBoundParameters.ContainsKey('ExitCode')) {
|
|
41
|
-
$ExitCode.Value =
|
|
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(
|
|
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) {
|
|
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/
|
|
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) {
|
|
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
|
-
|
|
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 ($
|
|
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) {
|
package/ci-local/ci-local.sh
CHANGED
|
@@ -11,23 +11,40 @@
|
|
|
11
11
|
# ./ci-local.sh detect # Mostrar stack detectado
|
|
12
12
|
# =============================================================================
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
printf '%s\n' '
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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="$
|
|
30
|
-
PROJECT_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
|