infernoflow 0.33.0 → 0.34.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.
- package/README.md +208 -120
- package/dist/bin/infernoflow.mjs +271 -85
- package/dist/lib/adopters/angular.mjs +128 -1
- package/dist/lib/adopters/css.mjs +111 -1
- package/dist/lib/adopters/react.mjs +104 -1
- package/dist/lib/ai/ideDetection.mjs +31 -1
- package/dist/lib/ai/localProvider.mjs +88 -1
- package/dist/lib/ai/providerRouter.mjs +295 -2
- package/dist/lib/commands/adopt.mjs +869 -20
- package/dist/lib/commands/adoptWizard.mjs +320 -9
- package/dist/lib/commands/agent.mjs +191 -5
- package/dist/lib/commands/ai.mjs +407 -2
- package/dist/lib/commands/ask.mjs +299 -0
- package/dist/lib/commands/audit.mjs +300 -13
- package/dist/lib/commands/changelog.mjs +594 -26
- package/dist/lib/commands/check.mjs +184 -3
- package/dist/lib/commands/ci.mjs +208 -3
- package/dist/lib/commands/claudeMd.mjs +139 -28
- package/dist/lib/commands/cloud.mjs +521 -5
- package/dist/lib/commands/context.mjs +346 -34
- package/dist/lib/commands/coverage.mjs +282 -2
- package/dist/lib/commands/dashboard.mjs +635 -123
- package/dist/lib/commands/demo.mjs +465 -8
- package/dist/lib/commands/diff.mjs +274 -5
- package/dist/lib/commands/docGate.mjs +81 -2
- package/dist/lib/commands/doctor.mjs +321 -3
- package/dist/lib/commands/explain.mjs +438 -8
- package/dist/lib/commands/export.mjs +239 -10
- package/dist/lib/commands/generateSkills.mjs +163 -38
- package/dist/lib/commands/graph.mjs +378 -11
- package/dist/lib/commands/health.mjs +309 -2
- package/dist/lib/commands/impact.mjs +325 -2
- package/dist/lib/commands/implement.mjs +103 -7
- package/dist/lib/commands/init.mjs +545 -23
- package/dist/lib/commands/installCursorHooks.mjs +36 -1
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
- package/dist/lib/commands/link.mjs +342 -2
- package/dist/lib/commands/log.mjs +164 -16
- package/dist/lib/commands/monorepo.mjs +428 -4
- package/dist/lib/commands/notify.mjs +258 -4
- package/dist/lib/commands/onboard.mjs +296 -4
- package/dist/lib/commands/prComment.mjs +361 -2
- package/dist/lib/commands/prImpact.mjs +157 -2
- package/dist/lib/commands/publish.mjs +316 -15
- package/dist/lib/commands/recap.mjs +359 -0
- package/dist/lib/commands/report.mjs +272 -28
- package/dist/lib/commands/review.mjs +223 -9
- package/dist/lib/commands/run.mjs +336 -8
- package/dist/lib/commands/scaffold.mjs +419 -54
- package/dist/lib/commands/scan.mjs +1118 -5
- package/dist/lib/commands/scout.mjs +291 -2
- package/dist/lib/commands/setup.mjs +310 -5
- package/dist/lib/commands/share.mjs +196 -13
- package/dist/lib/commands/snapshot.mjs +383 -3
- package/dist/lib/commands/stability.mjs +293 -2
- package/dist/lib/commands/stats.mjs +402 -0
- package/dist/lib/commands/status.mjs +172 -4
- package/dist/lib/commands/suggest.mjs +563 -21
- package/dist/lib/commands/switch.mjs +310 -0
- package/dist/lib/commands/syncAuto.mjs +96 -1
- package/dist/lib/commands/synthesize.mjs +228 -10
- package/dist/lib/commands/teamSync.mjs +388 -2
- package/dist/lib/commands/test.mjs +363 -6
- package/dist/lib/commands/theme.mjs +195 -18
- package/dist/lib/commands/upgrade.mjs +153 -0
- package/dist/lib/commands/version.mjs +282 -2
- package/dist/lib/commands/vibe.mjs +357 -7
- package/dist/lib/commands/watch.mjs +203 -4
- package/dist/lib/commands/why.mjs +358 -4
- package/dist/lib/cursorHooksInstall.mjs +60 -1
- package/dist/lib/draftToolingInstall.mjs +68 -7
- package/dist/lib/git/detect-drift.mjs +208 -4
- package/dist/lib/learning/adapt.mjs +101 -6
- package/dist/lib/learning/observe.mjs +119 -1
- package/dist/lib/learning/patternDetector.mjs +298 -1
- package/dist/lib/learning/profile.mjs +279 -2
- package/dist/lib/learning/skillSynthesizer.mjs +145 -24
- package/dist/lib/templates/index.mjs +131 -1
- package/dist/lib/theme/scanner.mjs +343 -4
- package/dist/lib/ui/errors.mjs +142 -1
- package/dist/lib/ui/output.mjs +72 -6
- package/dist/lib/ui/prompts.mjs +147 -6
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
- package/package.json +1 -1
|
@@ -1,26 +1,594 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
}
|