krasavacode 0.3.5 → 0.4.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/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 -20
- package/src/metrics-proxy.js +162 -75
- 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-gemini.js
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
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
|
-
|
|
9
|
-
const ROOT = join(homedir(), '.krasavacode');
|
|
10
|
-
const ENV_FILE = join(ROOT, 'gemini.env');
|
|
11
|
-
const STATE_FILE = join(ROOT, 'state.json');
|
|
12
|
-
|
|
13
|
-
const CONSOLE_URL = 'https://aistudio.google.com/apikey';
|
|
14
|
-
|
|
15
|
-
function openBrowser(url) {
|
|
16
|
-
const cmd = platform() === 'darwin' ? 'open'
|
|
17
|
-
: platform() === 'win32' ? 'start'
|
|
18
|
-
: 'xdg-open';
|
|
19
|
-
const args = platform() === 'win32' ? ['', url] : [url];
|
|
20
|
-
try {
|
|
21
|
-
spawn(cmd, args, { detached: true, stdio: 'ignore', shell: platform() === 'win32' }).unref();
|
|
22
|
-
return true;
|
|
23
|
-
} catch { return false; }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function readState() { return readFile(STATE_FILE, 'utf8').then(JSON.parse).catch(() => ({})); }
|
|
27
|
-
async function writeState(s) { await writeFile(STATE_FILE, JSON.stringify(s, null, 2)); }
|
|
28
|
-
|
|
29
|
-
function isValidKeyFormat(key) {
|
|
30
|
-
return /^AIza[A-Za-z0-9_-]{35}$/.test(key);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function verifyKey(key) {
|
|
34
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${encodeURIComponent(key)}`;
|
|
35
|
-
const t0 = Date.now();
|
|
36
|
-
let res;
|
|
37
|
-
try {
|
|
38
|
-
res = await fetch(url, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: { 'content-type': 'application/json' },
|
|
41
|
-
body: JSON.stringify({
|
|
42
|
-
contents: [{ parts: [{ text: 'Say only: ok' }] }],
|
|
43
|
-
generationConfig: { maxOutputTokens: 20 },
|
|
44
|
-
}),
|
|
45
|
-
signal: AbortSignal.timeout(15000),
|
|
46
|
-
});
|
|
47
|
-
} catch (e) {
|
|
48
|
-
return { ok: false, error: 'Сеть не отвечает: ' + e.message, ms: Date.now() - t0 };
|
|
49
|
-
}
|
|
50
|
-
const ms = Date.now() - t0;
|
|
51
|
-
if (!res.ok) {
|
|
52
|
-
let msg = `HTTP ${res.status}`;
|
|
53
|
-
try { const j = await res.json(); msg = j.error?.message || msg; } catch {}
|
|
54
|
-
return { ok: false, error: msg, ms };
|
|
55
|
-
}
|
|
56
|
-
const data = await res.json();
|
|
57
|
-
const text = (data.candidates?.[0]?.content?.parts?.[0]?.text || '').trim();
|
|
58
|
-
return { ok: true, text, ms };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function getFreePort() {
|
|
62
|
-
return new Promise((resolve, reject) => {
|
|
63
|
-
const srv = net.createServer();
|
|
64
|
-
srv.unref();
|
|
65
|
-
srv.on('error', reject);
|
|
66
|
-
srv.listen(0, '127.0.0.1', () => {
|
|
67
|
-
const { port } = srv.address();
|
|
68
|
-
srv.close(() => resolve(port));
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function persistKey(key) {
|
|
74
|
-
await mkdir(ROOT, { recursive: true });
|
|
75
|
-
await writeFile(ENV_FILE, `GEMINI_API_KEY=${key}\n`);
|
|
76
|
-
try { await chmod(ENV_FILE, 0o600); } catch {}
|
|
77
|
-
const state = await readState();
|
|
78
|
-
state.geminiConfigured = true;
|
|
79
|
-
state.geminiConfiguredAt = new Date().toISOString();
|
|
80
|
-
await writeState(state);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const HTML = (port) => `<!doctype html>
|
|
84
|
-
<html lang="ru">
|
|
85
|
-
<head>
|
|
86
|
-
<meta charset="utf-8">
|
|
87
|
-
<title>KRASAVACODE — подключение Google Gemini</title>
|
|
88
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
89
|
-
<style>
|
|
90
|
-
* { box-sizing: border-box; }
|
|
91
|
-
body {
|
|
92
|
-
margin: 0; padding: 32px 16px;
|
|
93
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
94
|
-
background: linear-gradient(180deg, #f7f7fb 0%, #ecedf3 100%);
|
|
95
|
-
color: #1d1d1f; min-height: 100vh;
|
|
96
|
-
display: flex; align-items: flex-start; justify-content: center;
|
|
97
|
-
}
|
|
98
|
-
.card {
|
|
99
|
-
width: 100%; max-width: 640px;
|
|
100
|
-
background: #fff; border-radius: 20px;
|
|
101
|
-
box-shadow: 0 20px 60px rgba(0,0,0,.08), 0 4px 16px rgba(0,0,0,.04);
|
|
102
|
-
padding: 36px;
|
|
103
|
-
}
|
|
104
|
-
h1 { font-size: 28px; margin: 0 0 6px; letter-spacing: -.5px; }
|
|
105
|
-
.sub { color: #65656d; margin: 0 0 28px; }
|
|
106
|
-
.step { display: flex; gap: 14px; margin: 18px 0; align-items: flex-start; }
|
|
107
|
-
.num {
|
|
108
|
-
flex: 0 0 32px; height: 32px; border-radius: 50%;
|
|
109
|
-
background: #1d1d1f; color: #fff; font-weight: 600;
|
|
110
|
-
display: flex; align-items: center; justify-content: center; font-size: 15px;
|
|
111
|
-
}
|
|
112
|
-
.num.done { background: #2ecc71; }
|
|
113
|
-
.body { flex: 1; padding-top: 4px; }
|
|
114
|
-
.body strong { display: block; font-weight: 600; margin-bottom: 4px; }
|
|
115
|
-
.body p { margin: 0; color: #515158; font-size: 14.5px; line-height: 1.5; }
|
|
116
|
-
.open-btn {
|
|
117
|
-
display: inline-block; margin-top: 8px; padding: 10px 18px;
|
|
118
|
-
background: #1a73e8; color: #fff; text-decoration: none;
|
|
119
|
-
border-radius: 10px; font-weight: 500; font-size: 14px;
|
|
120
|
-
}
|
|
121
|
-
.open-btn:hover { background: #1666d3; }
|
|
122
|
-
.field { margin-top: 26px; }
|
|
123
|
-
label { display: block; font-weight: 600; margin-bottom: 8px; font-size: 15px; }
|
|
124
|
-
input[type="text"] {
|
|
125
|
-
width: 100%; padding: 14px 16px;
|
|
126
|
-
border: 2px solid #e3e3eb; border-radius: 12px;
|
|
127
|
-
font-size: 15px; font-family: 'SF Mono', Menlo, Consolas, monospace;
|
|
128
|
-
transition: border-color .15s; outline: none;
|
|
129
|
-
}
|
|
130
|
-
input[type="text"]:focus { border-color: #1a73e8; }
|
|
131
|
-
.submit {
|
|
132
|
-
margin-top: 12px; width: 100%; padding: 14px;
|
|
133
|
-
background: #1d1d1f; color: #fff;
|
|
134
|
-
border: none; border-radius: 12px;
|
|
135
|
-
font-size: 16px; font-weight: 600; cursor: pointer;
|
|
136
|
-
transition: background .15s;
|
|
137
|
-
}
|
|
138
|
-
.submit:hover:not(:disabled) { background: #000; }
|
|
139
|
-
.submit:disabled { background: #aaa; cursor: not-allowed; }
|
|
140
|
-
.msg { margin-top: 14px; padding: 14px 16px; border-radius: 10px; font-size: 14px; }
|
|
141
|
-
.msg.error { background: #fff1f0; color: #b00020; border: 1px solid #ffd1cc; }
|
|
142
|
-
.msg.ok { background: #effaf3; color: #1a7f4d; border: 1px solid #b8e6c9; font-size: 15px; }
|
|
143
|
-
.msg.ok strong { display: block; font-size: 17px; margin-bottom: 4px; }
|
|
144
|
-
.footer { margin-top: 24px; font-size: 13px; color: #8b8b94; text-align: center; }
|
|
145
|
-
.countdown { font-weight: 600; color: #1a7f4d; }
|
|
146
|
-
@media (prefers-color-scheme: dark) {
|
|
147
|
-
body { background: linear-gradient(180deg, #1a1a1f 0%, #0f0f12 100%); color: #f0f0f5; }
|
|
148
|
-
.card { background: #232328; box-shadow: 0 20px 60px rgba(0,0,0,.4); }
|
|
149
|
-
.num { background: #f0f0f5; color: #1d1d1f; }
|
|
150
|
-
.body p { color: #b8b8c0; }
|
|
151
|
-
input[type="text"] { background: #2c2c33; border-color: #3a3a42; color: #f0f0f5; }
|
|
152
|
-
input[type="text"]:focus { border-color: #4a90e2; }
|
|
153
|
-
.submit { background: #f0f0f5; color: #1d1d1f; }
|
|
154
|
-
.submit:hover:not(:disabled) { background: #fff; }
|
|
155
|
-
.footer { color: #6a6a72; }
|
|
156
|
-
.msg.error { background: #3a1f1f; color: #ff8a80; border-color: #5a2929; }
|
|
157
|
-
.msg.ok { background: #1f3a2a; color: #8eddb0; border-color: #295a3c; }
|
|
158
|
-
}
|
|
159
|
-
</style>
|
|
160
|
-
</head>
|
|
161
|
-
<body>
|
|
162
|
-
<div class="card">
|
|
163
|
-
<h1>Подключение Google Gemini</h1>
|
|
164
|
-
<p class="sub">Бесплатно. 1500 запросов в день. Без банковской карты.</p>
|
|
165
|
-
|
|
166
|
-
<div class="step">
|
|
167
|
-
<div class="num">1</div>
|
|
168
|
-
<div class="body">
|
|
169
|
-
<strong>Открой Google AI Studio и войди через свой Gmail</strong>
|
|
170
|
-
<p>Нажми кнопку ниже — откроется новая вкладка. Войди под тем же гуглом, что и обычно.</p>
|
|
171
|
-
<a class="open-btn" href="${CONSOLE_URL}" target="_blank" rel="noopener">Открыть Google AI Studio →</a>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
|
|
175
|
-
<div class="step">
|
|
176
|
-
<div class="num">2</div>
|
|
177
|
-
<div class="body">
|
|
178
|
-
<strong>Нажми «Create API key»</strong>
|
|
179
|
-
<p>Большая синяя кнопка наверху страницы. Если попросят выбрать проект — оставь предложенный.</p>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
<div class="step">
|
|
184
|
-
<div class="num">3</div>
|
|
185
|
-
<div class="body">
|
|
186
|
-
<strong>Скопируй полученный ключ</strong>
|
|
187
|
-
<p>Длинная строка, которая начинается с «AIza». Нажми «Copy» рядом с ней.</p>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
<div class="field">
|
|
192
|
-
<label for="key">Вставь ключ сюда (Cmd/Ctrl+V):</label>
|
|
193
|
-
<input id="key" type="text" autocomplete="off" spellcheck="false" placeholder="AIzaSy…">
|
|
194
|
-
<button id="submit" class="submit">Подключить и проверить</button>
|
|
195
|
-
<div id="msg"></div>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<div class="footer">Это окно открыл сам KRASAVACODE на твоём компьютере. Никуда ничего не отправляется, кроме одного тестового запроса в Google.</div>
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
<script>
|
|
202
|
-
const input = document.getElementById('key');
|
|
203
|
-
const btn = document.getElementById('submit');
|
|
204
|
-
const msg = document.getElementById('msg');
|
|
205
|
-
input.focus();
|
|
206
|
-
|
|
207
|
-
btn.addEventListener('click', submit);
|
|
208
|
-
input.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); });
|
|
209
|
-
|
|
210
|
-
async function submit() {
|
|
211
|
-
const key = input.value.trim();
|
|
212
|
-
msg.className = '';
|
|
213
|
-
msg.textContent = '';
|
|
214
|
-
if (!key) { showError('Поле пустое — вставь ключ из шага 3.'); return; }
|
|
215
|
-
if (!/^AIza[A-Za-z0-9_-]{35}$/.test(key)) {
|
|
216
|
-
showError('Это не похоже на ключ Gemini. Должно быть AIza + 35 символов (всего 39). Скопируй ещё раз.');
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
btn.disabled = true; btn.textContent = 'Проверяю…';
|
|
220
|
-
try {
|
|
221
|
-
const r = await fetch('/api/verify', {
|
|
222
|
-
method: 'POST',
|
|
223
|
-
headers: {'content-type': 'application/json'},
|
|
224
|
-
body: JSON.stringify({ key }),
|
|
225
|
-
});
|
|
226
|
-
const data = await r.json();
|
|
227
|
-
if (!r.ok || !data.ok) {
|
|
228
|
-
showError(data.error || 'Не получилось проверить. Попробуй ещё раз.');
|
|
229
|
-
btn.disabled = false; btn.textContent = 'Подключить и проверить';
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
showSuccess(data);
|
|
233
|
-
} catch (e) {
|
|
234
|
-
showError('Сеть не отвечает: ' + e.message);
|
|
235
|
-
btn.disabled = false; btn.textContent = 'Подключить и проверить';
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function showError(text) {
|
|
240
|
-
msg.className = 'msg error';
|
|
241
|
-
msg.textContent = text;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function showSuccess(data) {
|
|
245
|
-
msg.className = 'msg ok';
|
|
246
|
-
msg.innerHTML = '<strong>✅ Готово!</strong>Gemini ответил «' + escapeHtml(data.text || 'ok') + '» за ' + (data.ms/1000).toFixed(1) + ' сек. Теперь твой вайбкодинг — на Gemini 2.5 Flash. <span class="countdown">Окно закроется через <span id="cd">5</span>…</span>';
|
|
247
|
-
btn.style.display = 'none';
|
|
248
|
-
let n = 5;
|
|
249
|
-
const cd = document.getElementById('cd');
|
|
250
|
-
const timer = setInterval(() => {
|
|
251
|
-
n--;
|
|
252
|
-
if (cd) cd.textContent = n;
|
|
253
|
-
if (n <= 0) {
|
|
254
|
-
clearInterval(timer);
|
|
255
|
-
fetch('/api/done', { method: 'POST' }).catch(() => {});
|
|
256
|
-
window.close();
|
|
257
|
-
}
|
|
258
|
-
}, 1000);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function escapeHtml(s) {
|
|
262
|
-
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
window.addEventListener('beforeunload', () => {
|
|
266
|
-
// best-effort cancel if user closes the tab without finishing
|
|
267
|
-
navigator.sendBeacon && navigator.sendBeacon('/api/cancel');
|
|
268
|
-
});
|
|
269
|
-
</script>
|
|
270
|
-
</body>
|
|
271
|
-
</html>
|
|
272
|
-
`;
|
|
273
|
-
|
|
274
|
-
function readJsonBody(req, max = 8 * 1024) {
|
|
275
|
-
return new Promise((resolve, reject) => {
|
|
276
|
-
const chunks = [];
|
|
277
|
-
let total = 0;
|
|
278
|
-
req.on('data', d => {
|
|
279
|
-
total += d.length;
|
|
280
|
-
if (total > max) { req.destroy(); reject(new Error('body too large')); }
|
|
281
|
-
chunks.push(d);
|
|
282
|
-
});
|
|
283
|
-
req.on('end', () => {
|
|
284
|
-
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
285
|
-
catch { reject(new Error('bad json')); }
|
|
286
|
-
});
|
|
287
|
-
req.on('error', reject);
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async function browserOnboarding() {
|
|
292
|
-
const port = await getFreePort();
|
|
293
|
-
const url = `http://127.0.0.1:${port}`;
|
|
294
|
-
|
|
295
|
-
let resolveResult;
|
|
296
|
-
const done = new Promise(r => { resolveResult = r; });
|
|
297
|
-
|
|
298
|
-
const server = http.createServer(async (req, res) => {
|
|
299
|
-
if (req.method === 'GET' && (req.url === '/' || req.url.startsWith('/?'))) {
|
|
300
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
301
|
-
return res.end(HTML(port));
|
|
302
|
-
}
|
|
303
|
-
if (req.method === 'POST' && req.url === '/api/verify') {
|
|
304
|
-
try {
|
|
305
|
-
const body = await readJsonBody(req);
|
|
306
|
-
const key = String(body.key || '').trim();
|
|
307
|
-
if (!isValidKeyFormat(key)) {
|
|
308
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
309
|
-
return res.end(JSON.stringify({ error: 'Не похоже на ключ Gemini.' }));
|
|
310
|
-
}
|
|
311
|
-
const r = await verifyKey(key);
|
|
312
|
-
if (!r.ok) {
|
|
313
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
314
|
-
return res.end(JSON.stringify({ error: r.error }));
|
|
315
|
-
}
|
|
316
|
-
await persistKey(key);
|
|
317
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
318
|
-
res.end(JSON.stringify({ ok: true, text: r.text, ms: r.ms }));
|
|
319
|
-
// Schedule shutdown after 6s — gives the page time to show the success state
|
|
320
|
-
setTimeout(() => resolveResult({ launchAfter: true, configured: true }), 6000);
|
|
321
|
-
} catch (e) {
|
|
322
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
323
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
324
|
-
}
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (req.method === 'POST' && (req.url === '/api/done' || req.url === '/api/cancel')) {
|
|
328
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
329
|
-
res.end('{}');
|
|
330
|
-
// If cancel arrived before a successful verify, treat as cancellation.
|
|
331
|
-
// If after — resolveResult was already called by /api/verify timeout.
|
|
332
|
-
setTimeout(() => resolveResult({ launchAfter: false, configured: false }), 100);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
res.writeHead(404);
|
|
336
|
-
res.end();
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
server.listen(port, '127.0.0.1');
|
|
340
|
-
|
|
341
|
-
const opened = openBrowser(url);
|
|
342
|
-
console.log('');
|
|
343
|
-
console.log(' ╔══════════════════════════════════════════════════╗');
|
|
344
|
-
console.log(' ║ KRASAVACODE — подключаем Google Gemini ║');
|
|
345
|
-
console.log(' ╚══════════════════════════════════════════════════╝');
|
|
346
|
-
console.log('');
|
|
347
|
-
if (opened) {
|
|
348
|
-
console.log(` 🌐 Открыл настройку в браузере: ${url}`);
|
|
349
|
-
} else {
|
|
350
|
-
console.log(` Не получилось открыть браузер автоматически.`);
|
|
351
|
-
console.log(` Скопируй и открой эту ссылку вручную: ${url}`);
|
|
352
|
-
}
|
|
353
|
-
console.log(' Это окно (терминал) можно не трогать — оно закроется само.');
|
|
354
|
-
console.log('');
|
|
355
|
-
console.log(' Нажми Ctrl+C чтобы отменить и вернуться позже.');
|
|
356
|
-
console.log('');
|
|
357
|
-
|
|
358
|
-
let result;
|
|
359
|
-
try {
|
|
360
|
-
result = await Promise.race([
|
|
361
|
-
done,
|
|
362
|
-
new Promise((_, rej) => process.once('SIGINT', () => rej(new Error('cancelled'))) ),
|
|
363
|
-
]);
|
|
364
|
-
} catch {
|
|
365
|
-
result = { launchAfter: false, configured: false };
|
|
366
|
-
}
|
|
367
|
-
server.close();
|
|
368
|
-
return result;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/* ────── CLI fallback (kept for non-GUI environments) ────── */
|
|
372
|
-
|
|
373
|
-
function prompt(q) {
|
|
374
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
375
|
-
return new Promise(resolve => rl.question(q, ans => { rl.close(); resolve(ans); }));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
async function cliOnboarding() {
|
|
379
|
-
console.log('\n Браузерный режим недоступен — запускаю текстовый.\n');
|
|
380
|
-
console.log(` Открой в браузере: ${CONSOLE_URL}`);
|
|
381
|
-
console.log(' Войди через Google → «Create API key» → скопируй ключ.\n');
|
|
382
|
-
|
|
383
|
-
let key;
|
|
384
|
-
for (let i = 0; i < 3; i++) {
|
|
385
|
-
key = (await prompt(' Вставь ключ Gemini сюда: ')).trim();
|
|
386
|
-
if (isValidKeyFormat(key)) break;
|
|
387
|
-
console.log(' ⚠️ Не похоже на ключ Gemini. Попробуй ещё раз.\n');
|
|
388
|
-
}
|
|
389
|
-
if (!isValidKeyFormat(key)) {
|
|
390
|
-
console.log('\n ❌ Не удалось получить ключ. Запусти команду ещё раз позже.');
|
|
391
|
-
return { launchAfter: false, configured: false };
|
|
392
|
-
}
|
|
393
|
-
console.log('\n ⏳ Проверяю…');
|
|
394
|
-
const r = await verifyKey(key);
|
|
395
|
-
if (!r.ok) {
|
|
396
|
-
console.log(` ❌ ${r.error}\n Запусти команду ещё раз.`);
|
|
397
|
-
return { launchAfter: false, configured: false };
|
|
398
|
-
}
|
|
399
|
-
await persistKey(key);
|
|
400
|
-
console.log(` ✅ Готово! Gemini ответил за ${(r.ms/1000).toFixed(1)} сек.\n`);
|
|
401
|
-
const launch = (await prompt(' Запустить вайбкодинг сейчас? [Enter — да, n — позже]: ')).trim().toLowerCase();
|
|
402
|
-
return { launchAfter: launch === '' || launch === 'y' || launch === 'yes' || launch === 'д' || launch === 'да', configured: true };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export async function runSetupGemini() {
|
|
406
|
-
const useBrowser = process.env.KRASAVACODE_NO_BROWSER !== '1' && process.stdout.isTTY !== false;
|
|
407
|
-
if (useBrowser) {
|
|
408
|
-
try { return await browserOnboarding(); }
|
|
409
|
-
catch (e) {
|
|
410
|
-
console.error(' Браузерный мастер упал:', e.message);
|
|
411
|
-
return cliOnboarding();
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return cliOnboarding();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export async function loadGeminiKey() {
|
|
418
|
-
try {
|
|
419
|
-
const content = await readFile(ENV_FILE, 'utf8');
|
|
420
|
-
const m = content.match(/^GEMINI_API_KEY=(.+)$/m);
|
|
421
|
-
return m ? m[1].trim() : null;
|
|
422
|
-
} catch { return null; }
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export async function isGeminiConfigured() {
|
|
426
|
-
return access(ENV_FILE).then(() => true).catch(() => false);
|
|
427
|
-
}
|