krasavacode 0.1.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 ADDED
@@ -0,0 +1,64 @@
1
+ # KRASAVACODE
2
+
3
+ Однокнопочный бесплатный вайбкодинг на Claude Code — для учеников.
4
+
5
+ ## Установка и запуск
6
+
7
+ **Один способ:**
8
+
9
+ ```bash
10
+ npx krasavacode
11
+ ```
12
+
13
+ Всё. Pollinations работает без логина и без карты, лимитов хватит на 2–5 учебных MVP.
14
+
15
+ При первой команде ставится локальный gateway и Claude Code (один раз, ~30 сек). Потом запуск моментальный.
16
+
17
+ ## Если хочешь больше моделей
18
+
19
+ ```bash
20
+ npx krasavacode upgrade
21
+ ```
22
+
23
+ Откроется дашборд в браузере. Там одним кликом подключишь:
24
+ - **Kiro AI** — Claude Sonnet/Haiku через AWS Builder ID
25
+ - **Qoder** — Kimi K2 / Qwen3-Coder / DeepSeek-R1
26
+ - **Qwen Code** — 4 модели Alibaba
27
+ - **LongCat** — 50M токенов в день
28
+
29
+ Всё бесплатно, без карты.
30
+
31
+ ## Если что-то не работает
32
+
33
+ ```bash
34
+ npx krasavacode doctor
35
+ ```
36
+
37
+ Покажет, что сломано. Самые частые проблемы:
38
+ - **Старый Node.js** → обнови до 20+ с https://nodejs.org
39
+ - **Порт занят** → перезапусти терминал
40
+ - **Корпоративный прокси/Россия/Китай** → запусти upgrade и в дашборде включи SOCKS5
41
+
42
+ ## Что под капотом
43
+
44
+ ```
45
+ krasavacode → локальный OmniRoute → Pollinations / Kiro / Qoder / …
46
+ (free gateway, MIT) (free providers без карты)
47
+ ```
48
+
49
+ CLI = `@anthropic-ai/claude-code` с подменённым backend. Все обновления Claude Code от Anthropic прилетают автоматом.
50
+
51
+ ## Бинарники без Node.js
52
+
53
+ Для уроков с непрофильными студентами — скачай готовый бинарник со страницы релиза:
54
+ - `krasavacode.exe` (Windows)
55
+ - `krasavacode-mac-arm64`, `krasavacode-mac-x64` (macOS)
56
+ - `krasavacode-linux-x64` (Linux)
57
+
58
+ Дабл-клик → откроется терминал с Claude Code. Node.js не нужен.
59
+
60
+ > Замечание: бинарники тащат portable Node при первом запуске (~30 МБ, один раз). После — работают офлайн.
61
+
62
+ ## Лицензия
63
+
64
+ MIT. Pollinations / Kiro AI / Qoder / другие провайдеры имеют свои ToS — читай их условия. На свой риск.
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { ensureRuntime } from '../src/runtime.js';
3
+ import { startHub, stopHub } from '../src/hub.js';
4
+ import { ensurePreset } from '../src/preset.js';
5
+ import { launchClaude } from '../src/launch.js';
6
+ import { runUpgrade } from '../src/upgrade.js';
7
+ import { runDoctor } from '../src/doctor.js';
8
+
9
+ const cmd = process.argv[2];
10
+
11
+ async function main() {
12
+ if (cmd === 'doctor') return runDoctor();
13
+ if (cmd === 'upgrade') return runUpgrade();
14
+ if (cmd === '--version' || cmd === '-v') {
15
+ const { readFile } = await import('node:fs/promises');
16
+ const { fileURLToPath } = await import('node:url');
17
+ const { dirname, join } = await import('node:path');
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ const pkg = JSON.parse(await readFile(join(here, '..', 'package.json'), 'utf8'));
20
+ console.log(`KRASAVACODE v${pkg.version}`);
21
+ return;
22
+ }
23
+
24
+ const paths = await ensureRuntime();
25
+ await ensurePreset();
26
+ const hub = await startHub(paths);
27
+
28
+ process.on('SIGINT', () => stopHub(hub).then(() => process.exit(0)));
29
+ process.on('SIGTERM', () => stopHub(hub).then(() => process.exit(0)));
30
+
31
+ await launchClaude(paths, hub);
32
+
33
+ await stopHub(hub);
34
+ }
35
+
36
+ main().catch(err => {
37
+ console.error('\n❌ KRASAVACODE упал:', err.message);
38
+ if (process.env.KRASAVACODE_DEBUG) console.error(err.stack);
39
+ console.error('\nЗапусти `krasavacode doctor` для диагностики.');
40
+ process.exit(1);
41
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "Krasava-Free",
3
+ "description": "Дефолтный preset KRASAVACODE: бесплатные провайдеры без карты, в порядке предпочтения.",
4
+ "providers": [
5
+ { "id": "pollinations", "auth": "none", "models": ["openai-large", "claude", "deepseek"] },
6
+ { "id": "longcat", "auth": "none", "models": ["LongCat-Flash-Lite"] },
7
+ { "id": "kiro", "auth": "oauth-aws-builder-id", "models": ["claude-sonnet-4.5", "claude-haiku-4.5"] },
8
+ { "id": "qoder", "auth": "oauth-google", "models": ["kimi-k2-thinking", "qwen3-coder-plus", "deepseek-r1"] },
9
+ { "id": "qwen-code", "auth": "oauth-alibaba", "models": ["qwen3-coder-plus"] }
10
+ ],
11
+ "compression": "stacked",
12
+ "notes": "v0.1 этот файл — справочный. v0.2 будет инжектить combo через REST API OmniRoute при первом запуске."
13
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "krasavacode",
3
+ "version": "0.1.0",
4
+ "description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway.",
5
+ "type": "module",
6
+ "bin": {
7
+ "krasavacode": "bin/krasavacode.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "config",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20.0.0"
17
+ },
18
+ "scripts": {
19
+ "start": "node bin/krasavacode.js",
20
+ "build:binaries": "bun build --compile --target=bun-darwin-arm64 ./bin/krasavacode.js --outfile dist/krasavacode-mac-arm64 && bun build --compile --target=bun-darwin-x64 ./bin/krasavacode.js --outfile dist/krasavacode-mac-x64 && bun build --compile --target=bun-windows-x64 ./bin/krasavacode.js --outfile dist/krasavacode.exe && bun build --compile --target=bun-linux-x64 ./bin/krasavacode.js --outfile dist/krasavacode-linux-x64"
21
+ },
22
+ "keywords": [
23
+ "claude-code",
24
+ "ai",
25
+ "cli",
26
+ "vibecoding",
27
+ "free",
28
+ "education"
29
+ ],
30
+ "license": "MIT"
31
+ }
package/src/doctor.js ADDED
@@ -0,0 +1,75 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { homedir, platform, arch, totalmem } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { access, readFile } from 'node:fs/promises';
5
+ import net from 'node:net';
6
+
7
+ const ROOT = join(homedir(), '.krasavacode');
8
+
9
+ function spawnCmd(cmd, args) {
10
+ return new Promise(resolve => {
11
+ const c = spawn(cmd, args, { stdio: 'pipe' });
12
+ let out = '';
13
+ c.stdout.on('data', d => out += d);
14
+ c.stderr.on('data', d => out += d);
15
+ c.on('error', () => resolve({ ok: false, out: 'not found' }));
16
+ c.on('exit', code => resolve({ ok: code === 0, out: out.trim() }));
17
+ });
18
+ }
19
+
20
+ async function checkPort(port) {
21
+ return new Promise(resolve => {
22
+ const srv = net.createServer();
23
+ srv.once('error', () => resolve(false));
24
+ srv.once('listening', () => srv.close(() => resolve(true)));
25
+ srv.listen(port, '127.0.0.1');
26
+ });
27
+ }
28
+
29
+ async function checkNetwork(url) {
30
+ try {
31
+ const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
32
+ return res.ok || (res.status >= 200 && res.status < 500);
33
+ } catch { return false; }
34
+ }
35
+
36
+ const check = (label, ok, detail) =>
37
+ console.log(` ${ok ? '✅' : '❌'} ${label}${detail ? ' — ' + detail : ''}`);
38
+
39
+ export async function runDoctor() {
40
+ console.log('🩺 KRASAVACODE doctor\n');
41
+
42
+ console.log('Система:');
43
+ console.log(` ${platform()}/${arch()} RAM ${(totalmem() / 1024 / 1024 / 1024).toFixed(1)} GB`);
44
+ console.log(` Node ${process.version}\n`);
45
+
46
+ console.log('Бинарники:');
47
+ const npm = await spawnCmd('npm', ['--version']);
48
+ check('npm', npm.ok, npm.out);
49
+
50
+ const ext = platform() === 'win32' ? '.cmd' : '';
51
+ const claudeBin = join(ROOT, 'runtime', 'node_modules', '.bin', 'claude' + ext);
52
+ const ccrBin = join(ROOT, 'runtime', 'node_modules', '.bin', 'ccr' + ext);
53
+ const haveClaude = await access(claudeBin).then(() => true).catch(() => false);
54
+ const haveCcr = await access(ccrBin).then(() => true).catch(() => false);
55
+ check('claude (runtime)', haveClaude, haveClaude ? claudeBin : 'не установлен — будет поставлен при `krasavacode`');
56
+ check('ccr / claude-code-router (runtime)', haveCcr, haveCcr ? ccrBin : 'не установлен — будет поставлен при `krasavacode`');
57
+
58
+ console.log('\nСеть:');
59
+ check('npm registry', await checkNetwork('https://registry.npmjs.org/'));
60
+ check('Pollinations', await checkNetwork('https://text.pollinations.ai/openai/chat/completions'));
61
+
62
+ console.log('\nПорты:');
63
+ check('3456 (claude-code-router)', await checkPort(3456), 'свободен или используется ccr');
64
+ check('20128 (omniroute upgrade)', await checkPort(20128));
65
+
66
+ console.log('\nState:');
67
+ try {
68
+ const state = JSON.parse(await readFile(join(ROOT, 'state.json'), 'utf8'));
69
+ console.log(JSON.stringify(state, null, 2).split('\n').map(l => ' ' + l).join('\n'));
70
+ } catch {
71
+ console.log(' пусто (запусти `krasavacode` хотя бы раз)');
72
+ }
73
+
74
+ console.log('');
75
+ }
package/src/hub.js ADDED
@@ -0,0 +1,97 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { setTimeout as sleep } from 'node:timers/promises';
3
+ import { CCR_PORT } from './preset.js';
4
+
5
+ const HOST = '127.0.0.1';
6
+ const PORT = CCR_PORT;
7
+
8
+ async function probe() {
9
+ const url = `http://${HOST}:${PORT}/`;
10
+ try {
11
+ const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
12
+ // CCR returns any 2xx/3xx/4xx — any HTTP response means it's listening
13
+ return res.status !== undefined;
14
+ } catch { return false; }
15
+ }
16
+
17
+ async function isAlreadyRunning() {
18
+ return await probe();
19
+ }
20
+
21
+ async function waitForHealthy(timeoutMs) {
22
+ const deadline = Date.now() + timeoutMs;
23
+ while (Date.now() < deadline) {
24
+ if (await probe()) return true;
25
+ await sleep(500);
26
+ }
27
+ return false;
28
+ }
29
+
30
+ export async function startHub(paths) {
31
+ const baseUrl = `http://${HOST}:${PORT}`;
32
+
33
+ // CCR is a singleton: one server per machine on port 3456. If something
34
+ // is already there, hope it's a healthy CCR — don't start a second one.
35
+ if (await isAlreadyRunning()) {
36
+ return { process: null, port: PORT, baseUrl, ownedByUs: false };
37
+ }
38
+
39
+ process.stdout.write(`🚀 Поднимаю локальный gateway на порту ${PORT}… `);
40
+
41
+ const child = spawn(paths.ccrBin, ['start'], {
42
+ stdio: process.env.KRASAVACODE_DEBUG ? 'inherit' : 'pipe',
43
+ detached: false,
44
+ });
45
+
46
+ let stderrTail = '';
47
+ if (child.stderr) {
48
+ child.stderr.on('data', d => {
49
+ stderrTail = (stderrTail + d.toString()).slice(-2000);
50
+ });
51
+ }
52
+ if (child.stdout) {
53
+ child.stdout.on('data', d => {
54
+ stderrTail = (stderrTail + d.toString()).slice(-2000);
55
+ });
56
+ }
57
+
58
+ let exitedEarly = false;
59
+ child.once('exit', code => {
60
+ exitedEarly = true;
61
+ if (code !== 0 && code !== null) {
62
+ console.error(`\n❌ ccr упал с кодом ${code}.`);
63
+ }
64
+ });
65
+
66
+ const ok = await waitForHealthy(30000);
67
+
68
+ if (!ok) {
69
+ if (!child.killed) child.kill('SIGTERM');
70
+ if (stderrTail) console.error('\n--- ccr output ---\n' + stderrTail);
71
+ throw new Error(`claude-code-router не поднялся за 30s на ${baseUrl}`);
72
+ }
73
+
74
+ console.log('OK');
75
+
76
+ if (exitedEarly) {
77
+ // Maybe CCR daemonised; check if endpoint is alive
78
+ if (!(await probe())) {
79
+ throw new Error('ccr процесс завершился, но порт не отвечает');
80
+ }
81
+ }
82
+
83
+ return { process: child, port: PORT, baseUrl, ownedByUs: true };
84
+ }
85
+
86
+ export async function stopHub(hub) {
87
+ if (!hub) return;
88
+ if (!hub.ownedByUs) return; // we didn't start it; leave it running for the user
89
+ if (!hub.process || hub.process.killed) return;
90
+
91
+ hub.process.kill('SIGTERM');
92
+ await Promise.race([
93
+ new Promise(r => hub.process.once('exit', r)),
94
+ sleep(5000),
95
+ ]);
96
+ if (!hub.process.killed) hub.process.kill('SIGKILL');
97
+ }
package/src/launch.js ADDED
@@ -0,0 +1,38 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ const PLACEHOLDER_TOKEN = 'sk-krasavacode-local';
4
+
5
+ export async function launchClaude(paths, hub /*, detection */) {
6
+ const env = {
7
+ ...process.env,
8
+ ANTHROPIC_BASE_URL: hub.baseUrl,
9
+ ANTHROPIC_AUTH_TOKEN: PLACEHOLDER_TOKEN,
10
+ ANTHROPIC_API_KEY: PLACEHOLDER_TOKEN,
11
+ DISABLE_AUTOUPDATER: '1',
12
+ DISABLE_TELEMETRY: '1',
13
+ DISABLE_ERROR_REPORTING: '1',
14
+ // Tell Claude Code which model to ask for. CCR will route any of these
15
+ // to Pollinations via Router.default.
16
+ ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-5',
17
+ };
18
+
19
+ const passthroughArgs = process.argv.slice(2).filter(a => !['doctor', 'upgrade'].includes(a));
20
+
21
+ console.log('');
22
+ console.log('━'.repeat(58));
23
+ console.log(' KRASAVACODE — вайбкодинг через локальный hub');
24
+ console.log(' Hub: ' + hub.baseUrl + ' (claude-code-router → Pollinations)');
25
+ console.log(' Пиши задачу обычным языком, ИИ сделает.');
26
+ console.log('━'.repeat(58));
27
+ console.log('');
28
+
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(paths.claudeBin, passthroughArgs, {
31
+ env,
32
+ stdio: 'inherit',
33
+ });
34
+
35
+ child.on('error', reject);
36
+ child.on('exit', () => resolve());
37
+ });
38
+ }
package/src/preset.js ADDED
@@ -0,0 +1,84 @@
1
+ import { mkdir, readFile, writeFile, copyFile, access } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join, dirname } from 'node:path';
4
+
5
+ const CCR_DIR = join(homedir(), '.claude-code-router');
6
+ const CCR_CONFIG = join(CCR_DIR, 'config.json');
7
+ const STATE_FILE = join(homedir(), '.krasavacode', 'state.json');
8
+
9
+ const KRASAVACODE_MARKER = 'krasavacode/managed';
10
+
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
+ };
35
+
36
+ async function readState() {
37
+ try { return JSON.parse(await readFile(STATE_FILE, 'utf8')); }
38
+ catch { return {}; }
39
+ }
40
+ async function writeState(s) {
41
+ await writeFile(STATE_FILE, JSON.stringify(s, null, 2));
42
+ }
43
+
44
+ async function exists(p) {
45
+ return access(p).then(() => true).catch(() => false);
46
+ }
47
+
48
+ /**
49
+ * Writes a default claude-code-router config pointing at Pollinations
50
+ * (free, no-API-key). Backs up any existing user config before overwriting.
51
+ *
52
+ * Returns { mode: 'anthropic-direct' } so launch.js stays generic.
53
+ */
54
+ export async function ensurePreset(/* hub unused in CCR mode */) {
55
+ await mkdir(CCR_DIR, { recursive: true });
56
+ const state = await readState();
57
+
58
+ if (await exists(CCR_CONFIG)) {
59
+ let existing;
60
+ try { existing = JSON.parse(await readFile(CCR_CONFIG, 'utf8')); }
61
+ catch { existing = null; }
62
+
63
+ const isOurs = existing?._krasavacode === KRASAVACODE_MARKER;
64
+
65
+ if (!isOurs && !state.userConfigBackedUp) {
66
+ const backupPath = `${CCR_CONFIG}.backup-${Date.now()}`;
67
+ await copyFile(CCR_CONFIG, backupPath);
68
+ state.userConfigBackedUp = backupPath;
69
+ await writeState(state);
70
+ console.log(`💾 Найден свой config.json у claude-code-router — сохранил резервную копию: ${backupPath}`);
71
+ }
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
+ }
79
+
80
+ await writeFile(CCR_CONFIG, JSON.stringify(FREE_CONFIG, null, 2));
81
+ return { mode: 'anthropic-direct' };
82
+ }
83
+
84
+ export const CCR_PORT = FREE_CONFIG.PORT;
package/src/runtime.js ADDED
@@ -0,0 +1,112 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, access, readFile, writeFile } from 'node:fs/promises';
3
+ import { homedir, platform } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ const ROOT = join(homedir(), '.krasavacode');
7
+ const RUNTIME = join(ROOT, 'runtime');
8
+ const STATE_FILE = join(ROOT, 'state.json');
9
+
10
+ const REQUIRED_PACKAGES = [
11
+ '@anthropic-ai/claude-code@latest',
12
+ '@musistudio/claude-code-router@latest',
13
+ ];
14
+
15
+ function exists(path) {
16
+ return access(path).then(() => true).catch(() => false);
17
+ }
18
+
19
+ async function readState() {
20
+ try {
21
+ return JSON.parse(await readFile(STATE_FILE, 'utf8'));
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ async function writeState(state) {
28
+ await writeFile(STATE_FILE, JSON.stringify(state, null, 2));
29
+ }
30
+
31
+ function spawnCmd(cmd, args, opts = {}) {
32
+ return new Promise((resolve, reject) => {
33
+ const child = spawn(cmd, args, { stdio: opts.silent ? 'pipe' : 'inherit', ...opts });
34
+ let stdout = '', stderr = '';
35
+ if (opts.silent) {
36
+ child.stdout.on('data', d => { stdout += d; });
37
+ child.stderr.on('data', d => { stderr += d; });
38
+ }
39
+ child.on('error', reject);
40
+ child.on('exit', code => {
41
+ if (code === 0) resolve({ stdout, stderr });
42
+ else reject(new Error(`${cmd} exited with code ${code}: ${stderr}`));
43
+ });
44
+ });
45
+ }
46
+
47
+ async function ensureNpm() {
48
+ try {
49
+ await spawnCmd('npm', ['--version'], { silent: true });
50
+ } catch {
51
+ throw new Error(
52
+ 'npm не найден в PATH. Установи Node.js (≥20) с https://nodejs.org и перезапусти терминал.'
53
+ );
54
+ }
55
+ }
56
+
57
+ async function installPackages() {
58
+ console.log('📦 Устанавливаю компоненты (один раз)…');
59
+ await mkdir(RUNTIME, { recursive: true });
60
+
61
+ const pkgPath = join(RUNTIME, 'package.json');
62
+ if (!(await exists(pkgPath))) {
63
+ await writeFile(pkgPath, JSON.stringify({
64
+ name: 'krasavacode-runtime',
65
+ private: true,
66
+ version: '0.0.0',
67
+ dependencies: {},
68
+ }, null, 2));
69
+ }
70
+
71
+ await spawnCmd('npm', [
72
+ 'install',
73
+ '--prefix', RUNTIME,
74
+ '--no-audit',
75
+ '--no-fund',
76
+ '--loglevel=error',
77
+ ...REQUIRED_PACKAGES,
78
+ ]);
79
+ }
80
+
81
+ function binPath(name) {
82
+ const ext = platform() === 'win32' ? '.cmd' : '';
83
+ return join(RUNTIME, 'node_modules', '.bin', name + ext);
84
+ }
85
+
86
+ export async function ensureRuntime() {
87
+ await ensureNpm();
88
+ await mkdir(ROOT, { recursive: true });
89
+
90
+ const state = await readState();
91
+ const ccrBin = binPath('ccr');
92
+ const claudeBin = binPath('claude');
93
+
94
+ const haveCcr = await exists(ccrBin);
95
+ const haveClaude = await exists(claudeBin);
96
+
97
+ if (!haveCcr || !haveClaude || !state.installedAt) {
98
+ await installPackages();
99
+ state.installedAt = new Date().toISOString();
100
+ await writeState(state);
101
+ console.log('✅ Готово.\n');
102
+ }
103
+
104
+ return {
105
+ root: ROOT,
106
+ runtime: RUNTIME,
107
+ ccrBin,
108
+ claudeBin,
109
+ state,
110
+ saveState: writeState,
111
+ };
112
+ }
package/src/upgrade.js ADDED
@@ -0,0 +1,64 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, access, writeFile } from 'node:fs/promises';
3
+ import { homedir, platform } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ const ROOT = join(homedir(), '.krasavacode');
7
+ const RUNTIME = join(ROOT, 'runtime');
8
+
9
+ function binPath(name) {
10
+ const ext = platform() === 'win32' ? '.cmd' : '';
11
+ return join(RUNTIME, 'node_modules', '.bin', name + ext);
12
+ }
13
+
14
+ function exists(p) { return access(p).then(() => true).catch(() => false); }
15
+
16
+ function spawnP(cmd, args, opts = {}) {
17
+ return new Promise((resolve, reject) => {
18
+ const c = spawn(cmd, args, { stdio: 'inherit', ...opts });
19
+ c.on('error', reject);
20
+ c.on('exit', code => code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`)));
21
+ });
22
+ }
23
+
24
+ function openBrowser(url) {
25
+ const cmd = platform() === 'darwin' ? 'open'
26
+ : platform() === 'win32' ? 'start'
27
+ : 'xdg-open';
28
+ const args = platform() === 'win32' ? ['', url] : [url];
29
+ spawn(cmd, args, { detached: true, stdio: 'ignore', shell: platform() === 'win32' }).unref();
30
+ }
31
+
32
+ export async function runUpgrade() {
33
+ console.log('🔧 KRASAVACODE upgrade');
34
+ console.log(' Pollinations покрывает первые шаги, но для серьёзных проектов');
35
+ console.log(' стоит подключить более мощные модели бесплатно через OmniRoute:');
36
+ console.log(' • Kiro AI — Claude Sonnet/Haiku через AWS Builder ID');
37
+ console.log(' • Qoder — Kimi K2 / Qwen3-Coder / DeepSeek-R1');
38
+ console.log(' • Qwen Code — 4 модели Alibaba');
39
+ console.log(' • LongCat — 50M токенов в день\n');
40
+
41
+ const omniBin = binPath('omniroute');
42
+ if (!(await exists(omniBin))) {
43
+ console.log('📦 Ставлю omniroute (один раз)…');
44
+ await mkdir(RUNTIME, { recursive: true });
45
+ const pkgPath = join(RUNTIME, 'package.json');
46
+ if (!(await exists(pkgPath))) {
47
+ await writeFile(pkgPath, JSON.stringify({ name: 'krasavacode-runtime', private: true, version: '0.0.0' }, null, 2));
48
+ }
49
+ await spawnP('npm', ['install', '--prefix', RUNTIME, '--no-audit', '--no-fund', 'omniroute@latest']);
50
+ console.log('✅ Готово.\n');
51
+ }
52
+
53
+ console.log('🌐 Открываю дашборд OmniRoute в браузере: http://localhost:20128');
54
+ console.log(' В дашборде: Providers → Add → выбирай Kiro / Qoder / Pollinations.');
55
+ console.log(' После настройки нажми Ctrl+C тут, потом обычная команда `krasavacode`.\n');
56
+
57
+ setTimeout(() => openBrowser('http://localhost:20128'), 3000);
58
+
59
+ process.on('SIGINT', () => process.exit(0));
60
+
61
+ await spawnP(omniBin, ['--no-open'], {
62
+ env: { ...process.env, REQUIRE_API_KEY: 'false', DATA_DIR: ROOT },
63
+ });
64
+ }