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,309 +1,2 @@
1
- /**
2
- * infernoflow health
3
- *
4
- * Computes a weighted 0–100 health score for the capability contract.
5
- * Breaks it down across five dimensions so you know exactly where to improve.
6
- *
7
- * Dimensions:
8
- * Coverage % of capabilities that have descriptions (weight 25)
9
- * Documentation % with at least one scenario/test (weight 20)
10
- * Freshness How recently the contract was updated (weight 20)
11
- * Completeness Version field, owner, tags present (weight 15)
12
- * Drift risk Open issues from `infernoflow check` (weight 20)
13
- *
14
- * Usage:
15
- * infernoflow health Print score + breakdown
16
- * infernoflow health --json Machine-readable
17
- * infernoflow health --fail-below 70 Exit 1 if score < 70 (CI gate)
18
- * infernoflow health --watch Re-run every 30s (for terminals)
19
- */
20
-
21
- import * as fs from "node:fs";
22
- import * as path from "node:path";
23
- import { execSync } from "node:child_process";
24
- import { done, warn, info, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
25
-
26
- const WEIGHTS = {
27
- coverage: 25,
28
- documentation: 20,
29
- freshness: 20,
30
- completeness: 15,
31
- drift: 20,
32
- };
33
-
34
- // ── Readers ───────────────────────────────────────────────────────────────────
35
-
36
- function readContract(infernoDir) {
37
- for (const f of ["contract.json", "capabilities.json"]) {
38
- const p = path.join(infernoDir, f);
39
- if (!fs.existsSync(p)) continue;
40
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
41
- }
42
- return null;
43
- }
44
-
45
- function readScenarios(infernoDir) {
46
- const scenDir = path.join(infernoDir, "scenarios");
47
- if (!fs.existsSync(scenDir)) return [];
48
- return fs.readdirSync(scenDir)
49
- .filter(f => f.endsWith(".json"))
50
- .flatMap(f => {
51
- try {
52
- const data = JSON.parse(fs.readFileSync(path.join(scenDir, f), "utf8"));
53
- return data.capability ? [data.capability] : (data.capabilities || []);
54
- } catch { return []; }
55
- });
56
- }
57
-
58
- function runCheck(cwd) {
59
- try {
60
- const out = execSync("npx infernoflow check --json", {
61
- cwd, encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"],
62
- });
63
- return JSON.parse(out);
64
- } catch (err) {
65
- try { return JSON.parse(err.stdout || "{}"); } catch { return {}; }
66
- }
67
- }
68
-
69
- function lastModifiedDaysAgo(infernoDir) {
70
- const files = ["contract.json", "capabilities.json"]
71
- .map(f => path.join(infernoDir, f))
72
- .filter(fs.existsSync);
73
- if (!files.length) return 999;
74
- const mtime = Math.max(...files.map(f => fs.statSync(f).mtimeMs));
75
- return (Date.now() - mtime) / (1000 * 60 * 60 * 24);
76
- }
77
-
78
- // ── Scorers ───────────────────────────────────────────────────────────────────
79
-
80
- function scoreCoverage(caps) {
81
- if (!caps.length) return { score: 0, detail: "No capabilities" };
82
- const withDesc = caps.filter(c => c.description && c.description.length > 5).length;
83
- const pct = Math.round((withDesc / caps.length) * 100);
84
- return {
85
- score: pct,
86
- detail: `${withDesc}/${caps.length} capabilities have descriptions`,
87
- pct,
88
- };
89
- }
90
-
91
- function scoreDocumentation(caps, scenarioCaps) {
92
- if (!caps.length) return { score: 0, detail: "No capabilities" };
93
- const scenSet = new Set(scenarioCaps.map(s => String(s).toLowerCase()));
94
- const withScen = caps.filter(c => scenSet.has(c.id.toLowerCase())).length;
95
- const pct = Math.round((withScen / caps.length) * 100);
96
- return {
97
- score: Math.min(100, pct + (scenSet.size === 0 ? 0 : 10)), // bonus for having any scenarios
98
- detail: `${withScen}/${caps.length} capabilities have test scenarios`,
99
- pct,
100
- };
101
- }
102
-
103
- function scoreFreshness(daysAgo) {
104
- let score;
105
- let label;
106
- if (daysAgo <= 1) { score = 100; label = "updated today"; }
107
- else if (daysAgo <= 3) { score = 95; label = `updated ${Math.round(daysAgo)}d ago`; }
108
- else if (daysAgo <= 7) { score = 85; label = `updated ${Math.round(daysAgo)}d ago`; }
109
- else if (daysAgo <= 14) { score = 70; label = `updated ${Math.round(daysAgo)}d ago`; }
110
- else if (daysAgo <= 30) { score = 50; label = `updated ${Math.round(daysAgo)}d ago`; }
111
- else if (daysAgo <= 60) { score = 30; label = `updated ${Math.round(daysAgo)}d ago — stale`; }
112
- else { score = 10; label = `not updated in ${Math.round(daysAgo)}d — very stale`; }
113
- return { score, detail: label };
114
- }
115
-
116
- function scoreCompleteness(caps, contract) {
117
- if (!caps.length) return { score: 0, detail: "No capabilities" };
118
- const hasVersion = !!(contract?.version || contract?.contractVersion);
119
- const withTags = caps.filter(c => c.tags?.length).length;
120
- const withOwner = caps.filter(c => c.owner).length;
121
- const withSince = caps.filter(c => c.since).length;
122
-
123
- const tagPct = Math.round((withTags / caps.length) * 100);
124
- const ownerPct = Math.round((withOwner / caps.length) * 100);
125
- const sincePct = Math.round((withSince / caps.length) * 100);
126
-
127
- const score = Math.round(
128
- (hasVersion ? 20 : 0) +
129
- tagPct * 0.3 +
130
- ownerPct * 0.3 +
131
- sincePct * 0.2
132
- );
133
-
134
- return {
135
- score: Math.min(100, score),
136
- detail: `version: ${hasVersion ? "✓" : "✗"}, tags: ${tagPct}%, owner: ${ownerPct}%, since: ${sincePct}%`,
137
- };
138
- }
139
-
140
- function scoreDrift(checkResult) {
141
- const issues = checkResult?.issues || [];
142
- const warnings = issues.filter(i => (i.severity || i.level || "error") === "warning").length;
143
- const errors = issues.filter(i => (i.severity || i.level || "error") === "error").length;
144
- const status = checkResult?.status;
145
-
146
- if (status === "ok" || (!errors && !warnings)) return { score: 100, detail: "No issues found" };
147
-
148
- const score = Math.max(0, 100 - (errors * 20) - (warnings * 8));
149
- return {
150
- score,
151
- detail: `${errors} error${errors !== 1 ? "s" : ""}, ${warnings} warning${warnings !== 1 ? "s" : ""}`,
152
- };
153
- }
154
-
155
- // ── Aggregate ─────────────────────────────────────────────────────────────────
156
-
157
- function computeHealth(infernoDir, cwd) {
158
- const contract = readContract(infernoDir);
159
- const caps = (contract?.capabilities || []).map(c =>
160
- typeof c === "string" ? { id: c, description: "", tags: [], owner: "", since: "" } : c
161
- );
162
- const scenarioCaps = readScenarios(infernoDir);
163
- const daysAgo = lastModifiedDaysAgo(infernoDir);
164
- const checkResult = runCheck(cwd);
165
-
166
- const dimensions = {
167
- coverage: scoreCoverage(caps),
168
- documentation: scoreDocumentation(caps, scenarioCaps),
169
- freshness: scoreFreshness(daysAgo),
170
- completeness: scoreCompleteness(caps, contract),
171
- drift: scoreDrift(checkResult),
172
- };
173
-
174
- const totalScore = Math.round(
175
- Object.entries(dimensions).reduce((sum, [key, dim]) => {
176
- return sum + (dim.score * WEIGHTS[key]) / 100;
177
- }, 0)
178
- );
179
-
180
- return { totalScore, dimensions, caps, daysAgo, checkResult };
181
- }
182
-
183
- // ── Renderer ──────────────────────────────────────────────────────────────────
184
-
185
- function scoreColor(score) {
186
- if (score >= 80) return green;
187
- if (score >= 60) return yellow;
188
- return red;
189
- }
190
-
191
- function scoreGrade(score) {
192
- if (score >= 90) return "A";
193
- if (score >= 80) return "B";
194
- if (score >= 70) return "C";
195
- if (score >= 60) return "D";
196
- return "F";
197
- }
198
-
199
- function barOf(score, width = 30) {
200
- const filled = Math.round((score / 100) * width);
201
- const empty = width - filled;
202
- return "█".repeat(filled) + "░".repeat(empty);
203
- }
204
-
205
- function printReport(totalScore, dimensions) {
206
- const col = scoreColor(totalScore);
207
- const grade = scoreGrade(totalScore);
208
-
209
- console.log();
210
- console.log(` ${bold("🔥 infernoflow health score")}`);
211
- console.log();
212
- console.log(` ${col(bold(String(totalScore)))} / 100 ${col(bold(grade))} ${col(barOf(totalScore))}`);
213
- console.log();
214
-
215
- const dimNames = {
216
- coverage: "Coverage ",
217
- documentation: "Docs/Tests ",
218
- freshness: "Freshness ",
219
- completeness: "Completeness ",
220
- drift: "Drift risk ",
221
- };
222
-
223
- for (const [key, dim] of Object.entries(dimensions)) {
224
- const w = WEIGHTS[key];
225
- const sc = dim.score;
226
- const c = scoreColor(sc);
227
- const bar = barOf(sc, 20);
228
- const weighted = Math.round((sc * w) / 100);
229
- console.log(
230
- ` ${bold(dimNames[key])} ${c(String(sc).padStart(3))} ${c(bar)} ${gray(`×${w}% = ${weighted}pts ${dim.detail}`)}`
231
- );
232
- }
233
- console.log();
234
- }
235
-
236
- function printTips(totalScore, dimensions) {
237
- const tips = [];
238
-
239
- if (dimensions.coverage.score < 70)
240
- tips.push("Add descriptions to your capabilities in contract.json");
241
- if (dimensions.documentation.score < 60)
242
- tips.push("Create scenario files in inferno/scenarios/ for each capability");
243
- if (dimensions.freshness.score < 70)
244
- tips.push("Run `infernoflow suggest` to sync recent changes to the contract");
245
- if (dimensions.completeness.score < 60)
246
- tips.push("Add version, tags, owner, and since fields to your capabilities");
247
- if (dimensions.drift.score < 80)
248
- tips.push("Run `infernoflow check` and fix the reported issues");
249
-
250
- if (tips.length) {
251
- console.log(` ${bold("Tips to improve:")}`);
252
- tips.forEach(t => console.log(` ${yellow("·")} ${t}`));
253
- console.log();
254
- }
255
- }
256
-
257
- // ── Entry ─────────────────────────────────────────────────────────────────────
258
-
259
- export async function healthCommand(rawArgs) {
260
- const args = rawArgs.slice(1);
261
- const jsonMode = args.includes("--json");
262
- const watchMode = args.includes("--watch");
263
- const cwd = process.cwd();
264
- const infernoDir = path.join(cwd, "inferno");
265
-
266
- if (!fs.existsSync(infernoDir)) {
267
- const msg = "inferno/ not found. Run: infernoflow init";
268
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
269
- else warn(msg);
270
- process.exit(1);
271
- }
272
-
273
- const failBelowIdx = args.indexOf("--fail-below");
274
- const failBelow = failBelowIdx !== -1 ? parseInt(args[failBelowIdx + 1], 10) : null;
275
-
276
- const intervalIdx = args.indexOf("--interval");
277
- const intervalSecs = intervalIdx !== -1 ? parseInt(args[intervalIdx + 1], 10) : 30;
278
-
279
- const runOnce = () => {
280
- const { totalScore, dimensions } = computeHealth(infernoDir, cwd);
281
-
282
- if (jsonMode) {
283
- const dimFlat = Object.fromEntries(
284
- Object.entries(dimensions).map(([k, v]) => [k, { score: v.score, detail: v.detail, weight: WEIGHTS[k] }])
285
- );
286
- console.log(JSON.stringify({ ok: true, score: totalScore, grade: scoreGrade(totalScore), dimensions: dimFlat }));
287
- } else {
288
- if (watchMode) process.stdout.write("\x1Bc"); // clear screen
289
- printReport(totalScore, dimensions);
290
- if (!watchMode) printTips(totalScore, dimensions);
291
- }
292
-
293
- if (failBelow !== null && totalScore < failBelow) {
294
- if (!jsonMode) console.error(red(` ✗ Score ${totalScore} is below threshold ${failBelow} — failing.\n`));
295
- process.exit(1);
296
- }
297
-
298
- return totalScore;
299
- };
300
-
301
- if (watchMode) {
302
- if (!jsonMode) info(`Watching health score every ${intervalSecs}s — press Ctrl+C to stop`);
303
- runOnce();
304
- setInterval(runOnce, intervalSecs * 1000);
305
- await new Promise(() => {});
306
- } else {
307
- runOnce();
308
- }
309
- }
1
+ import*as d from"node:fs";import*as h from"node:path";import{execSync as x}from"node:child_process";import{warn as C,info as O,bold as p,gray as N,green as D,yellow as S,red as b}from"../ui/output.mjs";const m={coverage:25,documentation:20,freshness:20,completeness:15,drift:20};function k(e){for(const n of["contract.json","capabilities.json"]){const t=h.join(e,n);if(d.existsSync(t))try{return JSON.parse(d.readFileSync(t,"utf8"))}catch{}}return null}function F(e){const n=h.join(e,"scenarios");return d.existsSync(n)?d.readdirSync(n).filter(t=>t.endsWith(".json")).flatMap(t=>{try{const o=JSON.parse(d.readFileSync(h.join(n,t),"utf8"));return o.capability?[o.capability]:o.capabilities||[]}catch{return[]}}):[]}function I(e){try{const n=x("npx infernoflow check --json",{cwd:e,encoding:"utf8",timeout:15e3,stdio:["ignore","pipe","pipe"]});return JSON.parse(n)}catch(n){try{return JSON.parse(n.stdout||"{}")}catch{return{}}}}function J(e){const n=["contract.json","capabilities.json"].map(o=>h.join(e,o)).filter(d.existsSync);if(!n.length)return 999;const t=Math.max(...n.map(o=>d.statSync(o).mtimeMs));return(Date.now()-t)/(1e3*60*60*24)}function T(e){if(!e.length)return{score:0,detail:"No capabilities"};const n=e.filter(o=>o.description&&o.description.length>5).length,t=Math.round(n/e.length*100);return{score:t,detail:`${n}/${e.length} capabilities have descriptions`,pct:t}}function B(e,n){if(!e.length)return{score:0,detail:"No capabilities"};const t=new Set(n.map(s=>String(s).toLowerCase())),o=e.filter(s=>t.has(s.id.toLowerCase())).length,l=Math.round(o/e.length*100);return{score:Math.min(100,l+(t.size===0?0:10)),detail:`${o}/${e.length} capabilities have test scenarios`,pct:l}}function P(e){let n,t;return e<=1?(n=100,t="updated today"):e<=3?(n=95,t=`updated ${Math.round(e)}d ago`):e<=7?(n=85,t=`updated ${Math.round(e)}d ago`):e<=14?(n=70,t=`updated ${Math.round(e)}d ago`):e<=30?(n=50,t=`updated ${Math.round(e)}d ago`):e<=60?(n=30,t=`updated ${Math.round(e)}d ago \u2014 stale`):(n=10,t=`not updated in ${Math.round(e)}d \u2014 very stale`),{score:n,detail:t}}function R(e,n){if(!e.length)return{score:0,detail:"No capabilities"};const t=!!(n?.version||n?.contractVersion),o=e.filter(a=>a.tags?.length).length,l=e.filter(a=>a.owner).length,s=e.filter(a=>a.since).length,r=Math.round(o/e.length*100),f=Math.round(l/e.length*100),u=Math.round(s/e.length*100),c=Math.round((t?20:0)+r*.3+f*.3+u*.2);return{score:Math.min(100,c),detail:`version: ${t?"\u2713":"\u2717"}, tags: ${r}%, owner: ${f}%, since: ${u}%`}}function E(e){const n=e?.issues||[],t=n.filter(r=>(r.severity||r.level||"error")==="warning").length,o=n.filter(r=>(r.severity||r.level||"error")==="error").length;return e?.status==="ok"||!o&&!t?{score:100,detail:"No issues found"}:{score:Math.max(0,100-o*20-t*8),detail:`${o} error${o!==1?"s":""}, ${t} warning${t!==1?"s":""}`}}function W(e,n){const t=k(e),o=(t?.capabilities||[]).map(c=>typeof c=="string"?{id:c,description:"",tags:[],owner:"",since:""}:c),l=F(e),s=J(e),r=I(n),f={coverage:T(o),documentation:B(o,l),freshness:P(s),completeness:R(o,t),drift:E(r)};return{totalScore:Math.round(Object.entries(f).reduce((c,[a,i])=>c+i.score*m[a]/100,0)),dimensions:f,caps:o,daysAgo:s,checkResult:r}}function y(e){return e>=80?D:e>=60?S:b}function M(e){return e>=90?"A":e>=80?"B":e>=70?"C":e>=60?"D":"F"}function v(e,n=30){const t=Math.round(e/100*n),o=n-t;return"\u2588".repeat(t)+"\u2591".repeat(o)}function G(e,n){const t=y(e),o=M(e);console.log(),console.log(` ${p("\u{1F525} infernoflow health score")}`),console.log(),console.log(` ${t(p(String(e)))} / 100 ${t(p(o))} ${t(v(e))}`),console.log();const l={coverage:"Coverage ",documentation:"Docs/Tests ",freshness:"Freshness ",completeness:"Completeness ",drift:"Drift risk "};for(const[s,r]of Object.entries(n)){const f=m[s],u=r.score,c=y(u),a=v(u,20),i=Math.round(u*f/100);console.log(` ${p(l[s])} ${c(String(u).padStart(3))} ${c(a)} ${N(`\xD7${f}% = ${i}pts ${r.detail}`)}`)}console.log()}function H(e,n){const t=[];n.coverage.score<70&&t.push("Add descriptions to your capabilities in contract.json"),n.documentation.score<60&&t.push("Create scenario files in inferno/scenarios/ for each capability"),n.freshness.score<70&&t.push("Run `infernoflow suggest` to sync recent changes to the contract"),n.completeness.score<60&&t.push("Add version, tags, owner, and since fields to your capabilities"),n.drift.score<80&&t.push("Run `infernoflow check` and fix the reported issues"),t.length&&(console.log(` ${p("Tips to improve:")}`),t.forEach(o=>console.log(` ${S("\xB7")} ${o}`)),console.log())}async function q(e){const n=e.slice(1),t=n.includes("--json"),o=n.includes("--watch"),l=process.cwd(),s=h.join(l,"inferno");if(!d.existsSync(s)){const i="inferno/ not found. Run: infernoflow init";t?console.log(JSON.stringify({ok:!1,error:i})):C(i),process.exit(1)}const r=n.indexOf("--fail-below"),f=r!==-1?parseInt(n[r+1],10):null,u=n.indexOf("--interval"),c=u!==-1?parseInt(n[u+1],10):30,a=()=>{const{totalScore:i,dimensions:g}=W(s,l);if(t){const j=Object.fromEntries(Object.entries(g).map(([w,$])=>[w,{score:$.score,detail:$.detail,weight:m[w]}]));console.log(JSON.stringify({ok:!0,score:i,grade:M(i),dimensions:j}))}else o&&process.stdout.write("\x1Bc"),G(i,g),o||H(i,g);return f!==null&&i<f&&(t||console.error(b(` \u2717 Score ${i} is below threshold ${f} \u2014 failing.
2
+ `)),process.exit(1)),i};o?(t||O(`Watching health score every ${c}s \u2014 press Ctrl+C to stop`),a(),setInterval(a,c*1e3),await new Promise(()=>{})):a()}export{q as healthCommand};
@@ -1,325 +1,2 @@
1
- /**
2
- * infernoflow impact
3
- *
4
- * Before you touch a capability — see the blast radius.
5
- *
6
- * Given a capability ID, answers:
7
- * • Which caps directly depend on this one?
8
- * • Which caps transitively depend on it?
9
- * • What scenarios would be affected?
10
- * • What is the overall risk level? (low / medium / high / critical)
11
- * • Are any frozen/stable caps in the blast zone?
12
- *
13
- * Pure graph traversal — no AI needed. Fast and deterministic.
14
- *
15
- * Usage:
16
- * infernoflow impact auth-login
17
- * infernoflow impact auth-login --depth 5
18
- * infernoflow impact auth-login --json
19
- * infernoflow impact auth-login --check Exit 1 if risk is HIGH or CRITICAL
20
- */
21
-
22
- import * as fs from "node:fs";
23
- import * as path from "node:path";
24
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
25
-
26
- // ── helpers ───────────────────────────────────────────────────────────────────
27
-
28
- function loadJson(p) {
29
- try { return JSON.parse(fs.readFileSync(p, "utf8")); }
30
- catch { return null; }
31
- }
32
-
33
- const LEVEL_ICON = { frozen: "🧊", stable: "〰️ ", experimental: "🌊" };
34
- const LEVEL_COLOR = { frozen: red, stable: yellow, experimental: green };
35
-
36
- function stability(cap) {
37
- return cap?.stability || "experimental";
38
- }
39
-
40
- // ── blast radius (BFS on reverse graph) ──────────────────────────────────────
41
-
42
- /**
43
- * Walk the reverse dependency graph (dependents) starting from capId.
44
- * Returns: { direct: Set<string>, transitive: Set<string> }
45
- */
46
- function blastRadius(capId, dependents, maxDepth = 10) {
47
- const direct = new Set(dependents[capId] || []);
48
- const transitive = new Set();
49
- const queue = [...direct].map(id => ({ id, depth: 1 }));
50
- const visited = new Set([capId, ...direct]);
51
-
52
- while (queue.length > 0) {
53
- const { id, depth } = queue.shift();
54
- if (depth >= maxDepth) continue;
55
- for (const dep of (dependents[id] || [])) {
56
- if (!visited.has(dep)) {
57
- visited.add(dep);
58
- transitive.add(dep);
59
- queue.push({ id: dep, depth: depth + 1 });
60
- }
61
- }
62
- }
63
-
64
- return { direct, transitive };
65
- }
66
-
67
- // ── scenario finder ───────────────────────────────────────────────────────────
68
-
69
- function loadScenarios(infernoDir) {
70
- const scenariosDir = path.join(infernoDir, "scenarios");
71
- if (!fs.existsSync(scenariosDir)) return [];
72
-
73
- const scenarios = [];
74
- for (const f of fs.readdirSync(scenariosDir)) {
75
- if (!f.endsWith(".json")) continue;
76
- try {
77
- const s = JSON.parse(fs.readFileSync(path.join(scenariosDir, f), "utf8"));
78
- scenarios.push(s);
79
- } catch {}
80
- }
81
- return scenarios;
82
- }
83
-
84
- function scenariosForCap(capId, scenarios) {
85
- return scenarios.filter(s => {
86
- const covered = s.capabilitiesCovered || s.capabilities || [];
87
- return covered.some(c => c.toLowerCase() === capId.toLowerCase());
88
- });
89
- }
90
-
91
- // ── risk calculator ───────────────────────────────────────────────────────────
92
-
93
- /**
94
- * Risk levels:
95
- * critical — the target cap itself is frozen AND has dependents
96
- * high — a frozen cap is in the blast zone
97
- * medium — a stable cap is in the blast zone
98
- * low — all dependents are experimental
99
- */
100
- function computeRisk(targetCap, allInBlast, allCaps) {
101
- const targetLevel = stability(targetCap);
102
-
103
- if (targetLevel === "frozen" && allInBlast.size > 0) {
104
- return "critical";
105
- }
106
-
107
- for (const id of allInBlast) {
108
- const cap = allCaps.find(c => c.id === id);
109
- if (stability(cap) === "frozen") return "high";
110
- }
111
-
112
- for (const id of allInBlast) {
113
- const cap = allCaps.find(c => c.id === id);
114
- if (stability(cap) === "stable") return "medium";
115
- }
116
-
117
- return "low";
118
- }
119
-
120
- const RISK_COLOR = {
121
- critical: red,
122
- high: red,
123
- medium: yellow,
124
- low: green,
125
- };
126
-
127
- const RISK_ICON = {
128
- critical: "🔴",
129
- high: "🔴",
130
- medium: "🟡",
131
- low: "🟢",
132
- };
133
-
134
- const RISK_ADVICE = {
135
- critical: "This capability is FROZEN — any change is high-risk. Requires explicit approval.",
136
- high: "A frozen capability depends on this — test thoroughly before merging.",
137
- medium: "A stable capability is in the blast zone — prefer additive changes only.",
138
- low: "All dependents are experimental — safe to iterate freely.",
139
- };
140
-
141
- // ── printer ───────────────────────────────────────────────────────────────────
142
-
143
- function printImpact({ capId, targetCap, direct, transitive, affectedScenarios, risk, allCaps, deps }) {
144
- const targetLevel = stability(targetCap);
145
- const targetIcon = LEVEL_ICON[targetLevel] || "🌊";
146
- const targetColor = LEVEL_COLOR[targetLevel] || green;
147
- const riskColor = RISK_COLOR[risk];
148
- const riskIcon = RISK_ICON[risk];
149
-
150
- console.log();
151
- console.log(bold(` ${targetIcon} ${targetColor(capId)}`));
152
- if (targetCap?.name || targetCap?.title) {
153
- console.log(gray(` ${targetCap.name || targetCap.title}`));
154
- }
155
- console.log(gray(` stability: `) + targetColor(targetLevel));
156
- console.log();
157
-
158
- // What this cap calls (downstream)
159
- if (deps.length > 0) {
160
- console.log(gray(" This cap calls:"));
161
- for (const dep of deps) {
162
- const d = allCaps.find(c => c.id === dep);
163
- const icon = LEVEL_ICON[stability(d)] || "🌊";
164
- const color = LEVEL_COLOR[stability(d)] || green;
165
- console.log(` ${icon} ${color(dep)}`);
166
- }
167
- console.log();
168
- }
169
-
170
- // Blast radius
171
- if (direct.size === 0) {
172
- console.log(gray(" No capabilities depend on this one."));
173
- console.log(gray(" ✔ Safe to change freely."));
174
- console.log();
175
- } else {
176
- console.log(bold(" Direct dependents (will be immediately affected):"));
177
- for (const id of direct) {
178
- const cap = allCaps.find(c => c.id === id);
179
- const level = stability(cap);
180
- const icon = LEVEL_ICON[level] || "🌊";
181
- const color = LEVEL_COLOR[level] || green;
182
- console.log(` ${icon} ${color(id)}`);
183
- }
184
- console.log();
185
-
186
- if (transitive.size > 0) {
187
- console.log(bold(" Transitive dependents (indirectly affected):"));
188
- for (const id of transitive) {
189
- const cap = allCaps.find(c => c.id === id);
190
- const level = stability(cap);
191
- const icon = LEVEL_ICON[level] || "🌊";
192
- const color = LEVEL_COLOR[level] || green;
193
- console.log(` ${icon} ${color(id)}`);
194
- }
195
- console.log();
196
- }
197
- }
198
-
199
- // Affected scenarios
200
- if (affectedScenarios.length > 0) {
201
- console.log(bold(" Scenarios at risk:"));
202
- for (const s of affectedScenarios) {
203
- console.log(` ${yellow("⚠")} ${s.scenarioId || s.description || "(unnamed)"}`);
204
- if (s.description) console.log(gray(` ${s.description}`));
205
- }
206
- console.log();
207
- } else if (direct.size > 0) {
208
- console.log(gray(" No scenarios cover the affected capabilities."));
209
- console.log(gray(" Consider adding scenarios before making this change."));
210
- console.log();
211
- }
212
-
213
- // Risk banner
214
- console.log(` ${riskIcon} Risk level: ${bold(riskColor(risk.toUpperCase()))}`);
215
- console.log(` ${gray(RISK_ADVICE[risk])}`);
216
- console.log();
217
-
218
- // Summary numbers
219
- const total = direct.size + transitive.size;
220
- if (total > 0) {
221
- console.log(gray(
222
- ` ── ${direct.size} direct · ${transitive.size} transitive · ${total} total affected · ${affectedScenarios.length} scenario(s) at risk`
223
- ));
224
- console.log();
225
- }
226
- }
227
-
228
- // ── entry point ───────────────────────────────────────────────────────────────
229
-
230
- export async function impactCommand(rawArgs) {
231
- const args = (rawArgs || []).slice(1); // skip command name
232
- const jsonMode = args.includes("--json");
233
- const checkMode = args.includes("--check");
234
- const depthIdx = args.indexOf("--depth");
235
- const maxDepth = depthIdx !== -1 ? parseInt(args[depthIdx + 1], 10) || 10 : 10;
236
-
237
- const capId = args.find((a, i) => !a.startsWith("--") && (depthIdx === -1 || i !== depthIdx + 1));
238
-
239
- if (!capId) {
240
- console.error(red("✗ Usage: infernoflow impact <capability-id> [--depth N] [--json] [--check]"));
241
- console.error(gray(" Example: infernoflow impact user-auth"));
242
- process.exit(1);
243
- }
244
-
245
- const cwd = process.cwd();
246
- const infernoDir = path.join(cwd, "inferno");
247
-
248
- // Load graph
249
- const graph = loadJson(path.join(infernoDir, "graph.json"));
250
- if (!graph) {
251
- console.error(red("✗ inferno/graph.json not found — run `infernoflow graph` first."));
252
- process.exit(1);
253
- }
254
-
255
- // Load capabilities
256
- let allCaps = [];
257
- const rawCaps = loadJson(path.join(infernoDir, "capabilities.json"));
258
- if (rawCaps) allCaps = Array.isArray(rawCaps) ? rawCaps : (rawCaps.capabilities || []);
259
-
260
- // Validate cap exists
261
- const targetCap = allCaps.find(c => c.id === capId);
262
- if (!targetCap) {
263
- console.error(red(`✗ Capability "${capId}" not found in capabilities.json`));
264
- console.error(gray(" Run: infernoflow stability — to list all capability IDs"));
265
- process.exit(1);
266
- }
267
-
268
- // Blast radius
269
- const { direct, transitive } = blastRadius(capId, graph.dependents || {}, maxDepth);
270
- const allInBlast = new Set([...direct, ...transitive]);
271
-
272
- // Dependencies (what this cap calls)
273
- const deps = graph.deps?.[capId] || [];
274
-
275
- // Scenarios
276
- const scenarios = loadScenarios(infernoDir);
277
- const targetScenarios = scenariosForCap(capId, scenarios);
278
- // Collect scenarios for all affected caps too
279
- const affectedScenarioSet = new Map();
280
- for (const s of targetScenarios) {
281
- affectedScenarioSet.set(s.scenarioId || s.description, s);
282
- }
283
- for (const id of allInBlast) {
284
- for (const s of scenariosForCap(id, scenarios)) {
285
- affectedScenarioSet.set(s.scenarioId || s.description, s);
286
- }
287
- }
288
- const affectedScenarios = [...affectedScenarioSet.values()];
289
-
290
- // Risk
291
- const risk = computeRisk(targetCap, allInBlast, allCaps);
292
-
293
- // JSON mode
294
- if (jsonMode) {
295
- const out = {
296
- capId,
297
- name: targetCap.name || targetCap.title,
298
- stability: stability(targetCap),
299
- risk,
300
- direct: [...direct],
301
- transitive: [...transitive],
302
- deps,
303
- affectedScenarios: affectedScenarios.map(s => s.scenarioId || s.description),
304
- summary: {
305
- directCount: direct.size,
306
- transitiveCount: transitive.size,
307
- totalAffected: allInBlast.size,
308
- scenariosAtRisk: affectedScenarios.length,
309
- },
310
- };
311
- console.log(JSON.stringify(out, null, 2));
312
- if (checkMode && (risk === "high" || risk === "critical")) process.exit(1);
313
- return;
314
- }
315
-
316
- console.log(gray(`\n infernoflow impact → ${bold(capId)}`));
317
- console.log(gray(" ──────────────────────────────────────────────────────────────"));
318
-
319
- printImpact({ capId, targetCap, direct, transitive, affectedScenarios, risk, allCaps, deps });
320
-
321
- if (checkMode && (risk === "high" || risk === "critical")) {
322
- console.log(red(" ✗ --check failed: risk level is " + risk.toUpperCase()));
323
- process.exit(1);
324
- }
325
- }
1
+ import*as R from"node:fs";import*as k from"node:path";import{bold as C,gray as l,green as j,yellow as A,red as x}from"../ui/output.mjs";function E(t){try{return JSON.parse(R.readFileSync(t,"utf8"))}catch{return null}}const L={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},O={frozen:x,stable:A,experimental:j};function h(t){return t?.stability||"experimental"}function _(t,o,i=10){const c=new Set(o[t]||[]),n=new Set,r=[...c].map(u=>({id:u,depth:1})),e=new Set([t,...c]);for(;r.length>0;){const{id:u,depth:f}=r.shift();if(!(f>=i))for(const d of o[u]||[])e.has(d)||(e.add(d),n.add(d),r.push({id:d,depth:f+1}))}return{direct:c,transitive:n}}function F(t){const o=k.join(t,"scenarios");if(!R.existsSync(o))return[];const i=[];for(const c of R.readdirSync(o))if(c.endsWith(".json"))try{const n=JSON.parse(R.readFileSync(k.join(o,c),"utf8"));i.push(n)}catch{}return i}function D(t,o){return o.filter(i=>(i.capabilitiesCovered||i.capabilities||[]).some(n=>n.toLowerCase()===t.toLowerCase()))}function J(t,o,i){if(h(t)==="frozen"&&o.size>0)return"critical";for(const n of o){const r=i.find(e=>e.id===n);if(h(r)==="frozen")return"high"}for(const n of o){const r=i.find(e=>e.id===n);if(h(r)==="stable")return"medium"}return"low"}const K={critical:x,high:x,medium:A,low:j},M={critical:"\u{1F534}",high:"\u{1F534}",medium:"\u{1F7E1}",low:"\u{1F7E2}"},T={critical:"This capability is FROZEN \u2014 any change is high-risk. Requires explicit approval.",high:"A frozen capability depends on this \u2014 test thoroughly before merging.",medium:"A stable capability is in the blast zone \u2014 prefer additive changes only.",low:"All dependents are experimental \u2014 safe to iterate freely."};function U({capId:t,targetCap:o,direct:i,transitive:c,affectedScenarios:n,risk:r,allCaps:e,deps:u}){const f=h(o),d=L[f]||"\u{1F30A}",w=O[f]||j,I=K[r],m=M[r];if(console.log(),console.log(C(` ${d} ${w(t)}`)),(o?.name||o?.title)&&console.log(l(` ${o.name||o.title}`)),console.log(l(" stability: ")+w(f)),console.log(),u.length>0){console.log(l(" This cap calls:"));for(const s of u){const p=e.find(S=>S.id===s),g=L[h(p)]||"\u{1F30A}",y=O[h(p)]||j;console.log(` ${g} ${y(s)}`)}console.log()}if(i.size===0)console.log(l(" No capabilities depend on this one.")),console.log(l(" \u2714 Safe to change freely.")),console.log();else{console.log(C(" Direct dependents (will be immediately affected):"));for(const s of i){const p=e.find(v=>v.id===s),g=h(p),y=L[g]||"\u{1F30A}",S=O[g]||j;console.log(` ${y} ${S(s)}`)}if(console.log(),c.size>0){console.log(C(" Transitive dependents (indirectly affected):"));for(const s of c){const p=e.find(v=>v.id===s),g=h(p),y=L[g]||"\u{1F30A}",S=O[g]||j;console.log(` ${y} ${S(s)}`)}console.log()}}if(n.length>0){console.log(C(" Scenarios at risk:"));for(const s of n)console.log(` ${A("\u26A0")} ${s.scenarioId||s.description||"(unnamed)"}`),s.description&&console.log(l(` ${s.description}`));console.log()}else i.size>0&&(console.log(l(" No scenarios cover the affected capabilities.")),console.log(l(" Consider adding scenarios before making this change.")),console.log());console.log(` ${m} Risk level: ${C(I(r.toUpperCase()))}`),console.log(` ${l(T[r])}`),console.log();const $=i.size+c.size;$>0&&(console.log(l(` \u2500\u2500 ${i.size} direct \xB7 ${c.size} transitive \xB7 ${$} total affected \xB7 ${n.length} scenario(s) at risk`)),console.log())}async function W(t){const o=(t||[]).slice(1),i=o.includes("--json"),c=o.includes("--check"),n=o.indexOf("--depth"),r=n!==-1&&parseInt(o[n+1],10)||10,e=o.find((a,b)=>!a.startsWith("--")&&(n===-1||b!==n+1));e||(console.error(x("\u2717 Usage: infernoflow impact <capability-id> [--depth N] [--json] [--check]")),console.error(l(" Example: infernoflow impact user-auth")),process.exit(1));const u=process.cwd(),f=k.join(u,"inferno"),d=E(k.join(f,"graph.json"));d||(console.error(x("\u2717 inferno/graph.json not found \u2014 run `infernoflow graph` first.")),process.exit(1));let w=[];const I=E(k.join(f,"capabilities.json"));I&&(w=Array.isArray(I)?I:I.capabilities||[]);const m=w.find(a=>a.id===e);m||(console.error(x(`\u2717 Capability "${e}" not found in capabilities.json`)),console.error(l(" Run: infernoflow stability \u2014 to list all capability IDs")),process.exit(1));const{direct:$,transitive:s}=_(e,d.dependents||{},r),p=new Set([...$,...s]),g=d.deps?.[e]||[],y=F(f),S=D(e,y),v=new Map;for(const a of S)v.set(a.scenarioId||a.description,a);for(const a of p)for(const b of D(a,y))v.set(b.scenarioId||b.description,b);const N=[...v.values()],z=J(m,p,w);if(i){const a={capId:e,name:m.name||m.title,stability:h(m),risk:z,direct:[...$],transitive:[...s],deps:g,affectedScenarios:N.map(b=>b.scenarioId||b.description),summary:{directCount:$.size,transitiveCount:s.size,totalAffected:p.size,scenariosAtRisk:N.length}};console.log(JSON.stringify(a,null,2)),c&&(z==="high"||z==="critical")&&process.exit(1);return}console.log(l(`
2
+ infernoflow impact \u2192 ${C(e)}`)),console.log(l(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),U({capId:e,targetCap:m,direct:$,transitive:s,affectedScenarios:N,risk:z,allCaps:w,deps:g}),c&&(z==="high"||z==="critical")&&(console.log(x(" \u2717 --check failed: risk level is "+z.toUpperCase())),process.exit(1))}export{W as impactCommand};