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 +86 -55
- package/bin/krasavacode.js +7 -1
- package/package.json +1 -1
- package/src/hub.js +8 -1
- package/src/launch.js +9 -1
- package/src/preset.js +64 -45
- package/src/setup-gemini.js +169 -0
package/README.md
CHANGED
|
@@ -1,85 +1,116 @@
|
|
|
1
1
|
# KRASAVACODE
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Бесплатный вайбкодинг через ИИ. Ставится двумя кликами, без регистрации, без карты.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 🍏 У меня Mac
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
npx krasavacode
|
|
11
|
-
```
|
|
9
|
+
**Шаг 1.** Скачай файл-установщик:
|
|
12
10
|
|
|
13
|
-
|
|
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
|
-
**
|
|
30
|
-
```
|
|
31
|
-
krasavacode.exe
|
|
32
|
-
```
|
|
33
|
-
Если выскочит «Windows protected your PC» → жми **More info** → **Run anyway**.
|
|
21
|
+
**Шаг 4.** Откроется чёрное окно (это нормально — это Терминал). Жди ≈ минуту, оно само скачает программу. В конце нажми **Enter**.
|
|
34
22
|
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
```bash
|
|
44
|
-
chmod +x krasavacode-linux-x64
|
|
45
|
-
./krasavacode-linux-x64
|
|
46
|
-
```
|
|
25
|
+
---
|
|
47
26
|
|
|
48
|
-
|
|
27
|
+
## 🪟 У меня Windows
|
|
49
28
|
|
|
50
|
-
|
|
29
|
+
**Шаг 1.** Скачай файл-установщик:
|
|
51
30
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
Работает на любой ОС с Node.js 20+. Если Node старше 20 или вообще нет — CLI сам подтянет нужный. Источники: https://github.com/alexrexby/krasavacode
|
|
111
|
+
|
|
112
|
+
---
|
|
82
113
|
|
|
83
114
|
## Лицензия
|
|
84
115
|
|
|
85
|
-
MIT.
|
|
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). Их условия использования — на их сайтах.
|
package/bin/krasavacode.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
45
|
-
return
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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 {
|
|
75
|
+
* Returns { withGemini: boolean }.
|
|
53
76
|
*/
|
|
54
|
-
export async function ensurePreset(
|
|
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(
|
|
81
|
-
return {
|
|
99
|
+
await writeFile(CCR_CONFIG, JSON.stringify(config, null, 2));
|
|
100
|
+
return { withGemini };
|
|
82
101
|
}
|
|
83
102
|
|
|
84
|
-
export const CCR_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
|
+
}
|