krasavacode 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,23 +4,56 @@
4
4
 
5
5
  ## Установка и запуск
6
6
 
7
- **Один способ:**
7
+ ### Вариант А — у меня есть Node.js
8
8
 
9
9
  ```bash
10
10
  npx krasavacode
11
11
  ```
12
12
 
13
- Всё. Pollinations работает без логина и без карты, лимитов хватит на 2–5 учебных MVP.
13
+ Всё. Pollinations работает без логина и без карты. Если у тебя Node старее 20 — наш CLI сам подтянет нужную версию.
14
14
 
15
- При первой команде ставится локальный gateway и Claude Code (один раз, ~30 сек). Потом запуск моментальный.
15
+ ### Вариант Б у меня нет ничего
16
16
 
17
- ## Если хочешь больше моделей
17
+ Скачай готовый бинарник из последнего релиза:
18
+ **https://github.com/alexrexby/krasavacode/releases/latest**
19
+
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` |
26
+
27
+ После скачивания — открой терминал в папке со скачанным файлом и запусти:
28
+
29
+ **Windows:**
30
+ ```
31
+ krasavacode.exe
32
+ ```
33
+ Если выскочит «Windows protected your PC» → жми **More info** → **Run anyway**.
34
+
35
+ **macOS:**
36
+ ```bash
37
+ chmod +x krasavacode-mac-arm64
38
+ xattr -d com.apple.quarantine krasavacode-mac-arm64 # снимает блок Gatekeeper
39
+ ./krasavacode-mac-arm64
40
+ ```
41
+
42
+ **Linux:**
43
+ ```bash
44
+ chmod +x krasavacode-linux-x64
45
+ ./krasavacode-linux-x64
46
+ ```
47
+
48
+ При первом запуске бинарник скачает Node.js и Claude Code в `~/.krasavacode/` (≈100 МБ, один раз). Дальше — мгновенно.
49
+
50
+ ## Если хочется моделей помощнее
18
51
 
19
52
  ```bash
20
53
  npx krasavacode upgrade
21
54
  ```
22
55
 
23
- Откроется дашборд в браузере. Там одним кликом подключишь:
56
+ Откроется дашборд OmniRoute в браузере. Подключи одним кликом:
24
57
  - **Kiro AI** — Claude Sonnet/Haiku через AWS Builder ID
25
58
  - **Qoder** — Kimi K2 / Qwen3-Coder / DeepSeek-R1
26
59
  - **Qwen Code** — 4 модели Alibaba
@@ -34,31 +67,19 @@ npx krasavacode upgrade
34
67
  npx krasavacode doctor
35
68
  ```
36
69
 
37
- Покажет, что сломано. Самые частые проблемы:
38
- - **Старый Node.js**обнови до 20+ с https://nodejs.org
39
- - **Порт занят** → перезапусти терминал
40
- - **Корпоративный прокси/Россия/Китай** → запусти upgrade и в дашборде включи SOCKS5
70
+ Покажет что сломано. Самые частые проблемы:
71
+ - **Корпоративный прокси / Россия / Китай** запусти upgrade и в дашборде включи SOCKS5
72
+ - **Порт 3456 занят** → перезапусти терминал
41
73
 
42
74
  ## Что под капотом
43
75
 
44
76
  ```
45
- krasavacode → локальный OmniRoute → Pollinations / Kiro / Qoder / …
46
- (free gateway, MIT) (free providers без карты)
77
+ krasavacode → claude-code-router (порт 3456) → Pollinations
78
+ (Anthropic OpenAI bridge) (free, no API key)
47
79
  ```
48
80
 
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 МБ, один раз). После — работают офлайн.
81
+ CLI = `@anthropic-ai/claude-code` с подменённым `ANTHROPIC_BASE_URL`. Все обновления Claude Code от Anthropic прилетают автоматом.
61
82
 
62
83
  ## Лицензия
63
84
 
64
- MIT. Pollinations / Kiro AI / Qoder / другие провайдеры имеют свои ToS — читай их условия. На свой риск.
85
+ MIT. Pollinations / Kiro AI / Qoder / другие провайдеры имеют свои ToS — читай их условия.
@@ -6,18 +6,16 @@ import { launchClaude } from '../src/launch.js';
6
6
  import { runUpgrade } from '../src/upgrade.js';
7
7
  import { runDoctor } from '../src/doctor.js';
8
8
 
9
+ // Hardcoded so it works inside Bun --compile (no FS access to package.json)
10
+ const VERSION = '0.2.1';
11
+
9
12
  const cmd = process.argv[2];
10
13
 
11
14
  async function main() {
12
15
  if (cmd === 'doctor') return runDoctor();
13
16
  if (cmd === 'upgrade') return runUpgrade();
14
17
  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}`);
18
+ console.log(`KRASAVACODE v${VERSION}`);
21
19
  return;
22
20
  }
23
21
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "krasavacode",
3
- "version": "0.1.0",
4
- "description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway.",
3
+ "version": "0.2.1",
4
+ "description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway. Сам ставит Node при необходимости.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "krasavacode": "bin/krasavacode.js"
@@ -27,5 +27,13 @@
27
27
  "free",
28
28
  "education"
29
29
  ],
30
- "license": "MIT"
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/alexrexby/krasavacode.git"
34
+ },
35
+ "homepage": "https://github.com/alexrexby/krasavacode#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/alexrexby/krasavacode/issues"
38
+ }
31
39
  }
package/src/hub.js CHANGED
@@ -41,6 +41,7 @@ export async function startHub(paths) {
41
41
  const child = spawn(paths.ccrBin, ['start'], {
42
42
  stdio: process.env.KRASAVACODE_DEBUG ? 'inherit' : 'pipe',
43
43
  detached: false,
44
+ env: paths.env,
44
45
  });
45
46
 
46
47
  let stderrTail = '';
package/src/launch.js CHANGED
@@ -26,9 +26,12 @@ export async function launchClaude(paths, hub /*, detection */) {
26
26
  console.log('━'.repeat(58));
27
27
  console.log('');
28
28
 
29
+ // Merge env from runtime (PATH with bundled Node when applicable) with our overrides
30
+ const finalEnv = { ...(paths.env || process.env), ...env };
31
+
29
32
  return new Promise((resolve, reject) => {
30
33
  const child = spawn(paths.claudeBin, passthroughArgs, {
31
- env,
34
+ env: finalEnv,
32
35
  stdio: 'inherit',
33
36
  });
34
37
 
@@ -0,0 +1,159 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, writeFile, access, chmod } from 'node:fs/promises';
3
+ import { homedir, platform, arch, tmpdir } from 'node:os';
4
+ import { join, dirname } from 'node:path';
5
+ import { createWriteStream } from 'node:fs';
6
+ import { Readable } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+
9
+ const NODE_VERSION = '22.11.0'; // pinned LTS — bumped only when we test it
10
+ const ROOT = join(homedir(), '.krasavacode');
11
+ const NODE_DIR = join(ROOT, 'runtime', 'node');
12
+
13
+ function exists(p) { return access(p).then(() => true).catch(() => false); }
14
+
15
+ function spawnP(cmd, args, opts = {}) {
16
+ return new Promise((resolve, reject) => {
17
+ const c = spawn(cmd, args, { stdio: 'pipe', ...opts });
18
+ let stderr = '';
19
+ c.stderr?.on('data', d => stderr += d);
20
+ c.on('error', reject);
21
+ c.on('exit', code => code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}: ${stderr}`)));
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Returns descriptors for the Node distribution we should fetch.
27
+ * { url, archive, binDir, ext }
28
+ */
29
+ function pickDistribution() {
30
+ const p = platform();
31
+ const a = arch();
32
+
33
+ let nodePlatform, nodeArch, ext;
34
+ if (p === 'darwin') {
35
+ nodePlatform = 'darwin';
36
+ nodeArch = a === 'arm64' ? 'arm64' : 'x64';
37
+ ext = 'tar.gz';
38
+ } else if (p === 'linux') {
39
+ nodePlatform = 'linux';
40
+ nodeArch = a === 'arm64' ? 'arm64' : 'x64';
41
+ ext = 'tar.xz';
42
+ } else if (p === 'win32') {
43
+ nodePlatform = 'win';
44
+ nodeArch = 'x64';
45
+ ext = 'zip';
46
+ } else {
47
+ throw new Error(`Неподдерживаемая платформа: ${p}/${a}`);
48
+ }
49
+
50
+ const archive = `node-v${NODE_VERSION}-${nodePlatform}-${nodeArch}`;
51
+ const url = `https://nodejs.org/dist/v${NODE_VERSION}/${archive}.${ext}`;
52
+ return { url, archive, ext };
53
+ }
54
+
55
+ function bundledPaths() {
56
+ const p = platform();
57
+ if (p === 'win32') {
58
+ return {
59
+ node: join(NODE_DIR, 'node.exe'),
60
+ npm: join(NODE_DIR, 'npm.cmd'),
61
+ binDir: NODE_DIR,
62
+ };
63
+ }
64
+ return {
65
+ node: join(NODE_DIR, 'bin', 'node'),
66
+ npm: join(NODE_DIR, 'bin', 'npm'),
67
+ binDir: join(NODE_DIR, 'bin'),
68
+ };
69
+ }
70
+
71
+ async function downloadFile(url, dest) {
72
+ process.stdout.write(`📥 Тяну Node.js v${NODE_VERSION} (≈30 МБ, один раз)… `);
73
+ const t0 = Date.now();
74
+ const res = await fetch(url);
75
+ if (!res.ok) throw new Error(`HTTP ${res.status} от ${url}`);
76
+ await mkdir(dirname(dest), { recursive: true });
77
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
78
+ console.log(`OK (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
79
+ }
80
+
81
+ async function extractArchive(archivePath, ext, destDir) {
82
+ process.stdout.write('📂 Распаковываю… ');
83
+ await mkdir(destDir, { recursive: true });
84
+
85
+ if (ext === 'zip') {
86
+ // Windows 10+ has tar.exe with zip support; fall back to PowerShell.
87
+ try {
88
+ await spawnP('tar', ['-xf', archivePath, '-C', destDir]);
89
+ } catch {
90
+ await spawnP('powershell', ['-NoProfile', '-Command',
91
+ `Expand-Archive -Force -Path '${archivePath}' -DestinationPath '${destDir}'`]);
92
+ }
93
+ } else if (ext === 'tar.gz') {
94
+ await spawnP('tar', ['-xzf', archivePath, '-C', destDir]);
95
+ } else if (ext === 'tar.xz') {
96
+ // System tar usually has xz support; if not, this throws and we surface it.
97
+ await spawnP('tar', ['-xJf', archivePath, '-C', destDir]);
98
+ }
99
+ console.log('OK');
100
+ }
101
+
102
+ /**
103
+ * The archive extracts to a directory like `node-v22.11.0-darwin-arm64/`.
104
+ * We want everything in NODE_DIR directly. Move contents up.
105
+ */
106
+ async function flatten(parentDir, archiveName) {
107
+ const { rename, readdir, rm } = await import('node:fs/promises');
108
+ const inner = join(parentDir, archiveName);
109
+ if (!(await exists(inner))) return; // already flat
110
+ const entries = await readdir(inner);
111
+ for (const e of entries) {
112
+ await rename(join(inner, e), join(parentDir, e));
113
+ }
114
+ await rm(inner, { recursive: true, force: true });
115
+ }
116
+
117
+ /**
118
+ * Install bundled Node into ~/.krasavacode/runtime/node/ if not already.
119
+ * Returns absolute paths to node and npm binaries.
120
+ */
121
+ export async function ensureBundledNode() {
122
+ const paths = bundledPaths();
123
+
124
+ if (await exists(paths.node)) return paths;
125
+
126
+ const { url, archive, ext } = pickDistribution();
127
+ const archivePath = join(tmpdir(), `${archive}.${ext}`);
128
+
129
+ await downloadFile(url, archivePath);
130
+ await extractArchive(archivePath, ext, NODE_DIR);
131
+ await flatten(NODE_DIR, archive);
132
+
133
+ // Make sure binaries are executable on POSIX
134
+ if (platform() !== 'win32') {
135
+ try {
136
+ await chmod(paths.node, 0o755);
137
+ await chmod(paths.npm, 0o755);
138
+ } catch {}
139
+ }
140
+
141
+ return paths;
142
+ }
143
+
144
+ /**
145
+ * Ask system Node for its version. Returns major version (number) or null.
146
+ */
147
+ export async function systemNodeMajor() {
148
+ return new Promise(resolve => {
149
+ const c = spawn('node', ['--version'], { stdio: 'pipe' });
150
+ let out = '';
151
+ c.stdout.on('data', d => out += d);
152
+ c.on('error', () => resolve(null));
153
+ c.on('exit', code => {
154
+ if (code !== 0) return resolve(null);
155
+ const m = out.trim().match(/^v(\d+)/);
156
+ resolve(m ? Number(m[1]) : null);
157
+ });
158
+ });
159
+ }
package/src/runtime.js CHANGED
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { mkdir, access, readFile, writeFile } from 'node:fs/promises';
3
3
  import { homedir, platform } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { ensureBundledNode, systemNodeMajor } from './node-installer.js';
5
6
 
6
7
  const ROOT = join(homedir(), '.krasavacode');
7
8
  const RUNTIME = join(ROOT, 'runtime');
@@ -17,11 +18,8 @@ function exists(path) {
17
18
  }
18
19
 
19
20
  async function readState() {
20
- try {
21
- return JSON.parse(await readFile(STATE_FILE, 'utf8'));
22
- } catch {
23
- return {};
24
- }
21
+ try { return JSON.parse(await readFile(STATE_FILE, 'utf8')); }
22
+ catch { return {}; }
25
23
  }
26
24
 
27
25
  async function writeState(state) {
@@ -44,17 +42,21 @@ function spawnCmd(cmd, args, opts = {}) {
44
42
  });
45
43
  }
46
44
 
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
- );
45
+ /**
46
+ * Decide which Node/npm to use:
47
+ * 1. System Node 20 — use system npm (no download).
48
+ * 2. Otherwise — install bundled Node into ~/.krasavacode/runtime/node/ and use it.
49
+ */
50
+ async function pickNode() {
51
+ const major = await systemNodeMajor();
52
+ if (major !== null && major >= 20) {
53
+ return { source: 'system', node: 'node', npm: 'npm', binDir: null };
54
54
  }
55
+ const bundled = await ensureBundledNode();
56
+ return { source: 'bundled', node: bundled.node, npm: bundled.npm, binDir: bundled.binDir };
55
57
  }
56
58
 
57
- async function installPackages() {
59
+ async function installPackages(npmCmd, env) {
58
60
  console.log('📦 Устанавливаю компоненты (один раз)…');
59
61
  await mkdir(RUNTIME, { recursive: true });
60
62
 
@@ -68,14 +70,14 @@ async function installPackages() {
68
70
  }, null, 2));
69
71
  }
70
72
 
71
- await spawnCmd('npm', [
73
+ await spawnCmd(npmCmd, [
72
74
  'install',
73
75
  '--prefix', RUNTIME,
74
76
  '--no-audit',
75
77
  '--no-fund',
76
78
  '--loglevel=error',
77
79
  ...REQUIRED_PACKAGES,
78
- ]);
80
+ ], { env });
79
81
  }
80
82
 
81
83
  function binPath(name) {
@@ -84,9 +86,19 @@ function binPath(name) {
84
86
  }
85
87
 
86
88
  export async function ensureRuntime() {
87
- await ensureNpm();
88
89
  await mkdir(ROOT, { recursive: true });
89
90
 
91
+ const node = await pickNode();
92
+
93
+ // Build env that all child processes (npm install, ccr, claude) inherit.
94
+ // For bundled mode, bundled Node must come first in PATH so that
95
+ // shebangs `#!/usr/bin/env node` resolve to our Node.
96
+ const env = { ...process.env };
97
+ if (node.source === 'bundled' && node.binDir) {
98
+ const sep = platform() === 'win32' ? ';' : ':';
99
+ env.PATH = `${node.binDir}${sep}${process.env.PATH || ''}`;
100
+ }
101
+
90
102
  const state = await readState();
91
103
  const ccrBin = binPath('ccr');
92
104
  const claudeBin = binPath('claude');
@@ -95,8 +107,9 @@ export async function ensureRuntime() {
95
107
  const haveClaude = await exists(claudeBin);
96
108
 
97
109
  if (!haveCcr || !haveClaude || !state.installedAt) {
98
- await installPackages();
110
+ await installPackages(node.npm, env);
99
111
  state.installedAt = new Date().toISOString();
112
+ state.nodeSource = node.source;
100
113
  await writeState(state);
101
114
  console.log('✅ Готово.\n');
102
115
  }
@@ -106,6 +119,10 @@ export async function ensureRuntime() {
106
119
  runtime: RUNTIME,
107
120
  ccrBin,
108
121
  claudeBin,
122
+ nodeBin: node.node,
123
+ nodeSource: node.source,
124
+ pathPrefix: node.binDir,
125
+ env,
109
126
  state,
110
127
  saveState: writeState,
111
128
  };