infernoflow 0.10.23 → 0.10.26

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 (36) hide show
  1. package/CHANGELOG.md +13 -6
  2. package/dist/bin/infernoflow.mjs +135 -41
  3. package/dist/lib/adopters/angular.mjs +128 -1
  4. package/dist/lib/adopters/css.mjs +111 -1
  5. package/dist/lib/adopters/react.mjs +104 -1
  6. package/dist/lib/ai/ideDetection.mjs +31 -1
  7. package/dist/lib/ai/localProvider.mjs +88 -1
  8. package/dist/lib/ai/providerRouter.mjs +73 -1
  9. package/dist/lib/commands/adopt.mjs +869 -20
  10. package/dist/lib/commands/changelog.mjs +343 -21
  11. package/dist/lib/commands/check.mjs +179 -3
  12. package/dist/lib/commands/context.mjs +287 -31
  13. package/dist/lib/commands/diff.mjs +274 -5
  14. package/dist/lib/commands/docGate.mjs +81 -2
  15. package/dist/lib/commands/generateSkills.mjs +163 -38
  16. package/dist/lib/commands/implement.mjs +103 -7
  17. package/dist/lib/commands/init.mjs +394 -10
  18. package/dist/lib/commands/installCursorHooks.mjs +36 -1
  19. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
  20. package/dist/lib/commands/prImpact.mjs +157 -2
  21. package/dist/lib/commands/publish.mjs +293 -15
  22. package/dist/lib/commands/run.mjs +336 -8
  23. package/dist/lib/commands/setup.mjs +234 -4
  24. package/dist/lib/commands/status.mjs +172 -4
  25. package/dist/lib/commands/suggest.mjs +563 -21
  26. package/dist/lib/commands/syncAuto.mjs +96 -1
  27. package/dist/lib/cursorHooksInstall.mjs +60 -1
  28. package/dist/lib/draftToolingInstall.mjs +68 -7
  29. package/dist/lib/git/detect-drift.mjs +208 -4
  30. package/dist/lib/learning/adapt.mjs +101 -6
  31. package/dist/lib/learning/observe.mjs +114 -1
  32. package/dist/lib/learning/profile.mjs +212 -2
  33. package/dist/lib/ui/output.mjs +72 -6
  34. package/dist/lib/ui/prompts.mjs +147 -6
  35. package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
  36. package/package.json +47 -47
@@ -1,21 +1,343 @@
1
- import*as g from"node:fs";import*as S from"node:path";import{execSync as v}from"node:child_process";import{header as O,ok as R,fail as C,warn as m,info as b,done as G,bold as E,cyan as h,gray as f,green as F,yellow as L}from"../ui/output.mjs";function p(e,n){try{return v(e,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}catch{return null}}function y(e){return p("git describe --tags --abbrev=0",e)}function $(e,n){const o=e?`${e}..HEAD`:"",t=p(`git log ${o} --format=%H%x1f%s%x1f%b%x1e`,n);return t?t.split("").map(s=>s.trim()).filter(Boolean).map(s=>{const r=s.split("");return{hash:(r[0]||"").trim().slice(0,8),subject:(r[1]||"").trim(),body:(r[2]||"").trim()}}).filter(s=>s.subject):[]}function M(e,n){return p(`git log -1 --format=%ci "${e}"`,n)?.slice(0,10)||null}const w={feat:"Added",feature:"Added",add:"Added",fix:"Fixed",bugfix:"Fixed",hotfix:"Fixed",perf:"Changed",refactor:"Changed",change:"Changed",chore:"Changed",docs:"Changed",style:"Changed",test:"Changed",ci:"Changed",remove:"Removed",revert:"Removed",deprecate:"Removed"};function x(e){const n=e.match(/^(\w+)(?:\([^)]+\))?[!]?:\s*(.+)/);if(n){const t=n[1].toLowerCase();return{section:w[t]||"Changed",message:n[2].trim(),breaking:e.includes("!")}}const o=e.toLowerCase();for(const[t,s]of Object.entries(w))if(o.startsWith(t+" ")||o.startsWith(t+":"))return{section:s,message:e,breaking:!1};return{section:"Changed",message:e,breaking:!1}}function j(e){const n={Added:[],Fixed:[],Changed:[],Removed:[],Breaking:[]};for(const o of e){const{section:t,message:s,breaking:r}=x(o.subject);r&&n.Breaking.push(s),n[t].push(s)}for(const o of Object.keys(n))n[o]=[...new Set(n[o])];return n}function H(e,n){const o=["## Unreleased",""];n&&o.push(`> Changes since ${n}`,"");const t=["Breaking","Added","Fixed","Changed","Removed"];let s=!1;for(const r of t){const i=e[r];if(!(!i||!i.length)){s=!0,o.push(`### ${r}`);for(const c of i)o.push(`- ${c}`);o.push("")}}return s||o.push("- No significant changes",""),o.join(`
2
- `)}function k(e){return g.existsSync(e)?g.readFileSync(e,"utf8"):null}function D(e){const n=e.match(/^## Unreleased[\s\S]*?(?=\n## |\n---|\z)/im);return n?n[0].trim():null}function U(e,n){return/^## Unreleased/im.test(e)?e.replace(/^## Unreleased[\s\S]*?(?=\n## |\n---)/im,n+`
3
-
4
- `):/^# .+/im.test(e)?e.replace(/^(# .+\n)/im,`$1
5
- ${n}
6
-
7
- `):`${n}
8
-
9
- ${e}`}function J(e,n){const o=n.split(`
10
- `).filter(t=>t.startsWith("- ")).join(`
11
- `);return o?/^## Unreleased/im.test(e)?e.replace(/(^## Unreleased[\s\S]*?)(\n## )/im,`$1
12
- ${o}
13
- $2`):U(e,n):e}function W(e,n){const o=n||y(e),t=$(o,e);if(!t.length){b(`No commits since ${o||"beginning"}`);return}console.log(`
14
- ${E("Commits since")} ${h(o||"beginning")} ${f("("+t.length+")")}
15
- `);for(const s of t){const{section:r}=x(s.subject),i=r==="Added"?F:r==="Fixed"?L:f;console.log(` ${f(s.hash)} ${i(s.subject)}`)}console.log()}function B(e){const n=k(e);if(!n){C("CHANGELOG.md not found");return}const o=D(n);if(!o){m("No ## Unreleased section found in CHANGELOG.md");return}console.log(`
16
- `+o+`
17
- `)}async function T(e,n,o){const{ref:t,dryRun:s,append:r,asJson:i}=o,c=t||y(e),a=$(c,e);if(!a.length){if(i){console.log(JSON.stringify({ok:!0,ref:c,commits:0,message:"No new commits"}));return}m(`No commits found since ${c||"beginning of repo"}`),console.log();return}const d=j(a),l=H(d,c);if(i){console.log(JSON.stringify({ok:!0,ref:c,commits:a.length,sections:{breaking:d.Breaking,added:d.Added,fixed:d.Fixed,changed:d.Changed,removed:d.Removed},markdown:l},null,2));return}if(console.log(),console.log(f(" \u2500\u2500\u2500 Drafted entry \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")),l.split(`
18
- `).forEach(N=>console.log(" "+N)),console.log(f(" \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")),console.log(),b(`${a.length} commit${a.length>1?"s":""} since ${h(c||"beginning")}`),s){m("Dry run \u2014 CHANGELOG.md not modified"),console.log();return}let u=k(n);u||(u=`# Changelog
19
-
20
- `);const A=r?J(u,l):U(u,l);g.writeFileSync(n,A),R(`CHANGELOG.md updated ${f("("+(r?"appended":"replaced")+" ## Unreleased)")}`),console.log(),G("Changelog drafted \u2014 review and edit before your next release"),console.log(` Run ${h("infernoflow publish")} when ready to cut the release
21
- `)}async function P(e){const n=e.slice(1),o=n.find(l=>!l.startsWith("-"))||"update",t=n.includes("--dry-run"),s=n.includes("--append"),r=n.includes("--json"),i=n.indexOf("--ref"),c=i!==-1?n[i+1]:null,a=process.cwd(),d=S.join(a,"CHANGELOG.md");if(r||O("changelog "+o),o==="list"){W(a,c);return}if(o==="show"){B(d);return}if(o==="update"){await T(a,d,{ref:c,dryRun:t,append:s,asJson:r});return}C(`Unknown sub-command: ${o}`,"Use: update | show | list"),process.exit(1)}export{P as changelogCommand};
1
+ /**
2
+ * infernoflow changelog update
3
+ *
4
+ * Reads git commits since the last tag, groups them by type (feat/fix/chore/…),
5
+ * drafts a clean ## Unreleased section, and writes it into CHANGELOG.md.
6
+ *
7
+ * Sub-commands:
8
+ * infernoflow changelog update # draft Unreleased from commits
9
+ * infernoflow changelog show # print current Unreleased block
10
+ * infernoflow changelog list # list commits since last tag
11
+ *
12
+ * Flags (update):
13
+ * --ref <tag|commit> Compare from a specific ref instead of last tag
14
+ * --dry-run Print what would be written without touching the file
15
+ * --append Append to existing ## Unreleased instead of replacing it
16
+ * --json Machine-readable output
17
+ */
18
+
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import { execSync } from "node:child_process";
22
+ import { header, ok, fail, warn, info, done, bold, cyan, gray, green, yellow } from "../ui/output.mjs";
23
+
24
+ // ── git helpers ──────────────────────────────────────────────────────────────
25
+
26
+ function capture(cmd, cwd) {
27
+ try {
28
+ return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function lastTag(cwd) {
35
+ return capture("git describe --tags --abbrev=0", cwd);
36
+ }
37
+
38
+ function commitsSince(ref, cwd) {
39
+ // Returns array of { hash, subject, body }
40
+ // Use NUL-delimited output to avoid shell escaping issues with separators
41
+ const range = ref ? `${ref}..HEAD` : "";
42
+ const raw = capture(`git log ${range} --format=%H%x1f%s%x1f%b%x1e`, cwd);
43
+ if (!raw) return [];
44
+
45
+ return raw
46
+ .split("\x1e")
47
+ .map(s => s.trim())
48
+ .filter(Boolean)
49
+ .map(block => {
50
+ const parts = block.split("\x1f");
51
+ return {
52
+ hash: (parts[0] || "").trim().slice(0, 8),
53
+ subject: (parts[1] || "").trim(),
54
+ body: (parts[2] || "").trim(),
55
+ };
56
+ })
57
+ .filter(c => c.subject);
58
+ }
59
+
60
+ function refDate(ref, cwd) {
61
+ return capture(`git log -1 --format=%ci "${ref}"`, cwd)?.slice(0, 10) || null;
62
+ }
63
+
64
+ // ── commit classification ────────────────────────────────────────────────────
65
+
66
+ const TYPE_MAP = {
67
+ feat: "Added",
68
+ feature: "Added",
69
+ add: "Added",
70
+ fix: "Fixed",
71
+ bugfix: "Fixed",
72
+ hotfix: "Fixed",
73
+ perf: "Changed",
74
+ refactor: "Changed",
75
+ change: "Changed",
76
+ chore: "Changed",
77
+ docs: "Changed",
78
+ style: "Changed",
79
+ test: "Changed",
80
+ ci: "Changed",
81
+ remove: "Removed",
82
+ revert: "Removed",
83
+ deprecate:"Removed",
84
+ };
85
+
86
+ function classifyCommit(subject) {
87
+ // Conventional commits: "feat: ..." or "feat(scope): ..."
88
+ const match = subject.match(/^(\w+)(?:\([^)]+\))?[!]?:\s*(.+)/);
89
+ if (match) {
90
+ const type = match[1].toLowerCase();
91
+ return {
92
+ section: TYPE_MAP[type] || "Changed",
93
+ message: match[2].trim(),
94
+ breaking: subject.includes("!"),
95
+ };
96
+ }
97
+
98
+ // Heuristic: starts with a keyword
99
+ const lower = subject.toLowerCase();
100
+ for (const [keyword, section] of Object.entries(TYPE_MAP)) {
101
+ if (lower.startsWith(keyword + " ") || lower.startsWith(keyword + ":")) {
102
+ return { section, message: subject, breaking: false };
103
+ }
104
+ }
105
+
106
+ return { section: "Changed", message: subject, breaking: false };
107
+ }
108
+
109
+ function groupCommits(commits) {
110
+ const sections = { Added: [], Fixed: [], Changed: [], Removed: [], Breaking: [] };
111
+
112
+ for (const c of commits) {
113
+ const { section, message, breaking } = classifyCommit(c.subject);
114
+ if (breaking) sections.Breaking.push(message);
115
+ sections[section].push(message);
116
+ }
117
+
118
+ // Remove duplicates (some commits end up in Breaking AND their category)
119
+ for (const key of Object.keys(sections)) {
120
+ sections[key] = [...new Set(sections[key])];
121
+ }
122
+
123
+ return sections;
124
+ }
125
+
126
+ // ── markdown rendering ───────────────────────────────────────────────────────
127
+
128
+ function renderUnreleased(sections, ref) {
129
+ const lines = ["## Unreleased", ""];
130
+
131
+ if (ref) {
132
+ lines.push(`> Changes since ${ref}`, "");
133
+ }
134
+
135
+ const ORDER = ["Breaking", "Added", "Fixed", "Changed", "Removed"];
136
+ let hasContent = false;
137
+
138
+ for (const heading of ORDER) {
139
+ const items = sections[heading];
140
+ if (!items || !items.length) continue;
141
+ hasContent = true;
142
+ lines.push(`### ${heading}`);
143
+ for (const item of items) {
144
+ lines.push(`- ${item}`);
145
+ }
146
+ lines.push("");
147
+ }
148
+
149
+ if (!hasContent) {
150
+ lines.push("- No significant changes", "");
151
+ }
152
+
153
+ return lines.join("\n");
154
+ }
155
+
156
+ // ── CHANGELOG file operations ────────────────────────────────────────────────
157
+
158
+ function readChangelog(changelogPath) {
159
+ if (!fs.existsSync(changelogPath)) return null;
160
+ return fs.readFileSync(changelogPath, "utf8");
161
+ }
162
+
163
+ function extractUnreleased(text) {
164
+ // Returns the content of the ## Unreleased block, or null
165
+ const match = text.match(/^## Unreleased[\s\S]*?(?=\n## |\n---|\z)/im);
166
+ return match ? match[0].trim() : null;
167
+ }
168
+
169
+ function injectUnreleased(text, newBlock) {
170
+ // Replace existing ## Unreleased block
171
+ if (/^## Unreleased/im.test(text)) {
172
+ return text.replace(
173
+ /^## Unreleased[\s\S]*?(?=\n## |\n---)/im,
174
+ newBlock + "\n\n"
175
+ );
176
+ }
177
+
178
+ // No existing Unreleased block — insert after the first # heading
179
+ if (/^# .+/im.test(text)) {
180
+ return text.replace(
181
+ /^(# .+\n)/im,
182
+ `$1\n${newBlock}\n\n`
183
+ );
184
+ }
185
+
186
+ // Prepend
187
+ return `${newBlock}\n\n${text}`;
188
+ }
189
+
190
+ function appendToUnreleased(text, newBlock) {
191
+ // Extract just the bullet lines from newBlock and append to existing Unreleased
192
+ const newLines = newBlock.split("\n").filter(l => l.startsWith("- ")).join("\n");
193
+ if (!newLines) return text;
194
+
195
+ if (/^## Unreleased/im.test(text)) {
196
+ // Find end of Unreleased and insert before next section
197
+ return text.replace(
198
+ /(^## Unreleased[\s\S]*?)(\n## )/im,
199
+ `$1\n${newLines}\n$2`
200
+ );
201
+ }
202
+
203
+ return injectUnreleased(text, newBlock);
204
+ }
205
+
206
+ // ── sub-commands ─────────────────────────────────────────────────────────────
207
+
208
+ function subcmdList(cwd, ref) {
209
+ const tag = ref || lastTag(cwd);
210
+ const commits = commitsSince(tag, cwd);
211
+
212
+ if (!commits.length) {
213
+ info(`No commits since ${tag || "beginning"}`);
214
+ return;
215
+ }
216
+
217
+ console.log(`\n ${bold("Commits since")} ${cyan(tag || "beginning")} ${gray("(" + commits.length + ")")}\n`);
218
+ for (const c of commits) {
219
+ const { section } = classifyCommit(c.subject);
220
+ const color = section === "Added" ? green : section === "Fixed" ? yellow : gray;
221
+ console.log(` ${gray(c.hash)} ${color(c.subject)}`);
222
+ }
223
+ console.log();
224
+ }
225
+
226
+ function subcmdShow(changelogPath) {
227
+ const text = readChangelog(changelogPath);
228
+ if (!text) {
229
+ fail("CHANGELOG.md not found");
230
+ return;
231
+ }
232
+ const block = extractUnreleased(text);
233
+ if (!block) {
234
+ warn("No ## Unreleased section found in CHANGELOG.md");
235
+ return;
236
+ }
237
+ console.log("\n" + block + "\n");
238
+ }
239
+
240
+ async function subcmdUpdate(cwd, changelogPath, opts) {
241
+ const { ref, dryRun, append, asJson } = opts;
242
+
243
+ const tag = ref || lastTag(cwd);
244
+ const commits = commitsSince(tag, cwd);
245
+
246
+ if (!commits.length) {
247
+ if (asJson) {
248
+ console.log(JSON.stringify({ ok: true, ref: tag, commits: 0, message: "No new commits" }));
249
+ return;
250
+ }
251
+ warn(`No commits found since ${tag || "beginning of repo"}`);
252
+ console.log();
253
+ return;
254
+ }
255
+
256
+ const sections = groupCommits(commits);
257
+ const newBlock = renderUnreleased(sections, tag);
258
+
259
+ if (asJson) {
260
+ console.log(JSON.stringify({
261
+ ok: true,
262
+ ref: tag,
263
+ commits: commits.length,
264
+ sections: {
265
+ breaking: sections.Breaking,
266
+ added: sections.Added,
267
+ fixed: sections.Fixed,
268
+ changed: sections.Changed,
269
+ removed: sections.Removed,
270
+ },
271
+ markdown: newBlock,
272
+ }, null, 2));
273
+ return;
274
+ }
275
+
276
+ // Print the drafted block
277
+ console.log();
278
+ console.log(gray(" ─── Drafted entry ─────────────────────────────────"));
279
+ newBlock.split("\n").forEach(l => console.log(" " + l));
280
+ console.log(gray(" ────────────────────────────────────────────────────"));
281
+ console.log();
282
+ info(`${commits.length} commit${commits.length > 1 ? "s" : ""} since ${cyan(tag || "beginning")}`);
283
+
284
+ if (dryRun) {
285
+ warn("Dry run — CHANGELOG.md not modified");
286
+ console.log();
287
+ return;
288
+ }
289
+
290
+ // Write to CHANGELOG.md
291
+ let text = readChangelog(changelogPath);
292
+ if (!text) {
293
+ // Create a fresh changelog
294
+ text = `# Changelog\n\n`;
295
+ }
296
+
297
+ const updated = append ? appendToUnreleased(text, newBlock) : injectUnreleased(text, newBlock);
298
+ fs.writeFileSync(changelogPath, updated);
299
+
300
+ ok(`CHANGELOG.md updated ${gray("(" + (append ? "appended" : "replaced") + " ## Unreleased)")}`);
301
+ console.log();
302
+ done("Changelog drafted — review and edit before your next release");
303
+ console.log(` Run ${cyan("infernoflow publish")} when ready to cut the release\n`);
304
+ }
305
+
306
+ // ── main ─────────────────────────────────────────────────────────────────────
307
+
308
+ export async function changelogCommand(rawArgs) {
309
+ const args = rawArgs.slice(1); // drop "changelog"
310
+
311
+ // Sub-command: first non-flag arg
312
+ const sub = args.find(a => !a.startsWith("-")) || "update";
313
+
314
+ const dryRun = args.includes("--dry-run");
315
+ const append = args.includes("--append");
316
+ const asJson = args.includes("--json");
317
+
318
+ const refIdx = args.indexOf("--ref");
319
+ const ref = refIdx !== -1 ? args[refIdx + 1] : null;
320
+
321
+ const cwd = process.cwd();
322
+ const changelogPath = path.join(cwd, "CHANGELOG.md");
323
+
324
+ if (!asJson) header("changelog " + sub);
325
+
326
+ if (sub === "list") {
327
+ subcmdList(cwd, ref);
328
+ return;
329
+ }
330
+
331
+ if (sub === "show") {
332
+ subcmdShow(changelogPath);
333
+ return;
334
+ }
335
+
336
+ if (sub === "update") {
337
+ await subcmdUpdate(cwd, changelogPath, { ref, dryRun, append, asJson });
338
+ return;
339
+ }
340
+
341
+ fail(`Unknown sub-command: ${sub}`, "Use: update | show | list");
342
+ process.exit(1);
343
+ }
@@ -1,3 +1,179 @@
1
- import*as c from"node:fs";import*as l from"node:path";import{header as v,ok as f,fail as o,warn as E,section as g,done as A,errorAndExit as N,bold as w,red as I,yellow as J}from"../ui/output.mjs";import{docGateCommand as F}from"./docGate.mjs";function O(s,t=!1){try{return JSON.parse(c.readFileSync(s,"utf8"))}catch{if(t)throw new Error(`Cannot parse ${l.basename(s)}`);N(`Cannot parse ${l.basename(s)}`,`Check JSON syntax in: ${s}`)}}function H(s){return c.existsSync(s)?c.readdirSync(s).filter(t=>t.endsWith(".json")).map(t=>l.join(s,t)):[]}function L(s){const t=new Set;for(const a of s)try{(JSON.parse(c.readFileSync(a,"utf8")).capabilitiesCovered||[]).forEach(e=>t.add(e))}catch{}return t}async function B(s){const t=process.cwd(),a=l.join(t,"inferno"),m=s.includes("--skip-doc-gate"),e=s.includes("--json");e||v("check");const i=[],d=[];c.existsSync(a)||(e&&(console.log(JSON.stringify({ok:!1,errors:["inferno/ not found"]})),process.exit(1)),N("inferno/ not found","Run: infernoflow init"));const b=l.join(a,"contract.json"),S=l.join(a,"capabilities.json"),$=l.join(a,"scenarios"),C=l.join(a,"CHANGELOG.md");if(e||g("Contract"),!c.existsSync(b))o("contract.json not found","Run: infernoflow init"),i.push("contract.json missing");else{let r;try{r=O(b,e)}catch(p){i.push(p.message),e||(o(p.message),process.exit(1)),console.log(JSON.stringify({ok:!1,errors:i,warnings:d},null,2)),process.exit(1)}const h=r.capabilities||[];if(r.policyId?e||f(`policyId: ${w(r.policyId)}`):(o("policyId missing"),i.push("policyId missing")),Number.isInteger(r.policyVersion)?e||f(`policyVersion: ${w("v"+r.policyVersion)}`):(o("policyVersion must be an integer"),i.push("policyVersion invalid")),h.length===0?(o("capabilities array is empty"),i.push("no capabilities")):e||f(`${h.length} capabilities declared`),e||g("Capabilities Registry"),!c.existsSync(S))o("capabilities.json not found"),i.push("capabilities.json missing");else{let p;try{p=O(S,e)}catch(n){i.push(n.message),e||(o(n.message),process.exit(1)),console.log(JSON.stringify({ok:!1,errors:i,warnings:d},null,2)),process.exit(1)}const j=new Set((p.capabilities||[]).map(n=>n?.id).filter(Boolean)),x=h.filter(n=>!j.has(n));x.length>0?x.forEach(n=>{e||o(`"${n}" in contract but missing from capabilities.json`,"Add it to inferno/capabilities.json"),i.push(`"${n}" not registered`)}):e||f(`All ${j.size} capabilities registered`),e||g("Scenarios");const y=H($);if(y.length===0)E("No scenarios found"),d.push("no scenarios");else{const n=L(y),k=r?.rules?.requireScenarioForEachCapability!==!1,G=h.filter(u=>!n.has(u));e||f(`${y.length} scenario file(s) found`),G.length>0&&k?G.forEach(u=>{e||o(`"${u}" has no scenario coverage`,"Add to capabilitiesCovered in a scenario file"),i.push(`"${u}" uncovered`)}):e||f("All capabilities covered by scenarios")}}}if(e||g("Changelog"),!c.existsSync(C))o("inferno/CHANGELOG.md not found"),i.push("CHANGELOG missing");else{const r=c.readFileSync(C,"utf8");/##\s+Unreleased/i.test(r)?e||f("CHANGELOG.md has ## Unreleased section"):(o("Missing '## Unreleased' section","Add it to inferno/CHANGELOG.md"),i.push("CHANGELOG missing Unreleased"))}if(m||(e||g("Doc Gate"),await F({silent:e,captureExit:!0}).catch(()=>{i.push("doc-gate failed")})),e){console.log(JSON.stringify({ok:i.length===0,errors:i,warnings:d},null,2)),i.length>0&&process.exit(1);return}console.log(),i.length>0?(console.log(" "+I(`\u2718 check failed \u2014 ${i.length} error(s)
2
- `)),process.exit(1)):d.length>0?console.log(" "+J(`\u26A0 check passed with ${d.length} warning(s)
3
- `)):A("check passed \u2014 everything is in sync")}export{B as checkCommand};
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { header, ok, fail, warn, info, section, done, errorAndExit, cyan, bold, red, green, yellow, gray } from "../ui/output.mjs";
4
+ import { docGateCommand } from "./docGate.mjs";
5
+
6
+ function readJson(filePath, jsonOut = false) {
7
+ try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
8
+ catch (err) {
9
+ if (jsonOut) {
10
+ throw new Error(`Cannot parse ${path.basename(filePath)}`);
11
+ }
12
+ errorAndExit(
13
+ `Cannot parse ${path.basename(filePath)}`,
14
+ `Check JSON syntax in: ${filePath}`
15
+ );
16
+ }
17
+ }
18
+
19
+ function getScenarioFiles(scenariosDir) {
20
+ if (!fs.existsSync(scenariosDir)) return [];
21
+ return fs.readdirSync(scenariosDir)
22
+ .filter(f => f.endsWith(".json"))
23
+ .map(f => path.join(scenariosDir, f));
24
+ }
25
+
26
+ function getCovered(scenarioFiles) {
27
+ const covered = new Set();
28
+ for (const f of scenarioFiles) {
29
+ try {
30
+ const s = JSON.parse(fs.readFileSync(f, "utf8"));
31
+ (s.capabilitiesCovered || []).forEach(c => covered.add(c));
32
+ } catch {}
33
+ }
34
+ return covered;
35
+ }
36
+
37
+ export async function checkCommand(args) {
38
+ const cwd = process.cwd();
39
+ const infernoDir = path.join(cwd, "inferno");
40
+ const skipDocGate = args.includes("--skip-doc-gate");
41
+ const jsonOut = args.includes("--json");
42
+
43
+ if (!jsonOut) header("check");
44
+
45
+ const errors = [];
46
+ const warnings = [];
47
+
48
+ // ── inferno/ exists ─────────────────────────────────────────────
49
+ if (!fs.existsSync(infernoDir)) {
50
+ if (jsonOut) { console.log(JSON.stringify({ ok: false, errors: ["inferno/ not found"] })); process.exit(1); }
51
+ errorAndExit("inferno/ not found", `Run: infernoflow init`);
52
+ }
53
+
54
+ // ── contract.json ───────────────────────────────────────────────
55
+ const contractPath = path.join(infernoDir, "contract.json");
56
+ const capsPath = path.join(infernoDir, "capabilities.json");
57
+ const scenariosDir = path.join(infernoDir, "scenarios");
58
+ const changelogPath = path.join(infernoDir, "CHANGELOG.md");
59
+
60
+ if (!jsonOut) section("Contract");
61
+ if (!fs.existsSync(contractPath)) {
62
+ fail("contract.json not found", "Run: infernoflow init");
63
+ errors.push("contract.json missing");
64
+ } else {
65
+ let contract;
66
+ try {
67
+ contract = readJson(contractPath, jsonOut);
68
+ } catch (err) {
69
+ errors.push(err.message);
70
+ if (!jsonOut) {
71
+ fail(err.message);
72
+ process.exit(1);
73
+ }
74
+ console.log(JSON.stringify({ ok: false, errors, warnings }, null, 2));
75
+ process.exit(1);
76
+ }
77
+ const caps = contract.capabilities || [];
78
+
79
+ if (!contract.policyId) { fail("policyId missing"); errors.push("policyId missing"); }
80
+ else if (!jsonOut) ok(`policyId: ${bold(contract.policyId)}`);
81
+
82
+ if (!Number.isInteger(contract.policyVersion)) { fail("policyVersion must be an integer"); errors.push("policyVersion invalid"); }
83
+ else if (!jsonOut) ok(`policyVersion: ${bold("v" + contract.policyVersion)}`);
84
+
85
+ if (caps.length === 0) { fail("capabilities array is empty"); errors.push("no capabilities"); }
86
+ else if (!jsonOut) ok(`${caps.length} capabilities declared`);
87
+
88
+ // ── capabilities.json ────────────────────────────────────────
89
+ if (!jsonOut) section("Capabilities Registry");
90
+ if (!fs.existsSync(capsPath)) {
91
+ fail("capabilities.json not found"); errors.push("capabilities.json missing");
92
+ } else {
93
+ let registry;
94
+ try {
95
+ registry = readJson(capsPath, jsonOut);
96
+ } catch (err) {
97
+ errors.push(err.message);
98
+ if (!jsonOut) {
99
+ fail(err.message);
100
+ process.exit(1);
101
+ }
102
+ console.log(JSON.stringify({ ok: false, errors, warnings }, null, 2));
103
+ process.exit(1);
104
+ }
105
+ const registryIds = new Set((registry.capabilities || []).map(c => c?.id).filter(Boolean));
106
+
107
+ const missingInRegistry = caps.filter(c => !registryIds.has(c));
108
+ if (missingInRegistry.length > 0) {
109
+ missingInRegistry.forEach(c => {
110
+ if (!jsonOut) fail(`"${c}" in contract but missing from capabilities.json`, `Add it to inferno/capabilities.json`);
111
+ errors.push(`"${c}" not registered`);
112
+ });
113
+ } else if (!jsonOut) {
114
+ ok(`All ${registryIds.size} capabilities registered`);
115
+ }
116
+
117
+ // ── scenarios ───────────────────────────────────────────────
118
+ if (!jsonOut) section("Scenarios");
119
+ const scenarioFiles = getScenarioFiles(scenariosDir);
120
+ if (scenarioFiles.length === 0) {
121
+ warn("No scenarios found"); warnings.push("no scenarios");
122
+ } else {
123
+ const covered = getCovered(scenarioFiles);
124
+ const requireCoverage = contract?.rules?.requireScenarioForEachCapability !== false;
125
+ const uncovered = caps.filter(c => !covered.has(c));
126
+
127
+ if (!jsonOut) ok(`${scenarioFiles.length} scenario file(s) found`);
128
+
129
+ if (uncovered.length > 0 && requireCoverage) {
130
+ uncovered.forEach(c => {
131
+ if (!jsonOut) fail(`"${c}" has no scenario coverage`, `Add to capabilitiesCovered in a scenario file`);
132
+ errors.push(`"${c}" uncovered`);
133
+ });
134
+ } else if (!jsonOut) {
135
+ ok(`All capabilities covered by scenarios`);
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ // ── CHANGELOG ────────────────────────────────────────────────────
142
+ if (!jsonOut) section("Changelog");
143
+ if (!fs.existsSync(changelogPath)) {
144
+ fail("inferno/CHANGELOG.md not found"); errors.push("CHANGELOG missing");
145
+ } else {
146
+ const txt = fs.readFileSync(changelogPath, "utf8");
147
+ if (!/##\s+Unreleased/i.test(txt)) {
148
+ fail("Missing '## Unreleased' section", "Add it to inferno/CHANGELOG.md");
149
+ errors.push("CHANGELOG missing Unreleased");
150
+ } else if (!jsonOut) {
151
+ ok("CHANGELOG.md has ## Unreleased section");
152
+ }
153
+ }
154
+
155
+ // ── doc-gate ─────────────────────────────────────────────────────
156
+ if (!skipDocGate) {
157
+ if (!jsonOut) section("Doc Gate");
158
+ await docGateCommand({ silent: jsonOut, captureExit: true }).catch(() => {
159
+ errors.push("doc-gate failed");
160
+ });
161
+ }
162
+
163
+ // ── Summary ──────────────────────────────────────────────────────
164
+ if (jsonOut) {
165
+ console.log(JSON.stringify({ ok: errors.length === 0, errors, warnings }, null, 2));
166
+ if (errors.length > 0) process.exit(1);
167
+ return;
168
+ }
169
+
170
+ console.log();
171
+ if (errors.length > 0) {
172
+ console.log(" " + red(`✘ check failed — ${errors.length} error(s)\n`));
173
+ process.exit(1);
174
+ } else if (warnings.length > 0) {
175
+ console.log(" " + yellow(`⚠ check passed with ${warnings.length} warning(s)\n`));
176
+ } else {
177
+ done("check passed — everything is in sync");
178
+ }
179
+ }