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,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};