infernoflow 0.37.1 → 0.37.4

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 +71 -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,299 +1,4 @@
1
- /**
2
- * infernoflow ask
3
- *
4
- * Query session memory by keyword, topic, or type.
5
- * The killer use case: before an AI agent tries something, it asks
6
- * "what have we already tried for this?" — avoiding repeated mistakes.
7
- *
8
- * Ranking:
9
- * 1. gotcha (highest — these are landmines, must surface first)
10
- * 2. decision (architectural choices)
11
- * 3. attempt (things tried, especially failed ones)
12
- * 4. preference
13
- * 5. theme
14
- * 6. note / error / handoff
15
- *
16
- * Usage:
17
- * infernoflow ask "auth" Search all entries containing "auth"
18
- * infernoflow ask "upload flow" Multi-word fuzzy search
19
- * infernoflow ask --type gotcha All gotchas, no filter
20
- * infernoflow ask "s3" --type attempt Attempts mentioning S3
21
- * infernoflow ask --recent Last 10 entries regardless of type
22
- * infernoflow ask "stripe" --json Machine-readable results
23
- *
24
- * MCP use (agent-facing):
25
- * infernoflow_ask({ query: "what did we try for payments?" })
26
- * → Returns relevant entries so the agent avoids repeating failed work
27
- */
28
-
29
- import * as fs from "node:fs";
30
- import * as path from "node:path";
31
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
32
-
33
- const INFERNO_DIR = "inferno";
34
- const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
35
-
36
- // ── Type priority (lower = shown first) ──────────────────────────────────────
37
-
38
- const TYPE_PRIORITY = {
39
- gotcha: 0,
40
- decision: 1,
41
- attempt: 2,
42
- preference: 3,
43
- theme: 4,
44
- note: 5,
45
- error: 5,
46
- handoff: 6,
47
- };
48
-
49
- const TYPE_ICONS = {
50
- gotcha: "⚠",
51
- decision: "✓",
52
- attempt: "↺",
53
- preference: "♦",
54
- theme: "🎨",
55
- note: "·",
56
- error: "✗",
57
- handoff: "→",
58
- };
59
-
60
- const TYPE_COLORS = {
61
- gotcha: yellow,
62
- decision: green,
63
- attempt: cyan,
64
- preference: cyan,
65
- theme: cyan,
66
- note: gray,
67
- error: red,
68
- handoff: gray,
69
- };
70
-
71
- // ── Tokeniser for fuzzy matching ──────────────────────────────────────────────
72
-
73
- function tokenise(str) {
74
- return str.toLowerCase()
75
- .replace(/[^a-z0-9\s]/g, " ")
76
- .split(/\s+/)
77
- .filter(t => t.length > 1);
78
- }
79
-
80
- /**
81
- * Score an entry against a query.
82
- * Returns 0 if no match, >0 otherwise.
83
- * Higher score = more relevant.
84
- */
85
- function scoreEntry(entry, queryTokens) {
86
- const text = [entry.summary || "", entry.type || ""].join(" ").toLowerCase();
87
- const entryTokens = tokenise(text);
88
-
89
- let score = 0;
90
- for (const qt of queryTokens) {
91
- // Exact substring match in summary (strongest signal)
92
- if ((entry.summary || "").toLowerCase().includes(qt)) {
93
- score += 3;
94
- }
95
- // Token in entry text
96
- if (entryTokens.includes(qt)) {
97
- score += 1;
98
- }
99
- // Partial prefix match (e.g. "auth" matches "authentication")
100
- if (entryTokens.some(et => et.startsWith(qt) || qt.startsWith(et))) {
101
- score += 0.5;
102
- }
103
- }
104
-
105
- return score;
106
- }
107
-
108
- // ── formatters ────────────────────────────────────────────────────────────────
109
-
110
- function fmtRelDate(iso) {
111
- if (!iso) return "";
112
- const d = new Date(iso);
113
- const diff = Date.now() - d.getTime();
114
- const days = Math.floor(diff / 86400000);
115
- if (days === 0) return "today";
116
- if (days === 1) return "yesterday";
117
- if (days < 7) return `${days}d ago`;
118
- if (days < 30) return `${Math.floor(days / 7)}w ago`;
119
- return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short" });
120
- }
121
-
122
- function printEntry(entry, highlight) {
123
- const type = entry.type || "note";
124
- const icon = TYPE_ICONS[type] || "·";
125
- const colorFn = TYPE_COLORS[type] || gray;
126
- const result = entry.result ? gray(` [${entry.result}]`) : "";
127
- const agent = entry.agent ? gray(` — ${entry.agent}`) : "";
128
- const date = gray(` (${fmtRelDate(entry.ts)})`);
129
-
130
- let summary = entry.summary || "";
131
-
132
- // Bold the matched keyword in the summary
133
- if (highlight) {
134
- for (const word of highlight) {
135
- const re = new RegExp(`(${word})`, "gi");
136
- summary = summary.replace(re, (m) => bold(m));
137
- }
138
- }
139
-
140
- console.log(` ${colorFn(icon + " " + type.padEnd(11))}${result}${agent}${date}`);
141
- console.log(` ${summary}`);
142
- }
143
-
144
- // ── load + search ─────────────────────────────────────────────────────────────
145
-
146
- function loadSessions(cwd) {
147
- const p = path.join(cwd, SESSIONS_FILE);
148
- if (!fs.existsSync(p)) return [];
149
- return fs.readFileSync(p, "utf8")
150
- .split("\n").filter(Boolean)
151
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
152
- .filter(Boolean);
153
- }
154
-
155
- function search(entries, queryTokens, typeFilter, limit) {
156
- let candidates = entries;
157
-
158
- // Filter by type if specified
159
- if (typeFilter) {
160
- candidates = candidates.filter(e => (e.type || "note") === typeFilter);
161
- }
162
-
163
- // Score and filter
164
- let scored;
165
- if (queryTokens.length > 0) {
166
- scored = candidates
167
- .map(e => ({ entry: e, score: scoreEntry(e, queryTokens) }))
168
- .filter(({ score }) => score > 0)
169
- .sort((a, b) => {
170
- // Primary: score descending
171
- if (b.score !== a.score) return b.score - a.score;
172
- // Secondary: type priority
173
- const pa = TYPE_PRIORITY[a.entry.type] ?? 9;
174
- const pb = TYPE_PRIORITY[b.entry.type] ?? 9;
175
- if (pa !== pb) return pa - pb;
176
- // Tertiary: newest first
177
- return new Date(b.entry.ts || 0) - new Date(a.entry.ts || 0);
178
- });
179
- } else {
180
- // No query — sort by type priority then newest
181
- scored = candidates
182
- .map(e => ({ entry: e, score: 1 }))
183
- .sort((a, b) => {
184
- const pa = TYPE_PRIORITY[a.entry.type] ?? 9;
185
- const pb = TYPE_PRIORITY[b.entry.type] ?? 9;
186
- if (pa !== pb) return pa - pb;
187
- return new Date(b.entry.ts || 0) - new Date(a.entry.ts || 0);
188
- });
189
- }
190
-
191
- return scored.slice(0, limit || 20);
192
- }
193
-
194
- // ── entry point ───────────────────────────────────────────────────────────────
195
-
196
- export async function askCommand(rawArgs = []) {
197
- const args = rawArgs;
198
-
199
- // Parse flags
200
- const typeIdx = args.indexOf("--type");
201
- const typeFilter = typeIdx !== -1 ? args[typeIdx + 1] : null;
202
- const limitIdx = args.indexOf("--limit") !== -1 ? args.indexOf("--limit") : args.indexOf("-n");
203
- const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1] || "20", 10) : 15;
204
- const jsonMode = args.includes("--json");
205
- const recentMode = args.includes("--recent") || args.includes("-r");
206
-
207
- // Query = all non-flag args joined
208
- const queryWords = args.filter((a, i) => {
209
- if (a.startsWith("--")) return false;
210
- if (i > 0 && args[i - 1].startsWith("--")) return false; // flag value
211
- return true;
212
- });
213
- const queryStr = queryWords.join(" ").trim();
214
- const queryTokens = recentMode ? [] : tokenise(queryStr);
215
-
216
- const cwd = process.cwd();
217
- const sessions = loadSessions(cwd);
218
-
219
- if (sessions.length === 0) {
220
- if (jsonMode) { console.log(JSON.stringify({ results: [], total: 0 })); return; }
221
- console.log(gray("\n No session memory yet."));
222
- console.log(gray(" Run: infernoflow log \"<what happened>\" --type gotcha\n"));
223
- return;
224
- }
225
-
226
- // For --recent: just return last N entries without scoring
227
- const results = recentMode
228
- ? sessions.slice(-limit).reverse().map(e => ({ entry: e, score: 1 }))
229
- : search(sessions, queryTokens, typeFilter, limit);
230
-
231
- if (jsonMode) {
232
- console.log(JSON.stringify({
233
- query: queryStr,
234
- type: typeFilter,
235
- total: sessions.length,
236
- matched: results.length,
237
- results: results.map(({ entry, score }) => ({ ...entry, relevanceScore: score })),
238
- }, null, 2));
239
- return;
240
- }
241
-
242
- // ── Header ──────────────────────────────────────────────────────────────
243
- console.log();
244
- if (queryStr) {
245
- console.log(` ${bold("🔥 infernoflow ask")} ${cyan(`"${queryStr}"`)}${typeFilter ? gray(` [${typeFilter}]`) : ""}`);
246
- } else if (recentMode) {
247
- console.log(` ${bold("🔥 infernoflow ask")} ${gray("— recent entries")}`);
248
- } else {
249
- console.log(` ${bold("🔥 infernoflow ask")} ${gray("— all entries")}${typeFilter ? gray(` [${typeFilter}]`) : ""}`);
250
- }
251
- console.log(gray(` ${"─".repeat(52)}`));
252
-
253
- if (results.length === 0) {
254
- console.log();
255
- if (queryStr) {
256
- console.log(gray(` No entries found for "${queryStr}"`));
257
- if (typeFilter) console.log(gray(` Try removing --type ${typeFilter} to widen the search`));
258
- } else {
259
- console.log(gray(" No entries found."));
260
- }
261
- console.log();
262
- return;
263
- }
264
-
265
- // ── Group by type for display ────────────────────────────────────────────
266
- const byType = new Map();
267
- for (const { entry, score } of results) {
268
- const t = entry.type || "note";
269
- if (!byType.has(t)) byType.set(t, []);
270
- byType.get(t).push({ entry, score });
271
- }
272
-
273
- // Print in priority order
274
- const typeOrder = Object.keys(TYPE_PRIORITY).sort((a, b) => TYPE_PRIORITY[a] - TYPE_PRIORITY[b]);
275
-
276
- let printed = 0;
277
- for (const type of typeOrder) {
278
- const group = byType.get(type);
279
- if (!group?.length) continue;
280
-
281
- console.log();
282
- const colorFn = TYPE_COLORS[type] || gray;
283
- console.log(colorFn(` ${TYPE_ICONS[type]} ${type.toUpperCase()}S (${group.length})`));
284
- console.log(gray(" " + "─".repeat(50)));
285
-
286
- for (const { entry } of group) {
287
- console.log();
288
- printEntry(entry, queryTokens);
289
- printed++;
290
- }
291
- }
292
-
293
- console.log();
294
- console.log(gray(` ${printed} result${printed !== 1 ? "s" : ""} from ${sessions.length} total entries`));
295
- if (results.length === limit && sessions.length > limit) {
296
- console.log(gray(` Use --limit N to see more`));
297
- }
298
- console.log();
299
- }
1
+ import*as x from"node:fs";import*as E from"node:path";import{bold as w,cyan as S,gray as s,green as k,yellow as C,red as L}from"../ui/output.mjs";const W="inferno",q=E.join(W,"sessions.jsonl"),p={gotcha:0,decision:1,attempt:2,preference:3,theme:4,note:5,error:5,handoff:6},I={gotcha:"\u26A0",decision:"\u2713",attempt:"\u21BA",preference:"\u2666",theme:"\u{1F3A8}",note:"\xB7",error:"\u2717",handoff:"\u2192"},D={gotcha:C,decision:k,attempt:S,preference:S,theme:S,note:s,error:L,handoff:s};function j(o){return o.toLowerCase().replace(/[^a-z0-9\s]/g," ").split(/\s+/).filter(e=>e.length>1)}function F(o,e){const r=[o.summary||"",o.type||""].join(" ").toLowerCase(),n=j(r);let i=0;for(const c of e)(o.summary||"").toLowerCase().includes(c)&&(i+=3),n.includes(c)&&(i+=1),n.some(t=>t.startsWith(c)||c.startsWith(t))&&(i+=.5);return i}function M(o){if(!o)return"";const e=new Date(o),r=Date.now()-e.getTime(),n=Math.floor(r/864e5);return n===0?"today":n===1?"yesterday":n<7?`${n}d ago`:n<30?`${Math.floor(n/7)}w ago`:e.toLocaleDateString("en-GB",{day:"2-digit",month:"short"})}function _(o,e){const r=o.type||"note",n=I[r]||"\xB7",i=D[r]||s,c=o.result?s(` [${o.result}]`):"",t=o.agent?s(` \u2014 ${o.agent}`):"",a=s(` (${M(o.ts)})`);let g=o.summary||"";if(e)for(const f of e){const h=new RegExp(`(${f})`,"gi");g=g.replace(h,O=>w(O))}console.log(` ${i(n+" "+r.padEnd(11))}${c}${t}${a}`),console.log(` ${g}`)}function P(o){const e=E.join(o,q);return x.existsSync(e)?x.readFileSync(e,"utf8").split(`
2
+ `).filter(Boolean).map(r=>{try{return JSON.parse(r)}catch{return null}}).filter(Boolean):[]}function Y(o,e,r,n){let i=o;r&&(i=i.filter(t=>(t.type||"note")===r));let c;return e.length>0?c=i.map(t=>({entry:t,score:F(t,e)})).filter(({score:t})=>t>0).sort((t,a)=>{if(a.score!==t.score)return a.score-t.score;const g=p[t.entry.type]??9,f=p[a.entry.type]??9;return g!==f?g-f:new Date(a.entry.ts||0)-new Date(t.entry.ts||0)}):c=i.map(t=>({entry:t,score:1})).sort((t,a)=>{const g=p[t.entry.type]??9,f=p[a.entry.type]??9;return g!==f?g-f:new Date(a.entry.ts||0)-new Date(t.entry.ts||0)}),c.slice(0,n||20)}async function B(o=[]){const e=o,r=e.indexOf("--type"),n=r!==-1?e[r+1]:null,i=e.indexOf("--limit")!==-1?e.indexOf("--limit"):e.indexOf("-n"),c=i!==-1?parseInt(e[i+1]||"20",10):15,t=e.includes("--json"),a=e.includes("--recent")||e.includes("-r"),f=e.filter((l,u)=>!(l.startsWith("--")||u>0&&e[u-1].startsWith("--"))).join(" ").trim(),h=a?[]:j(f),O=process.cwd(),y=P(O);if(y.length===0){if(t){console.log(JSON.stringify({results:[],total:0}));return}console.log(s(`
3
+ No session memory yet.`)),console.log(s(` Run: infernoflow log "<what happened>" --type gotcha
4
+ `));return}const d=a?y.slice(-c).reverse().map(l=>({entry:l,score:1})):Y(y,h,n,c);if(t){console.log(JSON.stringify({query:f,type:n,total:y.length,matched:d.length,results:d.map(({entry:l,score:u})=>({...l,relevanceScore:u}))},null,2));return}if(console.log(),console.log(f?` ${w("\u{1F525} infernoflow ask")} ${S(`"${f}"`)}${n?s(` [${n}]`):""}`:a?` ${w("\u{1F525} infernoflow ask")} ${s("\u2014 recent entries")}`:` ${w("\u{1F525} infernoflow ask")} ${s("\u2014 all entries")}${n?s(` [${n}]`):""}`),console.log(s(` ${"\u2500".repeat(52)}`)),d.length===0){console.log(),f?(console.log(s(` No entries found for "${f}"`)),n&&console.log(s(` Try removing --type ${n} to widen the search`))):console.log(s(" No entries found.")),console.log();return}const $=new Map;for(const{entry:l,score:u}of d){const m=l.type||"note";$.has(m)||$.set(m,[]),$.get(m).push({entry:l,score:u})}const R=Object.keys(p).sort((l,u)=>p[l]-p[u]);let N=0;for(const l of R){const u=$.get(l);if(!u?.length)continue;console.log();const m=D[l]||s;console.log(m(` ${I[l]} ${l.toUpperCase()}S (${u.length})`)),console.log(s(" "+"\u2500".repeat(50)));for(const{entry:T}of u)console.log(),_(T,h),N++}console.log(),console.log(s(` ${N} result${N!==1?"s":""} from ${y.length} total entries`)),d.length===c&&y.length>c&&console.log(s(" Use --limit N to see more")),console.log()}export{B as askCommand};
@@ -1,204 +1,6 @@
1
- /**
2
- * infernoflow audit
3
- *
4
- * Classify capabilities by security sensitivity and generate a surface map.
5
- * Tags each capability with one or more sensitivity labels:
6
- * auth — authentication / authorization / sessions / tokens
7
- * payment — billing, subscriptions, pricing, invoices
8
- * pii — personal data, email, address, phone, GDPR scope
9
- * admin — privileged operations, configuration, user management
10
- * public — read-only, no auth required
11
- *
12
- * Stores results in inferno/audit.json (git-trackable).
13
- *
14
- * Usage:
15
- * infernoflow audit Run audit, print summary
16
- * infernoflow audit --format json Machine-readable JSON to stdout
17
- * infernoflow audit --format html Write HTML report
18
- * infernoflow audit --out audit.html Custom output path
19
- * infernoflow audit --fail-on high Exit 1 if any HIGH caps are unreviewed
20
- * infernoflow audit --json Alias for --format json
21
- */
22
-
23
- import * as fs from "node:fs";
24
- import * as path from "node:path";
25
- import { done, warn, info, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
26
-
27
- const AUDIT_FILE = "audit.json";
28
-
29
- // ── Sensitivity keyword maps ──────────────────────────────────────────────────
30
-
31
- const SENSITIVITY_RULES = [
32
- {
33
- tag: "auth",
34
- severity: "high",
35
- label: "Authentication / Authorization",
36
- keywords: [
37
- "auth", "login", "logout", "signin", "signout", "signup",
38
- "password", "credential", "token", "session", "oauth",
39
- "jwt", "permission", "role", "access", "privilege", "2fa",
40
- "mfa", "sso", "saml", "openid", "verify", "authenticate",
41
- ],
42
- },
43
- {
44
- tag: "payment",
45
- severity: "high",
46
- label: "Payment / Billing",
47
- keywords: [
48
- "payment", "billing", "invoice", "charge", "subscription",
49
- "checkout", "stripe", "card", "credit", "debit", "price",
50
- "plan", "tier", "coupon", "refund", "transaction", "purchase",
51
- "order", "cart", "paypal", "wallet",
52
- ],
53
- },
54
- {
55
- tag: "pii",
56
- severity: "high",
57
- label: "Personal / PII Data",
58
- keywords: [
59
- "pii", "personal", "profile", "email", "phone", "address",
60
- "name", "user", "account", "gdpr", "privacy", "data",
61
- "export", "download", "delete account", "identity", "dob",
62
- "birthday", "ssn", "passport", "tax",
63
- ],
64
- },
65
- {
66
- tag: "admin",
67
- severity: "medium",
68
- label: "Admin / Privileged",
69
- keywords: [
70
- "admin", "manage", "config", "setting", "system", "deploy",
71
- "migration", "seed", "reset", "purge", "archive", "batch",
72
- "bulk", "impersonate", "override", "feature flag",
73
- ],
74
- },
75
- {
76
- tag: "public",
77
- severity: "low",
78
- label: "Public / Read-only",
79
- keywords: [
80
- "list", "search", "view", "read", "fetch", "get", "show",
81
- "display", "browse", "filter", "sort", "paginate",
82
- ],
83
- },
84
- ];
85
-
86
- const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, unknown: 0 };
87
-
88
- // ── Classification ────────────────────────────────────────────────────────────
89
-
90
- function classifyCapability(cap) {
91
- const text = [
92
- typeof cap === "string" ? cap : "",
93
- cap?.id || "",
94
- cap?.name || "",
95
- cap?.description || "",
96
- (cap?.tags || []).join(" "),
97
- ].join(" ").toLowerCase();
98
-
99
- const matched = [];
100
- for (const rule of SENSITIVITY_RULES) {
101
- if (rule.keywords.some(kw => text.includes(kw))) {
102
- matched.push(rule);
103
- }
104
- }
105
-
106
- // Remove "public" if any higher-severity tag also matched
107
- const hasHigher = matched.some(r => r.tag !== "public" && r.severity !== "low");
108
- const filtered = hasHigher ? matched.filter(r => r.tag !== "public") : matched;
109
-
110
- if (!filtered.length) {
111
- return { tags: ["unknown"], severity: "unknown", labels: ["Unclassified"] };
112
- }
113
-
114
- const severity = filtered.reduce((best, r) => {
115
- return SEVERITY_ORDER[r.severity] > SEVERITY_ORDER[best] ? r.severity : best;
116
- }, "low");
117
-
118
- return {
119
- tags: filtered.map(r => r.tag),
120
- severity,
121
- labels: filtered.map(r => r.label),
122
- };
123
- }
124
-
125
- // ── Storage ───────────────────────────────────────────────────────────────────
126
-
127
- function readAudit(infernoDir) {
128
- const p = path.join(infernoDir, AUDIT_FILE);
129
- if (!fs.existsSync(p)) return {};
130
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return {}; }
131
- }
132
-
133
- function writeAudit(infernoDir, data) {
134
- fs.writeFileSync(path.join(infernoDir, AUDIT_FILE), JSON.stringify(data, null, 2) + "\n");
135
- }
136
-
137
- function readContract(infernoDir) {
138
- for (const f of ["contract.json", "capabilities.json"]) {
139
- const p = path.join(infernoDir, f);
140
- if (fs.existsSync(p)) {
141
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
142
- }
143
- }
144
- return null;
145
- }
146
-
147
- // ── Formatters ────────────────────────────────────────────────────────────────
148
-
149
- function severityColor(sev) {
150
- if (sev === "high") return red;
151
- if (sev === "medium") return yellow;
152
- if (sev === "low") return green;
153
- return gray;
154
- }
155
-
156
- function severityIcon(sev) {
157
- if (sev === "high") return "🔴";
158
- if (sev === "medium") return "🟡";
159
- if (sev === "low") return "🟢";
160
- return "⚪";
161
- }
162
-
163
- function printTextReport(results, stats) {
164
- const bySeverity = { high: [], medium: [], low: [], unknown: [] };
165
- for (const r of results) (bySeverity[r.severity] || bySeverity.unknown).push(r);
166
-
167
- console.log();
168
- console.log(` ${bold("🔥 infernoflow audit — Security Surface Map")}`);
169
- console.log();
170
- console.log(` ${bold(String(results.length))} capabilities scanned`);
171
- console.log(` ${red(String(stats.high))} high · ${yellow(String(stats.medium))} medium · ${green(String(stats.low))} low · ${gray(String(stats.unknown))} unclassified`);
172
- console.log();
173
-
174
- for (const [sev, items] of Object.entries(bySeverity)) {
175
- if (!items.length) continue;
176
- const col = severityColor(sev);
177
- console.log(` ${col(bold(`${sev.toUpperCase()} (${items.length})`))}`);
178
- for (const item of items) {
179
- const tagStr = item.tags.join(", ");
180
- console.log(` ${col("▸")} ${bold(item.id.padEnd(30))} ${gray(tagStr)}`);
181
- if (item.description) console.log(` ${gray(item.description.slice(0, 72))}`);
182
- }
183
- console.log();
184
- }
185
-
186
- if (stats.unknown > 0) {
187
- console.log(` ${gray("Tip: Add descriptions to unclassified capabilities for better detection.")}`);
188
- console.log();
189
- }
190
- }
191
-
192
- function buildHtmlReport(results, stats, runAt) {
193
- const rows = results.map(r => {
194
- const sev = r.severity;
195
- const cls = sev === "high" ? "high" : sev === "medium" ? "med" : sev === "low" ? "low" : "unk";
196
- const tags = r.tags.join(", ");
197
- const icon = severityIcon(sev);
198
- return `<tr class="${cls}"><td>${icon} ${sev}</td><td><strong>${escHtml(r.id)}</strong></td><td>${escHtml(tags)}</td><td>${escHtml(r.description || "")}</td></tr>`;
199
- }).join("\n");
200
-
201
- return `<!DOCTYPE html>
1
+ import*as d from"node:fs";import*as u from"node:path";import{done as I,warn as N,bold as g,gray as m,green as T,yellow as C,red as b}from"../ui/output.mjs";const x="audit.json",A=[{tag:"auth",severity:"high",label:"Authentication / Authorization",keywords:["auth","login","logout","signin","signout","signup","password","credential","token","session","oauth","jwt","permission","role","access","privilege","2fa","mfa","sso","saml","openid","verify","authenticate"]},{tag:"payment",severity:"high",label:"Payment / Billing",keywords:["payment","billing","invoice","charge","subscription","checkout","stripe","card","credit","debit","price","plan","tier","coupon","refund","transaction","purchase","order","cart","paypal","wallet"]},{tag:"pii",severity:"high",label:"Personal / PII Data",keywords:["pii","personal","profile","email","phone","address","name","user","account","gdpr","privacy","data","export","download","delete account","identity","dob","birthday","ssn","passport","tax"]},{tag:"admin",severity:"medium",label:"Admin / Privileged",keywords:["admin","manage","config","setting","system","deploy","migration","seed","reset","purge","archive","batch","bulk","impersonate","override","feature flag"]},{tag:"public",severity:"low",label:"Public / Read-only",keywords:["list","search","view","read","fetch","get","show","display","browse","filter","sort","paginate"]}],p={high:3,medium:2,low:1,unknown:0};function R(o){const t=[typeof o=="string"?o:"",o?.id||"",o?.name||"",o?.description||"",(o?.tags||[]).join(" ")].join(" ").toLowerCase(),n=[];for(const e of A)e.keywords.some(l=>t.includes(l))&&n.push(e);const s=n.some(e=>e.tag!=="public"&&e.severity!=="low")?n.filter(e=>e.tag!=="public"):n;if(!s.length)return{tags:["unknown"],severity:"unknown",labels:["Unclassified"]};const r=s.reduce((e,l)=>p[l.severity]>p[e]?l.severity:e,"low");return{tags:s.map(e=>e.tag),severity:r,labels:s.map(e=>e.label)}}function _(o){const t=u.join(o,x);if(!d.existsSync(t))return{};try{return JSON.parse(d.readFileSync(t,"utf8"))}catch{return{}}}function H(o,t){d.writeFileSync(u.join(o,x),JSON.stringify(t,null,2)+`
2
+ `)}function P(o){for(const t of["contract.json","capabilities.json"]){const n=u.join(o,t);if(d.existsSync(n))try{return JSON.parse(d.readFileSync(n,"utf8"))}catch{}}return null}function U(o){return o==="high"?b:o==="medium"?C:o==="low"?T:m}function F(o){return o==="high"?"\u{1F534}":o==="medium"?"\u{1F7E1}":o==="low"?"\u{1F7E2}":"\u26AA"}function J(o,t){const n={high:[],medium:[],low:[],unknown:[]};for(const c of o)(n[c.severity]||n.unknown).push(c);console.log(),console.log(` ${g("\u{1F525} infernoflow audit \u2014 Security Surface Map")}`),console.log(),console.log(` ${g(String(o.length))} capabilities scanned`),console.log(` ${b(String(t.high))} high \xB7 ${C(String(t.medium))} medium \xB7 ${T(String(t.low))} low \xB7 ${m(String(t.unknown))} unclassified`),console.log();for(const[c,s]of Object.entries(n)){if(!s.length)continue;const r=U(c);console.log(` ${r(g(`${c.toUpperCase()} (${s.length})`))}`);for(const e of s){const l=e.tags.join(", ");console.log(` ${r("\u25B8")} ${g(e.id.padEnd(30))} ${m(l)}`),e.description&&console.log(` ${m(e.description.slice(0,72))}`)}console.log()}t.unknown>0&&(console.log(` ${m("Tip: Add descriptions to unclassified capabilities for better detection.")}`),console.log())}function z(o,t,n){const c=o.map(s=>{const r=s.severity,e=r==="high"?"high":r==="medium"?"med":r==="low"?"low":"unk",l=s.tags.join(", "),y=F(r);return`<tr class="${e}"><td>${y} ${r}</td><td><strong>${k(s.id)}</strong></td><td>${k(l)}</td><td>${k(s.description||"")}</td></tr>`}).join(`
3
+ `);return`<!DOCTYPE html>
202
4
  <html lang="en">
203
5
  <head>
204
6
  <meta charset="UTF-8">
@@ -226,110 +28,21 @@ function buildHtmlReport(results, stats, runAt) {
226
28
  </style>
227
29
  </head>
228
30
  <body>
229
- <h1>🔥 infernoflow audit</h1>
230
- <p class="meta">Generated ${runAt}</p>
31
+ <h1>\u{1F525} infernoflow audit</h1>
32
+ <p class="meta">Generated ${n}</p>
231
33
  <div class="stats">
232
- <div class="stat high"><div class="n">${stats.high}</div><div class="l">HIGH</div></div>
233
- <div class="stat med"><div class="n">${stats.medium}</div><div class="l">MEDIUM</div></div>
234
- <div class="stat low"><div class="n">${stats.low}</div><div class="l">LOW</div></div>
235
- <div class="stat unk"><div class="n">${stats.unknown}</div><div class="l">UNKNOWN</div></div>
34
+ <div class="stat high"><div class="n">${t.high}</div><div class="l">HIGH</div></div>
35
+ <div class="stat med"><div class="n">${t.medium}</div><div class="l">MEDIUM</div></div>
36
+ <div class="stat low"><div class="n">${t.low}</div><div class="l">LOW</div></div>
37
+ <div class="stat unk"><div class="n">${t.unknown}</div><div class="l">UNKNOWN</div></div>
236
38
  </div>
237
39
  <table>
238
40
  <thead><tr><th>Severity</th><th>Capability</th><th>Tags</th><th>Description</th></tr></thead>
239
41
  <tbody>
240
- ${rows}
42
+ ${c}
241
43
  </tbody>
242
44
  </table>
243
45
  </body>
244
- </html>`;
245
- }
246
-
247
- function escHtml(str) {
248
- return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
249
- }
250
-
251
- // ── Entry ─────────────────────────────────────────────────────────────────────
252
-
253
- export async function auditCommand(rawArgs) {
254
- const args = rawArgs.slice(1);
255
- const jsonMode = args.includes("--json") || args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
256
- const cwd = process.cwd();
257
- const infernoDir = path.join(cwd, "inferno");
258
-
259
- if (!fs.existsSync(infernoDir)) {
260
- const msg = "inferno/ not found. Run: infernoflow init";
261
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); }
262
- else { warn(msg); }
263
- process.exit(1);
264
- }
265
-
266
- // Parse flags
267
- const fmtIdx = args.indexOf("--format");
268
- const format = fmtIdx !== -1 ? args[fmtIdx + 1] : (jsonMode ? "json" : "text");
269
- const outIdx = args.indexOf("--out");
270
- const outPath = outIdx !== -1 ? args[outIdx + 1] : null;
271
- const failOnIdx = args.indexOf("--fail-on");
272
- const failOn = failOnIdx !== -1 ? args[failOnIdx + 1] : null;
273
-
274
- const contract = readContract(infernoDir);
275
- if (!contract) {
276
- const msg = "No contract.json or capabilities.json found.";
277
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); }
278
- else { warn(msg); }
279
- process.exit(1);
280
- }
281
-
282
- const rawCaps = contract.capabilities || [];
283
- const runAt = new Date().toISOString();
284
-
285
- // Classify every capability
286
- const results = rawCaps.map(cap => {
287
- const id = typeof cap === "string" ? cap : (cap.id || cap.name || "unknown");
288
- const description = typeof cap === "string" ? "" : (cap.description || "");
289
- const cls = classifyCapability(cap);
290
- return { id, description, ...cls };
291
- });
292
-
293
- // Sort by severity descending
294
- results.sort((a, b) => (SEVERITY_ORDER[b.severity] || 0) - (SEVERITY_ORDER[a.severity] || 0));
295
-
296
- // Stats
297
- const stats = { high: 0, medium: 0, low: 0, unknown: 0 };
298
- for (const r of results) {
299
- const key = r.severity in stats ? r.severity : "unknown";
300
- stats[key]++;
301
- }
302
-
303
- // Persist to audit.json
304
- const auditData = {
305
- runAt,
306
- stats,
307
- capabilities: results,
308
- };
309
- writeAudit(infernoDir, auditData);
310
-
311
- // Output
312
- if (format === "json" || jsonMode) {
313
- console.log(JSON.stringify({ ok: true, ...auditData }));
314
- } else if (format === "html") {
315
- const html = buildHtmlReport(results, stats, runAt);
316
- const dest = outPath || path.join(infernoDir, "audit.html");
317
- fs.writeFileSync(dest, html);
318
- if (!jsonMode) done(`HTML audit report written to ${bold(dest)}`);
319
- } else {
320
- printTextReport(results, stats);
321
- if (!jsonMode) done(`Saved to ${bold(path.join(infernoDir, AUDIT_FILE))}`);
322
- }
323
-
324
- // Exit code enforcement
325
- if (failOn) {
326
- const threshold = SEVERITY_ORDER[failOn] || 0;
327
- const violations = results.filter(r => (SEVERITY_ORDER[r.severity] || 0) >= threshold);
328
- if (violations.length > 0) {
329
- if (!jsonMode) {
330
- console.error(red(`\n ✗ ${violations.length} capability/capabilities at or above "${failOn}" severity — review required.\n`));
331
- }
332
- process.exit(1);
333
- }
334
- }
335
- }
46
+ </html>`}function k(o){return String(o).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}async function G(o){const t=o.slice(1),n=t.includes("--json")||t.includes("--format")&&t[t.indexOf("--format")+1]==="json",c=process.cwd(),s=u.join(c,"inferno");if(!d.existsSync(s)){const i="inferno/ not found. Run: infernoflow init";n?console.log(JSON.stringify({ok:!1,error:i})):N(i),process.exit(1)}const r=t.indexOf("--format"),e=r!==-1?t[r+1]:n?"json":"text",l=t.indexOf("--out"),y=l!==-1?t[l+1]:null,S=t.indexOf("--fail-on"),v=S!==-1?t[S+1]:null,$=P(s);if(!$){const i="No contract.json or capabilities.json found.";n?console.log(JSON.stringify({ok:!1,error:i})):N(i),process.exit(1)}const D=$.capabilities||[],j=new Date().toISOString(),f=D.map(i=>{const a=typeof i=="string"?i:i.id||i.name||"unknown",w=typeof i=="string"?"":i.description||"",E=R(i);return{id:a,description:w,...E}});f.sort((i,a)=>(p[a.severity]||0)-(p[i.severity]||0));const h={high:0,medium:0,low:0,unknown:0};for(const i of f){const a=i.severity in h?i.severity:"unknown";h[a]++}const O={runAt:j,stats:h,capabilities:f};if(H(s,O),e==="json"||n)console.log(JSON.stringify({ok:!0,...O}));else if(e==="html"){const i=z(f,h,j),a=y||u.join(s,"audit.html");d.writeFileSync(a,i),n||I(`HTML audit report written to ${g(a)}`)}else J(f,h),n||I(`Saved to ${g(u.join(s,x))}`);if(v){const i=p[v]||0,a=f.filter(w=>(p[w.severity]||0)>=i);a.length>0&&(n||console.error(b(`
47
+ \u2717 ${a.length} capability/capabilities at or above "${v}" severity \u2014 review required.
48
+ `)),process.exit(1))}}export{G as auditCommand};