krasavacode 0.2.2 → 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 +22 -0
- 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
|
@@ -67,6 +67,28 @@ chmod +x ~/krasavacode
|
|
|
67
67
|
|
|
68
68
|
---
|
|
69
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
|
+
|
|
70
92
|
## Что-то пошло не так?
|
|
71
93
|
|
|
72
94
|
| Проблема | Что делать |
|
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
|
+
}
|