infernoflow 0.32.1 β†’ 0.32.3

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.
@@ -196,35 +196,52 @@ async function cmdStatus(cwd) {
196
196
  async function cmdSetup(cwd) {
197
197
  const config = loadConfig(cwd);
198
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
+
199
207
  console.log();
200
208
  console.log(` ${bold("πŸ”₯ infernoflow ai setup")}`);
201
209
  console.log(` ${gray("Connect an AI provider for explain, why, review, and changelog.")}`);
202
210
  console.log();
203
- console.log(` Providers: ${PROVIDERS.map(p => bold(p.id)).join(" ")}`);
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
+
204
223
  console.log();
205
224
 
206
225
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
207
226
 
208
227
  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);
228
+ // Numbered selection
229
+ const choice = await prompt(rl, ` Select provider [1]: `);
230
+ const idx = (parseInt(choice.trim()) || 1) - 1;
213
231
 
214
- if (!provider) {
215
- console.log(red(` Unknown provider "${providerId}". Options: ${PROVIDERS.map(p => p.id).join(", ")}`));
232
+ if (idx < 0 || idx >= PROVIDERS.length) {
233
+ console.log(red(` Invalid choice. Enter a number 1–${PROVIDERS.length}.`));
216
234
  return;
217
235
  }
218
236
 
237
+ const provider = PROVIDERS[idx];
238
+ const providerId = provider.id;
239
+
219
240
  console.log();
220
241
  console.log(` ${bold(provider.name)}`);
221
242
 
222
- if (provider.docsUrl) {
223
- console.log(` ${gray("Get API key:")} ${cyan(provider.docsUrl)}`);
224
- }
225
-
226
243
  if (providerId === "ollama") {
227
- // Ollama β€” no key, just model + host
244
+ // Ollama β€” no key needed
228
245
  const hostInput = await prompt(rl, ` Ollama host [http://localhost:11434]: `);
229
246
  const modelInput = await prompt(rl, ` Model [${provider.default}]: `);
230
247
 
@@ -235,49 +252,70 @@ async function cmdSetup(cwd) {
235
252
  saveConfig(cwd, config);
236
253
 
237
254
  console.log();
238
- console.log(` ${green("βœ“")} Ollama configured. Testing connection…`);
255
+ process.stdout.write(` ${green("βœ“")} Saved. Testing connection… `);
239
256
  const probe = await httpGet(`${config.ollama.host}/api/tags`).catch(() => null);
240
257
  if (probe?.status === 200) {
241
- console.log(` ${green("βœ“")} Ollama is running. You're all set.`);
258
+ console.log(green("OK"));
242
259
  } else {
243
- console.log(` ${yellow("⚠")} Could not reach Ollama. Make sure it's running: ${cyan("ollama serve")}`);
260
+ console.log(yellow("not reachable"));
261
+ console.log(` ${yellow("⚠")} Start Ollama first: ${cyan("ollama serve")}`);
244
262
  }
263
+
245
264
  } else {
246
265
  // 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;
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 };
255
293
  }
256
294
 
257
- const modelInput = await prompt(rl, ` Model [${provider.default}]: `);
258
- const model = modelInput.trim() || provider.default;
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;
259
301
 
260
- config[providerId] = { apiKey, model };
261
302
  saveConfig(cwd, config);
262
303
 
263
304
  console.log();
264
- console.log(` ${green("βœ“")} Saved to inferno/integrations.json`);
265
- console.log();
266
- process.stdout.write(` Testing connection… `);
305
+ process.stdout.write(` ${green("βœ“")} Saved. Testing connection… `);
267
306
 
268
307
  const result = await testProvider(providerId, config, cwd);
269
308
  if (result?.text) {
270
- console.log(green("OK"));
271
- console.log(` ${gray(result.text.trim().slice(0, 80))}`);
309
+ console.log(green("OK") + gray(` (${config[providerId].model})`));
272
310
  } else {
273
311
  console.log(yellow("no response"));
274
- console.log(` ${yellow("⚠")} Could not get a test response. Check your API key.`);
312
+ console.log(` ${yellow("⚠")} Connection failed β€” double-check your API key.`);
275
313
  }
276
314
  }
277
315
 
278
316
  console.log();
279
317
  console.log(` ${green("βœ“")} ${bold(provider.name)} is ready.`);
280
- console.log(` ${gray("Commands that now use AI:")} explain why review changelog`);
318
+ console.log(` ${gray("AI-powered commands:")} explain why review changelog`);
281
319
  console.log();
282
320
 
283
321
  // gitignore reminder
@@ -285,8 +323,7 @@ async function cmdSetup(cwd) {
285
323
  if (fs.existsSync(gitignorePath)) {
286
324
  const content = fs.readFileSync(gitignorePath, "utf8");
287
325
  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.")}`);
326
+ console.log(` ${yellow("⚠")} Add ${cyan("inferno/integrations.json")} to your .gitignore to avoid committing your API key.`);
290
327
  console.log();
291
328
  }
292
329
  }
@@ -21,6 +21,7 @@
21
21
 
22
22
  import * as fs from "node:fs";
23
23
  import * as path from "node:path";
24
+ import { fileURLToPath } from "node:url";
24
25
  import { spawnSync } from "node:child_process";
25
26
  import { header, ok, warn, info, done, bold, cyan, gray, green, red, yellow } from "../ui/output.mjs";
26
27
 
@@ -42,7 +43,7 @@ function runJson(command, cwd) {
42
43
  try {
43
44
  const [bin, ...args] = command.split(" ");
44
45
  const result = spawnSync(process.execPath, [
45
- path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), "..", "bin", "infernoflow.mjs"),
46
+ path.join(path.dirname(path.dirname(fileURLToPath(import.meta.url))), "..", "bin", "infernoflow.mjs"),
46
47
  ...command.split(" ").slice(1)
47
48
  ], { cwd, encoding: "utf8", timeout: 30_000 });
48
49
  const out = result.stdout?.trim();
@@ -54,7 +55,7 @@ function runJson(command, cwd) {
54
55
  function runCli(args, cwd) {
55
56
  try {
56
57
  const result = spawnSync(process.execPath, [
57
- path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), "..", "bin", "infernoflow.mjs"),
58
+ path.join(path.dirname(path.dirname(fileURLToPath(import.meta.url))), "..", "bin", "infernoflow.mjs"),
58
59
  ...args
59
60
  ], { cwd, encoding: "utf8", timeout: 30_000 });
60
61
  return result.stdout?.trim() || "";
@@ -23,6 +23,7 @@
23
23
  import * as fs from "node:fs";
24
24
  import * as path from "node:path";
25
25
  import * as os from "node:os";
26
+ import { fileURLToPath } from "node:url";
26
27
  import { execSync, spawnSync } from "node:child_process";
27
28
  import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
28
29
 
@@ -355,7 +356,7 @@ export async function demoCommand(rawArgs) {
355
356
 
356
357
  // Find infernoflow bin
357
358
  const ifBin = path.resolve(
358
- path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))),
359
+ path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))),
359
360
  "bin", "infernoflow.mjs"
360
361
  );
361
362
 
@@ -23,6 +23,7 @@
23
23
 
24
24
  import * as fs from "node:fs";
25
25
  import * as path from "node:path";
26
+ import { fileURLToPath } from "node:url";
26
27
  import { spawnSync } from "node:child_process";
27
28
  import { header, ok, warn, info, done, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
28
29
 
@@ -134,7 +135,7 @@ function contractStatus(pkgDir) {
134
135
  // ── CLI runner ────────────────────────────────────────────────────────────────
135
136
 
136
137
  function runInferno(args, cwd) {
137
- const binPath = path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), "..", "bin", "infernoflow.mjs");
138
+ const binPath = path.join(path.dirname(path.dirname(fileURLToPath(import.meta.url))), "..", "bin", "infernoflow.mjs");
138
139
  const result = spawnSync(process.execPath, [binPath, ...args], {
139
140
  cwd,
140
141
  encoding: "utf8",
@@ -50,7 +50,7 @@ function loadNotifyConfig(infernoDir, args) {
50
50
  function runJson(cmd, cwd) {
51
51
  try {
52
52
  const result = spawnSync(process.execPath, [
53
- path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), "..", "bin", "infernoflow.mjs"),
53
+ path.join(path.dirname(path.dirname(fileURLToPath(import.meta.url))), "..", "bin", "infernoflow.mjs"),
54
54
  ...cmd.split(" ").slice(1),
55
55
  ], { cwd, encoding: "utf8", timeout: 20_000 });
56
56
  const out = result.stdout?.trim();
@@ -21,6 +21,7 @@
21
21
 
22
22
  import * as fs from "node:fs";
23
23
  import * as path from "node:path";
24
+ import { fileURLToPath } from "node:url";
24
25
  import { execSync, spawnSync } from "node:child_process";
25
26
  import { ok, warn, info, bold, cyan, gray, green, yellow } from "../ui/output.mjs";
26
27
 
@@ -79,7 +80,7 @@ function runSuggest(changedFiles, cwd, infernoDir, dryRun, silent) {
79
80
 
80
81
  try {
81
82
  spawnSync(process.execPath, [
82
- path.join(path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))), "bin", "infernoflow.mjs"),
83
+ path.join(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), "bin", "infernoflow.mjs"),
83
84
  "suggest", desc, "--json"
84
85
  ], { cwd, encoding: "utf8", timeout: 30_000, stdio: "ignore" });
85
86
 
@@ -91,7 +92,7 @@ function runSuggest(changedFiles, cwd, infernoDir, dryRun, silent) {
91
92
  // Silent check β€” write issues to WATCH.log
92
93
  try {
93
94
  const result = spawnSync(process.execPath, [
94
- path.join(path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))), "bin", "infernoflow.mjs"),
95
+ path.join(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), "bin", "infernoflow.mjs"),
95
96
  "check", "--json"
96
97
  ], { cwd, encoding: "utf8", timeout: 15_000 });
97
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.32.1",
3
+ "version": "0.32.3",
4
4
  "description": "The forge for liquid code - keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {