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 +45 -24
- package/bin/krasavacode.js +4 -6
- package/package.json +11 -3
- 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/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 работает без логина и без
|
|
13
|
+
Всё. Pollinations работает без логина и без карты. Если у тебя Node старее 20 — наш CLI сам подтянет нужную версию.
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
-
|
|
39
|
-
- **Порт занят** → перезапусти терминал
|
|
40
|
-
- **Корпоративный прокси/Россия/Китай** → запусти upgrade и в дашборде включи SOCKS5
|
|
70
|
+
Покажет что сломано. Самые частые проблемы:
|
|
71
|
+
- **Корпоративный прокси / Россия / Китай** → запусти upgrade и в дашборде включи SOCKS5
|
|
72
|
+
- **Порт 3456 занят** → перезапусти терминал
|
|
41
73
|
|
|
42
74
|
## Что под капотом
|
|
43
75
|
|
|
44
76
|
```
|
|
45
|
-
krasavacode →
|
|
46
|
-
(
|
|
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` с подменённым
|
|
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 — читай их условия.
|
package/bin/krasavacode.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
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
|
};
|