infernoflow 0.13.0 → 0.16.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/dist/bin/infernoflow.mjs +34 -0
- package/dist/lib/commands/changelog.mjs +255 -6
- package/dist/lib/commands/cloud.mjs +521 -0
- package/dist/lib/commands/dashboard.mjs +399 -0
- package/dist/lib/commands/onboard.mjs +296 -0
- package/dist/lib/commands/prComment.mjs +361 -0
- package/dist/lib/commands/teamSync.mjs +388 -0
- package/dist/templates/ci/github-pr-comment.yml +50 -0
- package/package.json +1 -1
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -32,6 +32,11 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
32
32
|
synthesize: "Auto-detect workflow patterns and synthesize reusable skills + agents",
|
|
33
33
|
agent: "Manage and run auto-synthesized agents (list | run | show | delete)",
|
|
34
34
|
version: "Smart semver bump recommendation based on capability changes (--apply to write)",
|
|
35
|
+
"pr-comment": "Post capability drift analysis as a GitHub PR comment (works in CI automatically)",
|
|
36
|
+
dashboard: "Launch local web dashboard on localhost:7337 — live contract health, capabilities, agents",
|
|
37
|
+
"team-sync": "Sync capability contract across a team via a shared git branch (push | pull | status | init)",
|
|
38
|
+
onboard: "Interactive onboarding wizard for new developers — explains infernoflow in 5 minutes",
|
|
39
|
+
cloud: "Sync capability contracts via infernoflow cloud (init | push | pull | status | dashboard)",
|
|
35
40
|
};
|
|
36
41
|
|
|
37
42
|
const COMMAND_HANDLERS = {
|
|
@@ -57,6 +62,11 @@ const COMMAND_HANDLERS = {
|
|
|
57
62
|
synthesize: async (args) => (await import("../lib/commands/synthesize.mjs")).synthesizeCommand(args),
|
|
58
63
|
agent: async (args) => (await import("../lib/commands/agent.mjs")).agentCommand(args),
|
|
59
64
|
version: async (args) => (await import("../lib/commands/version.mjs")).versionCommand(args),
|
|
65
|
+
"pr-comment": async (args) => (await import("../lib/commands/prComment.mjs")).prCommentCommand(args),
|
|
66
|
+
dashboard: async (args) => (await import("../lib/commands/dashboard.mjs")).dashboardCommand(args),
|
|
67
|
+
"team-sync": async (args) => (await import("../lib/commands/teamSync.mjs")).teamSyncCommand(args),
|
|
68
|
+
onboard: async (args) => (await import("../lib/commands/onboard.mjs")).onboardCommand(args),
|
|
69
|
+
cloud: async (args) => (await import("../lib/commands/cloud.mjs")).cloudCommand(args),
|
|
60
70
|
};
|
|
61
71
|
|
|
62
72
|
function formatCommandsHelp() {
|
|
@@ -86,7 +96,9 @@ ${formatCommandsHelp()}
|
|
|
86
96
|
update Draft ## Unreleased from commits (default sub-command)
|
|
87
97
|
show Print the current ## Unreleased block
|
|
88
98
|
list List commits since last tag
|
|
99
|
+
ai Generate human-readable changelog with AI (Anthropic or Ollama)
|
|
89
100
|
--ref <tag|commit> Use a specific ref instead of last tag
|
|
101
|
+
--version <x.y.z> Version label for the AI-generated entry
|
|
90
102
|
--dry-run Print what would be written without modifying file
|
|
91
103
|
--append Append to existing ## Unreleased instead of replacing
|
|
92
104
|
--json Machine-readable output
|
|
@@ -167,6 +179,28 @@ ${formatCommandsHelp()}
|
|
|
167
179
|
--apply Write recommended version bump to package.json
|
|
168
180
|
--json Machine-readable output
|
|
169
181
|
|
|
182
|
+
${bold("pr-comment options:")}
|
|
183
|
+
--pr <number> PR number to comment on (auto-detected in GitHub Actions)
|
|
184
|
+
--repo <owner/repo> GitHub repository (auto-detected in GitHub Actions)
|
|
185
|
+
--token <ghp_...> GitHub token (auto-detected from GITHUB_TOKEN env var)
|
|
186
|
+
--ref <ref> Base ref to diff against (auto-detected from GITHUB_BASE_REF)
|
|
187
|
+
--dry-run Print the comment without posting it
|
|
188
|
+
--json Machine-readable output
|
|
189
|
+
|
|
190
|
+
${bold("cloud sub-commands:")}
|
|
191
|
+
init Generate a project token and configure cloud sync
|
|
192
|
+
push Upload local capability contract to cloud
|
|
193
|
+
pull Download latest contract from cloud (conflict detection)
|
|
194
|
+
status Compare local vs cloud (hashes, capability counts)
|
|
195
|
+
dashboard Print hosted dashboard URL and open in browser
|
|
196
|
+
|
|
197
|
+
${bold("cloud options:")}
|
|
198
|
+
--token <tok> Override token (or set INFERNOFLOW_TOKEN env var)
|
|
199
|
+
--endpoint <url> Override default endpoint (https://cloud.infernoflow.dev)
|
|
200
|
+
--force, -f Overwrite on init; overwrite local on conflicted pull
|
|
201
|
+
--dry-run Print what would happen without sending
|
|
202
|
+
--json Machine-readable output
|
|
203
|
+
|
|
170
204
|
${bold("Machine output:")}
|
|
171
205
|
${gray("status --json")}
|
|
172
206
|
${gray("check --json")}
|
|
@@ -303,6 +303,248 @@ async function subcmdUpdate(cwd, changelogPath, opts) {
|
|
|
303
303
|
console.log(` Run ${cyan("infernoflow publish")} when ready to cut the release\n`);
|
|
304
304
|
}
|
|
305
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
|
+
const { execSync: _exec } = await import ? null : null; // handled below
|
|
327
|
+
try {
|
|
328
|
+
const { execSync: ex } = await import("node:child_process");
|
|
329
|
+
for (const name of ["capabilities.json", "contract.json"]) {
|
|
330
|
+
try {
|
|
331
|
+
const content = ex(`git show "${ref}:inferno/${name}"`, {
|
|
332
|
+
cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
|
|
333
|
+
}).trim();
|
|
334
|
+
if (content) return parseCaps(content);
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
} catch {}
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function diffCapsSimple(before, after) {
|
|
342
|
+
const beforeMap = new Map(before.map(c => [c.id, c]));
|
|
343
|
+
const afterMap = new Map(after.map(c => [c.id, c]));
|
|
344
|
+
return {
|
|
345
|
+
added: after.filter(c => !beforeMap.has(c.id)),
|
|
346
|
+
removed: before.filter(c => !afterMap.has(c.id)),
|
|
347
|
+
changed: after.filter(c => {
|
|
348
|
+
const old = beforeMap.get(c.id);
|
|
349
|
+
return old && old.title !== c.title;
|
|
350
|
+
}),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function buildAiPrompt(commits, diff, ref, version) {
|
|
355
|
+
const lines = [];
|
|
356
|
+
lines.push(`You are writing a changelog entry for a software release.`);
|
|
357
|
+
lines.push(`Write it in a friendly, clear, developer-facing tone.`);
|
|
358
|
+
lines.push(`Use Markdown. Start with ## ${version || "Unreleased"}`);
|
|
359
|
+
lines.push(`Include sections: ### Added, ### Fixed, ### Changed, ### Removed (only if non-empty).`);
|
|
360
|
+
lines.push(`Be concise — one line per item. Do not include commit hashes.`);
|
|
361
|
+
lines.push(`Do not invent features that are not listed below.`);
|
|
362
|
+
lines.push(``);
|
|
363
|
+
lines.push(`## Capability changes since ${ref || "last release"}:`);
|
|
364
|
+
if (diff.added.length) lines.push(`Added: ${diff.added.map(c => c.title || c.id).join(", ")}`);
|
|
365
|
+
if (diff.removed.length) lines.push(`Removed: ${diff.removed.map(c => c.title || c.id).join(", ")}`);
|
|
366
|
+
if (diff.changed.length) lines.push(`Changed: ${diff.changed.map(c => c.title || c.id).join(", ")}`);
|
|
367
|
+
if (!diff.added.length && !diff.removed.length && !diff.changed.length) lines.push(`(no capability changes)`);
|
|
368
|
+
lines.push(``);
|
|
369
|
+
lines.push(`## Git commits since ${ref || "last release"}:`);
|
|
370
|
+
for (const c of commits.slice(0, 40)) lines.push(`- ${c.subject}`);
|
|
371
|
+
lines.push(``);
|
|
372
|
+
lines.push(`Write the changelog entry now:`);
|
|
373
|
+
return lines.join("\n");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildStructuredFallback(commits, diff, version) {
|
|
377
|
+
// High-quality template fallback when no AI provider is available
|
|
378
|
+
const sections = groupCommits(commits);
|
|
379
|
+
|
|
380
|
+
// Merge capability changes into sections
|
|
381
|
+
for (const c of diff.added) sections.Added.unshift(`${c.title || c.id} capability`);
|
|
382
|
+
for (const c of diff.removed) sections.Removed.unshift(`${c.title || c.id} capability`);
|
|
383
|
+
for (const c of diff.changed) sections.Changed.unshift(`Updated ${c.title || c.id} capability`);
|
|
384
|
+
|
|
385
|
+
// Deduplicate
|
|
386
|
+
for (const key of Object.keys(sections)) sections[key] = [...new Set(sections[key])];
|
|
387
|
+
|
|
388
|
+
const tag = version || "Unreleased";
|
|
389
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
390
|
+
const lines = [`## ${tag} — ${date}`, ""];
|
|
391
|
+
|
|
392
|
+
const ORDER = ["Breaking", "Added", "Fixed", "Changed", "Removed"];
|
|
393
|
+
for (const heading of ORDER) {
|
|
394
|
+
const items = sections[heading];
|
|
395
|
+
if (!items?.length) continue;
|
|
396
|
+
lines.push(`### ${heading}`);
|
|
397
|
+
for (const item of items) lines.push(`- ${item}`);
|
|
398
|
+
lines.push("");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return lines.join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function callLocalAi(prompt) {
|
|
405
|
+
// Try Ollama (localhost:11434) — most common local AI setup
|
|
406
|
+
try {
|
|
407
|
+
const { default: http } = await import("node:http");
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
const body = JSON.stringify({ model: "llama3", prompt, stream: false });
|
|
410
|
+
const req = http.request({
|
|
411
|
+
hostname: "localhost", port: 11434,
|
|
412
|
+
path: "/api/generate", method: "POST",
|
|
413
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
414
|
+
}, (res) => {
|
|
415
|
+
let raw = "";
|
|
416
|
+
res.on("data", d => raw += d);
|
|
417
|
+
res.on("end", () => {
|
|
418
|
+
try { resolve(JSON.parse(raw).response || null); } catch { resolve(null); }
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
req.setTimeout(30_000, () => { req.destroy(); reject(new Error("timeout")); });
|
|
422
|
+
req.on("error", reject);
|
|
423
|
+
req.write(body);
|
|
424
|
+
req.end();
|
|
425
|
+
});
|
|
426
|
+
} catch { return null; }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function callAnthropicAi(prompt, apiKey) {
|
|
430
|
+
const { default: https } = await import("node:https");
|
|
431
|
+
return new Promise((resolve, reject) => {
|
|
432
|
+
const body = JSON.stringify({
|
|
433
|
+
model: "claude-haiku-4-5-20251001",
|
|
434
|
+
max_tokens: 1024,
|
|
435
|
+
messages: [{ role: "user", content: prompt }],
|
|
436
|
+
});
|
|
437
|
+
const req = https.request({
|
|
438
|
+
hostname: "api.anthropic.com",
|
|
439
|
+
path: "/v1/messages", method: "POST",
|
|
440
|
+
headers: {
|
|
441
|
+
"x-api-key": apiKey,
|
|
442
|
+
"anthropic-version": "2023-06-01",
|
|
443
|
+
"Content-Type": "application/json",
|
|
444
|
+
"Content-Length": Buffer.byteLength(body),
|
|
445
|
+
},
|
|
446
|
+
}, (res) => {
|
|
447
|
+
let raw = "";
|
|
448
|
+
res.on("data", d => raw += d);
|
|
449
|
+
res.on("end", () => {
|
|
450
|
+
try {
|
|
451
|
+
const data = JSON.parse(raw);
|
|
452
|
+
resolve(data.content?.[0]?.text || null);
|
|
453
|
+
} catch { resolve(null); }
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
req.setTimeout(30_000, () => { req.destroy(); reject(new Error("timeout")); });
|
|
457
|
+
req.on("error", reject);
|
|
458
|
+
req.write(body);
|
|
459
|
+
req.end();
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function subcmdAi(cwd, changelogPath, opts) {
|
|
464
|
+
const { ref, dryRun, asJson, version } = opts;
|
|
465
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
466
|
+
|
|
467
|
+
const tag = ref || lastTag(cwd);
|
|
468
|
+
const commits = commitsSince(tag, cwd);
|
|
469
|
+
|
|
470
|
+
// Load capability diff
|
|
471
|
+
const currentCaps = fs.existsSync(infernoDir) ? loadCapsFromDisk(infernoDir) : [];
|
|
472
|
+
const prevCaps = tag ? (() => {
|
|
473
|
+
try {
|
|
474
|
+
const ex = require("child_process").execSync;
|
|
475
|
+
for (const name of ["capabilities.json", "contract.json"]) {
|
|
476
|
+
try {
|
|
477
|
+
const c = ex(`git show "${tag}:inferno/${name}"`, { cwd, encoding: "utf8", stdio: ["ignore","pipe","pipe"] }).trim();
|
|
478
|
+
if (c) return parseCaps(c);
|
|
479
|
+
} catch {}
|
|
480
|
+
}
|
|
481
|
+
return [];
|
|
482
|
+
} catch { return []; }
|
|
483
|
+
})() : [];
|
|
484
|
+
|
|
485
|
+
const diff = diffCapsSimple(prevCaps, currentCaps);
|
|
486
|
+
|
|
487
|
+
if (!asJson) {
|
|
488
|
+
info(`Generating AI changelog since ${bold(tag || "beginning")}...`);
|
|
489
|
+
info(`${commits.length} commits · ${diff.added.length} added · ${diff.removed.length} removed · ${diff.changed.length} changed`);
|
|
490
|
+
console.log();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const prompt = buildAiPrompt(commits, diff, tag, version);
|
|
494
|
+
let aiText = null;
|
|
495
|
+
let provider = "template";
|
|
496
|
+
|
|
497
|
+
// Try AI providers in order: Anthropic API → Ollama → structured template
|
|
498
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
499
|
+
if (anthropicKey) {
|
|
500
|
+
try {
|
|
501
|
+
aiText = await callAnthropicAi(prompt, anthropicKey);
|
|
502
|
+
if (aiText) provider = "anthropic";
|
|
503
|
+
} catch {}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!aiText) {
|
|
507
|
+
try {
|
|
508
|
+
aiText = await callLocalAi(prompt);
|
|
509
|
+
if (aiText) provider = "ollama";
|
|
510
|
+
} catch {}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Fallback: structured template
|
|
514
|
+
if (!aiText) {
|
|
515
|
+
aiText = buildStructuredFallback(commits, diff, version);
|
|
516
|
+
provider = "template";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (asJson) {
|
|
520
|
+
console.log(JSON.stringify({ ok: true, provider, ref: tag, commits: commits.length, markdown: aiText }));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log(gray(" ─── Generated changelog ──────────────────────────────"));
|
|
525
|
+
aiText.split("\n").forEach(l => console.log(" " + l));
|
|
526
|
+
console.log(gray(" ──────────────────────────────────────────────────────"));
|
|
527
|
+
console.log();
|
|
528
|
+
info(`Generated via: ${bold(provider)}`);
|
|
529
|
+
|
|
530
|
+
if (dryRun) {
|
|
531
|
+
warn("Dry run — CHANGELOG.md not modified");
|
|
532
|
+
console.log();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Write to CHANGELOG.md
|
|
537
|
+
let text = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, "utf8") : "# Changelog\n\n";
|
|
538
|
+
const updated = injectUnreleased(text, aiText);
|
|
539
|
+
fs.writeFileSync(changelogPath, updated);
|
|
540
|
+
|
|
541
|
+
ok(`CHANGELOG.md updated with AI-generated entry`);
|
|
542
|
+
if (provider === "template") {
|
|
543
|
+
info(`Tip: set ANTHROPIC_API_KEY for richer AI-written changelogs`);
|
|
544
|
+
}
|
|
545
|
+
console.log();
|
|
546
|
+
}
|
|
547
|
+
|
|
306
548
|
// ── main ─────────────────────────────────────────────────────────────────────
|
|
307
549
|
|
|
308
550
|
export async function changelogCommand(rawArgs) {
|
|
@@ -311,12 +553,14 @@ export async function changelogCommand(rawArgs) {
|
|
|
311
553
|
// Sub-command: first non-flag arg
|
|
312
554
|
const sub = args.find(a => !a.startsWith("-")) || "update";
|
|
313
555
|
|
|
314
|
-
const dryRun
|
|
315
|
-
const append
|
|
316
|
-
const asJson
|
|
556
|
+
const dryRun = args.includes("--dry-run");
|
|
557
|
+
const append = args.includes("--append");
|
|
558
|
+
const asJson = args.includes("--json");
|
|
317
559
|
|
|
318
|
-
const refIdx
|
|
319
|
-
const
|
|
560
|
+
const refIdx = args.indexOf("--ref");
|
|
561
|
+
const versionIdx = args.indexOf("--version");
|
|
562
|
+
const ref = refIdx !== -1 ? args[refIdx + 1] : null;
|
|
563
|
+
const version = versionIdx !== -1 ? args[versionIdx + 1] : null;
|
|
320
564
|
|
|
321
565
|
const cwd = process.cwd();
|
|
322
566
|
const changelogPath = path.join(cwd, "CHANGELOG.md");
|
|
@@ -338,6 +582,11 @@ export async function changelogCommand(rawArgs) {
|
|
|
338
582
|
return;
|
|
339
583
|
}
|
|
340
584
|
|
|
341
|
-
|
|
585
|
+
if (sub === "ai") {
|
|
586
|
+
await subcmdAi(cwd, changelogPath, { ref, dryRun, asJson, version });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
fail(`Unknown sub-command: ${sub}`, "Use: update | show | list | ai");
|
|
342
591
|
process.exit(1);
|
|
343
592
|
}
|