infernoflow 0.14.0 → 0.17.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 +44 -0
- package/dist/lib/commands/changelog.mjs +255 -6
- package/dist/lib/commands/ci.mjs +207 -0
- package/dist/lib/commands/cloud.mjs +521 -0
- package/dist/lib/commands/onboard.mjs +296 -0
- package/dist/lib/commands/share.mjs +236 -0
- package/dist/lib/commands/watch.mjs +203 -0
- package/package.json +1 -1
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -35,6 +35,11 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
35
35
|
"pr-comment": "Post capability drift analysis as a GitHub PR comment (works in CI automatically)",
|
|
36
36
|
dashboard: "Launch local web dashboard on localhost:7337 — live contract health, capabilities, agents",
|
|
37
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)",
|
|
40
|
+
share: "Generate a public read-only HTML snapshot of your capability contract",
|
|
41
|
+
watch: "Watch source files and run suggest automatically on save",
|
|
42
|
+
ci: "CI-native check: GitHub Actions annotations, GitLab code quality, exit codes",
|
|
38
43
|
};
|
|
39
44
|
|
|
40
45
|
const COMMAND_HANDLERS = {
|
|
@@ -63,6 +68,11 @@ const COMMAND_HANDLERS = {
|
|
|
63
68
|
"pr-comment": async (args) => (await import("../lib/commands/prComment.mjs")).prCommentCommand(args),
|
|
64
69
|
dashboard: async (args) => (await import("../lib/commands/dashboard.mjs")).dashboardCommand(args),
|
|
65
70
|
"team-sync": async (args) => (await import("../lib/commands/teamSync.mjs")).teamSyncCommand(args),
|
|
71
|
+
onboard: async (args) => (await import("../lib/commands/onboard.mjs")).onboardCommand(args),
|
|
72
|
+
cloud: async (args) => (await import("../lib/commands/cloud.mjs")).cloudCommand(args),
|
|
73
|
+
share: async (args) => (await import("../lib/commands/share.mjs")).shareCommand(args),
|
|
74
|
+
watch: async (args) => (await import("../lib/commands/watch.mjs")).watchCommand(args),
|
|
75
|
+
ci: async (args) => (await import("../lib/commands/ci.mjs")).ciCommand(args),
|
|
66
76
|
};
|
|
67
77
|
|
|
68
78
|
function formatCommandsHelp() {
|
|
@@ -92,7 +102,9 @@ ${formatCommandsHelp()}
|
|
|
92
102
|
update Draft ## Unreleased from commits (default sub-command)
|
|
93
103
|
show Print the current ## Unreleased block
|
|
94
104
|
list List commits since last tag
|
|
105
|
+
ai Generate human-readable changelog with AI (Anthropic or Ollama)
|
|
95
106
|
--ref <tag|commit> Use a specific ref instead of last tag
|
|
107
|
+
--version <x.y.z> Version label for the AI-generated entry
|
|
96
108
|
--dry-run Print what would be written without modifying file
|
|
97
109
|
--append Append to existing ## Unreleased instead of replacing
|
|
98
110
|
--json Machine-readable output
|
|
@@ -181,6 +193,38 @@ ${formatCommandsHelp()}
|
|
|
181
193
|
--dry-run Print the comment without posting it
|
|
182
194
|
--json Machine-readable output
|
|
183
195
|
|
|
196
|
+
${bold("cloud sub-commands:")}
|
|
197
|
+
init Generate a project token and configure cloud sync
|
|
198
|
+
push Upload local capability contract to cloud
|
|
199
|
+
pull Download latest contract from cloud (conflict detection)
|
|
200
|
+
status Compare local vs cloud (hashes, capability counts)
|
|
201
|
+
dashboard Print hosted dashboard URL and open in browser
|
|
202
|
+
|
|
203
|
+
${bold("cloud options:")}
|
|
204
|
+
--token <tok> Override token (or set INFERNOFLOW_TOKEN env var)
|
|
205
|
+
--endpoint <url> Override default endpoint (https://cloud.infernoflow.dev)
|
|
206
|
+
--force, -f Overwrite on init; overwrite local on conflicted pull
|
|
207
|
+
--dry-run Print what would happen without sending
|
|
208
|
+
--json Machine-readable output
|
|
209
|
+
|
|
210
|
+
${bold("share options:")}
|
|
211
|
+
--upload Upload to dpaste.com and print a public URL
|
|
212
|
+
--open Open the snapshot in your browser immediately
|
|
213
|
+
--copy Copy HTML to clipboard
|
|
214
|
+
--out <path> Custom output path (default: inferno/share.html)
|
|
215
|
+
--json Machine-readable: { ok, file, url }
|
|
216
|
+
|
|
217
|
+
${bold("watch options:")}
|
|
218
|
+
[dirs...] Directories to watch (default: src/, lib/, app/)
|
|
219
|
+
--interval <secs> Debounce interval in seconds (default: 3)
|
|
220
|
+
--dry-run Print what would run without executing
|
|
221
|
+
--silent No output (for git hook use)
|
|
222
|
+
|
|
223
|
+
${bold("ci options:")}
|
|
224
|
+
--platform <name> github | gitlab | bitbucket | generic (auto-detected)
|
|
225
|
+
--fail-on <level> error | warning (default: error)
|
|
226
|
+
--json Machine-readable result + exit code
|
|
227
|
+
|
|
184
228
|
${bold("Machine output:")}
|
|
185
229
|
${gray("status --json")}
|
|
186
230
|
${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
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow ci
|
|
3
|
+
*
|
|
4
|
+
* Auto-detect the CI environment and output structured annotations that
|
|
5
|
+
* integrate natively with each platform's UI.
|
|
6
|
+
*
|
|
7
|
+
* Supported platforms:
|
|
8
|
+
* GitHub Actions → ::error:: / ::warning:: / step summary (GITHUB_STEP_SUMMARY)
|
|
9
|
+
* GitLab CI → gl-code-quality.json artifact
|
|
10
|
+
* Bitbucket → annotations API via curl
|
|
11
|
+
* Generic → exit code + JSON output (for any CI)
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* infernoflow ci Auto-detect platform, run check + diff
|
|
15
|
+
* infernoflow ci --platform github Force a platform
|
|
16
|
+
* infernoflow ci --fail-on warning Fail on warning or higher (default: error)
|
|
17
|
+
* infernoflow ci --json Machine-readable result
|
|
18
|
+
*
|
|
19
|
+
* Exits 0 on success, 1 on error/warning (based on --fail-on).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { header, ok, warn, info, done, bold, cyan, gray, green, red, yellow } from "../ui/output.mjs";
|
|
26
|
+
|
|
27
|
+
// ── Platform detection ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function detectPlatform() {
|
|
30
|
+
if (process.env.GITHUB_ACTIONS === "true") return "github";
|
|
31
|
+
if (process.env.GITLAB_CI === "true") return "gitlab";
|
|
32
|
+
if (process.env.BITBUCKET_BUILD_NUMBER) return "bitbucket";
|
|
33
|
+
if (process.env.CIRCLECI === "true") return "circleci";
|
|
34
|
+
if (process.env.JENKINS_URL) return "jenkins";
|
|
35
|
+
if (process.env.CI === "true") return "generic";
|
|
36
|
+
return "local";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── CLI runner ────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function runJson(command, cwd) {
|
|
42
|
+
try {
|
|
43
|
+
const [bin, ...args] = command.split(" ");
|
|
44
|
+
const result = spawnSync(process.execPath, [
|
|
45
|
+
path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), "..", "bin", "infernoflow.mjs"),
|
|
46
|
+
...command.split(" ").slice(1)
|
|
47
|
+
], { cwd, encoding: "utf8", timeout: 30_000 });
|
|
48
|
+
const out = result.stdout?.trim();
|
|
49
|
+
if (out) return JSON.parse(out);
|
|
50
|
+
} catch {}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runCli(args, cwd) {
|
|
55
|
+
try {
|
|
56
|
+
const result = spawnSync(process.execPath, [
|
|
57
|
+
path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), "..", "bin", "infernoflow.mjs"),
|
|
58
|
+
...args
|
|
59
|
+
], { cwd, encoding: "utf8", timeout: 30_000 });
|
|
60
|
+
return result.stdout?.trim() || "";
|
|
61
|
+
} catch { return ""; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── GitHub Actions output ─────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function emitGithub(checkResult, diffResult, failOn) {
|
|
67
|
+
const status = checkResult?.status || "unknown";
|
|
68
|
+
const issues = checkResult?.issues || [];
|
|
69
|
+
const caps = checkResult?.capabilities || 0;
|
|
70
|
+
const added = diffResult?.added?.length || 0;
|
|
71
|
+
const removed = diffResult?.removed?.length || 0;
|
|
72
|
+
const changed = diffResult?.changed?.length || 0;
|
|
73
|
+
|
|
74
|
+
// GitHub workflow commands
|
|
75
|
+
if (status === "error") {
|
|
76
|
+
issues.filter(i => i.severity === "error").forEach(i => {
|
|
77
|
+
console.log(`::error::infernoflow: ${i.message}`);
|
|
78
|
+
});
|
|
79
|
+
} else if (status === "warning") {
|
|
80
|
+
issues.filter(i => i.severity === "warning").forEach(i => {
|
|
81
|
+
console.log(`::warning::infernoflow: ${i.message}`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (added > 0) console.log(`::notice::infernoflow: ${added} new capability${added !== 1 ? "ies" : "y"} added`);
|
|
86
|
+
if (removed > 0) console.log(`::warning::infernoflow: ${removed} capability${removed !== 1 ? "ies" : "y"} removed`);
|
|
87
|
+
|
|
88
|
+
// Step summary
|
|
89
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
90
|
+
if (summaryPath) {
|
|
91
|
+
const statusIcon = status === "ok" ? "✅" : status === "warning" ? "⚠️" : "❌";
|
|
92
|
+
const lines = [
|
|
93
|
+
`## 🔥 infernoflow CI report`,
|
|
94
|
+
"",
|
|
95
|
+
`${statusIcon} **Status:** ${status.toUpperCase()} · **Capabilities:** ${caps}`,
|
|
96
|
+
"",
|
|
97
|
+
];
|
|
98
|
+
if (added || removed || changed) {
|
|
99
|
+
lines.push("### Capability changes");
|
|
100
|
+
if (added) lines.push(`- ✅ **${added}** added`);
|
|
101
|
+
if (removed) lines.push(`- ❌ **${removed}** removed`);
|
|
102
|
+
if (changed) lines.push(`- 📝 **${changed}** changed`);
|
|
103
|
+
lines.push("");
|
|
104
|
+
}
|
|
105
|
+
if (issues.length) {
|
|
106
|
+
lines.push("### Issues");
|
|
107
|
+
issues.forEach(i => lines.push(`- **${i.severity?.toUpperCase() || "INFO"}**: ${i.message}`));
|
|
108
|
+
lines.push("");
|
|
109
|
+
}
|
|
110
|
+
lines.push("---");
|
|
111
|
+
lines.push("*Generated by [infernoflow](https://github.com/ronmiz/infernoflow)*");
|
|
112
|
+
try { fs.appendFileSync(summaryPath, lines.join("\n") + "\n"); } catch {}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── GitLab code quality report ────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function emitGitlab(checkResult, cwd) {
|
|
119
|
+
const issues = checkResult?.issues || [];
|
|
120
|
+
const report = issues.map((issue, i) => ({
|
|
121
|
+
description: issue.message || "infernoflow issue",
|
|
122
|
+
fingerprint: Buffer.from(`infernoflow-${i}-${issue.message}`).toString("hex").slice(0, 40),
|
|
123
|
+
severity: issue.severity === "error" ? "critical" : "minor",
|
|
124
|
+
location: {
|
|
125
|
+
path: "inferno/contract.json",
|
|
126
|
+
lines: { begin: 1 },
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
const reportPath = path.join(cwd, "gl-code-quality-report.json");
|
|
130
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
131
|
+
console.log(`infernoflow: GitLab code quality report written → gl-code-quality-report.json`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Generic CI output ─────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function emitGeneric(checkResult, diffResult, platform) {
|
|
137
|
+
const status = checkResult?.status || "unknown";
|
|
138
|
+
const caps = checkResult?.capabilities || 0;
|
|
139
|
+
const added = diffResult?.added?.length || 0;
|
|
140
|
+
const removed = diffResult?.removed?.length || 0;
|
|
141
|
+
|
|
142
|
+
console.log(`[infernoflow] platform=${platform} status=${status} capabilities=${caps} added=${added} removed=${removed}`);
|
|
143
|
+
if (checkResult?.issues?.length) {
|
|
144
|
+
checkResult.issues.forEach(i => {
|
|
145
|
+
console.log(`[infernoflow] ${(i.severity || "info").toUpperCase()}: ${i.message}`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export async function ciCommand(rawArgs) {
|
|
153
|
+
const args = rawArgs.slice(1);
|
|
154
|
+
const jsonMode = args.includes("--json");
|
|
155
|
+
const platformArg = args.includes("--platform") ? args[args.indexOf("--platform") + 1] : null;
|
|
156
|
+
const failOn = args.includes("--fail-on") ? args[args.indexOf("--fail-on") + 1] : "error";
|
|
157
|
+
const cwd = process.cwd();
|
|
158
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(infernoDir)) {
|
|
161
|
+
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "inferno/ not found" })); }
|
|
162
|
+
else { console.log("[infernoflow] inferno/ not found — skipping CI check"); }
|
|
163
|
+
process.exit(0); // Don't block CI if infernoflow isn't set up
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const platform = platformArg || detectPlatform();
|
|
167
|
+
|
|
168
|
+
if (!jsonMode) {
|
|
169
|
+
console.log(`[infernoflow] running CI check (platform: ${platform})`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Run check + diff
|
|
173
|
+
const checkResult = runJson("check --json", cwd);
|
|
174
|
+
const diffResult = runJson("diff --json", cwd);
|
|
175
|
+
const status = checkResult?.status || "unknown";
|
|
176
|
+
|
|
177
|
+
// Platform-specific output
|
|
178
|
+
switch (platform) {
|
|
179
|
+
case "github":
|
|
180
|
+
emitGithub(checkResult, diffResult, failOn);
|
|
181
|
+
break;
|
|
182
|
+
case "gitlab":
|
|
183
|
+
emitGitlab(checkResult, cwd);
|
|
184
|
+
emitGeneric(checkResult, diffResult, platform);
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
emitGeneric(checkResult, diffResult, platform);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (jsonMode) {
|
|
191
|
+
console.log(JSON.stringify({
|
|
192
|
+
ok: status === "ok" || status === "warning",
|
|
193
|
+
platform,
|
|
194
|
+
status,
|
|
195
|
+
capabilities: checkResult?.capabilities || 0,
|
|
196
|
+
issues: checkResult?.issues || [],
|
|
197
|
+
diff: { added: diffResult?.added || [], removed: diffResult?.removed || [], changed: diffResult?.changed || [] },
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Exit code
|
|
202
|
+
const shouldFail = failOn === "warning"
|
|
203
|
+
? (status === "error" || status === "warning")
|
|
204
|
+
: (status === "error");
|
|
205
|
+
|
|
206
|
+
process.exit(shouldFail ? 1 : 0);
|
|
207
|
+
}
|