javi-forge 1.25.0 → 1.25.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.
- package/dist/cli/dispatch/tdd.d.ts +6 -3
- package/dist/cli/dispatch/tdd.js +62 -31
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +2 -2
- package/dist/commands/hooks.d.ts +39 -3
- package/dist/commands/hooks.js +84 -6
- package/dist/commands/tdd.d.ts +0 -14
- package/dist/commands/tdd.js +0 -88
- package/dist/lib/ci-config.d.ts +10 -0
- package/dist/lib/ci-config.js +33 -0
- package/dist/types/index.d.ts +0 -7
- package/package.json +1 -1
- package/dist/commands/tdd-pipeline.d.ts +0 -17
- package/dist/commands/tdd-pipeline.js +0 -144
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* `javi-forge tdd <init|pipeline>` handler.
|
|
3
3
|
*
|
|
4
4
|
* Console-only (no Ink, no React, no CIContextProvider).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* hook-consolidation S3: the handlers no longer WRITE `.git/hooks` themselves.
|
|
7
|
+
* They flip the `hooks.*.tdd` flag in `.javi-forge/ci.yaml` and delegate the
|
|
8
|
+
* hook install to the hardened `installCIHooks` — the single writer of
|
|
9
|
+
* `.git/hooks`. Heavy modules are lazy-imported INSIDE the function to preserve
|
|
10
|
+
* cold-start performance.
|
|
8
11
|
*/
|
|
9
12
|
import type { CLI } from "./types.js";
|
|
10
13
|
export declare function handleTdd(cli: CLI): Promise<void>;
|
package/dist/cli/dispatch/tdd.js
CHANGED
|
@@ -2,43 +2,74 @@
|
|
|
2
2
|
* `javi-forge tdd <init|pipeline>` handler.
|
|
3
3
|
*
|
|
4
4
|
* Console-only (no Ink, no React, no CIContextProvider).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* hook-consolidation S3: the handlers no longer WRITE `.git/hooks` themselves.
|
|
7
|
+
* They flip the `hooks.*.tdd` flag in `.javi-forge/ci.yaml` and delegate the
|
|
8
|
+
* hook install to the hardened `installCIHooks` — the single writer of
|
|
9
|
+
* `.git/hooks`. Heavy modules are lazy-imported INSIDE the function to preserve
|
|
10
|
+
* cold-start performance.
|
|
8
11
|
*/
|
|
12
|
+
/** Surface an installCIHooks result to the console (notes → backups → installed
|
|
13
|
+
* → upgraded → errors), matching the `ci init` output contract. */
|
|
14
|
+
function reportInstall(result) {
|
|
15
|
+
for (const note of result.notes) {
|
|
16
|
+
console.log(`ℹ ${note}`);
|
|
17
|
+
}
|
|
18
|
+
for (const backup of result.backups) {
|
|
19
|
+
console.log(`⚠ Backed up the previous hook → ${backup}`);
|
|
20
|
+
}
|
|
21
|
+
if (result.installed.length > 0) {
|
|
22
|
+
console.log(`✓ Installed git hooks: ${result.installed.join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
for (const hook of result.upgraded) {
|
|
25
|
+
const was = result.states.find((entry) => entry.name === hook)?.state;
|
|
26
|
+
console.log(`↑ Upgraded ${hook}${was === undefined ? "" : ` (was ${was})`}`);
|
|
27
|
+
}
|
|
28
|
+
for (const err of result.errors) {
|
|
29
|
+
console.error(`✗ ${err}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
9
32
|
export async function handleTdd(cli) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
process.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
const sub = cli.input[1];
|
|
34
|
+
const force = cli.flags.force === true;
|
|
35
|
+
if (sub === "init") {
|
|
36
|
+
const { setHookFeature } = await import("../../lib/ci-config.js");
|
|
37
|
+
const { installCIHooks } = await import("../../commands/ci.js");
|
|
38
|
+
// Install FIRST: installCIHooks writes generic static shims and does not
|
|
39
|
+
// depend on the tdd flag (the flag is read at hook-RUN time). Only once the
|
|
40
|
+
// managed hook is actually in place do we flip the config — otherwise a
|
|
41
|
+
// refused install (foreign hook / husky core.hooksPath) would leave ci.yaml
|
|
42
|
+
// claiming tdd:true while no managed hook exists.
|
|
43
|
+
const result = await installCIHooks(process.cwd(), { force });
|
|
44
|
+
reportInstall(result);
|
|
45
|
+
if (result.errors.length > 0)
|
|
46
|
+
process.exit(1);
|
|
47
|
+
const configPath = await setHookFeature(process.cwd(), "pre-commit", "tdd", true);
|
|
48
|
+
console.log(`✓ Enabled TDD in ${configPath} (hooks.pre-commit.tdd: true)`);
|
|
49
|
+
console.log(" The pre-commit hook now runs the stack test command via the dispatcher.");
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
else if (sub === "pipeline") {
|
|
24
53
|
const mode = cli.flags.mode === "warn" ? "warn" : "strict";
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
const { setHookFeature } = await import("../../lib/ci-config.js");
|
|
55
|
+
const { installCIHooks } = await import("../../commands/ci.js");
|
|
56
|
+
// Install FIRST (see `init` above): never leave the config ahead of a
|
|
57
|
+
// refused install.
|
|
58
|
+
const result = await installCIHooks(process.cwd(), { force });
|
|
59
|
+
reportInstall(result);
|
|
60
|
+
if (result.errors.length > 0)
|
|
61
|
+
process.exit(1);
|
|
62
|
+
const configPath = await setHookFeature(process.cwd(), "pre-push", "tdd", mode);
|
|
63
|
+
console.log(`✓ Enabled TDD pipeline in ${configPath} (hooks.pre-push.tdd: ${mode})`);
|
|
64
|
+
console.log(mode === "warn"
|
|
65
|
+
? " The pre-push hook runs tests as an ADVISORY section (never blocks)."
|
|
66
|
+
: " The pre-push hook BLOCKS the push when tests fail.");
|
|
67
|
+
process.exit(0);
|
|
37
68
|
}
|
|
38
69
|
else {
|
|
39
70
|
console.error("Usage: javi-forge tdd <command>");
|
|
40
|
-
console.error(" init
|
|
41
|
-
console.error(" pipeline
|
|
71
|
+
console.error(" init Enable the TDD pre-commit section and install the managed hooks");
|
|
72
|
+
console.error(" pipeline Enable the TDD pre-push section (--mode strict|warn)");
|
|
42
73
|
process.exit(1);
|
|
43
74
|
}
|
|
44
75
|
}
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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
|
|
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 Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--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
|
package/dist/cli/help.js
CHANGED
|
@@ -17,8 +17,8 @@ export const HELP_TEXT = `
|
|
|
17
17
|
ci Run CI simulation (lint + compile + test + security + ghagga)
|
|
18
18
|
ci validate Validate .javi-forge/ci.yaml without running anything
|
|
19
19
|
ci init Install git hooks that call javi-forge ci
|
|
20
|
-
tdd init
|
|
21
|
-
tdd pipeline
|
|
20
|
+
tdd init Enable the TDD pre-commit section + install managed hooks
|
|
21
|
+
tdd pipeline Enable the TDD pre-push section (--mode strict|warn)
|
|
22
22
|
hooks run Run a git hook's composed sections (pre-commit | pre-push)
|
|
23
23
|
analyze Run repoforge skills analysis
|
|
24
24
|
doctor Show health report
|
package/dist/commands/hooks.d.ts
CHANGED
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
* behavior); a config that FAILS to validate exits 1 — a broken config never
|
|
11
11
|
* silently skips a gate.
|
|
12
12
|
*
|
|
13
|
-
* S1a scope:
|
|
14
|
-
* sections have no factory yet (they land in
|
|
15
|
-
* feature is skipped by `composeSections` until its
|
|
13
|
+
* S1a/S3 scope: the `ci` and `tdd` sections have bodies. The
|
|
14
|
+
* secrets/permissions/deps sections have no factory yet (they land in S4); an
|
|
15
|
+
* enabled-but-unregistered feature is skipped by `composeSections` until its
|
|
16
|
+
* slice wires a factory.
|
|
16
17
|
*/
|
|
17
18
|
import { type CIHooksConfig } from "../lib/ci-config.js";
|
|
19
|
+
import type { Stack } from "../types/index.js";
|
|
18
20
|
import { runCI } from "./ci.js";
|
|
21
|
+
import { getTddTestCommand } from "./tdd.js";
|
|
19
22
|
/** A single composable unit of hook work. */
|
|
20
23
|
export interface HookSection {
|
|
21
24
|
/** stable id — "secrets" | "permissions" | "tdd" | "deps" | "ci" */
|
|
@@ -41,6 +44,39 @@ export type HookName = "pre-commit" | "pre-push";
|
|
|
41
44
|
* in later slices.
|
|
42
45
|
*/
|
|
43
46
|
export declare function composeSections(name: HookName, config: CIHooksConfig | null, registry: SectionRegistry): HookSection[];
|
|
47
|
+
/**
|
|
48
|
+
* Injectable seams for the `tdd` section. The section resolves the project's
|
|
49
|
+
* test command at HOOK-RUN time (never interpolated into a file), so the stack
|
|
50
|
+
* detector, the command resolver and the command runner are all overridable in
|
|
51
|
+
* tests. Every field defaults to the real implementation in `defaultRegistry`.
|
|
52
|
+
*/
|
|
53
|
+
export interface TddSectionDeps {
|
|
54
|
+
/** Only the stack + build tool are consumed — the wider CIStackInfo is fine. */
|
|
55
|
+
detectStack: (projectDir: string) => Promise<{
|
|
56
|
+
stackType: Stack;
|
|
57
|
+
buildTool: string;
|
|
58
|
+
}>;
|
|
59
|
+
resolveTestCmd: typeof getTddTestCommand;
|
|
60
|
+
runCommand: (cmd: string, projectDir: string) => Promise<{
|
|
61
|
+
ok: boolean;
|
|
62
|
+
detail?: string;
|
|
63
|
+
}>;
|
|
64
|
+
log: (msg: string) => void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The `tdd` section: at hook-run time it detects the stack, resolves the stack
|
|
68
|
+
* test command and runs it. A null command (no test runner configured) is NOT a
|
|
69
|
+
* failure — it prints an honest skip notice and returns ok:true (parity with the
|
|
70
|
+
* old warning-only generated hook). A missing test runner must never block a
|
|
71
|
+
* commit or push. When the command resolves it runs and its exit code decides.
|
|
72
|
+
*/
|
|
73
|
+
export declare function tddSection(deps: TddSectionDeps): HookSection;
|
|
74
|
+
/**
|
|
75
|
+
* The default section registry: the real `ci` and `tdd` factories. `tddDeps`
|
|
76
|
+
* overrides the tdd section's seams in tests (stack/command/runner); every
|
|
77
|
+
* omitted seam falls back to the real implementation.
|
|
78
|
+
*/
|
|
79
|
+
export declare function defaultRegistry(runCIImpl: typeof runCI, log: (msg: string) => void, tddDeps?: Partial<TddSectionDeps>): SectionRegistry;
|
|
44
80
|
/** Resolve the parsed `hooks:` config for a project (null → default [ci]). */
|
|
45
81
|
export declare function loadHooksConfig(projectDir: string): Promise<CIHooksConfig | null>;
|
|
46
82
|
/** Injectable seams for tests; every field defaults to the real implementation. */
|
package/dist/commands/hooks.js
CHANGED
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
* behavior); a config that FAILS to validate exits 1 — a broken config never
|
|
11
11
|
* silently skips a gate.
|
|
12
12
|
*
|
|
13
|
-
* S1a scope:
|
|
14
|
-
* sections have no factory yet (they land in
|
|
15
|
-
* feature is skipped by `composeSections` until its
|
|
13
|
+
* S1a/S3 scope: the `ci` and `tdd` sections have bodies. The
|
|
14
|
+
* secrets/permissions/deps sections have no factory yet (they land in S4); an
|
|
15
|
+
* enabled-but-unregistered feature is skipped by `composeSections` until its
|
|
16
|
+
* slice wires a factory.
|
|
16
17
|
*/
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
17
19
|
import { findCIConfig, loadCIConfig, } from "../lib/ci-config.js";
|
|
18
|
-
import { runCI } from "./ci.js";
|
|
20
|
+
import { detectCIStack, runCI } from "./ci.js";
|
|
21
|
+
import { getTddTestCommand } from "./tdd.js";
|
|
19
22
|
/** Fixed cheap→expensive order per hook (deterministic is a feature). */
|
|
20
23
|
const PRE_COMMIT_ORDER = ["secrets", "permissions", "tdd", "ci"];
|
|
21
24
|
const PRE_PUSH_ORDER = ["deps", "tdd", "ci"];
|
|
@@ -114,8 +117,83 @@ function reportStep(step, log) {
|
|
|
114
117
|
else if (step.status === "warning")
|
|
115
118
|
log(` ⚠ ${step.label}`);
|
|
116
119
|
}
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Run a resolved test command through `bash -c`, matching the rest of the
|
|
122
|
+
* codebase's project-command idiom (`runStep` in ci.ts). `getTddTestCommand`
|
|
123
|
+
* only ever returns a fixed, controlled set of commands (`npm test`,
|
|
124
|
+
* `<buildTool> run test`, `pytest`, `go test ./...`) — never user input — so
|
|
125
|
+
* `bash -c` introduces no injection vector, and it fixes native Windows where
|
|
126
|
+
* `npm`/`pnpm`/`yarn` are `.cmd` shims that a shell-less `spawn` cannot exec
|
|
127
|
+
* (ENOENT). A non-zero exit or a spawn error is a blocking failure; a passing
|
|
128
|
+
* command is ok:true.
|
|
129
|
+
*/
|
|
130
|
+
function runTestCommand(cmd, projectDir) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const proc = spawn("bash", ["-c", cmd], {
|
|
133
|
+
cwd: projectDir,
|
|
134
|
+
stdio: "inherit",
|
|
135
|
+
env: { ...process.env, CI: "true" },
|
|
136
|
+
});
|
|
137
|
+
proc.on("close", (code) => resolve(code === 0
|
|
138
|
+
? { ok: true }
|
|
139
|
+
: { ok: false, detail: `tests failed (exit ${code ?? "unknown"})` }));
|
|
140
|
+
proc.on("error", (e) => resolve({
|
|
141
|
+
ok: false,
|
|
142
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
143
|
+
}));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The `tdd` section: at hook-run time it detects the stack, resolves the stack
|
|
148
|
+
* test command and runs it. A null command (no test runner configured) is NOT a
|
|
149
|
+
* failure — it prints an honest skip notice and returns ok:true (parity with the
|
|
150
|
+
* old warning-only generated hook). A missing test runner must never block a
|
|
151
|
+
* commit or push. When the command resolves it runs and its exit code decides.
|
|
152
|
+
*/
|
|
153
|
+
export function tddSection(deps) {
|
|
154
|
+
return {
|
|
155
|
+
id: "tdd",
|
|
156
|
+
blocking: true,
|
|
157
|
+
async run({ projectDir }) {
|
|
158
|
+
// A thrown detector / resolver / runner must NEVER propagate out of the
|
|
159
|
+
// section — an unhandled rejection would crash the hook (raw stack trace)
|
|
160
|
+
// and, for an advisory pre-push tdd:"warn", would BLOCK the push,
|
|
161
|
+
// violating the advisory-never-blocks invariant. Mirror ciSection: on a
|
|
162
|
+
// throw, report a blocking-mappable failure instead.
|
|
163
|
+
try {
|
|
164
|
+
const { stackType, buildTool } = await deps.detectStack(projectDir);
|
|
165
|
+
const testCmd = await deps.resolveTestCmd(stackType, buildTool, projectDir);
|
|
166
|
+
if (testCmd === null) {
|
|
167
|
+
deps.log(` ⓘ tdd: no test command detected for stack '${stackType}' — skipping (a missing test runner does not block).`);
|
|
168
|
+
return { ok: true };
|
|
169
|
+
}
|
|
170
|
+
deps.log(` ▶ tdd: ${testCmd}`);
|
|
171
|
+
return await deps.runCommand(testCmd, projectDir);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The default section registry: the real `ci` and `tdd` factories. `tddDeps`
|
|
184
|
+
* overrides the tdd section's seams in tests (stack/command/runner); every
|
|
185
|
+
* omitted seam falls back to the real implementation.
|
|
186
|
+
*/
|
|
187
|
+
export function defaultRegistry(runCIImpl, log, tddDeps = {}) {
|
|
188
|
+
return {
|
|
189
|
+
ci: () => ciSection(runCIImpl, log),
|
|
190
|
+
tdd: () => tddSection({
|
|
191
|
+
detectStack: tddDeps.detectStack ?? detectCIStack,
|
|
192
|
+
resolveTestCmd: tddDeps.resolveTestCmd ?? getTddTestCommand,
|
|
193
|
+
runCommand: tddDeps.runCommand ?? runTestCommand,
|
|
194
|
+
log: tddDeps.log ?? log,
|
|
195
|
+
}),
|
|
196
|
+
};
|
|
119
197
|
}
|
|
120
198
|
/** Resolve the parsed `hooks:` config for a project (null → default [ci]). */
|
|
121
199
|
export async function loadHooksConfig(projectDir) {
|
package/dist/commands/tdd.d.ts
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import type { Stack } from "../types/index.js";
|
|
2
|
-
export interface TddHookResult {
|
|
3
|
-
installed: string[];
|
|
4
|
-
errors: string[];
|
|
5
|
-
}
|
|
6
2
|
/**
|
|
7
3
|
* Resolve the correct test command for TDD hook based on stack and build tool.
|
|
8
4
|
* Returns null if no test command can be determined.
|
|
9
5
|
*/
|
|
10
6
|
export declare function getTddTestCommand(stack: Stack, buildTool: string, projectDir: string): Promise<string | null>;
|
|
11
|
-
/**
|
|
12
|
-
* Generate a TDD-enforcing pre-commit hook script.
|
|
13
|
-
* If testCmd is null, generates a warning-only hook.
|
|
14
|
-
*/
|
|
15
|
-
export declare function generateTddHook(testCmd: string | null, stack: Stack): string;
|
|
16
|
-
/**
|
|
17
|
-
* Install TDD pre-commit hook into .git/hooks/.
|
|
18
|
-
* Detects the project stack automatically and generates the appropriate hook.
|
|
19
|
-
*/
|
|
20
|
-
export declare function installTddHooks(projectDir: string): Promise<TddHookResult>;
|
|
21
7
|
//# sourceMappingURL=tdd.d.ts.map
|
package/dist/commands/tdd.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
import { detectCIStack } from "./ci.js";
|
|
4
3
|
// =============================================================================
|
|
5
4
|
// Test command resolution
|
|
6
5
|
// =============================================================================
|
|
@@ -30,91 +29,4 @@ export async function getTddTestCommand(stack, buildTool, projectDir) {
|
|
|
30
29
|
return null;
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
|
-
// =============================================================================
|
|
34
|
-
// Hook generation
|
|
35
|
-
// =============================================================================
|
|
36
|
-
/**
|
|
37
|
-
* Generate a TDD-enforcing pre-commit hook script.
|
|
38
|
-
* If testCmd is null, generates a warning-only hook.
|
|
39
|
-
*/
|
|
40
|
-
export function generateTddHook(testCmd, stack) {
|
|
41
|
-
if (!testCmd) {
|
|
42
|
-
return `#!/bin/bash
|
|
43
|
-
# =============================================================================
|
|
44
|
-
# TDD PRE-COMMIT: No test command detected for stack "${stack}"
|
|
45
|
-
# =============================================================================
|
|
46
|
-
# Install a test runner and re-run: javi-forge tdd init
|
|
47
|
-
# =============================================================================
|
|
48
|
-
|
|
49
|
-
echo "TDD HOOK: No test command configured for stack '${stack}' — skipping."
|
|
50
|
-
exit 0
|
|
51
|
-
`;
|
|
52
|
-
}
|
|
53
|
-
return `#!/bin/bash
|
|
54
|
-
# =============================================================================
|
|
55
|
-
# TDD PRE-COMMIT: Enforced test-driven development
|
|
56
|
-
# =============================================================================
|
|
57
|
-
# Flow: Tests MUST pass before commit is allowed.
|
|
58
|
-
# Stack: ${stack} | Command: ${testCmd}
|
|
59
|
-
# To skip: git commit --no-verify
|
|
60
|
-
# =============================================================================
|
|
61
|
-
|
|
62
|
-
set -e
|
|
63
|
-
|
|
64
|
-
echo "TDD PRE-COMMIT: Running tests..."
|
|
65
|
-
echo " Stack: ${stack}"
|
|
66
|
-
echo " Command: ${testCmd}"
|
|
67
|
-
echo ""
|
|
68
|
-
|
|
69
|
-
${testCmd} || {
|
|
70
|
-
echo ""
|
|
71
|
-
echo "TDD FAILED — Tests did not pass."
|
|
72
|
-
echo " Fix failing tests before committing."
|
|
73
|
-
echo " To skip: git commit --no-verify"
|
|
74
|
-
exit 1
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
echo ""
|
|
78
|
-
echo "TDD PASSED — All tests green. Commit allowed."
|
|
79
|
-
`;
|
|
80
|
-
}
|
|
81
|
-
// =============================================================================
|
|
82
|
-
// Hook installation
|
|
83
|
-
// =============================================================================
|
|
84
|
-
/**
|
|
85
|
-
* Install TDD pre-commit hook into .git/hooks/.
|
|
86
|
-
* Detects the project stack automatically and generates the appropriate hook.
|
|
87
|
-
*/
|
|
88
|
-
export async function installTddHooks(projectDir) {
|
|
89
|
-
const gitDir = path.join(projectDir, ".git");
|
|
90
|
-
if (!(await fs.pathExists(gitDir))) {
|
|
91
|
-
return {
|
|
92
|
-
installed: [],
|
|
93
|
-
errors: ["Not a git repository. Run git init first."],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const hooksDir = path.join(gitDir, "hooks");
|
|
97
|
-
await fs.ensureDir(hooksDir);
|
|
98
|
-
// Detect stack
|
|
99
|
-
let stackInfo;
|
|
100
|
-
try {
|
|
101
|
-
stackInfo = await detectCIStack(projectDir);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
return { installed: [], errors: ["Failed to detect project stack."] };
|
|
105
|
-
}
|
|
106
|
-
const testCmd = await getTddTestCommand(stackInfo.stackType, stackInfo.buildTool, projectDir);
|
|
107
|
-
const hookContent = generateTddHook(testCmd, stackInfo.stackType);
|
|
108
|
-
const installed = [];
|
|
109
|
-
const errors = [];
|
|
110
|
-
const hookPath = path.join(hooksDir, "pre-commit");
|
|
111
|
-
try {
|
|
112
|
-
await fs.writeFile(hookPath, hookContent, { mode: 0o755 });
|
|
113
|
-
installed.push("pre-commit");
|
|
114
|
-
}
|
|
115
|
-
catch (e) {
|
|
116
|
-
errors.push(`pre-commit: ${e instanceof Error ? e.message : String(e)}`);
|
|
117
|
-
}
|
|
118
|
-
return { installed, errors };
|
|
119
|
-
}
|
|
120
32
|
//# sourceMappingURL=tdd.js.map
|
package/dist/lib/ci-config.d.ts
CHANGED
|
@@ -144,6 +144,16 @@ export declare function parseCIConfig(rawYaml: string, source?: string): CIConfi
|
|
|
144
144
|
* validation error throws — no config is ever silently ignored.
|
|
145
145
|
*/
|
|
146
146
|
export declare function loadCIConfig(configPath: string): Promise<CIConfig>;
|
|
147
|
+
/**
|
|
148
|
+
* Set a single `hooks.<hook>.<feature>` flag in a project's CI config, creating
|
|
149
|
+
* a minimal `version: 2` `.javi-forge/ci.yaml` when none exists. Existing
|
|
150
|
+
* content — runners, gates, other hooks, comments and formatting — is preserved
|
|
151
|
+
* via a YAML document round-trip (`parseDocument`), so this NEVER clobbers a
|
|
152
|
+
* hand-authored config. `hooks:` is a v2-only key, so the version is bumped to 2
|
|
153
|
+
* when a hook feature is written into an older document. Returns the path
|
|
154
|
+
* written. Fail-closed callers still validate on the next `loadCIConfig`.
|
|
155
|
+
*/
|
|
156
|
+
export declare function setHookFeature(projectDir: string, hook: "pre-commit" | "pre-push", feature: string, value: boolean | string): Promise<string>;
|
|
147
157
|
/**
|
|
148
158
|
* Discover the default CI config for a project directory.
|
|
149
159
|
* Returns the config path, or null when the project has no config
|
package/dist/lib/ci-config.js
CHANGED
|
@@ -633,6 +633,39 @@ export async function loadCIConfig(configPath) {
|
|
|
633
633
|
const raw = await fs.readFile(configPath, "utf-8");
|
|
634
634
|
return parseCIConfig(raw, configPath);
|
|
635
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Set a single `hooks.<hook>.<feature>` flag in a project's CI config, creating
|
|
638
|
+
* a minimal `version: 2` `.javi-forge/ci.yaml` when none exists. Existing
|
|
639
|
+
* content — runners, gates, other hooks, comments and formatting — is preserved
|
|
640
|
+
* via a YAML document round-trip (`parseDocument`), so this NEVER clobbers a
|
|
641
|
+
* hand-authored config. `hooks:` is a v2-only key, so the version is bumped to 2
|
|
642
|
+
* when a hook feature is written into an older document. Returns the path
|
|
643
|
+
* written. Fail-closed callers still validate on the next `loadCIConfig`.
|
|
644
|
+
*/
|
|
645
|
+
export async function setHookFeature(projectDir, hook, feature, value) {
|
|
646
|
+
const existing = await findCIConfig(projectDir);
|
|
647
|
+
const configPath = existing ?? path.join(projectDir, CI_CONFIG_CANDIDATES[0]);
|
|
648
|
+
const doc = existing
|
|
649
|
+
? YAML.parseDocument(await fs.readFile(existing, "utf-8"))
|
|
650
|
+
: YAML.parseDocument("version: 2\n");
|
|
651
|
+
// Fail-closed: never write over a malformed config. A parse error means the
|
|
652
|
+
// document is only partially represented, so setIn + write would silently
|
|
653
|
+
// drop the unparseable remainder of a hand-authored file.
|
|
654
|
+
if (doc.errors.length > 0) {
|
|
655
|
+
throw new CIConfigError(doc.errors.map((e) => ({
|
|
656
|
+
path: "<document>",
|
|
657
|
+
message: `invalid YAML: ${e.message.split("\n")[0]}`,
|
|
658
|
+
})), configPath);
|
|
659
|
+
}
|
|
660
|
+
// `hooks:` requires version 2; bump an older/unset document in place.
|
|
661
|
+
if (doc.get("version") !== 2) {
|
|
662
|
+
doc.set("version", 2);
|
|
663
|
+
}
|
|
664
|
+
doc.setIn(["hooks", hook, feature], value);
|
|
665
|
+
await fs.ensureDir(path.dirname(configPath));
|
|
666
|
+
await fs.writeFile(configPath, doc.toString());
|
|
667
|
+
return configPath;
|
|
668
|
+
}
|
|
636
669
|
/**
|
|
637
670
|
* Discover the default CI config for a project directory.
|
|
638
671
|
* Returns the config path, or null when the project has no config
|
package/dist/types/index.d.ts
CHANGED
|
@@ -67,13 +67,6 @@ export interface DoctorSection {
|
|
|
67
67
|
export interface DoctorResult {
|
|
68
68
|
sections: DoctorSection[];
|
|
69
69
|
}
|
|
70
|
-
export type TddPipelineMode = "strict" | "warn";
|
|
71
|
-
export interface TddPipelineResult {
|
|
72
|
-
installed: string[];
|
|
73
|
-
skipped: string[];
|
|
74
|
-
errors: string[];
|
|
75
|
-
mode: TddPipelineMode;
|
|
76
|
-
}
|
|
77
70
|
export interface PluginManifest {
|
|
78
71
|
name: string;
|
|
79
72
|
version: string;
|
package/package.json
CHANGED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Stack, TddPipelineMode, TddPipelineResult } from "../types/index.js";
|
|
2
|
-
/**
|
|
3
|
-
* Generate a TDD pipeline enforcement pre-push hook script.
|
|
4
|
-
*
|
|
5
|
-
* - strict: tests MUST pass or push is blocked (exit 1).
|
|
6
|
-
* - warn: tests are run and results shown, but push is never blocked.
|
|
7
|
-
*
|
|
8
|
-
* If testCmd is null, generates a skip-only hook regardless of mode.
|
|
9
|
-
*/
|
|
10
|
-
export declare function generateTddPipelineHook(mode: TddPipelineMode, testCmd: string | null, stack: Stack): string;
|
|
11
|
-
/**
|
|
12
|
-
* Install TDD pipeline pre-push hook into .git/hooks/.
|
|
13
|
-
* Detects the project stack automatically and generates the appropriate hook.
|
|
14
|
-
* Backs up any existing pre-push hook to pre-push.bak.
|
|
15
|
-
*/
|
|
16
|
-
export declare function installTddPipelineHook(projectDir: string, mode: TddPipelineMode): Promise<TddPipelineResult>;
|
|
17
|
-
//# sourceMappingURL=tdd-pipeline.d.ts.map
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import fs from "fs-extra";
|
|
3
|
-
import { detectCIStack } from "./ci.js";
|
|
4
|
-
import { getTddTestCommand } from "./tdd.js";
|
|
5
|
-
// =============================================================================
|
|
6
|
-
// Hook generation
|
|
7
|
-
// =============================================================================
|
|
8
|
-
/**
|
|
9
|
-
* Generate a TDD pipeline enforcement pre-push hook script.
|
|
10
|
-
*
|
|
11
|
-
* - strict: tests MUST pass or push is blocked (exit 1).
|
|
12
|
-
* - warn: tests are run and results shown, but push is never blocked.
|
|
13
|
-
*
|
|
14
|
-
* If testCmd is null, generates a skip-only hook regardless of mode.
|
|
15
|
-
*/
|
|
16
|
-
export function generateTddPipelineHook(mode, testCmd, stack) {
|
|
17
|
-
if (!testCmd) {
|
|
18
|
-
return `#!/bin/bash
|
|
19
|
-
# =============================================================================
|
|
20
|
-
# TDD PIPELINE (pre-push): No test command detected for stack "${stack}"
|
|
21
|
-
# =============================================================================
|
|
22
|
-
# Install a test runner and re-run: javi-forge tdd pipeline --mode ${mode}
|
|
23
|
-
# =============================================================================
|
|
24
|
-
|
|
25
|
-
echo "TDD PIPELINE: No test command configured for stack '${stack}' — skipping."
|
|
26
|
-
exit 0
|
|
27
|
-
`;
|
|
28
|
-
}
|
|
29
|
-
if (mode === "warn") {
|
|
30
|
-
return `#!/bin/bash
|
|
31
|
-
# =============================================================================
|
|
32
|
-
# TDD PIPELINE (pre-push): WARN mode
|
|
33
|
-
# =============================================================================
|
|
34
|
-
# Flow: Spec → Tests → Fail → Implement → Pass
|
|
35
|
-
# Stack: ${stack} | Command: ${testCmd}
|
|
36
|
-
# Mode: warn — tests run but push is NEVER blocked
|
|
37
|
-
# To skip: git push --no-verify
|
|
38
|
-
# =============================================================================
|
|
39
|
-
|
|
40
|
-
echo "TDD PIPELINE [WARN]: Running tests before push..."
|
|
41
|
-
echo " Stack: ${stack}"
|
|
42
|
-
echo " Command: ${testCmd}"
|
|
43
|
-
echo " Mode: warn (push will proceed regardless)"
|
|
44
|
-
echo ""
|
|
45
|
-
|
|
46
|
-
${testCmd} && {
|
|
47
|
-
echo ""
|
|
48
|
-
echo "TDD PIPELINE [WARN]: All tests passed."
|
|
49
|
-
} || {
|
|
50
|
-
echo ""
|
|
51
|
-
echo "TDD PIPELINE [WARN]: Tests FAILED — but push will proceed (warn mode)."
|
|
52
|
-
echo " Consider fixing tests before merging."
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
exit 0
|
|
56
|
-
`;
|
|
57
|
-
}
|
|
58
|
-
// strict mode (default)
|
|
59
|
-
return `#!/bin/bash
|
|
60
|
-
# =============================================================================
|
|
61
|
-
# TDD PIPELINE (pre-push): STRICT mode
|
|
62
|
-
# =============================================================================
|
|
63
|
-
# Flow: Spec → Tests → Fail → Implement → Pass
|
|
64
|
-
# Stack: ${stack} | Command: ${testCmd}
|
|
65
|
-
# Mode: strict — push is BLOCKED if tests fail
|
|
66
|
-
# To skip: git push --no-verify
|
|
67
|
-
# =============================================================================
|
|
68
|
-
|
|
69
|
-
set -e
|
|
70
|
-
|
|
71
|
-
echo "TDD PIPELINE [STRICT]: Running tests before push..."
|
|
72
|
-
echo " Stack: ${stack}"
|
|
73
|
-
echo " Command: ${testCmd}"
|
|
74
|
-
echo " Mode: strict (push blocked on failure)"
|
|
75
|
-
echo ""
|
|
76
|
-
|
|
77
|
-
${testCmd} || {
|
|
78
|
-
echo ""
|
|
79
|
-
echo "TDD PIPELINE [STRICT]: FAILED — Tests did not pass."
|
|
80
|
-
echo " Fix failing tests before pushing."
|
|
81
|
-
echo " To skip: git push --no-verify"
|
|
82
|
-
exit 1
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
echo ""
|
|
86
|
-
echo "TDD PIPELINE [STRICT]: All tests passed. Push allowed."
|
|
87
|
-
`;
|
|
88
|
-
}
|
|
89
|
-
// =============================================================================
|
|
90
|
-
// Hook installation
|
|
91
|
-
// =============================================================================
|
|
92
|
-
/**
|
|
93
|
-
* Install TDD pipeline pre-push hook into .git/hooks/.
|
|
94
|
-
* Detects the project stack automatically and generates the appropriate hook.
|
|
95
|
-
* Backs up any existing pre-push hook to pre-push.bak.
|
|
96
|
-
*/
|
|
97
|
-
export async function installTddPipelineHook(projectDir, mode) {
|
|
98
|
-
const result = {
|
|
99
|
-
installed: [],
|
|
100
|
-
skipped: [],
|
|
101
|
-
errors: [],
|
|
102
|
-
mode,
|
|
103
|
-
};
|
|
104
|
-
const gitDir = path.join(projectDir, ".git");
|
|
105
|
-
if (!(await fs.pathExists(gitDir))) {
|
|
106
|
-
result.errors.push("Not a git repository. Run git init first.");
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
const hooksDir = path.join(gitDir, "hooks");
|
|
110
|
-
await fs.ensureDir(hooksDir);
|
|
111
|
-
// Detect stack
|
|
112
|
-
let stackInfo;
|
|
113
|
-
try {
|
|
114
|
-
stackInfo = await detectCIStack(projectDir);
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
result.errors.push("Failed to detect project stack.");
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
const testCmd = await getTddTestCommand(stackInfo.stackType, stackInfo.buildTool, projectDir);
|
|
121
|
-
const hookContent = generateTddPipelineHook(mode, testCmd, stackInfo.stackType);
|
|
122
|
-
const hookPath = path.join(hooksDir, "pre-push");
|
|
123
|
-
// Backup existing hook
|
|
124
|
-
if (await fs.pathExists(hookPath)) {
|
|
125
|
-
const backupPath = path.join(hooksDir, "pre-push.bak");
|
|
126
|
-
try {
|
|
127
|
-
await fs.copy(hookPath, backupPath, { overwrite: true });
|
|
128
|
-
result.skipped.push("pre-push (backed up to pre-push.bak)");
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
result.errors.push(`backup: ${e instanceof Error ? e.message : String(e)}`);
|
|
132
|
-
return result;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
await fs.writeFile(hookPath, hookContent, { mode: 0o755 });
|
|
137
|
-
result.installed.push("pre-push");
|
|
138
|
-
}
|
|
139
|
-
catch (e) {
|
|
140
|
-
result.errors.push(`pre-push: ${e instanceof Error ? e.message : String(e)}`);
|
|
141
|
-
}
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
//# sourceMappingURL=tdd-pipeline.js.map
|