infernoflow 0.33.1 → 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 -9
- 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
package/dist/lib/commands/ai.mjs
CHANGED
|
@@ -1,2 +1,407 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
+
// Check which providers already have keys (env vars or saved config)
|
|
200
|
+
const envKeys = {
|
|
201
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
202
|
+
openai: process.env.OPENAI_API_KEY,
|
|
203
|
+
gemini: process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY,
|
|
204
|
+
openrouter: process.env.OPENROUTER_API_KEY,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
console.log();
|
|
208
|
+
console.log(` ${bold("🔥 infernoflow ai setup")}`);
|
|
209
|
+
console.log(` ${gray("Connect an AI provider for explain, why, review, and changelog.")}`);
|
|
210
|
+
console.log();
|
|
211
|
+
|
|
212
|
+
// Numbered menu
|
|
213
|
+
PROVIDERS.forEach((p, i) => {
|
|
214
|
+
const envKey = envKeys[p.id];
|
|
215
|
+
const savedKey = config[p.id]?.apiKey;
|
|
216
|
+
const detected = envKey ? green(" ✓ key detected in environment") :
|
|
217
|
+
savedKey ? green(" ✓ key already saved") : "";
|
|
218
|
+
const num = bold(String(i + 1));
|
|
219
|
+
const local = p.id === "ollama" ? gray(" (local, no key needed)") : "";
|
|
220
|
+
console.log(` ${num}) ${bold(p.name.padEnd(22))}${local}${detected}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
console.log();
|
|
224
|
+
|
|
225
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Numbered selection
|
|
229
|
+
const choice = await prompt(rl, ` Select provider [1]: `);
|
|
230
|
+
const idx = (parseInt(choice.trim()) || 1) - 1;
|
|
231
|
+
|
|
232
|
+
if (idx < 0 || idx >= PROVIDERS.length) {
|
|
233
|
+
console.log(red(` Invalid choice. Enter a number 1–${PROVIDERS.length}.`));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const provider = PROVIDERS[idx];
|
|
238
|
+
const providerId = provider.id;
|
|
239
|
+
|
|
240
|
+
console.log();
|
|
241
|
+
console.log(` ${bold(provider.name)}`);
|
|
242
|
+
|
|
243
|
+
if (providerId === "ollama") {
|
|
244
|
+
// Ollama — no key needed
|
|
245
|
+
const hostInput = await prompt(rl, ` Ollama host [http://localhost:11434]: `);
|
|
246
|
+
const modelInput = await prompt(rl, ` Model [${provider.default}]: `);
|
|
247
|
+
|
|
248
|
+
config.ollama = {
|
|
249
|
+
host: hostInput.trim() || "http://localhost:11434",
|
|
250
|
+
model: modelInput.trim() || provider.default,
|
|
251
|
+
};
|
|
252
|
+
saveConfig(cwd, config);
|
|
253
|
+
|
|
254
|
+
console.log();
|
|
255
|
+
process.stdout.write(` ${green("✓")} Saved. Testing connection… `);
|
|
256
|
+
const probe = await httpGet(`${config.ollama.host}/api/tags`).catch(() => null);
|
|
257
|
+
if (probe?.status === 200) {
|
|
258
|
+
console.log(green("OK"));
|
|
259
|
+
} else {
|
|
260
|
+
console.log(yellow("not reachable"));
|
|
261
|
+
console.log(` ${yellow("⚠")} Start Ollama first: ${cyan("ollama serve")}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
} else {
|
|
265
|
+
// API key provider
|
|
266
|
+
const envKey = envKeys[providerId];
|
|
267
|
+
const savedKey = config[providerId]?.apiKey;
|
|
268
|
+
const existing = envKey || savedKey;
|
|
269
|
+
|
|
270
|
+
if (existing) {
|
|
271
|
+
// Key already detected — confirm or replace
|
|
272
|
+
const source = envKey ? "environment variable" : "saved config";
|
|
273
|
+
console.log(` ${green("✓")} API key detected from ${source}: ${gray(existing.slice(0, 12) + "…")}`);
|
|
274
|
+
const useIt = await prompt(rl, ` Use this key? [Y/n]: `);
|
|
275
|
+
if (useIt.trim().toLowerCase() === "n") {
|
|
276
|
+
console.log();
|
|
277
|
+
if (provider.docsUrl) console.log(` ${gray("Get a key at:")} ${cyan(provider.docsUrl)}`);
|
|
278
|
+
const keyInput = await prompt(rl, ` Paste new API key: `);
|
|
279
|
+
if (!keyInput.trim()) { console.log(red(" No key provided. Exiting.")); return; }
|
|
280
|
+
config[providerId] = { apiKey: keyInput.trim(), model: config[providerId]?.model || provider.default };
|
|
281
|
+
} else {
|
|
282
|
+
// Use existing key — just confirm/update model
|
|
283
|
+
config[providerId] = { apiKey: existing, model: config[providerId]?.model || provider.default };
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
// No key found — ask for it
|
|
287
|
+
console.log(` ${gray("Get your API key at:")} ${cyan(provider.docsUrl)}`);
|
|
288
|
+
console.log(` ${gray("Tip: paste the key below — it starts with")} ${gray(provider.keyHint)}`);
|
|
289
|
+
console.log();
|
|
290
|
+
const keyInput = await prompt(rl, ` Paste API key: `);
|
|
291
|
+
if (!keyInput.trim()) { console.log(red(" No key provided. Exiting.")); return; }
|
|
292
|
+
config[providerId] = { apiKey: keyInput.trim(), model: provider.default };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Model selection (just press Enter to keep default)
|
|
296
|
+
const currentModel = config[providerId].model;
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(` ${gray("Available models:")} ${provider.models.join(" ")}`);
|
|
299
|
+
const modelInput = await prompt(rl, ` Model [${currentModel}]: `);
|
|
300
|
+
config[providerId].model = modelInput.trim() || currentModel;
|
|
301
|
+
|
|
302
|
+
saveConfig(cwd, config);
|
|
303
|
+
|
|
304
|
+
console.log();
|
|
305
|
+
process.stdout.write(` ${green("✓")} Saved. Testing connection… `);
|
|
306
|
+
|
|
307
|
+
const result = await testProvider(providerId, config, cwd);
|
|
308
|
+
if (result?.text) {
|
|
309
|
+
console.log(green("OK") + gray(` (${config[providerId].model})`));
|
|
310
|
+
} else {
|
|
311
|
+
console.log(yellow("no response"));
|
|
312
|
+
console.log(` ${yellow("⚠")} Connection failed — double-check your API key.`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log();
|
|
317
|
+
console.log(` ${green("✓")} ${bold(provider.name)} is ready.`);
|
|
318
|
+
console.log(` ${gray("AI-powered commands:")} explain why review changelog`);
|
|
319
|
+
console.log();
|
|
320
|
+
|
|
321
|
+
// gitignore reminder
|
|
322
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
323
|
+
if (fs.existsSync(gitignorePath)) {
|
|
324
|
+
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
325
|
+
if (!content.includes("integrations.json")) {
|
|
326
|
+
console.log(` ${yellow("⚠")} Add ${cyan("inferno/integrations.json")} to your .gitignore to avoid committing your API key.`);
|
|
327
|
+
console.log();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
} finally {
|
|
332
|
+
rl.close();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function cmdTest(args, cwd) {
|
|
337
|
+
const config = loadConfig(cwd);
|
|
338
|
+
const providerId = args.find(a => !a.startsWith("--")) || null;
|
|
339
|
+
|
|
340
|
+
console.log();
|
|
341
|
+
console.log(` ${bold("infernoflow ai test")}`);
|
|
342
|
+
console.log();
|
|
343
|
+
|
|
344
|
+
const toTest = providerId
|
|
345
|
+
? PROVIDERS.filter(p => p.id === providerId)
|
|
346
|
+
: PROVIDERS;
|
|
347
|
+
|
|
348
|
+
for (const p of toTest) {
|
|
349
|
+
const status = await checkProviderStatus(p.id, config);
|
|
350
|
+
if (!status.configured) {
|
|
351
|
+
console.log(` ${gray("○")} ${bold(p.name.padEnd(22))} ${gray("not configured — skipping")}`);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
process.stdout.write(` ${yellow("…")} ${bold(p.name.padEnd(22))} testing… `);
|
|
356
|
+
const result = await testProvider(p.id, config, cwd);
|
|
357
|
+
if (result?.text) {
|
|
358
|
+
console.log(green("OK") + gray(` (${result.model || p.id})`));
|
|
359
|
+
console.log(` ${gray(result.text.trim().slice(0, 80))}`);
|
|
360
|
+
} else {
|
|
361
|
+
console.log(red("FAIL"));
|
|
362
|
+
console.log(` ${red("No response — check API key or model name")}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function cmdClear(args, cwd) {
|
|
370
|
+
const config = loadConfig(cwd);
|
|
371
|
+
const providerId = args.find(a => !a.startsWith("--"));
|
|
372
|
+
|
|
373
|
+
if (!providerId) {
|
|
374
|
+
console.error(red("✗ Usage: infernoflow ai clear <provider>"));
|
|
375
|
+
console.error(gray(" Example: infernoflow ai clear openai"));
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!config[providerId]) {
|
|
380
|
+
console.log(gray(` No config found for "${providerId}"`));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
delete config[providerId];
|
|
385
|
+
saveConfig(cwd, config);
|
|
386
|
+
console.log(green(` ✓ Cleared config for ${providerId}`));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── entry point ───────────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
export async function aiCommand(rawArgs) {
|
|
392
|
+
const args = (rawArgs || []).slice(1);
|
|
393
|
+
const sub = args.find(a => !a.startsWith("--")) || "status";
|
|
394
|
+
const subArgs = args.filter(a => a !== sub);
|
|
395
|
+
const cwd = process.cwd();
|
|
396
|
+
|
|
397
|
+
switch (sub) {
|
|
398
|
+
case "setup": return cmdSetup(cwd);
|
|
399
|
+
case "status": return cmdStatus(cwd);
|
|
400
|
+
case "test": return cmdTest(subArgs, cwd);
|
|
401
|
+
case "clear": return cmdClear(subArgs, cwd);
|
|
402
|
+
default:
|
|
403
|
+
console.error(red(`✗ Unknown subcommand: "${sub}"`));
|
|
404
|
+
console.error(gray(" Usage: infernoflow ai <setup|status|test|clear>"));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
}
|