javi-forge 1.22.3 → 1.23.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.
- package/ci-local/README.md +2 -2
- package/ci-local/ci-local.ps1 +1 -3
- package/ci-local/ci-local.sh +1 -3
- package/ci-local/docker/elixir.Dockerfile +6 -0
- package/ci-local/docker/go.Dockerfile +7 -0
- package/ci-local/docker/java-gradle.Dockerfile +7 -0
- package/ci-local/docker/java-maven.Dockerfile +7 -0
- package/ci-local/docker/java.Dockerfile +7 -0
- package/ci-local/docker/python.Dockerfile +7 -0
- package/ci-local/docker/rust.Dockerfile +7 -0
- package/dist/cli/dispatch/hooks.d.ts +12 -0
- package/dist/cli/dispatch/hooks.js +31 -0
- package/dist/cli/help.d.ts +6 -1
- package/dist/cli/help.js +26 -0
- package/dist/commands/hooks.d.ts +60 -0
- package/dist/commands/hooks.js +169 -0
- package/dist/index.js +8 -3
- package/dist/lib/ci-config.d.ts +38 -0
- package/dist/lib/ci-config.js +125 -3
- package/dist/lib/docker.js +23 -3
- package/package.json +1 -1
package/ci-local/README.md
CHANGED
|
@@ -174,7 +174,7 @@ Developer workflow:
|
|
|
174
174
|
│ ├── commit-msg # Valida mensaje sin AI attribution
|
|
175
175
|
│ └── pre-push # CI simulation en Docker
|
|
176
176
|
└── docker/
|
|
177
|
-
└── *.Dockerfile #
|
|
177
|
+
└── *.Dockerfile # Incluidos para cada stack (fallback: se regeneran si faltan)
|
|
178
178
|
|
|
179
179
|
lib/ # Dependencia requerida (copiar junto con .ci-local/)
|
|
180
180
|
├── common.sh # Funciones compartidas (Bash)
|
|
@@ -193,7 +193,7 @@ Editar `lib/common.sh` (detección de stack) o `setup_ci_commands()` en `ci-loca
|
|
|
193
193
|
|
|
194
194
|
### Docker image custom
|
|
195
195
|
|
|
196
|
-
Editar los Dockerfiles en `.ci-local/docker/`
|
|
196
|
+
Editar los Dockerfiles en `.ci-local/docker/` (incluidos por stack).
|
|
197
197
|
|
|
198
198
|
## FAQ
|
|
199
199
|
|
package/ci-local/ci-local.ps1
CHANGED
|
@@ -177,7 +177,6 @@ RUN apt-get update && apt-get install -y git curl unzip && rm -rf /var/lib/apt/l
|
|
|
177
177
|
RUN useradd -m -s /bin/bash runner
|
|
178
178
|
USER runner
|
|
179
179
|
WORKDIR /home/runner/work
|
|
180
|
-
ENV GRADLE_USER_HOME=/home/runner/.gradle
|
|
181
180
|
ENTRYPOINT ["/bin/bash", "-c"]
|
|
182
181
|
'@
|
|
183
182
|
}
|
|
@@ -207,8 +206,7 @@ ENTRYPOINT ["/bin/bash", "-c"]
|
|
|
207
206
|
@'
|
|
208
207
|
FROM golang:1.23-bookworm@sha256:167053a2bb901972bf2c1611f8f52c44d5fe7e762e5cab213708d82c421614db
|
|
209
208
|
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
|
210
|
-
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 &&
|
|
211
|
-
mv /root/go/bin/golangci-lint /usr/local/bin/
|
|
209
|
+
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 && mv /root/go/bin/golangci-lint /usr/local/bin/
|
|
212
210
|
RUN useradd -m -s /bin/bash runner
|
|
213
211
|
USER runner
|
|
214
212
|
WORKDIR /home/runner/work
|
package/ci-local/ci-local.sh
CHANGED
|
@@ -174,7 +174,6 @@ RUN apt-get update && apt-get install -y git curl unzip && rm -rf /var/lib/apt/l
|
|
|
174
174
|
RUN useradd -m -s /bin/bash runner
|
|
175
175
|
USER runner
|
|
176
176
|
WORKDIR /home/runner/work
|
|
177
|
-
ENV GRADLE_USER_HOME=/home/runner/.gradle
|
|
178
177
|
ENTRYPOINT ["/bin/bash", "-c"]
|
|
179
178
|
DOCKERFILE
|
|
180
179
|
;;
|
|
@@ -204,8 +203,7 @@ DOCKERFILE
|
|
|
204
203
|
cat > "$docker_dir/$DOCKERFILE" << 'DOCKERFILE'
|
|
205
204
|
FROM golang:1.23-bookworm@sha256:167053a2bb901972bf2c1611f8f52c44d5fe7e762e5cab213708d82c421614db
|
|
206
205
|
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
|
207
|
-
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 &&
|
|
208
|
-
mv /root/go/bin/golangci-lint /usr/local/bin/
|
|
206
|
+
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 && mv /root/go/bin/golangci-lint /usr/local/bin/
|
|
209
207
|
RUN useradd -m -s /bin/bash runner
|
|
210
208
|
USER runner
|
|
211
209
|
WORKDIR /home/runner/work
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
FROM ubuntu:24.04@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b
|
|
2
|
+
RUN apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*
|
|
3
|
+
RUN useradd -m -s /bin/bash runner
|
|
4
|
+
USER runner
|
|
5
|
+
WORKDIR /home/runner/work
|
|
6
|
+
ENTRYPOINT ["/bin/bash", "-c"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
FROM golang:1.23-bookworm@sha256:167053a2bb901972bf2c1611f8f52c44d5fe7e762e5cab213708d82c421614db
|
|
2
|
+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
|
3
|
+
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 && mv /root/go/bin/golangci-lint /usr/local/bin/
|
|
4
|
+
RUN useradd -m -s /bin/bash runner
|
|
5
|
+
USER runner
|
|
6
|
+
WORKDIR /home/runner/work
|
|
7
|
+
ENTRYPOINT ["/bin/bash", "-c"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ARG JAVA_VERSION=21
|
|
2
|
+
FROM eclipse-temurin:${JAVA_VERSION}-jdk-noble
|
|
3
|
+
RUN apt-get update && apt-get install -y git curl unzip && rm -rf /var/lib/apt/lists/*
|
|
4
|
+
RUN useradd -m -s /bin/bash runner
|
|
5
|
+
USER runner
|
|
6
|
+
WORKDIR /home/runner/work
|
|
7
|
+
ENTRYPOINT ["/bin/bash", "-c"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ARG JAVA_VERSION=21
|
|
2
|
+
FROM eclipse-temurin:${JAVA_VERSION}-jdk-noble
|
|
3
|
+
RUN apt-get update && apt-get install -y git curl unzip && rm -rf /var/lib/apt/lists/*
|
|
4
|
+
RUN useradd -m -s /bin/bash runner
|
|
5
|
+
USER runner
|
|
6
|
+
WORKDIR /home/runner/work
|
|
7
|
+
ENTRYPOINT ["/bin/bash", "-c"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ARG JAVA_VERSION=21
|
|
2
|
+
FROM eclipse-temurin:${JAVA_VERSION}-jdk-noble
|
|
3
|
+
RUN apt-get update && apt-get install -y git curl unzip && rm -rf /var/lib/apt/lists/*
|
|
4
|
+
RUN useradd -m -s /bin/bash runner
|
|
5
|
+
USER runner
|
|
6
|
+
WORKDIR /home/runner/work
|
|
7
|
+
ENTRYPOINT ["/bin/bash", "-c"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
FROM python:3.12-slim@sha256:401f6e1a67dad31a1bd78e9ad22d0ee0a3b52154e6bd30e90be696bb6a3d7461
|
|
2
|
+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
|
3
|
+
RUN pip install --no-cache-dir pytest ruff pylint poetry
|
|
4
|
+
RUN useradd -m -s /bin/bash runner
|
|
5
|
+
USER runner
|
|
6
|
+
WORKDIR /home/runner/work
|
|
7
|
+
ENTRYPOINT ["/bin/bash", "-c"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
FROM rust:1.83-slim@sha256:540c902e99c384163b688bbd8b5b8520e94e7731b27f7bd0eaa56ae1960627ab
|
|
2
|
+
RUN apt-get update && apt-get install -y git pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
|
3
|
+
RUN rustup component add clippy rustfmt
|
|
4
|
+
RUN useradd -m -s /bin/bash runner
|
|
5
|
+
USER runner
|
|
6
|
+
WORKDIR /home/runner/work
|
|
7
|
+
ENTRYPOINT ["/bin/bash", "-c"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `javi-forge hooks` handler — console-only (no Ink; a git hook runs in a
|
|
3
|
+
* terminal git owns). Mirrors the `ci init` branch: the command module
|
|
4
|
+
* (`./commands/hooks.js`) is lazy-imported inside the handler to keep cold-start
|
|
5
|
+
* minimal, because hooks are on the commit/push hot path.
|
|
6
|
+
*
|
|
7
|
+
* Only subcommand: `hooks run <pre-commit|pre-push>` → dispatches to runHook and
|
|
8
|
+
* exits with its code. Any other subcommand or a missing name → usage + exit 1.
|
|
9
|
+
*/
|
|
10
|
+
import type { CLI } from "./types.js";
|
|
11
|
+
export declare function handleHooks(cli: CLI): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `javi-forge hooks` handler — console-only (no Ink; a git hook runs in a
|
|
3
|
+
* terminal git owns). Mirrors the `ci init` branch: the command module
|
|
4
|
+
* (`./commands/hooks.js`) is lazy-imported inside the handler to keep cold-start
|
|
5
|
+
* minimal, because hooks are on the commit/push hot path.
|
|
6
|
+
*
|
|
7
|
+
* Only subcommand: `hooks run <pre-commit|pre-push>` → dispatches to runHook and
|
|
8
|
+
* exits with its code. Any other subcommand or a missing name → usage + exit 1.
|
|
9
|
+
*/
|
|
10
|
+
import { HOOKS_HELP_TEXT } from "../help.js";
|
|
11
|
+
export async function handleHooks(cli) {
|
|
12
|
+
if (cli.flags.help === true) {
|
|
13
|
+
console.log(HOOKS_HELP_TEXT);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
if (cli.input[1] === "run") {
|
|
17
|
+
const name = cli.input[2];
|
|
18
|
+
if (name === undefined) {
|
|
19
|
+
console.error("Usage: javi-forge hooks run <pre-commit|pre-push>");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const { runHook } = await import("../../commands/hooks.js");
|
|
23
|
+
const code = await runHook(name, process.cwd());
|
|
24
|
+
process.exit(code);
|
|
25
|
+
}
|
|
26
|
+
// No subcommand → show usage (exit 0). An unknown subcommand is a typo →
|
|
27
|
+
// show usage but exit 1 rather than run nothing silently.
|
|
28
|
+
console.log(HOOKS_HELP_TEXT);
|
|
29
|
+
process.exit(cli.input[1] === undefined ? 0 : 1);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=hooks.js.map
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
* Help banner shown by meow when `--help` is passed or invalid args are supplied.
|
|
9
9
|
* Multi-line template literal — preserve exact formatting (whitespace is significant).
|
|
10
10
|
*/
|
|
11
|
-
export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Install TDD-enforcing pre-commit hook (auto-detects stack)\n tdd pipeline Install TDD pipeline pre-push hook (--mode strict|warn)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
|
|
11
|
+
export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Install TDD-enforcing pre-commit hook (auto-detects stack)\n tdd pipeline Install TDD pipeline pre-push hook (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
|
|
12
12
|
/**
|
|
13
13
|
* Per-command help for `ci`, shown by `javi-forge ci --help` (or when `ci` is
|
|
14
14
|
* given an unknown subcommand). Kept consistent with the global HELP_TEXT
|
|
15
15
|
* layout — whitespace is significant.
|
|
16
16
|
*/
|
|
17
17
|
export declare const CI_HELP_TEXT = "\n Usage\n $ javi-forge ci [subcommand] [options]\n\n Run a local CI simulation (lint + compile + test + security + ghagga).\n With no subcommand, the full pipeline runs.\n\n Subcommands\n init Install git hooks that call javi-forge ci\n validate Validate .javi-forge/ci.yaml without running anything\n\n Options\n --quick Lint + compile only (fast, for pre-commit)\n --no-docker Run commands natively (no Docker)\n --no-security Skip Semgrep security scan\n --no-ci-ghagga Skip GHAGGA review\n --force (ci init) Overwrite a foreign or modified hook (backs up first)\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only)\n --json (ci validate) Emit the result as JSON\n --help Show this help\n\n Examples\n $ javi-forge ci\n $ javi-forge ci --quick\n $ javi-forge ci validate\n $ javi-forge ci validate --json\n $ javi-forge ci init --force\n";
|
|
18
|
+
/**
|
|
19
|
+
* Per-command help for `hooks`, shown by `javi-forge hooks --help` (or when
|
|
20
|
+
* `hooks` is given an unknown subcommand). Whitespace is significant.
|
|
21
|
+
*/
|
|
22
|
+
export declare const HOOKS_HELP_TEXT = "\n Usage\n $ javi-forge hooks run <pre-commit|pre-push>\n\n Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed\n cheap\u2192expensive order, fail-fast. With no hooks: config the default is the\n quick native CI gate (setup + lint + compile + gates \u2014 no tests, no coverage).\n\n Subcommands\n run pre-commit Run the composed pre-commit sections\n run pre-push Run the composed pre-push sections\n\n Notes\n A blocking section failure exits non-zero and blocks the commit/push.\n A broken .javi-forge/ci.yaml exits 1 (fail-closed \u2014 never skips a gate).\n To skip: git commit --no-verify (pre-push: git push --no-verify)\n\n Examples\n $ javi-forge hooks run pre-commit\n $ javi-forge hooks run pre-push\n";
|
|
18
23
|
export declare const FLAGS_SCHEMA: {
|
|
19
24
|
readonly help: {
|
|
20
25
|
readonly type: "boolean";
|
package/dist/cli/help.js
CHANGED
|
@@ -19,6 +19,7 @@ export const HELP_TEXT = `
|
|
|
19
19
|
ci init Install git hooks that call javi-forge ci
|
|
20
20
|
tdd init Install TDD-enforcing pre-commit hook (auto-detects stack)
|
|
21
21
|
tdd pipeline Install TDD pipeline pre-push hook (--mode strict|warn)
|
|
22
|
+
hooks run Run a git hook's composed sections (pre-commit | pre-push)
|
|
22
23
|
analyze Run repoforge skills analysis
|
|
23
24
|
doctor Show health report
|
|
24
25
|
workflow show Render a workflow graph as ASCII (--template <name> or file path)
|
|
@@ -143,6 +144,31 @@ export const CI_HELP_TEXT = `
|
|
|
143
144
|
$ javi-forge ci validate --json
|
|
144
145
|
$ javi-forge ci init --force
|
|
145
146
|
`;
|
|
147
|
+
/**
|
|
148
|
+
* Per-command help for `hooks`, shown by `javi-forge hooks --help` (or when
|
|
149
|
+
* `hooks` is given an unknown subcommand). Whitespace is significant.
|
|
150
|
+
*/
|
|
151
|
+
export const HOOKS_HELP_TEXT = `
|
|
152
|
+
Usage
|
|
153
|
+
$ javi-forge hooks run <pre-commit|pre-push>
|
|
154
|
+
|
|
155
|
+
Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed
|
|
156
|
+
cheap→expensive order, fail-fast. With no hooks: config the default is the
|
|
157
|
+
quick native CI gate (setup + lint + compile + gates — no tests, no coverage).
|
|
158
|
+
|
|
159
|
+
Subcommands
|
|
160
|
+
run pre-commit Run the composed pre-commit sections
|
|
161
|
+
run pre-push Run the composed pre-push sections
|
|
162
|
+
|
|
163
|
+
Notes
|
|
164
|
+
A blocking section failure exits non-zero and blocks the commit/push.
|
|
165
|
+
A broken .javi-forge/ci.yaml exits 1 (fail-closed — never skips a gate).
|
|
166
|
+
To skip: git commit --no-verify (pre-push: git push --no-verify)
|
|
167
|
+
|
|
168
|
+
Examples
|
|
169
|
+
$ javi-forge hooks run pre-commit
|
|
170
|
+
$ javi-forge hooks run pre-push
|
|
171
|
+
`;
|
|
146
172
|
export const FLAGS_SCHEMA = {
|
|
147
173
|
// `--help` is handled manually (autoHelp is disabled at the entrypoint so
|
|
148
174
|
// `ci --help` can show ci-specific usage instead of the global banner).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `javi-forge hooks run <name>` dispatcher (hook-consolidation S1a).
|
|
3
|
+
*
|
|
4
|
+
* Pure logic, no Ink: composes the ENABLED sections for a hook from the
|
|
5
|
+
* `hooks:` section of `.javi-forge/ci.yaml`, in a fixed cheap→expensive order,
|
|
6
|
+
* and runs them fail-fast. A blocking section failure exits non-zero; only an
|
|
7
|
+
* advisory section (pre-push `tdd: "warn"`) prints and continues.
|
|
8
|
+
*
|
|
9
|
+
* Fail-closed: a missing config means the default `[ci]` composition (today's
|
|
10
|
+
* behavior); a config that FAILS to validate exits 1 — a broken config never
|
|
11
|
+
* silently skips a gate.
|
|
12
|
+
*
|
|
13
|
+
* S1a scope: only the `ci` section has a body. The tdd/secrets/permissions/deps
|
|
14
|
+
* sections have no factory yet (they land in S3/S4); an enabled-but-unregistered
|
|
15
|
+
* feature is skipped by `composeSections` until its slice wires a factory.
|
|
16
|
+
*/
|
|
17
|
+
import { type CIHooksConfig } from "../lib/ci-config.js";
|
|
18
|
+
import { runCI } from "./ci.js";
|
|
19
|
+
/** A single composable unit of hook work. */
|
|
20
|
+
export interface HookSection {
|
|
21
|
+
/** stable id — "secrets" | "permissions" | "tdd" | "deps" | "ci" */
|
|
22
|
+
id: string;
|
|
23
|
+
/** false ONLY for an advisory section (pre-push tdd:"warn") */
|
|
24
|
+
blocking: boolean;
|
|
25
|
+
run(ctx: {
|
|
26
|
+
projectDir: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
ok: boolean;
|
|
29
|
+
detail?: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export type SectionId = "secrets" | "permissions" | "tdd" | "deps" | "ci";
|
|
33
|
+
export type SectionFactory = () => HookSection;
|
|
34
|
+
export type SectionRegistry = Partial<Record<SectionId, SectionFactory>>;
|
|
35
|
+
export type HookName = "pre-commit" | "pre-push";
|
|
36
|
+
/**
|
|
37
|
+
* Compose the enabled, implemented sections for a hook in fixed order. The
|
|
38
|
+
* section's blocking flag is derived from config (so pre-push tdd:"warn" is
|
|
39
|
+
* advisory) regardless of the factory's own default. An enabled feature with no
|
|
40
|
+
* registered factory is skipped — the S1a gate for tdd/security bodies that land
|
|
41
|
+
* in later slices.
|
|
42
|
+
*/
|
|
43
|
+
export declare function composeSections(name: HookName, config: CIHooksConfig | null, registry: SectionRegistry): HookSection[];
|
|
44
|
+
/** Resolve the parsed `hooks:` config for a project (null → default [ci]). */
|
|
45
|
+
export declare function loadHooksConfig(projectDir: string): Promise<CIHooksConfig | null>;
|
|
46
|
+
/** Injectable seams for tests; every field defaults to the real implementation. */
|
|
47
|
+
export interface RunHookDeps {
|
|
48
|
+
loadConfig?: (projectDir: string) => Promise<CIHooksConfig | null>;
|
|
49
|
+
registry?: SectionRegistry;
|
|
50
|
+
runCIImpl?: typeof runCI;
|
|
51
|
+
log?: (msg: string) => void;
|
|
52
|
+
logError?: (msg: string) => void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run the composed sections for `name`. Returns the process exit code:
|
|
56
|
+
* 0 iff every blocking section passed. Fail-fast on the first blocking failure;
|
|
57
|
+
* fail-closed (exit 1) on an unknown hook name or an unparseable config.
|
|
58
|
+
*/
|
|
59
|
+
export declare function runHook(name: string, projectDir: string, deps?: RunHookDeps): Promise<number>;
|
|
60
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `javi-forge hooks run <name>` dispatcher (hook-consolidation S1a).
|
|
3
|
+
*
|
|
4
|
+
* Pure logic, no Ink: composes the ENABLED sections for a hook from the
|
|
5
|
+
* `hooks:` section of `.javi-forge/ci.yaml`, in a fixed cheap→expensive order,
|
|
6
|
+
* and runs them fail-fast. A blocking section failure exits non-zero; only an
|
|
7
|
+
* advisory section (pre-push `tdd: "warn"`) prints and continues.
|
|
8
|
+
*
|
|
9
|
+
* Fail-closed: a missing config means the default `[ci]` composition (today's
|
|
10
|
+
* behavior); a config that FAILS to validate exits 1 — a broken config never
|
|
11
|
+
* silently skips a gate.
|
|
12
|
+
*
|
|
13
|
+
* S1a scope: only the `ci` section has a body. The tdd/secrets/permissions/deps
|
|
14
|
+
* sections have no factory yet (they land in S3/S4); an enabled-but-unregistered
|
|
15
|
+
* feature is skipped by `composeSections` until its slice wires a factory.
|
|
16
|
+
*/
|
|
17
|
+
import { findCIConfig, loadCIConfig, } from "../lib/ci-config.js";
|
|
18
|
+
import { runCI } from "./ci.js";
|
|
19
|
+
/** Fixed cheap→expensive order per hook (deterministic is a feature). */
|
|
20
|
+
const PRE_COMMIT_ORDER = ["secrets", "permissions", "tdd", "ci"];
|
|
21
|
+
const PRE_PUSH_ORDER = ["deps", "tdd", "ci"];
|
|
22
|
+
/** No config / no `hooks:` section → the [ci]-only default (byte-for-byte today). */
|
|
23
|
+
const DEFAULT_HOOKS = {
|
|
24
|
+
preCommit: { ci: true, tdd: false, secrets: false, permissions: false },
|
|
25
|
+
prePush: { ci: true, tdd: false, deps: false },
|
|
26
|
+
};
|
|
27
|
+
function featureState(id, name, config) {
|
|
28
|
+
if (name === "pre-commit") {
|
|
29
|
+
const pc = config.preCommit;
|
|
30
|
+
switch (id) {
|
|
31
|
+
case "secrets":
|
|
32
|
+
return { enabled: pc.secrets, blocking: true };
|
|
33
|
+
case "permissions":
|
|
34
|
+
return { enabled: pc.permissions, blocking: true };
|
|
35
|
+
case "tdd":
|
|
36
|
+
return { enabled: pc.tdd, blocking: true };
|
|
37
|
+
case "ci":
|
|
38
|
+
return { enabled: pc.ci, blocking: true };
|
|
39
|
+
default:
|
|
40
|
+
return { enabled: false, blocking: true };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const pp = config.prePush;
|
|
44
|
+
switch (id) {
|
|
45
|
+
case "deps":
|
|
46
|
+
return { enabled: pp.deps, blocking: true };
|
|
47
|
+
case "tdd":
|
|
48
|
+
// "warn" is advisory (never blocks); false is off; "strict"/true block.
|
|
49
|
+
return { enabled: pp.tdd !== false, blocking: pp.tdd !== "warn" };
|
|
50
|
+
case "ci":
|
|
51
|
+
return { enabled: pp.ci, blocking: true };
|
|
52
|
+
default:
|
|
53
|
+
return { enabled: false, blocking: true };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compose the enabled, implemented sections for a hook in fixed order. The
|
|
58
|
+
* section's blocking flag is derived from config (so pre-push tdd:"warn" is
|
|
59
|
+
* advisory) regardless of the factory's own default. An enabled feature with no
|
|
60
|
+
* registered factory is skipped — the S1a gate for tdd/security bodies that land
|
|
61
|
+
* in later slices.
|
|
62
|
+
*/
|
|
63
|
+
export function composeSections(name, config, registry) {
|
|
64
|
+
const cfg = config ?? DEFAULT_HOOKS;
|
|
65
|
+
const order = name === "pre-commit" ? PRE_COMMIT_ORDER : PRE_PUSH_ORDER;
|
|
66
|
+
const sections = [];
|
|
67
|
+
for (const id of order) {
|
|
68
|
+
const state = featureState(id, name, cfg);
|
|
69
|
+
if (!state.enabled)
|
|
70
|
+
continue;
|
|
71
|
+
const factory = registry[id];
|
|
72
|
+
if (!factory)
|
|
73
|
+
continue; // S1a: enabled but not yet implemented → skip
|
|
74
|
+
const section = factory();
|
|
75
|
+
sections.push({ ...section, blocking: state.blocking });
|
|
76
|
+
}
|
|
77
|
+
return sections;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The `ci` section: runs the quick native CI gate IN-PROCESS (no subprocess, no
|
|
81
|
+
* PATH/version skew). `runCI` throws on a blocking failure → the section reports
|
|
82
|
+
* ok:false. `quick` runs setup + lint + compile + gates — NO tests, NO coverage.
|
|
83
|
+
*/
|
|
84
|
+
function ciSection(runCIImpl, log) {
|
|
85
|
+
return {
|
|
86
|
+
id: "ci",
|
|
87
|
+
blocking: true,
|
|
88
|
+
async run({ projectDir }) {
|
|
89
|
+
try {
|
|
90
|
+
await runCIImpl({
|
|
91
|
+
projectDir,
|
|
92
|
+
mode: "quick",
|
|
93
|
+
noDocker: true,
|
|
94
|
+
noSecurity: true,
|
|
95
|
+
noGhagga: true,
|
|
96
|
+
}, (step) => reportStep(step, log));
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Console step feedback (no Ink in a git hook). Honest — never claims tests. */
|
|
109
|
+
function reportStep(step, log) {
|
|
110
|
+
if (step.status === "done")
|
|
111
|
+
log(` ✓ ${step.label}`);
|
|
112
|
+
else if (step.status === "error")
|
|
113
|
+
log(` ✗ ${step.label}${step.detail ? ` — ${step.detail}` : ""}`);
|
|
114
|
+
else if (step.status === "warning")
|
|
115
|
+
log(` ⚠ ${step.label}`);
|
|
116
|
+
}
|
|
117
|
+
function defaultRegistry(runCIImpl, log) {
|
|
118
|
+
return { ci: () => ciSection(runCIImpl, log) };
|
|
119
|
+
}
|
|
120
|
+
/** Resolve the parsed `hooks:` config for a project (null → default [ci]). */
|
|
121
|
+
export async function loadHooksConfig(projectDir) {
|
|
122
|
+
const configPath = await findCIConfig(projectDir);
|
|
123
|
+
if (!configPath)
|
|
124
|
+
return null;
|
|
125
|
+
// Throws CIConfigError on an invalid config → runHook maps that to exit 1.
|
|
126
|
+
const config = await loadCIConfig(configPath);
|
|
127
|
+
return config.hooks ?? null;
|
|
128
|
+
}
|
|
129
|
+
const USAGE = "Usage: javi-forge hooks run <pre-commit|pre-push>";
|
|
130
|
+
/**
|
|
131
|
+
* Run the composed sections for `name`. Returns the process exit code:
|
|
132
|
+
* 0 iff every blocking section passed. Fail-fast on the first blocking failure;
|
|
133
|
+
* fail-closed (exit 1) on an unknown hook name or an unparseable config.
|
|
134
|
+
*/
|
|
135
|
+
export async function runHook(name, projectDir, deps = {}) {
|
|
136
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
137
|
+
const logError = deps.logError ?? ((m) => console.error(m));
|
|
138
|
+
if (name !== "pre-commit" && name !== "pre-push") {
|
|
139
|
+
logError(USAGE);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
const loadConfig = deps.loadConfig ?? loadHooksConfig;
|
|
143
|
+
let config;
|
|
144
|
+
try {
|
|
145
|
+
config = await loadConfig(projectDir);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
// Fail-closed: a broken config never silently skips a gate.
|
|
149
|
+
logError(e instanceof Error ? e.message : String(e));
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
const runCIImpl = deps.runCIImpl ?? runCI;
|
|
153
|
+
const registry = deps.registry ?? defaultRegistry(runCIImpl, log);
|
|
154
|
+
const sections = composeSections(name, config, registry);
|
|
155
|
+
for (const section of sections) {
|
|
156
|
+
log(`▶ ${section.id}`);
|
|
157
|
+
const result = await section.run({ projectDir });
|
|
158
|
+
if (result.ok)
|
|
159
|
+
continue;
|
|
160
|
+
const detail = result.detail ? `: ${result.detail}` : "";
|
|
161
|
+
if (section.blocking) {
|
|
162
|
+
logError(`✗ ${section.id} failed${detail}`);
|
|
163
|
+
return 1; // fail-fast
|
|
164
|
+
}
|
|
165
|
+
log(`⚠ ${section.id} (advisory) failed${detail}`);
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=hooks.js.map
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import meow from "meow";
|
|
4
4
|
import { handleCi } from "./cli/dispatch/ci.js";
|
|
5
|
+
import { handleHooks } from "./cli/dispatch/hooks.js";
|
|
5
6
|
import { handleSecurity } from "./cli/dispatch/security.js";
|
|
6
7
|
import { handleAnalyze, handleDoctor, handleInitDefault, handleLlmsTxt, handlePlugin, } from "./cli/dispatch/simple-renderers.js";
|
|
7
8
|
import { handleSkillPublish } from "./cli/dispatch/skill-publish.js";
|
|
@@ -22,9 +23,9 @@ const cli = meow(HELP_TEXT, {
|
|
|
22
23
|
autoHelp: false,
|
|
23
24
|
});
|
|
24
25
|
const subcommand = cli.input[0] ?? "init";
|
|
25
|
-
// Global --help: every command except `ci` shows the global banner
|
|
26
|
-
//
|
|
27
|
-
if (cli.flags.help && subcommand !== "ci") {
|
|
26
|
+
// Global --help: every command except `ci` and `hooks` shows the global banner
|
|
27
|
+
// here. Those two own their per-command help inside their handlers.
|
|
28
|
+
if (cli.flags.help && subcommand !== "ci" && subcommand !== "hooks") {
|
|
28
29
|
console.log(HELP_TEXT);
|
|
29
30
|
process.exit(0);
|
|
30
31
|
}
|
|
@@ -39,6 +40,10 @@ switch (subcommand) {
|
|
|
39
40
|
await handleCi(cli, { inkStdin, isCI });
|
|
40
41
|
break;
|
|
41
42
|
}
|
|
43
|
+
case "hooks": {
|
|
44
|
+
await handleHooks(cli);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
42
47
|
case "doctor": {
|
|
43
48
|
handleDoctor(cli, { inkStdin, isCI });
|
|
44
49
|
break;
|
package/dist/lib/ci-config.d.ts
CHANGED
|
@@ -74,11 +74,41 @@ export interface CIGateConfig {
|
|
|
74
74
|
*/
|
|
75
75
|
timeout?: number;
|
|
76
76
|
}
|
|
77
|
+
/** pre-commit hook feature toggles (version 2 only). */
|
|
78
|
+
export interface CIHookCommitConfig {
|
|
79
|
+
/** quick native CI gate (default true when `hooks:` is present) */
|
|
80
|
+
ci: boolean;
|
|
81
|
+
/** run the stack test command */
|
|
82
|
+
tdd: boolean;
|
|
83
|
+
/** L1 secret scan */
|
|
84
|
+
secrets: boolean;
|
|
85
|
+
/** L3 permission boundaries */
|
|
86
|
+
permissions: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** pre-push hook feature toggles (version 2 only). */
|
|
89
|
+
export interface CIHookPushConfig {
|
|
90
|
+
/** quick native CI gate (default true when `hooks:` is present) */
|
|
91
|
+
ci: boolean;
|
|
92
|
+
/** run the stack test command — false | "strict" | "warn" ("warn" = advisory) */
|
|
93
|
+
tdd: boolean | "strict" | "warn";
|
|
94
|
+
/** L2 dependency audit */
|
|
95
|
+
deps: boolean;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parsed `hooks:` section. Every hook is always present with defaults applied
|
|
99
|
+
* (ci on, every other feature off) so the dispatcher never branches on absence.
|
|
100
|
+
*/
|
|
101
|
+
export interface CIHooksConfig {
|
|
102
|
+
preCommit: CIHookCommitConfig;
|
|
103
|
+
prePush: CIHookPushConfig;
|
|
104
|
+
}
|
|
77
105
|
export interface CIConfig {
|
|
78
106
|
version: number;
|
|
79
107
|
runners: CIRunnerConfig[];
|
|
80
108
|
/** Present only under version 2 when `gates:` is declared. */
|
|
81
109
|
gates?: CIGateConfig[];
|
|
110
|
+
/** Present only under version 2 when `hooks:` is declared. */
|
|
111
|
+
hooks?: CIHooksConfig;
|
|
82
112
|
}
|
|
83
113
|
export interface CIConfigValidationError {
|
|
84
114
|
path: string;
|
|
@@ -96,6 +126,14 @@ export declare const CI_STACKS: readonly string[];
|
|
|
96
126
|
* (the caller discards them all if `errors` is non-empty — fail closed).
|
|
97
127
|
*/
|
|
98
128
|
export declare function validateGates(raw: unknown, errors: CIConfigValidationError[]): CIGateConfig[];
|
|
129
|
+
/**
|
|
130
|
+
* Validate the `hooks:` block (version 2 only). Mirrors validateGate: every
|
|
131
|
+
* schema error names the offending field and the caller discards the whole
|
|
132
|
+
* config if `errors` is non-empty (fail closed). Always returns a fully
|
|
133
|
+
* defaulted config (ci on, every other feature off) so the dispatcher never
|
|
134
|
+
* branches on an omitted hook or feature.
|
|
135
|
+
*/
|
|
136
|
+
export declare function validateHooks(raw: unknown, errors: CIConfigValidationError[]): CIHooksConfig;
|
|
99
137
|
/**
|
|
100
138
|
* Parse and validate CI config YAML text. Throws CIConfigError listing every
|
|
101
139
|
* validation problem; never returns a partially valid config.
|
package/dist/lib/ci-config.js
CHANGED
|
@@ -396,6 +396,113 @@ export function validateGates(raw, errors) {
|
|
|
396
396
|
});
|
|
397
397
|
return gates;
|
|
398
398
|
}
|
|
399
|
+
const HOOK_NAMES = new Set(["pre-commit", "pre-push"]);
|
|
400
|
+
const PRE_COMMIT_FEATURES = new Set(["ci", "tdd", "secrets", "permissions"]);
|
|
401
|
+
const PRE_PUSH_FEATURES = new Set(["ci", "tdd", "deps"]);
|
|
402
|
+
/** Validate one boolean feature toggle, returning `dflt` when absent. */
|
|
403
|
+
function validateBoolFeature(raw, key, base, errors, dflt) {
|
|
404
|
+
const value = raw[key];
|
|
405
|
+
if (value === undefined)
|
|
406
|
+
return dflt;
|
|
407
|
+
if (typeof value !== "boolean") {
|
|
408
|
+
errors.push({
|
|
409
|
+
path: `${base}.${key}`,
|
|
410
|
+
message: `${key} must be a boolean`,
|
|
411
|
+
});
|
|
412
|
+
return dflt;
|
|
413
|
+
}
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
/** Validate the tri-state pre-push.tdd toggle (false | "strict" | "warn"). */
|
|
417
|
+
function validatePushTdd(value, base, errors) {
|
|
418
|
+
if (value === undefined)
|
|
419
|
+
return false;
|
|
420
|
+
if (typeof value === "boolean")
|
|
421
|
+
return value;
|
|
422
|
+
if (value === "strict" || value === "warn")
|
|
423
|
+
return value;
|
|
424
|
+
errors.push({
|
|
425
|
+
path: `${base}.tdd`,
|
|
426
|
+
message: `tdd must be a boolean or one of: strict, warn (got "${String(value)}")`,
|
|
427
|
+
});
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Validate the `hooks:` block (version 2 only). Mirrors validateGate: every
|
|
432
|
+
* schema error names the offending field and the caller discards the whole
|
|
433
|
+
* config if `errors` is non-empty (fail closed). Always returns a fully
|
|
434
|
+
* defaulted config (ci on, every other feature off) so the dispatcher never
|
|
435
|
+
* branches on an omitted hook or feature.
|
|
436
|
+
*/
|
|
437
|
+
export function validateHooks(raw, errors) {
|
|
438
|
+
const preCommit = {
|
|
439
|
+
ci: true,
|
|
440
|
+
tdd: false,
|
|
441
|
+
secrets: false,
|
|
442
|
+
permissions: false,
|
|
443
|
+
};
|
|
444
|
+
const prePush = { ci: true, tdd: false, deps: false };
|
|
445
|
+
if (!isRecord(raw)) {
|
|
446
|
+
errors.push({
|
|
447
|
+
path: "hooks",
|
|
448
|
+
message: "hooks must be a mapping of hook names to feature toggles",
|
|
449
|
+
});
|
|
450
|
+
return { preCommit, prePush };
|
|
451
|
+
}
|
|
452
|
+
for (const key of Object.keys(raw)) {
|
|
453
|
+
if (!HOOK_NAMES.has(key)) {
|
|
454
|
+
errors.push({ path: `hooks.${key}`, message: `unknown field "${key}"` });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (raw["pre-commit"] !== undefined) {
|
|
458
|
+
const pc = raw["pre-commit"];
|
|
459
|
+
const base = "hooks.pre-commit";
|
|
460
|
+
if (!isRecord(pc)) {
|
|
461
|
+
errors.push({
|
|
462
|
+
path: base,
|
|
463
|
+
message: "pre-commit must be a mapping of feature toggles",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
for (const key of Object.keys(pc)) {
|
|
468
|
+
if (!PRE_COMMIT_FEATURES.has(key)) {
|
|
469
|
+
errors.push({
|
|
470
|
+
path: `${base}.${key}`,
|
|
471
|
+
message: `unknown field "${key}"`,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
preCommit.ci = validateBoolFeature(pc, "ci", base, errors, true);
|
|
476
|
+
preCommit.tdd = validateBoolFeature(pc, "tdd", base, errors, false);
|
|
477
|
+
preCommit.secrets = validateBoolFeature(pc, "secrets", base, errors, false);
|
|
478
|
+
preCommit.permissions = validateBoolFeature(pc, "permissions", base, errors, false);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (raw["pre-push"] !== undefined) {
|
|
482
|
+
const pp = raw["pre-push"];
|
|
483
|
+
const base = "hooks.pre-push";
|
|
484
|
+
if (!isRecord(pp)) {
|
|
485
|
+
errors.push({
|
|
486
|
+
path: base,
|
|
487
|
+
message: "pre-push must be a mapping of feature toggles",
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
for (const key of Object.keys(pp)) {
|
|
492
|
+
if (!PRE_PUSH_FEATURES.has(key)) {
|
|
493
|
+
errors.push({
|
|
494
|
+
path: `${base}.${key}`,
|
|
495
|
+
message: `unknown field "${key}"`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
prePush.ci = validateBoolFeature(pp, "ci", base, errors, true);
|
|
500
|
+
prePush.tdd = validatePushTdd(pp.tdd, base, errors);
|
|
501
|
+
prePush.deps = validateBoolFeature(pp, "deps", base, errors, false);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return { preCommit, prePush };
|
|
505
|
+
}
|
|
399
506
|
// =============================================================================
|
|
400
507
|
// Public API
|
|
401
508
|
// =============================================================================
|
|
@@ -434,6 +541,7 @@ export function parseCIConfig(rawYaml, source) {
|
|
|
434
541
|
}
|
|
435
542
|
const isV2 = version === 2;
|
|
436
543
|
const hasGates = doc.gates !== undefined;
|
|
544
|
+
const hasHooks = doc.hooks !== undefined;
|
|
437
545
|
for (const key of Object.keys(doc)) {
|
|
438
546
|
if (TOP_LEVEL_FIELDS.has(key))
|
|
439
547
|
continue;
|
|
@@ -445,16 +553,25 @@ export function parseCIConfig(rawYaml, source) {
|
|
|
445
553
|
}
|
|
446
554
|
continue;
|
|
447
555
|
}
|
|
556
|
+
if (key === "hooks") {
|
|
557
|
+
// Same version-gating as `gates`: `hooks` is a v2-only key, so under any
|
|
558
|
+
// other version it reports a named error, not the generic unknown-field.
|
|
559
|
+
if (!isV2) {
|
|
560
|
+
errors.push({ path: "hooks", message: "hooks require version: 2" });
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
448
564
|
errors.push({ path: key, message: `unknown field "${key}"` });
|
|
449
565
|
}
|
|
450
566
|
if (isV2) {
|
|
451
|
-
// v2: runners OPTIONAL when gates present;
|
|
452
|
-
// closed (nothing to run).
|
|
567
|
+
// v2: runners OPTIONAL when gates OR hooks are present; declaring NONE of the
|
|
568
|
+
// three fails closed (nothing to run).
|
|
453
569
|
if (!hasGates &&
|
|
570
|
+
!hasHooks &&
|
|
454
571
|
(!Array.isArray(doc.runners) || doc.runners.length === 0)) {
|
|
455
572
|
errors.push({
|
|
456
573
|
path: "runners",
|
|
457
|
-
message: "a version 2 config must declare runners or
|
|
574
|
+
message: "a version 2 config must declare runners, gates or hooks (nothing to run otherwise)",
|
|
458
575
|
});
|
|
459
576
|
}
|
|
460
577
|
}
|
|
@@ -468,6 +585,9 @@ export function parseCIConfig(rawYaml, source) {
|
|
|
468
585
|
// Gates are validated ONLY under v2 — a v1+gates config is already rejected
|
|
469
586
|
// above and must not surface a second, confusing wave of gate-field errors.
|
|
470
587
|
const gates = isV2 && hasGates ? validateGates(doc.gates, errors) : [];
|
|
588
|
+
// Hooks are validated ONLY under v2 — a v1+hooks config is already rejected
|
|
589
|
+
// above and must not surface a second wave of hook-field errors.
|
|
590
|
+
const hooks = isV2 && hasHooks ? validateHooks(doc.hooks, errors) : undefined;
|
|
471
591
|
const runners = [];
|
|
472
592
|
if (Array.isArray(doc.runners)) {
|
|
473
593
|
doc.runners.forEach((raw, index) => {
|
|
@@ -496,6 +616,8 @@ export function parseCIConfig(rawYaml, source) {
|
|
|
496
616
|
const config = { version: version ?? CI_CONFIG_VERSION, runners };
|
|
497
617
|
if (gates.length > 0)
|
|
498
618
|
config.gates = gates;
|
|
619
|
+
if (hooks)
|
|
620
|
+
config.hooks = hooks;
|
|
499
621
|
return config;
|
|
500
622
|
}
|
|
501
623
|
/**
|
package/dist/lib/docker.js
CHANGED
|
@@ -38,6 +38,14 @@ export function getDockerfileContent(stack) {
|
|
|
38
38
|
"RUN apt-get update && apt-get install -y git curl unzip && rm -rf /var/lib/apt/lists/*",
|
|
39
39
|
"RUN useradd -m -s /bin/bash runner",
|
|
40
40
|
"USER runner",
|
|
41
|
+
// No GRADLE_USER_HOME override (audit R4-001): on noble the
|
|
42
|
+
// `ubuntu` user owns uid 1000, so `useradd -m runner` lands at
|
|
43
|
+
// uid 1001 and /home/runner is 0750 runner:runner. Pinning
|
|
44
|
+
// GRADLE_USER_HOME=/home/runner/.gradle hard-fails the TS
|
|
45
|
+
// runtime path, which runs as the HOST uid (ENV-1, typically
|
|
46
|
+
// 1000) and cannot create that dir. Without the env both paths
|
|
47
|
+
// work off $HOME: shell path (--user runner) → /home/runner;
|
|
48
|
+
// host-uid path → getpwuid(1000) → /home/ubuntu.
|
|
41
49
|
"WORKDIR /home/runner/work",
|
|
42
50
|
'ENTRYPOINT ["/bin/bash", "-c"]',
|
|
43
51
|
].join("\n");
|
|
@@ -143,10 +151,22 @@ export async function ensureImage(options) {
|
|
|
143
151
|
path.join(path.dirname(fileURLToPath(import.meta.url)), "../../ci-local/docker");
|
|
144
152
|
dockerfilePath = path.join(dockerDir, `${stack}.Dockerfile`);
|
|
145
153
|
contextDir = dockerDir;
|
|
146
|
-
// Write Dockerfile if it doesn't exist yet (first run)
|
|
147
154
|
if (!(await fs.pathExists(dockerfilePath))) {
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
if (dockerfilesDir) {
|
|
156
|
+
// Caller-provided generation dir (dev / project-local flow):
|
|
157
|
+
// first-run generation is expected — write the canonical content.
|
|
158
|
+
await fs.ensureDir(dockerDir);
|
|
159
|
+
await fs.writeFile(dockerfilePath, getDockerfileContent(stack), "utf-8");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// PKG-002: the package bundles a Dockerfile for EVERY stack, so a
|
|
163
|
+
// miss here means a corrupted/incomplete install. Writing into the
|
|
164
|
+
// install dir would EACCES on root-owned global installs and mask
|
|
165
|
+
// the real problem — fail closed with a clear message instead.
|
|
166
|
+
throw new Error(`bundled Dockerfile "${stack}.Dockerfile" is missing from ` +
|
|
167
|
+
`"${dockerDir}" — the javi-forge installation looks corrupted; ` +
|
|
168
|
+
"reinstall the package (or pass an explicit dockerfilesDir)");
|
|
169
|
+
}
|
|
150
170
|
}
|
|
151
171
|
}
|
|
152
172
|
// Staleness check: compare Dockerfile hash with the one embedded in the image label
|