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