@hanzo/dev 2.1.0 → 3.0.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/bin/dev.js +420 -0
- package/package.json +39 -56
- package/postinstall.js +589 -0
- package/.eslintrc.json +0 -24
- package/README.md +0 -359
- package/dist/cli/dev.js +0 -24739
- 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 -379
- 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,589 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Non-functional change to trigger release workflow
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync, unlinkSync, statSync, openSync, closeSync, copyFileSync } from 'fs';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { get } from 'https';
|
|
8
|
+
import { platform, arch } from 'os';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
// Map Node.js platform/arch to Rust target triples
|
|
15
|
+
function getTargetTriple() {
|
|
16
|
+
const platformMap = {
|
|
17
|
+
'darwin': 'apple-darwin',
|
|
18
|
+
'linux': 'unknown-linux-musl', // Default to musl for better compatibility
|
|
19
|
+
'win32': 'pc-windows-msvc'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const archMap = {
|
|
23
|
+
'x64': 'x86_64',
|
|
24
|
+
'arm64': 'aarch64'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const rustArch = archMap[arch()] || arch();
|
|
28
|
+
const rustPlatform = platformMap[platform()] || platform();
|
|
29
|
+
|
|
30
|
+
return `${rustArch}-${rustPlatform}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Resolve a persistent user cache directory for binaries so that repeated
|
|
34
|
+
// npx installs can reuse a previously downloaded artifact and skip work.
|
|
35
|
+
function getCacheDir(version) {
|
|
36
|
+
const plt = platform();
|
|
37
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
38
|
+
let base = '';
|
|
39
|
+
if (plt === 'win32') {
|
|
40
|
+
base = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
|
41
|
+
} else if (plt === 'darwin') {
|
|
42
|
+
base = join(home, 'Library', 'Caches');
|
|
43
|
+
} else {
|
|
44
|
+
base = process.env.XDG_CACHE_HOME || join(home, '.cache');
|
|
45
|
+
}
|
|
46
|
+
const dir = join(base, 'just-every', 'code', version);
|
|
47
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getCachedBinaryPath(version, targetTriple, isWindows) {
|
|
52
|
+
const ext = isWindows ? '.exe' : '';
|
|
53
|
+
const cacheDir = getCacheDir(version);
|
|
54
|
+
return join(cacheDir, `code-${targetTriple}${ext}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
|
|
58
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
59
|
+
|
|
60
|
+
const doAttempt = () => new Promise((resolve, reject) => {
|
|
61
|
+
const attempt = (currentUrl, redirectsLeft) => {
|
|
62
|
+
const req = get(currentUrl, (response) => {
|
|
63
|
+
const status = response.statusCode || 0;
|
|
64
|
+
const location = response.headers.location;
|
|
65
|
+
|
|
66
|
+
if ((status === 301 || status === 302 || status === 303 || status === 307 || status === 308) && location) {
|
|
67
|
+
if (redirectsLeft <= 0) {
|
|
68
|
+
reject(new Error(`Too many redirects while downloading ${currentUrl}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
attempt(location, redirectsLeft - 1);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (status === 200) {
|
|
76
|
+
const expected = parseInt(response.headers['content-length'] || '0', 10) || 0;
|
|
77
|
+
let bytes = 0;
|
|
78
|
+
let timer;
|
|
79
|
+
const timeoutMs = 30000; // 30s inactivity timeout
|
|
80
|
+
|
|
81
|
+
const resetTimer = () => {
|
|
82
|
+
if (timer) clearTimeout(timer);
|
|
83
|
+
timer = setTimeout(() => {
|
|
84
|
+
req.destroy(new Error('download stalled'));
|
|
85
|
+
}, timeoutMs);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
resetTimer();
|
|
89
|
+
response.on('data', (chunk) => {
|
|
90
|
+
bytes += chunk.length;
|
|
91
|
+
resetTimer();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const file = createWriteStream(dest);
|
|
95
|
+
response.pipe(file);
|
|
96
|
+
file.on('finish', () => {
|
|
97
|
+
if (timer) clearTimeout(timer);
|
|
98
|
+
file.close();
|
|
99
|
+
if (expected && bytes !== expected) {
|
|
100
|
+
try { unlinkSync(dest); } catch {}
|
|
101
|
+
reject(new Error(`incomplete download: got ${bytes} of ${expected} bytes`));
|
|
102
|
+
} else if (bytes === 0) {
|
|
103
|
+
try { unlinkSync(dest); } catch {}
|
|
104
|
+
reject(new Error('empty download'));
|
|
105
|
+
} else {
|
|
106
|
+
resolve();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
file.on('error', (err) => {
|
|
110
|
+
if (timer) clearTimeout(timer);
|
|
111
|
+
try { unlinkSync(dest); } catch {}
|
|
112
|
+
reject(err);
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
reject(new Error(`Failed to download: HTTP ${status}`));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
req.on('error', (err) => {
|
|
120
|
+
try { unlinkSync(dest); } catch {}
|
|
121
|
+
reject(err);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Absolute request timeout to avoid hanging forever
|
|
125
|
+
req.setTimeout(120000, () => {
|
|
126
|
+
req.destroy(new Error('download timed out'));
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
attempt(url, maxRedirects);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let attemptNum = 0;
|
|
134
|
+
while (true) {
|
|
135
|
+
try {
|
|
136
|
+
return await doAttempt();
|
|
137
|
+
} catch (e) {
|
|
138
|
+
attemptNum += 1;
|
|
139
|
+
if (attemptNum > maxRetries) throw e;
|
|
140
|
+
const backoff = Math.min(2000, 200 * attemptNum);
|
|
141
|
+
await sleep(backoff);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateDownloadedBinary(p) {
|
|
147
|
+
try {
|
|
148
|
+
const st = statSync(p);
|
|
149
|
+
if (!st.isFile() || st.size === 0) {
|
|
150
|
+
return { ok: false, reason: 'empty or not a regular file' };
|
|
151
|
+
}
|
|
152
|
+
const fd = openSync(p, 'r');
|
|
153
|
+
try {
|
|
154
|
+
const buf = Buffer.alloc(4);
|
|
155
|
+
const n = readSync(fd, buf, 0, 4, 0);
|
|
156
|
+
if (n < 2) return { ok: false, reason: 'too short' };
|
|
157
|
+
const plt = platform();
|
|
158
|
+
if (plt === 'win32') {
|
|
159
|
+
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: 'invalid PE header (missing MZ)' };
|
|
160
|
+
} else if (plt === 'linux' || plt === 'android') {
|
|
161
|
+
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: 'invalid ELF header' };
|
|
162
|
+
} else if (plt === 'darwin') {
|
|
163
|
+
const isMachO = (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) ||
|
|
164
|
+
(buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe);
|
|
165
|
+
if (!isMachO) return { ok: false, reason: 'invalid Mach-O header' };
|
|
166
|
+
}
|
|
167
|
+
return { ok: true };
|
|
168
|
+
} finally {
|
|
169
|
+
closeSync(fd);
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return { ok: false, reason: e.message };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function main() {
|
|
177
|
+
// Detect potential PATH conflict with an existing `code` command (e.g., VS Code)
|
|
178
|
+
// Only relevant for global installs; skip for npx/local installs to keep postinstall fast.
|
|
179
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
180
|
+
const isNpx = ua.includes('npx');
|
|
181
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
182
|
+
if (isGlobal && !isNpx) {
|
|
183
|
+
try {
|
|
184
|
+
const whichCmd = process.platform === 'win32' ? 'where code' : 'command -v code || which code || true';
|
|
185
|
+
const resolved = execSync(whichCmd, { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).toString().split(/\r?\n/).filter(Boolean)[0];
|
|
186
|
+
if (resolved) {
|
|
187
|
+
let contents = '';
|
|
188
|
+
try {
|
|
189
|
+
contents = readFileSync(resolved, 'utf8');
|
|
190
|
+
} catch {
|
|
191
|
+
contents = '';
|
|
192
|
+
}
|
|
193
|
+
const looksLikeOurs = contents.includes('@just-every/code') || contents.includes('bin/coder.js');
|
|
194
|
+
if (!looksLikeOurs) {
|
|
195
|
+
console.warn('[notice] Found an existing `code` on PATH at:');
|
|
196
|
+
console.warn(` ${resolved}`);
|
|
197
|
+
console.warn('[notice] We will still install our CLI, also available as `coder`.');
|
|
198
|
+
console.warn(' If `code` runs another tool, prefer using: coder');
|
|
199
|
+
console.warn(' Or run our CLI explicitly via: npx -y @just-every/code');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Ignore detection failures; proceed with install.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const targetTriple = getTargetTriple();
|
|
208
|
+
const isWindows = platform() === 'win32';
|
|
209
|
+
const binaryExt = isWindows ? '.exe' : '';
|
|
210
|
+
|
|
211
|
+
const binDir = join(__dirname, 'bin');
|
|
212
|
+
if (!existsSync(binDir)) {
|
|
213
|
+
mkdirSync(binDir, { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Get package version - use readFileSync for compatibility
|
|
217
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
218
|
+
const version = packageJson.version;
|
|
219
|
+
|
|
220
|
+
// Download only the primary binary; we'll create wrappers for legacy names.
|
|
221
|
+
const binaries = ['code'];
|
|
222
|
+
|
|
223
|
+
console.log(`Installing @just-every/code v${version} for ${targetTriple}...`);
|
|
224
|
+
|
|
225
|
+
for (const binary of binaries) {
|
|
226
|
+
const binaryName = `${binary}-${targetTriple}${binaryExt}`;
|
|
227
|
+
const localPath = join(binDir, binaryName);
|
|
228
|
+
const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
|
|
229
|
+
|
|
230
|
+
// Skip if already exists and has correct permissions
|
|
231
|
+
if (existsSync(localPath)) {
|
|
232
|
+
// Always try to fix permissions on Unix-like systems
|
|
233
|
+
if (!isWindows) {
|
|
234
|
+
try {
|
|
235
|
+
chmodSync(localPath, 0o755);
|
|
236
|
+
console.log(`✓ ${binaryName} already exists (permissions fixed)`);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.log(`✓ ${binaryName} already exists`);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
console.log(`✓ ${binaryName} already exists`);
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Fast path: if a valid cached binary exists for this version+triple, reuse it.
|
|
247
|
+
try {
|
|
248
|
+
if (existsSync(cachePath)) {
|
|
249
|
+
const valid = validateDownloadedBinary(cachePath);
|
|
250
|
+
if (valid.ok) {
|
|
251
|
+
copyFileSync(cachePath, localPath);
|
|
252
|
+
if (!isWindows) chmodSync(localPath, 0o755);
|
|
253
|
+
console.log(`✓ Installed ${binaryName} from user cache`);
|
|
254
|
+
continue; // next binary
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// Ignore cache errors and fall through to normal paths
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// First try platform package via npm optionalDependencies (fast path on npm CDN).
|
|
262
|
+
const require = createRequire(import.meta.url);
|
|
263
|
+
const platformPkg = (() => {
|
|
264
|
+
const name = (() => {
|
|
265
|
+
if (isWindows) return '@just-every/code-win32-x64';
|
|
266
|
+
const plt = platform();
|
|
267
|
+
const cpu = arch();
|
|
268
|
+
if (plt === 'darwin' && cpu === 'arm64') return '@just-every/code-darwin-arm64';
|
|
269
|
+
if (plt === 'darwin' && cpu === 'x64') return '@just-every/code-darwin-x64';
|
|
270
|
+
if (plt === 'linux' && cpu === 'x64') return '@just-every/code-linux-x64-musl';
|
|
271
|
+
if (plt === 'linux' && cpu === 'arm64') return '@just-every/code-linux-arm64-musl';
|
|
272
|
+
return null;
|
|
273
|
+
})();
|
|
274
|
+
if (!name) return null;
|
|
275
|
+
try {
|
|
276
|
+
const pkgJsonPath = require.resolve(`${name}/package.json`);
|
|
277
|
+
const pkgDir = dirname(pkgJsonPath);
|
|
278
|
+
return { name, dir: pkgDir };
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
})();
|
|
283
|
+
|
|
284
|
+
if (platformPkg) {
|
|
285
|
+
try {
|
|
286
|
+
// Expect binary inside platform package bin directory
|
|
287
|
+
const src = join(platformPkg.dir, 'bin', binaryName);
|
|
288
|
+
if (!existsSync(src)) {
|
|
289
|
+
throw new Error(`platform package missing binary: ${platformPkg.name}`);
|
|
290
|
+
}
|
|
291
|
+
copyFileSync(src, localPath);
|
|
292
|
+
if (!isWindows) chmodSync(localPath, 0o755);
|
|
293
|
+
console.log(`✓ Installed ${binaryName} from ${platformPkg.name}`);
|
|
294
|
+
// Populate cache for future npx runs
|
|
295
|
+
try {
|
|
296
|
+
if (!existsSync(cachePath)) {
|
|
297
|
+
copyFileSync(localPath, cachePath);
|
|
298
|
+
}
|
|
299
|
+
} catch {}
|
|
300
|
+
continue; // next binary
|
|
301
|
+
} catch (e) {
|
|
302
|
+
console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Decide archive format per OS with fallback on macOS/Linux:
|
|
307
|
+
// - Windows: .zip
|
|
308
|
+
// - macOS/Linux: prefer .zst if `zstd` CLI is available; otherwise use .tar.gz
|
|
309
|
+
const isWin = isWindows;
|
|
310
|
+
let useZst = false;
|
|
311
|
+
if (!isWin) {
|
|
312
|
+
try {
|
|
313
|
+
execSync('zstd --version', { stdio: 'ignore', shell: true });
|
|
314
|
+
useZst = true;
|
|
315
|
+
} catch {
|
|
316
|
+
useZst = false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const archiveName = isWin ? `${binaryName}.zip` : (useZst ? `${binaryName}.zst` : `${binaryName}.tar.gz`);
|
|
320
|
+
const downloadUrl = `https://github.com/just-every/code/releases/download/v${version}/${archiveName}`;
|
|
321
|
+
|
|
322
|
+
console.log(`Downloading ${archiveName}...`);
|
|
323
|
+
try {
|
|
324
|
+
const tmpPath = join(binDir, `.${archiveName}.part`);
|
|
325
|
+
await downloadBinary(downloadUrl, tmpPath);
|
|
326
|
+
|
|
327
|
+
if (isWin) {
|
|
328
|
+
// Unzip the single-file archive using PowerShell (built-in)
|
|
329
|
+
try {
|
|
330
|
+
const psCmd = `powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmpPath}' -DestinationPath '${binDir}' -Force"`;
|
|
331
|
+
execSync(psCmd, { stdio: 'ignore' });
|
|
332
|
+
} catch (e) {
|
|
333
|
+
throw new Error(`failed to unzip archive: ${e.message}`);
|
|
334
|
+
} finally {
|
|
335
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
if (useZst) {
|
|
339
|
+
// Decompress .zst via system zstd
|
|
340
|
+
try {
|
|
341
|
+
execSync(`zstd -d '${tmpPath}' -o '${localPath}'`, { stdio: 'ignore', shell: true });
|
|
342
|
+
} catch (e) {
|
|
343
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
344
|
+
throw new Error(`failed to decompress .zst (need zstd CLI): ${e.message}`);
|
|
345
|
+
}
|
|
346
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
347
|
+
} else {
|
|
348
|
+
// Extract .tar.gz using system tar
|
|
349
|
+
try {
|
|
350
|
+
execSync(`tar -xzf '${tmpPath}' -C '${binDir}'`, { stdio: 'ignore', shell: true });
|
|
351
|
+
} catch (e) {
|
|
352
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
353
|
+
throw new Error(`failed to extract .tar.gz: ${e.message}`);
|
|
354
|
+
}
|
|
355
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Validate header to avoid corrupt binaries causing spawn EFTYPE/ENOEXEC
|
|
360
|
+
const valid = validateDownloadedBinary(localPath);
|
|
361
|
+
if (!valid.ok) {
|
|
362
|
+
try { unlinkSync(localPath); } catch {}
|
|
363
|
+
throw new Error(`invalid binary (${valid.reason})`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Make executable on Unix-like systems
|
|
367
|
+
if (!isWindows) {
|
|
368
|
+
chmodSync(localPath, 0o755);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
console.log(`✓ Installed ${binaryName}`);
|
|
372
|
+
// Save into persistent cache for future fast installs
|
|
373
|
+
try {
|
|
374
|
+
copyFileSync(localPath, cachePath);
|
|
375
|
+
} catch {}
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error(`✗ Failed to install ${binaryName}: ${error.message}`);
|
|
378
|
+
console.error(` Downloaded from: ${downloadUrl}`);
|
|
379
|
+
// Continue with other binaries even if one fails
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Create platform-specific symlink/copy for main binary
|
|
384
|
+
const mainBinary = `code-${targetTriple}${binaryExt}`;
|
|
385
|
+
const mainBinaryPath = join(binDir, mainBinary);
|
|
386
|
+
|
|
387
|
+
if (existsSync(mainBinaryPath)) {
|
|
388
|
+
try {
|
|
389
|
+
const stats = statSync(mainBinaryPath);
|
|
390
|
+
if (!stats.size) {
|
|
391
|
+
throw new Error('binary is empty (download likely failed)');
|
|
392
|
+
}
|
|
393
|
+
const valid = validateDownloadedBinary(mainBinaryPath);
|
|
394
|
+
if (!valid.ok) {
|
|
395
|
+
console.warn(`⚠ Main dev binary appears invalid: ${valid.reason}`);
|
|
396
|
+
console.warn(' Try reinstalling or check your network/proxy settings.');
|
|
397
|
+
}
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.warn(`⚠ Main dev binary appears invalid: ${e.message}`);
|
|
400
|
+
console.warn(' Try reinstalling or check your network/proxy settings.');
|
|
401
|
+
}
|
|
402
|
+
console.log('Setting up main dev binary...');
|
|
403
|
+
|
|
404
|
+
// On Windows, we can't use symlinks easily, so update the JS wrapper
|
|
405
|
+
// On Unix, the JS wrapper will find the correct binary
|
|
406
|
+
console.log('✓ Installation complete!');
|
|
407
|
+
} else {
|
|
408
|
+
console.warn('⚠ Main code binary not found. You may need to build from source.');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Handle collisions (e.g., VS Code) and add wrappers. We no longer publish a
|
|
412
|
+
// `code` bin in package.json. Instead, for global installs we create a `code`
|
|
413
|
+
// wrapper only when there is no conflicting `code` earlier on PATH. This avoids
|
|
414
|
+
// hijacking the VS Code CLI while still giving users a friendly name when safe.
|
|
415
|
+
// For upgrades from older versions that published a `code` bin, we also remove
|
|
416
|
+
// our old shim if a conflict is detected.
|
|
417
|
+
if (isGlobal && !isNpx) try {
|
|
418
|
+
const isTTY = process.stdout && process.stdout.isTTY;
|
|
419
|
+
const isWindows = platform() === 'win32';
|
|
420
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
421
|
+
const isBun = ua.includes('bun') || !!process.env.BUN_INSTALL;
|
|
422
|
+
|
|
423
|
+
const installedCmds = new Set(['coder']); // global install always exposes coder via package manager
|
|
424
|
+
const skippedCmds = [];
|
|
425
|
+
|
|
426
|
+
// Helper to resolve all 'code' on PATH
|
|
427
|
+
const resolveAllOnPath = () => {
|
|
428
|
+
try {
|
|
429
|
+
if (isWindows) {
|
|
430
|
+
const out = execSync('where code', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
431
|
+
return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
432
|
+
}
|
|
433
|
+
let out = '';
|
|
434
|
+
try {
|
|
435
|
+
out = execSync('bash -lc "which -a code 2>/dev/null"', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
436
|
+
} catch {
|
|
437
|
+
try {
|
|
438
|
+
out = execSync('command -v code || true', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
439
|
+
} catch { out = ''; }
|
|
440
|
+
}
|
|
441
|
+
return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
442
|
+
} catch {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
if (isBun) {
|
|
448
|
+
// Bun creates shims for every bin; if another 'code' exists elsewhere on PATH, remove Bun's shim
|
|
449
|
+
let bunBin = '';
|
|
450
|
+
try {
|
|
451
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
452
|
+
const bunBase = process.env.BUN_INSTALL || join(home, '.bun');
|
|
453
|
+
bunBin = join(bunBase, 'bin');
|
|
454
|
+
} catch {}
|
|
455
|
+
|
|
456
|
+
const bunShim = join(bunBin || '', isWindows ? 'code.cmd' : 'code');
|
|
457
|
+
const candidates = resolveAllOnPath();
|
|
458
|
+
const other = candidates.find(p => p && (!bunBin || !p.startsWith(bunBin)));
|
|
459
|
+
if (other && existsSync(bunShim)) {
|
|
460
|
+
try {
|
|
461
|
+
unlinkSync(bunShim);
|
|
462
|
+
console.log(`✓ Skipped global 'code' shim under Bun (existing: ${other})`);
|
|
463
|
+
skippedCmds.push({ name: 'code', reason: `existing: ${other}` });
|
|
464
|
+
} catch (e) {
|
|
465
|
+
console.log(`⚠ Could not remove Bun shim '${bunShim}': ${e.message}`);
|
|
466
|
+
}
|
|
467
|
+
} else if (!other) {
|
|
468
|
+
// No conflict: create a wrapper that forwards to `coder`
|
|
469
|
+
try {
|
|
470
|
+
const wrapperPath = bunShim;
|
|
471
|
+
if (isWindows) {
|
|
472
|
+
const content = `@echo off\r\n"%~dp0coder" %*\r\n`;
|
|
473
|
+
writeFileSync(wrapperPath, content);
|
|
474
|
+
} else {
|
|
475
|
+
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
|
|
476
|
+
writeFileSync(wrapperPath, content);
|
|
477
|
+
chmodSync(wrapperPath, 0o755);
|
|
478
|
+
}
|
|
479
|
+
console.log("✓ Created 'code' wrapper -> coder (bun)");
|
|
480
|
+
installedCmds.add('code');
|
|
481
|
+
} catch (e) {
|
|
482
|
+
console.log(`⚠ Failed to create 'code' wrapper (bun): ${e.message}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Print summary for Bun
|
|
487
|
+
const list = Array.from(installedCmds).sort().join(', ');
|
|
488
|
+
console.log(`Commands installed (bun): ${list}`);
|
|
489
|
+
if (skippedCmds.length) {
|
|
490
|
+
for (const s of skippedCmds) console.error(`Commands skipped: ${s.name} (${s.reason})`);
|
|
491
|
+
console.error('→ Use `coder` to run this tool.');
|
|
492
|
+
}
|
|
493
|
+
// Final friendly usage hint
|
|
494
|
+
if (installedCmds.has('code')) {
|
|
495
|
+
console.log("Use 'dev' to launch Hanzo Dev.");
|
|
496
|
+
} else {
|
|
497
|
+
console.log("Use 'dev' to launch Hanzo Dev.");
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
// npm/pnpm/yarn path
|
|
501
|
+
let globalBin = '';
|
|
502
|
+
try {
|
|
503
|
+
globalBin = execSync('npm bin -g', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
504
|
+
} catch {}
|
|
505
|
+
|
|
506
|
+
const ourShim = join(globalBin || '', isWindows ? 'code.cmd' : 'code');
|
|
507
|
+
const candidates = resolveAllOnPath();
|
|
508
|
+
const others = candidates.filter(p => p && (!ourShim || p !== ourShim));
|
|
509
|
+
const collision = others.length > 0;
|
|
510
|
+
|
|
511
|
+
const ensureWrapper = (name, args) => {
|
|
512
|
+
if (!globalBin) return;
|
|
513
|
+
try {
|
|
514
|
+
const wrapperPath = join(globalBin, isWindows ? `${name}.cmd` : name);
|
|
515
|
+
if (isWindows) {
|
|
516
|
+
const content = `@echo off\r\n"%~dp0${collision ? 'coder' : 'code'}" ${args} %*\r\n`;
|
|
517
|
+
writeFileSync(wrapperPath, content);
|
|
518
|
+
} else {
|
|
519
|
+
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/${collision ? 'coder' : 'code'}" ${args} "$@"\n`;
|
|
520
|
+
writeFileSync(wrapperPath, content);
|
|
521
|
+
chmodSync(wrapperPath, 0o755);
|
|
522
|
+
}
|
|
523
|
+
console.log(`✓ Created wrapper '${name}' -> ${collision ? 'coder' : 'code'} ${args}`);
|
|
524
|
+
installedCmds.add(name);
|
|
525
|
+
} catch (e) {
|
|
526
|
+
console.log(`⚠ Failed to create '${name}' wrapper: ${e.message}`);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// Always create legacy wrappers so existing scripts keep working
|
|
531
|
+
ensureWrapper('code-tui', '');
|
|
532
|
+
ensureWrapper('code-exec', 'exec');
|
|
533
|
+
|
|
534
|
+
if (collision) {
|
|
535
|
+
console.error('⚠ Detected existing `code` on PATH:');
|
|
536
|
+
for (const p of others) console.error(` - ${p}`);
|
|
537
|
+
if (globalBin) {
|
|
538
|
+
try {
|
|
539
|
+
if (existsSync(ourShim)) {
|
|
540
|
+
unlinkSync(ourShim);
|
|
541
|
+
console.error(`✓ Skipped global 'code' shim (removed ${ourShim})`);
|
|
542
|
+
skippedCmds.push({ name: 'code', reason: `existing: ${others[0]}` });
|
|
543
|
+
}
|
|
544
|
+
} catch (e) {
|
|
545
|
+
console.error(`⚠ Could not remove npm shim '${ourShim}': ${e.message}`);
|
|
546
|
+
}
|
|
547
|
+
console.error('→ Use `coder` to run this tool.');
|
|
548
|
+
} else {
|
|
549
|
+
console.log('Note: could not determine npm global bin; skipping alias creation.');
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
// No collision; ensure a 'code' wrapper exists forwarding to 'coder'
|
|
553
|
+
if (globalBin) {
|
|
554
|
+
try {
|
|
555
|
+
const content = isWindows
|
|
556
|
+
? `@echo off\r\n"%~dp0coder" %*\r\n`
|
|
557
|
+
: `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
|
|
558
|
+
writeFileSync(ourShim, content);
|
|
559
|
+
if (!isWindows) chmodSync(ourShim, 0o755);
|
|
560
|
+
console.log("✓ Created 'code' wrapper -> coder");
|
|
561
|
+
installedCmds.add('code');
|
|
562
|
+
} catch (e) {
|
|
563
|
+
console.log(`⚠ Failed to create 'code' wrapper: ${e.message}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Print summary for npm/pnpm/yarn
|
|
569
|
+
const list = Array.from(installedCmds).sort().join(', ');
|
|
570
|
+
console.log(`Commands installed: ${list}`);
|
|
571
|
+
if (skippedCmds.length) {
|
|
572
|
+
for (const s of skippedCmds) console.log(`Commands skipped: ${s.name} (${s.reason})`);
|
|
573
|
+
}
|
|
574
|
+
// Final friendly usage hint
|
|
575
|
+
if (installedCmds.has('code')) {
|
|
576
|
+
console.log("Use 'dev' to launch Hanzo Dev.");
|
|
577
|
+
} else {
|
|
578
|
+
console.log("Use 'dev' to launch Hanzo Dev.");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
// non-fatal
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
main().catch(error => {
|
|
587
|
+
console.error('Installation failed:', error);
|
|
588
|
+
process.exit(1);
|
|
589
|
+
});
|
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
|
-
}
|