krasavacode 0.3.0 → 0.3.2
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 +16 -10
- package/bin/krasavacode.js +1 -1
- package/package.json +1 -1
- package/src/launch.js +28 -10
- package/src/setup-gemini.js +357 -99
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
94
|
+
С этого момента значок **«ВАЙБКОДИНГ»** работает уже на Gemini. Качество — ощутимо выше.
|
|
89
95
|
|
|
90
96
|
---
|
|
91
97
|
|
package/bin/krasavacode.js
CHANGED
|
@@ -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.
|
|
11
|
+
const VERSION = '0.3.2';
|
|
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.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway. Сам ставит Node при необходимости.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/launch.js
CHANGED
|
@@ -6,32 +6,50 @@ const PLACEHOLDER_TOKEN = 'sk-krasavacode-local';
|
|
|
6
6
|
export async function launchClaude(paths, hub /*, detection */) {
|
|
7
7
|
const geminiOn = await isGeminiConfigured();
|
|
8
8
|
|
|
9
|
+
// Drop any pre-existing ANTHROPIC_API_KEY (from the user's shell or a real
|
|
10
|
+
// Anthropic login) so it doesn't conflict with our auth-token, and so that
|
|
11
|
+
// Claude Code's welcome screen doesn't show the user's real Anthropic org.
|
|
12
|
+
const cleanEnv = { ...process.env };
|
|
13
|
+
delete cleanEnv.ANTHROPIC_API_KEY;
|
|
14
|
+
delete cleanEnv.ANTHROPIC_VERTEX_PROJECT_ID;
|
|
15
|
+
delete cleanEnv.ANTHROPIC_BEDROCK_BASE_URL;
|
|
16
|
+
|
|
9
17
|
const env = {
|
|
10
|
-
...
|
|
18
|
+
...cleanEnv,
|
|
11
19
|
ANTHROPIC_BASE_URL: hub.baseUrl,
|
|
12
20
|
ANTHROPIC_AUTH_TOKEN: PLACEHOLDER_TOKEN,
|
|
13
|
-
ANTHROPIC_API_KEY: PLACEHOLDER_TOKEN,
|
|
14
21
|
DISABLE_AUTOUPDATER: '1',
|
|
15
22
|
DISABLE_TELEMETRY: '1',
|
|
16
23
|
DISABLE_ERROR_REPORTING: '1',
|
|
17
24
|
// Tell Claude Code which model to ask for. CCR will route any of these
|
|
18
|
-
// to Pollinations via Router.default.
|
|
25
|
+
// to Pollinations / Gemini via Router.default.
|
|
19
26
|
ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-5',
|
|
20
27
|
};
|
|
21
28
|
|
|
22
29
|
const passthroughArgs = process.argv.slice(2).filter(a => !['doctor', 'upgrade'].includes(a));
|
|
23
30
|
|
|
31
|
+
const W = 64;
|
|
32
|
+
const line = (txt) => {
|
|
33
|
+
const pad = Math.max(0, W - 2 - [...txt].length);
|
|
34
|
+
return '┃ ' + txt + ' '.repeat(pad) + '┃';
|
|
35
|
+
};
|
|
24
36
|
console.log('');
|
|
25
|
-
console.log('━'.repeat(
|
|
26
|
-
console.log('
|
|
37
|
+
console.log('┏' + '━'.repeat(W - 1) + '┓');
|
|
38
|
+
console.log(line(' K R A S A V A C O D E'));
|
|
39
|
+
console.log(line(' Бесплатный вайбкодинг через локальный hub'));
|
|
40
|
+
console.log('┣' + '━'.repeat(W - 1) + '┫');
|
|
27
41
|
if (geminiOn) {
|
|
28
|
-
console.log(' Модель: Google Gemini 2.5 Flash
|
|
42
|
+
console.log(line(' ✓ Модель: Google Gemini 2.5 Flash'));
|
|
43
|
+
console.log(line(' (1500 запросов в день, бесплатно)'));
|
|
29
44
|
} else {
|
|
30
|
-
console.log(' Модель: gpt-oss-20b через Pollinations
|
|
31
|
-
console.log('
|
|
45
|
+
console.log(line(' · Модель: gpt-oss-20b через Pollinations'));
|
|
46
|
+
console.log(line(' (бесплатно, без логина)'));
|
|
47
|
+
console.log(line(' 💡 Лучше модель: krasavacode setup-gemini'));
|
|
32
48
|
}
|
|
33
|
-
console.log('
|
|
34
|
-
console.log('
|
|
49
|
+
console.log('┗' + '━'.repeat(W - 1) + '┛');
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(' Дальше открывается Claude Code от Anthropic — это');
|
|
52
|
+
console.log(' его экран приветствия, не наш. Просто пиши задачу.');
|
|
35
53
|
console.log('');
|
|
36
54
|
|
|
37
55
|
// Merge env from runtime (PATH with bundled Node when applicable) with our overrides
|
package/src/setup-gemini.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
57
|
+
const text = (data.candidates?.[0]?.content?.parts?.[0]?.text || '').trim();
|
|
58
|
+
return { ok: true, text, ms };
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 —
|
|
344
|
+
console.log(' ║ KRASAVACODE — подключаем Google Gemini ║');
|
|
75
345
|
console.log(' ╚══════════════════════════════════════════════════╝');
|
|
76
346
|
console.log('');
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log('');
|
|
97
|
-
console.log(
|
|
98
|
-
console.log('
|
|
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
|
|
384
|
+
for (let i = 0; i < 3; i++) {
|
|
103
385
|
key = (await prompt(' Вставь ключ Gemini сюда: ')).trim();
|
|
104
|
-
if (
|
|
105
|
-
|
|
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 ❌ Не удалось получить ключ. Запусти
|
|
118
|
-
return;
|
|
390
|
+
console.log('\n ❌ Не удалось получить ключ. Запусти команду ещё раз позже.');
|
|
391
|
+
return { launchAfter: false, configured: false };
|
|
119
392
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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');
|