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.
@@ -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 = args.includes("--dry-run");
315
- const append = args.includes("--append");
316
- const asJson = args.includes("--json");
556
+ const dryRun = args.includes("--dry-run");
557
+ const append = args.includes("--append");
558
+ const asJson = args.includes("--json");
317
559
 
318
- const refIdx = args.indexOf("--ref");
319
- const ref = refIdx !== -1 ? args[refIdx + 1] : null;
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
- fail(`Unknown sub-command: ${sub}`, "Use: update | show | list");
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
+ }