krasavacode 0.3.0 → 0.3.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 CHANGED
@@ -73,19 +73,25 @@ chmod +x ~/krasavacode
73
73
 
74
74
  **Как:**
75
75
 
76
- 1. На значке **«ВАЙБКОДИНГ»** на Рабочем столе пока не дабл-кликай.
77
- 2. Открой Терминал (Mac: Spotlight → «Терминал»; Win: меню «Пуск» → «Командная строка»).
78
- 3. Скопируй и вставь одну строку:
76
+ 1. Запусти **значок «ВАЙБКОДИНГ»** на Рабочем столе. В программе напиши команду:
79
77
 
80
- ```bash
81
- npx krasavacode setup-gemini
82
78
  ```
83
- 4. Программа откроет в браузере страницу Google AI Studio. Войди через свой Gmail.
84
- 5. Нажми **«Create API key»** — появится длинная строка-ключ.
85
- 6. Скопируй ключ, вернись в окно Терминала и **вставь** (Mac: ⌘+V, Win: правая кнопка → Paste).
86
- 7. Программа сама проверит ключ и переключит вайбкодинг на Gemini.
79
+ /setup-gemini
80
+ ```
81
+
82
+ *(или из Терминала: `npx krasavacode setup-gemini`)*
83
+
84
+ 2. У тебя в браузере **сама откроется страница** с тремя шагами и красивой кнопкой.
85
+
86
+ 3. На этой странице:
87
+ - Кликни **«Открыть Google AI Studio»** → войдёшь через свой Gmail
88
+ - Нажми **«Create API key»** — появится строка вида `AIzaSy…`
89
+ - Скопируй её, вернись в нашу страницу и **вставь** в поле
90
+ - Нажми **«Подключить и проверить»**
91
+
92
+ 4. Если зелёная галочка — **готово**. Окно само закроется.
87
93
 
88
- После этого обычный значок **«ВАЙБКОДИНГ»** работает уже на Gemini. Качество — ощутимо выше.
94
+ С этого момента значок **«ВАЙБКОДИНГ»** работает уже на Gemini. Качество — ощутимо выше.
89
95
 
90
96
  ---
91
97
 
@@ -8,7 +8,7 @@ import { runDoctor } from '../src/doctor.js';
8
8
  import { runSetupGemini } from '../src/setup-gemini.js';
9
9
 
10
10
  // Hardcoded so it works inside Bun --compile (no FS access to package.json)
11
- const VERSION = '0.3.0';
11
+ const VERSION = '0.3.1';
12
12
 
13
13
  const cmd = process.argv[2];
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "krasavacode",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway. Сам ставит Node при необходимости.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,6 +3,8 @@ import { mkdir, writeFile, chmod, readFile, access } from 'node:fs/promises';
3
3
  import { homedir, platform } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
+ import http from 'node:http';
7
+ import net from 'node:net';
6
8
 
7
9
  const ROOT = join(homedir(), '.krasavacode');
8
10
  const ENV_FILE = join(ROOT, 'gemini.env');
@@ -15,37 +17,36 @@ function openBrowser(url) {
15
17
  : platform() === 'win32' ? 'start'
16
18
  : 'xdg-open';
17
19
  const args = platform() === 'win32' ? ['', url] : [url];
18
- spawn(cmd, args, { detached: true, stdio: 'ignore', shell: platform() === 'win32' }).unref();
20
+ try {
21
+ spawn(cmd, args, { detached: true, stdio: 'ignore', shell: platform() === 'win32' }).unref();
22
+ return true;
23
+ } catch { return false; }
19
24
  }
20
25
 
21
- function readState() {
22
- return readFile(STATE_FILE, 'utf8').then(JSON.parse).catch(() => ({}));
23
- }
26
+ function readState() { return readFile(STATE_FILE, 'utf8').then(JSON.parse).catch(() => ({})); }
24
27
  async function writeState(s) { await writeFile(STATE_FILE, JSON.stringify(s, null, 2)); }
25
28
 
26
- function prompt(question) {
27
- const rl = createInterface({ input: process.stdin, output: process.stdout });
28
- return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans); }));
29
- }
30
-
31
29
  function isValidKeyFormat(key) {
32
- // Google API keys look like AIza followed by 35 chars [A-Za-z0-9_-]
33
30
  return /^AIza[A-Za-z0-9_-]{35}$/.test(key);
34
31
  }
35
32
 
36
- /** Sanity-check the key with a tiny Gemini API call. */
37
33
  async function verifyKey(key) {
38
34
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${encodeURIComponent(key)}`;
39
35
  const t0 = Date.now();
40
- const res = await fetch(url, {
41
- method: 'POST',
42
- headers: { 'content-type': 'application/json' },
43
- body: JSON.stringify({
44
- contents: [{ parts: [{ text: 'Say only the word: ok' }] }],
45
- generationConfig: { maxOutputTokens: 20 },
46
- }),
47
- signal: AbortSignal.timeout(15000),
48
- });
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
+ }
49
50
  const ms = Date.now() - t0;
50
51
  if (!res.ok) {
51
52
  let msg = `HTTP ${res.status}`;
@@ -53,109 +54,366 @@ async function verifyKey(key) {
53
54
  return { ok: false, error: msg, ms };
54
55
  }
55
56
  const data = await res.json();
56
- const text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
57
- return { ok: true, text: text.trim(), ms };
57
+ const text = (data.candidates?.[0]?.content?.parts?.[0]?.text || '').trim();
58
+ return { ok: true, text, ms };
58
59
  }
59
60
 
60
- function header(text) {
61
- const line = '━'.repeat(58);
62
- console.log('');
63
- console.log(line);
64
- console.log(' ' + text);
65
- console.log(line);
66
- console.log('');
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
+ });
67
71
  }
68
72
 
69
- export async function runSetupGemini() {
73
+ async function persistKey(key) {
70
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>
71
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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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);
72
342
  console.log('');
73
343
  console.log(' ╔══════════════════════════════════════════════════╗');
74
- console.log(' ║ KRASAVACODE — апгрейд на Gemini 2.5 Flash ║');
344
+ console.log(' ║ KRASAVACODE — подключаем Google Gemini ║');
75
345
  console.log(' ╚══════════════════════════════════════════════════╝');
76
346
  console.log('');
77
- console.log(' Сейчас вайбкодинг работает на простой модели.');
78
- console.log(' Подключив бесплатный Google Gemini, получишь:');
79
- console.log(' ✓ Качество в разы выше');
80
- console.log(' ✓ 1500 запросов в день бесплатно');
81
- 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 чтобы отменить и вернуться позже.');
82
356
  console.log('');
83
- console.log(' Это займёт 60 секунд.');
84
357
 
85
- header('Открываю в браузере: ' + CONSOLE_URL);
86
- openBrowser(CONSOLE_URL);
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
+ }
87
370
 
88
- console.log(' ШАГ 1. Войди через Google.');
89
- console.log(' (если у тебя Gmail, YouTube или Android — это он)');
90
- console.log('');
91
- console.log(' ШАГ 2. Нажми кнопку «Create API key» наверху страницы.');
92
- console.log(' Если попросят выбрать проект оставь предложенный.');
93
- console.log('');
94
- console.log(' ШАГ 3. Появится длинная строка-ключ. Нажми «Copy».');
95
- console.log(' Ключ начинается с «AIza».');
96
- console.log('');
97
- console.log(' ШАГ 4. Вернись сюда в это окно и вставь ключ ниже.');
98
- console.log(' Mac: ⌘+V Windows/Linux: Ctrl+V или правая кнопкаPaste');
99
- console.log('');
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');
100
382
 
101
383
  let key;
102
- for (let attempt = 0; attempt < 3; attempt++) {
384
+ for (let i = 0; i < 3; i++) {
103
385
  key = (await prompt(' Вставь ключ Gemini сюда: ')).trim();
104
- if (!key) {
105
- console.log(' ⚠️ Пусто. Скопируй ключ и попробуй ещё раз.\n');
106
- continue;
107
- }
108
- if (!isValidKeyFormat(key)) {
109
- console.log(' ⚠️ Не похоже на ключ Gemini.');
110
- console.log(' Должно быть AIza + 35 символов (всего 39).');
111
- console.log(' Скопируй ещё раз внимательно.\n');
112
- continue;
113
- }
114
- break;
386
+ if (isValidKeyFormat(key)) break;
387
+ console.log(' ⚠️ Не похоже на ключ Gemini. Попробуй ещё раз.\n');
115
388
  }
116
389
  if (!isValidKeyFormat(key)) {
117
- console.log('\n ❌ Не удалось получить ключ. Запусти `krasavacode setup-gemini` ещё раз.');
118
- return;
390
+ console.log('\n ❌ Не удалось получить ключ. Запусти команду ещё раз позже.');
391
+ return { launchAfter: false, configured: false };
119
392
  }
120
-
121
- console.log('\n ⏳ Проверяю ключ через тестовый запрос…');
122
- const result = await verifyKey(key);
123
- if (!result.ok) {
124
- console.log(` ❌ Ключ не работает: ${result.error}`);
125
- console.log(' Проверь, что скопировал целиком, без пробелов.');
126
- console.log(' Иногда Google требует подождать ~30 секунд после создания ключа.');
127
- console.log(' Запусти `krasavacode setup-gemini` ещё раз.');
128
- return;
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 };
129
398
  }
130
- console.log(` ✅ Работает! Gemini ответил «${result.text}» за ${(result.ms / 1000).toFixed(1)} сек.`);
131
-
132
- // Save the key in a private env file (chmod 600)
133
- const envContent = `GEMINI_API_KEY=${key}\n`;
134
- await writeFile(ENV_FILE, envContent);
135
- try { await chmod(ENV_FILE, 0o600); } catch {}
136
-
137
- // Mark in state.json so future `krasavacode` runs know to use Gemini
138
- const state = await readState();
139
- state.geminiConfigured = true;
140
- state.geminiConfiguredAt = new Date().toISOString();
141
- await writeState(state);
142
-
143
- console.log('');
144
- console.log(' 💾 Ключ сохранён в ' + ENV_FILE);
145
- console.log(' (он остаётся только у тебя на компьютере, никуда не отправляется)');
146
- console.log('');
147
- console.log(' ✅ Готово! Теперь твой вайбкодинг — на Gemini 2.5 Flash.');
148
- console.log('');
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
+ }
149
404
 
150
- const launch = (await prompt(' Запустить вайбкодинг прямо сейчас? [Enter — да, n — позже]: ')).trim().toLowerCase();
151
- if (launch === '' || launch === 'y' || launch === 'yes' || launch === 'д' || launch === 'да') {
152
- return { launchAfter: true };
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
+ }
153
413
  }
154
- console.log('\n ОК. Когда захочешь — запусти `krasavacode`.');
155
- return { launchAfter: false };
414
+ return cliOnboarding();
156
415
  }
157
416
 
158
- /** Read GEMINI_API_KEY from gemini.env. Returns null if not configured. */
159
417
  export async function loadGeminiKey() {
160
418
  try {
161
419
  const content = await readFile(ENV_FILE, 'utf8');