infernoflow 0.32.7 → 0.32.9

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 (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -320
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -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};