@hanzo/dev 0.6.91 → 0.6.93

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,101 @@
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, '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 ? ".exe" : "";
52
+ const ext = isWindows ? '.exe' : '';
66
53
  const cacheDir = getCacheDir(version);
67
54
  return join(cacheDir, `dev-${targetTriple}${ext}`);
68
55
  }
69
56
 
70
- function isWSL() {
71
- if (platform() !== "linux") return false;
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 ver = readFileSync("/proc/version", "utf8").toLowerCase();
74
- return ver.includes("microsoft") || !!process.env.WSL_DISTRO_NAME;
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("/proc/mounts", "utf8")
83
- .split(/\n/)
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 === "drvfs" || best.type === "cifs";
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
- try {
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
- const fd = openSync(tmp, "r");
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((r) => setTimeout(r, delays[i]));
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 === "win32" ? prefixPath : join(prefixPath, "bin");
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: ["ignore", "pipe", "ignore"],
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("npm prefix -g"));
152
+ const prefixFromNpm = fromPrefix(tryExec('npm prefix -g'));
172
153
  if (prefixFromNpm) return prefixFromNpm;
173
154
 
174
- const binFromNpm = tryExec("npm bin -g");
155
+ const binFromNpm = tryExec('npm bin -g');
175
156
  if (binFromNpm) return binFromNpm;
176
157
 
177
- if (userAgent.includes("pnpm")) {
178
- const pnpmBin = tryExec("pnpm bin --global");
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("yarn")) {
183
- const yarnBin = tryExec("yarn global bin");
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((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
- }
174
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
217
175
 
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;
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
- const resetTimer = () => {
226
- if (timer) clearTimeout(timer);
227
- timer = setTimeout(() => {
228
- req.destroy(new Error("download stalled"));
229
- }, timeoutMs);
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
- 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
- });
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
- req.on("error", (err) => {
274
- try {
275
- unlinkSync(dest);
276
- } catch {}
277
- reject(err);
278
- });
235
+ req.on('error', (err) => {
236
+ try { unlinkSync(dest); } catch {}
237
+ reject(err);
238
+ });
279
239
 
280
- req.setTimeout(120000, () => {
281
- req.destroy(new Error("download timed out"));
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
- attempt(url, maxRedirects);
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: "empty or not a regular file" };
266
+ return { ok: false, reason: 'empty or not a regular file' };
306
267
  }
307
- const fd = openSync(p, "r");
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: "too short" };
272
+ if (n < 2) return { ok: false, reason: 'too short' };
312
273
  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" };
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 {
@@ -346,88 +291,104 @@ function validateDownloadedBinary(p) {
346
291
 
347
292
  export async function runPostinstall(options = {}) {
348
293
  const { skipGlobalAlias = false, invokedByRuntime = false } = options;
349
- if (process.env.DEV_POSTINSTALL_DRY_RUN === "1") {
294
+ if (process.env.CODE_POSTINSTALL_DRY_RUN === '1') {
350
295
  return { skipped: true };
351
296
  }
352
297
 
353
298
  if (invokedByRuntime) {
354
- process.env.DEV_RUNTIME_POSTINSTALL =
355
- process.env.DEV_RUNTIME_POSTINSTALL || "1";
299
+ process.env.CODE_RUNTIME_POSTINSTALL = process.env.CODE_RUNTIME_POSTINSTALL || '1';
300
+ }
301
+ // Detect potential PATH conflict with an existing `code` command (e.g., VS Code)
302
+ // Only relevant for global installs; skip for npx/local installs to keep postinstall fast.
303
+ const ua = process.env.npm_config_user_agent || '';
304
+ const isNpx = ua.includes('npx');
305
+ const isGlobal = process.env.npm_config_global === 'true';
306
+ if (!skipGlobalAlias && isGlobal && !isNpx) {
307
+ try {
308
+ const whichCmd = process.platform === 'win32' ? 'where code' : 'command -v code || which code || true';
309
+ const resolved = execSync(whichCmd, { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).toString().split(/\r?\n/).filter(Boolean)[0];
310
+ if (resolved) {
311
+ let contents = '';
312
+ try {
313
+ contents = readFileSync(resolved, 'utf8');
314
+ } catch {
315
+ contents = '';
316
+ }
317
+ const looksLikeOurs = shimContentsLookOurs(contents);
318
+ if (!looksLikeOurs) {
319
+ console.warn('[notice] Found an existing `code` on PATH at:');
320
+ console.warn(` ${resolved}`);
321
+ console.warn('[notice] We will still install our CLI, also available as `dev`.');
322
+ console.warn(' If `code` runs another tool, prefer using: dev');
323
+ console.warn(' Or run our CLI explicitly via: npx -y @hanzo/dev');
324
+ }
325
+ }
326
+ } catch {
327
+ // Ignore detection failures; proceed with install.
328
+ }
356
329
  }
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
330
 
362
331
  const targetTriple = getTargetTriple();
363
- const isWindows = platform() === "win32";
364
- const binaryExt = isWindows ? ".exe" : "";
365
-
366
- const binDir = join(__dirname, "bin");
332
+ const isWindows = platform() === 'win32';
333
+ const binaryExt = isWindows ? '.exe' : '';
334
+
335
+ const binDir = join(__dirname, 'bin');
367
336
  if (!existsSync(binDir)) {
368
337
  mkdirSync(binDir, { recursive: true });
369
338
  }
370
-
371
- const packageJson = JSON.parse(
372
- readFileSync(join(__dirname, "package.json"), "utf8"),
373
- );
339
+
340
+ // Get package version - use readFileSync for compatibility
341
+ const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
374
342
  const version = packageJson.version;
375
-
376
- // The release produces dev-* binaries directly
377
- const binaries = ["dev"];
343
+
344
+ // Download only the primary binary; we'll create wrappers for legacy names.
345
+ const binaries = ['dev'];
378
346
 
379
347
  console.log(`Installing @hanzo/dev v${version} for ${targetTriple}...`);
380
-
348
+
381
349
  for (const binary of binaries) {
382
350
  const binaryName = `${binary}-${targetTriple}${binaryExt}`;
383
351
  const localPath = join(binDir, binaryName);
384
352
  const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
385
-
386
- // Fast path: if a valid cached binary exists, reuse it
353
+
354
+ // On Windows we avoid placing the executable inside node_modules to prevent
355
+ // EBUSY/EPERM during global upgrades when the binary is in use.
356
+ // We treat the user cache path as the canonical home of the native binary.
357
+ // For macOS/Linux we keep previous behavior and also place a copy in binDir
358
+ // for convenience.
359
+
360
+ // Fast path: if a valid cached binary exists for this version+triple, reuse it.
387
361
  try {
388
362
  if (existsSync(cachePath)) {
389
363
  const valid = validateDownloadedBinary(cachePath);
390
364
  if (valid.ok) {
365
+ // Avoid mirroring into node_modules on Windows or WSL-on-NTFS.
391
366
  const wsl = isWSL();
392
- const binDirReal = (() => {
393
- try {
394
- return realpathSync(binDir);
395
- } catch {
396
- return binDir;
397
- }
398
- })();
399
- const mirrorToLocal = !(
400
- isWindows ||
401
- (wsl && isPathOnWindowsFs(binDirReal))
402
- );
367
+ const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
368
+ const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
403
369
  if (mirrorToLocal) {
404
370
  copyFileSync(cachePath, localPath);
405
- try {
406
- chmodSync(localPath, 0o755);
407
- } catch {}
371
+ try { chmodSync(localPath, 0o755); } catch {}
408
372
  }
409
373
  console.log(`✓ ${binaryName} ready from user cache`);
410
- continue;
374
+ continue; // next binary
411
375
  }
412
376
  }
413
377
  } catch {
414
- // Ignore cache errors
378
+ // Ignore cache errors and fall through to normal paths
415
379
  }
416
-
417
- // Try platform package via npm optionalDependencies
380
+
381
+ // First try platform package via npm optionalDependencies (fast path on npm CDN).
418
382
  const require = createRequire(import.meta.url);
419
383
  const platformPkg = (() => {
420
384
  const name = (() => {
421
- if (isWindows) return "@hanzo/dev-win32-x64";
385
+ if (isWindows) return '@hanzo/dev-win32-x64';
422
386
  const plt = platform();
423
387
  const cpu = arch();
424
- if (plt === "darwin" && cpu === "arm64")
425
- return "@hanzo/dev-darwin-arm64";
426
- if (plt === "darwin" && cpu === "x64") return "@hanzo/dev-darwin-x64";
427
- if (plt === "linux" && cpu === "x64")
428
- return "@hanzo/dev-linux-x64-musl";
429
- if (plt === "linux" && cpu === "arm64")
430
- return "@hanzo/dev-linux-arm64-musl";
388
+ if (plt === 'darwin' && cpu === 'arm64') return '@hanzo/dev-darwin-arm64';
389
+ if (plt === 'darwin' && cpu === 'x64') return '@hanzo/dev-darwin-x64';
390
+ if (plt === 'linux' && cpu === 'x64') return '@hanzo/dev-linux-x64-musl';
391
+ if (plt === 'linux' && cpu === 'arm64') return '@hanzo/dev-linux-arm64-musl';
431
392
  return null;
432
393
  })();
433
394
  if (!name) return null;
@@ -442,261 +403,370 @@ export async function runPostinstall(options = {}) {
442
403
 
443
404
  if (platformPkg) {
444
405
  try {
445
- const src = join(platformPkg.dir, "bin", binaryName);
406
+ // Expect binary inside platform package bin directory
407
+ const src = join(platformPkg.dir, 'bin', binaryName);
446
408
  if (!existsSync(src)) {
447
- throw new Error(
448
- `platform package missing binary: ${platformPkg.name}`,
449
- );
409
+ throw new Error(`platform package missing binary: ${platformPkg.name}`);
450
410
  }
411
+ // Populate cache first (canonical location) atomically
451
412
  await writeCacheAtomic(src, cachePath);
413
+ // Mirror into local bin only on Unix-like filesystems (not Windows/WSL-on-NTFS)
452
414
  const wsl = isWSL();
453
- const binDirReal = (() => {
454
- try {
455
- return realpathSync(binDir);
456
- } catch {
457
- return binDir;
458
- }
459
- })();
460
- const mirrorToLocal = !(
461
- isWindows ||
462
- (wsl && isPathOnWindowsFs(binDirReal))
463
- );
415
+ const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
416
+ const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
464
417
  if (mirrorToLocal) {
465
418
  copyFileSync(cachePath, localPath);
466
- try {
467
- chmodSync(localPath, 0o755);
468
- } catch {}
419
+ try { chmodSync(localPath, 0o755); } catch {}
469
420
  }
470
- console.log(
471
- `✓ Installed ${binaryName} from ${platformPkg.name} (cached)`,
472
- );
473
- continue;
421
+ console.log(`✓ Installed ${binaryName} from ${platformPkg.name} (cached)`);
422
+ continue; // next binary
474
423
  } catch (e) {
475
- console.warn(
476
- `⚠ Failed platform package install (${e.message}), falling back to GitHub download`,
477
- );
424
+ console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
478
425
  }
479
426
  }
480
427
 
481
- // Download from GitHub release
428
+ // Decide archive format per OS with fallback on macOS/Linux:
429
+ // - Windows: .zip
430
+ // - macOS/Linux: prefer .zst if `zstd` CLI is available; otherwise use .tar.gz
482
431
  const isWin = isWindows;
483
- const detectedWSL = isWSL();
484
- const binDirReal = (() => {
432
+ const detectedWSL = (() => {
433
+ if (platform() !== 'linux') return false;
485
434
  try {
486
- return realpathSync(binDir);
487
- } catch {
488
- return binDir;
489
- }
435
+ const ver = readFileSync('/proc/version', 'utf8').toLowerCase();
436
+ return ver.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
437
+ } catch { return false; }
490
438
  })();
491
- const mirrorToLocal = !(
492
- isWin ||
493
- (detectedWSL && isPathOnWindowsFs(binDirReal))
494
- );
439
+ const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
440
+ const mirrorToLocal = !(isWin || (detectedWSL && isPathOnWindowsFs(binDirReal)));
495
441
  let useZst = false;
496
442
  if (!isWin) {
497
443
  try {
498
- execSync("zstd --version", { stdio: "ignore", shell: true });
444
+ execSync('zstd --version', { stdio: 'ignore', shell: true });
499
445
  useZst = true;
500
446
  } catch {
501
447
  useZst = false;
502
448
  }
503
449
  }
504
- const archiveName = isWin
505
- ? `${binaryName}.zip`
506
- : useZst
507
- ? `${binaryName}.zst`
508
- : `${binaryName}.tar.gz`;
450
+ const archiveName = isWin ? `${binaryName}.zip` : (useZst ? `${binaryName}.zst` : `${binaryName}.tar.gz`);
509
451
  const downloadUrl = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
510
452
 
511
453
  console.log(`Downloading ${archiveName}...`);
512
454
  try {
513
- const needsIsolation = isWin || (!isWin && !mirrorToLocal);
514
- let safeTempDir = needsIsolation
515
- ? join(tmpdir(), "hanzo", "dev", version)
516
- : binDir;
455
+ const needsIsolation = isWin || (!isWin && !mirrorToLocal); // Windows or WSL-on-NTFS
456
+ let safeTempDir = needsIsolation ? join(tmpdir(), 'hanzoai', 'dev', version) : binDir;
457
+ // Ensure staging dir exists; if tmp fails (permissions/space), fall back to user cache.
517
458
  if (needsIsolation) {
518
459
  try {
519
- if (!existsSync(safeTempDir))
520
- mkdirSync(safeTempDir, { recursive: true });
460
+ if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
521
461
  } catch {
522
462
  try {
523
463
  safeTempDir = getCacheDir(version);
524
- if (!existsSync(safeTempDir))
525
- mkdirSync(safeTempDir, { recursive: true });
464
+ if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
526
465
  } catch {}
527
466
  }
528
467
  }
529
- const tmpPath = join(
530
- needsIsolation ? safeTempDir : binDir,
531
- `.${archiveName}.part`,
532
- );
468
+ const tmpPath = join(needsIsolation ? safeTempDir : binDir, `.${archiveName}.part`);
533
469
  await downloadBinary(downloadUrl, tmpPath);
534
470
 
535
471
  if (isWin) {
472
+ // Unzip to a temp directory, then move into the per-user cache.
536
473
  const unzipDest = safeTempDir;
537
474
  try {
538
- const sysRoot =
539
- process.env.SystemRoot || process.env.windir || "C:\\Windows";
540
- const psFull = join(
541
- sysRoot,
542
- "System32",
543
- "WindowsPowerShell",
544
- "v1.0",
545
- "powershell.exe",
546
- );
475
+ const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
476
+ const psFull = join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
547
477
  const psCmd = `Expand-Archive -Path '${tmpPath}' -DestinationPath '${unzipDest}' -Force`;
548
478
  let ok = false;
549
- try {
550
- execSync(
551
- `"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`,
552
- { stdio: "ignore" },
553
- );
554
- ok = true;
555
- } catch {}
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
- }
479
+ // Attempt full-path powershell.exe
480
+ try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
481
+ // Fallback to powershell in PATH
482
+ if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
483
+ // Fallback to pwsh (PowerShell 7)
484
+ if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
485
+ // Final fallback: bsdtar can extract .zip
486
+ if (!ok) { execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, { stdio: 'ignore', shell: true }); }
579
487
  } catch (e) {
580
488
  throw new Error(`failed to unzip archive: ${e.message}`);
581
489
  } finally {
582
- try {
583
- unlinkSync(tmpPath);
584
- } catch {}
490
+ try { unlinkSync(tmpPath); } catch {}
585
491
  }
492
+ // Move the extracted file from temp to cache; do not leave a copy in node_modules
586
493
  try {
587
494
  const extractedPath = join(unzipDest, binaryName);
588
495
  await writeCacheAtomic(extractedPath, cachePath);
589
- try {
590
- unlinkSync(extractedPath);
591
- } catch {}
496
+ try { unlinkSync(extractedPath); } catch {}
592
497
  } catch (e) {
593
498
  throw new Error(`failed to move binary to cache: ${e.message}`);
594
499
  }
595
500
  } else {
596
- const downloadedPath = join(binDir, binaryName);
597
501
  if (useZst) {
502
+ // Decompress .zst via system zstd
598
503
  try {
599
- execSync(`zstd -d '${tmpPath}' -o '${downloadedPath}'`, {
600
- stdio: "ignore",
601
- shell: true,
602
- });
504
+ const outPath = mirrorToLocal ? localPath : join(safeTempDir, binaryName);
505
+ execSync(`zstd -d '${tmpPath}' -o '${outPath}'`, { stdio: 'ignore', shell: true });
603
506
  } catch (e) {
604
- try {
605
- unlinkSync(tmpPath);
606
- } catch {}
607
- throw new Error(
608
- `failed to decompress .zst (need zstd CLI): ${e.message}`,
609
- );
507
+ try { unlinkSync(tmpPath); } catch {}
508
+ throw new Error(`failed to decompress .zst (need zstd CLI): ${e.message}`);
610
509
  }
611
- try {
612
- unlinkSync(tmpPath);
613
- } catch {}
510
+ try { unlinkSync(tmpPath); } catch {}
614
511
  } else {
512
+ // Extract .tar.gz using system tar
615
513
  try {
616
- execSync(`tar -xzf '${tmpPath}' -C '${binDir}'`, {
617
- stdio: "ignore",
618
- shell: true,
619
- });
514
+ const dest = mirrorToLocal ? binDir : safeTempDir;
515
+ execSync(`tar -xzf '${tmpPath}' -C '${dest}'`, { stdio: 'ignore', shell: true });
620
516
  } catch (e) {
621
- try {
622
- unlinkSync(tmpPath);
623
- } catch {}
517
+ try { unlinkSync(tmpPath); } catch {}
624
518
  throw new Error(`failed to extract .tar.gz: ${e.message}`);
625
519
  }
626
- try {
627
- unlinkSync(tmpPath);
628
- } catch {}
520
+ try { unlinkSync(tmpPath); } catch {}
629
521
  }
630
- if (existsSync(downloadedPath) && !mirrorToLocal) {
631
- await writeCacheAtomic(downloadedPath, cachePath);
522
+ if (!mirrorToLocal) {
632
523
  try {
633
- unlinkSync(downloadedPath);
634
- } catch {}
524
+ const extractedPath = join(safeTempDir, binaryName);
525
+ await writeCacheAtomic(extractedPath, cachePath);
526
+ try { unlinkSync(extractedPath); } catch {}
527
+ } catch (e) {
528
+ throw new Error(`failed to move binary to cache: ${e.message}`);
529
+ }
635
530
  }
636
531
  }
637
532
 
638
- const valid = validateDownloadedBinary(
639
- isWin ? cachePath : mirrorToLocal ? localPath : cachePath,
640
- );
533
+ // Validate header to avoid corrupt binaries causing spawn EFTYPE/ENOEXEC
534
+
535
+ const valid = validateDownloadedBinary(isWin ? cachePath : (mirrorToLocal ? localPath : cachePath));
641
536
  if (!valid.ok) {
642
- try {
643
- isWin || !mirrorToLocal
644
- ? unlinkSync(cachePath)
645
- : unlinkSync(localPath);
646
- } catch {}
537
+ try { (isWin || !mirrorToLocal) ? unlinkSync(cachePath) : unlinkSync(localPath); } catch {}
647
538
  throw new Error(`invalid binary (${valid.reason})`);
648
539
  }
649
540
 
541
+ // Make executable on Unix-like systems
650
542
  if (!isWin && mirrorToLocal) {
651
543
  chmodSync(localPath, 0o755);
652
544
  }
653
-
654
- console.log(
655
- `✓ Installed ${binaryName}${isWin || !mirrorToLocal ? " (cached)" : ""}`,
656
- );
545
+
546
+ console.log(`✓ Installed ${binaryName}${(isWin || !mirrorToLocal) ? ' (cached)' : ''}`);
547
+ // Ensure persistent cache holds the binary (already true for Windows path)
657
548
  if (!isWin && mirrorToLocal) {
658
- try {
659
- await writeCacheAtomic(localPath, cachePath);
660
- } catch {}
549
+ try { await writeCacheAtomic(localPath, cachePath); } catch {}
661
550
  }
662
551
  } catch (error) {
663
552
  console.error(`✗ Failed to install ${binaryName}: ${error.message}`);
664
553
  console.error(` Downloaded from: ${downloadUrl}`);
554
+ // Continue with other binaries even if one fails
665
555
  }
666
556
  }
667
557
 
558
+ // Create platform-specific symlink/copy for main binary
668
559
  const mainBinary = `dev-${targetTriple}${binaryExt}`;
669
560
  const mainBinaryPath = join(binDir, mainBinary);
670
-
671
- if (
672
- existsSync(mainBinaryPath) ||
673
- existsSync(
674
- getCachedBinaryPath(version, targetTriple, platform() === "win32"),
675
- )
676
- ) {
561
+
562
+ if (existsSync(mainBinaryPath) || existsSync(getCachedBinaryPath(version, targetTriple, platform() === 'win32'))) {
677
563
  try {
678
- const probePath = existsSync(mainBinaryPath)
679
- ? mainBinaryPath
680
- : getCachedBinaryPath(version, targetTriple, platform() === "win32");
564
+ const probePath = existsSync(mainBinaryPath) ? mainBinaryPath : getCachedBinaryPath(version, targetTriple, platform() === 'win32');
681
565
  const stats = statSync(probePath);
682
- if (!stats.size)
683
- throw new Error("binary is empty (download likely failed)");
566
+ if (!stats.size) throw new Error('binary is empty (download likely failed)');
684
567
  const valid = validateDownloadedBinary(probePath);
685
568
  if (!valid.ok) {
686
569
  console.warn(`⚠ Main dev binary appears invalid: ${valid.reason}`);
687
- console.warn(
688
- " Try reinstalling or check your network/proxy settings.",
689
- );
570
+ console.warn(' Try reinstalling or check your network/proxy settings.');
690
571
  }
691
572
  } catch (e) {
692
573
  console.warn(`⚠ Main dev binary appears invalid: ${e.message}`);
693
- console.warn(" Try reinstalling or check your network/proxy settings.");
574
+ console.warn(' Try reinstalling or check your network/proxy settings.');
694
575
  }
695
- console.log("✓ Installation complete!");
576
+ console.log('Setting up main dev binary...');
577
+
578
+ // On Windows, we can't use symlinks easily, so update the JS wrapper
579
+ // On Unix, the JS wrapper will find the correct binary
580
+ console.log('✓ Installation complete!');
696
581
  } else {
697
- console.warn(
698
- "⚠ Main dev binary not found. You may need to build from source.",
699
- );
582
+ console.warn('⚠ Main dev binary not found. You may need to build from source.');
583
+ }
584
+
585
+ // `dev` is the only bin in package.json, so the package manager always creates
586
+ // it and every wrapper below forwards to it. `code` is an alias we create only
587
+ // when nothing else owns that name on PATH, so we never hijack another CLI.
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(['dev']); // global install always exposes dev 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 `dev`
642
+ try {
643
+ const wrapperPath = bunShim;
644
+ if (isWindows) {
645
+ const content = `@echo off\r\n"%~dp0dev" %*\r\n`;
646
+ writeFileSync(wrapperPath, content);
647
+ } else {
648
+ const content = `#!/bin/sh\nexec "$(dirname \"$0\")/dev" "$@"\n`;
649
+ writeFileSync(wrapperPath, content);
650
+ chmodSync(wrapperPath, 0o755);
651
+ }
652
+ console.log("✓ Created 'code' wrapper -> dev (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 `dev` to run this tool.');
665
+ }
666
+ // Final friendly usage hint
667
+ if (installedCmds.has('code')) {
668
+ console.log("Use 'code' to launch Hanzo Dev.");
669
+ } else {
670
+ console.log("Use 'dev' to launch Hanzo Dev.");
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 && looksLikeOurShim(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"%~dp0dev" ${args} %*\r\n`;
692
+ writeFileSync(wrapperPath, content);
693
+ } else {
694
+ const content = `#!/bin/sh\nexec "$(dirname \"$0\")/dev" ${args} "$@"\n`;
695
+ writeFileSync(wrapperPath, content);
696
+ chmodSync(wrapperPath, 0o755);
697
+ }
698
+ console.log(`✓ Created wrapper '${name}' -> dev ${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
+ // These are distinct commands and keep their own names; only their target
707
+ // changed from `code` to `dev`.
708
+ ensureWrapper('code-tui', '');
709
+ ensureWrapper('code-exec', 'exec');
710
+
711
+ if (collision) {
712
+ console.error('⚠ Detected existing `code` on PATH:');
713
+ for (const p of conflictPaths) console.error(` - ${p}`);
714
+ if (globalBin) {
715
+ try {
716
+ if (ourShimExists) {
717
+ if (shimLooksOurs && others.length > 0) {
718
+ unlinkSync(ourShim);
719
+ console.error(`✓ Removed global 'code' shim (ours) at ${ourShim}`);
720
+ const reason = others[0] || ourShim;
721
+ skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
722
+ } else if (!shimLooksOurs) {
723
+ console.error(`✓ Skipped global 'code' shim (different CLI at ${ourShim})`);
724
+ const reason = conflictPaths[0] || ourShim;
725
+ skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
726
+ }
727
+ } else {
728
+ const reason = conflictPaths[0] || 'another command on PATH';
729
+ skippedCmds.push({ name: 'code', reason: `existing: ${reason}` });
730
+ }
731
+ } catch (e) {
732
+ console.error(`⚠ Could not remove npm shim '${ourShim}': ${e.message}`);
733
+ }
734
+ console.error('→ Use `dev` to run this tool.');
735
+ } else {
736
+ console.log('Note: could not determine npm global bin; skipping alias creation.');
737
+ }
738
+ } else {
739
+ // No collision; ensure a 'code' wrapper exists forwarding to 'dev'
740
+ if (globalBin) {
741
+ try {
742
+ const content = isWindows
743
+ ? `@echo off\r\n"%~dp0dev" %*\r\n`
744
+ : `#!/bin/sh\nexec "$(dirname \"$0\")/dev" "$@"\n`;
745
+ writeFileSync(ourShim, content);
746
+ if (!isWindows) chmodSync(ourShim, 0o755);
747
+ console.log("✓ Created 'code' wrapper -> dev");
748
+ installedCmds.add('code');
749
+ } catch (e) {
750
+ console.log(`⚠ Failed to create 'code' wrapper: ${e.message}`);
751
+ }
752
+ }
753
+ }
754
+
755
+ // Print summary for npm/pnpm/yarn
756
+ const list = Array.from(installedCmds).sort().join(', ');
757
+ console.log(`Commands installed: ${list}`);
758
+ if (skippedCmds.length) {
759
+ for (const s of skippedCmds) console.log(`Commands skipped: ${s.name} (${s.reason})`);
760
+ }
761
+ // Final friendly usage hint
762
+ if (installedCmds.has('code')) {
763
+ console.log("Use 'code' to launch Hanzo Dev.");
764
+ } else {
765
+ console.log("Use 'dev' to launch Hanzo Dev.");
766
+ }
767
+ }
768
+ } catch {
769
+ // non-fatal
700
770
  }
701
771
  }
702
772
 
@@ -711,8 +781,8 @@ function isExecutedDirectly() {
711
781
  }
712
782
 
713
783
  if (isExecutedDirectly()) {
714
- runPostinstall().catch((error) => {
715
- console.error("Installation failed:", error);
784
+ runPostinstall().catch(error => {
785
+ console.error('Installation failed:', error);
716
786
  process.exit(1);
717
787
  });
718
788
  }