@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/postinstall.js CHANGED
@@ -1,100 +1,100 @@
1
1
  #!/usr/bin/env node
2
- // Postinstall script for @hanzo/dev
3
-
4
- import {
5
- existsSync,
6
- mkdirSync,
7
- createWriteStream,
8
- chmodSync,
9
- readFileSync,
10
- readSync,
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: "apple-darwin",
33
- linux: "unknown-linux-musl",
34
- win32: "pc-windows-msvc",
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: "x86_64",
39
- arm64: "aarch64",
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 === "win32") {
53
- base = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
54
- } else if (plt === "darwin") {
55
- base = join(home, "Library", "Caches");
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, ".cache");
44
+ base = process.env.XDG_CACHE_HOME || join(home, '.cache');
58
45
  }
59
- const dir = join(base, "hanzo", "dev", version);
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 ? ".exe" : "";
52
+ const ext = isWindows ? '.exe' : '';
66
53
  const cacheDir = getCacheDir(version);
67
- return join(cacheDir, `dev-${targetTriple}${ext}`);
54
+ return join(cacheDir, `code-${targetTriple}${ext}`);
68
55
  }
69
56
 
70
- function isWSL() {
71
- if (platform() !== "linux") return false;
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 ver = readFileSync("/proc/version", "utf8").toLowerCase();
74
- return ver.includes("microsoft") || !!process.env.WSL_DISTRO_NAME;
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("/proc/mounts", "utf8")
83
- .split(/\n/)
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 === "drvfs" || best.type === "cifs";
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
- try {
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
- const fd = openSync(tmp, "r");
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((r) => setTimeout(r, delays[i]));
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 === "win32" ? prefixPath : join(prefixPath, "bin");
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: ["ignore", "pipe", "ignore"],
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("npm prefix -g"));
151
+ const prefixFromNpm = fromPrefix(tryExec('npm prefix -g'));
172
152
  if (prefixFromNpm) return prefixFromNpm;
173
153
 
174
- const binFromNpm = tryExec("npm bin -g");
154
+ const binFromNpm = tryExec('npm bin -g');
175
155
  if (binFromNpm) return binFromNpm;
176
156
 
177
- if (userAgent.includes("pnpm")) {
178
- const pnpmBin = tryExec("pnpm bin --global");
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("yarn")) {
183
- const yarnBin = tryExec("yarn global bin");
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((r) => setTimeout(r, ms));
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
- if (status === 200) {
219
- const expected =
220
- parseInt(response.headers["content-length"] || "0", 10) || 0;
221
- let bytes = 0;
222
- let timer;
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
- const resetTimer = () => {
226
- if (timer) clearTimeout(timer);
227
- timer = setTimeout(() => {
228
- req.destroy(new Error("download stalled"));
229
- }, timeoutMs);
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
- response.on("data", (chunk) => {
234
- bytes += chunk.length;
235
- resetTimer();
236
- });
237
-
238
- const file = createWriteStream(dest);
239
- response.pipe(file);
240
- file.on("finish", () => {
241
- if (timer) clearTimeout(timer);
242
- file.close();
243
- if (expected && bytes !== expected) {
244
- try {
245
- unlinkSync(dest);
246
- } catch {}
247
- reject(
248
- new Error(
249
- `incomplete download: got ${bytes} of ${expected} bytes`,
250
- ),
251
- );
252
- } else if (bytes === 0) {
253
- try {
254
- unlinkSync(dest);
255
- } catch {}
256
- reject(new Error("empty download"));
257
- } else {
258
- resolve();
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
- req.on("error", (err) => {
274
- try {
275
- unlinkSync(dest);
276
- } catch {}
277
- reject(err);
278
- });
234
+ req.on('error', (err) => {
235
+ try { unlinkSync(dest); } catch {}
236
+ reject(err);
237
+ });
279
238
 
280
- req.setTimeout(120000, () => {
281
- req.destroy(new Error("download timed out"));
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
- attempt(url, maxRedirects);
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: "empty or not a regular file" };
265
+ return { ok: false, reason: 'empty or not a regular file' };
306
266
  }
307
- const fd = openSync(p, "r");
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: "too short" };
271
+ if (n < 2) return { ok: false, reason: 'too short' };
312
272
  const plt = platform();
313
- if (plt === "win32") {
314
- if (!(buf[0] === 0x4d && buf[1] === 0x5a))
315
- return { ok: false, reason: "invalid PE header (missing MZ)" };
316
- } else if (plt === "linux" || plt === "android") {
317
- if (
318
- !(
319
- buf[0] === 0x7f &&
320
- buf[1] === 0x45 &&
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.DEV_POSTINSTALL_DRY_RUN === "1") {
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.DEV_RUNTIME_POSTINSTALL =
355
- process.env.DEV_RUNTIME_POSTINSTALL || "1";
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() === "win32";
364
- const binaryExt = isWindows ? ".exe" : "";
365
-
366
- const binDir = join(__dirname, "bin");
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
- const packageJson = JSON.parse(
372
- readFileSync(join(__dirname, "package.json"), "utf8"),
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
- // The release produces 'code-*' binaries; we'll download and rename to 'dev-*'
377
- const binaries = ["code"];
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 devBinaryName = `dev-${targetTriple}${binaryExt}`;
384
- const localPath = join(binDir, devBinaryName);
350
+ const localPath = join(binDir, binaryName);
385
351
  const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
386
-
387
- // Fast path: if a valid cached binary exists, reuse it
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
- try {
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(`✓ ${devBinaryName} ready from user cache`);
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
- // Try platform package via npm optionalDependencies
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 "@hanzo/dev-win32-x64";
384
+ if (isWindows) return '@hanzo/dev-win32-x64';
423
385
  const plt = platform();
424
386
  const cpu = arch();
425
- if (plt === "darwin" && cpu === "arm64")
426
- return "@hanzo/dev-darwin-arm64";
427
- if (plt === "darwin" && cpu === "x64") return "@hanzo/dev-darwin-x64";
428
- if (plt === "linux" && cpu === "x64")
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
- const src = join(platformPkg.dir, "bin", devBinaryName);
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
- try {
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
- `✓ Installed ${devBinaryName} from ${platformPkg.name} (cached)`,
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
- // Download from GitHub release
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 = isWSL();
485
- const binDirReal = (() => {
431
+ const detectedWSL = (() => {
432
+ if (platform() !== 'linux') return false;
486
433
  try {
487
- return realpathSync(binDir);
488
- } catch {
489
- return binDir;
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 mirrorToLocal = !(
493
- isWin ||
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("zstd --version", { stdio: "ignore", shell: true });
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
- ? join(tmpdir(), "hanzo", "dev", version)
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
- process.env.SystemRoot || process.env.windir || "C:\\Windows";
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
- try {
551
- execSync(
552
- `"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`,
553
- { stdio: "ignore" },
554
- );
555
- ok = true;
556
- } catch {}
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
- // Rename code-* to dev-* in cache
590
- const devCachePath = cachePath;
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
- execSync(`zstd -d '${tmpPath}' -o '${downloadedPath}'`, {
603
- stdio: "ignore",
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
- unlinkSync(tmpPath);
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
- execSync(`tar -xzf '${tmpPath}' -C '${binDir}'`, {
620
- stdio: "ignore",
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
- // Rename code-* to dev-*
634
- if (existsSync(downloadedPath)) {
635
- if (mirrorToLocal) {
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
- unlinkSync(extractedPath);
642
- } catch {}
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
- const valid = validateDownloadedBinary(
648
- isWin ? cachePath : mirrorToLocal ? localPath : cachePath,
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
- `✓ Installed ${devBinaryName}${isWin || !mirrorToLocal ? " (cached)" : ""}`,
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 ${devBinaryName}: ${error.message}`);
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
- const mainBinary = `dev-${targetTriple}${binaryExt}`;
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 dev binary appears invalid: ${valid.reason}`);
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 dev binary appears invalid: ${e.message}`);
702
- console.warn(" Try reinstalling or check your network/proxy settings.");
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("✓ Installation complete!");
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
- "⚠ Main dev binary not found. You may need to build from source.",
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((error) => {
724
- console.error("Installation failed:", error);
782
+ runPostinstall().catch(error => {
783
+ console.error('Installation failed:', error);
725
784
  process.exit(1);
726
785
  });
727
786
  }