@hanzo/dev 0.6.90 → 0.6.92

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