infernoflow 0.37.1 β†’ 0.37.3

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 (88) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -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,195 +1,18 @@
1
- /**
2
- * infernoflow theme
3
- *
4
- * Scans the project for design tokens β€” fonts, colors, CSS variables β€”
5
- * and writes inferno/theme.json so AI agents always know the visual system.
6
- *
7
- * This captures what AI can't reliably infer from scattered style files:
8
- * the actual palette, typography, and spacing tokens in use.
9
- *
10
- * When theme.json changes between runs, the delta is auto-logged to sessions.jsonl
11
- * so agents know the design system evolved (not just the code).
12
- *
13
- * Usage:
14
- * infernoflow theme Scan + write inferno/theme.json
15
- * infernoflow theme --show Print current theme.json
16
- * infernoflow theme --json Output as JSON
17
- * infernoflow theme --dry-run Scan but don't write
18
- * infernoflow theme --watch Re-scan on style file changes
19
- */
20
-
21
- import * as fs from "node:fs";
22
- import * as path from "node:path";
23
- import { scanTheme } from "../theme/scanner.mjs";
24
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
25
-
26
- const INFERNO_DIR = "inferno";
27
- const THEME_FILE = path.join(INFERNO_DIR, "theme.json");
28
- const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
29
-
30
- function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
31
-
32
- function appendSession(entry) {
33
- if (!fs.existsSync(SESSIONS_FILE)) return;
34
- fs.appendFileSync(SESSIONS_FILE, JSON.stringify(entry) + "\n", "utf8");
35
- }
36
-
37
- function diffTheme(prev, next) {
38
- const changes = [];
39
-
40
- // Font changes
41
- if (prev?.fonts?.primary !== next?.fonts?.primary) {
42
- changes.push(`primary font: ${prev?.fonts?.primary || "none"} β†’ ${next?.fonts?.primary || "none"}`);
43
- }
44
- if (prev?.fonts?.mono !== next?.fonts?.mono) {
45
- changes.push(`mono font: ${prev?.fonts?.mono || "none"} β†’ ${next?.fonts?.mono || "none"}`);
46
- }
47
-
48
- // Mode change
49
- if (prev?.colors?.mode !== next?.colors?.mode) {
50
- changes.push(`color mode: ${prev?.colors?.mode || "unknown"} β†’ ${next?.colors?.mode}`);
51
- }
52
-
53
- // Palette changes
54
- const prevPalette = prev?.colors?.palette || {};
55
- const nextPalette = next?.colors?.palette || {};
56
- for (const key of new Set([...Object.keys(prevPalette), ...Object.keys(nextPalette)])) {
57
- if (prevPalette[key] !== nextPalette[key]) {
58
- changes.push(`${key} color: ${prevPalette[key] || "none"} β†’ ${nextPalette[key] || "none"}`);
59
- }
60
- }
61
-
62
- // CSS var changes (new or changed vars only)
63
- const prevVars = prev?.cssVars || {};
64
- const nextVars = next?.cssVars || {};
65
- const newVars = Object.keys(nextVars).filter(k => !prevVars[k]);
66
- const changedVars = Object.keys(nextVars).filter(k => prevVars[k] && prevVars[k] !== nextVars[k]);
67
- if (newVars.length) changes.push(`new CSS vars: ${newVars.slice(0,5).join(", ")}`);
68
- if (changedVars.length) changes.push(`changed CSS vars: ${changedVars.slice(0,5).join(", ")}`);
69
-
70
- return changes;
71
- }
72
-
73
- function printTheme(theme) {
74
- const { fonts, colors, cssVars, framework, stats } = theme;
75
-
76
- console.log("\n " + bold("🎨 Design System"));
77
- console.log(" " + "─".repeat(50));
78
-
79
- console.log(cyan("\n Fonts"));
80
- if (fonts.primary) console.log(` Primary : ${fonts.primary}`);
81
- if (fonts.mono) console.log(` Mono : ${fonts.mono}`);
82
- if (fonts.all?.length > 2) console.log(gray(` All : ${fonts.all.join(", ")}`));
83
- if (fonts.sources?.length) console.log(gray(` Sources : ${fonts.sources.join(", ")}`));
84
-
85
- console.log(cyan("\n Colors") + gray(` (${colors.mode} mode)`));
86
- for (const [role, hex] of Object.entries(colors.palette)) {
87
- const swatch = `\x1b[48;2;${parseInt(hex.slice(1,3),16)};${parseInt(hex.slice(3,5),16)};${parseInt(hex.slice(5,7),16)}m \x1b[0m`;
88
- console.log(` ${role.padEnd(14)} ${swatch} ${hex}`);
89
- }
90
-
91
- if (Object.keys(cssVars).length) {
92
- console.log(cyan("\n CSS Variables") + gray(` (${Object.keys(cssVars).length} found)`));
93
- const entries = Object.entries(cssVars).slice(0, 12);
94
- for (const [name, val] of entries) {
95
- console.log(` ${name.padEnd(24)} ${gray(val)}`);
96
- }
97
- if (Object.keys(cssVars).length > 12) {
98
- console.log(gray(` … and ${Object.keys(cssVars).length - 12} more`));
99
- }
100
- }
101
-
102
- console.log(cyan("\n Framework") + ` ${framework}`);
103
- console.log(gray(`\n Scanned: ${stats.styleFiles} style files Β· ${stats.colorsFound} colors Β· ${stats.varsFound} CSS vars\n`));
104
- }
105
-
106
- export async function themeCommand(args) {
107
- const has = (f) => args.includes(f);
108
- const dryRun = has("--dry-run");
109
- const showOnly = has("--show") || has("-s");
110
- const jsonFlag = has("--json");
111
- const watchFlag = has("--watch");
112
-
113
- console.log("\n " + bold("πŸ”₯ infernoflow β€” theme"));
114
- console.log(" " + "─".repeat(50) + "\n");
115
-
116
- if (!fs.existsSync(INFERNO_DIR)) {
117
- console.error(red(" ✘ inferno/ not found β€” run: infernoflow init\n"));
118
- process.exit(1);
119
- }
120
-
121
- // ── Show existing theme ────────────────────────────────────────────────────
122
- if (showOnly) {
123
- const existing = readJSON(THEME_FILE);
124
- if (!existing) {
125
- console.log(yellow(" ⚠ No theme.json yet β€” run: infernoflow theme\n"));
126
- return;
127
- }
128
- if (jsonFlag) { console.log(JSON.stringify(existing, null, 2)); return; }
129
- printTheme(existing);
130
- return;
131
- }
132
-
133
- const runScan = () => {
134
- console.log(gray(" Scanning style files…"));
135
- const cwd = process.cwd();
136
- const theme = scanTheme(cwd);
137
-
138
- if (jsonFlag) { console.log(JSON.stringify(theme, null, 2)); return; }
139
-
140
- printTheme(theme);
141
-
142
- if (dryRun) {
143
- console.log(yellow(" βš‘ Dry run β€” theme.json not written\n"));
144
- return;
145
- }
146
-
147
- // Read previous theme to diff
148
- const prev = readJSON(THEME_FILE);
149
-
150
- const output = {
151
- ...theme,
152
- scannedAt: new Date().toISOString(),
153
- };
154
-
155
- fs.writeFileSync(THEME_FILE, JSON.stringify(output, null, 2) + "\n", "utf8");
156
- console.log(green(" βœ” Written β†’ inferno/theme.json\n"));
157
-
158
- // Auto-log changes to sessions.jsonl if theme changed
159
- if (prev) {
160
- const changes = diffTheme(prev, theme);
161
- if (changes.length) {
162
- appendSession({
163
- ts: new Date().toISOString(),
164
- agent: "infernoflow",
165
- type: "theme",
166
- summary: "Theme changed: " + changes.join("; "),
167
- });
168
- console.log(yellow(" ⚑ Theme changes logged to sessions.jsonl"));
169
- for (const c of changes) console.log(gray(` β€’ ${c}`));
170
- console.log();
171
- }
172
- }
173
- };
174
-
175
- runScan();
176
-
177
- // ── Watch mode ─────────────────────────────────────────────────────────────
178
- if (watchFlag) {
179
- console.log(cyan(" Watching style files for changes… (Ctrl+C to stop)\n"));
180
- const { watch } = await import("node:fs");
181
- let debounce = null;
182
- watch(process.cwd(), { recursive: true }, (_, filename) => {
183
- if (!filename) return;
184
- const ext = path.extname(filename);
185
- if (![".css",".scss",".sass",".less",".styl"].includes(ext)) return;
186
- if (debounce) clearTimeout(debounce);
187
- debounce = setTimeout(() => {
188
- console.log(gray(`\n Change detected: ${filename}`));
189
- runScan();
190
- }, 1000);
191
- });
192
- // Keep alive
193
- await new Promise(() => {});
194
- }
195
- }
1
+ import*as u from"node:fs";import*as y from"node:path";import{scanTheme as F}from"../theme/scanner.mjs";import{bold as $,cyan as h,gray as i,green as k,yellow as d,red as I}from"../ui/output.mjs";const p="inferno",S=y.join(p,"theme.json"),w=y.join(p,"sessions.jsonl");function j(e){try{return JSON.parse(u.readFileSync(e,"utf8"))}catch{return null}}function N(e){u.existsSync(w)&&u.appendFileSync(w,JSON.stringify(e)+`
2
+ `,"utf8")}function C(e,o){const c=[];e?.fonts?.primary!==o?.fonts?.primary&&c.push(`primary font: ${e?.fonts?.primary||"none"} \u2192 ${o?.fonts?.primary||"none"}`),e?.fonts?.mono!==o?.fonts?.mono&&c.push(`mono font: ${e?.fonts?.mono||"none"} \u2192 ${o?.fonts?.mono||"none"}`),e?.colors?.mode!==o?.colors?.mode&&c.push(`color mode: ${e?.colors?.mode||"unknown"} \u2192 ${o?.colors?.mode}`);const l=e?.colors?.palette||{},f=o?.colors?.palette||{};for(const t of new Set([...Object.keys(l),...Object.keys(f)]))l[t]!==f[t]&&c.push(`${t} color: ${l[t]||"none"} \u2192 ${f[t]||"none"}`);const a=e?.cssVars||{},r=o?.cssVars||{},n=Object.keys(r).filter(t=>!a[t]),s=Object.keys(r).filter(t=>a[t]&&a[t]!==r[t]);return n.length&&c.push(`new CSS vars: ${n.slice(0,5).join(", ")}`),s.length&&c.push(`changed CSS vars: ${s.slice(0,5).join(", ")}`),c}function O(e){const{fonts:o,colors:c,cssVars:l,framework:f,stats:a}=e;console.log(`
3
+ `+$("\u{1F3A8} Design System")),console.log(" "+"\u2500".repeat(50)),console.log(h(`
4
+ Fonts`)),o.primary&&console.log(` Primary : ${o.primary}`),o.mono&&console.log(` Mono : ${o.mono}`),o.all?.length>2&&console.log(i(` All : ${o.all.join(", ")}`)),o.sources?.length&&console.log(i(` Sources : ${o.sources.join(", ")}`)),console.log(h(`
5
+ Colors`)+i(` (${c.mode} mode)`));for(const[r,n]of Object.entries(c.palette)){const s=`\x1B[48;2;${parseInt(n.slice(1,3),16)};${parseInt(n.slice(3,5),16)};${parseInt(n.slice(5,7),16)}m \x1B[0m`;console.log(` ${r.padEnd(14)} ${s} ${n}`)}if(Object.keys(l).length){console.log(h(`
6
+ CSS Variables`)+i(` (${Object.keys(l).length} found)`));const r=Object.entries(l).slice(0,12);for(const[n,s]of r)console.log(` ${n.padEnd(24)} ${i(s)}`);Object.keys(l).length>12&&console.log(i(` \u2026 and ${Object.keys(l).length-12} more`))}console.log(h(`
7
+ Framework`)+` ${f}`),console.log(i(`
8
+ Scanned: ${a.styleFiles} style files \xB7 ${a.colorsFound} colors \xB7 ${a.varsFound} CSS vars
9
+ `))}async function V(e){const o=n=>e.includes(n),c=o("--dry-run"),l=o("--show")||o("-s"),f=o("--json"),a=o("--watch");if(console.log(`
10
+ `+$("\u{1F525} infernoflow \u2014 theme")),console.log(" "+"\u2500".repeat(50)+`
11
+ `),u.existsSync(p)||(console.error(I(` \u2718 inferno/ not found \u2014 run: infernoflow init
12
+ `)),process.exit(1)),l){const n=j(S);if(!n){console.log(d(` \u26A0 No theme.json yet \u2014 run: infernoflow theme
13
+ `));return}if(f){console.log(JSON.stringify(n,null,2));return}O(n);return}const r=()=>{console.log(i(" Scanning style files\u2026"));const n=process.cwd(),s=F(n);if(f){console.log(JSON.stringify(s,null,2));return}if(O(s),c){console.log(d(` \u2691 Dry run \u2014 theme.json not written
14
+ `));return}const t=j(S),g={...s,scannedAt:new Date().toISOString()};if(u.writeFileSync(S,JSON.stringify(g,null,2)+`
15
+ `,"utf8"),console.log(k(` \u2714 Written \u2192 inferno/theme.json
16
+ `)),t){const m=C(t,s);if(m.length){N({ts:new Date().toISOString(),agent:"infernoflow",type:"theme",summary:"Theme changed: "+m.join("; ")}),console.log(d(" \u26A1 Theme changes logged to sessions.jsonl"));for(const b of m)console.log(i(` \u2022 ${b}`));console.log()}}};if(r(),a){console.log(h(` Watching style files for changes\u2026 (Ctrl+C to stop)
17
+ `));const{watch:n}=await import("node:fs");let s=null;n(process.cwd(),{recursive:!0},(t,g)=>{if(!g)return;const m=y.extname(g);[".css",".scss",".sass",".less",".styl"].includes(m)&&(s&&clearTimeout(s),s=setTimeout(()=>{console.log(i(`
18
+ Change detected: ${g}`)),r()},1e3))}),await new Promise(()=>{})}}export{V as themeCommand};