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,361 +1,2 @@
1
- /**
2
- * infernoflow pr-comment
3
- *
4
- * Posts a capability drift analysis as a GitHub PR comment.
5
- * Designed to run in CI (GitHub Actions) on pull_request events.
6
- *
7
- * Auto-reads context from GitHub Actions environment variables:
8
- * GITHUB_TOKEN — required for posting comments
9
- * GITHUB_REPOSITORY — e.g. "owner/repo"
10
- * GITHUB_EVENT_PATH — path to the event JSON (contains PR number)
11
- * GITHUB_SHA — current commit SHA
12
- * GITHUB_BASE_REF — base branch name (e.g. "main")
13
- *
14
- * Usage:
15
- * infernoflow pr-comment # auto-detect from CI env
16
- * infernoflow pr-comment --pr 42 # explicit PR number
17
- * infernoflow pr-comment --repo owner/r # explicit repo
18
- * infernoflow pr-comment --token ghp_... # explicit token
19
- * infernoflow pr-comment --dry-run # print comment without posting
20
- * infernoflow pr-comment --json # machine-readable result
21
- */
22
-
23
- import * as fs from "node:fs";
24
- import * as path from "node:path";
25
- import * as https from "node:https";
26
- import { execSync } from "node:child_process";
27
- import { header, ok, warn, info, done, bold, cyan, gray, green, red, yellow } from "../ui/output.mjs";
28
-
29
- // ── git helpers ───────────────────────────────────────────────────────────────
30
-
31
- function capture(cmd, cwd) {
32
- try {
33
- return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
34
- } catch { return null; }
35
- }
36
-
37
- function lastTag(cwd) {
38
- return capture("git describe --tags --abbrev=0", cwd) || null;
39
- }
40
-
41
- function fileAtRef(ref, relPath, cwd) {
42
- return capture(`git show "${ref}:${relPath}"`, cwd);
43
- }
44
-
45
- // ── capability helpers ────────────────────────────────────────────────────────
46
-
47
- function parseCaps(jsonText) {
48
- if (!jsonText) return null;
49
- try {
50
- const obj = JSON.parse(jsonText);
51
- const raw = obj.capabilities || [];
52
- return raw.map(c => {
53
- if (typeof c === "string") return { id: c, title: c };
54
- return { id: c.id || c, title: c.title || c.id || String(c), status: c.status };
55
- });
56
- } catch { return null; }
57
- }
58
-
59
- function loadCapsFromDisk(infernoDir) {
60
- for (const name of ["capabilities.json", "contract.json"]) {
61
- const p = path.join(infernoDir, name);
62
- if (fs.existsSync(p)) return parseCaps(fs.readFileSync(p, "utf8"));
63
- }
64
- return null;
65
- }
66
-
67
- function loadCapsAtRef(ref, cwd) {
68
- for (const name of ["capabilities.json", "contract.json"]) {
69
- const content = fileAtRef(ref, `inferno/${name}`, cwd);
70
- if (content) return parseCaps(content);
71
- }
72
- return null;
73
- }
74
-
75
- function diffCaps(before, after) {
76
- const beforeMap = new Map(before.map(c => [c.id, c]));
77
- const afterMap = new Map(after.map(c => [c.id, c]));
78
- const added = after.filter(c => !beforeMap.has(c.id));
79
- const removed = before.filter(c => !afterMap.has(c.id));
80
- const changed = [];
81
- for (const c of after) {
82
- const old = beforeMap.get(c.id);
83
- if (!old) continue;
84
- const changes = [];
85
- if (old.title !== c.title) changes.push({ field: "title", from: old.title, to: c.title });
86
- if ((old.status || "") !== (c.status || "")) changes.push({ field: "status", from: old.status || "—", to: c.status || "—" });
87
- if (changes.length) changed.push({ id: c.id, changes });
88
- }
89
- return { added, removed, changed };
90
- }
91
-
92
- function classifyBump(diff) {
93
- if (diff.removed.length > 0) return "major";
94
- if (diff.added.length > 0) return "minor";
95
- if (diff.changed.length > 0) return "patch";
96
- return "none";
97
- }
98
-
99
- // ── comment builder ───────────────────────────────────────────────────────────
100
-
101
- function buildComment(diff, bump, ref, currentVersion, nextVersion) {
102
- const lines = [];
103
-
104
- // Header
105
- const bumpEmoji = bump === "major" ? "🔴" : bump === "minor" ? "🟡" : bump === "patch" ? "🟢" : "✅";
106
- const bumpLabel = bump === "none" ? "No capability changes" : `${bump.toUpperCase()} bump recommended`;
107
- lines.push(`## 🔥 infernoflow — Capability Analysis`);
108
- lines.push(``);
109
- lines.push(`${bumpEmoji} **${bumpLabel}**${bump !== "none" ? ` · \`${currentVersion}\` → \`${nextVersion}\`` : ""}`);
110
- lines.push(``);
111
-
112
- // Summary table
113
- const hasChanges = diff.added.length || diff.removed.length || diff.changed.length;
114
- if (!hasChanges) {
115
- lines.push(`> No capability changes detected since \`${ref}\`. Contract is in sync.`);
116
- } else {
117
- lines.push(`| Change | Count |`);
118
- lines.push(`|--------|-------|`);
119
- if (diff.added.length) lines.push(`| ➕ Added | ${diff.added.length} |`);
120
- if (diff.removed.length) lines.push(`| ❌ Removed | ${diff.removed.length} |`);
121
- if (diff.changed.length) lines.push(`| ✏️ Modified | ${diff.changed.length} |`);
122
- lines.push(``);
123
-
124
- // Detail sections
125
- if (diff.added.length) {
126
- lines.push(`<details><summary>➕ Added capabilities (${diff.added.length})</summary>`);
127
- lines.push(``);
128
- for (const c of diff.added) lines.push(`- \`${c.id}\` — ${c.title}`);
129
- lines.push(``);
130
- lines.push(`</details>`);
131
- lines.push(``);
132
- }
133
-
134
- if (diff.removed.length) {
135
- lines.push(`<details><summary>❌ Removed capabilities (${diff.removed.length}) — breaking change</summary>`);
136
- lines.push(``);
137
- for (const c of diff.removed) lines.push(`- \`${c.id}\` — ${c.title}`);
138
- lines.push(``);
139
- lines.push(`</details>`);
140
- lines.push(``);
141
- }
142
-
143
- if (diff.changed.length) {
144
- lines.push(`<details><summary>✏️ Modified capabilities (${diff.changed.length})</summary>`);
145
- lines.push(``);
146
- for (const item of diff.changed) {
147
- lines.push(`- \`${item.id}\``);
148
- for (const ch of item.changes) lines.push(` - ${ch.field}: \`${ch.from}\` → \`${ch.to}\``);
149
- }
150
- lines.push(``);
151
- lines.push(`</details>`);
152
- lines.push(``);
153
- }
154
- }
155
-
156
- // Bump recommendation
157
- if (bump === "major") {
158
- lines.push(`> ⚠️ **Breaking change detected.** Capabilities were removed. Consider a major version bump.`);
159
- lines.push(`> Run \`infernoflow version --apply\` to update \`package.json\`.`);
160
- } else if (bump === "minor") {
161
- lines.push(`> ℹ️ New capabilities added. A minor version bump is recommended.`);
162
- lines.push(`> Run \`infernoflow version --apply\` to update \`package.json\`.`);
163
- }
164
-
165
- lines.push(``);
166
- lines.push(`---`);
167
- lines.push(`<sub>Generated by [infernoflow](https://github.com/ronmiz/infernoflow) · compared against \`${ref}\`</sub>`);
168
-
169
- return lines.join("\n");
170
- }
171
-
172
- // ── GitHub API ────────────────────────────────────────────────────────────────
173
-
174
- function githubRequest(method, pathname, body, token) {
175
- return new Promise((resolve, reject) => {
176
- const data = body ? JSON.stringify(body) : null;
177
- const req = https.request({
178
- hostname: "api.github.com",
179
- path: pathname,
180
- method,
181
- headers: {
182
- "Authorization": `Bearer ${token}`,
183
- "Accept": "application/vnd.github+json",
184
- "Content-Type": "application/json",
185
- "User-Agent": "infernoflow-cli",
186
- ...(data ? { "Content-Length": Buffer.byteLength(data) } : {}),
187
- },
188
- }, (res) => {
189
- let raw = "";
190
- res.on("data", chunk => raw += chunk);
191
- res.on("end", () => {
192
- try { resolve({ status: res.statusCode, body: JSON.parse(raw) }); }
193
- catch { resolve({ status: res.statusCode, body: raw }); }
194
- });
195
- });
196
- req.on("error", reject);
197
- if (data) req.write(data);
198
- req.end();
199
- });
200
- }
201
-
202
- async function findExistingComment(repo, prNumber, token) {
203
- // Look for a previous infernoflow comment to update instead of creating a new one
204
- const res = await githubRequest("GET", `/repos/${repo}/issues/${prNumber}/comments?per_page=100`, null, token);
205
- if (res.status !== 200 || !Array.isArray(res.body)) return null;
206
- return res.body.find(c => c.body && c.body.includes("🔥 infernoflow — Capability Analysis")) || null;
207
- }
208
-
209
- async function postComment(repo, prNumber, body, token) {
210
- // Update existing comment if found (avoids spam on multiple pushes)
211
- const existing = await findExistingComment(repo, prNumber, token);
212
- if (existing) {
213
- return githubRequest("PATCH", `/repos/${repo}/issues/comments/${existing.id}`, { body }, token);
214
- }
215
- return githubRequest("POST", `/repos/${repo}/issues/${prNumber}/comments`, { body }, token);
216
- }
217
-
218
- // ── env helpers ───────────────────────────────────────────────────────────────
219
-
220
- function readGithubEventPr() {
221
- const eventPath = process.env.GITHUB_EVENT_PATH;
222
- if (!eventPath || !fs.existsSync(eventPath)) return null;
223
- try {
224
- const event = JSON.parse(fs.readFileSync(eventPath, "utf8"));
225
- return event.pull_request?.number || event.number || null;
226
- } catch { return null; }
227
- }
228
-
229
- function applyBump(version, type) {
230
- const parts = (version || "0.0.0").split(".").map(Number);
231
- if (type === "major") { parts[0]++; parts[1] = 0; parts[2] = 0; }
232
- else if (type === "minor") { parts[1]++; parts[2] = 0; }
233
- else if (type === "patch") { parts[2]++; }
234
- return parts.join(".");
235
- }
236
-
237
- function readPackageVersion(cwd) {
238
- const p = path.join(cwd, "package.json");
239
- if (!fs.existsSync(p)) return "0.0.0";
240
- try { return JSON.parse(fs.readFileSync(p, "utf8")).version || "0.0.0"; } catch { return "0.0.0"; }
241
- }
242
-
243
- // ── main ──────────────────────────────────────────────────────────────────────
244
-
245
- export async function prCommentCommand(rawArgs) {
246
- const args = rawArgs.slice(1);
247
- const dryRun = args.includes("--dry-run");
248
- const asJson = args.includes("--json");
249
-
250
- const prIdx = args.indexOf("--pr");
251
- const repoIdx = args.indexOf("--repo");
252
- const tokenIdx = args.indexOf("--token");
253
- const refIdx = args.indexOf("--ref");
254
-
255
- const cwd = process.cwd();
256
- const infernoDir = path.join(cwd, "inferno");
257
-
258
- if (!asJson) header("infernoflow pr-comment");
259
-
260
- // ── Resolve inputs ────────────────────────────────────────────────────────
261
- const token = tokenIdx !== -1 ? args[tokenIdx + 1] : process.env.GITHUB_TOKEN;
262
- const repo = repoIdx !== -1 ? args[repoIdx + 1] : process.env.GITHUB_REPOSITORY;
263
- const prNumber = prIdx !== -1 ? parseInt(args[prIdx + 1], 10)
264
- : readGithubEventPr();
265
-
266
- let ref = refIdx !== -1 ? args[refIdx + 1] : null;
267
- if (!ref) ref = process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : null;
268
- if (!ref) ref = lastTag(cwd);
269
- if (!ref) {
270
- const parentExists = capture("git rev-parse HEAD~1", cwd);
271
- ref = parentExists ? "HEAD~1" : null;
272
- }
273
-
274
- // ── Validate ──────────────────────────────────────────────────────────────
275
- if (!fs.existsSync(infernoDir)) {
276
- const msg = "inferno/ not found — run: infernoflow init";
277
- if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
278
- warn(msg); process.exit(1);
279
- }
280
-
281
- if (!dryRun && !token) {
282
- const msg = "No GitHub token found. Set GITHUB_TOKEN env var or use --token";
283
- if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
284
- warn(msg); process.exit(1);
285
- }
286
-
287
- if (!dryRun && !repo) {
288
- const msg = "No repository found. Set GITHUB_REPOSITORY env var or use --repo owner/repo";
289
- if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
290
- warn(msg); process.exit(1);
291
- }
292
-
293
- if (!dryRun && !prNumber) {
294
- const msg = "No PR number found. Use --pr <number> or run in GitHub Actions on pull_request event";
295
- if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
296
- warn(msg); process.exit(1);
297
- }
298
-
299
- // ── Load capabilities and compute diff ───────────────────────────────────
300
- const current = loadCapsFromDisk(infernoDir);
301
- const previous = ref ? loadCapsAtRef(ref, cwd) : null;
302
-
303
- if (!current) {
304
- const msg = "No capabilities.json or contract.json found in inferno/";
305
- if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
306
- warn(msg); process.exit(1);
307
- }
308
-
309
- const diff = diffCaps(previous || [], current);
310
- const bump = classifyBump(diff);
311
- const currentVersion = readPackageVersion(cwd);
312
- const nextVersion = bump !== "none" ? applyBump(currentVersion, bump) : currentVersion;
313
-
314
- // ── Build comment ─────────────────────────────────────────────────────────
315
- const commentBody = buildComment(diff, bump, ref || "HEAD", currentVersion, nextVersion);
316
-
317
- // ── Dry run ───────────────────────────────────────────────────────────────
318
- if (dryRun) {
319
- if (asJson) {
320
- console.log(JSON.stringify({ ok: true, dryRun: true, bump, currentVersion, nextVersion, comment: commentBody }));
321
- } else {
322
- console.log();
323
- info("DRY RUN — comment that would be posted:");
324
- console.log();
325
- console.log(commentBody);
326
- console.log();
327
- }
328
- return;
329
- }
330
-
331
- // ── Post comment ──────────────────────────────────────────────────────────
332
- if (!asJson) info(`Posting to ${bold(repo)} PR #${prNumber}...`);
333
-
334
- try {
335
- const result = await postComment(repo, prNumber, commentBody, token);
336
-
337
- if (result.status === 200 || result.status === 201) {
338
- const commentUrl = result.body?.html_url || "";
339
- if (asJson) {
340
- console.log(JSON.stringify({ ok: true, bump, currentVersion, nextVersion, prNumber, repo, commentUrl }));
341
- } else {
342
- ok(`Comment posted → ${cyan(commentUrl || `PR #${prNumber}`)}`);
343
- if (bump !== "none") {
344
- console.log();
345
- info(`Recommended bump: ${bold(bump.toUpperCase())} ${currentVersion} → ${nextVersion}`);
346
- }
347
- console.log();
348
- }
349
- } else {
350
- const msg = `GitHub API error ${result.status}: ${JSON.stringify(result.body)}`;
351
- if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); }
352
- else { warn(msg); }
353
- process.exit(1);
354
- }
355
- } catch (err) {
356
- const msg = `Failed to post comment: ${err.message}`;
357
- if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); }
358
- else { warn(msg); }
359
- process.exit(1);
360
- }
361
- }
1
+ import*as h from"node:fs";import*as x from"node:path";import*as E from"node:https";import{execSync as T}from"node:child_process";import{header as J,ok as B,warn as g,info as w,bold as A,cyan as H}from"../ui/output.mjs";function N(e,n){try{return T(e,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}catch{return null}}function I(e){return N("git describe --tags --abbrev=0",e)||null}function P(e,n,s){return N(`git show "${e}:${n}"`,s)}function R(e){if(!e)return null;try{return(JSON.parse(e).capabilities||[]).map(o=>typeof o=="string"?{id:o,title:o}:{id:o.id||o,title:o.title||o.id||String(o),status:o.status})}catch{return null}}function _(e){for(const n of["capabilities.json","contract.json"]){const s=x.join(e,n);if(h.existsSync(s))return R(h.readFileSync(s,"utf8"))}return null}function G(e,n){for(const s of["capabilities.json","contract.json"]){const o=P(e,`inferno/${s}`,n);if(o)return R(o)}return null}function U(e,n){const s=new Map(e.map(r=>[r.id,r])),o=new Map(n.map(r=>[r.id,r])),u=n.filter(r=>!s.has(r.id)),t=e.filter(r=>!o.has(r.id)),l=[];for(const r of n){const c=s.get(r.id);if(!c)continue;const a=[];c.title!==r.title&&a.push({field:"title",from:c.title,to:r.title}),(c.status||"")!==(r.status||"")&&a.push({field:"status",from:c.status||"\u2014",to:r.status||"\u2014"}),a.length&&l.push({id:r.id,changes:a})}return{added:u,removed:t,changed:l}}function F(e){return e.removed.length>0?"major":e.added.length>0?"minor":e.changed.length>0?"patch":"none"}function D(e,n,s,o,u){const t=[],l=n==="major"?"\u{1F534}":n==="minor"?"\u{1F7E1}":n==="patch"?"\u{1F7E2}":"\u2705",r=n==="none"?"No capability changes":`${n.toUpperCase()} bump recommended`;if(t.push("## \u{1F525} infernoflow \u2014 Capability Analysis"),t.push(""),t.push(`${l} **${r}**${n!=="none"?` \xB7 \`${o}\` \u2192 \`${u}\``:""}`),t.push(""),!(e.added.length||e.removed.length||e.changed.length))t.push(`> No capability changes detected since \`${s}\`. Contract is in sync.`);else{if(t.push("| Change | Count |"),t.push("|--------|-------|"),e.added.length&&t.push(`| \u2795 Added | ${e.added.length} |`),e.removed.length&&t.push(`| \u274C Removed | ${e.removed.length} |`),e.changed.length&&t.push(`| \u270F\uFE0F Modified | ${e.changed.length} |`),t.push(""),e.added.length){t.push(`<details><summary>\u2795 Added capabilities (${e.added.length})</summary>`),t.push("");for(const a of e.added)t.push(`- \`${a.id}\` \u2014 ${a.title}`);t.push(""),t.push("</details>"),t.push("")}if(e.removed.length){t.push(`<details><summary>\u274C Removed capabilities (${e.removed.length}) \u2014 breaking change</summary>`),t.push("");for(const a of e.removed)t.push(`- \`${a.id}\` \u2014 ${a.title}`);t.push(""),t.push("</details>"),t.push("")}if(e.changed.length){t.push(`<details><summary>\u270F\uFE0F Modified capabilities (${e.changed.length})</summary>`),t.push("");for(const a of e.changed){t.push(`- \`${a.id}\``);for(const d of a.changes)t.push(` - ${d.field}: \`${d.from}\` \u2192 \`${d.to}\``)}t.push(""),t.push("</details>"),t.push("")}}return n==="major"?(t.push("> \u26A0\uFE0F **Breaking change detected.** Capabilities were removed. Consider a major version bump."),t.push("> Run `infernoflow version --apply` to update `package.json`.")):n==="minor"&&(t.push("> \u2139\uFE0F New capabilities added. A minor version bump is recommended."),t.push("> Run `infernoflow version --apply` to update `package.json`.")),t.push(""),t.push("---"),t.push(`<sub>Generated by [infernoflow](https://github.com/ronmiz/infernoflow) \xB7 compared against \`${s}\`</sub>`),t.join(`
2
+ `)}function C(e,n,s,o){return new Promise((u,t)=>{const l=s?JSON.stringify(s):null,r=E.request({hostname:"api.github.com",path:n,method:e,headers:{Authorization:`Bearer ${o}`,Accept:"application/vnd.github+json","Content-Type":"application/json","User-Agent":"infernoflow-cli",...l?{"Content-Length":Buffer.byteLength(l)}:{}}},c=>{let a="";c.on("data",d=>a+=d),c.on("end",()=>{try{u({status:c.statusCode,body:JSON.parse(a)})}catch{u({status:c.statusCode,body:a})}})});r.on("error",t),l&&r.write(l),r.end()})}async function M(e,n,s){const o=await C("GET",`/repos/${e}/issues/${n}/comments?per_page=100`,null,s);return o.status!==200||!Array.isArray(o.body)?null:o.body.find(u=>u.body&&u.body.includes("\u{1F525} infernoflow \u2014 Capability Analysis"))||null}async function q(e,n,s,o){const u=await M(e,n,o);return u?C("PATCH",`/repos/${e}/issues/comments/${u.id}`,{body:s},o):C("POST",`/repos/${e}/issues/${n}/comments`,{body:s},o)}function V(){const e=process.env.GITHUB_EVENT_PATH;if(!e||!h.existsSync(e))return null;try{const n=JSON.parse(h.readFileSync(e,"utf8"));return n.pull_request?.number||n.number||null}catch{return null}}function L(e,n){const s=(e||"0.0.0").split(".").map(Number);return n==="major"?(s[0]++,s[1]=0,s[2]=0):n==="minor"?(s[1]++,s[2]=0):n==="patch"&&s[2]++,s.join(".")}function Y(e){const n=x.join(e,"package.json");if(!h.existsSync(n))return"0.0.0";try{return JSON.parse(h.readFileSync(n,"utf8")).version||"0.0.0"}catch{return"0.0.0"}}async function ne(e){const n=e.slice(1),s=n.includes("--dry-run"),o=n.includes("--json"),u=n.indexOf("--pr"),t=n.indexOf("--repo"),l=n.indexOf("--token"),r=n.indexOf("--ref"),c=process.cwd(),a=x.join(c,"inferno");o||J("infernoflow pr-comment");const d=l!==-1?n[l+1]:process.env.GITHUB_TOKEN,$=t!==-1?n[t+1]:process.env.GITHUB_REPOSITORY,b=u!==-1?parseInt(n[u+1],10):V();let p=r!==-1?n[r+1]:null;if(p||(p=process.env.GITHUB_BASE_REF?`origin/${process.env.GITHUB_BASE_REF}`:null),p||(p=I(c)),p||(p=N("git rev-parse HEAD~1",c)?"HEAD~1":null),!h.existsSync(a)){const i="inferno/ not found \u2014 run: infernoflow init";o&&(console.log(JSON.stringify({ok:!1,error:i})),process.exit(1)),g(i),process.exit(1)}if(!s&&!d){const i="No GitHub token found. Set GITHUB_TOKEN env var or use --token";o&&(console.log(JSON.stringify({ok:!1,error:i})),process.exit(1)),g(i),process.exit(1)}if(!s&&!$){const i="No repository found. Set GITHUB_REPOSITORY env var or use --repo owner/repo";o&&(console.log(JSON.stringify({ok:!1,error:i})),process.exit(1)),g(i),process.exit(1)}if(!s&&!b){const i="No PR number found. Use --pr <number> or run in GitHub Actions on pull_request event";o&&(console.log(JSON.stringify({ok:!1,error:i})),process.exit(1)),g(i),process.exit(1)}const O=_(a),k=p?G(p,c):null;if(!O){const i="No capabilities.json or contract.json found in inferno/";o&&(console.log(JSON.stringify({ok:!1,error:i})),process.exit(1)),g(i),process.exit(1)}const j=U(k||[],O),m=F(j),y=Y(c),v=m!=="none"?L(y,m):y,S=D(j,m,p||"HEAD",y,v);if(s){o?console.log(JSON.stringify({ok:!0,dryRun:!0,bump:m,currentVersion:y,nextVersion:v,comment:S})):(console.log(),w("DRY RUN \u2014 comment that would be posted:"),console.log(),console.log(S),console.log());return}o||w(`Posting to ${A($)} PR #${b}...`);try{const i=await q($,b,S,d);if(i.status===200||i.status===201){const f=i.body?.html_url||"";o?console.log(JSON.stringify({ok:!0,bump:m,currentVersion:y,nextVersion:v,prNumber:b,repo:$,commentUrl:f})):(B(`Comment posted \u2192 ${H(f||`PR #${b}`)}`),m!=="none"&&(console.log(),w(`Recommended bump: ${A(m.toUpperCase())} ${y} \u2192 ${v}`)),console.log())}else{const f=`GitHub API error ${i.status}: ${JSON.stringify(i.body)}`;o?console.log(JSON.stringify({ok:!1,error:f})):g(f),process.exit(1)}}catch(i){const f=`Failed to post comment: ${i.message}`;o?console.log(JSON.stringify({ok:!1,error:f})):g(f),process.exit(1)}}export{ne as prCommentCommand};
@@ -1,157 +1,2 @@
1
- import { execSync } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { header, section, ok, warn, fail, gray, cyan, yellow } from "../ui/output.mjs";
5
-
6
- const CODE_PREFIXES = ["src/", "frontend/", "backend/", "app/", "pages/", "components/", "lib/", "api/", "server/", "Controllers/"];
7
-
8
- function sh(cmd) {
9
- return execSync(cmd, { stdio: ["ignore", "pipe", "pipe"] }).toString("utf8").trim();
10
- }
11
-
12
- function readJson(filePath, fallback = null) {
13
- try {
14
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
15
- } catch {
16
- return fallback;
17
- }
18
- }
19
-
20
- function readFile(filePath, fallback = "") {
21
- try {
22
- return fs.readFileSync(filePath, "utf8");
23
- } catch {
24
- return fallback;
25
- }
26
- }
27
-
28
- function getChangedFiles(base, head) {
29
- const out = base && head
30
- ? sh(`git diff --name-only ${base}..${head}`)
31
- : sh("git diff --name-only HEAD");
32
- return out ? out.split("\n").map((s) => s.trim()).filter(Boolean) : [];
33
- }
34
-
35
- function buildCapabilityHints(cwd) {
36
- const infernoDir = path.join(cwd, "inferno");
37
- const contract = readJson(path.join(infernoDir, "contract.json"), { capabilities: [] });
38
- const registry = readJson(path.join(infernoDir, "capabilities.json"), { capabilities: [] });
39
- const titleById = new Map((registry.capabilities || []).map((c) => [c.id, c.title || c.id]));
40
- return (contract.capabilities || []).map((id) => {
41
- const title = titleById.get(id) || id;
42
- const keywords = new Set(
43
- `${id} ${title}`
44
- .replace(/([A-Z])/g, " $1")
45
- .toLowerCase()
46
- .split(/[^a-z0-9]+/)
47
- .filter((k) => k.length >= 4)
48
- );
49
- return { id, title, keywords: Array.from(keywords) };
50
- });
51
- }
52
-
53
- function inferImpactedCapabilities(cwd, changedCodeFiles) {
54
- const hints = buildCapabilityHints(cwd);
55
- const impacted = [];
56
- for (const hint of hints) {
57
- const matched = [];
58
- for (const rel of changedCodeFiles) {
59
- const abs = path.join(cwd, rel);
60
- const text = readFile(abs, "").toLowerCase();
61
- if (!text) continue;
62
- if (hint.keywords.some((k) => text.includes(k))) {
63
- matched.push(rel);
64
- }
65
- }
66
- if (matched.length) {
67
- impacted.push({ id: hint.id, title: hint.title, matchedFiles: matched.slice(0, 5) });
68
- }
69
- }
70
- return impacted;
71
- }
72
-
73
- export async function prImpactCommand(args = []) {
74
- const asJson = args.includes("--json");
75
- const cwd = process.cwd();
76
- const base = process.env.BASE_SHA || null;
77
- const head = process.env.HEAD_SHA || null;
78
-
79
- let changedFiles = [];
80
- try {
81
- changedFiles = getChangedFiles(base, head);
82
- } catch {
83
- const payload = { ok: true, skipped: true, reason: "no_git_available" };
84
- if (asJson) {
85
- console.log(JSON.stringify(payload, null, 2));
86
- return;
87
- }
88
- header("pr-impact");
89
- warn("git not available; cannot compute PR impact");
90
- console.log();
91
- return;
92
- }
93
-
94
- const changedCodeFiles = changedFiles.filter((f) => CODE_PREFIXES.some((p) => f.startsWith(p)));
95
- const changedInfernoFiles = changedFiles.filter((f) => f.startsWith("inferno/"));
96
- const impactedCapabilities = inferImpactedCapabilities(cwd, changedCodeFiles);
97
- const inferredBehaviorChange = changedCodeFiles.length > 0;
98
- const missingInfernoUpdate = inferredBehaviorChange && changedInfernoFiles.length === 0;
99
- const confidence = impactedCapabilities.length > 0 ? "high" : inferredBehaviorChange ? "medium" : "low";
100
- const reasonCodes = [];
101
- if (inferredBehaviorChange) reasonCodes.push("CODE_CHANGED");
102
- if (missingInfernoUpdate) reasonCodes.push("INFERNO_NOT_UPDATED");
103
- if (impactedCapabilities.length > 0) reasonCodes.push("CAPABILITY_HINT_MATCH");
104
- if (!reasonCodes.length) reasonCodes.push("NO_BEHAVIOR_SIGNAL");
105
-
106
- const payload = {
107
- ok: !missingInfernoUpdate,
108
- base: base || "HEAD",
109
- head: head || "WORKTREE",
110
- changedFiles,
111
- changedCodeFiles,
112
- changedInfernoFiles,
113
- inferredBehaviorChange,
114
- impactedCapabilities,
115
- confidence,
116
- reasonCodes,
117
- recommendations: missingInfernoUpdate
118
- ? ["Run infernoflow suggest \"describe behavior change\" and update inferno/", "Run infernoflow check --json"]
119
- : ["Run infernoflow check --json to validate final state"],
120
- };
121
-
122
- if (asJson) {
123
- console.log(JSON.stringify(payload, null, 2));
124
- process.exit(payload.ok ? 0 : 1);
125
- }
126
-
127
- header("pr-impact");
128
-
129
- section("Diff Scope");
130
- ok(`Changed files: ${cyan(String(changedFiles.length))}`);
131
- ok(`Code files: ${cyan(String(changedCodeFiles.length))}`);
132
- ok(`Inferno files: ${cyan(String(changedInfernoFiles.length))}`);
133
-
134
- section("Capability Impact");
135
- if (impactedCapabilities.length === 0) {
136
- warn("No capability hints matched changed code files");
137
- } else {
138
- impactedCapabilities.forEach((c) => {
139
- console.log(` ${cyan("•")} ${c.id} ${gray(`(${c.title})`)}`);
140
- c.matchedFiles.slice(0, 3).forEach((f) => console.log(` ${gray("- " + f)}`));
141
- });
142
- }
143
-
144
- section("Doc Sync");
145
- if (missingInfernoUpdate) {
146
- fail("Code changed but inferno/ was not updated", "Run infernoflow suggest and then infernoflow check");
147
- } else {
148
- ok("No immediate inferno drift signal from changed files");
149
- }
150
- ok(`Confidence: ${cyan(confidence)}`);
151
-
152
- section("Suggested Next");
153
- payload.recommendations.forEach((r) => console.log(` ${yellow("→")} ${r}`));
154
- console.log();
155
- process.exit(payload.ok ? 0 : 1);
156
- }
157
-
1
+ import{execSync as k}from"node:child_process";import*as S from"node:fs";import*as m from"node:path";import{header as w,section as y,ok as d,warn as E,fail as N,gray as $,cyan as g,yellow as _}from"../ui/output.mjs";const D=["src/","frontend/","backend/","app/","pages/","components/","lib/","api/","server/","Controllers/"];function A(n){return k(n,{stdio:["ignore","pipe","pipe"]}).toString("utf8").trim()}function I(n,t=null){try{return JSON.parse(S.readFileSync(n,"utf8"))}catch{return t}}function F(n,t=""){try{return S.readFileSync(n,"utf8")}catch{return t}}function H(n,t){const a=A(n&&t?`git diff --name-only ${n}..${t}`:"git diff --name-only HEAD");return a?a.split(`
2
+ `).map(r=>r.trim()).filter(Boolean):[]}function O(n){const t=m.join(n,"inferno"),a=I(m.join(t,"contract.json"),{capabilities:[]}),r=I(m.join(t,"capabilities.json"),{capabilities:[]}),c=new Map((r.capabilities||[]).map(e=>[e.id,e.title||e.id]));return(a.capabilities||[]).map(e=>{const i=c.get(e)||e,l=new Set(`${e} ${i}`.replace(/([A-Z])/g," $1").toLowerCase().split(/[^a-z0-9]+/).filter(s=>s.length>=4));return{id:e,title:i,keywords:Array.from(l)}})}function R(n,t){const a=O(n),r=[];for(const c of a){const e=[];for(const i of t){const l=m.join(n,i),s=F(l,"").toLowerCase();s&&c.keywords.some(f=>s.includes(f))&&e.push(i)}e.length&&r.push({id:c.id,title:c.title,matchedFiles:e.slice(0,5)})}return r}async function x(n=[]){const t=n.includes("--json"),a=process.cwd(),r=process.env.BASE_SHA||null,c=process.env.HEAD_SHA||null;let e=[];try{e=H(r,c)}catch{const o={ok:!0,skipped:!0,reason:"no_git_available"};if(t){console.log(JSON.stringify(o,null,2));return}w("pr-impact"),E("git not available; cannot compute PR impact"),console.log();return}const i=e.filter(o=>D.some(C=>o.startsWith(C))),l=e.filter(o=>o.startsWith("inferno/")),s=R(a,i),f=i.length>0,h=f&&l.length===0,b=s.length>0?"high":f?"medium":"low",p=[];f&&p.push("CODE_CHANGED"),h&&p.push("INFERNO_NOT_UPDATED"),s.length>0&&p.push("CAPABILITY_HINT_MATCH"),p.length||p.push("NO_BEHAVIOR_SIGNAL");const u={ok:!h,base:r||"HEAD",head:c||"WORKTREE",changedFiles:e,changedCodeFiles:i,changedInfernoFiles:l,inferredBehaviorChange:f,impactedCapabilities:s,confidence:b,reasonCodes:p,recommendations:h?['Run infernoflow suggest "describe behavior change" and update inferno/',"Run infernoflow check --json"]:["Run infernoflow check --json to validate final state"]};t&&(console.log(JSON.stringify(u,null,2)),process.exit(u.ok?0:1)),w("pr-impact"),y("Diff Scope"),d(`Changed files: ${g(String(e.length))}`),d(`Code files: ${g(String(i.length))}`),d(`Inferno files: ${g(String(l.length))}`),y("Capability Impact"),s.length===0?E("No capability hints matched changed code files"):s.forEach(o=>{console.log(` ${g("\u2022")} ${o.id} ${$(`(${o.title})`)}`),o.matchedFiles.slice(0,3).forEach(C=>console.log(` ${$("- "+C)}`))}),y("Doc Sync"),h?N("Code changed but inferno/ was not updated","Run infernoflow suggest and then infernoflow check"):d("No immediate inferno drift signal from changed files"),d(`Confidence: ${g(b)}`),y("Suggested Next"),u.recommendations.forEach(o=>console.log(` ${_("\u2192")} ${o}`)),console.log(),process.exit(u.ok?0:1)}export{x as prImpactCommand};