javi-forge 1.6.1 → 1.7.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/README.md +45 -1
- package/ci-local/README.md +56 -11
- package/ci-local/ci-local.ps1 +445 -0
- package/ci-local/ci-local.sh +52 -9
- package/ci-local/hooks/commit-msg +120 -44
- package/ci-local/hooks/commit-msg.test.sh +286 -0
- package/ci-local/hooks/pre-commit +9 -1
- package/ci-local/hooks/pre-push +13 -2
- package/ci-local/install.ps1 +367 -0
- package/ci-local/install.sh +151 -5
- package/ci-local/uninstall.ps1 +85 -0
- package/ci-local/uninstall.sh +95 -0
- package/dist/cli/dispatch/ci.d.ts +12 -0
- package/dist/cli/dispatch/ci.js +38 -0
- package/dist/cli/dispatch/security.d.ts +11 -0
- package/dist/cli/dispatch/security.js +68 -0
- package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
- package/dist/cli/dispatch/simple-renderers.js +65 -0
- package/dist/cli/dispatch/skill-publish.d.ts +15 -0
- package/dist/cli/dispatch/skill-publish.js +44 -0
- package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
- package/dist/cli/dispatch/skills-cmd.js +93 -0
- package/dist/cli/dispatch/tdd.d.ts +11 -0
- package/dist/cli/dispatch/tdd.js +45 -0
- package/dist/cli/dispatch/types.d.ts +29 -0
- package/dist/cli/dispatch/types.js +2 -0
- package/dist/cli/dispatch/workflow.d.ts +11 -0
- package/dist/cli/dispatch/workflow.js +67 -0
- package/dist/cli/help.d.ts +127 -0
- package/dist/cli/help.js +138 -0
- package/dist/cli/runtime.d.ts +16 -0
- package/dist/cli/runtime.js +38 -0
- package/dist/cli/validators.d.ts +17 -0
- package/dist/cli/validators.js +35 -0
- package/dist/commands/analyze.js +1 -3
- package/dist/commands/ci.d.ts +47 -0
- package/dist/commands/ci.js +386 -67
- package/dist/commands/doctor.js +2 -4
- package/dist/commands/init/report.d.ts +13 -0
- package/dist/commands/init/report.js +13 -0
- package/dist/commands/init/steps/agent-skills.d.ts +13 -0
- package/dist/commands/init/steps/agent-skills.js +45 -0
- package/dist/commands/init/steps/ai-sync.d.ts +15 -0
- package/dist/commands/init/steps/ai-sync.js +62 -0
- package/dist/commands/init/steps/ci.d.ts +24 -0
- package/dist/commands/init/steps/ci.js +74 -0
- package/dist/commands/init/steps/claude-md.d.ts +14 -0
- package/dist/commands/init/steps/claude-md.js +54 -0
- package/dist/commands/init/steps/code-graph.d.ts +16 -0
- package/dist/commands/init/steps/code-graph.js +68 -0
- package/dist/commands/init/steps/context-dir.d.ts +13 -0
- package/dist/commands/init/steps/context-dir.js +47 -0
- package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
- package/dist/commands/init/steps/docker-deploy.js +60 -0
- package/dist/commands/init/steps/ghagga.d.ts +15 -0
- package/dist/commands/init/steps/ghagga.js +58 -0
- package/dist/commands/init/steps/git.d.ts +25 -0
- package/dist/commands/init/steps/git.js +81 -0
- package/dist/commands/init/steps/gitignore.d.ts +13 -0
- package/dist/commands/init/steps/gitignore.js +39 -0
- package/dist/commands/init/steps/local-ai.d.ts +14 -0
- package/dist/commands/init/steps/local-ai.js +53 -0
- package/dist/commands/init/steps/manifest.d.ts +13 -0
- package/dist/commands/init/steps/manifest.js +58 -0
- package/dist/commands/init/steps/memory.d.ts +15 -0
- package/dist/commands/init/steps/memory.js +60 -0
- package/dist/commands/init/steps/mock.d.ts +13 -0
- package/dist/commands/init/steps/mock.js +64 -0
- package/dist/commands/init/steps/sdd.d.ts +12 -0
- package/dist/commands/init/steps/sdd.js +40 -0
- package/dist/commands/init/steps/security.d.ts +28 -0
- package/dist/commands/init/steps/security.js +103 -0
- package/dist/commands/init/types.d.ts +30 -0
- package/dist/commands/init/types.js +2 -0
- package/dist/commands/init.d.ts +5 -4
- package/dist/commands/init.js +42 -630
- package/dist/commands/llmstxt.js +1 -1
- package/dist/commands/plugin.js +1 -1
- package/dist/commands/security.js +23 -4
- package/dist/commands/skills/analysis.d.ts +19 -0
- package/dist/commands/skills/analysis.js +211 -0
- package/dist/commands/skills/benchmark.d.ts +6 -0
- package/dist/commands/skills/benchmark.js +82 -0
- package/dist/commands/skills/constants.d.ts +10 -0
- package/dist/commands/skills/constants.js +27 -0
- package/dist/commands/skills/directives.d.ts +21 -0
- package/dist/commands/skills/directives.js +101 -0
- package/dist/commands/skills/parsing.d.ts +16 -0
- package/dist/commands/skills/parsing.js +108 -0
- package/dist/commands/skills/rules.d.ts +5 -0
- package/dist/commands/skills/rules.js +32 -0
- package/dist/commands/skills/scoring.d.ts +47 -0
- package/dist/commands/skills/scoring.js +306 -0
- package/dist/commands/skills.d.ts +7 -125
- package/dist/commands/skills.js +8 -874
- package/dist/commands/tdd-pipeline.js +1 -1
- package/dist/commands/tdd.js +1 -1
- package/dist/commands/workflow.js +2 -0
- package/dist/constants.js +2 -2
- package/dist/index.js +26 -473
- package/dist/lib/agent-skills.js +1 -1
- package/dist/{commands → lib}/atlassian-mcp.js +2 -2
- package/dist/lib/auto-skill-install.js +2 -3
- package/dist/lib/auto-wire.js +7 -7
- package/dist/lib/ci-config.d.ts +65 -0
- package/dist/lib/ci-config.js +297 -0
- package/dist/lib/claudemd.js +1 -1
- package/dist/lib/codex-export.js +4 -8
- package/dist/lib/common.js +2 -2
- package/dist/lib/context.js +2 -2
- package/dist/{commands → lib}/crash-recovery.js +4 -4
- package/dist/lib/docker.d.ts +22 -1
- package/dist/lib/docker.js +57 -49
- package/dist/lib/exec.d.ts +10 -0
- package/dist/lib/exec.js +11 -0
- package/dist/lib/frontmatter.js +2 -2
- package/dist/{commands → lib}/harness-audit.js +1 -1
- package/dist/{commands → lib}/parallel-batch.js +1 -3
- package/dist/lib/plugin.js +2 -4
- package/dist/{commands → lib}/secret-scanner.js +3 -1
- package/dist/{commands → lib}/security-analysis.js +4 -2
- package/dist/lib/skill-publish.js +23 -5
- package/dist/{commands → lib}/skill-scanner.d.ts +1 -1
- package/dist/{commands → lib}/skill-scanner.js +3 -3
- package/dist/lib/stack-detector.js +3 -3
- package/dist/{commands → lib}/supply-chain.js +1 -1
- package/dist/lib/template.js +2 -2
- package/dist/lib/workflow/discovery.d.ts +8 -0
- package/dist/lib/workflow/discovery.js +18 -1
- package/dist/lib/workflow/parser.js +15 -15
- package/dist/lib/workflow/validator.js +4 -4
- package/dist/tasks/index.d.ts +3 -3
- package/dist/tasks/index.js +3 -3
- package/dist/tasks/scaffold-tasks.d.ts +1 -1
- package/dist/tasks/scaffold-tasks.js +10 -10
- package/dist/tasks/task-id.d.ts +1 -1
- package/dist/tasks/task-id.js +9 -5
- package/dist/tasks/task-tracker.d.ts +1 -1
- package/dist/tasks/task-tracker.js +7 -7
- package/dist/ui/App.js +1 -1
- package/dist/ui/CIContext.js +1 -2
- package/dist/ui/Header.js +4 -4
- package/dist/ui/NameInput.js +1 -1
- package/dist/ui/Progress.d.ts +1 -1
- package/dist/ui/Progress.js +1 -1
- package/dist/ui/StackSelector.js +1 -1
- package/lib/common.psm1 +167 -0
- package/lib/common.sh +30 -0
- package/package.json +38 -19
- /package/dist/{commands → lib}/atlassian-mcp.d.ts +0 -0
- /package/dist/{commands → lib}/crash-recovery.d.ts +0 -0
- /package/dist/{commands → lib}/harness-audit.d.ts +0 -0
- /package/dist/{commands → lib}/parallel-batch.d.ts +0 -0
- /package/dist/{commands → lib}/secret-scanner.d.ts +0 -0
- /package/dist/{commands → lib}/security-analysis.d.ts +0 -0
- /package/dist/{commands → lib}/supply-chain.d.ts +0 -0
- /package/dist/{commands → lib}/team-presets.d.ts +0 -0
- /package/dist/{commands → lib}/team-presets.js +0 -0
package/dist/commands/ci.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import fs from "fs-extra";
|
|
3
|
-
import
|
|
4
|
-
import { promisify } from "util";
|
|
4
|
+
import { CI_STACKS, findCIConfig, loadCIConfig, } from "../lib/ci-config.js";
|
|
5
5
|
import { refreshContextDir } from "../lib/context.js";
|
|
6
6
|
import { ensureImage, isDockerAvailable, openShell, runInContainer, } from "../lib/docker.js";
|
|
7
|
-
|
|
7
|
+
import { execFileAsync } from "../lib/exec.js";
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// Stack detection
|
|
10
10
|
// =============================================================================
|
|
@@ -133,6 +133,136 @@ async function buildCICommands(stack, buildTool, projectDir) {
|
|
|
133
133
|
return { lintCmd: null, compileCmd: null, testCmd: null };
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
function freezeRunner(runner) {
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
...runner,
|
|
139
|
+
setupCmds: Object.freeze([...runner.setupCmds]),
|
|
140
|
+
lintCmds: Object.freeze([...runner.lintCmds]),
|
|
141
|
+
compileCmds: Object.freeze([...runner.compileCmds]),
|
|
142
|
+
testCmds: Object.freeze([...runner.testCmds]),
|
|
143
|
+
securityCmds: Object.freeze([...runner.securityCmds]),
|
|
144
|
+
requiredTools: Object.freeze([...runner.requiredTools]),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function freezeRunners(source, runners) {
|
|
148
|
+
return Object.freeze({ source, runners: Object.freeze(runners) });
|
|
149
|
+
}
|
|
150
|
+
/** Build-tool heuristic for a known stack in a given directory. */
|
|
151
|
+
async function detectBuildTool(stack, dir) {
|
|
152
|
+
switch (stack) {
|
|
153
|
+
case "node":
|
|
154
|
+
if (await fs.pathExists(path.join(dir, "pnpm-lock.yaml")))
|
|
155
|
+
return "pnpm";
|
|
156
|
+
if (await fs.pathExists(path.join(dir, "yarn.lock")))
|
|
157
|
+
return "yarn";
|
|
158
|
+
return "npm";
|
|
159
|
+
case "python":
|
|
160
|
+
if (await fs.pathExists(path.join(dir, "uv.lock")))
|
|
161
|
+
return "uv";
|
|
162
|
+
if (await fs.pathExists(path.join(dir, "poetry.lock")))
|
|
163
|
+
return "poetry";
|
|
164
|
+
return "pip";
|
|
165
|
+
case "java-gradle":
|
|
166
|
+
return "gradle";
|
|
167
|
+
case "java-maven":
|
|
168
|
+
return "mvn";
|
|
169
|
+
case "go":
|
|
170
|
+
return "go";
|
|
171
|
+
case "rust":
|
|
172
|
+
return "cargo";
|
|
173
|
+
default:
|
|
174
|
+
return "";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function toList(command) {
|
|
178
|
+
return command ? [command] : [];
|
|
179
|
+
}
|
|
180
|
+
async function resolveConfiguredRunner(projectDir, config) {
|
|
181
|
+
const runnerDir = path.join(projectDir, config.directory);
|
|
182
|
+
const buildTool = await detectBuildTool(config.stack, runnerDir);
|
|
183
|
+
const defaults = await buildCICommands(config.stack, buildTool, runnerDir);
|
|
184
|
+
return freezeRunner({
|
|
185
|
+
name: config.name,
|
|
186
|
+
stack: config.stack,
|
|
187
|
+
buildTool,
|
|
188
|
+
javaVersion: "21",
|
|
189
|
+
directory: config.directory,
|
|
190
|
+
image: config.image,
|
|
191
|
+
buildContext: config.buildContext,
|
|
192
|
+
setupCmds: config.setup,
|
|
193
|
+
lintCmds: config.lint.length > 0 ? config.lint : toList(defaults.lintCmd),
|
|
194
|
+
compileCmds: config.build.length > 0 ? config.build : toList(defaults.compileCmd),
|
|
195
|
+
testCmds: config.test.length > 0 ? config.test : toList(defaults.testCmd),
|
|
196
|
+
securityCmds: config.security,
|
|
197
|
+
requiredTools: config.requires,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async function resolveExplicitStackRunner(projectDir, stack) {
|
|
201
|
+
const buildTool = await detectBuildTool(stack, projectDir);
|
|
202
|
+
const defaults = await buildCICommands(stack, buildTool, projectDir);
|
|
203
|
+
return freezeRunner({
|
|
204
|
+
name: stack,
|
|
205
|
+
stack,
|
|
206
|
+
buildTool,
|
|
207
|
+
javaVersion: "21",
|
|
208
|
+
directory: ".",
|
|
209
|
+
setupCmds: [],
|
|
210
|
+
lintCmds: toList(defaults.lintCmd),
|
|
211
|
+
compileCmds: toList(defaults.compileCmd),
|
|
212
|
+
testCmds: toList(defaults.testCmd),
|
|
213
|
+
securityCmds: [],
|
|
214
|
+
requiredTools: [],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve the ordered runner list exactly once per CI run.
|
|
219
|
+
*
|
|
220
|
+
* Precedence (fail closed on ambiguity):
|
|
221
|
+
* 1. --config <path> (or discovered .javi-forge/ci.yaml) → configured runners
|
|
222
|
+
* 2. --stack <stack> → single explicit runner (single-stack repos only)
|
|
223
|
+
* 3. otherwise → single auto-detected runner (zero-config default)
|
|
224
|
+
*/
|
|
225
|
+
export async function resolveCIRunners(projectDir, options = {}) {
|
|
226
|
+
const { config, stack } = options;
|
|
227
|
+
if (config && stack) {
|
|
228
|
+
throw new Error("Ambiguous CI options: --config and --stack cannot be used together. " +
|
|
229
|
+
"Use --config for hybrid repositories.");
|
|
230
|
+
}
|
|
231
|
+
if (stack) {
|
|
232
|
+
if (!CI_STACKS.includes(stack)) {
|
|
233
|
+
throw new Error(`Unknown stack "${stack}". Valid stacks: ${CI_STACKS.join(", ")}`);
|
|
234
|
+
}
|
|
235
|
+
return freezeRunners("stack-override", [
|
|
236
|
+
await resolveExplicitStackRunner(projectDir, stack),
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
const configPath = config ?? (await findCIConfig(projectDir));
|
|
240
|
+
if (configPath) {
|
|
241
|
+
const ciConfig = await loadCIConfig(configPath);
|
|
242
|
+
const runners = [];
|
|
243
|
+
for (const runnerConfig of ciConfig.runners) {
|
|
244
|
+
runners.push(await resolveConfiguredRunner(projectDir, runnerConfig));
|
|
245
|
+
}
|
|
246
|
+
return freezeRunners("config", runners);
|
|
247
|
+
}
|
|
248
|
+
// Zero-config default: single auto-detected runner (unchanged behavior).
|
|
249
|
+
const info = await detectCIStack(projectDir);
|
|
250
|
+
return freezeRunners("auto", [
|
|
251
|
+
freezeRunner({
|
|
252
|
+
name: info.stackType,
|
|
253
|
+
stack: info.stackType,
|
|
254
|
+
buildTool: info.buildTool,
|
|
255
|
+
javaVersion: info.javaVersion,
|
|
256
|
+
directory: ".",
|
|
257
|
+
setupCmds: [],
|
|
258
|
+
lintCmds: toList(info.lintCmd),
|
|
259
|
+
compileCmds: toList(info.compileCmd),
|
|
260
|
+
testCmds: toList(info.testCmd),
|
|
261
|
+
securityCmds: [],
|
|
262
|
+
requiredTools: [],
|
|
263
|
+
}),
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
136
266
|
// =============================================================================
|
|
137
267
|
// GHAGGA check
|
|
138
268
|
// =============================================================================
|
|
@@ -151,20 +281,48 @@ async function isGhaggaAvailable() {
|
|
|
151
281
|
function report(onStep, id, label, status, detail) {
|
|
152
282
|
onStep({ id, label, status, detail });
|
|
153
283
|
}
|
|
284
|
+
/** Detect-step label: legacy format for auto, explicit otherwise. */
|
|
285
|
+
function describeRunners(resolved) {
|
|
286
|
+
const first = resolved.runners[0];
|
|
287
|
+
if (resolved.source === "auto" && first) {
|
|
288
|
+
return `Stack: ${first.stack} (${first.buildTool})`;
|
|
289
|
+
}
|
|
290
|
+
if (resolved.source === "stack-override" && first) {
|
|
291
|
+
return `Stack: ${first.stack} (${first.buildTool}, --stack override)`;
|
|
292
|
+
}
|
|
293
|
+
const summary = resolved.runners
|
|
294
|
+
.map((r) => `${r.name} (${r.stack})`)
|
|
295
|
+
.join(", ");
|
|
296
|
+
return `Config: ${resolved.runners.length} runner(s) — ${summary}`;
|
|
297
|
+
}
|
|
154
298
|
export async function runCI(options, onStep) {
|
|
155
299
|
const { projectDir = process.cwd(), mode = "full", noDocker = false, noGhagga = false, noSecurity = false, timeout = 600, } = options;
|
|
156
|
-
// ──
|
|
300
|
+
// ── Resolve runners (once — nothing downstream re-detects) ─────────────────
|
|
157
301
|
const stepDetect = "detect";
|
|
158
302
|
report(onStep, stepDetect, "Detecting stack", "running");
|
|
159
|
-
let
|
|
303
|
+
let resolved;
|
|
160
304
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
305
|
+
resolved = await resolveCIRunners(projectDir, {
|
|
306
|
+
config: options.config,
|
|
307
|
+
stack: options.stack,
|
|
308
|
+
});
|
|
309
|
+
report(onStep, stepDetect, describeRunners(resolved), "done");
|
|
163
310
|
}
|
|
164
311
|
catch (e) {
|
|
165
312
|
report(onStep, stepDetect, "Detecting stack", "error", String(e));
|
|
166
313
|
throw e;
|
|
167
314
|
}
|
|
315
|
+
// Legacy single-runner view for the zero-config auto path. Keeping this
|
|
316
|
+
// shape guarantees single-stack repositories behave exactly as before.
|
|
317
|
+
const primary = resolved.runners[0];
|
|
318
|
+
const stackInfo = {
|
|
319
|
+
stackType: primary?.stack ?? "node",
|
|
320
|
+
buildTool: primary?.buildTool ?? "npm",
|
|
321
|
+
javaVersion: primary?.javaVersion ?? "21",
|
|
322
|
+
lintCmd: primary?.lintCmds[0] ?? null,
|
|
323
|
+
compileCmd: primary?.compileCmds[0] ?? null,
|
|
324
|
+
testCmd: primary?.testCmds[0] ?? null,
|
|
325
|
+
};
|
|
168
326
|
// ── Detect mode ─────────────────────────────────────────────────────────────
|
|
169
327
|
if (mode === "detect")
|
|
170
328
|
return;
|
|
@@ -175,8 +333,9 @@ export async function runCI(options, onStep) {
|
|
|
175
333
|
throw new Error("--shell requires Docker");
|
|
176
334
|
}
|
|
177
335
|
report(onStep, "docker-image", "Building Docker image", "running");
|
|
336
|
+
let shellImage;
|
|
178
337
|
try {
|
|
179
|
-
await ensureImage({
|
|
338
|
+
shellImage = await ensureImage({
|
|
180
339
|
stack: stackInfo.stackType,
|
|
181
340
|
javaVersion: stackInfo.javaVersion,
|
|
182
341
|
});
|
|
@@ -186,10 +345,14 @@ export async function runCI(options, onStep) {
|
|
|
186
345
|
report(onStep, "docker-image", "Building Docker image", "error", String(e));
|
|
187
346
|
throw e;
|
|
188
347
|
}
|
|
189
|
-
await openShell(projectDir);
|
|
348
|
+
await openShell(projectDir, shellImage);
|
|
190
349
|
return;
|
|
191
350
|
}
|
|
192
351
|
// ── Check Docker ─────────────────────────────────────────────────────────────
|
|
352
|
+
// Image resolved by the prologue for the auto path — threaded into the
|
|
353
|
+
// executor so nothing downstream re-derives it. Set iff
|
|
354
|
+
// `resolved.source === "auto" && !noDocker`.
|
|
355
|
+
let autoImage;
|
|
193
356
|
if (!noDocker) {
|
|
194
357
|
const stepDocker = "docker-check";
|
|
195
358
|
report(onStep, stepDocker, "Checking Docker", "running");
|
|
@@ -199,19 +362,22 @@ export async function runCI(options, onStep) {
|
|
|
199
362
|
throw new Error("Docker is not available");
|
|
200
363
|
}
|
|
201
364
|
report(onStep, stepDocker, "Docker available", "done");
|
|
202
|
-
// Build image
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
365
|
+
// Build image (auto path only — configured runners ensure their own
|
|
366
|
+
// image inside the runner loop)
|
|
367
|
+
if (resolved.source === "auto") {
|
|
368
|
+
const stepImage = "docker-image";
|
|
369
|
+
report(onStep, stepImage, `Building image for ${stackInfo.stackType}`, "running");
|
|
370
|
+
try {
|
|
371
|
+
autoImage = await ensureImage({
|
|
372
|
+
stack: stackInfo.stackType,
|
|
373
|
+
javaVersion: stackInfo.javaVersion,
|
|
374
|
+
});
|
|
375
|
+
report(onStep, stepImage, "Docker image ready", "done");
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
report(onStep, stepImage, "Building Docker image", "error", String(e));
|
|
379
|
+
throw e;
|
|
380
|
+
}
|
|
215
381
|
}
|
|
216
382
|
}
|
|
217
383
|
// ── Refresh .context/ ────────────────────────────────────────────────────────
|
|
@@ -230,46 +396,22 @@ export async function runCI(options, onStep) {
|
|
|
230
396
|
// Non-fatal: context refresh failure should not block CI
|
|
231
397
|
report(onStep, stepContext, "Refresh .context/ directory", "error", String(e));
|
|
232
398
|
}
|
|
233
|
-
// ──
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
report(onStep, stepCompile, `Compile: ${stackInfo.compileCmd}`, "running");
|
|
250
|
-
try {
|
|
251
|
-
// Run as root inside the container to rm/build output dirs owned by any host user,
|
|
252
|
-
// then chown back to runner so subsequent test steps can read the output.
|
|
253
|
-
await runStep(stackInfo.compileCmd, projectDir, noDocker, timeout, "root");
|
|
254
|
-
report(onStep, stepCompile, "Compile passed", "done");
|
|
255
|
-
}
|
|
256
|
-
catch (e) {
|
|
257
|
-
report(onStep, stepCompile, "Compile failed", "error", String(e));
|
|
258
|
-
throw e;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// ── Test (full mode only) ────────────────────────────────────────────────────
|
|
262
|
-
if (mode === "full" && stackInfo.testCmd) {
|
|
263
|
-
const stepTest = "test";
|
|
264
|
-
report(onStep, stepTest, `Test: ${stackInfo.testCmd}`, "running");
|
|
265
|
-
try {
|
|
266
|
-
await runStep(stackInfo.testCmd, projectDir, noDocker, timeout);
|
|
267
|
-
report(onStep, stepTest, "Tests passed", "done");
|
|
268
|
-
}
|
|
269
|
-
catch (e) {
|
|
270
|
-
report(onStep, stepTest, "Tests failed", "error", String(e));
|
|
271
|
-
throw e;
|
|
272
|
-
}
|
|
399
|
+
// ── Runner execution ─────────────────────────────────────────────────────────
|
|
400
|
+
// ONE executor for every resolution source. Naming is a function of
|
|
401
|
+
// `resolved.source` alone — never of `resolved.runners.length`, which would
|
|
402
|
+
// silently rename a single-runner CONFIG.
|
|
403
|
+
const naming = resolved.source === "auto" ? NAMING_MODE.BARE : NAMING_MODE.SUFFIXED;
|
|
404
|
+
for (const runner of resolved.runners) {
|
|
405
|
+
await runRunner(runner, {
|
|
406
|
+
projectDir,
|
|
407
|
+
mode,
|
|
408
|
+
noDocker,
|
|
409
|
+
noSecurity,
|
|
410
|
+
timeout,
|
|
411
|
+
onStep,
|
|
412
|
+
naming,
|
|
413
|
+
preresolvedImage: resolved.source === "auto" ? autoImage : undefined,
|
|
414
|
+
});
|
|
273
415
|
}
|
|
274
416
|
// ── Security scan (full mode only) ──────────────────────────────────────────
|
|
275
417
|
if (mode === "full" && !noSecurity) {
|
|
@@ -313,12 +455,156 @@ export async function runCI(options, onStep) {
|
|
|
313
455
|
// =============================================================================
|
|
314
456
|
// Step runners
|
|
315
457
|
// =============================================================================
|
|
316
|
-
|
|
458
|
+
/**
|
|
459
|
+
* Step-id and label naming, keyed on `resolved.source` and nothing else.
|
|
460
|
+
* `bare` is the zero-config (auto) presentation; `suffixed` carries the runner
|
|
461
|
+
* name, including for a CONFIG that happens to declare exactly one runner.
|
|
462
|
+
*/
|
|
463
|
+
const NAMING_MODE = {
|
|
464
|
+
BARE: "bare",
|
|
465
|
+
SUFFIXED: "suffixed",
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* Execute one resolved runner, whatever its source: resolve the image (already
|
|
469
|
+
* built by the prologue for auto, otherwise the default stack image, an
|
|
470
|
+
* explicit/digest-pinned image, or a deterministic build-context build), verify
|
|
471
|
+
* required tools fail-closed, then run setup/lint/compile/test/security
|
|
472
|
+
* commands in order, inside the runner's working directory.
|
|
473
|
+
* Any failure aborts the whole run — nothing is ever skipped silently.
|
|
474
|
+
*/
|
|
475
|
+
async function runRunner(runner, ctx) {
|
|
476
|
+
const { projectDir, mode, noDocker, noSecurity, timeout, onStep, naming } = ctx;
|
|
477
|
+
const bare = naming === NAMING_MODE.BARE;
|
|
478
|
+
// ── Image resolution (Docker mode only) ────────────────────────────────────
|
|
479
|
+
let imageName;
|
|
480
|
+
if (!noDocker) {
|
|
481
|
+
if (ctx.preresolvedImage) {
|
|
482
|
+
// Auto path: the prologue already built the image BEFORE the .context/
|
|
483
|
+
// refresh. Reuse it verbatim so no second `docker-image` step appears
|
|
484
|
+
// and the global step order is unchanged.
|
|
485
|
+
imageName = ctx.preresolvedImage;
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const stepImage = `docker-image:${runner.name}`;
|
|
489
|
+
try {
|
|
490
|
+
if (runner.buildContext) {
|
|
491
|
+
report(onStep, stepImage, `Building image for ${runner.name} from ${runner.buildContext}`, "running");
|
|
492
|
+
imageName = await ensureImage({
|
|
493
|
+
stack: runner.stack,
|
|
494
|
+
buildContext: path.resolve(projectDir, runner.buildContext),
|
|
495
|
+
imageTag: `javi-forge-ci-${runner.name}`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
else if (runner.image) {
|
|
499
|
+
// Explicit image (digest pins pass through verbatim).
|
|
500
|
+
imageName = runner.image;
|
|
501
|
+
report(onStep, stepImage, `Using configured image ${runner.image}`, "done");
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
report(onStep, stepImage, `Building image for ${runner.name} (${runner.stack})`, "running");
|
|
505
|
+
imageName = await ensureImage({
|
|
506
|
+
stack: runner.stack,
|
|
507
|
+
javaVersion: runner.javaVersion,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (!runner.image) {
|
|
511
|
+
report(onStep, stepImage, `Docker image ready (${imageName})`, "done");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
report(onStep, stepImage, "Building Docker image", "error", String(e));
|
|
516
|
+
throw e;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// ── Required tools (fail closed, before any phase) ─────────────────────────
|
|
521
|
+
if (runner.requiredTools.length > 0) {
|
|
522
|
+
const stepTools = `tools:${runner.name}`;
|
|
523
|
+
const envDesc = noDocker
|
|
524
|
+
? `native environment (directory "${runner.directory}")`
|
|
525
|
+
: `image ${imageName}`;
|
|
526
|
+
report(onStep, stepTools, `Checking required tools [${runner.name}]: ${runner.requiredTools.join(", ")}`, "running");
|
|
527
|
+
for (const tool of runner.requiredTools) {
|
|
528
|
+
try {
|
|
529
|
+
await runStep({
|
|
530
|
+
command: `command -v ${tool}`,
|
|
531
|
+
projectDir,
|
|
532
|
+
noDocker,
|
|
533
|
+
timeout,
|
|
534
|
+
runner,
|
|
535
|
+
image: imageName,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
const message = `runner "${runner.name}": required tool "${tool}" not found in ${envDesc} — ` +
|
|
540
|
+
"install it in the runner image/environment or remove it from requires";
|
|
541
|
+
report(onStep, stepTools, `Missing required tool [${runner.name}]: ${tool}`, "error", message);
|
|
542
|
+
throw new Error(message);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
report(onStep, stepTools, `Required tools OK [${runner.name}]`, "done");
|
|
546
|
+
}
|
|
547
|
+
const phases = [
|
|
548
|
+
{ id: "setup", label: "Setup", cmds: runner.setupCmds, skip: false },
|
|
549
|
+
{ id: "lint", label: "Lint", cmds: runner.lintCmds, skip: false },
|
|
550
|
+
{
|
|
551
|
+
id: "compile",
|
|
552
|
+
label: "Compile",
|
|
553
|
+
cmds: runner.compileCmds,
|
|
554
|
+
user: "root",
|
|
555
|
+
skip: false,
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: "test",
|
|
559
|
+
label: "Test",
|
|
560
|
+
doneLabel: "Tests",
|
|
561
|
+
cmds: runner.testCmds,
|
|
562
|
+
skip: mode !== "full",
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: "security",
|
|
566
|
+
label: "Security",
|
|
567
|
+
cmds: runner.securityCmds,
|
|
568
|
+
skip: mode !== "full" || noSecurity,
|
|
569
|
+
},
|
|
570
|
+
];
|
|
571
|
+
for (const phase of phases) {
|
|
572
|
+
if (phase.skip)
|
|
573
|
+
continue;
|
|
574
|
+
const stepId = bare ? phase.id : `${phase.id}:${runner.name}`;
|
|
575
|
+
// `doneLabel` is BARE-only: suffixed mode keeps the historical
|
|
576
|
+
// `${label} [${name}] passed` composition.
|
|
577
|
+
const subject = bare ? (phase.doneLabel ?? phase.label) : phase.label;
|
|
578
|
+
const suffix = bare ? "" : ` [${runner.name}]`;
|
|
579
|
+
for (const cmd of phase.cmds) {
|
|
580
|
+
report(onStep, stepId, `${phase.label}${suffix}: ${cmd}`, "running");
|
|
581
|
+
try {
|
|
582
|
+
await runStep({
|
|
583
|
+
command: cmd,
|
|
584
|
+
projectDir,
|
|
585
|
+
noDocker,
|
|
586
|
+
timeout,
|
|
587
|
+
runner,
|
|
588
|
+
user: phase.user,
|
|
589
|
+
image: imageName,
|
|
590
|
+
});
|
|
591
|
+
report(onStep, stepId, `${subject}${suffix} passed`, "done");
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
report(onStep, stepId, `${subject}${suffix} failed`, "error", String(e));
|
|
595
|
+
throw e;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async function runStep(options) {
|
|
601
|
+
const { command, projectDir, noDocker, timeout, runner, user, image } = options;
|
|
317
602
|
if (noDocker) {
|
|
318
|
-
// Run natively
|
|
603
|
+
// Run natively, in the runner's working directory.
|
|
604
|
+
const cwd = path.join(projectDir, runner.directory);
|
|
319
605
|
await new Promise((resolve, reject) => {
|
|
320
606
|
const proc = spawn("bash", ["-c", command], {
|
|
321
|
-
cwd
|
|
607
|
+
cwd,
|
|
322
608
|
stdio: "inherit",
|
|
323
609
|
env: { ...process.env, CI: "true" },
|
|
324
610
|
});
|
|
@@ -329,9 +615,22 @@ async function runStep(command, projectDir, noDocker, timeout, user) {
|
|
|
329
615
|
});
|
|
330
616
|
}
|
|
331
617
|
else {
|
|
618
|
+
// The image is always resolved upstream (prologue build for auto,
|
|
619
|
+
// per-runner ensureImage, explicit config image, or the default
|
|
620
|
+
// per-stack image). Docker receives it verbatim and never re-detects
|
|
621
|
+
// anything, so a missing image is a bug — fail loudly instead of
|
|
622
|
+
// silently re-deriving a name that may not be the one that was built.
|
|
623
|
+
if (!image) {
|
|
624
|
+
throw new Error(`internal invariant violated: no Docker image resolved for runner "${runner.name}" — ` +
|
|
625
|
+
"the image must be resolved before any step runs");
|
|
626
|
+
}
|
|
627
|
+
const workdir = runner.directory !== "."
|
|
628
|
+
? `/home/runner/work/${runner.directory}`
|
|
629
|
+
: "/home/runner/work";
|
|
332
630
|
const result = await runInContainer({
|
|
333
631
|
projectDir,
|
|
334
|
-
|
|
632
|
+
image,
|
|
633
|
+
command: `cd ${workdir} && ${command}`,
|
|
335
634
|
timeout,
|
|
336
635
|
stream: true,
|
|
337
636
|
user,
|
|
@@ -469,6 +768,26 @@ export async function installCIHooks(projectDir) {
|
|
|
469
768
|
for (const hook of hooks) {
|
|
470
769
|
const hookPath = path.join(hooksDir, hook.name);
|
|
471
770
|
try {
|
|
771
|
+
// Refuse to follow symlinks. .git/hooks/<name> as a symlink to a
|
|
772
|
+
// sensitive file (e.g., ~/.ssh/authorized_keys) would otherwise be
|
|
773
|
+
// clobbered by writeFile. lstat reports the link, not the target.
|
|
774
|
+
let isSymlink = false;
|
|
775
|
+
try {
|
|
776
|
+
const stat = await fs.lstat(hookPath);
|
|
777
|
+
isSymlink = stat.isSymbolicLink();
|
|
778
|
+
}
|
|
779
|
+
catch (statErr) {
|
|
780
|
+
const code = statErr && typeof statErr === "object" && "code" in statErr
|
|
781
|
+
? statErr.code
|
|
782
|
+
: "";
|
|
783
|
+
if (code !== "ENOENT") {
|
|
784
|
+
throw statErr;
|
|
785
|
+
}
|
|
786
|
+
// ENOENT is expected if the hook file doesn't exist yet.
|
|
787
|
+
}
|
|
788
|
+
if (isSymlink) {
|
|
789
|
+
throw new Error(`refusing to write through a symlink at ${hookPath}`);
|
|
790
|
+
}
|
|
472
791
|
await fs.writeFile(hookPath, hook.content, { mode: 0o755 });
|
|
473
792
|
installed.push(hook.name);
|
|
474
793
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { promisify } from "util";
|
|
5
3
|
import { FORGE_ROOT, MODULES_DIR, PLUGINS_DIR, TEMPLATES_DIR, } from "../constants.js";
|
|
6
4
|
import { detectStack } from "../lib/common.js";
|
|
7
5
|
import { refreshContextDir } from "../lib/context.js";
|
|
6
|
+
import { execFileAsync } from "../lib/exec.js";
|
|
8
7
|
import { listInstalledPlugins } from "../lib/plugin.js";
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
8
|
/** Resolve a binary name to its full path, returns null if not found */
|
|
11
9
|
async function which(bin) {
|
|
12
10
|
try {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InitStep } from "../../types/index.js";
|
|
2
|
+
import type { StepCallback } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Emit a step progress event via the callback.
|
|
5
|
+
*
|
|
6
|
+
* Centralized helper extracted during the init.ts split. Functionally
|
|
7
|
+
* identical to the original inline report function, with an explicit
|
|
8
|
+
* `void` return type added for type clarity. Behavior MUST stay
|
|
9
|
+
* compatible so tests asserting on emitted steps[] (and the ordering
|
|
10
|
+
* contract test) keep passing through the PR 1-6 extraction.
|
|
11
|
+
*/
|
|
12
|
+
export declare function report(onStep: StepCallback, id: string, label: string, status: InitStep["status"], detail?: string): void;
|
|
13
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit a step progress event via the callback.
|
|
3
|
+
*
|
|
4
|
+
* Centralized helper extracted during the init.ts split. Functionally
|
|
5
|
+
* identical to the original inline report function, with an explicit
|
|
6
|
+
* `void` return type added for type clarity. Behavior MUST stay
|
|
7
|
+
* compatible so tests asserting on emitted steps[] (and the ordering
|
|
8
|
+
* contract test) keep passing through the PR 1-6 extraction.
|
|
9
|
+
*/
|
|
10
|
+
export function report(onStep, id, label, status, detail) {
|
|
11
|
+
onStep({ id, label, status, detail });
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StepFn } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Step 17: Generate Agent Skills manifest (skills.json).
|
|
4
|
+
*
|
|
5
|
+
* - In dry-run mode, reports what would be created without touching disk.
|
|
6
|
+
* - Otherwise writes <projectDir>/<AGENT_SKILLS_MANIFEST_FILE> with an empty
|
|
7
|
+
* skills array unless the file already exists (idempotent).
|
|
8
|
+
* - Errors are swallowed and reported as status:"error" — never thrown.
|
|
9
|
+
*
|
|
10
|
+
* Extracted VERBATIM from src/commands/init.ts (PR 6 of 6).
|
|
11
|
+
*/
|
|
12
|
+
export declare const stepAgentSkills: StepFn;
|
|
13
|
+
//# sourceMappingURL=agent-skills.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { AGENT_SKILLS_MANIFEST_FILE } from "../../../constants.js";
|
|
4
|
+
import { report } from "../report.js";
|
|
5
|
+
/**
|
|
6
|
+
* Step 17: Generate Agent Skills manifest (skills.json).
|
|
7
|
+
*
|
|
8
|
+
* - In dry-run mode, reports what would be created without touching disk.
|
|
9
|
+
* - Otherwise writes <projectDir>/<AGENT_SKILLS_MANIFEST_FILE> with an empty
|
|
10
|
+
* skills array unless the file already exists (idempotent).
|
|
11
|
+
* - Errors are swallowed and reported as status:"error" — never thrown.
|
|
12
|
+
*
|
|
13
|
+
* Extracted VERBATIM from src/commands/init.ts (PR 6 of 6).
|
|
14
|
+
*/
|
|
15
|
+
export const stepAgentSkills = async (ctx) => {
|
|
16
|
+
const { projectDir, dryRun, onStep, options } = ctx;
|
|
17
|
+
const { projectName } = options;
|
|
18
|
+
const stepId = "agent-skills";
|
|
19
|
+
report(onStep, stepId, "Generate Agent Skills manifest (skills.json)", "running");
|
|
20
|
+
try {
|
|
21
|
+
if (!dryRun) {
|
|
22
|
+
const skillsManifest = {
|
|
23
|
+
name: projectName,
|
|
24
|
+
version: "0.1.0",
|
|
25
|
+
description: `Agent Skills manifest for ${projectName}`,
|
|
26
|
+
skills: [],
|
|
27
|
+
};
|
|
28
|
+
const skillsJsonPath = path.join(projectDir, AGENT_SKILLS_MANIFEST_FILE);
|
|
29
|
+
if (!(await fs.pathExists(skillsJsonPath))) {
|
|
30
|
+
await fs.writeJson(skillsJsonPath, skillsManifest, { spaces: 2 });
|
|
31
|
+
report(onStep, stepId, "Generate Agent Skills manifest (skills.json)", "done", AGENT_SKILLS_MANIFEST_FILE);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
report(onStep, stepId, "Generate Agent Skills manifest (skills.json)", "done", "already exists");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
report(onStep, stepId, "Generate Agent Skills manifest (skills.json)", "done", `dry-run: would generate ${AGENT_SKILLS_MANIFEST_FILE}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
report(onStep, stepId, "Generate Agent Skills manifest (skills.json)", "error", String(e));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=agent-skills.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StepFn } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Step 7: AI config sync (delegated to javi-ai).
|
|
4
|
+
*
|
|
5
|
+
* - When aiSync is false, reports "skipped".
|
|
6
|
+
* - When dryRun, reports a done message without invoking javi-ai.
|
|
7
|
+
* - Otherwise shells out to `npx javi-ai sync --project-dir <projectDir> --target all`.
|
|
8
|
+
* - Detects soft failures via stderr (e.g. "Raw mode is not supported", "ERROR").
|
|
9
|
+
* - Hard failures (ENOENT, not found, ERR_MODULE_NOT_FOUND) report a helpful install hint.
|
|
10
|
+
* - Errors are swallowed and reported as status:"error" — never thrown.
|
|
11
|
+
*
|
|
12
|
+
* Extracted VERBATIM from src/commands/init.ts (PR 3 of 6).
|
|
13
|
+
*/
|
|
14
|
+
export declare const stepAISync: StepFn;
|
|
15
|
+
//# sourceMappingURL=ai-sync.d.ts.map
|