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.
@@ -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 # Se generan automáticamente
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/` después de la primera ejecución.
196
+ Editar los Dockerfiles en `.ci-local/docker/` (incluidos por stack).
197
197
 
198
198
  ## FAQ
199
199
 
@@ -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
@@ -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
@@ -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 here. `ci`
26
- // owns its per-command help inside handleCi.
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;
@@ -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.
@@ -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; NEITHER runners nor gates fails
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 gates (nothing to run otherwise)",
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
  /**
@@ -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
- await fs.ensureDir(dockerDir);
149
- await fs.writeFile(dockerfilePath, getDockerfileContent(stack), "utf-8");
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.22.3",
3
+ "version": "1.23.1",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {