infernoflow 0.32.8 → 0.32.9

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.
Files changed (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -321
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -1,363 +1,6 @@
1
- /**
2
- * infernoflow test
3
- *
4
- * Run registered scenarios for a capability and report pass/fail.
5
- * When no test runner is configured, generates a minimal ad-hoc harness
6
- * from the scenario + source file and runs it with Node.
7
- *
8
- * Usage:
9
- * infernoflow test Run all caps with scenarios
10
- * infernoflow test user-auth Run scenarios for one cap
11
- * infernoflow test user-auth payment-process Run multiple caps
12
- * infernoflow test --all Run every cap (including no-scenario)
13
- * infernoflow test --generate user-auth Print generated test file, don't run
14
- * infernoflow test --json Machine-readable output
15
- * infernoflow test --bail Stop on first failure
16
- */
17
-
18
- import * as fs from "node:fs";
19
- import * as path from "node:path";
20
- import * as os from "node:os";
21
- import { execSync, spawnSync } from "node:child_process";
22
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
23
-
24
- // ── helpers ───────────────────────────────────────────────────────────────────
25
-
26
- function loadJson(p) {
27
- try { return JSON.parse(fs.readFileSync(p, "utf8")); }
28
- catch { return null; }
29
- }
30
-
31
- function stability(cap) { return cap?.stability || "experimental"; }
32
-
33
- const PASS_ICON = green("✓");
34
- const FAIL_ICON = red("✗");
35
- const SKIP_ICON = gray("○");
36
-
37
- // ── scenario loader ───────────────────────────────────────────────────────────
38
-
39
- function loadScenarios(capId, infernoDir) {
40
- const dir = path.join(infernoDir, "scenarios");
41
- if (!fs.existsSync(dir)) return [];
42
- const found = [];
43
- for (const f of fs.readdirSync(dir)) {
44
- if (!f.endsWith(".json")) continue;
45
- try {
46
- const s = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
47
- const covered = s.capabilitiesCovered || s.capabilities || [];
48
- if (covered.some(c => c.toLowerCase() === capId.toLowerCase())) {
49
- found.push({ ...s, _file: f });
50
- }
51
- } catch {}
52
- }
53
- return found;
54
- }
55
-
56
- // ── test runner detection ─────────────────────────────────────────────────────
57
-
58
- function detectTestRunner(cwd) {
59
- const pkg = loadJson(path.join(cwd, "package.json"));
60
- if (!pkg) return null;
61
-
62
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
63
- if (deps?.vitest) return "vitest";
64
- if (deps?.jest) return "jest";
65
- if (deps?.mocha) return "mocha";
66
- if (pkg.scripts?.test && !pkg.scripts.test.includes("no test")) {
67
- return { custom: pkg.scripts.test };
68
- }
69
- return null;
70
- }
71
-
72
- // ── ad-hoc test generator ─────────────────────────────────────────────────────
73
-
74
- function generateAdHocTest(capId, cap, scenario, scanEntry) {
75
- const name = cap.name || cap.title || capId;
76
- const desc = cap.description || "(no description)";
77
- const files = scanEntry?.codeAnalysis?.sourceFiles || [];
78
- const fns = scanEntry?.codeAnalysis?.functions || [];
79
- const steps = scenario?.steps || scenario?.actions || [];
80
- const expects = scenario?.expects || scenario?.assertions || [];
81
-
82
- const lines = [
83
- `// Auto-generated smoke test for: ${capId}`,
84
- `// Generated by infernoflow test — edit as needed`,
85
- ``,
86
- `import { strict as assert } from "node:assert";`,
87
- ``,
88
- `// Capability: ${name}`,
89
- `// ${desc}`,
90
- ``,
91
- ];
92
-
93
- // Import source if we know the file
94
- if (files.length) {
95
- lines.push(`// Source: ${files[0]}`);
96
- lines.push(`// import { ${fns[0] || capId} } from "./${path.basename(files[0])}";`);
97
- lines.push(``);
98
- }
99
-
100
- lines.push(`async function run() {`);
101
- lines.push(` const results = [];`);
102
- lines.push(``);
103
-
104
- // Generate test cases from scenario steps
105
- if (steps.length) {
106
- steps.forEach((step, i) => {
107
- const desc = typeof step === "string" ? step : (step.action || step.description || `step ${i + 1}`);
108
- lines.push(` // Step ${i + 1}: ${desc}`);
109
- lines.push(` results.push({ step: ${JSON.stringify(desc)}, status: "manual" });`);
110
- lines.push(``);
111
- });
112
- } else {
113
- lines.push(` // No explicit steps — running basic smoke test`);
114
- lines.push(` results.push({ step: "capability exists", status: "pass" });`);
115
- lines.push(``);
116
- }
117
-
118
- // Add assertion checks
119
- if (expects.length) {
120
- expects.forEach((exp, i) => {
121
- const desc = typeof exp === "string" ? exp : (exp.condition || exp.description || `assertion ${i + 1}`);
122
- lines.push(` // Assert: ${desc}`);
123
- lines.push(` // assert(condition, ${JSON.stringify(desc)});`);
124
- });
125
- lines.push(``);
126
- }
127
-
128
- lines.push(` return results;`);
129
- lines.push(`}`);
130
- lines.push(``);
131
- lines.push(`run().then(results => {`);
132
- lines.push(` const failed = results.filter(r => r.status === "fail");`);
133
- lines.push(` results.forEach(r => {`);
134
- lines.push(` const icon = r.status === "pass" ? "✓" : r.status === "fail" ? "✗" : "○";`);
135
- lines.push(` console.log(\` \${icon} \${r.step}\`);`);
136
- lines.push(` });`);
137
- lines.push(` if (failed.length) { console.error(\`\\n \${failed.length} failed\`); process.exit(1); }`);
138
- lines.push(` else console.log(\`\\n All steps passed\`);`);
139
- lines.push(`}).catch(err => { console.error(err); process.exit(1); });`);
140
-
141
- return lines.join("\n");
142
- }
143
-
144
- // ── run a single scenario ─────────────────────────────────────────────────────
145
-
146
- function runScenario(capId, cap, scenario, scanEntry, cwd) {
147
- const scenarioId = scenario?.scenarioId || scenario?.id || scenario?._file?.replace(".json", "") || "unnamed";
148
- const runner = detectTestRunner(cwd);
149
-
150
- // If there are testFiles registered in the scenario, run those
151
- const testFiles = scenario?.testFiles || scenario?.testFile ? [scenario.testFile].flat().filter(Boolean) : [];
152
- if (testFiles.length) {
153
- for (const tf of testFiles) {
154
- const absPath = path.resolve(cwd, tf);
155
- if (!fs.existsSync(absPath)) {
156
- return { scenarioId, status: "skip", reason: `test file not found: ${tf}` };
157
- }
158
- }
159
-
160
- // Run with detected test runner
161
- if (runner && typeof runner === "string") {
162
- const cmd = runner === "vitest"
163
- ? `npx vitest run ${testFiles.join(" ")} --reporter verbose`
164
- : runner === "jest"
165
- ? `npx jest ${testFiles.join(" ")} --no-coverage`
166
- : runner === "mocha"
167
- ? `npx mocha ${testFiles.join(" ")}`
168
- : null;
169
-
170
- if (cmd) {
171
- const result = spawnSync(cmd, { shell: true, cwd, encoding: "utf8", timeout: 60_000 });
172
- const passed = result.status === 0;
173
- return {
174
- scenarioId,
175
- status: passed ? "pass" : "fail",
176
- output: (result.stdout || "") + (result.stderr || ""),
177
- runner,
178
- };
179
- }
180
- }
181
-
182
- // Custom script runner
183
- if (runner?.custom) {
184
- const result = spawnSync(runner.custom, { shell: true, cwd, encoding: "utf8", timeout: 60_000 });
185
- return {
186
- scenarioId,
187
- status: result.status === 0 ? "pass" : "fail",
188
- output: (result.stdout || "") + (result.stderr || ""),
189
- runner: "npm test",
190
- };
191
- }
192
- }
193
-
194
- // No test files — generate and run ad-hoc test
195
- const testSrc = generateAdHocTest(capId, cap, scenario, scanEntry);
196
- const tmpFile = path.join(os.tmpdir(), `infernoflow-test-${capId}-${Date.now()}.mjs`);
197
-
198
- try {
199
- fs.writeFileSync(tmpFile, testSrc);
200
- const result = spawnSync(process.execPath, [tmpFile], {
201
- cwd,
202
- encoding: "utf8",
203
- timeout: 30_000,
204
- });
205
- const passed = result.status === 0;
206
- return {
207
- scenarioId,
208
- status: passed ? "pass" : "fail",
209
- output: (result.stdout || "") + (result.stderr || ""),
210
- runner: "ad-hoc",
211
- generated: true,
212
- };
213
- } finally {
214
- try { fs.unlinkSync(tmpFile); } catch {}
215
- }
216
- }
217
-
218
- // ── print result ──────────────────────────────────────────────────────────────
219
-
220
- function printCapResult(capId, cap, results, verbose) {
221
- const level = stability(cap);
222
- const badge = level === "frozen" ? red("frozen") : level === "stable" ? yellow("stable") : gray("experimental");
223
- const total = results.length;
224
- const passed = results.filter(r => r.status === "pass").length;
225
- const failed = results.filter(r => r.status === "fail").length;
226
- const skipped = results.filter(r => r.status === "skip").length;
227
-
228
- const statusIcon = failed > 0 ? red("✗") : passed > 0 ? green("✓") : gray("○");
229
- console.log(` ${statusIcon} ${bold(capId)} ${gray(`[${badge}]`)}`);
230
-
231
- for (const r of results) {
232
- const icon = r.status === "pass" ? PASS_ICON : r.status === "fail" ? FAIL_ICON : SKIP_ICON;
233
- const note = r.generated ? gray(" (generated)") : r.runner ? gray(` (${r.runner})`) : "";
234
- console.log(` ${icon} ${gray(r.scenarioId)}${note}`);
235
- if (r.reason) console.log(` ${gray(r.reason)}`);
236
- if (verbose && r.output) {
237
- const trimmed = r.output.trim().split("\n").slice(0, 10).join("\n");
238
- console.log(trimmed.split("\n").map(l => ` ${gray(l)}`).join("\n"));
239
- }
240
- }
241
-
242
- return { total, passed, failed, skipped };
243
- }
244
-
245
- // ── entry point ───────────────────────────────────────────────────────────────
246
-
247
- export async function testCommand(rawArgs) {
248
- const args = (rawArgs || []).slice(1);
249
- const jsonMode = args.includes("--json");
250
- const bail = args.includes("--bail");
251
- const generateMode = args.includes("--generate");
252
- const runAll = args.includes("--all");
253
- const verbose = args.includes("--verbose") || args.includes("-v");
254
-
255
- const capArgs = args.filter(a => !a.startsWith("--") && a !== "-v");
256
-
257
- const cwd = process.cwd();
258
- const infernoDir = path.join(cwd, "inferno");
259
-
260
- if (!fs.existsSync(infernoDir)) {
261
- if (!jsonMode) console.error(red("✗ inferno/ not found. Run: infernoflow init"));
262
- process.exit(1);
263
- }
264
-
265
- // Load capabilities
266
- let allCaps = [];
267
- const rawCaps = loadJson(path.join(infernoDir, "capabilities.json"));
268
- if (rawCaps) allCaps = Array.isArray(rawCaps) ? rawCaps : (rawCaps.capabilities || []);
269
-
270
- const scanData = loadJson(path.join(infernoDir, "scan.json"));
271
-
272
- // Determine which caps to test
273
- let targetCaps;
274
- if (capArgs.length) {
275
- targetCaps = capArgs.map(id => {
276
- const cap = allCaps.find(c => c.id === id);
277
- if (!cap) {
278
- if (!jsonMode) console.error(red(`✗ Capability "${id}" not found in capabilities.json`));
279
- process.exit(1);
280
- }
281
- return cap;
282
- });
283
- } else if (runAll) {
284
- targetCaps = allCaps;
285
- } else {
286
- // Default: caps that have scenarios registered
287
- targetCaps = allCaps.filter(cap => loadScenarios(cap.id, infernoDir).length > 0);
288
- if (!targetCaps.length) {
289
- if (!jsonMode) {
290
- console.log();
291
- console.log(` ${gray("No scenarios registered. Use --all to test all capabilities, or")} `);
292
- console.log(` ${gray("add scenarios to inferno/scenarios/ first.")}`);
293
- console.log();
294
- }
295
- process.exit(0);
296
- }
297
- }
298
-
299
- if (!jsonMode) {
300
- console.log();
301
- console.log(` ${bold("🧪 infernoflow test")}`);
302
- console.log();
303
- }
304
-
305
- // --generate mode: print generated test for first cap
306
- if (generateMode) {
307
- const cap = targetCaps[0];
308
- const scenarios = loadScenarios(cap.id, infernoDir);
309
- const scenario = scenarios[0] || {};
310
- const scanEntry = scanData?.capabilities?.find(c => c.id === cap.id);
311
- const src = generateAdHocTest(cap.id, cap, scenario, scanEntry);
312
- console.log(src);
313
- return;
314
- }
315
-
316
- // Run tests
317
- const summary = { total: 0, passed: 0, failed: 0, skipped: 0, caps: [] };
318
- let bailed = false;
319
-
320
- for (const cap of targetCaps) {
321
- const scenarios = loadScenarios(cap.id, infernoDir);
322
- const scanEntry = scanData?.capabilities?.find(c => c.id === cap.id);
323
-
324
- let capResults = [];
325
-
326
- if (!scenarios.length) {
327
- capResults = [{ scenarioId: "(no scenarios)", status: "skip", reason: "register scenarios in inferno/scenarios/" }];
328
- } else {
329
- for (const scenario of scenarios) {
330
- const r = runScenario(cap.id, cap, scenario, scanEntry, cwd);
331
- capResults.push(r);
332
- if (bail && r.status === "fail") { bailed = true; break; }
333
- }
334
- }
335
-
336
- const counts = printCapResult(cap.id, cap, capResults, verbose);
337
- summary.total += counts.total;
338
- summary.passed += counts.passed;
339
- summary.failed += counts.failed;
340
- summary.skipped += counts.skipped;
341
- summary.caps.push({ id: cap.id, stability: stability(cap), results: capResults });
342
-
343
- if (bailed) break;
344
- }
345
-
346
- if (!jsonMode) {
347
- console.log();
348
- const statusColor = summary.failed > 0 ? red : summary.passed > 0 ? green : gray;
349
- console.log(` ${statusColor(bold(String(summary.passed)))} passed ${summary.failed > 0 ? red(bold(String(summary.failed))) : gray("0")} failed ${gray(String(summary.skipped))} skipped`);
350
- if (bailed) console.log(` ${yellow("(bailed on first failure)")}`);
351
- console.log();
352
- if (!summary.failed) {
353
- console.log(gray(" ── infernoflow test complete"));
354
- }
355
- console.log();
356
- }
357
-
358
- if (jsonMode) {
359
- console.log(JSON.stringify(summary, null, 2));
360
- }
361
-
362
- process.exit(summary.failed > 0 ? 1 : 0);
363
- }
1
+ import*as y from"node:fs";import*as $ from"node:path";import*as E from"node:os";import{spawnSync as w}from"node:child_process";import{bold as A,gray as f,green as F,yellow as I,red as b}from"../ui/output.mjs";function N(i){try{return JSON.parse(y.readFileSync(i,"utf8"))}catch{return null}}function J(i){return i?.stability||"experimental"}const P=F("\u2713"),R=b("\u2717"),L=f("\u25CB");function O(i,n){const e=$.join(n,"scenarios");if(!y.existsSync(e))return[];const g=[];for(const c of y.readdirSync(e))if(c.endsWith(".json"))try{const p=JSON.parse(y.readFileSync($.join(e,c),"utf8"));(p.capabilitiesCovered||p.capabilities||[]).some(a=>a.toLowerCase()===i.toLowerCase())&&g.push({...p,_file:c})}catch{}return g}function z(i){const n=N($.join(i,"package.json"));if(!n)return null;const e={...n.dependencies,...n.devDependencies};return e?.vitest?"vitest":e?.jest?"jest":e?.mocha?"mocha":n.scripts?.test&&!n.scripts.test.includes("no test")?{custom:n.scripts.test}:null}function D(i,n,e,g){const c=n.name||n.title||i,p=n.description||"(no description)",r=g?.codeAnalysis?.sourceFiles||[],a=g?.codeAnalysis?.functions||[],m=e?.steps||e?.actions||[],u=e?.expects||e?.assertions||[],s=[`// Auto-generated smoke test for: ${i}`,"// Generated by infernoflow test \u2014 edit as needed","",'import { strict as assert } from "node:assert";',"",`// Capability: ${c}`,`// ${p}`,""];return r.length&&(s.push(`// Source: ${r[0]}`),s.push(`// import { ${a[0]||i} } from "./${$.basename(r[0])}";`),s.push("")),s.push("async function run() {"),s.push(" const results = [];"),s.push(""),m.length?m.forEach((t,h)=>{const d=typeof t=="string"?t:t.action||t.description||`step ${h+1}`;s.push(` // Step ${h+1}: ${d}`),s.push(` results.push({ step: ${JSON.stringify(d)}, status: "manual" });`),s.push("")}):(s.push(" // No explicit steps \u2014 running basic smoke test"),s.push(' results.push({ step: "capability exists", status: "pass" });'),s.push("")),u.length&&(u.forEach((t,h)=>{const d=typeof t=="string"?t:t.condition||t.description||`assertion ${h+1}`;s.push(` // Assert: ${d}`),s.push(` // assert(condition, ${JSON.stringify(d)});`)}),s.push("")),s.push(" return results;"),s.push("}"),s.push(""),s.push("run().then(results => {"),s.push(' const failed = results.filter(r => r.status === "fail");'),s.push(" results.forEach(r => {"),s.push(' const icon = r.status === "pass" ? "\u2713" : r.status === "fail" ? "\u2717" : "\u25CB";'),s.push(" console.log(` ${icon} ${r.step}`);"),s.push(" });"),s.push(" if (failed.length) { console.error(`\\n ${failed.length} failed`); process.exit(1); }"),s.push(" else console.log(`\\n All steps passed`);"),s.push("}).catch(err => { console.error(err); process.exit(1); });"),s.join(`
2
+ `)}function M(i,n,e,g,c){const p=e?.scenarioId||e?.id||e?._file?.replace(".json","")||"unnamed",r=z(c),a=e?.testFiles||e?.testFile?[e.testFile].flat().filter(Boolean):[];if(a.length){for(const s of a){const t=$.resolve(c,s);if(!y.existsSync(t))return{scenarioId:p,status:"skip",reason:`test file not found: ${s}`}}if(r&&typeof r=="string"){const s=r==="vitest"?`npx vitest run ${a.join(" ")} --reporter verbose`:r==="jest"?`npx jest ${a.join(" ")} --no-coverage`:r==="mocha"?`npx mocha ${a.join(" ")}`:null;if(s){const t=w(s,{shell:!0,cwd:c,encoding:"utf8",timeout:6e4}),h=t.status===0;return{scenarioId:p,status:h?"pass":"fail",output:(t.stdout||"")+(t.stderr||""),runner:r}}}if(r?.custom){const s=w(r.custom,{shell:!0,cwd:c,encoding:"utf8",timeout:6e4});return{scenarioId:p,status:s.status===0?"pass":"fail",output:(s.stdout||"")+(s.stderr||""),runner:"npm test"}}}const m=D(i,n,e,g),u=$.join(E.tmpdir(),`infernoflow-test-${i}-${Date.now()}.mjs`);try{y.writeFileSync(u,m);const s=w(process.execPath,[u],{cwd:c,encoding:"utf8",timeout:3e4}),t=s.status===0;return{scenarioId:p,status:t?"pass":"fail",output:(s.stdout||"")+(s.stderr||""),runner:"ad-hoc",generated:!0}}finally{try{y.unlinkSync(u)}catch{}}}function T(i,n,e,g){const c=J(n),p=c==="frozen"?b("frozen"):c==="stable"?I("stable"):f("experimental"),r=e.length,a=e.filter(t=>t.status==="pass").length,m=e.filter(t=>t.status==="fail").length,u=e.filter(t=>t.status==="skip").length,s=m>0?b("\u2717"):a>0?F("\u2713"):f("\u25CB");console.log(` ${s} ${A(i)} ${f(`[${p}]`)}`);for(const t of e){const h=t.status==="pass"?P:t.status==="fail"?R:L,d=t.generated?f(" (generated)"):t.runner?f(` (${t.runner})`):"";if(console.log(` ${h} ${f(t.scenarioId)}${d}`),t.reason&&console.log(` ${f(t.reason)}`),g&&t.output){const l=t.output.trim().split(`
3
+ `).slice(0,10).join(`
4
+ `);console.log(l.split(`
5
+ `).map(k=>` ${f(k)}`).join(`
6
+ `))}}return{total:r,passed:a,failed:m,skipped:u}}async function K(i){const n=(i||[]).slice(1),e=n.includes("--json"),g=n.includes("--bail"),c=n.includes("--generate"),p=n.includes("--all"),r=n.includes("--verbose")||n.includes("-v"),a=n.filter(o=>!o.startsWith("--")&&o!=="-v"),m=process.cwd(),u=$.join(m,"inferno");y.existsSync(u)||(e||console.error(b("\u2717 inferno/ not found. Run: infernoflow init")),process.exit(1));let s=[];const t=N($.join(u,"capabilities.json"));t&&(s=Array.isArray(t)?t:t.capabilities||[]);const h=N($.join(u,"scan.json"));let d;if(a.length?d=a.map(o=>{const j=s.find(v=>v.id===o);return j||(e||console.error(b(`\u2717 Capability "${o}" not found in capabilities.json`)),process.exit(1)),j}):p?d=s:(d=s.filter(o=>O(o.id,u).length>0),d.length||(e||(console.log(),console.log(` ${f("No scenarios registered. Use --all to test all capabilities, or")} `),console.log(` ${f("add scenarios to inferno/scenarios/ first.")}`),console.log()),process.exit(0))),e||(console.log(),console.log(` ${A("\u{1F9EA} infernoflow test")}`),console.log()),c){const o=d[0],v=O(o.id,u)[0]||{},S=h?.capabilities?.find(C=>C.id===o.id),x=D(o.id,o,v,S);console.log(x);return}const l={total:0,passed:0,failed:0,skipped:0,caps:[]};let k=!1;for(const o of d){const j=O(o.id,u),v=h?.capabilities?.find(C=>C.id===o.id);let S=[];if(!j.length)S=[{scenarioId:"(no scenarios)",status:"skip",reason:"register scenarios in inferno/scenarios/"}];else for(const C of j){const _=M(o.id,o,C,v,m);if(S.push(_),g&&_.status==="fail"){k=!0;break}}const x=T(o.id,o,S,r);if(l.total+=x.total,l.passed+=x.passed,l.failed+=x.failed,l.skipped+=x.skipped,l.caps.push({id:o.id,stability:J(o),results:S}),k)break}if(!e){console.log();const o=l.failed>0?b:l.passed>0?F:f;console.log(` ${o(A(String(l.passed)))} passed ${l.failed>0?b(A(String(l.failed))):f("0")} failed ${f(String(l.skipped))} skipped`),k&&console.log(` ${I("(bailed on first failure)")}`),console.log(),l.failed||console.log(f(" \u2500\u2500 infernoflow test complete")),console.log()}e&&console.log(JSON.stringify(l,null,2)),process.exit(l.failed>0?1:0)}export{K as testCommand};
@@ -1,282 +1,2 @@
1
- /**
2
- * infernoflow version
3
- *
4
- * Smart semver bump recommendation based on capability changes since the last
5
- * git tag (or a custom ref).
6
- *
7
- * Classification rules:
8
- * MAJOR — any capability was REMOVED (breaking: callers lose functionality)
9
- * MINOR — capabilities were ADDED (non-breaking: new surface area)
10
- * PATCH — only metadata changed (title / description / status edits)
11
- * NONE — no capability changes at all
12
- *
13
- * Usage:
14
- * infernoflow version # recommend bump type + show next version
15
- * infernoflow version --apply # apply recommended bump to package.json
16
- * infernoflow version --ref v1.2.3 # compare against a specific ref
17
- * infernoflow version --json # machine-readable output
18
- */
19
-
20
- import * as fs from "node:fs";
21
- import * as path from "node:path";
22
- import { execSync } from "node:child_process";
23
- import { header, ok, warn, info, bold, cyan, gray, green, red, yellow, done } from "../ui/output.mjs";
24
-
25
- // ── git helpers (shared with diff.mjs) ───────────────────────────────────────
26
-
27
- function capture(cmd, cwd) {
28
- try {
29
- return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- function lastTag(cwd) {
36
- return capture("git describe --tags --abbrev=0", cwd) || null;
37
- }
38
-
39
- function fileAtRef(ref, relPath, cwd) {
40
- return capture(`git show "${ref}:${relPath}"`, cwd);
41
- }
42
-
43
- // ── capability helpers ────────────────────────────────────────────────────────
44
-
45
- function parseCaps(jsonText) {
46
- if (!jsonText) return null;
47
- try {
48
- const obj = JSON.parse(jsonText);
49
- const raw = obj.capabilities || [];
50
- return raw.map(c => {
51
- if (typeof c === "string") return { id: c, title: c };
52
- return { id: c.id || c, title: c.title || c.id || String(c), status: c.status };
53
- });
54
- } catch {
55
- return null;
56
- }
57
- }
58
-
59
- function loadCapsFromDisk(infernoDir) {
60
- const capsPath = path.join(infernoDir, "capabilities.json");
61
- const contractPath = path.join(infernoDir, "contract.json");
62
- if (fs.existsSync(capsPath)) return parseCaps(fs.readFileSync(capsPath, "utf8"));
63
- if (fs.existsSync(contractPath)) return parseCaps(fs.readFileSync(contractPath, "utf8"));
64
- return null;
65
- }
66
-
67
- function loadCapsAtRef(ref, infernoRelDir, cwd) {
68
- const capsJson = fileAtRef(ref, `${infernoRelDir}/capabilities.json`, cwd);
69
- if (capsJson) return parseCaps(capsJson);
70
- const contractJson = fileAtRef(ref, `${infernoRelDir}/contract.json`, cwd);
71
- return parseCaps(contractJson);
72
- }
73
-
74
- function diffCaps(before, after) {
75
- const beforeMap = new Map(before.map(c => [c.id, c]));
76
- const afterMap = new Map(after.map(c => [c.id, c]));
77
-
78
- const added = after.filter(c => !beforeMap.has(c.id));
79
- const removed = before.filter(c => !afterMap.has(c.id));
80
-
81
- const changed = [];
82
- for (const c of after) {
83
- const old = beforeMap.get(c.id);
84
- if (!old) continue;
85
- const changes = [];
86
- if (old.title !== c.title) changes.push({ field: "title", from: old.title, to: c.title });
87
- if ((old.status || "") !== (c.status || "")) changes.push({ field: "status", from: old.status || "—", to: c.status || "—" });
88
- if (changes.length) changed.push({ id: c.id, changes });
89
- }
90
-
91
- return { added, removed, changed };
92
- }
93
-
94
- // ── semver helpers ────────────────────────────────────────────────────────────
95
-
96
- function classifyBump(diff) {
97
- if (diff.removed.length > 0) return "major";
98
- if (diff.added.length > 0) return "minor";
99
- if (diff.changed.length > 0) return "patch";
100
- return "none";
101
- }
102
-
103
- function applyBump(version, type) {
104
- const parts = (version || "0.0.0").split(".").map(Number);
105
- if (type === "major") { parts[0]++; parts[1] = 0; parts[2] = 0; }
106
- else if (type === "minor") { parts[1]++; parts[2] = 0; }
107
- else if (type === "patch") { parts[2]++; }
108
- return parts.join(".");
109
- }
110
-
111
- function readPackageVersion(cwd) {
112
- const pkgPath = path.join(cwd, "package.json");
113
- if (!fs.existsSync(pkgPath)) return null;
114
- try {
115
- return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version || null;
116
- } catch { return null; }
117
- }
118
-
119
- function writePackageVersion(cwd, newVersion) {
120
- const pkgPath = path.join(cwd, "package.json");
121
- const raw = fs.readFileSync(pkgPath, "utf8");
122
- const data = JSON.parse(raw);
123
- data.version = newVersion;
124
- fs.writeFileSync(pkgPath, JSON.stringify(data, null, 2) + "\n", "utf8");
125
- }
126
-
127
- // ── reason builder ────────────────────────────────────────────────────────────
128
-
129
- function buildReason(type, diff, ref) {
130
- const lines = [];
131
- if (type === "major") {
132
- lines.push(`${diff.removed.length} capability removed — breaking change`);
133
- for (const c of diff.removed.slice(0, 3)) lines.push(` - ${c.id}: ${c.title}`);
134
- if (diff.removed.length > 3) lines.push(` … and ${diff.removed.length - 3} more`);
135
- } else if (type === "minor") {
136
- lines.push(`${diff.added.length} new capability added`);
137
- for (const c of diff.added.slice(0, 3)) lines.push(` + ${c.id}: ${c.title}`);
138
- if (diff.added.length > 3) lines.push(` … and ${diff.added.length - 3} more`);
139
- } else if (type === "patch") {
140
- lines.push(`${diff.changed.length} capability metadata updated`);
141
- } else {
142
- lines.push(`No capability changes since ${ref}`);
143
- }
144
- return lines;
145
- }
146
-
147
- // ── MCP-compatible JSON output ────────────────────────────────────────────────
148
-
149
- function emitJson(payload) {
150
- console.log(JSON.stringify(payload, null, 2));
151
- }
152
-
153
- // ── main command ──────────────────────────────────────────────────────────────
154
-
155
- export async function versionCommand(rawArgs) {
156
- const args = rawArgs.slice(1);
157
- const asJson = args.includes("--json");
158
- const apply = args.includes("--apply");
159
-
160
- const refIdx = args.indexOf("--ref");
161
- let ref = refIdx !== -1 ? args[refIdx + 1] : null;
162
-
163
- const cwd = process.cwd();
164
- const infernoDir = path.join(cwd, "inferno");
165
-
166
- if (!asJson) header("infernoflow version");
167
-
168
- // ── Validate ───────────────────────────────────────────────────────────────
169
- if (!fs.existsSync(infernoDir)) {
170
- if (asJson) { emitJson({ ok: false, error: "inferno_not_found" }); process.exit(1); }
171
- warn("inferno/ not found — run: infernoflow init");
172
- process.exit(1);
173
- }
174
-
175
- // ── Resolve ref ────────────────────────────────────────────────────────────
176
- if (!ref) {
177
- ref = lastTag(cwd);
178
- if (!ref) {
179
- const parentExists = capture("git rev-parse HEAD~1", cwd);
180
- ref = parentExists ? "HEAD~1" : null;
181
- }
182
- }
183
-
184
- if (!ref) {
185
- const currentVersion = readPackageVersion(cwd) || "0.0.0";
186
- if (asJson) {
187
- emitJson({ ok: true, bump: "minor", current: currentVersion, next: applyBump(currentVersion, "minor"), reason: ["No git history — defaulting to minor for first release"], ref: null });
188
- } else {
189
- info("No git history found — defaulting to minor for first release");
190
- ok(`Recommended: ${bold(cyan("minor"))} → ${bold(applyBump(currentVersion, "minor"))}`);
191
- }
192
- return;
193
- }
194
-
195
- // ── Load capabilities ──────────────────────────────────────────────────────
196
- const current = loadCapsFromDisk(infernoDir);
197
- const previous = loadCapsAtRef(ref, "inferno", cwd);
198
-
199
- if (!current) {
200
- if (asJson) { emitJson({ ok: false, error: "no_capabilities" }); process.exit(1); }
201
- warn("No capabilities.json or contract.json found");
202
- process.exit(1);
203
- }
204
-
205
- // If no previous snapshot, treat all current caps as new → minor
206
- const prevCaps = previous || [];
207
- const diff = diffCaps(prevCaps, current);
208
- const bump = classifyBump(diff);
209
-
210
- const currentVersion = readPackageVersion(cwd) || "0.0.0";
211
- const nextVersion = bump === "none" ? currentVersion : applyBump(currentVersion, bump);
212
- const reason = buildReason(bump, diff, ref);
213
-
214
- // ── JSON output ────────────────────────────────────────────────────────────
215
- if (asJson) {
216
- emitJson({
217
- ok: true,
218
- bump,
219
- current: currentVersion,
220
- next: nextVersion,
221
- ref,
222
- reason,
223
- diff: {
224
- added: diff.added.length,
225
- removed: diff.removed.length,
226
- changed: diff.changed.length,
227
- },
228
- });
229
- return;
230
- }
231
-
232
- // ── Human output ──────────────────────────────────────────────────────────
233
- const bumpColor = bump === "major" ? red
234
- : bump === "minor" ? green
235
- : bump === "patch" ? yellow
236
- : gray;
237
-
238
- console.log();
239
- console.log(` Current version ${bold(currentVersion)} ${gray("(" + ref + ")")}`);
240
- console.log();
241
-
242
- if (bump === "none") {
243
- ok(`No capability changes — version stays at ${bold(currentVersion)}`);
244
- } else {
245
- console.log(` ${bold("Recommended bump:")} ${bumpColor(bold(bump.toUpperCase()))}`);
246
- console.log();
247
- for (const line of reason) {
248
- const prefix = line.startsWith(" +") ? green(" +")
249
- : line.startsWith(" -") ? red(" -")
250
- : line.startsWith(" …") ? gray(" …")
251
- : " ";
252
- const text = line.replace(/^\s+[+\-…]\s?/, "");
253
- if (line.startsWith(" ") && !line.startsWith(" …")) {
254
- console.log(` ${line.trim()}`);
255
- } else {
256
- console.log(` ${gray(line)}`);
257
- }
258
- }
259
- console.log();
260
- console.log(` ${bold(currentVersion)} → ${bumpColor(bold(nextVersion))}`);
261
- }
262
-
263
- // ── Apply ──────────────────────────────────────────────────────────────────
264
- if (apply && bump !== "none") {
265
- const pkgPath = path.join(cwd, "package.json");
266
- if (!fs.existsSync(pkgPath)) {
267
- warn("No package.json found — skipping --apply");
268
- } else {
269
- writePackageVersion(cwd, nextVersion);
270
- console.log();
271
- done(`package.json updated → ${bold(nextVersion)}`);
272
- }
273
- } else if (apply && bump === "none") {
274
- console.log();
275
- info("No changes to apply — version unchanged");
276
- } else if (bump !== "none") {
277
- console.log();
278
- info(`Run ${cyan("infernoflow version --apply")} to write ${nextVersion} to package.json`);
279
- }
280
-
281
- console.log();
282
- }
1
+ import*as u from"node:fs";import*as h from"node:path";import{execSync as V}from"node:child_process";import{header as W,ok as C,warn as v,info as k,bold as p,cyan as J,gray as $,green as P,red as F,yellow as A,done as E}from"../ui/output.mjs";function x(e,n){try{return V(e,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}catch{return null}}function M(e){return x("git describe --tags --abbrev=0",e)||null}function O(e,n,t){return x(`git show "${e}:${n}"`,t)}function j(e){if(!e)return null;try{return(JSON.parse(e).capabilities||[]).map(o=>typeof o=="string"?{id:o,title:o}:{id:o.id||o,title:o.title||o.id||String(o),status:o.status})}catch{return null}}function _(e){const n=h.join(e,"capabilities.json"),t=h.join(e,"contract.json");return u.existsSync(n)?j(u.readFileSync(n,"utf8")):u.existsSync(t)?j(u.readFileSync(t,"utf8")):null}function B(e,n,t){const o=O(e,`${n}/capabilities.json`,t);if(o)return j(o);const r=O(e,`${n}/contract.json`,t);return j(r)}function D(e,n){const t=new Map(e.map(s=>[s.id,s])),o=new Map(n.map(s=>[s.id,s])),r=n.filter(s=>!t.has(s.id)),c=e.filter(s=>!o.has(s.id)),l=[];for(const s of n){const f=t.get(s.id);if(!f)continue;const d=[];f.title!==s.title&&d.push({field:"title",from:f.title,to:s.title}),(f.status||"")!==(s.status||"")&&d.push({field:"status",from:f.status||"\u2014",to:s.status||"\u2014"}),d.length&&l.push({id:s.id,changes:d})}return{added:r,removed:c,changed:l}}function H(e){return e.removed.length>0?"major":e.added.length>0?"minor":e.changed.length>0?"patch":"none"}function S(e,n){const t=(e||"0.0.0").split(".").map(Number);return n==="major"?(t[0]++,t[1]=0,t[2]=0):n==="minor"?(t[1]++,t[2]=0):n==="patch"&&t[2]++,t.join(".")}function R(e){const n=h.join(e,"package.json");if(!u.existsSync(n))return null;try{return JSON.parse(u.readFileSync(n,"utf8")).version||null}catch{return null}}function I(e,n){const t=h.join(e,"package.json"),o=u.readFileSync(t,"utf8"),r=JSON.parse(o);r.version=n,u.writeFileSync(t,JSON.stringify(r,null,2)+`
2
+ `,"utf8")}function U(e,n,t){const o=[];if(e==="major"){o.push(`${n.removed.length} capability removed \u2014 breaking change`);for(const r of n.removed.slice(0,3))o.push(` - ${r.id}: ${r.title}`);n.removed.length>3&&o.push(` \u2026 and ${n.removed.length-3} more`)}else if(e==="minor"){o.push(`${n.added.length} new capability added`);for(const r of n.added.slice(0,3))o.push(` + ${r.id}: ${r.title}`);n.added.length>3&&o.push(` \u2026 and ${n.added.length-3} more`)}else e==="patch"?o.push(`${n.changed.length} capability metadata updated`):o.push(`No capability changes since ${t}`);return o}function b(e){console.log(JSON.stringify(e,null,2))}async function Q(e){const n=e.slice(1),t=n.includes("--json"),o=n.includes("--apply"),r=n.indexOf("--ref");let c=r!==-1?n[r+1]:null;const l=process.cwd(),s=h.join(l,"inferno");if(t||W("infernoflow version"),u.existsSync(s)||(t&&(b({ok:!1,error:"inferno_not_found"}),process.exit(1)),v("inferno/ not found \u2014 run: infernoflow init"),process.exit(1)),c||(c=M(l),c||(c=x("git rev-parse HEAD~1",l)?"HEAD~1":null)),!c){const i=R(l)||"0.0.0";t?b({ok:!0,bump:"minor",current:i,next:S(i,"minor"),reason:["No git history \u2014 defaulting to minor for first release"],ref:null}):(k("No git history found \u2014 defaulting to minor for first release"),C(`Recommended: ${p(J("minor"))} \u2192 ${p(S(i,"minor"))}`));return}const f=_(s),d=B(c,"inferno",l);f||(t&&(b({ok:!1,error:"no_capabilities"}),process.exit(1)),v("No capabilities.json or contract.json found"),process.exit(1));const m=D(d||[],f),a=H(m),g=R(l)||"0.0.0",y=a==="none"?g:S(g,a),w=U(a,m,c);if(t){b({ok:!0,bump:a,current:g,next:y,ref:c,reason:w,diff:{added:m.added.length,removed:m.removed.length,changed:m.changed.length}});return}const N=a==="major"?F:a==="minor"?P:a==="patch"?A:$;if(console.log(),console.log(` Current version ${p(g)} ${$("("+c+")")}`),console.log(),a==="none")C(`No capability changes \u2014 version stays at ${p(g)}`);else{console.log(` ${p("Recommended bump:")} ${N(p(a.toUpperCase()))}`),console.log();for(const i of w){const z=i.startsWith(" +")?P(" +"):i.startsWith(" -")?F(" -"):i.startsWith(" \u2026")?$(" \u2026"):" ",G=i.replace(/^\s+[+\-…]\s?/,"");i.startsWith(" ")&&!i.startsWith(" \u2026")?console.log(` ${i.trim()}`):console.log(` ${$(i)}`)}console.log(),console.log(` ${p(g)} \u2192 ${N(p(y))}`)}if(o&&a!=="none"){const i=h.join(l,"package.json");u.existsSync(i)?(I(l,y),console.log(),E(`package.json updated \u2192 ${p(y)}`)):v("No package.json found \u2014 skipping --apply")}else o&&a==="none"?(console.log(),k("No changes to apply \u2014 version unchanged")):a!=="none"&&(console.log(),k(`Run ${J("infernoflow version --apply")} to write ${y} to package.json`));console.log()}export{Q as versionCommand};