@hanzo/dev 0.6.90 → 0.6.92
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 +111 -346
- package/bin/{dev.js → coder.js} +83 -44
- package/bin/codex.js +144 -0
- package/package.json +11 -11
- package/postinstall.js +487 -444
- package/scripts/preinstall.js +23 -45
- package/scripts/windows-cleanup.ps1 +2 -2
package/postinstall.js
CHANGED
|
@@ -1,100 +1,101 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
writeFileSync,
|
|
12
|
-
unlinkSync,
|
|
13
|
-
statSync,
|
|
14
|
-
openSync,
|
|
15
|
-
closeSync,
|
|
16
|
-
copyFileSync,
|
|
17
|
-
fsyncSync,
|
|
18
|
-
renameSync,
|
|
19
|
-
realpathSync,
|
|
20
|
-
} from "fs";
|
|
21
|
-
import { join, dirname, resolve } from "path";
|
|
22
|
-
import { fileURLToPath } from "url";
|
|
23
|
-
import { get } from "https";
|
|
24
|
-
import { platform, arch, tmpdir } from "os";
|
|
25
|
-
import { execSync } from "child_process";
|
|
26
|
-
import { createRequire } from "module";
|
|
2
|
+
// Non-functional change to trigger release workflow
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync, unlinkSync, statSync, openSync, closeSync, copyFileSync, fsyncSync, renameSync, realpathSync } from 'fs';
|
|
5
|
+
import { join, dirname } 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';
|
|
27
11
|
|
|
28
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
13
|
|
|
14
|
+
// Map Node.js platform/arch to Rust target triples
|
|
30
15
|
function getTargetTriple() {
|
|
31
16
|
const platformMap = {
|
|
32
|
-
darwin:
|
|
33
|
-
linux:
|
|
34
|
-
win32:
|
|
17
|
+
'darwin': 'apple-darwin',
|
|
18
|
+
'linux': 'unknown-linux-musl', // Default to musl for better compatibility
|
|
19
|
+
'win32': 'pc-windows-msvc'
|
|
35
20
|
};
|
|
36
|
-
|
|
21
|
+
|
|
37
22
|
const archMap = {
|
|
38
|
-
x64:
|
|
39
|
-
arm64:
|
|
23
|
+
'x64': 'x86_64',
|
|
24
|
+
'arm64': 'aarch64'
|
|
40
25
|
};
|
|
41
|
-
|
|
26
|
+
|
|
42
27
|
const rustArch = archMap[arch()] || arch();
|
|
43
28
|
const rustPlatform = platformMap[platform()] || platform();
|
|
44
|
-
|
|
29
|
+
|
|
45
30
|
return `${rustArch}-${rustPlatform}`;
|
|
46
31
|
}
|
|
47
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.
|
|
48
35
|
function getCacheDir(version) {
|
|
49
36
|
const plt = platform();
|
|
50
|
-
const home = process.env.HOME || process.env.USERPROFILE ||
|
|
51
|
-
let base =
|
|
52
|
-
if (plt ===
|
|
53
|
-
base = process.env.LOCALAPPDATA || join(home,
|
|
54
|
-
} else if (plt ===
|
|
55
|
-
base = join(home,
|
|
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');
|
|
56
43
|
} else {
|
|
57
|
-
base = process.env.XDG_CACHE_HOME || join(home,
|
|
44
|
+
base = process.env.XDG_CACHE_HOME || join(home, '.cache');
|
|
58
45
|
}
|
|
59
|
-
const dir = join(base,
|
|
46
|
+
const dir = join(base, 'hanzoai', 'dev', version);
|
|
60
47
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
61
48
|
return dir;
|
|
62
49
|
}
|
|
63
50
|
|
|
64
51
|
function getCachedBinaryPath(version, targetTriple, isWindows) {
|
|
65
|
-
const ext = isWindows ?
|
|
52
|
+
const ext = isWindows ? '.exe' : '';
|
|
66
53
|
const cacheDir = getCacheDir(version);
|
|
67
54
|
return join(cacheDir, `dev-${targetTriple}${ext}`);
|
|
68
55
|
}
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
// Substrings that appear only in shims we wrote ourselves. Used before removing
|
|
58
|
+
// or replacing a `code` on PATH, so a third party's `code` is never touched.
|
|
59
|
+
const SHIM_SIGNATURES = [
|
|
60
|
+
'@hanzo/dev',
|
|
61
|
+
'$(dirname "$0")/dev',
|
|
62
|
+
'%~dp0dev'
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function shimContentsLookOurs(contents) {
|
|
66
|
+
return SHIM_SIGNATURES.some(sig => contents.includes(sig));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function looksLikeOurShim(path) {
|
|
72
70
|
try {
|
|
73
|
-
const
|
|
74
|
-
return
|
|
71
|
+
const contents = readFileSync(path, 'utf8');
|
|
72
|
+
return shimContentsLookOurs(contents);
|
|
75
73
|
} catch {
|
|
76
74
|
return false;
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
|
78
|
+
function isWSL() {
|
|
79
|
+
if (platform() !== 'linux') return false;
|
|
80
|
+
try {
|
|
81
|
+
const ver = readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
82
|
+
return ver.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
|
|
83
|
+
} catch { return false; }
|
|
84
|
+
}
|
|
85
|
+
|
|
80
86
|
function isPathOnWindowsFs(p) {
|
|
81
87
|
try {
|
|
82
|
-
const mounts = readFileSync(
|
|
83
|
-
|
|
84
|
-
.filter(Boolean);
|
|
85
|
-
let best = { mount: "/", type: "unknown", len: 1 };
|
|
88
|
+
const mounts = readFileSync('/proc/mounts', 'utf8').split(/\n/).filter(Boolean);
|
|
89
|
+
let best = { mount: '/', type: 'unknown', len: 1 };
|
|
86
90
|
for (const line of mounts) {
|
|
87
|
-
const parts = line.split(
|
|
91
|
+
const parts = line.split(' ');
|
|
88
92
|
if (parts.length < 3) continue;
|
|
89
93
|
const mnt = parts[1];
|
|
90
94
|
const typ = parts[2];
|
|
91
|
-
if (p.startsWith(mnt) && mnt.length > best.len)
|
|
92
|
-
best = { mount: mnt, type: typ, len: mnt.length };
|
|
95
|
+
if (p.startsWith(mnt) && mnt.length > best.len) best = { mount: mnt, type: typ, len: mnt.length };
|
|
93
96
|
}
|
|
94
|
-
return best.type ===
|
|
95
|
-
} catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
97
|
+
return best.type === 'drvfs' || best.type === 'cifs';
|
|
98
|
+
} catch { return false; }
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
async function writeCacheAtomic(srcPath, cachePath) {
|
|
@@ -105,185 +106,145 @@ async function writeCacheAtomic(srcPath, cachePath) {
|
|
|
105
106
|
}
|
|
106
107
|
} catch {}
|
|
107
108
|
const dir = dirname(cachePath);
|
|
108
|
-
if (!existsSync(dir)) {
|
|
109
|
-
|
|
110
|
-
mkdirSync(dir, { recursive: true });
|
|
111
|
-
} catch {}
|
|
112
|
-
}
|
|
113
|
-
const tmp = cachePath + ".tmp-" + Math.random().toString(36).slice(2, 8);
|
|
109
|
+
if (!existsSync(dir)) { try { mkdirSync(dir, { recursive: true }); } catch {} }
|
|
110
|
+
const tmp = cachePath + '.tmp-' + Math.random().toString(36).slice(2, 8);
|
|
114
111
|
copyFileSync(srcPath, tmp);
|
|
115
|
-
try {
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
fsyncSync(fd);
|
|
119
|
-
} finally {
|
|
120
|
-
closeSync(fd);
|
|
121
|
-
}
|
|
122
|
-
} catch {}
|
|
112
|
+
try { const fd = openSync(tmp, 'r'); try { fsyncSync(fd); } finally { closeSync(fd); } } catch {}
|
|
113
|
+
// Retry with exponential backoff up to ~1.6s total
|
|
123
114
|
const delays = [100, 200, 400, 800, 1200, 1600];
|
|
124
115
|
for (let i = 0; i < delays.length; i++) {
|
|
125
116
|
try {
|
|
126
|
-
if (existsSync(cachePath)) {
|
|
127
|
-
try {
|
|
128
|
-
unlinkSync(cachePath);
|
|
129
|
-
} catch {}
|
|
130
|
-
}
|
|
117
|
+
if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
|
|
131
118
|
renameSync(tmp, cachePath);
|
|
132
119
|
return;
|
|
133
120
|
} catch {
|
|
134
|
-
await new Promise(
|
|
121
|
+
await new Promise(r => setTimeout(r, delays[i]));
|
|
135
122
|
}
|
|
136
123
|
}
|
|
137
|
-
if (existsSync(cachePath)) {
|
|
138
|
-
try {
|
|
139
|
-
unlinkSync(cachePath);
|
|
140
|
-
} catch {}
|
|
141
|
-
}
|
|
124
|
+
if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
|
|
142
125
|
renameSync(tmp, cachePath);
|
|
143
126
|
}
|
|
144
127
|
|
|
145
128
|
function resolveGlobalBinDir() {
|
|
146
129
|
const plt = platform();
|
|
147
|
-
const userAgent = process.env.npm_config_user_agent ||
|
|
130
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
148
131
|
|
|
149
132
|
const fromPrefix = (prefixPath) => {
|
|
150
|
-
if (!prefixPath) return
|
|
151
|
-
return plt ===
|
|
133
|
+
if (!prefixPath) return '';
|
|
134
|
+
return plt === 'win32' ? prefixPath : join(prefixPath, 'bin');
|
|
152
135
|
};
|
|
153
136
|
|
|
154
|
-
const prefixEnv = process.env.npm_config_prefix || process.env.PREFIX ||
|
|
137
|
+
const prefixEnv = process.env.npm_config_prefix || process.env.PREFIX || '';
|
|
155
138
|
const direct = fromPrefix(prefixEnv);
|
|
156
139
|
if (direct) return direct;
|
|
157
140
|
|
|
158
141
|
const tryExec = (command) => {
|
|
159
142
|
try {
|
|
160
143
|
return execSync(command, {
|
|
161
|
-
stdio: [
|
|
144
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
162
145
|
shell: true,
|
|
163
|
-
})
|
|
164
|
-
.toString()
|
|
165
|
-
.trim();
|
|
146
|
+
}).toString().trim();
|
|
166
147
|
} catch {
|
|
167
|
-
return
|
|
148
|
+
return '';
|
|
168
149
|
}
|
|
169
150
|
};
|
|
170
151
|
|
|
171
|
-
const prefixFromNpm = fromPrefix(tryExec(
|
|
152
|
+
const prefixFromNpm = fromPrefix(tryExec('npm prefix -g'));
|
|
172
153
|
if (prefixFromNpm) return prefixFromNpm;
|
|
173
154
|
|
|
174
|
-
const binFromNpm = tryExec(
|
|
155
|
+
const binFromNpm = tryExec('npm bin -g');
|
|
175
156
|
if (binFromNpm) return binFromNpm;
|
|
176
157
|
|
|
177
|
-
if (userAgent.includes(
|
|
178
|
-
const pnpmBin = tryExec(
|
|
158
|
+
if (userAgent.includes('pnpm')) {
|
|
159
|
+
const pnpmBin = tryExec('pnpm bin --global');
|
|
179
160
|
if (pnpmBin) return pnpmBin;
|
|
161
|
+
const pnpmPrefix = fromPrefix(tryExec('pnpm env get prefix'));
|
|
162
|
+
if (pnpmPrefix) return pnpmPrefix;
|
|
180
163
|
}
|
|
181
164
|
|
|
182
|
-
if (userAgent.includes(
|
|
183
|
-
const yarnBin = tryExec(
|
|
165
|
+
if (userAgent.includes('yarn')) {
|
|
166
|
+
const yarnBin = tryExec('yarn global bin');
|
|
184
167
|
if (yarnBin) return yarnBin;
|
|
185
168
|
}
|
|
186
169
|
|
|
187
|
-
return
|
|
170
|
+
return '';
|
|
188
171
|
}
|
|
189
172
|
|
|
190
173
|
async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
|
|
191
|
-
const sleep = (ms) => new Promise(
|
|
192
|
-
|
|
193
|
-
const doAttempt = () =>
|
|
194
|
-
new Promise((resolve, reject) => {
|
|
195
|
-
const attempt = (currentUrl, redirectsLeft) => {
|
|
196
|
-
const req = get(currentUrl, (response) => {
|
|
197
|
-
const status = response.statusCode || 0;
|
|
198
|
-
const location = response.headers.location;
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
(status === 301 ||
|
|
202
|
-
status === 302 ||
|
|
203
|
-
status === 303 ||
|
|
204
|
-
status === 307 ||
|
|
205
|
-
status === 308) &&
|
|
206
|
-
location
|
|
207
|
-
) {
|
|
208
|
-
if (redirectsLeft <= 0) {
|
|
209
|
-
reject(
|
|
210
|
-
new Error(`Too many redirects while downloading ${currentUrl}`),
|
|
211
|
-
);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
attempt(location, redirectsLeft - 1);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
174
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
217
175
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const timeoutMs = 30000;
|
|
176
|
+
const doAttempt = () => new Promise((resolve, reject) => {
|
|
177
|
+
const attempt = (currentUrl, redirectsLeft) => {
|
|
178
|
+
const req = get(currentUrl, (response) => {
|
|
179
|
+
const status = response.statusCode || 0;
|
|
180
|
+
const location = response.headers.location;
|
|
224
181
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
182
|
+
if ((status === 301 || status === 302 || status === 303 || status === 307 || status === 308) && location) {
|
|
183
|
+
if (redirectsLeft <= 0) {
|
|
184
|
+
reject(new Error(`Too many redirects while downloading ${currentUrl}`));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
attempt(location, redirectsLeft - 1);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
231
190
|
|
|
191
|
+
if (status === 200) {
|
|
192
|
+
const expected = parseInt(response.headers['content-length'] || '0', 10) || 0;
|
|
193
|
+
let bytes = 0;
|
|
194
|
+
let timer;
|
|
195
|
+
const timeoutMs = 30000; // 30s inactivity timeout
|
|
196
|
+
|
|
197
|
+
const resetTimer = () => {
|
|
198
|
+
if (timer) clearTimeout(timer);
|
|
199
|
+
timer = setTimeout(() => {
|
|
200
|
+
req.destroy(new Error('download stalled'));
|
|
201
|
+
}, timeoutMs);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
resetTimer();
|
|
205
|
+
response.on('data', (chunk) => {
|
|
206
|
+
bytes += chunk.length;
|
|
232
207
|
resetTimer();
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
file.on("error", (err) => {
|
|
262
|
-
if (timer) clearTimeout(timer);
|
|
263
|
-
try {
|
|
264
|
-
unlinkSync(dest);
|
|
265
|
-
} catch {}
|
|
266
|
-
reject(err);
|
|
267
|
-
});
|
|
268
|
-
} else {
|
|
269
|
-
reject(new Error(`Failed to download: HTTP ${status}`));
|
|
270
|
-
}
|
|
271
|
-
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const file = createWriteStream(dest);
|
|
211
|
+
response.pipe(file);
|
|
212
|
+
file.on('finish', () => {
|
|
213
|
+
if (timer) clearTimeout(timer);
|
|
214
|
+
file.close();
|
|
215
|
+
if (expected && bytes !== expected) {
|
|
216
|
+
try { unlinkSync(dest); } catch {}
|
|
217
|
+
reject(new Error(`incomplete download: got ${bytes} of ${expected} bytes`));
|
|
218
|
+
} else if (bytes === 0) {
|
|
219
|
+
try { unlinkSync(dest); } catch {}
|
|
220
|
+
reject(new Error('empty download'));
|
|
221
|
+
} else {
|
|
222
|
+
resolve();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
file.on('error', (err) => {
|
|
226
|
+
if (timer) clearTimeout(timer);
|
|
227
|
+
try { unlinkSync(dest); } catch {}
|
|
228
|
+
reject(err);
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
reject(new Error(`Failed to download: HTTP ${status}`));
|
|
232
|
+
}
|
|
233
|
+
});
|
|
272
234
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
reject(err);
|
|
278
|
-
});
|
|
235
|
+
req.on('error', (err) => {
|
|
236
|
+
try { unlinkSync(dest); } catch {}
|
|
237
|
+
reject(err);
|
|
238
|
+
});
|
|
279
239
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
};
|
|
240
|
+
// Absolute request timeout to avoid hanging forever
|
|
241
|
+
req.setTimeout(120000, () => {
|
|
242
|
+
req.destroy(new Error('download timed out'));
|
|
243
|
+
});
|
|
244
|
+
};
|
|
284
245
|
|
|
285
|
-
|
|
286
|
-
|
|
246
|
+
attempt(url, maxRedirects);
|
|
247
|
+
});
|
|
287
248
|
|
|
288
249
|
let attemptNum = 0;
|
|
289
250
|
while (true) {
|
|
@@ -302,38 +263,22 @@ function validateDownloadedBinary(p) {
|
|
|
302
263
|
try {
|
|
303
264
|
const st = statSync(p);
|
|
304
265
|
if (!st.isFile() || st.size === 0) {
|
|
305
|
-
return { ok: false, reason:
|
|
266
|
+
return { ok: false, reason: 'empty or not a regular file' };
|
|
306
267
|
}
|
|
307
|
-
const fd = openSync(p,
|
|
268
|
+
const fd = openSync(p, 'r');
|
|
308
269
|
try {
|
|
309
270
|
const buf = Buffer.alloc(4);
|
|
310
271
|
const n = readSync(fd, buf, 0, 4, 0);
|
|
311
|
-
if (n < 2) return { ok: false, reason:
|
|
272
|
+
if (n < 2) return { ok: false, reason: 'too short' };
|
|
312
273
|
const plt = platform();
|
|
313
|
-
if (plt ===
|
|
314
|
-
if (!(buf[0] === 0x4d && buf[1] === 0x5a))
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
buf[2] === 0x4c &&
|
|
322
|
-
buf[3] === 0x46
|
|
323
|
-
)
|
|
324
|
-
)
|
|
325
|
-
return { ok: false, reason: "invalid ELF header" };
|
|
326
|
-
} else if (plt === "darwin") {
|
|
327
|
-
const isMachO =
|
|
328
|
-
(buf[0] === 0xcf &&
|
|
329
|
-
buf[1] === 0xfa &&
|
|
330
|
-
buf[2] === 0xed &&
|
|
331
|
-
buf[3] === 0xfe) ||
|
|
332
|
-
(buf[0] === 0xca &&
|
|
333
|
-
buf[1] === 0xfe &&
|
|
334
|
-
buf[2] === 0xba &&
|
|
335
|
-
buf[3] === 0xbe);
|
|
336
|
-
if (!isMachO) return { ok: false, reason: "invalid Mach-O header" };
|
|
274
|
+
if (plt === 'win32') {
|
|
275
|
+
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: 'invalid PE header (missing MZ)' };
|
|
276
|
+
} else if (plt === 'linux' || plt === 'android') {
|
|
277
|
+
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: 'invalid ELF header' };
|
|
278
|
+
} else if (plt === 'darwin') {
|
|
279
|
+
const isMachO = (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) ||
|
|
280
|
+
(buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe);
|
|
281
|
+
if (!isMachO) return { ok: false, reason: 'invalid Mach-O header' };
|
|
337
282
|
}
|
|
338
283
|
return { ok: true };
|
|
339
284
|
} finally {
|
|
@@ -344,90 +289,98 @@ function validateDownloadedBinary(p) {
|
|
|
344
289
|
}
|
|
345
290
|
}
|
|
346
291
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
process.
|
|
292
|
+
async function main() {
|
|
293
|
+
// Detect potential PATH conflict with an existing `code` command (e.g., VS Code)
|
|
294
|
+
// Only relevant for global installs; skip for npx/local installs to keep postinstall fast.
|
|
295
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
296
|
+
const isNpx = ua.includes('npx');
|
|
297
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
298
|
+
if (isGlobal && !isNpx) {
|
|
299
|
+
try {
|
|
300
|
+
const whichCmd = process.platform === 'win32' ? 'where code' : 'command -v code || which code || true';
|
|
301
|
+
const resolved = execSync(whichCmd, { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).toString().split(/\r?\n/).filter(Boolean)[0];
|
|
302
|
+
if (resolved) {
|
|
303
|
+
let contents = '';
|
|
304
|
+
try {
|
|
305
|
+
contents = readFileSync(resolved, 'utf8');
|
|
306
|
+
} catch {
|
|
307
|
+
contents = '';
|
|
308
|
+
}
|
|
309
|
+
const looksLikeOurs = shimContentsLookOurs(contents);
|
|
310
|
+
if (!looksLikeOurs) {
|
|
311
|
+
console.warn('[notice] Found an existing `code` on PATH at:');
|
|
312
|
+
console.warn(` ${resolved}`);
|
|
313
|
+
console.warn('[notice] We will still install our CLI, also available as `dev`.');
|
|
314
|
+
console.warn(' If `code` runs another tool, prefer using: dev');
|
|
315
|
+
console.warn(' Or run our CLI explicitly via: npx -y @hanzo/dev');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// Ignore detection failures; proceed with install.
|
|
320
|
+
}
|
|
356
321
|
}
|
|
357
322
|
|
|
358
|
-
const ua = process.env.npm_config_user_agent || "";
|
|
359
|
-
const isNpx = ua.includes("npx");
|
|
360
|
-
const isGlobal = process.env.npm_config_global === "true";
|
|
361
|
-
|
|
362
323
|
const targetTriple = getTargetTriple();
|
|
363
|
-
const isWindows = platform() ===
|
|
364
|
-
const binaryExt = isWindows ?
|
|
365
|
-
|
|
366
|
-
const binDir = join(__dirname,
|
|
324
|
+
const isWindows = platform() === 'win32';
|
|
325
|
+
const binaryExt = isWindows ? '.exe' : '';
|
|
326
|
+
|
|
327
|
+
const binDir = join(__dirname, 'bin');
|
|
367
328
|
if (!existsSync(binDir)) {
|
|
368
329
|
mkdirSync(binDir, { recursive: true });
|
|
369
330
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
);
|
|
331
|
+
|
|
332
|
+
// Get package version - use readFileSync for compatibility
|
|
333
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
374
334
|
const version = packageJson.version;
|
|
375
|
-
|
|
376
|
-
//
|
|
377
|
-
const binaries = [
|
|
335
|
+
|
|
336
|
+
// Download only the primary binary; we'll create wrappers for legacy names.
|
|
337
|
+
const binaries = ['dev'];
|
|
378
338
|
|
|
379
339
|
console.log(`Installing @hanzo/dev v${version} for ${targetTriple}...`);
|
|
380
|
-
|
|
340
|
+
|
|
381
341
|
for (const binary of binaries) {
|
|
382
342
|
const binaryName = `${binary}-${targetTriple}${binaryExt}`;
|
|
383
343
|
const localPath = join(binDir, binaryName);
|
|
384
344
|
const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
|
|
385
|
-
|
|
386
|
-
//
|
|
345
|
+
|
|
346
|
+
// On Windows we avoid placing the executable inside node_modules to prevent
|
|
347
|
+
// EBUSY/EPERM during global upgrades when the binary is in use.
|
|
348
|
+
// We treat the user cache path as the canonical home of the native binary.
|
|
349
|
+
// For macOS/Linux we keep previous behavior and also place a copy in binDir
|
|
350
|
+
// for convenience.
|
|
351
|
+
|
|
352
|
+
// Fast path: if a valid cached binary exists for this version+triple, reuse it.
|
|
387
353
|
try {
|
|
388
354
|
if (existsSync(cachePath)) {
|
|
389
355
|
const valid = validateDownloadedBinary(cachePath);
|
|
390
356
|
if (valid.ok) {
|
|
357
|
+
// Avoid mirroring into node_modules on Windows or WSL-on-NTFS.
|
|
391
358
|
const wsl = isWSL();
|
|
392
|
-
const binDirReal = (() => {
|
|
393
|
-
|
|
394
|
-
return realpathSync(binDir);
|
|
395
|
-
} catch {
|
|
396
|
-
return binDir;
|
|
397
|
-
}
|
|
398
|
-
})();
|
|
399
|
-
const mirrorToLocal = !(
|
|
400
|
-
isWindows ||
|
|
401
|
-
(wsl && isPathOnWindowsFs(binDirReal))
|
|
402
|
-
);
|
|
359
|
+
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
|
360
|
+
const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
|
|
403
361
|
if (mirrorToLocal) {
|
|
404
362
|
copyFileSync(cachePath, localPath);
|
|
405
|
-
try {
|
|
406
|
-
chmodSync(localPath, 0o755);
|
|
407
|
-
} catch {}
|
|
363
|
+
try { chmodSync(localPath, 0o755); } catch {}
|
|
408
364
|
}
|
|
409
365
|
console.log(`✓ ${binaryName} ready from user cache`);
|
|
410
|
-
continue;
|
|
366
|
+
continue; // next binary
|
|
411
367
|
}
|
|
412
368
|
}
|
|
413
369
|
} catch {
|
|
414
|
-
// Ignore cache errors
|
|
370
|
+
// Ignore cache errors and fall through to normal paths
|
|
415
371
|
}
|
|
416
|
-
|
|
417
|
-
//
|
|
372
|
+
|
|
373
|
+
// First try platform package via npm optionalDependencies (fast path on npm CDN).
|
|
418
374
|
const require = createRequire(import.meta.url);
|
|
419
375
|
const platformPkg = (() => {
|
|
420
376
|
const name = (() => {
|
|
421
|
-
if (isWindows) return
|
|
377
|
+
if (isWindows) return '@hanzo/dev-win32-x64';
|
|
422
378
|
const plt = platform();
|
|
423
379
|
const cpu = arch();
|
|
424
|
-
if (plt ===
|
|
425
|
-
|
|
426
|
-
if (plt ===
|
|
427
|
-
if (plt ===
|
|
428
|
-
return "@hanzo/dev-linux-x64-musl";
|
|
429
|
-
if (plt === "linux" && cpu === "arm64")
|
|
430
|
-
return "@hanzo/dev-linux-arm64-musl";
|
|
380
|
+
if (plt === 'darwin' && cpu === 'arm64') return '@hanzo/dev-darwin-arm64';
|
|
381
|
+
if (plt === 'darwin' && cpu === 'x64') return '@hanzo/dev-darwin-x64';
|
|
382
|
+
if (plt === 'linux' && cpu === 'x64') return '@hanzo/dev-linux-x64-musl';
|
|
383
|
+
if (plt === 'linux' && cpu === 'arm64') return '@hanzo/dev-linux-arm64-musl';
|
|
431
384
|
return null;
|
|
432
385
|
})();
|
|
433
386
|
if (!name) return null;
|
|
@@ -442,277 +395,367 @@ export async function runPostinstall(options = {}) {
|
|
|
442
395
|
|
|
443
396
|
if (platformPkg) {
|
|
444
397
|
try {
|
|
445
|
-
|
|
398
|
+
// Expect binary inside platform package bin directory
|
|
399
|
+
const src = join(platformPkg.dir, 'bin', binaryName);
|
|
446
400
|
if (!existsSync(src)) {
|
|
447
|
-
throw new Error(
|
|
448
|
-
`platform package missing binary: ${platformPkg.name}`,
|
|
449
|
-
);
|
|
401
|
+
throw new Error(`platform package missing binary: ${platformPkg.name}`);
|
|
450
402
|
}
|
|
403
|
+
// Populate cache first (canonical location) atomically
|
|
451
404
|
await writeCacheAtomic(src, cachePath);
|
|
405
|
+
// Mirror into local bin only on Unix-like filesystems (not Windows/WSL-on-NTFS)
|
|
452
406
|
const wsl = isWSL();
|
|
453
|
-
const binDirReal = (() => {
|
|
454
|
-
|
|
455
|
-
return realpathSync(binDir);
|
|
456
|
-
} catch {
|
|
457
|
-
return binDir;
|
|
458
|
-
}
|
|
459
|
-
})();
|
|
460
|
-
const mirrorToLocal = !(
|
|
461
|
-
isWindows ||
|
|
462
|
-
(wsl && isPathOnWindowsFs(binDirReal))
|
|
463
|
-
);
|
|
407
|
+
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
|
408
|
+
const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
|
|
464
409
|
if (mirrorToLocal) {
|
|
465
410
|
copyFileSync(cachePath, localPath);
|
|
466
|
-
try {
|
|
467
|
-
chmodSync(localPath, 0o755);
|
|
468
|
-
} catch {}
|
|
411
|
+
try { chmodSync(localPath, 0o755); } catch {}
|
|
469
412
|
}
|
|
470
|
-
console.log(
|
|
471
|
-
|
|
472
|
-
);
|
|
473
|
-
continue;
|
|
413
|
+
console.log(`✓ Installed ${binaryName} from ${platformPkg.name} (cached)`);
|
|
414
|
+
continue; // next binary
|
|
474
415
|
} catch (e) {
|
|
475
|
-
console.warn(
|
|
476
|
-
`⚠ Failed platform package install (${e.message}), falling back to GitHub download`,
|
|
477
|
-
);
|
|
416
|
+
console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
|
|
478
417
|
}
|
|
479
418
|
}
|
|
480
419
|
|
|
481
|
-
//
|
|
420
|
+
// Decide archive format per OS with fallback on macOS/Linux:
|
|
421
|
+
// - Windows: .zip
|
|
422
|
+
// - macOS/Linux: prefer .zst if `zstd` CLI is available; otherwise use .tar.gz
|
|
482
423
|
const isWin = isWindows;
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
try {
|
|
486
|
-
return realpathSync(binDir);
|
|
487
|
-
} catch {
|
|
488
|
-
return binDir;
|
|
489
|
-
}
|
|
490
|
-
})();
|
|
491
|
-
const mirrorToLocal = !(
|
|
492
|
-
isWin ||
|
|
493
|
-
(detectedWSL && isPathOnWindowsFs(binDirReal))
|
|
494
|
-
);
|
|
424
|
+
const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
|
|
425
|
+
const mirrorToLocal = !(isWin || (isWSL() && isPathOnWindowsFs(binDirReal)));
|
|
495
426
|
let useZst = false;
|
|
496
427
|
if (!isWin) {
|
|
497
428
|
try {
|
|
498
|
-
execSync(
|
|
429
|
+
execSync('zstd --version', { stdio: 'ignore', shell: true });
|
|
499
430
|
useZst = true;
|
|
500
431
|
} catch {
|
|
501
432
|
useZst = false;
|
|
502
433
|
}
|
|
503
434
|
}
|
|
504
|
-
const archiveName = isWin
|
|
505
|
-
? `${binaryName}.zip`
|
|
506
|
-
: useZst
|
|
507
|
-
? `${binaryName}.zst`
|
|
508
|
-
: `${binaryName}.tar.gz`;
|
|
435
|
+
const archiveName = isWin ? `${binaryName}.zip` : (useZst ? `${binaryName}.zst` : `${binaryName}.tar.gz`);
|
|
509
436
|
const downloadUrl = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
|
|
510
437
|
|
|
511
438
|
console.log(`Downloading ${archiveName}...`);
|
|
512
439
|
try {
|
|
513
|
-
const needsIsolation = isWin || (!isWin && !mirrorToLocal);
|
|
514
|
-
let safeTempDir = needsIsolation
|
|
515
|
-
|
|
516
|
-
: binDir;
|
|
440
|
+
const needsIsolation = isWin || (!isWin && !mirrorToLocal); // Windows or WSL-on-NTFS
|
|
441
|
+
let safeTempDir = needsIsolation ? join(tmpdir(), 'hanzoai', 'dev', version) : binDir;
|
|
442
|
+
// Ensure staging dir exists; if tmp fails (permissions/space), fall back to user cache.
|
|
517
443
|
if (needsIsolation) {
|
|
518
444
|
try {
|
|
519
|
-
if (!existsSync(safeTempDir))
|
|
520
|
-
mkdirSync(safeTempDir, { recursive: true });
|
|
445
|
+
if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
|
|
521
446
|
} catch {
|
|
522
447
|
try {
|
|
523
448
|
safeTempDir = getCacheDir(version);
|
|
524
|
-
if (!existsSync(safeTempDir))
|
|
525
|
-
mkdirSync(safeTempDir, { recursive: true });
|
|
449
|
+
if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
|
|
526
450
|
} catch {}
|
|
527
451
|
}
|
|
528
452
|
}
|
|
529
|
-
const tmpPath = join(
|
|
530
|
-
needsIsolation ? safeTempDir : binDir,
|
|
531
|
-
`.${archiveName}.part`,
|
|
532
|
-
);
|
|
453
|
+
const tmpPath = join(needsIsolation ? safeTempDir : binDir, `.${archiveName}.part`);
|
|
533
454
|
await downloadBinary(downloadUrl, tmpPath);
|
|
534
455
|
|
|
535
456
|
if (isWin) {
|
|
457
|
+
// Unzip to a temp directory, then move into the per-user cache.
|
|
536
458
|
const unzipDest = safeTempDir;
|
|
537
459
|
try {
|
|
538
|
-
const sysRoot =
|
|
539
|
-
|
|
540
|
-
const psFull = join(
|
|
541
|
-
sysRoot,
|
|
542
|
-
"System32",
|
|
543
|
-
"WindowsPowerShell",
|
|
544
|
-
"v1.0",
|
|
545
|
-
"powershell.exe",
|
|
546
|
-
);
|
|
460
|
+
const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
|
|
461
|
+
const psFull = join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
547
462
|
const psCmd = `Expand-Archive -Path '${tmpPath}' -DestinationPath '${unzipDest}' -Force`;
|
|
548
463
|
let ok = false;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (!ok) {
|
|
557
|
-
try {
|
|
558
|
-
execSync(
|
|
559
|
-
`powershell -NoProfile -NonInteractive -Command "${psCmd}"`,
|
|
560
|
-
{ stdio: "ignore" },
|
|
561
|
-
);
|
|
562
|
-
ok = true;
|
|
563
|
-
} catch {}
|
|
564
|
-
}
|
|
565
|
-
if (!ok) {
|
|
566
|
-
try {
|
|
567
|
-
execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, {
|
|
568
|
-
stdio: "ignore",
|
|
569
|
-
});
|
|
570
|
-
ok = true;
|
|
571
|
-
} catch {}
|
|
572
|
-
}
|
|
573
|
-
if (!ok) {
|
|
574
|
-
execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, {
|
|
575
|
-
stdio: "ignore",
|
|
576
|
-
shell: true,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
464
|
+
// Attempt full-path powershell.exe
|
|
465
|
+
try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
|
|
466
|
+
// Fallback to powershell in PATH
|
|
467
|
+
if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
|
468
|
+
// Fallback to pwsh (PowerShell 7)
|
|
469
|
+
if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
|
|
470
|
+
// Final fallback: bsdtar can extract .zip
|
|
471
|
+
if (!ok) { execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, { stdio: 'ignore', shell: true }); }
|
|
579
472
|
} catch (e) {
|
|
580
473
|
throw new Error(`failed to unzip archive: ${e.message}`);
|
|
581
474
|
} finally {
|
|
582
|
-
try {
|
|
583
|
-
unlinkSync(tmpPath);
|
|
584
|
-
} catch {}
|
|
475
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
585
476
|
}
|
|
477
|
+
// Move the extracted file from temp to cache; do not leave a copy in node_modules
|
|
586
478
|
try {
|
|
587
479
|
const extractedPath = join(unzipDest, binaryName);
|
|
588
480
|
await writeCacheAtomic(extractedPath, cachePath);
|
|
589
|
-
try {
|
|
590
|
-
unlinkSync(extractedPath);
|
|
591
|
-
} catch {}
|
|
481
|
+
try { unlinkSync(extractedPath); } catch {}
|
|
592
482
|
} catch (e) {
|
|
593
483
|
throw new Error(`failed to move binary to cache: ${e.message}`);
|
|
594
484
|
}
|
|
595
485
|
} else {
|
|
596
|
-
const downloadedPath = join(binDir, binaryName);
|
|
597
486
|
if (useZst) {
|
|
487
|
+
// Decompress .zst via system zstd
|
|
598
488
|
try {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
shell: true,
|
|
602
|
-
});
|
|
489
|
+
const outPath = mirrorToLocal ? localPath : join(safeTempDir, binaryName);
|
|
490
|
+
execSync(`zstd -d '${tmpPath}' -o '${outPath}'`, { stdio: 'ignore', shell: true });
|
|
603
491
|
} catch (e) {
|
|
604
|
-
try {
|
|
605
|
-
|
|
606
|
-
} catch {}
|
|
607
|
-
throw new Error(
|
|
608
|
-
`failed to decompress .zst (need zstd CLI): ${e.message}`,
|
|
609
|
-
);
|
|
492
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
493
|
+
throw new Error(`failed to decompress .zst (need zstd CLI): ${e.message}`);
|
|
610
494
|
}
|
|
611
|
-
try {
|
|
612
|
-
unlinkSync(tmpPath);
|
|
613
|
-
} catch {}
|
|
495
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
614
496
|
} else {
|
|
497
|
+
// Extract .tar.gz using system tar
|
|
615
498
|
try {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
shell: true,
|
|
619
|
-
});
|
|
499
|
+
const dest = mirrorToLocal ? binDir : safeTempDir;
|
|
500
|
+
execSync(`tar -xzf '${tmpPath}' -C '${dest}'`, { stdio: 'ignore', shell: true });
|
|
620
501
|
} catch (e) {
|
|
621
|
-
try {
|
|
622
|
-
unlinkSync(tmpPath);
|
|
623
|
-
} catch {}
|
|
502
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
624
503
|
throw new Error(`failed to extract .tar.gz: ${e.message}`);
|
|
625
504
|
}
|
|
626
|
-
try {
|
|
627
|
-
unlinkSync(tmpPath);
|
|
628
|
-
} catch {}
|
|
505
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
629
506
|
}
|
|
630
|
-
if (
|
|
631
|
-
await writeCacheAtomic(downloadedPath, cachePath);
|
|
507
|
+
if (!mirrorToLocal) {
|
|
632
508
|
try {
|
|
633
|
-
|
|
634
|
-
|
|
509
|
+
const extractedPath = join(safeTempDir, binaryName);
|
|
510
|
+
await writeCacheAtomic(extractedPath, cachePath);
|
|
511
|
+
try { unlinkSync(extractedPath); } catch {}
|
|
512
|
+
} catch (e) {
|
|
513
|
+
throw new Error(`failed to move binary to cache: ${e.message}`);
|
|
514
|
+
}
|
|
635
515
|
}
|
|
636
516
|
}
|
|
637
517
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
);
|
|
518
|
+
// Validate header to avoid corrupt binaries causing spawn EFTYPE/ENOEXEC
|
|
519
|
+
|
|
520
|
+
const valid = validateDownloadedBinary(isWin ? cachePath : (mirrorToLocal ? localPath : cachePath));
|
|
641
521
|
if (!valid.ok) {
|
|
642
|
-
try {
|
|
643
|
-
isWin || !mirrorToLocal
|
|
644
|
-
? unlinkSync(cachePath)
|
|
645
|
-
: unlinkSync(localPath);
|
|
646
|
-
} catch {}
|
|
522
|
+
try { (isWin || !mirrorToLocal) ? unlinkSync(cachePath) : unlinkSync(localPath); } catch {}
|
|
647
523
|
throw new Error(`invalid binary (${valid.reason})`);
|
|
648
524
|
}
|
|
649
525
|
|
|
526
|
+
// Make executable on Unix-like systems
|
|
650
527
|
if (!isWin && mirrorToLocal) {
|
|
651
528
|
chmodSync(localPath, 0o755);
|
|
652
529
|
}
|
|
653
|
-
|
|
654
|
-
console.log(
|
|
655
|
-
|
|
656
|
-
);
|
|
530
|
+
|
|
531
|
+
console.log(`✓ Installed ${binaryName}${(isWin || !mirrorToLocal) ? ' (cached)' : ''}`);
|
|
532
|
+
// Ensure persistent cache holds the binary (already true for Windows path)
|
|
657
533
|
if (!isWin && mirrorToLocal) {
|
|
658
|
-
try {
|
|
659
|
-
await writeCacheAtomic(localPath, cachePath);
|
|
660
|
-
} catch {}
|
|
534
|
+
try { await writeCacheAtomic(localPath, cachePath); } catch {}
|
|
661
535
|
}
|
|
662
536
|
} catch (error) {
|
|
663
537
|
console.error(`✗ Failed to install ${binaryName}: ${error.message}`);
|
|
664
538
|
console.error(` Downloaded from: ${downloadUrl}`);
|
|
539
|
+
// Continue with other binaries even if one fails
|
|
665
540
|
}
|
|
666
541
|
}
|
|
667
542
|
|
|
543
|
+
// Create platform-specific symlink/copy for main binary
|
|
668
544
|
const mainBinary = `dev-${targetTriple}${binaryExt}`;
|
|
669
545
|
const mainBinaryPath = join(binDir, mainBinary);
|
|
670
|
-
|
|
671
|
-
if (
|
|
672
|
-
existsSync(mainBinaryPath) ||
|
|
673
|
-
existsSync(
|
|
674
|
-
getCachedBinaryPath(version, targetTriple, platform() === "win32"),
|
|
675
|
-
)
|
|
676
|
-
) {
|
|
546
|
+
|
|
547
|
+
if (existsSync(mainBinaryPath) || existsSync(getCachedBinaryPath(version, targetTriple, platform() === 'win32'))) {
|
|
677
548
|
try {
|
|
678
|
-
const probePath = existsSync(mainBinaryPath)
|
|
679
|
-
? mainBinaryPath
|
|
680
|
-
: getCachedBinaryPath(version, targetTriple, platform() === "win32");
|
|
549
|
+
const probePath = existsSync(mainBinaryPath) ? mainBinaryPath : getCachedBinaryPath(version, targetTriple, platform() === 'win32');
|
|
681
550
|
const stats = statSync(probePath);
|
|
682
|
-
if (!stats.size)
|
|
683
|
-
throw new Error("binary is empty (download likely failed)");
|
|
551
|
+
if (!stats.size) throw new Error('binary is empty (download likely failed)');
|
|
684
552
|
const valid = validateDownloadedBinary(probePath);
|
|
685
553
|
if (!valid.ok) {
|
|
686
554
|
console.warn(`⚠ Main dev binary appears invalid: ${valid.reason}`);
|
|
687
|
-
console.warn(
|
|
688
|
-
" Try reinstalling or check your network/proxy settings.",
|
|
689
|
-
);
|
|
555
|
+
console.warn(' Try reinstalling or check your network/proxy settings.');
|
|
690
556
|
}
|
|
691
557
|
} catch (e) {
|
|
692
558
|
console.warn(`⚠ Main dev binary appears invalid: ${e.message}`);
|
|
693
|
-
console.warn(
|
|
559
|
+
console.warn(' Try reinstalling or check your network/proxy settings.');
|
|
694
560
|
}
|
|
695
|
-
console.log(
|
|
561
|
+
console.log('Setting up main dev binary...');
|
|
562
|
+
|
|
563
|
+
// On Windows, we can't use symlinks easily, so update the JS wrapper
|
|
564
|
+
// On Unix, the JS wrapper will find the correct binary
|
|
565
|
+
console.log('✓ Installation complete!');
|
|
696
566
|
} else {
|
|
697
|
-
console.warn(
|
|
698
|
-
"⚠ Main dev binary not found. You may need to build from source.",
|
|
699
|
-
);
|
|
567
|
+
console.warn('⚠ Main dev binary not found. You may need to build from source.');
|
|
700
568
|
}
|
|
701
|
-
}
|
|
702
569
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
570
|
+
// `dev` is the only bin in package.json, so the package manager always creates
|
|
571
|
+
// it and every wrapper below forwards to it. `code` is an alias we create only
|
|
572
|
+
// when nothing else owns that name on PATH, so we never hijack another CLI.
|
|
573
|
+
// For upgrades from older versions that published a `code` bin, we also remove
|
|
574
|
+
// our old shim if a conflict is detected.
|
|
575
|
+
if (isGlobal && !isNpx) try {
|
|
576
|
+
const isTTY = process.stdout && process.stdout.isTTY;
|
|
577
|
+
const isWindows = platform() === 'win32';
|
|
578
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
579
|
+
const isBun = ua.includes('bun') || !!process.env.BUN_INSTALL;
|
|
580
|
+
|
|
581
|
+
const installedCmds = new Set(['dev']); // global install always exposes dev via package manager
|
|
582
|
+
const skippedCmds = [];
|
|
583
|
+
|
|
584
|
+
// Helper to resolve all 'code' on PATH
|
|
585
|
+
const resolveAllOnPath = () => {
|
|
586
|
+
try {
|
|
587
|
+
if (isWindows) {
|
|
588
|
+
const out = execSync('where code', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
589
|
+
return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
590
|
+
}
|
|
591
|
+
let out = '';
|
|
592
|
+
try {
|
|
593
|
+
out = execSync('bash -lc "which -a code 2>/dev/null"', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
594
|
+
} catch {
|
|
595
|
+
try {
|
|
596
|
+
out = execSync('command -v code || true', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
597
|
+
} catch { out = ''; }
|
|
598
|
+
}
|
|
599
|
+
return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
600
|
+
} catch {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
if (isBun) {
|
|
606
|
+
// Bun creates shims for every bin; if another 'code' exists elsewhere on PATH, remove Bun's shim
|
|
607
|
+
let bunBin = '';
|
|
608
|
+
try {
|
|
609
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
610
|
+
const bunBase = process.env.BUN_INSTALL || join(home, '.bun');
|
|
611
|
+
bunBin = join(bunBase, 'bin');
|
|
612
|
+
} catch {}
|
|
613
|
+
|
|
614
|
+
const bunShim = join(bunBin || '', isWindows ? 'code.cmd' : 'code');
|
|
615
|
+
const candidates = resolveAllOnPath();
|
|
616
|
+
const other = candidates.find(p => p && (!bunBin || !p.startsWith(bunBin)));
|
|
617
|
+
if (other && existsSync(bunShim)) {
|
|
618
|
+
try {
|
|
619
|
+
unlinkSync(bunShim);
|
|
620
|
+
console.log(`✓ Skipped global 'code' shim under Bun (existing: ${other})`);
|
|
621
|
+
skippedCmds.push({ name: 'code', reason: `existing: ${other}` });
|
|
622
|
+
} catch (e) {
|
|
623
|
+
console.log(`⚠ Could not remove Bun shim '${bunShim}': ${e.message}`);
|
|
624
|
+
}
|
|
625
|
+
} else if (!other) {
|
|
626
|
+
// No conflict: create a wrapper that forwards to `dev`
|
|
627
|
+
try {
|
|
628
|
+
const wrapperPath = bunShim;
|
|
629
|
+
if (isWindows) {
|
|
630
|
+
const content = `@echo off\r\n"%~dp0dev" %*\r\n`;
|
|
631
|
+
writeFileSync(wrapperPath, content);
|
|
632
|
+
} else {
|
|
633
|
+
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/dev" "$@"\n`;
|
|
634
|
+
writeFileSync(wrapperPath, content);
|
|
635
|
+
chmodSync(wrapperPath, 0o755);
|
|
636
|
+
}
|
|
637
|
+
console.log("✓ Created 'code' wrapper -> dev (bun)");
|
|
638
|
+
installedCmds.add('code');
|
|
639
|
+
} catch (e) {
|
|
640
|
+
console.log(`⚠ Failed to create 'code' wrapper (bun): ${e.message}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Print summary for Bun
|
|
645
|
+
const list = Array.from(installedCmds).sort().join(', ');
|
|
646
|
+
console.log(`Commands installed (bun): ${list}`);
|
|
647
|
+
if (skippedCmds.length) {
|
|
648
|
+
for (const s of skippedCmds) console.error(`Commands skipped: ${s.name} (${s.reason})`);
|
|
649
|
+
console.error('→ Use `dev` to run this tool.');
|
|
650
|
+
}
|
|
651
|
+
// Final friendly usage hint
|
|
652
|
+
if (installedCmds.has('code')) {
|
|
653
|
+
console.log("Use 'code' to launch Hanzo Dev.");
|
|
654
|
+
} else {
|
|
655
|
+
console.log("Use 'dev' to launch Hanzo Dev.");
|
|
656
|
+
}
|
|
657
|
+
} else {
|
|
658
|
+
// npm/pnpm/yarn path
|
|
659
|
+
const globalBin = resolveGlobalBinDir();
|
|
660
|
+
const ourShim = globalBin ? join(globalBin, isWindows ? 'code.cmd' : 'code') : '';
|
|
661
|
+
const candidates = resolveAllOnPath();
|
|
662
|
+
const others = candidates.filter(p => p && (!ourShim || p !== ourShim));
|
|
663
|
+
const ourShimExists = ourShim && existsSync(ourShim);
|
|
664
|
+
const shimLooksOurs = ourShimExists && looksLikeOurShim(ourShim);
|
|
665
|
+
const conflictPaths = [
|
|
666
|
+
...others,
|
|
667
|
+
...(ourShimExists && !shimLooksOurs ? [ourShim] : []),
|
|
668
|
+
];
|
|
669
|
+
const collision = conflictPaths.length > 0;
|
|
670
|
+
|
|
671
|
+
const ensureWrapper = (name, args) => {
|
|
672
|
+
if (!globalBin) return;
|
|
673
|
+
try {
|
|
674
|
+
const wrapperPath = join(globalBin, isWindows ? `${name}.cmd` : name);
|
|
675
|
+
if (isWindows) {
|
|
676
|
+
const content = `@echo off\r\n"%~dp0dev" ${args} %*\r\n`;
|
|
677
|
+
writeFileSync(wrapperPath, content);
|
|
678
|
+
} else {
|
|
679
|
+
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/dev" ${args} "$@"\n`;
|
|
680
|
+
writeFileSync(wrapperPath, content);
|
|
681
|
+
chmodSync(wrapperPath, 0o755);
|
|
682
|
+
}
|
|
683
|
+
console.log(`✓ Created wrapper '${name}' -> dev ${args}`);
|
|
684
|
+
installedCmds.add(name);
|
|
685
|
+
} catch (e) {
|
|
686
|
+
console.log(`⚠ Failed to create '${name}' wrapper: ${e.message}`);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// Always create legacy wrappers so existing scripts keep working.
|
|
691
|
+
// These are distinct commands and keep their own names; only their target
|
|
692
|
+
// changed from `code` to `dev`.
|
|
693
|
+
ensureWrapper('code-tui', '');
|
|
694
|
+
ensureWrapper('code-exec', 'exec');
|
|
695
|
+
|
|
696
|
+
if (collision) {
|
|
697
|
+
console.error('⚠ Detected existing `code` on PATH:');
|
|
698
|
+
for (const p of conflictPaths) console.error(` - ${p}`);
|
|
699
|
+
if (globalBin) {
|
|
700
|
+
try {
|
|
701
|
+
if (ourShimExists) {
|
|
702
|
+
if (shimLooksOurs && others.length > 0) {
|
|
703
|
+
unlinkSync(ourShim);
|
|
704
|
+
console.error(`✓ Removed global 'code' shim (ours) at ${ourShim}`);
|
|
705
|
+
const reason = others[0] || ourShim;
|
|
706
|
+
skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
|
|
707
|
+
} else if (!shimLooksOurs) {
|
|
708
|
+
console.error(`✓ Skipped global 'code' shim (different CLI at ${ourShim})`);
|
|
709
|
+
const reason = conflictPaths[0] || ourShim;
|
|
710
|
+
skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
const reason = conflictPaths[0] || 'another command on PATH';
|
|
714
|
+
skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
|
|
715
|
+
}
|
|
716
|
+
} catch (e) {
|
|
717
|
+
console.error(`⚠ Could not remove npm shim '${ourShim}': ${e.message}`);
|
|
718
|
+
}
|
|
719
|
+
console.error('→ Use `dev` to run this tool.');
|
|
720
|
+
} else {
|
|
721
|
+
console.log('Note: could not determine npm global bin; skipping alias creation.');
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
// No collision; ensure a 'code' wrapper exists forwarding to 'dev'
|
|
725
|
+
if (globalBin) {
|
|
726
|
+
try {
|
|
727
|
+
const content = isWindows
|
|
728
|
+
? `@echo off\r\n"%~dp0dev" %*\r\n`
|
|
729
|
+
: `#!/bin/sh\nexec "$(dirname \"$0\")/dev" "$@"\n`;
|
|
730
|
+
writeFileSync(ourShim, content);
|
|
731
|
+
if (!isWindows) chmodSync(ourShim, 0o755);
|
|
732
|
+
console.log("✓ Created 'code' wrapper -> dev");
|
|
733
|
+
installedCmds.add('code');
|
|
734
|
+
} catch (e) {
|
|
735
|
+
console.log(`⚠ Failed to create 'code' wrapper: ${e.message}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Print summary for npm/pnpm/yarn
|
|
741
|
+
const list = Array.from(installedCmds).sort().join(', ');
|
|
742
|
+
console.log(`Commands installed: ${list}`);
|
|
743
|
+
if (skippedCmds.length) {
|
|
744
|
+
for (const s of skippedCmds) console.log(`Commands skipped: ${s.name} (${s.reason})`);
|
|
745
|
+
}
|
|
746
|
+
// Final friendly usage hint
|
|
747
|
+
if (installedCmds.has('code')) {
|
|
748
|
+
console.log("Use 'code' to launch Hanzo Dev.");
|
|
749
|
+
} else {
|
|
750
|
+
console.log("Use 'dev' to launch Hanzo Dev.");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
708
753
|
} catch {
|
|
709
|
-
|
|
754
|
+
// non-fatal
|
|
710
755
|
}
|
|
711
756
|
}
|
|
712
757
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
});
|
|
718
|
-
}
|
|
758
|
+
main().catch(error => {
|
|
759
|
+
console.error('Installation failed:', error);
|
|
760
|
+
process.exit(1);
|
|
761
|
+
});
|