javi-forge 1.22.2 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/modules/engram/install-engram.sh +12 -1
- package/package.json +1 -1
- package/templates/github/ci-go.yml +1 -1
- package/templates/github/ci-java.yml +1 -1
- package/templates/github/ci-node.yml +1 -1
- package/templates/github/ci-python.yml +1 -1
- package/templates/github/ci-rust.yml +1 -1
- package/templates/github/deploy-docker-zero-downtime.yml +16 -8
- package/templates/github/ghagga-review.yml +3 -1
- package/templates/local-ai/.env.example +9 -2
- package/templates/local-ai/docker-compose.yml +14 -6
|
@@ -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
|
/**
|
|
@@ -153,20 +153,31 @@ fi
|
|
|
153
153
|
CHECKSUM_URL="https://github.com/${REPO}/releases/download/${VERSION}/checksums.txt"
|
|
154
154
|
if curl -fsSL -o "${TMP_DIR}/checksums.txt" "$CHECKSUM_URL" 2>/dev/null; then
|
|
155
155
|
echo -e "${CYAN}Verifying checksum...${NC}"
|
|
156
|
+
CHECKSUM_RAN=false
|
|
156
157
|
pushd "$TMP_DIR" > /dev/null
|
|
157
158
|
if command -v sha256sum &>/dev/null; then
|
|
158
159
|
sha256sum -c checksums.txt --ignore-missing || {
|
|
159
160
|
echo -e "${RED}Checksum verification failed!${NC}"
|
|
160
161
|
exit 1
|
|
161
162
|
}
|
|
163
|
+
CHECKSUM_RAN=true
|
|
162
164
|
elif command -v shasum &>/dev/null; then
|
|
163
165
|
shasum -a 256 -c checksums.txt --ignore-missing || {
|
|
164
166
|
echo -e "${RED}Checksum verification failed!${NC}"
|
|
165
167
|
exit 1
|
|
166
168
|
}
|
|
169
|
+
CHECKSUM_RAN=true
|
|
167
170
|
fi
|
|
168
171
|
popd > /dev/null
|
|
169
|
-
|
|
172
|
+
if [[ "$CHECKSUM_RAN" == "true" ]]; then
|
|
173
|
+
echo -e "${GREEN}Checksum verified${NC}"
|
|
174
|
+
elif [[ "$NO_VERIFY" == "true" ]]; then
|
|
175
|
+
echo -e "${YELLOW}WARNING: neither sha256sum nor shasum found; skipping checksum verification (--no-verify)${NC}"
|
|
176
|
+
else
|
|
177
|
+
echo -e "${RED}Error: neither sha256sum nor shasum is available. Cannot verify download integrity.${NC}"
|
|
178
|
+
echo -e "${YELLOW}Install coreutils (sha256sum) or perl (shasum), or use --no-verify to skip verification (NOT RECOMMENDED).${NC}"
|
|
179
|
+
exit 1
|
|
180
|
+
fi
|
|
170
181
|
else
|
|
171
182
|
if [[ "$NO_VERIFY" == "true" ]]; then
|
|
172
183
|
echo -e "${YELLOW}WARNING: Skipping checksum verification (--no-verify)${NC}"
|
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@ concurrency:
|
|
|
34
34
|
|
|
35
35
|
jobs:
|
|
36
36
|
build:
|
|
37
|
-
uses: JNZader/javi-forge/.github/workflows/reusable-build-go.yml@main
|
|
37
|
+
uses: JNZader/javi-forge/.github/workflows/reusable-build-go.yml@667908ddb5e1aeb8e44ec65e84e4bc0e4f936d6c # main@2026-08-10
|
|
38
38
|
with:
|
|
39
39
|
go-version: '1.23' # Change to: 1.22, 1.23
|
|
40
40
|
run-lint: true
|
|
@@ -37,7 +37,7 @@ concurrency:
|
|
|
37
37
|
|
|
38
38
|
jobs:
|
|
39
39
|
build:
|
|
40
|
-
uses: JNZader/javi-forge/.github/workflows/reusable-build-java.yml@main
|
|
40
|
+
uses: JNZader/javi-forge/.github/workflows/reusable-build-java.yml@667908ddb5e1aeb8e44ec65e84e4bc0e4f936d6c # main@2026-08-10
|
|
41
41
|
with:
|
|
42
42
|
java-version: '21' # Change to your version: 17, 21, 25
|
|
43
43
|
run-spotless: true
|
|
@@ -34,7 +34,7 @@ concurrency:
|
|
|
34
34
|
|
|
35
35
|
jobs:
|
|
36
36
|
build:
|
|
37
|
-
uses: JNZader/javi-forge/.github/workflows/reusable-build-node.yml@main
|
|
37
|
+
uses: JNZader/javi-forge/.github/workflows/reusable-build-node.yml@667908ddb5e1aeb8e44ec65e84e4bc0e4f936d6c # main@2026-08-10
|
|
38
38
|
with:
|
|
39
39
|
node-version: '20' # Change to: 18, 20, 22
|
|
40
40
|
package-manager: 'npm' # Change to: npm, yarn, pnpm
|
|
@@ -34,7 +34,7 @@ concurrency:
|
|
|
34
34
|
|
|
35
35
|
jobs:
|
|
36
36
|
build:
|
|
37
|
-
uses: JNZader/javi-forge/.github/workflows/reusable-build-python.yml@main
|
|
37
|
+
uses: JNZader/javi-forge/.github/workflows/reusable-build-python.yml@667908ddb5e1aeb8e44ec65e84e4bc0e4f936d6c # main@2026-08-10
|
|
38
38
|
with:
|
|
39
39
|
python-version: '3.12' # Change to: 3.10, 3.11, 3.12
|
|
40
40
|
package-manager: 'pip' # Change to: pip, poetry, uv
|
|
@@ -34,7 +34,7 @@ concurrency:
|
|
|
34
34
|
|
|
35
35
|
jobs:
|
|
36
36
|
build:
|
|
37
|
-
uses: JNZader/javi-forge/.github/workflows/reusable-build-rust.yml@main
|
|
37
|
+
uses: JNZader/javi-forge/.github/workflows/reusable-build-rust.yml@667908ddb5e1aeb8e44ec65e84e4bc0e4f936d6c # main@2026-08-10
|
|
38
38
|
with:
|
|
39
39
|
toolchain: 'stable' # Change to: stable, beta, nightly
|
|
40
40
|
run-clippy: true
|
|
@@ -55,10 +55,10 @@ jobs:
|
|
|
55
55
|
outputs:
|
|
56
56
|
image_tag: ${{ steps.meta.outputs.tags }}
|
|
57
57
|
steps:
|
|
58
|
-
- uses: actions/checkout@v4
|
|
58
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
59
59
|
|
|
60
60
|
- name: Log in to Container Registry
|
|
61
|
-
uses: docker/login-action@v3
|
|
61
|
+
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
|
62
62
|
with:
|
|
63
63
|
registry: ${{ env.REGISTRY }}
|
|
64
64
|
username: ${{ github.actor }}
|
|
@@ -66,7 +66,7 @@ jobs:
|
|
|
66
66
|
|
|
67
67
|
- name: Extract metadata
|
|
68
68
|
id: meta
|
|
69
|
-
uses: docker/metadata-action@v5
|
|
69
|
+
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
|
70
70
|
with:
|
|
71
71
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
72
72
|
tags: |
|
|
@@ -74,7 +74,7 @@ jobs:
|
|
|
74
74
|
type=raw,value=latest
|
|
75
75
|
|
|
76
76
|
- name: Build and push
|
|
77
|
-
uses: docker/build-push-action@v5
|
|
77
|
+
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0
|
|
78
78
|
with:
|
|
79
79
|
context: .
|
|
80
80
|
push: true
|
|
@@ -88,11 +88,16 @@ jobs:
|
|
|
88
88
|
environment: production
|
|
89
89
|
steps:
|
|
90
90
|
- name: Deploy via SSH + docker rollout
|
|
91
|
-
uses: appleboy/ssh-action@v1
|
|
91
|
+
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
|
92
|
+
env:
|
|
93
|
+
# Expression-injection hardening: the workflow_dispatch input is passed
|
|
94
|
+
# as an env var (never interpolated into the shell script text).
|
|
95
|
+
SERVICE: ${{ github.event.inputs.service || '__SERVICE_NAME__' }}
|
|
92
96
|
with:
|
|
93
97
|
host: ${{ env.DEPLOY_HOST }}
|
|
94
98
|
username: ${{ env.DEPLOY_USER }}
|
|
95
99
|
key: ${{ secrets.DEPLOY_KEY }}
|
|
100
|
+
envs: SERVICE
|
|
96
101
|
script: |
|
|
97
102
|
set -euo pipefail
|
|
98
103
|
|
|
@@ -102,7 +107,6 @@ jobs:
|
|
|
102
107
|
docker compose pull
|
|
103
108
|
|
|
104
109
|
# Zero-downtime rollout for the target service
|
|
105
|
-
SERVICE="${{ github.event.inputs.service || '__SERVICE_NAME__' }}"
|
|
106
110
|
echo "::group::Rolling out ${SERVICE}"
|
|
107
111
|
docker rollout "${SERVICE}"
|
|
108
112
|
echo "::endgroup::"
|
|
@@ -111,15 +115,19 @@ jobs:
|
|
|
111
115
|
docker image prune -f
|
|
112
116
|
|
|
113
117
|
- name: Verify deployment
|
|
114
|
-
uses: appleboy/ssh-action@v1
|
|
118
|
+
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
|
119
|
+
env:
|
|
120
|
+
# Expression-injection hardening: the workflow_dispatch input is passed
|
|
121
|
+
# as an env var (never interpolated into the shell script text).
|
|
122
|
+
SERVICE: ${{ github.event.inputs.service || '__SERVICE_NAME__' }}
|
|
115
123
|
with:
|
|
116
124
|
host: ${{ env.DEPLOY_HOST }}
|
|
117
125
|
username: ${{ env.DEPLOY_USER }}
|
|
118
126
|
key: ${{ secrets.DEPLOY_KEY }}
|
|
127
|
+
envs: SERVICE
|
|
119
128
|
script: |
|
|
120
129
|
set -euo pipefail
|
|
121
130
|
cd ${{ secrets.DEPLOY_DIR || '~/app' }}
|
|
122
|
-
SERVICE="${{ github.event.inputs.service || '__SERVICE_NAME__' }}"
|
|
123
131
|
|
|
124
132
|
# Wait for healthcheck (max 60s)
|
|
125
133
|
TIMEOUT=60
|
|
@@ -21,7 +21,9 @@ jobs:
|
|
|
21
21
|
review:
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@v4
|
|
24
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
25
|
+
# TODO(pin): ghagga has no v1 tag upstream (releases start at v2) — this
|
|
26
|
+
# ref cannot be SHA-pinned as-is; pick a real released tag and pin it.
|
|
25
27
|
- uses: JNZader/ghagga@v1
|
|
26
28
|
with:
|
|
27
29
|
mode: simple
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
# Local AI Stack — Environment Variables
|
|
2
2
|
# Copy to .env in your project: cp .env.example .env
|
|
3
|
+
#
|
|
4
|
+
# Secure defaults: all ports bind to 127.0.0.1 (loopback), WebUI auth is ON,
|
|
5
|
+
# and SUPABASE_DB_PASSWORD is REQUIRED (no default) for the "full" profile.
|
|
3
6
|
|
|
4
7
|
# ── Ollama ──────────────────────────────────────────────────────────────────
|
|
5
8
|
OLLAMA_PORT=11434
|
|
6
9
|
|
|
7
10
|
# ── Open WebUI ──────────────────────────────────────────────────────────────
|
|
8
11
|
WEBUI_PORT=3000
|
|
9
|
-
|
|
12
|
+
# Auth is ON by default (create the admin account on first visit).
|
|
13
|
+
# Set to false ONLY if you accept an unauthenticated local UI.
|
|
14
|
+
WEBUI_AUTH=true
|
|
10
15
|
|
|
11
16
|
# ── n8n ─────────────────────────────────────────────────────────────────────
|
|
12
17
|
N8N_PORT=5678
|
|
13
18
|
|
|
14
19
|
# ── Supabase (Postgres) ────────────────────────────────────────────────────
|
|
15
20
|
SUPABASE_DB_PORT=5432
|
|
16
|
-
|
|
21
|
+
# CHANGE ME — required, no default (Postgres refuses to start if unset). Generate one:
|
|
22
|
+
# openssl rand -base64 24
|
|
23
|
+
SUPABASE_DB_PASSWORD=
|
|
17
24
|
SUPABASE_DB_NAME=app
|
|
@@ -18,7 +18,9 @@ services:
|
|
|
18
18
|
container_name: ollama
|
|
19
19
|
restart: unless-stopped
|
|
20
20
|
ports:
|
|
21
|
-
|
|
21
|
+
# Bound to loopback by default — this is a LOCAL dev stack. To expose on
|
|
22
|
+
# your LAN, change 127.0.0.1 to your interface IP (opt-in, not default).
|
|
23
|
+
- "127.0.0.1:${OLLAMA_PORT:-11434}:11434"
|
|
22
24
|
volumes:
|
|
23
25
|
- ollama_data:/root/.ollama
|
|
24
26
|
environment:
|
|
@@ -45,12 +47,14 @@ services:
|
|
|
45
47
|
profiles: ["ui", "auto", "full"]
|
|
46
48
|
restart: unless-stopped
|
|
47
49
|
ports:
|
|
48
|
-
- "
|
|
50
|
+
- "127.0.0.1:${WEBUI_PORT:-3000}:8080"
|
|
49
51
|
volumes:
|
|
50
52
|
- open_webui_data:/app/backend/data
|
|
51
53
|
environment:
|
|
52
54
|
- OLLAMA_BASE_URL=http://ollama:11434
|
|
53
|
-
|
|
55
|
+
# Auth ON by default. Set WEBUI_AUTH=false in .env only if you accept an
|
|
56
|
+
# unauthenticated UI (loopback-only mitigates, but any local process can reach it).
|
|
57
|
+
- WEBUI_AUTH=${WEBUI_AUTH:-true}
|
|
54
58
|
depends_on:
|
|
55
59
|
ollama:
|
|
56
60
|
condition: service_healthy
|
|
@@ -62,7 +66,7 @@ services:
|
|
|
62
66
|
profiles: ["auto", "full"]
|
|
63
67
|
restart: unless-stopped
|
|
64
68
|
ports:
|
|
65
|
-
- "
|
|
69
|
+
- "127.0.0.1:${N8N_PORT:-5678}:5678"
|
|
66
70
|
volumes:
|
|
67
71
|
- n8n_data:/home/node/.n8n
|
|
68
72
|
environment:
|
|
@@ -76,11 +80,15 @@ services:
|
|
|
76
80
|
profiles: ["full"]
|
|
77
81
|
restart: unless-stopped
|
|
78
82
|
ports:
|
|
79
|
-
- "
|
|
83
|
+
- "127.0.0.1:${SUPABASE_DB_PORT:-5432}:5432"
|
|
80
84
|
volumes:
|
|
81
85
|
- supabase_db_data:/var/lib/postgresql/data
|
|
82
86
|
environment:
|
|
83
|
-
|
|
87
|
+
# No insecure default — REQUIRED. If unset/empty, Postgres refuses to
|
|
88
|
+
# initialize ("superuser password is not specified"): set it in .env.
|
|
89
|
+
# Note: a compose-level `:?` guard would also break the core (ollama-only)
|
|
90
|
+
# profile at parse time, so the check is delegated to the postgres image.
|
|
91
|
+
POSTGRES_PASSWORD: ${SUPABASE_DB_PASSWORD}
|
|
84
92
|
POSTGRES_DB: ${SUPABASE_DB_NAME:-app}
|
|
85
93
|
healthcheck:
|
|
86
94
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|