infernoflow 0.32.8 → 0.33.0

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 (81) 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 +34 -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 -321
  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/log.mjs +16 -0
  37. package/dist/lib/commands/monorepo.mjs +4 -428
  38. package/dist/lib/commands/notify.mjs +4 -258
  39. package/dist/lib/commands/onboard.mjs +4 -296
  40. package/dist/lib/commands/prComment.mjs +2 -361
  41. package/dist/lib/commands/prImpact.mjs +2 -157
  42. package/dist/lib/commands/publish.mjs +15 -316
  43. package/dist/lib/commands/report.mjs +28 -272
  44. package/dist/lib/commands/review.mjs +9 -223
  45. package/dist/lib/commands/run.mjs +8 -336
  46. package/dist/lib/commands/scaffold.mjs +54 -419
  47. package/dist/lib/commands/scan.mjs +5 -558
  48. package/dist/lib/commands/scout.mjs +2 -291
  49. package/dist/lib/commands/setup.mjs +5 -310
  50. package/dist/lib/commands/share.mjs +13 -196
  51. package/dist/lib/commands/snapshot.mjs +3 -383
  52. package/dist/lib/commands/stability.mjs +2 -293
  53. package/dist/lib/commands/status.mjs +4 -172
  54. package/dist/lib/commands/suggest.mjs +21 -563
  55. package/dist/lib/commands/syncAuto.mjs +1 -96
  56. package/dist/lib/commands/synthesize.mjs +10 -228
  57. package/dist/lib/commands/teamSync.mjs +2 -388
  58. package/dist/lib/commands/test.mjs +6 -363
  59. package/dist/lib/commands/theme.mjs +18 -0
  60. package/dist/lib/commands/version.mjs +2 -282
  61. package/dist/lib/commands/vibe.mjs +7 -357
  62. package/dist/lib/commands/watch.mjs +4 -203
  63. package/dist/lib/commands/why.mjs +4 -358
  64. package/dist/lib/cursorHooksInstall.mjs +1 -60
  65. package/dist/lib/draftToolingInstall.mjs +7 -68
  66. package/dist/lib/git/detect-drift.mjs +4 -208
  67. package/dist/lib/learning/adapt.mjs +6 -101
  68. package/dist/lib/learning/observe.mjs +1 -119
  69. package/dist/lib/learning/patternDetector.mjs +1 -298
  70. package/dist/lib/learning/profile.mjs +2 -279
  71. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  72. package/dist/lib/templates/index.mjs +1 -131
  73. package/dist/lib/theme/scanner.mjs +4 -0
  74. package/dist/lib/ui/errors.mjs +1 -142
  75. package/dist/lib/ui/output.mjs +6 -72
  76. package/dist/lib/ui/prompts.mjs +6 -147
  77. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  78. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  79. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  80. package/dist/templates/github-app/app-manifest.json +20 -0
  81. package/package.json +1 -1
@@ -1,594 +1,26 @@
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
- // ── AI changelog writer ───────────────────────────────────────────────────────
307
-
308
- function parseCaps(jsonText) {
309
- if (!jsonText) return [];
310
- try {
311
- const obj = JSON.parse(jsonText);
312
- const raw = obj.capabilities || [];
313
- return raw.map(c => typeof c === "string" ? { id: c, title: c } : c);
314
- } catch { return []; }
315
- }
316
-
317
- function loadCapsFromDisk(infernoDir) {
318
- for (const name of ["capabilities.json", "contract.json"]) {
319
- const p = path.join(infernoDir, name);
320
- if (fs.existsSync(p)) return parseCaps(fs.readFileSync(p, "utf8"));
321
- }
322
- return [];
323
- }
324
-
325
- function loadCapsAtRef(ref, cwd) {
326
- try {
327
- for (const name of ["capabilities.json", "contract.json"]) {
328
- try {
329
- const content = execSync(`git show "${ref}:inferno/${name}"`, {
330
- cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
331
- }).trim();
332
- if (content) return parseCaps(content);
333
- } catch {}
334
- }
335
- } catch {}
336
- return [];
337
- }
338
-
339
- function diffCapsSimple(before, after) {
340
- const beforeMap = new Map(before.map(c => [c.id, c]));
341
- const afterMap = new Map(after.map(c => [c.id, c]));
342
- return {
343
- added: after.filter(c => !beforeMap.has(c.id)),
344
- removed: before.filter(c => !afterMap.has(c.id)),
345
- changed: after.filter(c => {
346
- const old = beforeMap.get(c.id);
347
- return old && old.title !== c.title;
348
- }),
349
- };
350
- }
351
-
352
- function buildAiPrompt(commits, diff, ref, version) {
353
- const lines = [];
354
- lines.push(`You are writing a changelog entry for a software release.`);
355
- lines.push(`Write it in a friendly, clear, developer-facing tone.`);
356
- lines.push(`Use Markdown. Start with ## ${version || "Unreleased"}`);
357
- lines.push(`Include sections: ### Added, ### Fixed, ### Changed, ### Removed (only if non-empty).`);
358
- lines.push(`Be concise — one line per item. Do not include commit hashes.`);
359
- lines.push(`Do not invent features that are not listed below.`);
360
- lines.push(``);
361
- lines.push(`## Capability changes since ${ref || "last release"}:`);
362
- if (diff.added.length) lines.push(`Added: ${diff.added.map(c => c.title || c.id).join(", ")}`);
363
- if (diff.removed.length) lines.push(`Removed: ${diff.removed.map(c => c.title || c.id).join(", ")}`);
364
- if (diff.changed.length) lines.push(`Changed: ${diff.changed.map(c => c.title || c.id).join(", ")}`);
365
- if (!diff.added.length && !diff.removed.length && !diff.changed.length) lines.push(`(no capability changes)`);
366
- lines.push(``);
367
- lines.push(`## Git commits since ${ref || "last release"}:`);
368
- for (const c of commits.slice(0, 40)) lines.push(`- ${c.subject}`);
369
- lines.push(``);
370
- lines.push(`Write the changelog entry now:`);
371
- return lines.join("\n");
372
- }
373
-
374
- function buildStructuredFallback(commits, diff, version) {
375
- // High-quality template fallback when no AI provider is available
376
- const sections = groupCommits(commits);
377
-
378
- // Merge capability changes into sections
379
- for (const c of diff.added) sections.Added.unshift(`${c.title || c.id} capability`);
380
- for (const c of diff.removed) sections.Removed.unshift(`${c.title || c.id} capability`);
381
- for (const c of diff.changed) sections.Changed.unshift(`Updated ${c.title || c.id} capability`);
382
-
383
- // Deduplicate
384
- for (const key of Object.keys(sections)) sections[key] = [...new Set(sections[key])];
385
-
386
- const tag = version || "Unreleased";
387
- const date = new Date().toISOString().slice(0, 10);
388
- const lines = [`## ${tag} — ${date}`, ""];
389
-
390
- const ORDER = ["Breaking", "Added", "Fixed", "Changed", "Removed"];
391
- for (const heading of ORDER) {
392
- const items = sections[heading];
393
- if (!items?.length) continue;
394
- lines.push(`### ${heading}`);
395
- for (const item of items) lines.push(`- ${item}`);
396
- lines.push("");
397
- }
398
-
399
- return lines.join("\n");
400
- }
401
-
402
- async function callLocalAi(prompt) {
403
- // Try Ollama (localhost:11434) — most common local AI setup
404
- try {
405
- const { default: http } = await import("node:http");
406
- return new Promise((resolve, reject) => {
407
- const body = JSON.stringify({ model: "llama3", prompt, stream: false });
408
- const req = http.request({
409
- hostname: "localhost", port: 11434,
410
- path: "/api/generate", method: "POST",
411
- headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
412
- }, (res) => {
413
- let raw = "";
414
- res.on("data", d => raw += d);
415
- res.on("end", () => {
416
- try { resolve(JSON.parse(raw).response || null); } catch { resolve(null); }
417
- });
418
- });
419
- req.setTimeout(30_000, () => { req.destroy(); reject(new Error("timeout")); });
420
- req.on("error", reject);
421
- req.write(body);
422
- req.end();
423
- });
424
- } catch { return null; }
425
- }
426
-
427
- async function callAnthropicAi(prompt, apiKey) {
428
- const { default: https } = await import("node:https");
429
- return new Promise((resolve, reject) => {
430
- const body = JSON.stringify({
431
- model: "claude-haiku-4-5-20251001",
432
- max_tokens: 1024,
433
- messages: [{ role: "user", content: prompt }],
434
- });
435
- const req = https.request({
436
- hostname: "api.anthropic.com",
437
- path: "/v1/messages", method: "POST",
438
- headers: {
439
- "x-api-key": apiKey,
440
- "anthropic-version": "2023-06-01",
441
- "Content-Type": "application/json",
442
- "Content-Length": Buffer.byteLength(body),
443
- },
444
- }, (res) => {
445
- let raw = "";
446
- res.on("data", d => raw += d);
447
- res.on("end", () => {
448
- try {
449
- const data = JSON.parse(raw);
450
- resolve(data.content?.[0]?.text || null);
451
- } catch { resolve(null); }
452
- });
453
- });
454
- req.setTimeout(30_000, () => { req.destroy(); reject(new Error("timeout")); });
455
- req.on("error", reject);
456
- req.write(body);
457
- req.end();
458
- });
459
- }
460
-
461
- async function subcmdAi(cwd, changelogPath, opts) {
462
- const { ref, dryRun, asJson, version } = opts;
463
- const infernoDir = path.join(cwd, "inferno");
464
-
465
- const tag = ref || lastTag(cwd);
466
- const commits = commitsSince(tag, cwd);
467
-
468
- // Load capability diff
469
- const currentCaps = fs.existsSync(infernoDir) ? loadCapsFromDisk(infernoDir) : [];
470
- const prevCaps = tag ? (() => {
471
- try {
472
- const ex = require("child_process").execSync;
473
- for (const name of ["capabilities.json", "contract.json"]) {
474
- try {
475
- const c = ex(`git show "${tag}:inferno/${name}"`, { cwd, encoding: "utf8", stdio: ["ignore","pipe","pipe"] }).trim();
476
- if (c) return parseCaps(c);
477
- } catch {}
478
- }
479
- return [];
480
- } catch { return []; }
481
- })() : [];
482
-
483
- const diff = diffCapsSimple(prevCaps, currentCaps);
484
-
485
- if (!asJson) {
486
- info(`Generating AI changelog since ${bold(tag || "beginning")}...`);
487
- info(`${commits.length} commits · ${diff.added.length} added · ${diff.removed.length} removed · ${diff.changed.length} changed`);
488
- console.log();
489
- }
490
-
491
- const prompt = buildAiPrompt(commits, diff, tag, version);
492
- let aiText = null;
493
- let provider = "template";
494
-
495
- // Try AI providers in order: Anthropic API → Ollama → structured template
496
- const anthropicKey = process.env.ANTHROPIC_API_KEY;
497
- if (anthropicKey) {
498
- try {
499
- aiText = await callAnthropicAi(prompt, anthropicKey);
500
- if (aiText) provider = "anthropic";
501
- } catch {}
502
- }
503
-
504
- if (!aiText) {
505
- try {
506
- aiText = await callLocalAi(prompt);
507
- if (aiText) provider = "ollama";
508
- } catch {}
509
- }
510
-
511
- // Fallback: structured template
512
- if (!aiText) {
513
- aiText = buildStructuredFallback(commits, diff, version);
514
- provider = "template";
515
- }
516
-
517
- if (asJson) {
518
- console.log(JSON.stringify({ ok: true, provider, ref: tag, commits: commits.length, markdown: aiText }));
519
- return;
520
- }
521
-
522
- console.log(gray(" ─── Generated changelog ──────────────────────────────"));
523
- aiText.split("\n").forEach(l => console.log(" " + l));
524
- console.log(gray(" ──────────────────────────────────────────────────────"));
525
- console.log();
526
- if (provider === "template") {
527
- console.log(` ${yellow("💡")} ${gray("For AI-written changelogs:")} ${cyan("infernoflow ai setup")}`);
528
- } else {
529
- info(`Generated via: ${bold(provider)}`);
530
- };
531
-
532
- if (dryRun) {
533
- warn("Dry run — CHANGELOG.md not modified");
534
- console.log();
535
- return;
536
- }
537
-
538
- // Write to CHANGELOG.md
539
- let text = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, "utf8") : "# Changelog\n\n";
540
- const updated = injectUnreleased(text, aiText);
541
- fs.writeFileSync(changelogPath, updated);
542
-
543
- ok(`CHANGELOG.md updated with AI-generated entry`);
544
- if (provider === "template") {
545
- info(`Tip: set ANTHROPIC_API_KEY for richer AI-written changelogs`);
546
- }
547
- console.log();
548
- }
549
-
550
- // ── main ─────────────────────────────────────────────────────────────────────
551
-
552
- export async function changelogCommand(rawArgs) {
553
- const args = rawArgs.slice(1); // drop "changelog"
554
-
555
- // Sub-command: first non-flag arg
556
- const sub = args.find(a => !a.startsWith("-")) || "update";
557
-
558
- const dryRun = args.includes("--dry-run");
559
- const append = args.includes("--append");
560
- const asJson = args.includes("--json");
561
-
562
- const refIdx = args.indexOf("--ref");
563
- const versionIdx = args.indexOf("--version");
564
- const ref = refIdx !== -1 ? args[refIdx + 1] : null;
565
- const version = versionIdx !== -1 ? args[versionIdx + 1] : null;
566
-
567
- const cwd = process.cwd();
568
- const changelogPath = path.join(cwd, "CHANGELOG.md");
569
-
570
- if (!asJson) header("changelog " + sub);
571
-
572
- if (sub === "list") {
573
- subcmdList(cwd, ref);
574
- return;
575
- }
576
-
577
- if (sub === "show") {
578
- subcmdShow(changelogPath);
579
- return;
580
- }
581
-
582
- if (sub === "update") {
583
- await subcmdUpdate(cwd, changelogPath, { ref, dryRun, append, asJson });
584
- return;
585
- }
586
-
587
- if (sub === "ai") {
588
- await subcmdAi(cwd, changelogPath, { ref, dryRun, asJson, version });
589
- return;
590
- }
591
-
592
- fail(`Unknown sub-command: ${sub}`, "Use: update | show | list | ai");
593
- process.exit(1);
594
- }
1
+ import*as h from"node:fs";import*as A from"node:path";import{execSync as U}from"node:child_process";import{header as _,ok as G,fail as E,warn as b,info as y,done as B,bold as S,cyan as w,gray as p,green as M,yellow as F}from"../ui/output.mjs";function v(n,e){try{return U(n,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}catch{return null}}function x(n){return v("git describe --tags --abbrev=0",n)}function k(n,e){const o=n?`${n}..HEAD`:"",i=v(`git log ${o} --format=%H%x1f%s%x1f%b%x1e`,e);return i?i.split("").map(t=>t.trim()).filter(Boolean).map(t=>{const s=t.split("");return{hash:(s[0]||"").trim().slice(0,8),subject:(s[1]||"").trim(),body:(s[2]||"").trim()}}).filter(t=>t.subject):[]}function re(n,e){return v(`git log -1 --format=%ci "${n}"`,e)?.slice(0,10)||null}const L={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 D(n){const e=n.match(/^(\w+)(?:\([^)]+\))?[!]?:\s*(.+)/);if(e){const i=e[1].toLowerCase();return{section:L[i]||"Changed",message:e[2].trim(),breaking:n.includes("!")}}const o=n.toLowerCase();for(const[i,t]of Object.entries(L))if(o.startsWith(i+" ")||o.startsWith(i+":"))return{section:t,message:n,breaking:!1};return{section:"Changed",message:n,breaking:!1}}function I(n){const e={Added:[],Fixed:[],Changed:[],Removed:[],Breaking:[]};for(const o of n){const{section:i,message:t,breaking:s}=D(o.subject);s&&e.Breaking.push(t),e[i].push(t)}for(const o of Object.keys(e))e[o]=[...new Set(e[o])];return e}function W(n,e){const o=["## Unreleased",""];e&&o.push(`> Changes since ${e}`,"");const i=["Breaking","Added","Fixed","Changed","Removed"];let t=!1;for(const s of i){const a=n[s];if(!(!a||!a.length)){t=!0,o.push(`### ${s}`);for(const c of a)o.push(`- ${c}`);o.push("")}}return t||o.push("- No significant changes",""),o.join(`
2
+ `)}function T(n){return h.existsSync(n)?h.readFileSync(n,"utf8"):null}function q(n){const e=n.match(/^## Unreleased[\s\S]*?(?=\n## |\n---|\z)/im);return e?e[0].trim():null}function O(n,e){return/^## Unreleased/im.test(n)?n.replace(/^## Unreleased[\s\S]*?(?=\n## |\n---)/im,e+`
3
+
4
+ `):/^# .+/im.test(n)?n.replace(/^(# .+\n)/im,`$1
5
+ ${e}
6
+
7
+ `):`${e}
8
+
9
+ ${n}`}function Y(n,e){const o=e.split(`
10
+ `).filter(i=>i.startsWith("- ")).join(`
11
+ `);return o?/^## Unreleased/im.test(n)?n.replace(/(^## Unreleased[\s\S]*?)(\n## )/im,`$1
12
+ ${o}
13
+ $2`):O(n,e):n}function K(n,e){const o=e||x(n),i=k(o,n);if(!i.length){y(`No commits since ${o||"beginning"}`);return}console.log(`
14
+ ${S("Commits since")} ${w(o||"beginning")} ${p("("+i.length+")")}
15
+ `);for(const t of i){const{section:s}=D(t.subject),a=s==="Added"?M:s==="Fixed"?F:p;console.log(` ${p(t.hash)} ${a(t.subject)}`)}console.log()}function z(n){const e=T(n);if(!e){E("CHANGELOG.md not found");return}const o=q(e);if(!o){b("No ## Unreleased section found in CHANGELOG.md");return}console.log(`
16
+ `+o+`
17
+ `)}async function Q(n,e,o){const{ref:i,dryRun:t,append:s,asJson:a}=o,c=i||x(n),r=k(c,n);if(!r.length){if(a){console.log(JSON.stringify({ok:!0,ref:c,commits:0,message:"No new commits"}));return}b(`No commits found since ${c||"beginning of repo"}`),console.log();return}const d=I(r),l=W(d,c);if(a){console.log(JSON.stringify({ok:!0,ref:c,commits:r.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(p(" \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($=>console.log(" "+$)),console.log(p(" \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(),y(`${r.length} commit${r.length>1?"s":""} since ${w(c||"beginning")}`),t){b("Dry run \u2014 CHANGELOG.md not modified"),console.log();return}let u=T(e);u||(u=`# Changelog
19
+
20
+ `);const g=s?Y(u,l):O(u,l);h.writeFileSync(e,g),G(`CHANGELOG.md updated ${p("("+(s?"appended":"replaced")+" ## Unreleased)")}`),console.log(),B("Changelog drafted \u2014 review and edit before your next release"),console.log(` Run ${w("infernoflow publish")} when ready to cut the release
21
+ `)}function j(n){if(!n)return[];try{return(JSON.parse(n).capabilities||[]).map(i=>typeof i=="string"?{id:i,title:i}:i)}catch{return[]}}function V(n){for(const e of["capabilities.json","contract.json"]){const o=A.join(n,e);if(h.existsSync(o))return j(h.readFileSync(o,"utf8"))}return[]}function ae(n,e){try{for(const o of["capabilities.json","contract.json"])try{const i=U(`git show "${n}:inferno/${o}"`,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim();if(i)return j(i)}catch{}}catch{}return[]}function X(n,e){const o=new Map(n.map(t=>[t.id,t])),i=new Map(e.map(t=>[t.id,t]));return{added:e.filter(t=>!o.has(t.id)),removed:n.filter(t=>!i.has(t.id)),changed:e.filter(t=>{const s=o.get(t.id);return s&&s.title!==t.title})}}function Z(n,e,o,i){const t=[];t.push("You are writing a changelog entry for a software release."),t.push("Write it in a friendly, clear, developer-facing tone."),t.push(`Use Markdown. Start with ## ${i||"Unreleased"}`),t.push("Include sections: ### Added, ### Fixed, ### Changed, ### Removed (only if non-empty)."),t.push("Be concise \u2014 one line per item. Do not include commit hashes."),t.push("Do not invent features that are not listed below."),t.push(""),t.push(`## Capability changes since ${o||"last release"}:`),e.added.length&&t.push(`Added: ${e.added.map(s=>s.title||s.id).join(", ")}`),e.removed.length&&t.push(`Removed: ${e.removed.map(s=>s.title||s.id).join(", ")}`),e.changed.length&&t.push(`Changed: ${e.changed.map(s=>s.title||s.id).join(", ")}`),!e.added.length&&!e.removed.length&&!e.changed.length&&t.push("(no capability changes)"),t.push(""),t.push(`## Git commits since ${o||"last release"}:`);for(const s of n.slice(0,40))t.push(`- ${s.subject}`);return t.push(""),t.push("Write the changelog entry now:"),t.join(`
22
+ `)}function ee(n,e,o){const i=I(n);for(const r of e.added)i.Added.unshift(`${r.title||r.id} capability`);for(const r of e.removed)i.Removed.unshift(`${r.title||r.id} capability`);for(const r of e.changed)i.Changed.unshift(`Updated ${r.title||r.id} capability`);for(const r of Object.keys(i))i[r]=[...new Set(i[r])];const t=o||"Unreleased",s=new Date().toISOString().slice(0,10),a=[`## ${t} \u2014 ${s}`,""],c=["Breaking","Added","Fixed","Changed","Removed"];for(const r of c){const d=i[r];if(d?.length){a.push(`### ${r}`);for(const l of d)a.push(`- ${l}`);a.push("")}}return a.join(`
23
+ `)}async function ne(n){try{const{default:e}=await import("node:http");return new Promise((o,i)=>{const t=JSON.stringify({model:"llama3",prompt:n,stream:!1}),s=e.request({hostname:"localhost",port:11434,path:"/api/generate",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(t)}},a=>{let c="";a.on("data",r=>c+=r),a.on("end",()=>{try{o(JSON.parse(c).response||null)}catch{o(null)}})});s.setTimeout(3e4,()=>{s.destroy(),i(new Error("timeout"))}),s.on("error",i),s.write(t),s.end()})}catch{return null}}async function te(n,e){const{default:o}=await import("node:https");return new Promise((i,t)=>{const s=JSON.stringify({model:"claude-haiku-4-5-20251001",max_tokens:1024,messages:[{role:"user",content:n}]}),a=o.request({hostname:"api.anthropic.com",path:"/v1/messages",method:"POST",headers:{"x-api-key":e,"anthropic-version":"2023-06-01","Content-Type":"application/json","Content-Length":Buffer.byteLength(s)}},c=>{let r="";c.on("data",d=>r+=d),c.on("end",()=>{try{const d=JSON.parse(r);i(d.content?.[0]?.text||null)}catch{i(null)}})});a.setTimeout(3e4,()=>{a.destroy(),t(new Error("timeout"))}),a.on("error",t),a.write(s),a.end()})}async function oe(n,e,o){const{ref:i,dryRun:t,asJson:s,version:a}=o,c=A.join(n,"inferno"),r=i||x(n),d=k(r,n),l=h.existsSync(c)?V(c):[],u=r?(()=>{try{const C=require("child_process").execSync;for(const P of["capabilities.json","contract.json"])try{const R=C(`git show "${r}:inferno/${P}"`,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim();if(R)return j(R)}catch{}return[]}catch{return[]}})():[],g=X(u,l);s||(y(`Generating AI changelog since ${S(r||"beginning")}...`),y(`${d.length} commits \xB7 ${g.added.length} added \xB7 ${g.removed.length} removed \xB7 ${g.changed.length} changed`),console.log());const $=Z(d,g,r,a);let f=null,m="template";const N=process.env.ANTHROPIC_API_KEY;if(N)try{f=await te($,N),f&&(m="anthropic")}catch{}if(!f)try{f=await ne($),f&&(m="ollama")}catch{}if(f||(f=ee(d,g,a),m="template"),s){console.log(JSON.stringify({ok:!0,provider:m,ref:r,commits:d.length,markdown:f}));return}if(console.log(p(" \u2500\u2500\u2500 Generated changelog \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")),f.split(`
24
+ `).forEach(C=>console.log(" "+C)),console.log(p(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(),m==="template"?console.log(` ${F("\u{1F4A1}")} ${p("For AI-written changelogs:")} ${w("infernoflow ai setup")}`):y(`Generated via: ${S(m)}`),t){b("Dry run \u2014 CHANGELOG.md not modified"),console.log();return}let H=h.existsSync(e)?h.readFileSync(e,"utf8"):`# Changelog
25
+
26
+ `;const J=O(H,f);h.writeFileSync(e,J),G("CHANGELOG.md updated with AI-generated entry"),m==="template"&&y("Tip: set ANTHROPIC_API_KEY for richer AI-written changelogs"),console.log()}async function ce(n){const e=n.slice(1),o=e.find(g=>!g.startsWith("-"))||"update",i=e.includes("--dry-run"),t=e.includes("--append"),s=e.includes("--json"),a=e.indexOf("--ref"),c=e.indexOf("--version"),r=a!==-1?e[a+1]:null,d=c!==-1?e[c+1]:null,l=process.cwd(),u=A.join(l,"CHANGELOG.md");if(s||_("changelog "+o),o==="list"){K(l,r);return}if(o==="show"){z(u);return}if(o==="update"){await Q(l,u,{ref:r,dryRun:i,append:t,asJson:s});return}if(o==="ai"){await oe(l,u,{ref:r,dryRun:i,asJson:s,version:d});return}E(`Unknown sub-command: ${o}`,"Use: update | show | list | ai"),process.exit(1)}export{ce as changelogCommand};