javi-forge 1.9.0 → 1.10.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/ci.js +48 -0
- package/dist/cli/help.d.ts +12 -1
- package/dist/cli/help.js +42 -0
- package/dist/commands/ci-validate.d.ts +42 -0
- package/dist/commands/ci-validate.js +61 -0
- package/dist/commands/ci.js +71 -20
- package/dist/index.js +9 -0
- package/package.json +1 -1
package/dist/cli/dispatch/ci.js
CHANGED
|
@@ -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
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/ci.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
|
+
import fsp from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import fs from "fs-extra";
|
|
6
7
|
import { HOOK_ASSETS_DIR } from "../constants.js";
|
|
@@ -284,13 +285,21 @@ async function isGhaggaAvailable() {
|
|
|
284
285
|
function report(onStep, id, label, status, detail) {
|
|
285
286
|
onStep({ id, label, status, detail });
|
|
286
287
|
}
|
|
287
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Detect-step label: legacy format for auto, explicit otherwise.
|
|
290
|
+
*
|
|
291
|
+
* INVARIANT (holds for every `ResolvedRunners` value): `runners` is never
|
|
292
|
+
* empty. The config path rejects an empty list before resolving
|
|
293
|
+
* (`src/lib/ci-config.ts` — "runners is required and must be a non-empty
|
|
294
|
+
* list"), and the `auto` and `stack-override` paths each yield exactly one
|
|
295
|
+
* runner. `runners[0]` therefore needs no fallback.
|
|
296
|
+
*/
|
|
288
297
|
function describeRunners(resolved) {
|
|
289
298
|
const first = resolved.runners[0];
|
|
290
|
-
if (resolved.source === "auto"
|
|
299
|
+
if (resolved.source === "auto") {
|
|
291
300
|
return `Stack: ${first.stack} (${first.buildTool})`;
|
|
292
301
|
}
|
|
293
|
-
if (resolved.source === "stack-override"
|
|
302
|
+
if (resolved.source === "stack-override") {
|
|
294
303
|
return `Stack: ${first.stack} (${first.buildTool}, --stack override)`;
|
|
295
304
|
}
|
|
296
305
|
const summary = resolved.runners
|
|
@@ -317,14 +326,16 @@ export async function runCI(options, onStep) {
|
|
|
317
326
|
}
|
|
318
327
|
// Legacy single-runner view for the zero-config auto path. Keeping this
|
|
319
328
|
// shape guarantees single-stack repositories behave exactly as before.
|
|
329
|
+
// `runners[0]` is always present — see the invariant on `describeRunners`.
|
|
330
|
+
// The command lists CAN be empty, so those keep their `?? null`.
|
|
320
331
|
const primary = resolved.runners[0];
|
|
321
332
|
const stackInfo = {
|
|
322
|
-
stackType: primary
|
|
323
|
-
buildTool: primary
|
|
324
|
-
javaVersion: primary
|
|
325
|
-
lintCmd: primary
|
|
326
|
-
compileCmd: primary
|
|
327
|
-
testCmd: primary
|
|
333
|
+
stackType: primary.stack,
|
|
334
|
+
buildTool: primary.buildTool,
|
|
335
|
+
javaVersion: primary.javaVersion,
|
|
336
|
+
lintCmd: primary.lintCmds[0] ?? null,
|
|
337
|
+
compileCmd: primary.compileCmds[0] ?? null,
|
|
338
|
+
testCmd: primary.testCmds[0] ?? null,
|
|
328
339
|
};
|
|
329
340
|
// ── Detect mode ─────────────────────────────────────────────────────────────
|
|
330
341
|
if (mode === "detect")
|
|
@@ -876,7 +887,16 @@ async function backupHook(hookPath) {
|
|
|
876
887
|
}
|
|
877
888
|
throw new Error(`could not write the backup ${candidate} (${errorCode(copyErr) || "unknown error"}): ${copyErr instanceof Error ? copyErr.message : String(copyErr)}. The hook was left unchanged.`);
|
|
878
889
|
}
|
|
879
|
-
|
|
890
|
+
// The mode restore addresses the FD of the file THIS call just created,
|
|
891
|
+
// not the path: a symlink planted at `candidate` after the
|
|
892
|
+
// `COPYFILE_EXCL` copy cannot capture the mode change (SEC-1).
|
|
893
|
+
const handle = await fsp.open(candidate, constants.O_RDONLY | O_NOFOLLOW);
|
|
894
|
+
try {
|
|
895
|
+
await handle.chmod(original.mode);
|
|
896
|
+
}
|
|
897
|
+
finally {
|
|
898
|
+
await handle.close();
|
|
899
|
+
}
|
|
880
900
|
return candidate;
|
|
881
901
|
}
|
|
882
902
|
throw new Error(`could not back up ${hookPath}: every candidate backup path is taken. The hook was left unchanged.`);
|
|
@@ -900,13 +920,51 @@ function assertHookManifestEntry(entry, hookName) {
|
|
|
900
920
|
? `has no "${hookName}" entry`
|
|
901
921
|
: typeof entry.sha256 !== "string" ||
|
|
902
922
|
typeof entry.version !== "number" ||
|
|
903
|
-
!Array.isArray(entry.historical)
|
|
904
|
-
|
|
923
|
+
!Array.isArray(entry.historical) ||
|
|
924
|
+
!entry.historical.every((h) => typeof h?.sha256 === "string")
|
|
925
|
+
? `has a malformed "${hookName}" entry (expected version:number, sha256:string, historical:array of {sha256:string})`
|
|
905
926
|
: "";
|
|
906
927
|
if (problem !== "") {
|
|
907
928
|
throw new Error(`${MANIFEST_PATH} ${problem}. The javi-forge install is incomplete; ${REINSTALL_REMEDY}.`);
|
|
908
929
|
}
|
|
909
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* `O_NOFOLLOW` on the platforms that have it, a no-op flag elsewhere. Windows
|
|
933
|
+
* has no such flag AND no hook-symlink threat worth the crash of an
|
|
934
|
+
* `undefined` in a bitmask.
|
|
935
|
+
*/
|
|
936
|
+
const O_NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
937
|
+
/**
|
|
938
|
+
* Write the hook through a FILE DESCRIPTOR, never through the path (SEC-1).
|
|
939
|
+
*
|
|
940
|
+
* `classifyHookPath` established moments earlier that `hookPath` is a regular
|
|
941
|
+
* file, but a local attacker with write access to `.git/hooks` could swap a
|
|
942
|
+
* symlink in during that window and turn the write into an arbitrary-write
|
|
943
|
+
* primitive. `O_NOFOLLOW` closes it: if the path IS a symlink when the write
|
|
944
|
+
* finally happens, `open` fails with `ELOOP` and the hook surfaces as a named
|
|
945
|
+
* per-hook error instead of clobbering the link target. Everything after the
|
|
946
|
+
* open then addresses the FD — `fchmod`, not a second path lookup — so the
|
|
947
|
+
* bytes and the mode provably land on the same inode.
|
|
948
|
+
*
|
|
949
|
+
* The mode argument applies ONLY when the file is CREATED: overwriting an
|
|
950
|
+
* existing 0644 hook would leave it non-executable and git would silently skip
|
|
951
|
+
* it. The `fchmod` is therefore unconditional, on every write path, which also
|
|
952
|
+
* makes the final mode independent of the umask.
|
|
953
|
+
*
|
|
954
|
+
* NOT covered (deferred by decision in SEC-1): a hardlink to a victim file
|
|
955
|
+
* survives `O_NOFOLLOW`. On modern Linux `fs.protected_hardlinks=1` blocks the
|
|
956
|
+
* cross-owner case; an `nlink > 1` refusal is parked in the backlog.
|
|
957
|
+
*/
|
|
958
|
+
async function writeHookFile(hookPath, content) {
|
|
959
|
+
const handle = await fsp.open(hookPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | O_NOFOLLOW, HOOK_MODE);
|
|
960
|
+
try {
|
|
961
|
+
await handle.writeFile(content, "utf8");
|
|
962
|
+
await handle.chmod(HOOK_MODE);
|
|
963
|
+
}
|
|
964
|
+
finally {
|
|
965
|
+
await handle.close();
|
|
966
|
+
}
|
|
967
|
+
}
|
|
910
968
|
/**
|
|
911
969
|
* Bring an already-current hook back to mode 0755 WITHOUT writing bytes.
|
|
912
970
|
*
|
|
@@ -997,14 +1055,7 @@ export async function installCIHooks(projectDir, options = {}) {
|
|
|
997
1055
|
backups.push(await backupHook(hookPath));
|
|
998
1056
|
}
|
|
999
1057
|
const body = await readHookBody(name);
|
|
1000
|
-
|
|
1001
|
-
// an existing 0644 hook would leave it non-executable and git would
|
|
1002
|
-
// silently skip it. The chmod is therefore unconditional, on every
|
|
1003
|
-
// write path, which also makes the mode independent of the umask.
|
|
1004
|
-
await fs.writeFile(hookPath, renderHook(body, name, entry), {
|
|
1005
|
-
mode: HOOK_MODE,
|
|
1006
|
-
});
|
|
1007
|
-
await fs.chmod(hookPath, HOOK_MODE);
|
|
1058
|
+
await writeHookFile(hookPath, renderHook(body, name, entry));
|
|
1008
1059
|
// `upgraded` means "javi-forge content was replaced by newer
|
|
1009
1060
|
// javi-forge content". A forced overwrite of someone else's file is a
|
|
1010
1061
|
// fresh install, not an upgrade.
|
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) {
|