javi-forge 1.9.1 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,49 @@ import { render } from "ink";
11
11
  import React from "react";
12
12
  import CI from "../../ui/CI.js";
13
13
  import { CIProvider as CIContextProvider } from "../../ui/CIContext.js";
14
+ import { CI_HELP_TEXT } from "../help.js";
14
15
  export async function handleCi(cli, ctx) {
16
+ // Per-command help: `javi-forge ci --help` shows ci-specific usage, not the
17
+ // global banner (autoHelp is disabled at the entrypoint).
18
+ if (cli.flags.help === true) {
19
+ console.log(CI_HELP_TEXT);
20
+ process.exit(0);
21
+ }
22
+ // Sub-command: javi-forge ci validate → dry config validation, no execution.
23
+ if (cli.input[1] === "validate") {
24
+ const { validateCIConfig } = await import("../../commands/ci-validate.js");
25
+ const result = await validateCIConfig(process.cwd(), cli.flags.config || undefined);
26
+ if (result.ok) {
27
+ if (result.mode === "auto-detect") {
28
+ if (cli.flags.json) {
29
+ console.log(JSON.stringify({ ok: true, mode: "auto-detect", runners: [] }, null, 2));
30
+ }
31
+ else {
32
+ console.log("✓ No .javi-forge/ci.yaml — auto-detect mode (no config to validate)");
33
+ }
34
+ }
35
+ else if (cli.flags.json) {
36
+ console.log(JSON.stringify({ ok: true, runners: result.runners }, null, 2));
37
+ }
38
+ else {
39
+ console.log(`✓ CI config valid: ${result.configPath}`);
40
+ console.log(` ${result.runners.length} runner(s):`);
41
+ for (const runner of result.runners) {
42
+ console.log(` - ${runner.name} (${runner.stack})`);
43
+ }
44
+ }
45
+ process.exit(0);
46
+ }
47
+ if (cli.flags.json) {
48
+ console.log(JSON.stringify({ ok: false, errors: result.errors }, null, 2));
49
+ }
50
+ else {
51
+ for (const err of result.errors) {
52
+ console.error(`${err.path}: ${err.message}`);
53
+ }
54
+ }
55
+ process.exit(1);
56
+ }
15
57
  // Sub-command: javi-forge ci init → install git hooks
16
58
  if (cli.input[1] === "init") {
17
59
  const { installCIHooks } = await import("../../commands/ci.js");
@@ -36,6 +78,12 @@ export async function handleCi(cli, ctx) {
36
78
  }
37
79
  process.exit(errors.length > 0 ? 1 : 0);
38
80
  }
81
+ // Any other positional is an unknown subcommand — show ci usage, don't run
82
+ // the pipeline against a typo.
83
+ if (cli.input[1] !== undefined) {
84
+ console.log(CI_HELP_TEXT);
85
+ process.exit(1);
86
+ }
39
87
  const ciMode = cli.flags.detect
40
88
  ? "detect"
41
89
  : cli.flags.shell
@@ -8,8 +8,19 @@
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 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 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 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
+ /**
13
+ * Per-command help for `ci`, shown by `javi-forge ci --help` (or when `ci` is
14
+ * given an unknown subcommand). Kept consistent with the global HELP_TEXT
15
+ * layout — whitespace is significant.
16
+ */
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";
12
18
  export declare const FLAGS_SCHEMA: {
19
+ readonly help: {
20
+ readonly type: "boolean";
21
+ readonly shortFlag: "h";
22
+ readonly default: false;
23
+ };
13
24
  readonly dryRun: {
14
25
  readonly type: "boolean";
15
26
  readonly default: false;
package/dist/cli/help.js CHANGED
@@ -15,6 +15,8 @@ export const HELP_TEXT = `
15
15
  Commands
16
16
  init Bootstrap a new project (default)
17
17
  ci Run CI simulation (lint + compile + test + security + ghagga)
18
+ ci validate Validate .javi-forge/ci.yaml without running anything
19
+ ci init Install git hooks that call javi-forge ci
18
20
  tdd init Install TDD-enforcing pre-commit hook (auto-detects stack)
19
21
  tdd pipeline Install TDD pipeline pre-push hook (--mode strict|warn)
20
22
  analyze Run repoforge skills analysis
@@ -99,12 +101,52 @@ export const HELP_TEXT = `
99
101
  $ javi-forge ci --no-docker
100
102
  $ javi-forge ci --shell
101
103
  $ javi-forge ci --config .javi-forge/ci.yaml
104
+ $ javi-forge ci validate
105
+ $ javi-forge ci --help
102
106
  $ javi-forge analyze
103
107
  $ javi-forge doctor
104
108
  $ javi-forge plugin add mapbox/agent-skills
105
109
  $ javi-forge plugin list
106
110
  `;
111
+ /**
112
+ * Per-command help for `ci`, shown by `javi-forge ci --help` (or when `ci` is
113
+ * given an unknown subcommand). Kept consistent with the global HELP_TEXT
114
+ * layout — whitespace is significant.
115
+ */
116
+ export const CI_HELP_TEXT = `
117
+ Usage
118
+ $ javi-forge ci [subcommand] [options]
119
+
120
+ Run a local CI simulation (lint + compile + test + security + ghagga).
121
+ With no subcommand, the full pipeline runs.
122
+
123
+ Subcommands
124
+ init Install git hooks that call javi-forge ci
125
+ validate Validate .javi-forge/ci.yaml without running anything
126
+
127
+ Options
128
+ --quick Lint + compile only (fast, for pre-commit)
129
+ --no-docker Run commands natively (no Docker)
130
+ --no-security Skip Semgrep security scan
131
+ --no-ci-ghagga Skip GHAGGA review
132
+ --force (ci init) Overwrite a foreign or modified hook (backs up first)
133
+ --config PATH Load ordered CI runners from a versioned config file
134
+ (default discovery: .javi-forge/ci.yaml)
135
+ --stack STACK Force a single explicit stack (single-stack repos only)
136
+ --json (ci validate) Emit the result as JSON
137
+ --help Show this help
138
+
139
+ Examples
140
+ $ javi-forge ci
141
+ $ javi-forge ci --quick
142
+ $ javi-forge ci validate
143
+ $ javi-forge ci validate --json
144
+ $ javi-forge ci init --force
145
+ `;
107
146
  export const FLAGS_SCHEMA = {
147
+ // `--help` is handled manually (autoHelp is disabled at the entrypoint so
148
+ // `ci --help` can show ci-specific usage instead of the global banner).
149
+ help: { type: "boolean", shortFlag: "h", default: false },
108
150
  dryRun: { type: "boolean", default: false },
109
151
  stack: { type: "string", default: "" },
110
152
  ci: { type: "string", default: "" },
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `javi-forge ci validate` — dry validation of `.javi-forge/ci.yaml`.
3
+ *
4
+ * Pure parse-and-report over the existing validator (src/lib/ci-config.ts):
5
+ * resolve the config path (same discovery `runCI` uses), call `loadCIConfig`,
6
+ * and return a structured result. It NEVER builds images, runs Docker, or
7
+ * executes any phase — the CLI surface stops BEFORE execution.
8
+ */
9
+ import { type CIConfigValidationError } from "../lib/ci-config.js";
10
+ /** One validated runner, reduced to what the report shows. */
11
+ export interface CIValidateRunnerSummary {
12
+ name: string;
13
+ stack: string;
14
+ }
15
+ export interface CIValidateOk {
16
+ ok: true;
17
+ /**
18
+ * "config" — a config file was found and validated.
19
+ * "auto-detect" — no config exists and none was explicitly requested, so
20
+ * `runCI` would use its zero-config single-runner auto-detect path
21
+ * (ci.ts:396-406). There is nothing to validate, and that is valid.
22
+ */
23
+ mode: "config" | "auto-detect";
24
+ /** Resolved config path, or null in auto-detect mode. */
25
+ configPath: string | null;
26
+ runners: CIValidateRunnerSummary[];
27
+ }
28
+ export interface CIValidateErr {
29
+ ok: false;
30
+ /** Resolved path we looked at, or null when discovery found nothing. */
31
+ configPath: string | null;
32
+ errors: CIConfigValidationError[];
33
+ }
34
+ export type CIValidateResult = CIValidateOk | CIValidateErr;
35
+ /**
36
+ * Validate a project's CI config without executing anything.
37
+ *
38
+ * @param projectDir Project root to discover `.javi-forge/ci.yaml` in.
39
+ * @param config Optional explicit config path (from `--config`).
40
+ */
41
+ export declare function validateCIConfig(projectDir: string, config?: string): Promise<CIValidateResult>;
42
+ //# sourceMappingURL=ci-validate.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `javi-forge ci validate` — dry validation of `.javi-forge/ci.yaml`.
3
+ *
4
+ * Pure parse-and-report over the existing validator (src/lib/ci-config.ts):
5
+ * resolve the config path (same discovery `runCI` uses), call `loadCIConfig`,
6
+ * and return a structured result. It NEVER builds images, runs Docker, or
7
+ * executes any phase — the CLI surface stops BEFORE execution.
8
+ */
9
+ import fs from "fs-extra";
10
+ import { CIConfigError, findCIConfig, loadCIConfig, } from "../lib/ci-config.js";
11
+ /**
12
+ * Validate a project's CI config without executing anything.
13
+ *
14
+ * @param projectDir Project root to discover `.javi-forge/ci.yaml` in.
15
+ * @param config Optional explicit config path (from `--config`).
16
+ */
17
+ export async function validateCIConfig(projectDir, config) {
18
+ // Discriminator: was a config path explicitly requested (`--config`)?
19
+ // - explicit path that is missing → ERROR (the user asserted a file that
20
+ // isn't there).
21
+ // - discovery finds nothing → VALID auto-detect: `runCI` runs fine in this
22
+ // exact state via its zero-config single-runner path (ci.ts:396-406), so
23
+ // there is nothing to validate and that is not a failure.
24
+ let configPath;
25
+ if (config) {
26
+ if (!(await fs.pathExists(config))) {
27
+ return {
28
+ ok: false,
29
+ configPath: config,
30
+ errors: [{ path: config, message: `no CI config found at ${config}` }],
31
+ };
32
+ }
33
+ configPath = config;
34
+ }
35
+ else {
36
+ const discovered = await findCIConfig(projectDir);
37
+ if (!discovered) {
38
+ return { ok: true, mode: "auto-detect", configPath: null, runners: [] };
39
+ }
40
+ configPath = discovered;
41
+ }
42
+ try {
43
+ const ciConfig = await loadCIConfig(configPath);
44
+ return {
45
+ ok: true,
46
+ mode: "config",
47
+ configPath,
48
+ runners: ciConfig.runners.map((r) => ({
49
+ name: r.name,
50
+ stack: r.stack,
51
+ })),
52
+ };
53
+ }
54
+ catch (e) {
55
+ if (e instanceof CIConfigError) {
56
+ return { ok: false, configPath, errors: e.errors };
57
+ }
58
+ throw e;
59
+ }
60
+ }
61
+ //# sourceMappingURL=ci-validate.js.map
@@ -83,13 +83,13 @@ async function buildCICommands(stack, buildTool, projectDir) {
83
83
  case "java-gradle":
84
84
  return {
85
85
  lintCmd: "./gradlew spotlessCheck --no-daemon",
86
- compileCmd: "./gradlew clean classes testClasses --no-daemon && chown -R runner:runner build/ .gradle/ 2>/dev/null || true",
86
+ compileCmd: "./gradlew clean classes testClasses --no-daemon",
87
87
  testCmd: "./gradlew test --no-daemon",
88
88
  };
89
89
  case "java-maven":
90
90
  return {
91
91
  lintCmd: "./mvnw spotless:check",
92
- compileCmd: "./mvnw clean compile test-compile && chown -R runner:runner target/ .mvn/ 2>/dev/null || true",
92
+ compileCmd: "./mvnw clean compile test-compile",
93
93
  testCmd: "./mvnw test",
94
94
  };
95
95
  case "node": {
@@ -101,14 +101,14 @@ async function buildCICommands(stack, buildTool, projectDir) {
101
101
  catch {
102
102
  /* no package.json */
103
103
  }
104
- // Clean dist/ before build and chown after so tests (as runner) can access output.
105
- // Runs as root inside the container to handle host-owned output dirs.
104
+ // Clean dist/ before build so a stale directory never masks a broken
105
+ // build. The container runs as the host uid (see runInContainer,
106
+ // ENV-1), so output lands host-owned — no chown needed.
106
107
  const buildPrefix = "rm -rf dist/ && ";
107
- const buildSuffix = " && chown -R runner:runner dist/ 2>/dev/null || true";
108
108
  return {
109
109
  lintCmd: pkgContent.includes('"lint"') ? `${buildTool} run lint` : null,
110
110
  compileCmd: pkgContent.includes('"build"')
111
- ? `${buildPrefix}${buildTool} run build${buildSuffix}`
111
+ ? `${buildPrefix}${buildTool} run build`
112
112
  : null,
113
113
  testCmd: pkgContent.includes('"test"')
114
114
  ? `${buildTool} ${buildTool === "npm" ? "test" : "run test"}`
@@ -124,13 +124,13 @@ async function buildCICommands(stack, buildTool, projectDir) {
124
124
  case "go":
125
125
  return {
126
126
  lintCmd: "golangci-lint run",
127
- compileCmd: "go clean -cache && go build ./... && chown -R runner:runner . 2>/dev/null || true",
127
+ compileCmd: "go clean -cache && go build ./...",
128
128
  testCmd: "go test ./...",
129
129
  };
130
130
  case "rust":
131
131
  return {
132
132
  lintCmd: "cargo clippy -- -D warnings",
133
- compileCmd: "cargo clean && cargo build && chown -R runner:runner target/ 2>/dev/null || true",
133
+ compileCmd: "cargo clean && cargo build",
134
134
  testCmd: "cargo test",
135
135
  };
136
136
  default:
@@ -565,7 +565,6 @@ async function runRunner(runner, ctx) {
565
565
  id: "compile",
566
566
  label: "Compile",
567
567
  cmds: runner.compileCmds,
568
- user: "root",
569
568
  skip: false,
570
569
  },
571
570
  {
package/dist/index.js CHANGED
@@ -17,8 +17,17 @@ setupUpdateNotifier(pkg);
17
17
  const cli = meow(HELP_TEXT, {
18
18
  importMeta: import.meta,
19
19
  flags: FLAGS_SCHEMA,
20
+ // Handle --help manually so `ci --help` can show ci-specific usage instead
21
+ // of the global banner (meow's autoHelp would print + exit before dispatch).
22
+ autoHelp: false,
20
23
  });
21
24
  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") {
28
+ console.log(HELP_TEXT);
29
+ process.exit(0);
30
+ }
22
31
  const isCI = detectCI(cli.flags);
23
32
  const inkStdin = createInkStdin();
24
33
  switch (subcommand) {
@@ -189,6 +189,20 @@ export async function runInContainer(options) {
189
189
  // detection happens on this path — ever.
190
190
  const imageName = image;
191
191
  const isInteractive = process.stdin.isTTY && stream;
192
+ // ENV-1: match the container process to the HOST user. The images bake a
193
+ // `runner` user whose uid depends on the base (1001 on node:22-slim, where
194
+ // uid 1000 is already `node`). When that uid differs from the host uid,
195
+ // everything the container writes to the bind-mounted workspace (dist/,
196
+ // node_modules/.vite-temp, build output) lands owned by the wrong user,
197
+ // and the host's local vitest then fails with EACCES. Running as the host
198
+ // uid:gid makes artifacts host-owned — no chown dance, ever. An explicit
199
+ // `user` override still wins (e.g. a caller that needs root). getuid/getgid
200
+ // are undefined on non-POSIX platforms (Windows); there we omit the flag
201
+ // and keep the image default.
202
+ const uid = process.getuid?.();
203
+ const gid = process.getgid?.();
204
+ const runAsUser = user ??
205
+ (uid !== undefined && gid !== undefined ? `${uid}:${gid}` : undefined);
192
206
  // Use --mount instead of -v: the -v form parses the value as a single
193
207
  // "src:dst[:opt]" colon-separated string, which breaks (and could be
194
208
  // hijacked) when projectDir itself contains a colon. --mount takes
@@ -201,7 +215,7 @@ export async function runInContainer(options) {
201
215
  "30",
202
216
  "--entrypoint",
203
217
  "",
204
- ...(user ? ["--user", user] : []),
218
+ ...(runAsUser ? ["--user", runAsUser] : []),
205
219
  "--mount",
206
220
  `type=bind,source=${projectDir},target=/home/runner/work`,
207
221
  "-e",
@@ -237,6 +251,11 @@ export async function runInContainer(options) {
237
251
  */
238
252
  export async function openShell(projectDir, image) {
239
253
  const imageName = image;
254
+ // ENV-1: run the interactive shell as the host user too, so anything
255
+ // written from the debug shell stays host-owned. See runInContainer.
256
+ const uid = process.getuid?.();
257
+ const gid = process.getgid?.();
258
+ const runAsUser = uid !== undefined && gid !== undefined ? `${uid}:${gid}` : undefined;
240
259
  await new Promise((resolve, reject) => {
241
260
  const proc = spawn("docker", [
242
261
  "run",
@@ -244,6 +263,7 @@ export async function openShell(projectDir, image) {
244
263
  "-it",
245
264
  "--entrypoint",
246
265
  "",
266
+ ...(runAsUser ? ["--user", runAsUser] : []),
247
267
  // --mount is colon-safe; see runInContainer for the rationale.
248
268
  "--mount",
249
269
  `type=bind,source=${projectDir},target=/home/runner/work`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.9.1",
3
+ "version": "1.10.1",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {