@hanzo/dev 2.1.1 → 3.0.2
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 +278 -279
- package/bin/dev.js +413 -0
- package/package.json +32 -61
- package/postinstall.js +513 -0
- package/scripts/preinstall.js +69 -0
- package/scripts/windows-cleanup.ps1 +31 -0
- package/.eslintrc.json +0 -24
- package/dist/cli/dev.js +0 -24746
- package/src/cli/dev.ts +0 -946
- package/src/lib/agent-loop.ts +0 -552
- package/src/lib/benchmark-runner.ts +0 -431
- package/src/lib/code-act-agent.ts +0 -378
- package/src/lib/config.ts +0 -163
- package/src/lib/editor.ts +0 -395
- package/src/lib/function-calling.ts +0 -318
- package/src/lib/mcp-client.ts +0 -259
- package/src/lib/peer-agent-network.ts +0 -584
- package/src/lib/swarm-runner.ts +0 -389
- package/src/lib/unified-workspace.ts +0 -435
- package/test-swarm/file1.js +0 -6
- package/test-swarm/file2.ts +0 -12
- package/test-swarm/file3.py +0 -15
- package/test-swarm/file4.md +0 -13
- package/test-swarm/file5.json +0 -12
- package/test-swarm-demo.sh +0 -22
- package/tests/browser-integration.test.ts +0 -242
- package/tests/code-act-agent.test.ts +0 -305
- package/tests/editor.test.ts +0 -223
- package/tests/fixtures/sample-code.js +0 -13
- package/tests/fixtures/sample-code.py +0 -28
- package/tests/fixtures/sample-code.ts +0 -22
- package/tests/mcp-client.test.ts +0 -238
- package/tests/peer-agent-network.test.ts +0 -340
- package/tests/swarm-runner.test.ts +0 -301
- package/tests/swe-bench.test.ts +0 -357
- package/tsconfig.cli.json +0 -25
- package/tsconfig.json +0 -35
- package/vitest.config.ts +0 -37
package/postinstall.js
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Postinstall script for @hanzo/dev
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync, unlinkSync, statSync, openSync, closeSync, copyFileSync, fsyncSync, renameSync, realpathSync } from 'fs';
|
|
5
|
+
import { join, dirname, resolve } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { get } from 'https';
|
|
8
|
+
import { platform, arch, tmpdir } from 'os';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
function getTargetTriple() {
|
|
15
|
+
const platformMap = {
|
|
16
|
+
'darwin': 'apple-darwin',
|
|
17
|
+
'linux': 'unknown-linux-musl',
|
|
18
|
+
'win32': 'pc-windows-msvc'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const archMap = {
|
|
22
|
+
'x64': 'x86_64',
|
|
23
|
+
'arm64': 'aarch64'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const rustArch = archMap[arch()] || arch();
|
|
27
|
+
const rustPlatform = platformMap[platform()] || platform();
|
|
28
|
+
|
|
29
|
+
return `${rustArch}-${rustPlatform}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getCacheDir(version) {
|
|
33
|
+
const plt = platform();
|
|
34
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
35
|
+
let base = '';
|
|
36
|
+
if (plt === 'win32') {
|
|
37
|
+
base = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
|
38
|
+
} else if (plt === 'darwin') {
|
|
39
|
+
base = join(home, 'Library', 'Caches');
|
|
40
|
+
} else {
|
|
41
|
+
base = process.env.XDG_CACHE_HOME || join(home, '.cache');
|
|
42
|
+
}
|
|
43
|
+
const dir = join(base, 'hanzo', 'dev', version);
|
|
44
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getCachedBinaryPath(version, targetTriple, isWindows) {
|
|
49
|
+
const ext = isWindows ? '.exe' : '';
|
|
50
|
+
const cacheDir = getCacheDir(version);
|
|
51
|
+
return join(cacheDir, `dev-${targetTriple}${ext}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isWSL() {
|
|
55
|
+
if (platform() !== 'linux') return false;
|
|
56
|
+
try {
|
|
57
|
+
const ver = readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
58
|
+
return ver.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
|
|
59
|
+
} catch { return false; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isPathOnWindowsFs(p) {
|
|
63
|
+
try {
|
|
64
|
+
const mounts = readFileSync('/proc/mounts', 'utf8').split(/\n/).filter(Boolean);
|
|
65
|
+
let best = { mount: '/', type: 'unknown', len: 1 };
|
|
66
|
+
for (const line of mounts) {
|
|
67
|
+
const parts = line.split(' ');
|
|
68
|
+
if (parts.length < 3) continue;
|
|
69
|
+
const mnt = parts[1];
|
|
70
|
+
const typ = parts[2];
|
|
71
|
+
if (p.startsWith(mnt) && mnt.length > best.len) best = { mount: mnt, type: typ, len: mnt.length };
|
|
72
|
+
}
|
|
73
|
+
return best.type === 'drvfs' || best.type === 'cifs';
|
|
74
|
+
} catch { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function writeCacheAtomic(srcPath, cachePath) {
|
|
78
|
+
try {
|
|
79
|
+
if (existsSync(cachePath)) {
|
|
80
|
+
const ok = validateDownloadedBinary(cachePath).ok;
|
|
81
|
+
if (ok) return;
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
const dir = dirname(cachePath);
|
|
85
|
+
if (!existsSync(dir)) { try { mkdirSync(dir, { recursive: true }); } catch {} }
|
|
86
|
+
const tmp = cachePath + '.tmp-' + Math.random().toString(36).slice(2, 8);
|
|
87
|
+
copyFileSync(srcPath, tmp);
|
|
88
|
+
try { const fd = openSync(tmp, 'r'); try { fsyncSync(fd); } finally { closeSync(fd); } } catch {}
|
|
89
|
+
const delays = [100, 200, 400, 800, 1200, 1600];
|
|
90
|
+
for (let i = 0; i < delays.length; i++) {
|
|
91
|
+
try {
|
|
92
|
+
if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
|
|
93
|
+
renameSync(tmp, cachePath);
|
|
94
|
+
return;
|
|
95
|
+
} catch {
|
|
96
|
+
await new Promise(r => setTimeout(r, delays[i]));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
|
|
100
|
+
renameSync(tmp, cachePath);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveGlobalBinDir() {
|
|
104
|
+
const plt = platform();
|
|
105
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
106
|
+
|
|
107
|
+
const fromPrefix = (prefixPath) => {
|
|
108
|
+
if (!prefixPath) return '';
|
|
109
|
+
return plt === 'win32' ? prefixPath : join(prefixPath, 'bin');
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const prefixEnv = process.env.npm_config_prefix || process.env.PREFIX || '';
|
|
113
|
+
const direct = fromPrefix(prefixEnv);
|
|
114
|
+
if (direct) return direct;
|
|
115
|
+
|
|
116
|
+
const tryExec = (command) => {
|
|
117
|
+
try {
|
|
118
|
+
return execSync(command, {
|
|
119
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
120
|
+
shell: true,
|
|
121
|
+
}).toString().trim();
|
|
122
|
+
} catch {
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const prefixFromNpm = fromPrefix(tryExec('npm prefix -g'));
|
|
128
|
+
if (prefixFromNpm) return prefixFromNpm;
|
|
129
|
+
|
|
130
|
+
const binFromNpm = tryExec('npm bin -g');
|
|
131
|
+
if (binFromNpm) return binFromNpm;
|
|
132
|
+
|
|
133
|
+
if (userAgent.includes('pnpm')) {
|
|
134
|
+
const pnpmBin = tryExec('pnpm bin --global');
|
|
135
|
+
if (pnpmBin) return pnpmBin;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (userAgent.includes('yarn')) {
|
|
139
|
+
const yarnBin = tryExec('yarn global bin');
|
|
140
|
+
if (yarnBin) return yarnBin;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
|
|
147
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
148
|
+
|
|
149
|
+
const doAttempt = () => new Promise((resolve, reject) => {
|
|
150
|
+
const attempt = (currentUrl, redirectsLeft) => {
|
|
151
|
+
const req = get(currentUrl, (response) => {
|
|
152
|
+
const status = response.statusCode || 0;
|
|
153
|
+
const location = response.headers.location;
|
|
154
|
+
|
|
155
|
+
if ((status === 301 || status === 302 || status === 303 || status === 307 || status === 308) && location) {
|
|
156
|
+
if (redirectsLeft <= 0) {
|
|
157
|
+
reject(new Error(`Too many redirects while downloading ${currentUrl}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
attempt(location, redirectsLeft - 1);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (status === 200) {
|
|
165
|
+
const expected = parseInt(response.headers['content-length'] || '0', 10) || 0;
|
|
166
|
+
let bytes = 0;
|
|
167
|
+
let timer;
|
|
168
|
+
const timeoutMs = 30000;
|
|
169
|
+
|
|
170
|
+
const resetTimer = () => {
|
|
171
|
+
if (timer) clearTimeout(timer);
|
|
172
|
+
timer = setTimeout(() => {
|
|
173
|
+
req.destroy(new Error('download stalled'));
|
|
174
|
+
}, timeoutMs);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
resetTimer();
|
|
178
|
+
response.on('data', (chunk) => {
|
|
179
|
+
bytes += chunk.length;
|
|
180
|
+
resetTimer();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const file = createWriteStream(dest);
|
|
184
|
+
response.pipe(file);
|
|
185
|
+
file.on('finish', () => {
|
|
186
|
+
if (timer) clearTimeout(timer);
|
|
187
|
+
file.close();
|
|
188
|
+
if (expected && bytes !== expected) {
|
|
189
|
+
try { unlinkSync(dest); } catch {}
|
|
190
|
+
reject(new Error(`incomplete download: got ${bytes} of ${expected} bytes`));
|
|
191
|
+
} else if (bytes === 0) {
|
|
192
|
+
try { unlinkSync(dest); } catch {}
|
|
193
|
+
reject(new Error('empty download'));
|
|
194
|
+
} else {
|
|
195
|
+
resolve();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
file.on('error', (err) => {
|
|
199
|
+
if (timer) clearTimeout(timer);
|
|
200
|
+
try { unlinkSync(dest); } catch {}
|
|
201
|
+
reject(err);
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
reject(new Error(`Failed to download: HTTP ${status}`));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
req.on('error', (err) => {
|
|
209
|
+
try { unlinkSync(dest); } catch {}
|
|
210
|
+
reject(err);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
req.setTimeout(120000, () => {
|
|
214
|
+
req.destroy(new Error('download timed out'));
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
attempt(url, maxRedirects);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
let attemptNum = 0;
|
|
222
|
+
while (true) {
|
|
223
|
+
try {
|
|
224
|
+
return await doAttempt();
|
|
225
|
+
} catch (e) {
|
|
226
|
+
attemptNum += 1;
|
|
227
|
+
if (attemptNum > maxRetries) throw e;
|
|
228
|
+
const backoff = Math.min(2000, 200 * attemptNum);
|
|
229
|
+
await sleep(backoff);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function validateDownloadedBinary(p) {
|
|
235
|
+
try {
|
|
236
|
+
const st = statSync(p);
|
|
237
|
+
if (!st.isFile() || st.size === 0) {
|
|
238
|
+
return { ok: false, reason: 'empty or not a regular file' };
|
|
239
|
+
}
|
|
240
|
+
const fd = openSync(p, 'r');
|
|
241
|
+
try {
|
|
242
|
+
const buf = Buffer.alloc(4);
|
|
243
|
+
const n = readSync(fd, buf, 0, 4, 0);
|
|
244
|
+
if (n < 2) return { ok: false, reason: 'too short' };
|
|
245
|
+
const plt = platform();
|
|
246
|
+
if (plt === 'win32') {
|
|
247
|
+
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: 'invalid PE header (missing MZ)' };
|
|
248
|
+
} else if (plt === 'linux' || plt === 'android') {
|
|
249
|
+
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: 'invalid ELF header' };
|
|
250
|
+
} else if (plt === 'darwin') {
|
|
251
|
+
const isMachO = (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) ||
|
|
252
|
+
(buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe);
|
|
253
|
+
if (!isMachO) return { ok: false, reason: 'invalid Mach-O header' };
|
|
254
|
+
}
|
|
255
|
+
return { ok: true };
|
|
256
|
+
} finally {
|
|
257
|
+
closeSync(fd);
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
return { ok: false, reason: e.message };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function runPostinstall(options = {}) {
|
|
265
|
+
const { skipGlobalAlias = false, invokedByRuntime = false } = options;
|
|
266
|
+
if (process.env.DEV_POSTINSTALL_DRY_RUN === '1') {
|
|
267
|
+
return { skipped: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (invokedByRuntime) {
|
|
271
|
+
process.env.DEV_RUNTIME_POSTINSTALL = process.env.DEV_RUNTIME_POSTINSTALL || '1';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
275
|
+
const isNpx = ua.includes('npx');
|
|
276
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
277
|
+
|
|
278
|
+
const targetTriple = getTargetTriple();
|
|
279
|
+
const isWindows = platform() === 'win32';
|
|
280
|
+
const binaryExt = isWindows ? '.exe' : '';
|
|
281
|
+
|
|
282
|
+
const binDir = join(__dirname, 'bin');
|
|
283
|
+
if (!existsSync(binDir)) {
|
|
284
|
+
mkdirSync(binDir, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
288
|
+
const version = packageJson.version;
|
|
289
|
+
|
|
290
|
+
// The release produces 'code-*' binaries; we'll download and rename to 'dev-*'
|
|
291
|
+
const binaries = ['code'];
|
|
292
|
+
|
|
293
|
+
console.log(`Installing @hanzo/dev v${version} for ${targetTriple}...`);
|
|
294
|
+
|
|
295
|
+
for (const binary of binaries) {
|
|
296
|
+
const binaryName = `${binary}-${targetTriple}${binaryExt}`;
|
|
297
|
+
const devBinaryName = `dev-${targetTriple}${binaryExt}`;
|
|
298
|
+
const localPath = join(binDir, devBinaryName);
|
|
299
|
+
const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
|
|
300
|
+
|
|
301
|
+
// Fast path: if a valid cached binary exists, reuse it
|
|
302
|
+
try {
|
|
303
|
+
if (existsSync(cachePath)) {
|
|
304
|
+
const valid = validateDownloadedBinary(cachePath);
|
|
305
|
+
if (valid.ok) {
|
|
306
|
+
const wsl = isWSL();
|
|
307
|
+
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
|
308
|
+
const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
|
|
309
|
+
if (mirrorToLocal) {
|
|
310
|
+
copyFileSync(cachePath, localPath);
|
|
311
|
+
try { chmodSync(localPath, 0o755); } catch {}
|
|
312
|
+
}
|
|
313
|
+
console.log(`✓ ${devBinaryName} ready from user cache`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// Ignore cache errors
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Try platform package via npm optionalDependencies
|
|
322
|
+
const require = createRequire(import.meta.url);
|
|
323
|
+
const platformPkg = (() => {
|
|
324
|
+
const name = (() => {
|
|
325
|
+
if (isWindows) return '@hanzo/dev-win32-x64';
|
|
326
|
+
const plt = platform();
|
|
327
|
+
const cpu = arch();
|
|
328
|
+
if (plt === 'darwin' && cpu === 'arm64') return '@hanzo/dev-darwin-arm64';
|
|
329
|
+
if (plt === 'darwin' && cpu === 'x64') return '@hanzo/dev-darwin-x64';
|
|
330
|
+
if (plt === 'linux' && cpu === 'x64') return '@hanzo/dev-linux-x64-musl';
|
|
331
|
+
if (plt === 'linux' && cpu === 'arm64') return '@hanzo/dev-linux-arm64-musl';
|
|
332
|
+
return null;
|
|
333
|
+
})();
|
|
334
|
+
if (!name) return null;
|
|
335
|
+
try {
|
|
336
|
+
const pkgJsonPath = require.resolve(`${name}/package.json`);
|
|
337
|
+
const pkgDir = dirname(pkgJsonPath);
|
|
338
|
+
return { name, dir: pkgDir };
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
|
|
344
|
+
if (platformPkg) {
|
|
345
|
+
try {
|
|
346
|
+
const src = join(platformPkg.dir, 'bin', devBinaryName);
|
|
347
|
+
if (!existsSync(src)) {
|
|
348
|
+
throw new Error(`platform package missing binary: ${platformPkg.name}`);
|
|
349
|
+
}
|
|
350
|
+
await writeCacheAtomic(src, cachePath);
|
|
351
|
+
const wsl = isWSL();
|
|
352
|
+
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
|
353
|
+
const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
|
|
354
|
+
if (mirrorToLocal) {
|
|
355
|
+
copyFileSync(cachePath, localPath);
|
|
356
|
+
try { chmodSync(localPath, 0o755); } catch {}
|
|
357
|
+
}
|
|
358
|
+
console.log(`✓ Installed ${devBinaryName} from ${platformPkg.name} (cached)`);
|
|
359
|
+
continue;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Download from GitHub release
|
|
366
|
+
const isWin = isWindows;
|
|
367
|
+
const detectedWSL = isWSL();
|
|
368
|
+
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
|
369
|
+
const mirrorToLocal = !(isWin || (detectedWSL && isPathOnWindowsFs(binDirReal)));
|
|
370
|
+
let useZst = false;
|
|
371
|
+
if (!isWin) {
|
|
372
|
+
try {
|
|
373
|
+
execSync('zstd --version', { stdio: 'ignore', shell: true });
|
|
374
|
+
useZst = true;
|
|
375
|
+
} catch {
|
|
376
|
+
useZst = false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const archiveName = isWin ? `${binaryName}.zip` : (useZst ? `${binaryName}.zst` : `${binaryName}.tar.gz`);
|
|
380
|
+
const downloadUrl = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
|
|
381
|
+
|
|
382
|
+
console.log(`Downloading ${archiveName}...`);
|
|
383
|
+
try {
|
|
384
|
+
const needsIsolation = isWin || (!isWin && !mirrorToLocal);
|
|
385
|
+
let safeTempDir = needsIsolation ? join(tmpdir(), 'hanzo', 'dev', version) : binDir;
|
|
386
|
+
if (needsIsolation) {
|
|
387
|
+
try {
|
|
388
|
+
if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
|
|
389
|
+
} catch {
|
|
390
|
+
try {
|
|
391
|
+
safeTempDir = getCacheDir(version);
|
|
392
|
+
if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
|
|
393
|
+
} catch {}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const tmpPath = join(needsIsolation ? safeTempDir : binDir, `.${archiveName}.part`);
|
|
397
|
+
await downloadBinary(downloadUrl, tmpPath);
|
|
398
|
+
|
|
399
|
+
if (isWin) {
|
|
400
|
+
const unzipDest = safeTempDir;
|
|
401
|
+
try {
|
|
402
|
+
const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
|
|
403
|
+
const psFull = join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
404
|
+
const psCmd = `Expand-Archive -Path '${tmpPath}' -DestinationPath '${unzipDest}' -Force`;
|
|
405
|
+
let ok = false;
|
|
406
|
+
try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
|
|
407
|
+
if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
|
408
|
+
if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
|
409
|
+
if (!ok) { execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, { stdio: 'ignore', shell: true }); }
|
|
410
|
+
} catch (e) {
|
|
411
|
+
throw new Error(`failed to unzip archive: ${e.message}`);
|
|
412
|
+
} finally {
|
|
413
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
const extractedPath = join(unzipDest, binaryName);
|
|
417
|
+
// Rename code-* to dev-* in cache
|
|
418
|
+
const devCachePath = cachePath;
|
|
419
|
+
await writeCacheAtomic(extractedPath, devCachePath);
|
|
420
|
+
try { unlinkSync(extractedPath); } catch {}
|
|
421
|
+
} catch (e) {
|
|
422
|
+
throw new Error(`failed to move binary to cache: ${e.message}`);
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
const downloadedPath = join(binDir, binaryName);
|
|
426
|
+
if (useZst) {
|
|
427
|
+
try {
|
|
428
|
+
execSync(`zstd -d '${tmpPath}' -o '${downloadedPath}'`, { stdio: 'ignore', shell: true });
|
|
429
|
+
} catch (e) {
|
|
430
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
431
|
+
throw new Error(`failed to decompress .zst (need zstd CLI): ${e.message}`);
|
|
432
|
+
}
|
|
433
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
434
|
+
} else {
|
|
435
|
+
try {
|
|
436
|
+
execSync(`tar -xzf '${tmpPath}' -C '${binDir}'`, { stdio: 'ignore', shell: true });
|
|
437
|
+
} catch (e) {
|
|
438
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
439
|
+
throw new Error(`failed to extract .tar.gz: ${e.message}`);
|
|
440
|
+
}
|
|
441
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
442
|
+
}
|
|
443
|
+
// Rename code-* to dev-*
|
|
444
|
+
if (existsSync(downloadedPath)) {
|
|
445
|
+
if (mirrorToLocal) {
|
|
446
|
+
renameSync(downloadedPath, localPath);
|
|
447
|
+
} else {
|
|
448
|
+
const extractedPath = downloadedPath;
|
|
449
|
+
await writeCacheAtomic(extractedPath, cachePath);
|
|
450
|
+
try { unlinkSync(extractedPath); } catch {}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const valid = validateDownloadedBinary(isWin ? cachePath : (mirrorToLocal ? localPath : cachePath));
|
|
456
|
+
if (!valid.ok) {
|
|
457
|
+
try { (isWin || !mirrorToLocal) ? unlinkSync(cachePath) : unlinkSync(localPath); } catch {}
|
|
458
|
+
throw new Error(`invalid binary (${valid.reason})`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!isWin && mirrorToLocal) {
|
|
462
|
+
chmodSync(localPath, 0o755);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
console.log(`✓ Installed ${devBinaryName}${(isWin || !mirrorToLocal) ? ' (cached)' : ''}`);
|
|
466
|
+
if (!isWin && mirrorToLocal) {
|
|
467
|
+
try { await writeCacheAtomic(localPath, cachePath); } catch {}
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.error(`✗ Failed to install ${devBinaryName}: ${error.message}`);
|
|
471
|
+
console.error(` Downloaded from: ${downloadUrl}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const mainBinary = `dev-${targetTriple}${binaryExt}`;
|
|
476
|
+
const mainBinaryPath = join(binDir, mainBinary);
|
|
477
|
+
|
|
478
|
+
if (existsSync(mainBinaryPath) || existsSync(getCachedBinaryPath(version, targetTriple, platform() === 'win32'))) {
|
|
479
|
+
try {
|
|
480
|
+
const probePath = existsSync(mainBinaryPath) ? mainBinaryPath : getCachedBinaryPath(version, targetTriple, platform() === 'win32');
|
|
481
|
+
const stats = statSync(probePath);
|
|
482
|
+
if (!stats.size) throw new Error('binary is empty (download likely failed)');
|
|
483
|
+
const valid = validateDownloadedBinary(probePath);
|
|
484
|
+
if (!valid.ok) {
|
|
485
|
+
console.warn(`⚠ Main dev binary appears invalid: ${valid.reason}`);
|
|
486
|
+
console.warn(' Try reinstalling or check your network/proxy settings.');
|
|
487
|
+
}
|
|
488
|
+
} catch (e) {
|
|
489
|
+
console.warn(`⚠ Main dev binary appears invalid: ${e.message}`);
|
|
490
|
+
console.warn(' Try reinstalling or check your network/proxy settings.');
|
|
491
|
+
}
|
|
492
|
+
console.log('✓ Installation complete!');
|
|
493
|
+
} else {
|
|
494
|
+
console.warn('⚠ Main dev binary not found. You may need to build from source.');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function isExecutedDirectly() {
|
|
499
|
+
const entry = process.argv[1];
|
|
500
|
+
if (!entry) return false;
|
|
501
|
+
try {
|
|
502
|
+
return resolve(entry) === fileURLToPath(import.meta.url);
|
|
503
|
+
} catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (isExecutedDirectly()) {
|
|
509
|
+
runPostinstall().catch(error => {
|
|
510
|
+
console.error('Installation failed:', error);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Windows-friendly preinstall: proactively free file locks from prior installs
|
|
3
|
+
// so npm/yarn/pnpm can stage the new package. No-ops on non-Windows.
|
|
4
|
+
|
|
5
|
+
import { platform } from 'os';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { existsSync, readdirSync, rmSync, readFileSync, statSync } from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
function isWSL() {
|
|
12
|
+
if (platform() !== 'linux') return false;
|
|
13
|
+
try {
|
|
14
|
+
const rel = readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
15
|
+
return rel.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
|
|
16
|
+
} catch { return false; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isWin = platform() === 'win32';
|
|
20
|
+
const wsl = isWSL();
|
|
21
|
+
const isWinLike = isWin || wsl;
|
|
22
|
+
|
|
23
|
+
// Scope: only run for global installs, unless explicitly forced. Allow opt-out.
|
|
24
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
25
|
+
const force = process.env.DEV_FORCE_PREINSTALL === '1';
|
|
26
|
+
const skip = process.env.DEV_SKIP_PREINSTALL === '1';
|
|
27
|
+
if (!isWinLike || skip || (!isGlobal && !force)) process.exit(0);
|
|
28
|
+
|
|
29
|
+
function tryExec(cmd, opts = {}) {
|
|
30
|
+
try { execSync(cmd, { stdio: ['ignore', 'ignore', 'ignore'], shell: true, ...opts }); } catch { /* ignore */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 1) Stop our native binary if it is holding locks. Avoid killing unrelated tools.
|
|
34
|
+
// Only available on native Windows; skip entirely on WSL to avoid noise.
|
|
35
|
+
if (isWin) {
|
|
36
|
+
tryExec('taskkill /IM dev-x86_64-pc-windows-msvc.exe /F');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2) Remove stale staging dirs from previous failed installs under the global
|
|
40
|
+
// @hanzo scope, which npm will reuse (e.g., .dev-XXXXX). Remove only
|
|
41
|
+
// old entries and never the current staging or live package.
|
|
42
|
+
try {
|
|
43
|
+
let scopeDir = '';
|
|
44
|
+
try {
|
|
45
|
+
const root = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'], shell: true }).toString().trim();
|
|
46
|
+
scopeDir = path.join(root, '@hanzo');
|
|
47
|
+
} catch {
|
|
48
|
+
// Fall back to guessing from this script location: <staging>\..\..\
|
|
49
|
+
const here = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
50
|
+
scopeDir = path.resolve(here, '..');
|
|
51
|
+
}
|
|
52
|
+
if (existsSync(scopeDir)) {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const maxAgeMs = 2 * 60 * 60 * 1000; // 2 hours
|
|
55
|
+
const currentDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
56
|
+
for (const name of readdirSync(scopeDir)) {
|
|
57
|
+
if (!name.startsWith('.dev-')) continue;
|
|
58
|
+
const p = path.join(scopeDir, name);
|
|
59
|
+
if (path.resolve(p) === currentDir) continue; // never remove our current dir
|
|
60
|
+
try {
|
|
61
|
+
const st = statSync(p);
|
|
62
|
+
const age = now - st.mtimeMs;
|
|
63
|
+
if (age > maxAgeMs) rmSync(p, { recursive: true, force: true });
|
|
64
|
+
} catch { /* ignore */ }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
|
|
69
|
+
process.exit(0);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<#
|
|
2
|
+
Helper to recover from EBUSY/EPERM during global npm upgrades on Windows.
|
|
3
|
+
Closes running processes and removes stale package folders.
|
|
4
|
+
|
|
5
|
+
Usage (PowerShell):
|
|
6
|
+
Set-ExecutionPolicy -Scope Process Bypass -Force
|
|
7
|
+
./dev-cli/scripts/windows-cleanup.ps1
|
|
8
|
+
#>
|
|
9
|
+
|
|
10
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
11
|
+
|
|
12
|
+
Write-Host "Stopping running Hanzo Dev processes..."
|
|
13
|
+
taskkill /IM dev-x86_64-pc-windows-msvc.exe /F 2>$null | Out-Null
|
|
14
|
+
taskkill /IM dev.exe /F 2>$null | Out-Null
|
|
15
|
+
taskkill /IM hanzo.exe /F 2>$null | Out-Null
|
|
16
|
+
|
|
17
|
+
Write-Host "Removing old global package (if present)..."
|
|
18
|
+
$npmRoot = (& npm root -g).Trim()
|
|
19
|
+
$pkgPath = Join-Path $npmRoot "@hanzo\dev"
|
|
20
|
+
if (Test-Path $pkgPath) {
|
|
21
|
+
try { Remove-Item -LiteralPath $pkgPath -Recurse -Force -ErrorAction Stop } catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Write-Host "Removing temp staging directories (if present)..."
|
|
25
|
+
Get-ChildItem -LiteralPath (Join-Path $npmRoot "@hanzo") -Force -ErrorAction SilentlyContinue |
|
|
26
|
+
Where-Object { $_.Name -like '.dev-*' } |
|
|
27
|
+
ForEach-Object {
|
|
28
|
+
try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop } catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Write-Host "Cleanup complete. You can now run: npm install -g @hanzo/dev@latest"
|
package/.eslintrc.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"parser": "@typescript-eslint/parser",
|
|
3
|
-
"extends": [
|
|
4
|
-
"eslint:recommended",
|
|
5
|
-
"plugin:@typescript-eslint/recommended"
|
|
6
|
-
],
|
|
7
|
-
"parserOptions": {
|
|
8
|
-
"ecmaVersion": 2020,
|
|
9
|
-
"sourceType": "module"
|
|
10
|
-
},
|
|
11
|
-
"env": {
|
|
12
|
-
"node": true,
|
|
13
|
-
"es2020": true
|
|
14
|
-
},
|
|
15
|
-
"rules": {
|
|
16
|
-
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
17
|
-
"@typescript-eslint/no-explicit-any": "warn",
|
|
18
|
-
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
|
19
|
-
"no-console": ["warn", { "allow": ["warn", "error"] }],
|
|
20
|
-
"prefer-const": "error",
|
|
21
|
-
"no-var": "error"
|
|
22
|
-
},
|
|
23
|
-
"ignorePatterns": ["dist/", "coverage/", "node_modules/", "*.js", "tests/"]
|
|
24
|
-
}
|