krasavacode 0.1.0 → 0.2.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/package.json +2 -2
- package/src/hub.js +1 -0
- package/src/launch.js +4 -1
- package/src/node-installer.js +159 -0
- package/src/runtime.js +34 -17
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "krasavacode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway.",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway. Сам ставит Node при необходимости.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"krasavacode": "bin/krasavacode.js"
|
package/src/hub.js
CHANGED
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
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
|
};
|