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.
@@ -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
  /**
@@ -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
- echo -e "${GREEN}Checksum verified${NC}"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.22.2",
3
+ "version": "1.23.0",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- WEBUI_AUTH=false
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
- SUPABASE_DB_PASSWORD=postgres
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
- - "${OLLAMA_PORT:-11434}:11434"
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
- - "${WEBUI_PORT:-3000}:8080"
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
- - WEBUI_AUTH=${WEBUI_AUTH:-false}
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
- - "${N8N_PORT:-5678}:5678"
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
- - "${SUPABASE_DB_PORT:-5432}:5432"
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
- POSTGRES_PASSWORD: ${SUPABASE_DB_PASSWORD:-postgres}
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"]