krasavacode 0.3.6 → 0.4.1
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 +23 -14
- package/bin/krasavacode.js +4 -4
- package/package.json +1 -1
- package/src/cooldowns.js +54 -0
- package/src/doctor.js +19 -0
- package/src/hub.js +7 -5
- package/src/launch.js +19 -22
- package/src/metrics-proxy.js +164 -122
- package/src/preset.js +35 -58
- package/src/providers.js +234 -0
- package/src/setup.js +409 -0
- package/src/setup-gemini.js +0 -427
package/src/setup.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, writeFile, chmod, readFile, access } from 'node:fs/promises';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
import {
|
|
9
|
+
PROVIDERS,
|
|
10
|
+
PROVIDER_PRIORITY,
|
|
11
|
+
KEYS_DIR,
|
|
12
|
+
loadProviderKey,
|
|
13
|
+
isProviderConfigured,
|
|
14
|
+
configuredProviders,
|
|
15
|
+
getProviderEnvVarName,
|
|
16
|
+
providerEnvFile,
|
|
17
|
+
} from './providers.js';
|
|
18
|
+
|
|
19
|
+
const ROOT = join(homedir(), '.krasavacode');
|
|
20
|
+
const STATE_FILE = join(ROOT, 'state.json');
|
|
21
|
+
|
|
22
|
+
function openBrowser(url) {
|
|
23
|
+
const cmd = platform() === 'darwin' ? 'open'
|
|
24
|
+
: platform() === 'win32' ? 'start'
|
|
25
|
+
: 'xdg-open';
|
|
26
|
+
const args = platform() === 'win32' ? ['', url] : [url];
|
|
27
|
+
try {
|
|
28
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore', shell: platform() === 'win32' }).unref();
|
|
29
|
+
return true;
|
|
30
|
+
} catch { return false; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readState() { try { return JSON.parse(await readFile(STATE_FILE, 'utf8')); } catch { return {}; } }
|
|
34
|
+
async function writeState(s) { await writeFile(STATE_FILE, JSON.stringify(s, null, 2)); }
|
|
35
|
+
|
|
36
|
+
function getFreePort() {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const srv = net.createServer();
|
|
39
|
+
srv.unref();
|
|
40
|
+
srv.on('error', reject);
|
|
41
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
42
|
+
const { port } = srv.address();
|
|
43
|
+
srv.close(() => resolve(port));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function persistKey(providerId, key) {
|
|
49
|
+
await mkdir(KEYS_DIR, { recursive: true });
|
|
50
|
+
const file = providerEnvFile(providerId);
|
|
51
|
+
const varName = getProviderEnvVarName(providerId);
|
|
52
|
+
await writeFile(file, `${varName}=${key}\n`);
|
|
53
|
+
try { await chmod(file, 0o600); } catch {}
|
|
54
|
+
const state = await readState();
|
|
55
|
+
state.lastSetupAt = new Date().toISOString();
|
|
56
|
+
state.lastConfiguredProvider = providerId;
|
|
57
|
+
await writeState(state);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJsonBody(req, max = 8 * 1024) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
let total = 0;
|
|
64
|
+
req.on('data', d => {
|
|
65
|
+
total += d.length;
|
|
66
|
+
if (total > max) { req.destroy(); reject(new Error('body too large')); }
|
|
67
|
+
chunks.push(d);
|
|
68
|
+
});
|
|
69
|
+
req.on('end', () => {
|
|
70
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
71
|
+
catch { reject(new Error('bad json')); }
|
|
72
|
+
});
|
|
73
|
+
req.on('error', reject);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function html() {
|
|
78
|
+
// ── HTML страницы ───────────────────────────────────────────────
|
|
79
|
+
// Три таба: Cerebras / Groq / Gemini. Inline CSS, dark/light theme.
|
|
80
|
+
const cards = PROVIDER_PRIORITY.map(id => {
|
|
81
|
+
const p = PROVIDERS[id];
|
|
82
|
+
const steps = p.keyHowto.map((s, i) => `<li>${s}</li>`).join('');
|
|
83
|
+
return `
|
|
84
|
+
<section class="tab-content" data-tab="${id}" hidden>
|
|
85
|
+
<div class="hero">
|
|
86
|
+
<h2>${p.name}</h2>
|
|
87
|
+
<p class="tagline">${p.tagline}</p>
|
|
88
|
+
<ul class="quota">
|
|
89
|
+
<li><b>Квота:</b> ${p.quota}</li>
|
|
90
|
+
<li><b>Лучшая модель:</b> ${p.bestModel}</li>
|
|
91
|
+
<li><b>Контекст:</b> ${p.contextLimit.toLocaleString('ru')} токенов</li>
|
|
92
|
+
</ul>
|
|
93
|
+
</div>
|
|
94
|
+
<ol class="steps">${steps}</ol>
|
|
95
|
+
<div class="open-row">
|
|
96
|
+
<a class="open-btn" href="${p.consoleUrl}" target="_blank" rel="noopener">Открыть страницу регистрации →</a>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="field">
|
|
99
|
+
<label>Вставь ключ ${p.name}:</label>
|
|
100
|
+
<input data-provider="${id}" type="text" autocomplete="off" spellcheck="false" placeholder="${p.keyExample}">
|
|
101
|
+
<button data-action="verify" data-provider="${id}" class="submit">Подключить и проверить</button>
|
|
102
|
+
<div class="msg" data-provider-msg="${id}"></div>
|
|
103
|
+
</div>
|
|
104
|
+
</section>`;
|
|
105
|
+
}).join('\n');
|
|
106
|
+
|
|
107
|
+
const tabs = PROVIDER_PRIORITY.map((id, i) => {
|
|
108
|
+
const p = PROVIDERS[id];
|
|
109
|
+
return `<button class="tab" data-tab-button="${id}"${i === 0 ? ' aria-current="true"' : ''}>${p.name} <span class="tab-status" data-provider-status="${id}"></span></button>`;
|
|
110
|
+
}).join('');
|
|
111
|
+
|
|
112
|
+
return `<!doctype html>
|
|
113
|
+
<html lang="ru">
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="utf-8">
|
|
116
|
+
<title>KRASAVACODE — подключение AI-провайдеров</title>
|
|
117
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
118
|
+
<style>
|
|
119
|
+
* { box-sizing: border-box; }
|
|
120
|
+
body { margin: 0; padding: 32px 16px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
121
|
+
background: linear-gradient(180deg,#f7f7fb 0%,#ecedf3 100%); color:#1d1d1f; min-height:100vh;
|
|
122
|
+
display:flex; align-items:flex-start; justify-content:center; }
|
|
123
|
+
.card { width:100%; max-width:680px; background:#fff; border-radius:20px;
|
|
124
|
+
box-shadow:0 20px 60px rgba(0,0,0,.08),0 4px 16px rgba(0,0,0,.04); padding:36px; }
|
|
125
|
+
h1 { font-size:28px; margin:0 0 6px; letter-spacing:-.5px; }
|
|
126
|
+
.sub { color:#65656d; margin:0 0 24px; }
|
|
127
|
+
.tabs { display:flex; gap:6px; border-bottom:2px solid #e3e3eb; margin-bottom:24px; flex-wrap:wrap; }
|
|
128
|
+
.tab { background:none; border:none; padding:12px 16px; cursor:pointer; font-size:15px; font-weight:500;
|
|
129
|
+
color:#65656d; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all .15s; }
|
|
130
|
+
.tab[aria-current="true"] { color:#1d1d1f; border-bottom-color:#1d1d1f; }
|
|
131
|
+
.tab-status { font-size:13px; }
|
|
132
|
+
.tab-status.ok::before { content:" ✅"; }
|
|
133
|
+
.tab-status.fail::before { content:" ⚠️"; }
|
|
134
|
+
.hero h2 { font-size:22px; margin:0 0 4px; }
|
|
135
|
+
.tagline { color:#65656d; margin:0 0 16px; font-size:15px; }
|
|
136
|
+
.quota { list-style:none; padding:0; margin:0 0 20px; background:#f5f5f9; border-radius:12px; padding:14px 18px; }
|
|
137
|
+
.quota li { padding:3px 0; font-size:14px; color:#3a3a44; }
|
|
138
|
+
.quota li b { color:#1d1d1f; }
|
|
139
|
+
.steps { padding-left:20px; margin:0 0 16px; color:#3a3a44; font-size:14.5px; line-height:1.7; }
|
|
140
|
+
.open-row { margin-bottom:24px; }
|
|
141
|
+
.open-btn { display:inline-block; padding:10px 18px; background:#1a73e8; color:#fff; text-decoration:none;
|
|
142
|
+
border-radius:10px; font-weight:500; font-size:14px; }
|
|
143
|
+
.open-btn:hover { background:#1666d3; }
|
|
144
|
+
.field label { display:block; font-weight:600; margin-bottom:8px; font-size:14px; color:#3a3a44; }
|
|
145
|
+
input[type="text"] { width:100%; padding:14px 16px; border:2px solid #e3e3eb; border-radius:12px;
|
|
146
|
+
font-size:15px; font-family:'SF Mono',Menlo,Consolas,monospace; outline:none; }
|
|
147
|
+
input[type="text"]:focus { border-color:#1a73e8; }
|
|
148
|
+
.submit { margin-top:12px; width:100%; padding:14px; background:#1d1d1f; color:#fff; border:none;
|
|
149
|
+
border-radius:12px; font-size:16px; font-weight:600; cursor:pointer; }
|
|
150
|
+
.submit:hover:not(:disabled) { background:#000; }
|
|
151
|
+
.submit:disabled { background:#aaa; cursor:not-allowed; }
|
|
152
|
+
.msg { margin-top:14px; padding:14px 16px; border-radius:10px; font-size:14px; }
|
|
153
|
+
.msg.error { background:#fff1f0; color:#b00020; border:1px solid #ffd1cc; }
|
|
154
|
+
.msg.ok { background:#effaf3; color:#1a7f4d; border:1px solid #b8e6c9; }
|
|
155
|
+
.msg.ok strong { display:block; font-size:16px; margin-bottom:4px; }
|
|
156
|
+
.footer { margin-top:24px; font-size:13px; color:#8b8b94; text-align:center; }
|
|
157
|
+
.footer .done-btn { display:inline-block; margin-top:8px; padding:10px 22px; background:#1a7f4d; color:#fff;
|
|
158
|
+
text-decoration:none; border-radius:10px; font-weight:600; }
|
|
159
|
+
@media (prefers-color-scheme:dark) {
|
|
160
|
+
body { background:linear-gradient(180deg,#1a1a1f 0%,#0f0f12 100%); color:#f0f0f5; }
|
|
161
|
+
.card { background:#232328; box-shadow:0 20px 60px rgba(0,0,0,.4); }
|
|
162
|
+
.tabs { border-bottom-color:#3a3a42; }
|
|
163
|
+
.tab { color:#8b8b94; }
|
|
164
|
+
.tab[aria-current="true"] { color:#f0f0f5; border-bottom-color:#f0f0f5; }
|
|
165
|
+
.tagline, .footer { color:#8b8b94; }
|
|
166
|
+
.quota { background:#2c2c33; }
|
|
167
|
+
.quota li { color:#b8b8c0; }
|
|
168
|
+
.quota li b { color:#f0f0f5; }
|
|
169
|
+
.steps { color:#b8b8c0; }
|
|
170
|
+
input[type="text"] { background:#2c2c33; border-color:#3a3a42; color:#f0f0f5; }
|
|
171
|
+
.submit { background:#f0f0f5; color:#1d1d1f; }
|
|
172
|
+
.submit:hover:not(:disabled) { background:#fff; }
|
|
173
|
+
.msg.error { background:#3a1f1f; color:#ff8a80; border-color:#5a2929; }
|
|
174
|
+
.msg.ok { background:#1f3a2a; color:#8eddb0; border-color:#295a3c; }
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
177
|
+
</head>
|
|
178
|
+
<body>
|
|
179
|
+
<div class="card">
|
|
180
|
+
<h1>Подключаем AI-провайдеры</h1>
|
|
181
|
+
<p class="sub">Любые из этих трёх дают бесплатный вайбкодинг. Чем больше подключишь — тем устойчивее (если один упрётся в лимит, переключится на следующий).</p>
|
|
182
|
+
|
|
183
|
+
<div class="tabs" role="tablist">${tabs}</div>
|
|
184
|
+
${cards}
|
|
185
|
+
|
|
186
|
+
<div class="footer">
|
|
187
|
+
<p>Ключи хранятся только у тебя в <code>~/.krasavacode/keys/</code> с chmod 600. Ничего не отправляется кроме одного тестового запроса в каждый сервис.</p>
|
|
188
|
+
<p style="margin-top:18px;"><a href="#" class="done-btn" data-action="done">Готово, запустить вайбкодинг</a></p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<script>
|
|
193
|
+
const tabs = document.querySelectorAll('[data-tab-button]');
|
|
194
|
+
const contents = document.querySelectorAll('[data-tab]');
|
|
195
|
+
|
|
196
|
+
function showTab(id) {
|
|
197
|
+
tabs.forEach(t => {
|
|
198
|
+
if (t.dataset.tabButton === id) t.setAttribute('aria-current', 'true');
|
|
199
|
+
else t.removeAttribute('aria-current');
|
|
200
|
+
});
|
|
201
|
+
contents.forEach(c => c.hidden = c.dataset.tab !== id);
|
|
202
|
+
}
|
|
203
|
+
tabs.forEach(t => t.addEventListener('click', () => showTab(t.dataset.tabButton)));
|
|
204
|
+
showTab('${PROVIDER_PRIORITY[0]}');
|
|
205
|
+
|
|
206
|
+
async function refreshStatus() {
|
|
207
|
+
try {
|
|
208
|
+
const r = await fetch('/api/status');
|
|
209
|
+
const data = await r.json();
|
|
210
|
+
document.querySelectorAll('[data-provider-status]').forEach(s => {
|
|
211
|
+
const id = s.dataset.providerStatus;
|
|
212
|
+
s.classList.remove('ok','fail');
|
|
213
|
+
if (data.configured.includes(id)) s.classList.add('ok');
|
|
214
|
+
});
|
|
215
|
+
} catch {}
|
|
216
|
+
}
|
|
217
|
+
refreshStatus();
|
|
218
|
+
|
|
219
|
+
document.querySelectorAll('[data-action="verify"]').forEach(btn => {
|
|
220
|
+
btn.addEventListener('click', async () => {
|
|
221
|
+
const id = btn.dataset.provider;
|
|
222
|
+
const input = document.querySelector('input[data-provider="' + id + '"]');
|
|
223
|
+
const msg = document.querySelector('[data-provider-msg="' + id + '"]');
|
|
224
|
+
const key = (input.value || '').trim();
|
|
225
|
+
msg.className = 'msg';
|
|
226
|
+
msg.textContent = '';
|
|
227
|
+
if (!key) { msg.className = 'msg error'; msg.textContent = 'Поле пустое — вставь ключ.'; return; }
|
|
228
|
+
btn.disabled = true; btn.textContent = 'Проверяю…';
|
|
229
|
+
try {
|
|
230
|
+
const r = await fetch('/api/verify', {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: {'content-type': 'application/json'},
|
|
233
|
+
body: JSON.stringify({ provider: id, key }),
|
|
234
|
+
});
|
|
235
|
+
const data = await r.json();
|
|
236
|
+
if (!r.ok || !data.ok) {
|
|
237
|
+
msg.className = 'msg error';
|
|
238
|
+
msg.textContent = data.error || 'Не получилось.';
|
|
239
|
+
btn.disabled = false; btn.textContent = 'Подключить и проверить';
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
msg.className = 'msg ok';
|
|
243
|
+
msg.innerHTML = '<strong>✅ Подключено!</strong>Ответил «' + escapeHtml(data.text || 'ok') + '». Можно подключить ещё провайдер во вкладках выше или нажать «Готово».';
|
|
244
|
+
btn.textContent = 'Подключено ✓';
|
|
245
|
+
input.disabled = true;
|
|
246
|
+
refreshStatus();
|
|
247
|
+
} catch (e) {
|
|
248
|
+
msg.className = 'msg error'; msg.textContent = 'Сеть не отвечает: ' + e.message;
|
|
249
|
+
btn.disabled = false; btn.textContent = 'Подключить и проверить';
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
document.querySelector('[data-action="done"]').addEventListener('click', async (e) => {
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
await fetch('/api/done', { method: 'POST' });
|
|
257
|
+
window.close();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
function escapeHtml(s) {
|
|
261
|
+
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
window.addEventListener('beforeunload', () => {
|
|
265
|
+
navigator.sendBeacon && navigator.sendBeacon('/api/cancel');
|
|
266
|
+
});
|
|
267
|
+
</script>
|
|
268
|
+
</body>
|
|
269
|
+
</html>`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function browserOnboarding() {
|
|
273
|
+
const port = await getFreePort();
|
|
274
|
+
const url = `http://127.0.0.1:${port}`;
|
|
275
|
+
|
|
276
|
+
let resolveResult;
|
|
277
|
+
const done = new Promise(r => { resolveResult = r; });
|
|
278
|
+
|
|
279
|
+
const server = http.createServer(async (req, res) => {
|
|
280
|
+
if (req.method === 'GET' && (req.url === '/' || req.url.startsWith('/?'))) {
|
|
281
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
282
|
+
return res.end(html());
|
|
283
|
+
}
|
|
284
|
+
if (req.method === 'GET' && req.url === '/api/status') {
|
|
285
|
+
const cfg = await configuredProviders();
|
|
286
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
287
|
+
return res.end(JSON.stringify({ configured: cfg }));
|
|
288
|
+
}
|
|
289
|
+
if (req.method === 'POST' && req.url === '/api/verify') {
|
|
290
|
+
try {
|
|
291
|
+
const body = await readJsonBody(req);
|
|
292
|
+
const provider = String(body.provider || '');
|
|
293
|
+
const key = String(body.key || '').trim();
|
|
294
|
+
const def = PROVIDERS[provider];
|
|
295
|
+
if (!def) throw new Error('Unknown provider: ' + provider);
|
|
296
|
+
if (!def.keyPattern.test(key)) {
|
|
297
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
298
|
+
return res.end(JSON.stringify({ error: `Не похоже на ключ ${def.name}. Ожидается: ${def.keyExample}` }));
|
|
299
|
+
}
|
|
300
|
+
const r = await def.verify(key);
|
|
301
|
+
if (!r.ok) {
|
|
302
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
303
|
+
return res.end(JSON.stringify({ error: r.error }));
|
|
304
|
+
}
|
|
305
|
+
await persistKey(provider, key);
|
|
306
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
307
|
+
return res.end(JSON.stringify({ ok: true, text: r.text }));
|
|
308
|
+
} catch (e) {
|
|
309
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
310
|
+
return res.end(JSON.stringify({ error: e.message }));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (req.method === 'POST' && (req.url === '/api/done' || req.url === '/api/cancel')) {
|
|
314
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
315
|
+
res.end('{}');
|
|
316
|
+
const ok = req.url === '/api/done';
|
|
317
|
+
setTimeout(async () => {
|
|
318
|
+
const cfg = await configuredProviders();
|
|
319
|
+
resolveResult({ launchAfter: ok, configured: cfg });
|
|
320
|
+
}, 100);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
res.writeHead(404);
|
|
324
|
+
res.end();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
server.listen(port, '127.0.0.1');
|
|
328
|
+
|
|
329
|
+
const opened = openBrowser(url);
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log(' ╔═══════════════════════════════════════════════════╗');
|
|
332
|
+
console.log(' ║ KRASAVACODE — подключаем AI-провайдеров ║');
|
|
333
|
+
console.log(' ╚═══════════════════════════════════════════════════╝');
|
|
334
|
+
console.log('');
|
|
335
|
+
if (opened) console.log(` 🌐 Открыл настройку в браузере: ${url}`);
|
|
336
|
+
else console.log(` Скопируй и открой: ${url}`);
|
|
337
|
+
console.log(' Можно подключить один или несколько (для надёжности).');
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(' Ctrl+C — отменить.');
|
|
340
|
+
console.log('');
|
|
341
|
+
|
|
342
|
+
let result;
|
|
343
|
+
try {
|
|
344
|
+
result = await Promise.race([
|
|
345
|
+
done,
|
|
346
|
+
new Promise((_, rej) => process.once('SIGINT', () => rej(new Error('cancelled'))) ),
|
|
347
|
+
]);
|
|
348
|
+
} catch {
|
|
349
|
+
result = { launchAfter: false, configured: await configuredProviders() };
|
|
350
|
+
}
|
|
351
|
+
server.close();
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* ─── CLI fallback (non-GUI envs) ─── */
|
|
356
|
+
|
|
357
|
+
function prompt(q) {
|
|
358
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
359
|
+
return new Promise(resolve => rl.question(q, ans => { rl.close(); resolve(ans); }));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function cliOnboarding() {
|
|
363
|
+
console.log('\n Браузерный режим недоступен — запускаю текстовый.\n');
|
|
364
|
+
console.log(' Доступные провайдеры:');
|
|
365
|
+
for (const id of PROVIDER_PRIORITY) {
|
|
366
|
+
const p = PROVIDERS[id];
|
|
367
|
+
const have = await isProviderConfigured(id) ? '✓' : ' ';
|
|
368
|
+
console.log(` [${have}] ${p.name} — ${p.tagline}`);
|
|
369
|
+
}
|
|
370
|
+
console.log('');
|
|
371
|
+
const which = (await prompt(' Какой настроить? (cerebras / groq / gemini): ')).trim().toLowerCase();
|
|
372
|
+
const def = PROVIDERS[which];
|
|
373
|
+
if (!def) { console.log(' Неизвестный провайдер.'); return { launchAfter: false }; }
|
|
374
|
+
|
|
375
|
+
console.log(`\n Открой: ${def.consoleUrl}`);
|
|
376
|
+
for (const s of def.keyHowto) console.log(` • ${s}`);
|
|
377
|
+
|
|
378
|
+
let key;
|
|
379
|
+
for (let i = 0; i < 3; i++) {
|
|
380
|
+
key = (await prompt(`\n Вставь ключ ${def.name}: `)).trim();
|
|
381
|
+
if (def.keyPattern.test(key)) break;
|
|
382
|
+
console.log(` ⚠️ Не похоже на ключ. Ожидается: ${def.keyExample}\n`);
|
|
383
|
+
}
|
|
384
|
+
if (!def.keyPattern.test(key)) return { launchAfter: false };
|
|
385
|
+
|
|
386
|
+
console.log(' ⏳ Проверяю…');
|
|
387
|
+
const r = await def.verify(key);
|
|
388
|
+
if (!r.ok) { console.log(` ❌ ${r.error}`); return { launchAfter: false }; }
|
|
389
|
+
await persistKey(which, key);
|
|
390
|
+
console.log(' ✅ Подключено.\n');
|
|
391
|
+
|
|
392
|
+
const launch = (await prompt(' Запустить вайбкодинг сейчас? [Enter — да, n — позже]: ')).trim().toLowerCase();
|
|
393
|
+
return { launchAfter: ['', 'y', 'yes', 'д', 'да'].includes(launch) };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function runSetup() {
|
|
397
|
+
const useBrowser = process.env.KRASAVACODE_NO_BROWSER !== '1' && process.stdout.isTTY !== false;
|
|
398
|
+
if (useBrowser) {
|
|
399
|
+
try { return await browserOnboarding(); }
|
|
400
|
+
catch (e) {
|
|
401
|
+
console.error(' Браузерный мастер упал:', e.message);
|
|
402
|
+
return cliOnboarding();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return cliOnboarding();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Backward-compat alias for old subcommand
|
|
409
|
+
export { runSetup as runSetupGemini };
|