infernoflow 0.29.0 → 0.31.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 +27 -0
- package/dist/lib/commands/ai.mjs +370 -0
- package/dist/lib/commands/changelog.mjs +5 -1
- package/dist/lib/commands/demo.mjs +569 -0
- package/dist/lib/commands/explain.mjs +2 -2
- package/dist/lib/commands/review.mjs +4 -4
- package/dist/lib/commands/test.mjs +363 -0
- package/package.json +1 -1
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -63,6 +63,9 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
63
63
|
impact: "Blast radius analysis — see every cap, scenario, and risk level affected before you change anything",
|
|
64
64
|
scaffold: "Generate a new capability — source skeleton, contract registration, and placeholder scenario in one command",
|
|
65
65
|
explain: "AI narrative about a capability — what it does, why it exists, what's risky, and what to test",
|
|
66
|
+
test: "Run registered scenarios for a capability — auto-generates a smoke harness if no test runner is configured",
|
|
67
|
+
ai: "Manage AI providers — setup, status, test connection (subcommands: setup | status | test | clear)",
|
|
68
|
+
demo: "Interactive walkthrough — scaffolds a sample project and runs the full capability chain end-to-end",
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
const COMMAND_HANDLERS = {
|
|
@@ -119,6 +122,9 @@ const COMMAND_HANDLERS = {
|
|
|
119
122
|
impact: async (args) => (await import("../lib/commands/impact.mjs")).impactCommand(args),
|
|
120
123
|
scaffold: async (args) => (await import("../lib/commands/scaffold.mjs")).scaffoldCommand(args),
|
|
121
124
|
explain: async (args) => (await import("../lib/commands/explain.mjs")).explainCommand(args),
|
|
125
|
+
test: async (args) => (await import("../lib/commands/test.mjs")).testCommand(args),
|
|
126
|
+
ai: async (args) => (await import("../lib/commands/ai.mjs")).aiCommand(args),
|
|
127
|
+
demo: async (args) => (await import("../lib/commands/demo.mjs")).demoCommand(args),
|
|
122
128
|
};
|
|
123
129
|
|
|
124
130
|
function formatCommandsHelp() {
|
|
@@ -422,6 +428,27 @@ ${formatCommandsHelp()}
|
|
|
422
428
|
--dry-run Print the AI prompt only — no API call made
|
|
423
429
|
--json Machine-readable output (narrative, stability, scenarios)
|
|
424
430
|
|
|
431
|
+
${bold("test options:")}
|
|
432
|
+
infernoflow test Run all caps that have registered scenarios
|
|
433
|
+
infernoflow test <cap-id> Run scenarios for a specific capability
|
|
434
|
+
infernoflow test --all Run every capability (including those without scenarios)
|
|
435
|
+
--generate Print generated ad-hoc test file without running
|
|
436
|
+
--bail Stop on first failure
|
|
437
|
+
--verbose, -v Show runner output for each scenario
|
|
438
|
+
--json Machine-readable output (passed/failed/skipped counts)
|
|
439
|
+
|
|
440
|
+
${bold("ai options:")}
|
|
441
|
+
infernoflow ai setup Interactive wizard — pick provider, enter API key, verify
|
|
442
|
+
infernoflow ai status Show all providers and which are configured
|
|
443
|
+
infernoflow ai test [provider] Send a test prompt and verify the connection
|
|
444
|
+
infernoflow ai clear <provider> Remove a provider's config from integrations.json
|
|
445
|
+
Supported providers: anthropic openai gemini openrouter ollama
|
|
446
|
+
|
|
447
|
+
${bold("demo options:")}
|
|
448
|
+
infernoflow demo Full interactive walkthrough (sample e-commerce project)
|
|
449
|
+
infernoflow demo --fast Skip pauses — good for CI or screen recording
|
|
450
|
+
infernoflow demo --no-cleanup Keep the temp demo project after the run
|
|
451
|
+
|
|
425
452
|
${bold("Machine output:")}
|
|
426
453
|
${gray("status --json")}
|
|
427
454
|
${gray("check --json")}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow ai
|
|
3
|
+
*
|
|
4
|
+
* Manage AI provider configuration for infernoflow commands
|
|
5
|
+
* (explain, why, review, changelog, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
* infernoflow ai setup Interactive guided setup
|
|
9
|
+
* infernoflow ai status Show configured providers and which is active
|
|
10
|
+
* infernoflow ai test [provider] Send a test prompt and show response
|
|
11
|
+
* infernoflow ai clear [provider] Remove a provider's API key from config
|
|
12
|
+
*
|
|
13
|
+
* Config is stored in inferno/integrations.json (project-scoped).
|
|
14
|
+
* API keys can also come from environment variables (checked first).
|
|
15
|
+
*
|
|
16
|
+
* Supported providers:
|
|
17
|
+
* anthropic ANTHROPIC_API_KEY claude-sonnet-4-6
|
|
18
|
+
* openai OPENAI_API_KEY gpt-4o
|
|
19
|
+
* gemini GOOGLE_AI_API_KEY gemini-2.0-flash
|
|
20
|
+
* openrouter OPENROUTER_API_KEY (any model)
|
|
21
|
+
* ollama (local, no key) llama3.2
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from "node:fs";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
import * as https from "node:https";
|
|
27
|
+
import * as http from "node:http";
|
|
28
|
+
import * as readline from "node:readline";
|
|
29
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
30
|
+
|
|
31
|
+
// ── config helpers ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function infernoDir(cwd) { return path.join(cwd, "inferno"); }
|
|
34
|
+
|
|
35
|
+
function loadConfig(cwd) {
|
|
36
|
+
const p = path.join(infernoDir(cwd), "integrations.json");
|
|
37
|
+
if (!fs.existsSync(p)) return {};
|
|
38
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return {}; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function saveConfig(cwd, config) {
|
|
42
|
+
const dir = infernoDir(cwd);
|
|
43
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(dir, "integrations.json"), JSON.stringify(config, null, 2) + "\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── provider definitions ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const PROVIDERS = [
|
|
50
|
+
{
|
|
51
|
+
id: "anthropic",
|
|
52
|
+
name: "Anthropic (Claude)",
|
|
53
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
54
|
+
models: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"],
|
|
55
|
+
default: "claude-sonnet-4-6",
|
|
56
|
+
keyHint: "sk-ant-api03-…",
|
|
57
|
+
docsUrl: "https://console.anthropic.com/settings/keys",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "openai",
|
|
61
|
+
name: "OpenAI (GPT)",
|
|
62
|
+
envKey: "OPENAI_API_KEY",
|
|
63
|
+
models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
|
|
64
|
+
default: "gpt-4o",
|
|
65
|
+
keyHint: "sk-…",
|
|
66
|
+
docsUrl: "https://platform.openai.com/api-keys",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "gemini",
|
|
70
|
+
name: "Google Gemini",
|
|
71
|
+
envKey: "GOOGLE_AI_API_KEY",
|
|
72
|
+
models: ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"],
|
|
73
|
+
default: "gemini-2.0-flash",
|
|
74
|
+
keyHint: "AIza…",
|
|
75
|
+
docsUrl: "https://aistudio.google.com/app/apikey",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "openrouter",
|
|
79
|
+
name: "OpenRouter",
|
|
80
|
+
envKey: "OPENROUTER_API_KEY",
|
|
81
|
+
models: ["anthropic/claude-sonnet-4-6", "openai/gpt-4o", "meta-llama/llama-3.1-8b-instruct:free"],
|
|
82
|
+
default: "anthropic/claude-sonnet-4-6",
|
|
83
|
+
keyHint: "sk-or-…",
|
|
84
|
+
docsUrl: "https://openrouter.ai/keys",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "ollama",
|
|
88
|
+
name: "Ollama (local)",
|
|
89
|
+
envKey: null,
|
|
90
|
+
models: ["llama3.2", "mistral", "codellama", "phi3"],
|
|
91
|
+
default: "llama3.2",
|
|
92
|
+
keyHint: null,
|
|
93
|
+
docsUrl: "https://ollama.com",
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// ── HTTP probe ────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function httpGet(url) {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const parsed = new URL(url);
|
|
102
|
+
const lib = parsed.protocol === "https:" ? https : http;
|
|
103
|
+
const req = lib.request({ hostname: parsed.hostname, port: parsed.port || (parsed.protocol === "https:" ? 443 : 80), path: parsed.pathname + (parsed.search || ""), method: "GET", timeout: 5000 }, (res) => {
|
|
104
|
+
let raw = "";
|
|
105
|
+
res.on("data", d => (raw += d));
|
|
106
|
+
res.on("end", () => { try { resolve({ status: res.statusCode, body: JSON.parse(raw) }); } catch { resolve({ status: res.statusCode, body: raw }); } });
|
|
107
|
+
});
|
|
108
|
+
req.on("error", () => resolve(null));
|
|
109
|
+
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
110
|
+
req.end();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── provider status check ─────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
async function checkProviderStatus(providerId, config) {
|
|
117
|
+
const envMap = {
|
|
118
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
119
|
+
openai: process.env.OPENAI_API_KEY,
|
|
120
|
+
gemini: process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY,
|
|
121
|
+
openrouter: process.env.OPENROUTER_API_KEY,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const fromEnv = envMap[providerId];
|
|
125
|
+
const fromConfig = config[providerId]?.apiKey;
|
|
126
|
+
const key = fromEnv || fromConfig;
|
|
127
|
+
const source = fromEnv ? "env" : fromConfig ? "integrations.json" : null;
|
|
128
|
+
const model = config[providerId]?.model || PROVIDERS.find(p => p.id === providerId)?.default;
|
|
129
|
+
|
|
130
|
+
if (providerId === "ollama") {
|
|
131
|
+
// Check if Ollama is running
|
|
132
|
+
const probe = await httpGet("http://localhost:11434/api/tags").catch(() => null);
|
|
133
|
+
if (probe?.status === 200) {
|
|
134
|
+
const models = probe.body?.models?.map(m => m.name) || [];
|
|
135
|
+
return { configured: true, source: "local", model: config.ollama?.model || "llama3.2", available: true, models };
|
|
136
|
+
}
|
|
137
|
+
return { configured: false, source: null, model: null, available: false };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { configured: !!key, source, model, available: null, masked: key ? key.slice(0, 8) + "…" : null };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── ai test prompt ────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async function testProvider(providerId, config, cwd) {
|
|
146
|
+
try {
|
|
147
|
+
const { callAI } = await import("../ai/providerRouter.mjs");
|
|
148
|
+
const testPrompt = `Reply with exactly: "infernoflow AI test OK — ${providerId}"`;
|
|
149
|
+
const result = await callAI(testPrompt, cwd, providerId);
|
|
150
|
+
return result;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── readline helper ───────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
function prompt(rl, question) {
|
|
159
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── subcommands ───────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
async function cmdStatus(cwd) {
|
|
165
|
+
const config = loadConfig(cwd);
|
|
166
|
+
|
|
167
|
+
console.log();
|
|
168
|
+
console.log(` ${bold("infernoflow ai")} ${gray("— provider status")}`);
|
|
169
|
+
console.log();
|
|
170
|
+
|
|
171
|
+
let anyConfigured = false;
|
|
172
|
+
|
|
173
|
+
for (const p of PROVIDERS) {
|
|
174
|
+
const status = await checkProviderStatus(p.id, config);
|
|
175
|
+
if (status.configured) anyConfigured = true;
|
|
176
|
+
|
|
177
|
+
const icon = status.configured ? green("✓") : gray("○");
|
|
178
|
+
const label = bold(p.name.padEnd(22));
|
|
179
|
+
const info = status.configured
|
|
180
|
+
? `${green("configured")} ${gray(status.source)} ${gray("model: " + status.model)}${status.masked ? " " + gray(status.masked) : ""}`
|
|
181
|
+
: gray("not configured");
|
|
182
|
+
console.log(` ${icon} ${label} ${info}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log();
|
|
186
|
+
|
|
187
|
+
if (!anyConfigured) {
|
|
188
|
+
console.log(` ${yellow("No AI providers configured.")} Run: ${cyan("infernoflow ai setup")}`);
|
|
189
|
+
console.log(` ${gray("Without a provider, explain/why/review use structural fallbacks.")}`);
|
|
190
|
+
} else {
|
|
191
|
+
console.log(` ${gray("Run")} ${cyan("infernoflow ai test")} ${gray("to verify the active provider.")}`);
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function cmdSetup(cwd) {
|
|
197
|
+
const config = loadConfig(cwd);
|
|
198
|
+
|
|
199
|
+
console.log();
|
|
200
|
+
console.log(` ${bold("🔥 infernoflow ai setup")}`);
|
|
201
|
+
console.log(` ${gray("Connect an AI provider for explain, why, review, and changelog.")}`);
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(` Providers: ${PROVIDERS.map(p => bold(p.id)).join(" ")}`);
|
|
204
|
+
console.log();
|
|
205
|
+
|
|
206
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Pick provider
|
|
210
|
+
const providerInput = await prompt(rl, ` Provider [anthropic]: `);
|
|
211
|
+
const providerId = providerInput.trim().toLowerCase() || "anthropic";
|
|
212
|
+
const provider = PROVIDERS.find(p => p.id === providerId);
|
|
213
|
+
|
|
214
|
+
if (!provider) {
|
|
215
|
+
console.log(red(` Unknown provider "${providerId}". Options: ${PROVIDERS.map(p => p.id).join(", ")}`));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log();
|
|
220
|
+
console.log(` ${bold(provider.name)}`);
|
|
221
|
+
|
|
222
|
+
if (provider.docsUrl) {
|
|
223
|
+
console.log(` ${gray("Get API key:")} ${cyan(provider.docsUrl)}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (providerId === "ollama") {
|
|
227
|
+
// Ollama — no key, just model + host
|
|
228
|
+
const hostInput = await prompt(rl, ` Ollama host [http://localhost:11434]: `);
|
|
229
|
+
const modelInput = await prompt(rl, ` Model [${provider.default}]: `);
|
|
230
|
+
|
|
231
|
+
config.ollama = {
|
|
232
|
+
host: hostInput.trim() || "http://localhost:11434",
|
|
233
|
+
model: modelInput.trim() || provider.default,
|
|
234
|
+
};
|
|
235
|
+
saveConfig(cwd, config);
|
|
236
|
+
|
|
237
|
+
console.log();
|
|
238
|
+
console.log(` ${green("✓")} Ollama configured. Testing connection…`);
|
|
239
|
+
const probe = await httpGet(`${config.ollama.host}/api/tags`).catch(() => null);
|
|
240
|
+
if (probe?.status === 200) {
|
|
241
|
+
console.log(` ${green("✓")} Ollama is running. You're all set.`);
|
|
242
|
+
} else {
|
|
243
|
+
console.log(` ${yellow("⚠")} Could not reach Ollama. Make sure it's running: ${cyan("ollama serve")}`);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
// API key provider
|
|
247
|
+
const existingKey = process.env[provider.envKey] || config[providerId]?.apiKey;
|
|
248
|
+
const keyHint = existingKey ? `[${existingKey.slice(0, 8)}… (existing)] ` : `[${provider.keyHint}] `;
|
|
249
|
+
const keyInput = await prompt(rl, ` API key ${keyHint}: `);
|
|
250
|
+
const apiKey = keyInput.trim() || existingKey;
|
|
251
|
+
|
|
252
|
+
if (!apiKey) {
|
|
253
|
+
console.log(red(" No API key provided. Exiting."));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const modelInput = await prompt(rl, ` Model [${provider.default}]: `);
|
|
258
|
+
const model = modelInput.trim() || provider.default;
|
|
259
|
+
|
|
260
|
+
config[providerId] = { apiKey, model };
|
|
261
|
+
saveConfig(cwd, config);
|
|
262
|
+
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(` ${green("✓")} Saved to inferno/integrations.json`);
|
|
265
|
+
console.log();
|
|
266
|
+
process.stdout.write(` Testing connection… `);
|
|
267
|
+
|
|
268
|
+
const result = await testProvider(providerId, config, cwd);
|
|
269
|
+
if (result?.text) {
|
|
270
|
+
console.log(green("OK"));
|
|
271
|
+
console.log(` ${gray(result.text.trim().slice(0, 80))}`);
|
|
272
|
+
} else {
|
|
273
|
+
console.log(yellow("no response"));
|
|
274
|
+
console.log(` ${yellow("⚠")} Could not get a test response. Check your API key.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log();
|
|
279
|
+
console.log(` ${green("✓")} ${bold(provider.name)} is ready.`);
|
|
280
|
+
console.log(` ${gray("Commands that now use AI:")} explain why review changelog`);
|
|
281
|
+
console.log();
|
|
282
|
+
|
|
283
|
+
// gitignore reminder
|
|
284
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
285
|
+
if (fs.existsSync(gitignorePath)) {
|
|
286
|
+
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
287
|
+
if (!content.includes("integrations.json")) {
|
|
288
|
+
console.log(` ${yellow("⚠")} Your API key is in inferno/integrations.json.`);
|
|
289
|
+
console.log(` ${gray("Add")} ${cyan("inferno/integrations.json")} ${gray("to .gitignore to avoid committing it.")}`);
|
|
290
|
+
console.log();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
} finally {
|
|
295
|
+
rl.close();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function cmdTest(args, cwd) {
|
|
300
|
+
const config = loadConfig(cwd);
|
|
301
|
+
const providerId = args.find(a => !a.startsWith("--")) || null;
|
|
302
|
+
|
|
303
|
+
console.log();
|
|
304
|
+
console.log(` ${bold("infernoflow ai test")}`);
|
|
305
|
+
console.log();
|
|
306
|
+
|
|
307
|
+
const toTest = providerId
|
|
308
|
+
? PROVIDERS.filter(p => p.id === providerId)
|
|
309
|
+
: PROVIDERS;
|
|
310
|
+
|
|
311
|
+
for (const p of toTest) {
|
|
312
|
+
const status = await checkProviderStatus(p.id, config);
|
|
313
|
+
if (!status.configured) {
|
|
314
|
+
console.log(` ${gray("○")} ${bold(p.name.padEnd(22))} ${gray("not configured — skipping")}`);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
process.stdout.write(` ${yellow("…")} ${bold(p.name.padEnd(22))} testing… `);
|
|
319
|
+
const result = await testProvider(p.id, config, cwd);
|
|
320
|
+
if (result?.text) {
|
|
321
|
+
console.log(green("OK") + gray(` (${result.model || p.id})`));
|
|
322
|
+
console.log(` ${gray(result.text.trim().slice(0, 80))}`);
|
|
323
|
+
} else {
|
|
324
|
+
console.log(red("FAIL"));
|
|
325
|
+
console.log(` ${red("No response — check API key or model name")}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function cmdClear(args, cwd) {
|
|
333
|
+
const config = loadConfig(cwd);
|
|
334
|
+
const providerId = args.find(a => !a.startsWith("--"));
|
|
335
|
+
|
|
336
|
+
if (!providerId) {
|
|
337
|
+
console.error(red("✗ Usage: infernoflow ai clear <provider>"));
|
|
338
|
+
console.error(gray(" Example: infernoflow ai clear openai"));
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!config[providerId]) {
|
|
343
|
+
console.log(gray(` No config found for "${providerId}"`));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
delete config[providerId];
|
|
348
|
+
saveConfig(cwd, config);
|
|
349
|
+
console.log(green(` ✓ Cleared config for ${providerId}`));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── entry point ───────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
export async function aiCommand(rawArgs) {
|
|
355
|
+
const args = (rawArgs || []).slice(1);
|
|
356
|
+
const sub = args.find(a => !a.startsWith("--")) || "status";
|
|
357
|
+
const subArgs = args.filter(a => a !== sub);
|
|
358
|
+
const cwd = process.cwd();
|
|
359
|
+
|
|
360
|
+
switch (sub) {
|
|
361
|
+
case "setup": return cmdSetup(cwd);
|
|
362
|
+
case "status": return cmdStatus(cwd);
|
|
363
|
+
case "test": return cmdTest(subArgs, cwd);
|
|
364
|
+
case "clear": return cmdClear(subArgs, cwd);
|
|
365
|
+
default:
|
|
366
|
+
console.error(red(`✗ Unknown subcommand: "${sub}"`));
|
|
367
|
+
console.error(gray(" Usage: infernoflow ai <setup|status|test|clear>"));
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -525,7 +525,11 @@ async function subcmdAi(cwd, changelogPath, opts) {
|
|
|
525
525
|
aiText.split("\n").forEach(l => console.log(" " + l));
|
|
526
526
|
console.log(gray(" ──────────────────────────────────────────────────────"));
|
|
527
527
|
console.log();
|
|
528
|
-
|
|
528
|
+
if (provider === "template") {
|
|
529
|
+
console.log(` ${yellow("💡")} ${gray("For AI-written changelogs:")} ${cyan("infernoflow ai setup")}`);
|
|
530
|
+
} else {
|
|
531
|
+
info(`Generated via: ${bold(provider)}`);
|
|
532
|
+
};
|
|
529
533
|
|
|
530
534
|
if (dryRun) {
|
|
531
535
|
warn("Dry run — CHANGELOG.md not modified");
|