krasavacode 0.2.1 → 0.3.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/README.md CHANGED
@@ -1,85 +1,116 @@
1
1
  # KRASAVACODE
2
2
 
3
- Однокнопочный бесплатный вайбкодинг на Claude Code для учеников.
3
+ Бесплатный вайбкодинг через ИИ. Ставится двумя кликами, без регистрации, без карты.
4
4
 
5
- ## Установка и запуск
5
+ ---
6
6
 
7
- ### Вариант А — у меня есть Node.js
7
+ ## 🍏 У меня Mac
8
8
 
9
- ```bash
10
- npx krasavacode
11
- ```
9
+ **Шаг 1.** Скачай файл-установщик:
12
10
 
13
- Всё. Pollinations работает без логина и без карты. Если у тебя Node старее 20 — наш CLI сам подтянет нужную версию.
11
+ ➡️ **[install-mac.command](https://github.com/alexrexby/krasavacode/raw/main/install-mac.command)**
14
12
 
15
- ### Вариант Б у меня нет ничего
13
+ (нажмёшь он сразу скачается в папку «Загрузки»)
16
14
 
17
- Скачай готовый бинарник из последнего релиза:
18
- **https://github.com/alexrexby/krasavacode/releases/latest**
15
+ **Шаг 2.** Открой папку «Загрузки» (значок Finder в Dock → слева **Загрузки**).
19
16
 
20
- | Твоя ОС | Файл |
21
- |---|---|
22
- | Windows | `krasavacode.exe` |
23
- | macOS Apple Silicon (M1/M2/M3/M4) | `krasavacode-mac-arm64` |
24
- | macOS Intel | `krasavacode-mac-x64` |
25
- | Linux x64 | `krasavacode-linux-x64` |
17
+ **Шаг 3.** Нажми **правой кнопкой мыши** на файл `install-mac.command` → в меню выбери **«Открыть»** → в появившемся окне ещё раз **«Открыть»**.
26
18
 
27
- После скачивания открой терминал в папке со скачанным файлом и запусти:
19
+ > ⚠️ Почему правой кнопкой? Файл от незнакомого автора, и macOS первый раз спрашивает разрешения. Через правую кнопку → «Открыть» macOS даст согласие. На обычный дабл-клик — заблокирует.
28
20
 
29
- **Windows:**
30
- ```
31
- krasavacode.exe
32
- ```
33
- Если выскочит «Windows protected your PC» → жми **More info** → **Run anyway**.
21
+ **Шаг 4.** Откроется чёрное окно (это нормально — это Терминал). Жди ≈ минуту, оно само скачает программу. В конце нажми **Enter**.
34
22
 
35
- **macOS:**
36
- ```bash
37
- chmod +x krasavacode-mac-arm64
38
- xattr -d com.apple.quarantine krasavacode-mac-arm64 # снимает блок Gatekeeper
39
- ./krasavacode-mac-arm64
40
- ```
23
+ **Шаг 5.** На Рабочем столе появился значок **«ВАЙБКОДИНГ»**. Дабл-клик — и пиши задачу обычным языком: «сделай мне сайт-визитку», «напиши игру в крестики-нолики», что угодно.
41
24
 
42
- **Linux:**
43
- ```bash
44
- chmod +x krasavacode-linux-x64
45
- ./krasavacode-linux-x64
46
- ```
25
+ ---
47
26
 
48
- При первом запуске бинарник скачает Node.js и Claude Code в `~/.krasavacode/` (≈100 МБ, один раз). Дальше — мгновенно.
27
+ ## 🪟 У меня Windows
49
28
 
50
- ## Если хочется моделей помощнее
29
+ **Шаг 1.** Скачай файл-установщик:
51
30
 
52
- ```bash
53
- npx krasavacode upgrade
54
- ```
31
+ ➡️ **[install-windows.bat](https://github.com/alexrexby/krasavacode/raw/main/install-windows.bat)**
32
+
33
+ (нажмёшь — он сразу скачается в папку «Загрузки». Если Edge спросит «оставить или отменить», нажми **«Сохранить»**)
34
+
35
+ **Шаг 2.** Открой папку **Загрузки** (Проводник → Загрузки слева).
36
+
37
+ **Шаг 3.** Дабл-клик на файл `install-windows.bat`.
55
38
 
56
- Откроется дашборд OmniRoute в браузере. Подключи одним кликом:
57
- - **Kiro AI** — Claude Sonnet/Haiku через AWS Builder ID
58
- - **Qoder** — Kimi K2 / Qwen3-Coder / DeepSeek-R1
59
- - **Qwen Code** — 4 модели Alibaba
60
- - **LongCat** — 50M токенов в день
39
+ > ⚠️ Если Windows покажет синее окно **«Windows protected your PC»** — нажми **More info** (или «Подробнее»), потом **Run anyway** (или «Выполнить в любом случае»). Файл безопасный, но без платной подписи Windows показывает это окно всегда.
61
40
 
62
- Всё бесплатно, без карты.
41
+ **Шаг 4.** Откроется чёрное окно (это Командная строка). Жди ≈ минуту, оно само скачает программу. В конце нажми **любую клавишу**.
63
42
 
64
- ## Если что-то не работает
43
+ **Шаг 5.** На Рабочем столе появился значок **«ВАЙБКОДИНГ»**. Дабл-клик — и пиши задачу.
44
+
45
+ ---
46
+
47
+ ## 🐧 Linux
65
48
 
66
49
  ```bash
67
- npx krasavacode doctor
50
+ curl -L https://github.com/alexrexby/krasavacode/releases/latest/download/krasavacode-linux-x64 -o ~/krasavacode
51
+ chmod +x ~/krasavacode
52
+ ~/krasavacode
68
53
  ```
69
54
 
70
- Покажет что сломано. Самые частые проблемы:
71
- - **Корпоративный прокси / Россия / Китай** → запусти upgrade и в дашборде включи SOCKS5
72
- - **Порт 3456 занят** → перезапусти терминал
55
+ ---
73
56
 
74
- ## Что под капотом
57
+ ## Что писать в окне «ВАЙБКОДИНГ»
75
58
 
59
+ Что угодно своими словами:
60
+
61
+ - «Сделай простой сайт обо мне с моими увлечениями»
62
+ - «Напиши на Python калькулятор подоходного налога»
63
+ - «Создай игру Тетрис на одной HTML-странице»
64
+ - «Объясни, что такое API, как для пятиклассника»
65
+
66
+ Программа сама напишет код, файлы, структуру проекта. Если не знаешь, что делать дальше — просто спроси её: **«а что мне дальше делать?»**.
67
+
68
+ ---
69
+
70
+ ## Хочешь модель помощнее? (опционально, 60 секунд)
71
+
72
+ По дефолту работает простая модель — её хватит на первые проекты. Если хочешь качество как у настоящего Claude — подключи бесплатный **Google Gemini 2.5 Flash** (1500 запросов в день, без карты).
73
+
74
+ **Как:**
75
+
76
+ 1. На значке **«ВАЙБКОДИНГ»** на Рабочем столе — пока не дабл-кликай.
77
+ 2. Открой Терминал (Mac: Spotlight → «Терминал»; Win: меню «Пуск» → «Командная строка»).
78
+ 3. Скопируй и вставь одну строку:
79
+
80
+ ```bash
81
+ npx krasavacode setup-gemini
82
+ ```
83
+ 4. Программа откроет в браузере страницу Google AI Studio. Войди через свой Gmail.
84
+ 5. Нажми **«Create API key»** — появится длинная строка-ключ.
85
+ 6. Скопируй ключ, вернись в окно Терминала и **вставь** (Mac: ⌘+V, Win: правая кнопка → Paste).
86
+ 7. Программа сама проверит ключ и переключит вайбкодинг на Gemini.
87
+
88
+ После этого обычный значок **«ВАЙБКОДИНГ»** работает уже на Gemini. Качество — ощутимо выше.
89
+
90
+ ---
91
+
92
+ ## Что-то пошло не так?
93
+
94
+ | Проблема | Что делать |
95
+ |---|---|
96
+ | Mac пишет «не может быть открыт» | Жми **правой кнопкой** → **Открыть** → **Открыть**. Не дабл-клик. |
97
+ | Windows пишет «protected your PC» | Жми **More info** → **Run anyway**. |
98
+ | Окно сразу закрылось | Перетащи файл `install-mac.command` или `install-windows.bat` в Терминал/cmd и нажми Enter — увидишь сообщение об ошибке. |
99
+ | Долго качает | Установщик тащит ≈100 МБ — нужен интернет. На медленных соединениях может занять 5–10 минут. |
100
+ | Ошибка про блокировку Pollinations / 429 | Подожди минуту и попробуй снова — это бесплатный сервис, у него лимиты. Или `npx krasavacode upgrade` для подключения других моделей. |
101
+
102
+ ---
103
+
104
+ ## Для тех, кто знает, что такое терминал
105
+
106
+ ```bash
107
+ npx krasavacode
76
108
  ```
77
- krasavacode → claude-code-router (порт 3456) → Pollinations
78
- (Anthropic ↔ OpenAI bridge) (free, no API key)
79
- ```
80
109
 
81
- CLI = `@anthropic-ai/claude-code` с подменённым `ANTHROPIC_BASE_URL`. Все обновления Claude Code от Anthropic прилетают автоматом.
110
+ Работает на любой ОС с Node.js 20+. Если Node старше 20 или вообще нет — CLI сам подтянет нужный. Источники: https://github.com/alexrexby/krasavacode
111
+
112
+ ---
82
113
 
83
114
  ## Лицензия
84
115
 
85
- MIT. Pollinations / Kiro AI / Qoder / другие провайдеры имеют свои ToSчитай их условия.
116
+ MIT. Под капотом: [@anthropic-ai/claude-code](https://www.npmjs.com/package/@anthropic-ai/claude-code) + [@musistudio/claude-code-router](https://www.npmjs.com/package/@musistudio/claude-code-router) + [Pollinations](https://pollinations.ai). Их условия использованияна их сайтах.
@@ -5,15 +5,21 @@ import { ensurePreset } from '../src/preset.js';
5
5
  import { launchClaude } from '../src/launch.js';
6
6
  import { runUpgrade } from '../src/upgrade.js';
7
7
  import { runDoctor } from '../src/doctor.js';
8
+ import { runSetupGemini } from '../src/setup-gemini.js';
8
9
 
9
10
  // Hardcoded so it works inside Bun --compile (no FS access to package.json)
10
- const VERSION = '0.2.1';
11
+ const VERSION = '0.3.0';
11
12
 
12
13
  const cmd = process.argv[2];
13
14
 
14
15
  async function main() {
15
16
  if (cmd === 'doctor') return runDoctor();
16
17
  if (cmd === 'upgrade') return runUpgrade();
18
+ if (cmd === 'setup-gemini' || cmd === 'gemini') {
19
+ const result = await runSetupGemini();
20
+ if (!result?.launchAfter) return;
21
+ // fall through to normal launch flow below
22
+ }
17
23
  if (cmd === '--version' || cmd === '-v') {
18
24
  console.log(`KRASAVACODE v${VERSION}`);
19
25
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "krasavacode",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway. Сам ставит Node при необходимости.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/hub.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { setTimeout as sleep } from 'node:timers/promises';
3
3
  import { CCR_PORT } from './preset.js';
4
+ import { loadGeminiKey } from './setup-gemini.js';
4
5
 
5
6
  const HOST = '127.0.0.1';
6
7
  const PORT = CCR_PORT;
@@ -38,10 +39,16 @@ export async function startHub(paths) {
38
39
 
39
40
  process.stdout.write(`🚀 Поднимаю локальный gateway на порту ${PORT}… `);
40
41
 
42
+ // Inject GEMINI_API_KEY into ccr env if user has configured Gemini.
43
+ // ccr's config.json references it as $GEMINI_API_KEY (env-interpolation).
44
+ const ccrEnv = { ...paths.env };
45
+ const geminiKey = await loadGeminiKey();
46
+ if (geminiKey) ccrEnv.GEMINI_API_KEY = geminiKey;
47
+
41
48
  const child = spawn(paths.ccrBin, ['start'], {
42
49
  stdio: process.env.KRASAVACODE_DEBUG ? 'inherit' : 'pipe',
43
50
  detached: false,
44
- env: paths.env,
51
+ env: ccrEnv,
45
52
  });
46
53
 
47
54
  let stderrTail = '';
package/src/launch.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { isGeminiConfigured } from './setup-gemini.js';
2
3
 
3
4
  const PLACEHOLDER_TOKEN = 'sk-krasavacode-local';
4
5
 
5
6
  export async function launchClaude(paths, hub /*, detection */) {
7
+ const geminiOn = await isGeminiConfigured();
8
+
6
9
  const env = {
7
10
  ...process.env,
8
11
  ANTHROPIC_BASE_URL: hub.baseUrl,
@@ -21,7 +24,12 @@ export async function launchClaude(paths, hub /*, detection */) {
21
24
  console.log('');
22
25
  console.log('━'.repeat(58));
23
26
  console.log(' KRASAVACODE — вайбкодинг через локальный hub');
24
- console.log(' Hub: ' + hub.baseUrl + ' (claude-code-router → Pollinations)');
27
+ if (geminiOn) {
28
+ console.log(' Модель: Google Gemini 2.5 Flash (1500 запросов в день)');
29
+ } else {
30
+ console.log(' Модель: gpt-oss-20b через Pollinations (бесплатно, без логина)');
31
+ console.log(' 💡 Хочешь модель посильнее бесплатно? → krasavacode setup-gemini');
32
+ }
25
33
  console.log(' Пиши задачу обычным языком, ИИ сделает.');
26
34
  console.log('━'.repeat(58));
27
35
  console.log('');
package/src/preset.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, writeFile, copyFile, access } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
- import { join, dirname } from 'node:path';
3
+ import { join } from 'node:path';
4
+ import { isGeminiConfigured } from './setup-gemini.js';
4
5
 
5
6
  const CCR_DIR = join(homedir(), '.claude-code-router');
6
7
  const CCR_CONFIG = join(CCR_DIR, 'config.json');
@@ -8,52 +9,76 @@ const STATE_FILE = join(homedir(), '.krasavacode', 'state.json');
8
9
 
9
10
  const KRASAVACODE_MARKER = 'krasavacode/managed';
10
11
 
11
- const FREE_CONFIG = {
12
- HOST: '127.0.0.1',
13
- PORT: 3456,
14
- LOG: false,
15
- API_TIMEOUT_MS: 600000,
16
- Providers: [
17
- {
18
- name: 'pollinations',
19
- api_base_url: 'https://text.pollinations.ai/openai/chat/completions',
20
- api_key: 'public',
21
- models: ['openai', 'openai-fast', 'gpt-oss-20b'],
22
- },
23
- ],
24
- Router: {
25
- default: 'pollinations,openai',
26
- background: 'pollinations,openai-fast',
27
- think: 'pollinations,openai',
28
- longContext: 'pollinations,openai',
29
- longContextThreshold: 60000,
30
- },
31
- // Marker so future runs know this config is ours and we may overwrite it.
32
- // If a user has manually edited config (no marker), we leave it alone.
33
- _krasavacode: KRASAVACODE_MARKER,
34
- };
12
+ function pollinationsProvider() {
13
+ return {
14
+ name: 'pollinations',
15
+ api_base_url: 'https://text.pollinations.ai/openai/chat/completions',
16
+ api_key: 'public',
17
+ models: ['openai', 'openai-fast', 'gpt-oss-20b'],
18
+ };
19
+ }
35
20
 
36
- async function readState() {
37
- try { return JSON.parse(await readFile(STATE_FILE, 'utf8')); }
38
- catch { return {}; }
21
+ function geminiProvider() {
22
+ return {
23
+ name: 'gemini',
24
+ api_base_url: 'https://generativelanguage.googleapis.com/v1beta/models/',
25
+ api_key: '$GEMINI_API_KEY',
26
+ models: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-flash-latest'],
27
+ transformer: { use: ['gemini'] },
28
+ };
39
29
  }
40
- async function writeState(s) {
41
- await writeFile(STATE_FILE, JSON.stringify(s, null, 2));
30
+
31
+ function buildConfig({ withGemini }) {
32
+ const Providers = withGemini
33
+ ? [geminiProvider(), pollinationsProvider()]
34
+ : [pollinationsProvider()];
35
+
36
+ const Router = withGemini
37
+ ? {
38
+ default: 'gemini,gemini-2.5-flash',
39
+ background: 'gemini,gemini-2.5-flash',
40
+ think: 'gemini,gemini-2.5-pro',
41
+ longContext: 'gemini,gemini-2.5-pro',
42
+ longContextThreshold: 60000,
43
+ }
44
+ : {
45
+ default: 'pollinations,openai',
46
+ background: 'pollinations,openai-fast',
47
+ think: 'pollinations,openai',
48
+ longContext: 'pollinations,openai',
49
+ longContextThreshold: 60000,
50
+ };
51
+
52
+ return {
53
+ HOST: '127.0.0.1',
54
+ PORT: 3456,
55
+ LOG: false,
56
+ API_TIMEOUT_MS: 600000,
57
+ Providers,
58
+ Router,
59
+ _krasavacode: KRASAVACODE_MARKER,
60
+ };
42
61
  }
43
62
 
44
- async function exists(p) {
45
- return access(p).then(() => true).catch(() => false);
63
+ async function readState() {
64
+ try { return JSON.parse(await readFile(STATE_FILE, 'utf8')); }
65
+ catch { return {}; }
46
66
  }
67
+ async function writeState(s) { await writeFile(STATE_FILE, JSON.stringify(s, null, 2)); }
68
+ async function exists(p) { return access(p).then(() => true).catch(() => false); }
47
69
 
48
70
  /**
49
- * Writes a default claude-code-router config pointing at Pollinations
50
- * (free, no-API-key). Backs up any existing user config before overwriting.
71
+ * Generates ~/.claude-code-router/config.json:
72
+ * - If user has run setup-gemini Gemini first, Pollinations as fallback Provider
73
+ * - Else → Pollinations only
51
74
  *
52
- * Returns { mode: 'anthropic-direct' } so launch.js stays generic.
75
+ * Returns { withGemini: boolean }.
53
76
  */
54
- export async function ensurePreset(/* hub unused in CCR mode */) {
77
+ export async function ensurePreset() {
55
78
  await mkdir(CCR_DIR, { recursive: true });
56
79
  const state = await readState();
80
+ const withGemini = await isGeminiConfigured();
81
+ const config = buildConfig({ withGemini });
57
82
 
58
83
  if (await exists(CCR_CONFIG)) {
59
84
  let existing;
@@ -69,16 +94,10 @@ export async function ensurePreset(/* hub unused in CCR mode */) {
69
94
  await writeState(state);
70
95
  console.log(`💾 Найден свой config.json у claude-code-router — сохранил резервную копию: ${backupPath}`);
71
96
  }
72
-
73
- if (isOurs) {
74
- // Already managed by us; rewrite each time so updates roll out.
75
- await writeFile(CCR_CONFIG, JSON.stringify(FREE_CONFIG, null, 2));
76
- return { mode: 'anthropic-direct' };
77
- }
78
97
  }
79
98
 
80
- await writeFile(CCR_CONFIG, JSON.stringify(FREE_CONFIG, null, 2));
81
- return { mode: 'anthropic-direct' };
99
+ await writeFile(CCR_CONFIG, JSON.stringify(config, null, 2));
100
+ return { withGemini };
82
101
  }
83
102
 
84
- export const CCR_PORT = FREE_CONFIG.PORT;
103
+ export const CCR_PORT = 3456;
@@ -0,0 +1,169 @@
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
+
7
+ const ROOT = join(homedir(), '.krasavacode');
8
+ const ENV_FILE = join(ROOT, 'gemini.env');
9
+ const STATE_FILE = join(ROOT, 'state.json');
10
+
11
+ const CONSOLE_URL = 'https://aistudio.google.com/apikey';
12
+
13
+ function openBrowser(url) {
14
+ const cmd = platform() === 'darwin' ? 'open'
15
+ : platform() === 'win32' ? 'start'
16
+ : 'xdg-open';
17
+ const args = platform() === 'win32' ? ['', url] : [url];
18
+ spawn(cmd, args, { detached: true, stdio: 'ignore', shell: platform() === 'win32' }).unref();
19
+ }
20
+
21
+ function readState() {
22
+ return readFile(STATE_FILE, 'utf8').then(JSON.parse).catch(() => ({}));
23
+ }
24
+ async function writeState(s) { await writeFile(STATE_FILE, JSON.stringify(s, null, 2)); }
25
+
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
+ function isValidKeyFormat(key) {
32
+ // Google API keys look like AIza followed by 35 chars [A-Za-z0-9_-]
33
+ return /^AIza[A-Za-z0-9_-]{35}$/.test(key);
34
+ }
35
+
36
+ /** Sanity-check the key with a tiny Gemini API call. */
37
+ async function verifyKey(key) {
38
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${encodeURIComponent(key)}`;
39
+ 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
+ });
49
+ const ms = Date.now() - t0;
50
+ if (!res.ok) {
51
+ let msg = `HTTP ${res.status}`;
52
+ try { const j = await res.json(); msg = j.error?.message || msg; } catch {}
53
+ return { ok: false, error: msg, ms };
54
+ }
55
+ const data = await res.json();
56
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
57
+ return { ok: true, text: text.trim(), ms };
58
+ }
59
+
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('');
67
+ }
68
+
69
+ export async function runSetupGemini() {
70
+ await mkdir(ROOT, { recursive: true });
71
+
72
+ console.log('');
73
+ console.log(' ╔══════════════════════════════════════════════════╗');
74
+ console.log(' ║ KRASAVACODE — апгрейд на Gemini 2.5 Flash ║');
75
+ console.log(' ╚══════════════════════════════════════════════════╝');
76
+ console.log('');
77
+ console.log(' Сейчас вайбкодинг работает на простой модели.');
78
+ console.log(' Подключив бесплатный Google Gemini, получишь:');
79
+ console.log(' ✓ Качество в разы выше');
80
+ console.log(' ✓ 1500 запросов в день бесплатно');
81
+ console.log(' ✓ Без банковской карты');
82
+ console.log('');
83
+ console.log(' Это займёт 60 секунд.');
84
+
85
+ header('Открываю в браузере: ' + CONSOLE_URL);
86
+ openBrowser(CONSOLE_URL);
87
+
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('');
100
+
101
+ let key;
102
+ for (let attempt = 0; attempt < 3; attempt++) {
103
+ 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;
115
+ }
116
+ if (!isValidKeyFormat(key)) {
117
+ console.log('\n ❌ Не удалось получить ключ. Запусти `krasavacode setup-gemini` ещё раз.');
118
+ return;
119
+ }
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;
129
+ }
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('');
149
+
150
+ const launch = (await prompt(' Запустить вайбкодинг прямо сейчас? [Enter — да, n — позже]: ')).trim().toLowerCase();
151
+ if (launch === '' || launch === 'y' || launch === 'yes' || launch === 'д' || launch === 'да') {
152
+ return { launchAfter: true };
153
+ }
154
+ console.log('\n ОК. Когда захочешь — запусти `krasavacode`.');
155
+ return { launchAfter: false };
156
+ }
157
+
158
+ /** Read GEMINI_API_KEY from gemini.env. Returns null if not configured. */
159
+ export async function loadGeminiKey() {
160
+ try {
161
+ const content = await readFile(ENV_FILE, 'utf8');
162
+ const m = content.match(/^GEMINI_API_KEY=(.+)$/m);
163
+ return m ? m[1].trim() : null;
164
+ } catch { return null; }
165
+ }
166
+
167
+ export async function isGeminiConfigured() {
168
+ return access(ENV_FILE).then(() => true).catch(() => false);
169
+ }