aiden-runtime 4.0.0 → 4.0.2
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 +16 -14
- package/dist/cli/v4/aidenCLI.js +92 -9
- package/dist/cli/v4/chatSession.js +16 -0
- package/dist/cli/v4/commands/index.js +4 -1
- package/dist/cli/v4/commands/setup.js +34 -0
- package/dist/cli/v4/display.js +3 -1
- package/dist/cli/v4/setupWizard.js +466 -232
- package/dist/core/permissionSystem.js +2 -2
- package/dist/core/toolRegistry.js +2 -2
- package/dist/core/v4/firstRun/providerDetection.js +287 -0
- package/dist/core/version.js +1 -1
- package/dist/providers/v4/nullAdapter.js +58 -0
- package/package.json +14 -6
|
@@ -42,41 +42,91 @@ const keyValidator_1 = require("./keyValidator");
|
|
|
42
42
|
const providerAuth_1 = require("../../core/v4/auth/providerAuth");
|
|
43
43
|
const loadProvider_1 = require("./auth/loadProvider");
|
|
44
44
|
const box_1 = require("./box");
|
|
45
|
+
// Phase 30.2.1 — provider order optimised for new-user time-to-first-chat.
|
|
46
|
+
// Free providers first (Groq → Gemini → OpenRouter → NVIDIA → Ollama),
|
|
47
|
+
// paid providers next (Anthropic, OpenAI, Together), subscription
|
|
48
|
+
// sign-ins last. The legacy entries (deepseek, mistral, zai, kimi,
|
|
49
|
+
// minimax, huggingface, vercel, nous, custom) trail the top 10 — still
|
|
50
|
+
// pickable for power users, never the default.
|
|
51
|
+
//
|
|
52
|
+
// Plain-English descriptions per spec — "TPM cap" replaced with
|
|
53
|
+
// "limited messages per minute", "tier 1 paid" with "best for complex
|
|
54
|
+
// tasks", etc. Subsequent prompts use `shortLabel` (e.g. just "Groq")
|
|
55
|
+
// to avoid restating the description.
|
|
45
56
|
exports.PROVIDERS = [
|
|
46
|
-
|
|
47
|
-
{ id: 'chatgpt-plus', label: 'Use my ChatGPT Plus subscription', kind: 'pro' },
|
|
57
|
+
// ── Free tier / no-cost ──
|
|
48
58
|
{
|
|
49
|
-
id: '
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
id: 'groq',
|
|
60
|
+
shortLabel: 'Groq',
|
|
61
|
+
label: 'Groq — free, fast, limited messages per minute',
|
|
62
|
+
kind: 'key',
|
|
63
|
+
envVar: 'GROQ_API_KEY',
|
|
64
|
+
keyUrl: 'https://console.groq.com/keys',
|
|
65
|
+
defaultModel: 'llama-3.3-70b-versatile',
|
|
66
|
+
models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'gemini',
|
|
70
|
+
shortLabel: 'Google Gemini',
|
|
71
|
+
label: 'Google Gemini — free, generous limits',
|
|
72
|
+
kind: 'key',
|
|
73
|
+
envVar: 'GEMINI_API_KEY',
|
|
74
|
+
keyUrl: 'https://aistudio.google.com/apikey',
|
|
75
|
+
defaultModel: 'gemini-2.0-flash',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'openrouter',
|
|
79
|
+
shortLabel: 'OpenRouter',
|
|
80
|
+
label: 'OpenRouter — free credits, then paid',
|
|
81
|
+
kind: 'key',
|
|
82
|
+
envVar: 'OPENROUTER_API_KEY',
|
|
83
|
+
keyUrl: 'https://openrouter.ai/keys',
|
|
84
|
+
defaultModel: 'anthropic/claude-opus-4',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'nvidia',
|
|
88
|
+
shortLabel: 'NVIDIA NIM',
|
|
89
|
+
label: 'NVIDIA NIM — free, but rate-limited',
|
|
90
|
+
kind: 'key',
|
|
91
|
+
envVar: 'NVIDIA_API_KEY',
|
|
92
|
+
keyUrl: 'https://build.nvidia.com',
|
|
93
|
+
defaultModel: 'meta/llama-3.3-70b-instruct',
|
|
53
94
|
},
|
|
95
|
+
{
|
|
96
|
+
id: 'ollama',
|
|
97
|
+
shortLabel: 'Ollama',
|
|
98
|
+
label: 'Ollama — fully offline, no key needed (requires Ollama install)',
|
|
99
|
+
kind: 'local',
|
|
100
|
+
defaultModel: 'llama3.1:8b',
|
|
101
|
+
},
|
|
102
|
+
// ── Paid (best quality) ──
|
|
54
103
|
{
|
|
55
104
|
id: 'anthropic',
|
|
56
|
-
|
|
105
|
+
shortLabel: 'Anthropic',
|
|
106
|
+
label: 'Anthropic — paid, best for complex tasks',
|
|
57
107
|
kind: 'key',
|
|
58
108
|
envVar: 'ANTHROPIC_API_KEY',
|
|
109
|
+
keyUrl: 'https://console.anthropic.com/settings/keys',
|
|
59
110
|
defaultModel: 'claude-opus-4-7',
|
|
60
111
|
models: ['claude-opus-4-7', 'claude-sonnet-4-5', 'claude-haiku-4-5'],
|
|
61
112
|
},
|
|
62
113
|
{
|
|
63
114
|
id: 'openai',
|
|
64
|
-
|
|
115
|
+
shortLabel: 'OpenAI',
|
|
116
|
+
label: 'OpenAI — paid, GPT models',
|
|
65
117
|
kind: 'key',
|
|
66
118
|
envVar: 'OPENAI_API_KEY',
|
|
119
|
+
keyUrl: 'https://platform.openai.com/api-keys',
|
|
67
120
|
defaultModel: 'gpt-5',
|
|
68
121
|
models: ['gpt-5', 'gpt-4o', 'gpt-4o-mini'],
|
|
69
122
|
},
|
|
70
123
|
{
|
|
71
124
|
id: 'together',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// signup covers a few hundred turns. Replaces Groq free tier as the
|
|
75
|
-
// first recommendation after the user's Groq slots kept hammering
|
|
76
|
-
// simultaneous 429s within 2 turns of normal use.
|
|
77
|
-
label: 'Together AI (recommended — Qwen3-235B, paid throughput tier)',
|
|
125
|
+
shortLabel: 'Together AI',
|
|
126
|
+
label: 'Together AI — paid, fast & reliable',
|
|
78
127
|
kind: 'key',
|
|
79
128
|
envVar: 'TOGETHER_API_KEY',
|
|
129
|
+
keyUrl: 'https://api.together.xyz/settings/api-keys',
|
|
80
130
|
defaultModel: 'Qwen/Qwen3-235B-A22B-Instruct-2507-tput',
|
|
81
131
|
models: [
|
|
82
132
|
'Qwen/Qwen3-235B-A22B-Instruct-2507-tput',
|
|
@@ -84,74 +134,91 @@ exports.PROVIDERS = [
|
|
|
84
134
|
'deepseek-ai/DeepSeek-V3',
|
|
85
135
|
],
|
|
86
136
|
},
|
|
137
|
+
// ── Subscription sign-ins ──
|
|
87
138
|
{
|
|
88
|
-
id: '
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
defaultModel: 'llama-3.3-70b-versatile',
|
|
93
|
-
models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'],
|
|
139
|
+
id: 'claude-pro',
|
|
140
|
+
shortLabel: 'Claude Pro',
|
|
141
|
+
label: 'Claude Pro — use your existing Claude subscription',
|
|
142
|
+
kind: 'pro',
|
|
94
143
|
},
|
|
95
144
|
{
|
|
96
|
-
id: '
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
defaultModel: 'anthropic/claude-opus-4',
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: 'gemini',
|
|
104
|
-
label: 'Google Gemini (free tier)',
|
|
105
|
-
kind: 'key',
|
|
106
|
-
envVar: 'GEMINI_API_KEY',
|
|
107
|
-
defaultModel: 'gemini-2.0-flash',
|
|
145
|
+
id: 'chatgpt-plus',
|
|
146
|
+
shortLabel: 'ChatGPT Plus',
|
|
147
|
+
label: 'ChatGPT Plus — use your existing ChatGPT subscription',
|
|
148
|
+
kind: 'pro',
|
|
108
149
|
},
|
|
150
|
+
// ── Legacy / power-user entries (kept to preserve env-var detection
|
|
151
|
+
// for users who already configured these out-of-band) ──
|
|
109
152
|
{
|
|
110
153
|
id: 'deepseek',
|
|
111
|
-
|
|
154
|
+
shortLabel: 'DeepSeek',
|
|
155
|
+
label: 'DeepSeek — paid',
|
|
112
156
|
kind: 'key',
|
|
113
157
|
envVar: 'DEEPSEEK_API_KEY',
|
|
114
158
|
defaultModel: 'deepseek-chat',
|
|
115
159
|
},
|
|
116
160
|
{
|
|
117
161
|
id: 'mistral',
|
|
118
|
-
|
|
162
|
+
shortLabel: 'Mistral',
|
|
163
|
+
label: 'Mistral — paid',
|
|
119
164
|
kind: 'key',
|
|
120
165
|
envVar: 'MISTRAL_API_KEY',
|
|
121
166
|
defaultModel: 'mistral-large-latest',
|
|
122
167
|
},
|
|
123
|
-
{
|
|
168
|
+
{
|
|
169
|
+
id: 'zai',
|
|
170
|
+
shortLabel: 'Z.AI',
|
|
171
|
+
label: 'Z.AI / GLM — paid',
|
|
172
|
+
kind: 'key',
|
|
173
|
+
envVar: 'ZAI_API_KEY',
|
|
174
|
+
defaultModel: 'glm-4-plus',
|
|
175
|
+
},
|
|
124
176
|
{
|
|
125
177
|
id: 'kimi',
|
|
126
|
-
|
|
178
|
+
shortLabel: 'Kimi',
|
|
179
|
+
label: 'Kimi / Moonshot — paid',
|
|
127
180
|
kind: 'key',
|
|
128
181
|
envVar: 'MOONSHOT_API_KEY',
|
|
129
182
|
defaultModel: 'moonshot-v1-128k',
|
|
130
183
|
},
|
|
131
|
-
{ id: 'minimax', label: 'MiniMax', kind: 'key', envVar: 'MINIMAX_API_KEY', defaultModel: 'abab6.5s-chat' },
|
|
132
184
|
{
|
|
133
|
-
id: '
|
|
134
|
-
|
|
185
|
+
id: 'minimax',
|
|
186
|
+
shortLabel: 'MiniMax',
|
|
187
|
+
label: 'MiniMax — paid',
|
|
135
188
|
kind: 'key',
|
|
136
|
-
envVar: '
|
|
137
|
-
defaultModel: '
|
|
189
|
+
envVar: 'MINIMAX_API_KEY',
|
|
190
|
+
defaultModel: 'abab6.5s-chat',
|
|
138
191
|
},
|
|
139
192
|
{
|
|
140
193
|
id: 'huggingface',
|
|
141
|
-
|
|
194
|
+
shortLabel: 'Hugging Face',
|
|
195
|
+
label: 'Hugging Face — free tier',
|
|
142
196
|
kind: 'key',
|
|
143
197
|
envVar: 'HF_API_KEY',
|
|
144
198
|
defaultModel: 'meta-llama/Llama-3.3-70B-Instruct',
|
|
145
199
|
},
|
|
146
200
|
{
|
|
147
201
|
id: 'vercel',
|
|
148
|
-
|
|
202
|
+
shortLabel: 'Vercel AI Gateway',
|
|
203
|
+
label: 'Vercel AI Gateway — paid',
|
|
149
204
|
kind: 'key',
|
|
150
205
|
envVar: 'VERCEL_AI_GATEWAY_KEY',
|
|
151
206
|
defaultModel: 'anthropic/claude-opus-4',
|
|
152
207
|
},
|
|
153
|
-
{
|
|
154
|
-
|
|
208
|
+
{
|
|
209
|
+
id: 'nous',
|
|
210
|
+
shortLabel: 'Nous Portal',
|
|
211
|
+
label: 'Nous Portal — subscription',
|
|
212
|
+
kind: 'subscription',
|
|
213
|
+
defaultModel: 'hermes-3-llama-3.1-405b',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'custom',
|
|
217
|
+
shortLabel: 'custom endpoint',
|
|
218
|
+
label: 'Custom OpenAI-compatible endpoint',
|
|
219
|
+
kind: 'custom',
|
|
220
|
+
envVar: 'CUSTOM_API_KEY',
|
|
221
|
+
},
|
|
155
222
|
];
|
|
156
223
|
/** Lazy-load @inquirer/prompts so unit tests don't need a TTY. */
|
|
157
224
|
async function defaultPrompts() {
|
|
@@ -345,6 +412,66 @@ async function probeOllama(opts) {
|
|
|
345
412
|
clearTimeout(timer);
|
|
346
413
|
}
|
|
347
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Phase 30.2.1 — open `url` in the user's default browser. Used by the
|
|
417
|
+
* recovery menu's "Get a key from <provider URL>" branch. Best-effort
|
|
418
|
+
* — failure is non-fatal (we still print the URL so the user can copy
|
|
419
|
+
* it manually).
|
|
420
|
+
*/
|
|
421
|
+
async function openUrlInBrowser(url, display) {
|
|
422
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
423
|
+
const { exec } = require('node:child_process');
|
|
424
|
+
const platform = process.platform;
|
|
425
|
+
let cmd;
|
|
426
|
+
if (platform === 'win32') {
|
|
427
|
+
// `start ""` swallows the URL into the title arg; double-empty-arg
|
|
428
|
+
// form keeps cmd.exe happy and leaves the URL as the actual target.
|
|
429
|
+
cmd = `cmd /c start "" "${url}"`;
|
|
430
|
+
}
|
|
431
|
+
else if (platform === 'darwin') {
|
|
432
|
+
cmd = `open "${url}"`;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
cmd = `xdg-open "${url}"`;
|
|
436
|
+
}
|
|
437
|
+
await new Promise((resolve) => {
|
|
438
|
+
exec(cmd, (err) => {
|
|
439
|
+
if (err) {
|
|
440
|
+
display.write(`${kleur_1.default.dim(`(could not auto-open browser — visit ${url} manually)`)}\n`);
|
|
441
|
+
}
|
|
442
|
+
resolve();
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
async function runRecoveryMenu(provider, prompts, display) {
|
|
447
|
+
const choices = [
|
|
448
|
+
'Try a different provider',
|
|
449
|
+
provider.keyUrl
|
|
450
|
+
? `Get a key from ${provider.keyUrl}`
|
|
451
|
+
: 'Get a key from the provider website (URL printed when picked)',
|
|
452
|
+
'Save without validation (writes config; key untested)',
|
|
453
|
+
'Skip — explore Aiden first (no chat, but / commands work)',
|
|
454
|
+
'Exit (try again later)',
|
|
455
|
+
];
|
|
456
|
+
const idx = await prompts.choose('What would you like to do?', choices,
|
|
457
|
+
/* default= */ 1);
|
|
458
|
+
switch (idx) {
|
|
459
|
+
case 1: return { kind: 'try-different' };
|
|
460
|
+
case 2: {
|
|
461
|
+
// Provider may not carry a keyUrl (legacy entries). Fall through
|
|
462
|
+
// to "try-different" so the user isn't stuck staring at nothing.
|
|
463
|
+
if (!provider.keyUrl) {
|
|
464
|
+
display.write(`${kleur_1.default.dim(`(no key URL on file for ${provider.shortLabel}; pick a different provider.)`)}\n`);
|
|
465
|
+
return { kind: 'try-different' };
|
|
466
|
+
}
|
|
467
|
+
return { kind: 'get-key', url: provider.keyUrl };
|
|
468
|
+
}
|
|
469
|
+
case 3: return { kind: 'save-anyway' };
|
|
470
|
+
case 4: return { kind: 'skip' };
|
|
471
|
+
case 5: return { kind: 'exit' };
|
|
472
|
+
default: return { kind: 'exit' };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
348
475
|
/**
|
|
349
476
|
* Append (or overwrite) an entry in `.env`. Existing entries with the same
|
|
350
477
|
* key are replaced. New entries are appended to the bottom. Keys are
|
|
@@ -381,59 +508,283 @@ async function runSetupWizard(opts = {}) {
|
|
|
381
508
|
const prompts = opts.prompts ?? (await defaultPrompts());
|
|
382
509
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
383
510
|
if (!opts.force && !(await isFreshInstall(paths))) {
|
|
384
|
-
|
|
511
|
+
// Existing config — wizard wasn't needed. Treat as already-configured
|
|
512
|
+
// so the boot path proceeds to resolver (which will surface real
|
|
513
|
+
// credential issues with its own error contract).
|
|
514
|
+
return {
|
|
515
|
+
status: 'configured',
|
|
516
|
+
ran: false,
|
|
517
|
+
skipReason: 'config.yaml already exists; pass force=true to re-run',
|
|
518
|
+
};
|
|
385
519
|
}
|
|
386
520
|
await (0, paths_1.ensureAidenDirsExist)(paths);
|
|
387
521
|
display.printBanner();
|
|
388
522
|
display.write('\nWelcome — let\'s pick a provider.\n');
|
|
389
|
-
display.write(`${kleur_1.default.dim('(Press Enter to accept
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const explainer = PRO_EXPLAINERS[provider.id] ??
|
|
405
|
-
'This connects your subscription via OAuth. No API key needed.';
|
|
406
|
-
display.write(`\n${explainer}\n`);
|
|
407
|
-
// Phase 18.1: honest beta framing per diagnostic 292c7cd. Some
|
|
408
|
-
// upstream errors are account-state-specific and have no client-side
|
|
409
|
-
// fix. We show this every time so a user who hits a wall has clear
|
|
410
|
-
// remediation: rerun setup with an API-key provider.
|
|
411
|
-
display.write('Note: OAuth flows are beta in v4.0. If signin fails, rerun `aiden setup` and pick an API-key provider instead.\n\n');
|
|
412
|
-
const proceed = await prompts.confirm(`Continue with ${provider.label}?`, true);
|
|
413
|
-
if (!proceed) {
|
|
414
|
-
display.write('\nSkipped. Run `aiden setup` again to retry, or pick a different provider.\n');
|
|
415
|
-
return { ran: false, skipReason: 'oauth-skipped' };
|
|
416
|
-
}
|
|
417
|
-
let oauthProvider;
|
|
523
|
+
display.write(`${kleur_1.default.dim('(Press Enter to accept Groq — free + fastest setup.)')}\n\n`);
|
|
524
|
+
// Phase 30.2.1 — Groq is the new recommended default for first-time
|
|
525
|
+
// users: free tier, fastest signup, and avoids the surprise charge
|
|
526
|
+
// path of paid providers. Together AI moved to position [8] paid.
|
|
527
|
+
const groqDefaultIdx = exports.PROVIDERS.findIndex((p) => p.id === 'groq') + 1;
|
|
528
|
+
// outer: provider-pick loop. Recovery option [1] "Try a different
|
|
529
|
+
// provider" jumps back to this prompt without losing progress on
|
|
530
|
+
// global state (display, paths, ensureAidenDirsExist already ran).
|
|
531
|
+
// Try/catch wraps inquirer to convert Ctrl+C ("User force closed
|
|
532
|
+
// the prompt") into the same "skipped" exit state as recovery [4]
|
|
533
|
+
// — the user clearly didn't want to finish, but we still want them
|
|
534
|
+
// to land in REPL "explore mode" rather than crash.
|
|
535
|
+
// eslint-disable-next-line no-constant-condition
|
|
536
|
+
outer: while (true) {
|
|
537
|
+
let providerIndex;
|
|
418
538
|
try {
|
|
419
|
-
|
|
539
|
+
providerIndex = await prompts.choose('Which provider would you like to use?', exports.PROVIDERS.map((p) => p.label), groqDefaultIdx > 0 ? groqDefaultIdx : undefined);
|
|
420
540
|
}
|
|
421
541
|
catch (err) {
|
|
422
|
-
|
|
423
|
-
|
|
542
|
+
const msg = err?.message ?? '';
|
|
543
|
+
if (/force closed|cancel/i.test(msg)) {
|
|
544
|
+
display.write('\nWizard cancelled — entering explore mode.\n');
|
|
545
|
+
return {
|
|
546
|
+
status: 'skipped',
|
|
547
|
+
ran: false,
|
|
548
|
+
skipReason: 'cancelled-at-provider-pick',
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
throw err;
|
|
424
552
|
}
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
553
|
+
const provider = exports.PROVIDERS[providerIndex - 1];
|
|
554
|
+
if (!provider)
|
|
555
|
+
throw new Error(`invalid provider selection: ${providerIndex}`);
|
|
556
|
+
// Phase 18: real OAuth flow for kind: 'pro' providers (claude-pro,
|
|
557
|
+
// chatgpt-plus). The flow is the same one /auth login uses (Task 5);
|
|
558
|
+
// single entry point.
|
|
559
|
+
if (provider.kind === 'pro') {
|
|
560
|
+
// 1-line explainer up-front so the user knows what they're agreeing to.
|
|
561
|
+
const explainer = PRO_EXPLAINERS[provider.id] ??
|
|
562
|
+
'This connects your subscription via OAuth. No API key needed.';
|
|
563
|
+
display.write(`\n${explainer}\n`);
|
|
564
|
+
// Phase 18.1: honest beta framing per diagnostic 292c7cd. Some
|
|
565
|
+
// upstream errors are account-state-specific and have no client-side
|
|
566
|
+
// fix. We show this every time so a user who hits a wall has clear
|
|
567
|
+
// remediation: rerun setup with an API-key provider.
|
|
568
|
+
display.write('Note: OAuth flows are beta in v4.0. If signin fails, rerun `aiden setup` and pick an API-key provider instead.\n\n');
|
|
569
|
+
const proceed = await prompts.confirm(`Continue with ${provider.shortLabel}?`, true);
|
|
570
|
+
if (!proceed) {
|
|
571
|
+
// Phase 30.2.1: don't dead-end. Loop back to provider pick so
|
|
572
|
+
// the user can choose another option without re-launching aiden.
|
|
573
|
+
display.write('\nNo problem — pick another provider.\n');
|
|
574
|
+
continue outer;
|
|
575
|
+
}
|
|
576
|
+
let oauthProvider;
|
|
577
|
+
try {
|
|
578
|
+
oauthProvider = await (0, loadProvider_1.loadOAuthProvider)(provider.id);
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
display.write(display.error(`Could not load OAuth plugin for ${provider.shortLabel}: ${err.message}`));
|
|
582
|
+
// Plugin missing — let the user pick another provider.
|
|
583
|
+
continue outer;
|
|
584
|
+
}
|
|
585
|
+
const ua = wizardUserAgent(prompts, display);
|
|
586
|
+
const runtime = new providerAuth_1.OAuthProviderRuntime(oauthProvider, paths);
|
|
587
|
+
let tokens;
|
|
588
|
+
try {
|
|
589
|
+
tokens = await runtime.login(ua);
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
display.write(display.error(`${provider.shortLabel} sign-in failed: ${err.message}`));
|
|
593
|
+
// OAuth failures are recoverable — loop back so the user can
|
|
594
|
+
// pick an API-key provider as a fallback.
|
|
595
|
+
continue outer;
|
|
596
|
+
}
|
|
597
|
+
// Pick a default model from the registry's known list; user can /model later.
|
|
598
|
+
const modelId = oauthProvider.defaultModels?.[0] ?? '';
|
|
599
|
+
const config = {
|
|
600
|
+
...config_1.DEFAULT_CONFIG,
|
|
601
|
+
model: { provider: provider.id, modelId },
|
|
602
|
+
agent: { ...config_1.DEFAULT_CONFIG.agent, max_turns: config_1.DEFAULT_CONFIG.agent.max_turns },
|
|
603
|
+
display: { ...config_1.DEFAULT_CONFIG.display, skin: 'default' },
|
|
604
|
+
memory: { ...config_1.DEFAULT_CONFIG.memory },
|
|
605
|
+
providers: {
|
|
606
|
+
...(config_1.DEFAULT_CONFIG.providers ?? {}),
|
|
607
|
+
// Marker for /providers + future tooling. The actual bearer
|
|
608
|
+
// lives in tokenStore — config.yaml does NOT carry the secret.
|
|
609
|
+
[provider.id]: { auth: 'oauth' },
|
|
610
|
+
},
|
|
611
|
+
terminal: { backend: 'auto' },
|
|
612
|
+
};
|
|
613
|
+
if (opts.smokeTest) {
|
|
614
|
+
display.write('\n✓ Smoke test complete — would have saved this config:\n');
|
|
615
|
+
display.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
616
|
+
display.write(`(would have saved tokens to ${node_path_1.default.join(paths.root, 'auth', `${provider.id}.json`)})\n`);
|
|
617
|
+
return {
|
|
618
|
+
status: 'configured',
|
|
619
|
+
ran: false,
|
|
620
|
+
skipReason: 'smoke-test',
|
|
621
|
+
config,
|
|
622
|
+
envFile: paths.envFile,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const cm = new config_1.ConfigManager(paths);
|
|
626
|
+
await cm.save(config);
|
|
627
|
+
// Confirmation surface — what's wired now.
|
|
628
|
+
const expIso = new Date(tokens.expiresAtMs).toISOString();
|
|
629
|
+
display.write(`\n✓ ${provider.shortLabel} authed.\n`);
|
|
630
|
+
if (tokens.account)
|
|
631
|
+
display.write(` Account: ${tokens.account}\n`);
|
|
632
|
+
if (oauthProvider.defaultModels?.length) {
|
|
633
|
+
display.write(` Models: ${oauthProvider.defaultModels.join(', ')}\n`);
|
|
634
|
+
}
|
|
635
|
+
display.write(` Tokens stored at: ${node_path_1.default.join(paths.root, 'auth', `${provider.id}.json`)}\n`);
|
|
636
|
+
display.write(` Expires: ${expIso}\n`);
|
|
637
|
+
display.write(`\nTokens encrypted with a machine-derived key. Protects against casual ` +
|
|
638
|
+
`file inspection but NOT against code execution on this machine. ` +
|
|
639
|
+
`Real OS keychain integration in v4.1.\n`);
|
|
640
|
+
printPostWizardTutorial(display, AIDEN_VERSION);
|
|
641
|
+
return { status: 'configured', ran: true, config, envFile: paths.envFile };
|
|
430
642
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
643
|
+
// Step 2: model selection. Phase 30.2.1: subsequent prompts use
|
|
644
|
+
// `shortLabel` instead of the full picker `label` to drop the
|
|
645
|
+
// parenthetical description from the wall of text.
|
|
646
|
+
let modelId = provider.defaultModel ?? '';
|
|
647
|
+
if (provider.models && provider.models.length > 1) {
|
|
648
|
+
const modelIndex = await prompts.choose(`Pick a model for ${provider.shortLabel}`, provider.models);
|
|
649
|
+
modelId = provider.models[modelIndex - 1];
|
|
650
|
+
}
|
|
651
|
+
else if (provider.kind === 'local') {
|
|
652
|
+
modelId = await prompts.input('Ollama model id', { default: provider.defaultModel ?? 'llama3.1:8b' });
|
|
653
|
+
}
|
|
654
|
+
else if (!modelId) {
|
|
655
|
+
modelId = await prompts.input('Model id', { default: '' });
|
|
656
|
+
}
|
|
657
|
+
// Step 3: credentials
|
|
658
|
+
let apiKey;
|
|
659
|
+
let baseUrl;
|
|
660
|
+
if (provider.kind === 'local') {
|
|
661
|
+
const reachable = await probeOllama({ fetchImpl });
|
|
662
|
+
if (!reachable) {
|
|
663
|
+
display.write(display.error('Ollama not reachable on http://localhost:11434', 'install from https://ollama.com, run `ollama serve`, then re-run `aiden setup`.'));
|
|
664
|
+
// Phase 30.2.1: Ollama unreachable is recoverable — loop back
|
|
665
|
+
// so the user can pick a different provider without restart.
|
|
666
|
+
continue outer;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else if (provider.kind === 'custom') {
|
|
670
|
+
baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)');
|
|
671
|
+
apiKey = await prompts.input('API key', { mask: true });
|
|
672
|
+
}
|
|
673
|
+
else if (provider.kind === 'key' || provider.kind === 'subscription') {
|
|
674
|
+
if (provider.envVar) {
|
|
675
|
+
apiKey = await prompts.input(`API key for ${provider.shortLabel}`, { mask: true });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// Step 3.5: validate the API key against the provider endpoint.
|
|
679
|
+
// Bypassed when smokeTest or skipValidation is set, or when there's no key
|
|
680
|
+
// to validate (Ollama, or a subscription provider without an env var).
|
|
681
|
+
const shouldValidate = !opts.smokeTest &&
|
|
682
|
+
!opts.skipValidation &&
|
|
683
|
+
typeof apiKey === 'string' &&
|
|
684
|
+
apiKey.length > 0;
|
|
685
|
+
if (shouldValidate) {
|
|
686
|
+
const validate = opts.validator ?? keyValidator_1.validateProviderKey;
|
|
687
|
+
const maxAttempts = 3;
|
|
688
|
+
let attempt = 1;
|
|
689
|
+
let validated = false;
|
|
690
|
+
let skipValidationForSave = false;
|
|
691
|
+
// Validation loop: at most 3 attempts before falling through to
|
|
692
|
+
// the recovery menu. First attempt uses the already-collected key;
|
|
693
|
+
// subsequent attempts re-prompt for a fresh key (and baseUrl, for
|
|
694
|
+
// custom). This loop labelled `validation` so the recovery flow
|
|
695
|
+
// can `continue validation` to retry with fresh attempts after
|
|
696
|
+
// option [2] "Get a key" opens the browser.
|
|
697
|
+
validation: while (attempt <= maxAttempts) {
|
|
698
|
+
const spinner = display.startSpinner(`Validating ${provider.shortLabel} API key…`);
|
|
699
|
+
let result;
|
|
700
|
+
try {
|
|
701
|
+
result = await validate(provider.id, apiKey, baseUrl, fetchImpl);
|
|
702
|
+
}
|
|
703
|
+
finally {
|
|
704
|
+
spinner.stop();
|
|
705
|
+
}
|
|
706
|
+
if (result.valid) {
|
|
707
|
+
if (result.skipped) {
|
|
708
|
+
display.write(`${kleur_1.default.dim(`Skipped validation: ${result.skipReason ?? 'no validation endpoint'}. The key will be tested on first call.`)}\n`);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
display.write(`${kleur_1.default.green(`✓ ${provider.shortLabel} API key validated`)}\n`);
|
|
712
|
+
}
|
|
713
|
+
validated = true;
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
// Invalid — show error, re-prompt if we have attempts left.
|
|
717
|
+
display.write(display.error(`Validation failed: ${result.reason ?? 'unknown error'}`, attempt < maxAttempts
|
|
718
|
+
? 'Re-enter the key, or press Ctrl+C to exit.'
|
|
719
|
+
: 'Three attempts used.'));
|
|
720
|
+
if (attempt >= maxAttempts) {
|
|
721
|
+
// Phase 30.2.1 — recovery menu replaces the prior dead-end
|
|
722
|
+
// `throw new Error(...)`. Five paths for the user to pick from:
|
|
723
|
+
// [1] try-different → loop back to provider picker
|
|
724
|
+
// [2] get-key (URL) → open browser, fresh 3 attempts
|
|
725
|
+
// [3] save-anyway → write config without validation
|
|
726
|
+
// [4] skip → boot REPL in explore mode
|
|
727
|
+
// [5] exit → clean exit
|
|
728
|
+
const choice = await runRecoveryMenu(provider, prompts, display);
|
|
729
|
+
if (choice.kind === 'try-different') {
|
|
730
|
+
continue outer;
|
|
731
|
+
}
|
|
732
|
+
if (choice.kind === 'get-key') {
|
|
733
|
+
display.write(`\nOpening ${choice.url} in your browser…\n`);
|
|
734
|
+
await openUrlInBrowser(choice.url, display);
|
|
735
|
+
display.write(`${kleur_1.default.dim('Paste the new key when prompted. You have 3 fresh attempts.')}\n`);
|
|
736
|
+
// Reset the attempt counter and re-prompt for a fresh key.
|
|
737
|
+
attempt = 1;
|
|
738
|
+
if (provider.kind === 'custom') {
|
|
739
|
+
baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)', { default: baseUrl });
|
|
740
|
+
apiKey = await prompts.input('API key', { mask: true });
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
apiKey = await prompts.input(`API key for ${provider.shortLabel}`, { mask: true });
|
|
744
|
+
}
|
|
745
|
+
continue validation;
|
|
746
|
+
}
|
|
747
|
+
if (choice.kind === 'save-anyway') {
|
|
748
|
+
display.write(`${kleur_1.default.yellow('Saving without validation. The key will be tested on your first chat.')}\n`);
|
|
749
|
+
skipValidationForSave = true;
|
|
750
|
+
break validation;
|
|
751
|
+
}
|
|
752
|
+
if (choice.kind === 'skip') {
|
|
753
|
+
display.write('\nEntering explore mode — chat is disabled but slash commands work.\n');
|
|
754
|
+
return {
|
|
755
|
+
status: 'skipped',
|
|
756
|
+
ran: false,
|
|
757
|
+
skipReason: 'recovery-explore-mode',
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
// choice.kind === 'exit'
|
|
761
|
+
display.write('\nExited. Run `aiden setup` to try again.\n');
|
|
762
|
+
return { status: 'exited', ran: false, skipReason: 'recovery-exited' };
|
|
763
|
+
}
|
|
764
|
+
// Re-prompt for credentials (only reached when attempt < maxAttempts).
|
|
765
|
+
if (provider.kind === 'custom') {
|
|
766
|
+
baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)', {
|
|
767
|
+
default: baseUrl,
|
|
768
|
+
});
|
|
769
|
+
apiKey = await prompts.input('API key', { mask: true });
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
apiKey = await prompts.input(`API key for ${provider.shortLabel}`, { mask: true });
|
|
773
|
+
}
|
|
774
|
+
attempt += 1;
|
|
775
|
+
}
|
|
776
|
+
// Tag the outer scope so the post-validation save path knows
|
|
777
|
+
// whether to print the "untested" warning. (validated is read
|
|
778
|
+
// by the smoke-test branch below; skipValidationForSave is
|
|
779
|
+
// currently unused outside this block but documented for
|
|
780
|
+
// post-save UX hooks.)
|
|
781
|
+
void validated;
|
|
782
|
+
void skipValidationForSave;
|
|
434
783
|
}
|
|
435
|
-
//
|
|
436
|
-
|
|
784
|
+
// Step 4: terminal backend (basic — keeps wizard in scope for 14a).
|
|
785
|
+
// Default to "auto" — full picker lands in 14b.
|
|
786
|
+
const terminalBackend = 'auto';
|
|
787
|
+
// Step 5: save
|
|
437
788
|
const config = {
|
|
438
789
|
...config_1.DEFAULT_CONFIG,
|
|
439
790
|
model: { provider: provider.id, modelId },
|
|
@@ -442,17 +793,25 @@ async function runSetupWizard(opts = {}) {
|
|
|
442
793
|
memory: { ...config_1.DEFAULT_CONFIG.memory },
|
|
443
794
|
providers: {
|
|
444
795
|
...(config_1.DEFAULT_CONFIG.providers ?? {}),
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
796
|
+
[provider.id]: {
|
|
797
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
798
|
+
...(provider.envVar ? { apiKey: `\${${provider.envVar}}` } : {}),
|
|
799
|
+
},
|
|
448
800
|
},
|
|
449
|
-
terminal: { backend:
|
|
801
|
+
terminal: { backend: terminalBackend },
|
|
450
802
|
};
|
|
451
803
|
if (opts.smokeTest) {
|
|
452
804
|
display.write('\n✓ Smoke test complete — would have saved this config:\n');
|
|
453
805
|
display.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
454
|
-
|
|
806
|
+
if (apiKey && provider.envVar) {
|
|
807
|
+
display.write(`(would have written ${provider.envVar}=*** to ${paths.envFile})\n`);
|
|
808
|
+
}
|
|
809
|
+
if (baseUrl && provider.kind === 'custom') {
|
|
810
|
+
display.write(`(would have written CUSTOM_BASE_URL=${baseUrl} to ${paths.envFile})\n`);
|
|
811
|
+
}
|
|
812
|
+
display.write('(no files written because --smoke-test was passed)\n');
|
|
455
813
|
return {
|
|
814
|
+
status: 'configured',
|
|
456
815
|
ran: false,
|
|
457
816
|
skipReason: 'smoke-test',
|
|
458
817
|
config,
|
|
@@ -461,145 +820,19 @@ async function runSetupWizard(opts = {}) {
|
|
|
461
820
|
}
|
|
462
821
|
const cm = new config_1.ConfigManager(paths);
|
|
463
822
|
await cm.save(config);
|
|
464
|
-
// Confirmation surface — what's wired now.
|
|
465
|
-
const expIso = new Date(tokens.expiresAtMs).toISOString();
|
|
466
|
-
display.write(`\n✓ ${provider.label} authed.\n`);
|
|
467
|
-
if (tokens.account)
|
|
468
|
-
display.write(` Account: ${tokens.account}\n`);
|
|
469
|
-
if (oauthProvider.defaultModels?.length) {
|
|
470
|
-
display.write(` Models: ${oauthProvider.defaultModels.join(', ')}\n`);
|
|
471
|
-
}
|
|
472
|
-
display.write(` Tokens stored at: ${node_path_1.default.join(paths.root, 'auth', `${provider.id}.json`)}\n`);
|
|
473
|
-
display.write(` Expires: ${expIso}\n`);
|
|
474
|
-
display.write(`\nTokens encrypted with a machine-derived key. Protects against casual ` +
|
|
475
|
-
`file inspection but NOT against code execution on this machine. ` +
|
|
476
|
-
`Real OS keychain integration in v4.1.\n`);
|
|
477
|
-
printPostWizardTutorial(display, AIDEN_VERSION);
|
|
478
|
-
return { ran: true, config, envFile: paths.envFile };
|
|
479
|
-
}
|
|
480
|
-
// Step 2: model selection
|
|
481
|
-
let modelId = provider.defaultModel ?? '';
|
|
482
|
-
if (provider.models && provider.models.length > 1) {
|
|
483
|
-
const modelIndex = await prompts.choose(`Pick a model for ${provider.label}`, provider.models);
|
|
484
|
-
modelId = provider.models[modelIndex - 1];
|
|
485
|
-
}
|
|
486
|
-
else if (provider.kind === 'local') {
|
|
487
|
-
modelId = await prompts.input('Ollama model id', { default: provider.defaultModel ?? 'llama3.1:8b' });
|
|
488
|
-
}
|
|
489
|
-
else if (!modelId) {
|
|
490
|
-
modelId = await prompts.input('Model id', { default: '' });
|
|
491
|
-
}
|
|
492
|
-
// Step 3: credentials
|
|
493
|
-
let apiKey;
|
|
494
|
-
let baseUrl;
|
|
495
|
-
if (provider.kind === 'local') {
|
|
496
|
-
const reachable = await probeOllama({ fetchImpl });
|
|
497
|
-
if (!reachable) {
|
|
498
|
-
display.write(display.error('Ollama not reachable on http://localhost:11434', 'install from https://ollama.com, run `ollama serve`, then re-run `aiden setup`.'));
|
|
499
|
-
return { ran: false, skipReason: 'ollama-not-reachable' };
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
else if (provider.kind === 'custom') {
|
|
503
|
-
baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)');
|
|
504
|
-
apiKey = await prompts.input('API key', { mask: true });
|
|
505
|
-
}
|
|
506
|
-
else if (provider.kind === 'key' || provider.kind === 'subscription') {
|
|
507
|
-
if (provider.envVar) {
|
|
508
|
-
apiKey = await prompts.input(`API key for ${provider.label}`, { mask: true });
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
// Step 3.5: validate the API key against the provider endpoint.
|
|
512
|
-
// Bypassed when smokeTest or skipValidation is set, or when there's no key
|
|
513
|
-
// to validate (Ollama, or a subscription provider without an env var).
|
|
514
|
-
const shouldValidate = !opts.smokeTest &&
|
|
515
|
-
!opts.skipValidation &&
|
|
516
|
-
typeof apiKey === 'string' &&
|
|
517
|
-
apiKey.length > 0;
|
|
518
|
-
if (shouldValidate) {
|
|
519
|
-
const validate = opts.validator ?? keyValidator_1.validateProviderKey;
|
|
520
|
-
const maxAttempts = 3;
|
|
521
|
-
let attempt = 1;
|
|
522
|
-
// First attempt uses the key already collected. Subsequent attempts
|
|
523
|
-
// re-prompt for a fresh key (and baseUrl, for custom).
|
|
524
|
-
while (attempt <= maxAttempts) {
|
|
525
|
-
const spinner = display.startSpinner(`Validating ${provider.label} API key…`);
|
|
526
|
-
let result;
|
|
527
|
-
try {
|
|
528
|
-
result = await validate(provider.id, apiKey, baseUrl, fetchImpl);
|
|
529
|
-
}
|
|
530
|
-
finally {
|
|
531
|
-
spinner.stop();
|
|
532
|
-
}
|
|
533
|
-
if (result.valid) {
|
|
534
|
-
if (result.skipped) {
|
|
535
|
-
display.write(`${kleur_1.default.dim(`Skipped validation: ${result.skipReason ?? 'no validation endpoint'}. The key will be tested on first call.`)}\n`);
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
display.write(`${kleur_1.default.green(`✓ ${provider.label} API key validated`)}\n`);
|
|
539
|
-
}
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
// Invalid — show error, re-prompt if we have attempts left.
|
|
543
|
-
display.write(display.error(`Validation failed: ${result.reason ?? 'unknown error'}`, 'Re-enter the key, or press Ctrl+C to exit.'));
|
|
544
|
-
if (attempt >= maxAttempts) {
|
|
545
|
-
throw new Error('Could not validate key after 3 attempts. Run `aiden setup --skip-validation` to bypass.');
|
|
546
|
-
}
|
|
547
|
-
// Re-prompt for credentials.
|
|
548
|
-
if (provider.kind === 'custom') {
|
|
549
|
-
baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)', {
|
|
550
|
-
default: baseUrl,
|
|
551
|
-
});
|
|
552
|
-
apiKey = await prompts.input('API key', { mask: true });
|
|
553
|
-
}
|
|
554
|
-
else {
|
|
555
|
-
apiKey = await prompts.input(`API key for ${provider.label}`, { mask: true });
|
|
556
|
-
}
|
|
557
|
-
attempt += 1;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
// Step 4: terminal backend (basic — keeps wizard in scope for 14a).
|
|
561
|
-
// Default to "auto" — full picker lands in 14b.
|
|
562
|
-
const terminalBackend = 'auto';
|
|
563
|
-
// Step 5: save
|
|
564
|
-
const config = {
|
|
565
|
-
...config_1.DEFAULT_CONFIG,
|
|
566
|
-
model: { provider: provider.id, modelId },
|
|
567
|
-
agent: { ...config_1.DEFAULT_CONFIG.agent, max_turns: config_1.DEFAULT_CONFIG.agent.max_turns },
|
|
568
|
-
display: { ...config_1.DEFAULT_CONFIG.display, skin: 'default' },
|
|
569
|
-
memory: { ...config_1.DEFAULT_CONFIG.memory },
|
|
570
|
-
providers: {
|
|
571
|
-
...(config_1.DEFAULT_CONFIG.providers ?? {}),
|
|
572
|
-
[provider.id]: {
|
|
573
|
-
...(baseUrl ? { baseUrl } : {}),
|
|
574
|
-
...(provider.envVar ? { apiKey: `\${${provider.envVar}}` } : {}),
|
|
575
|
-
},
|
|
576
|
-
},
|
|
577
|
-
terminal: { backend: terminalBackend },
|
|
578
|
-
};
|
|
579
|
-
if (opts.smokeTest) {
|
|
580
|
-
display.write('\n✓ Smoke test complete — would have saved this config:\n');
|
|
581
|
-
display.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
582
823
|
if (apiKey && provider.envVar) {
|
|
583
|
-
|
|
824
|
+
await upsertEnvVar(paths.envFile, provider.envVar, apiKey);
|
|
584
825
|
}
|
|
585
826
|
if (baseUrl && provider.kind === 'custom') {
|
|
586
|
-
|
|
827
|
+
await upsertEnvVar(paths.envFile, 'CUSTOM_BASE_URL', baseUrl);
|
|
587
828
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
if (baseUrl && provider.kind === 'custom') {
|
|
597
|
-
await upsertEnvVar(paths.envFile, 'CUSTOM_BASE_URL', baseUrl);
|
|
598
|
-
}
|
|
599
|
-
// Step 6: tutorial
|
|
600
|
-
display.write(`\n${kleur_1.default.green(`✓ ${provider.label}`)} configured with model ${kleur_1.default.cyan(modelId)}.\n`);
|
|
601
|
-
printPostWizardTutorial(display, AIDEN_VERSION);
|
|
602
|
-
return { ran: true, config, envFile: paths.envFile };
|
|
829
|
+
// Step 6: tutorial
|
|
830
|
+
display.write(`\n${kleur_1.default.green(`✓ ${provider.shortLabel}`)} configured with model ${kleur_1.default.cyan(modelId)}.\n`);
|
|
831
|
+
printPostWizardTutorial(display, AIDEN_VERSION);
|
|
832
|
+
return { status: 'configured', ran: true, config, envFile: paths.envFile };
|
|
833
|
+
} // end of outer: while (true) — every path inside either continues,
|
|
834
|
+
// returns, or breaks. Reaching this `}` is impossible (guarded by
|
|
835
|
+
// the no-constant-condition eslint comment above the outer label).
|
|
603
836
|
}
|
|
604
837
|
// ---------------------------------------------------------------------------
|
|
605
838
|
// Direct invocation: `npx tsx cli/v4/setupWizard.ts [--smoke-test] [--force]`
|
|
@@ -611,10 +844,11 @@ if (require.main === module) {
|
|
|
611
844
|
const skipValidation = argv.includes('--skip-validation');
|
|
612
845
|
runSetupWizard({ smokeTest, force, skipValidation })
|
|
613
846
|
.then((result) => {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
847
|
+
// Phase 30.2.1: status is the new authoritative signal. Direct
|
|
848
|
+
// CLI invocation always exits 0 — the wizard already printed
|
|
849
|
+
// its own outcome lines, and a non-zero exit confuses shell
|
|
850
|
+
// wrappers that pipe the wizard's output into other tools.
|
|
851
|
+
void result.status;
|
|
618
852
|
process.exit(0);
|
|
619
853
|
})
|
|
620
854
|
.catch((err) => {
|