@hanzo/dev 3.0.1 → 3.0.2

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,37 +1,34 @@
1
1
  #!/usr/bin/env node
2
- // Non-functional change to trigger release workflow
2
+ // Postinstall script for @hanzo/dev
3
3
 
4
- import { existsSync, mkdirSync, createWriteStream, chmodSync, readFileSync, readSync, writeFileSync, unlinkSync, statSync, openSync, closeSync, copyFileSync } from 'fs';
5
- import { join, dirname } from 'path';
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
6
  import { fileURLToPath } from 'url';
7
7
  import { get } from 'https';
8
- import { platform, arch } from 'os';
8
+ import { platform, arch, tmpdir } from 'os';
9
9
  import { execSync } from 'child_process';
10
10
  import { createRequire } from 'module';
11
11
 
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
 
14
- // Map Node.js platform/arch to Rust target triples
15
14
  function getTargetTriple() {
16
15
  const platformMap = {
17
16
  'darwin': 'apple-darwin',
18
- 'linux': 'unknown-linux-musl', // Default to musl for better compatibility
17
+ 'linux': 'unknown-linux-musl',
19
18
  'win32': 'pc-windows-msvc'
20
19
  };
21
-
20
+
22
21
  const archMap = {
23
22
  'x64': 'x86_64',
24
23
  'arm64': 'aarch64'
25
24
  };
26
-
25
+
27
26
  const rustArch = archMap[arch()] || arch();
28
27
  const rustPlatform = platformMap[platform()] || platform();
29
-
28
+
30
29
  return `${rustArch}-${rustPlatform}`;
31
30
  }
32
31
 
33
- // Resolve a persistent user cache directory for binaries so that repeated
34
- // npx installs can reuse a previously downloaded artifact and skip work.
35
32
  function getCacheDir(version) {
36
33
  const plt = platform();
37
34
  const home = process.env.HOME || process.env.USERPROFILE || '';
@@ -43,7 +40,7 @@ function getCacheDir(version) {
43
40
  } else {
44
41
  base = process.env.XDG_CACHE_HOME || join(home, '.cache');
45
42
  }
46
- const dir = join(base, 'just-every', 'code', version);
43
+ const dir = join(base, 'hanzo', 'dev', version);
47
44
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
48
45
  return dir;
49
46
  }
@@ -51,7 +48,99 @@ function getCacheDir(version) {
51
48
  function getCachedBinaryPath(version, targetTriple, isWindows) {
52
49
  const ext = isWindows ? '.exe' : '';
53
50
  const cacheDir = getCacheDir(version);
54
- return join(cacheDir, `code-${targetTriple}${ext}`);
51
+ return join(cacheDir, `dev-${targetTriple}${ext}`);
52
+ }
53
+
54
+ function isWSL() {
55
+ if (platform() !== 'linux') return false;
56
+ try {
57
+ const ver = readFileSync('/proc/version', 'utf8').toLowerCase();
58
+ return ver.includes('microsoft') || !!process.env.WSL_DISTRO_NAME;
59
+ } catch { return false; }
60
+ }
61
+
62
+ function isPathOnWindowsFs(p) {
63
+ try {
64
+ const mounts = readFileSync('/proc/mounts', 'utf8').split(/\n/).filter(Boolean);
65
+ let best = { mount: '/', type: 'unknown', len: 1 };
66
+ for (const line of mounts) {
67
+ const parts = line.split(' ');
68
+ if (parts.length < 3) continue;
69
+ const mnt = parts[1];
70
+ const typ = parts[2];
71
+ if (p.startsWith(mnt) && mnt.length > best.len) best = { mount: mnt, type: typ, len: mnt.length };
72
+ }
73
+ return best.type === 'drvfs' || best.type === 'cifs';
74
+ } catch { return false; }
75
+ }
76
+
77
+ async function writeCacheAtomic(srcPath, cachePath) {
78
+ try {
79
+ if (existsSync(cachePath)) {
80
+ const ok = validateDownloadedBinary(cachePath).ok;
81
+ if (ok) return;
82
+ }
83
+ } catch {}
84
+ const dir = dirname(cachePath);
85
+ if (!existsSync(dir)) { try { mkdirSync(dir, { recursive: true }); } catch {} }
86
+ const tmp = cachePath + '.tmp-' + Math.random().toString(36).slice(2, 8);
87
+ copyFileSync(srcPath, tmp);
88
+ try { const fd = openSync(tmp, 'r'); try { fsyncSync(fd); } finally { closeSync(fd); } } catch {}
89
+ const delays = [100, 200, 400, 800, 1200, 1600];
90
+ for (let i = 0; i < delays.length; i++) {
91
+ try {
92
+ if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
93
+ renameSync(tmp, cachePath);
94
+ return;
95
+ } catch {
96
+ await new Promise(r => setTimeout(r, delays[i]));
97
+ }
98
+ }
99
+ if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
100
+ renameSync(tmp, cachePath);
101
+ }
102
+
103
+ function resolveGlobalBinDir() {
104
+ const plt = platform();
105
+ const userAgent = process.env.npm_config_user_agent || '';
106
+
107
+ const fromPrefix = (prefixPath) => {
108
+ if (!prefixPath) return '';
109
+ return plt === 'win32' ? prefixPath : join(prefixPath, 'bin');
110
+ };
111
+
112
+ const prefixEnv = process.env.npm_config_prefix || process.env.PREFIX || '';
113
+ const direct = fromPrefix(prefixEnv);
114
+ if (direct) return direct;
115
+
116
+ const tryExec = (command) => {
117
+ try {
118
+ return execSync(command, {
119
+ stdio: ['ignore', 'pipe', 'ignore'],
120
+ shell: true,
121
+ }).toString().trim();
122
+ } catch {
123
+ return '';
124
+ }
125
+ };
126
+
127
+ const prefixFromNpm = fromPrefix(tryExec('npm prefix -g'));
128
+ if (prefixFromNpm) return prefixFromNpm;
129
+
130
+ const binFromNpm = tryExec('npm bin -g');
131
+ if (binFromNpm) return binFromNpm;
132
+
133
+ if (userAgent.includes('pnpm')) {
134
+ const pnpmBin = tryExec('pnpm bin --global');
135
+ if (pnpmBin) return pnpmBin;
136
+ }
137
+
138
+ if (userAgent.includes('yarn')) {
139
+ const yarnBin = tryExec('yarn global bin');
140
+ if (yarnBin) return yarnBin;
141
+ }
142
+
143
+ return '';
55
144
  }
56
145
 
57
146
  async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
@@ -76,7 +165,7 @@ async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
76
165
  const expected = parseInt(response.headers['content-length'] || '0', 10) || 0;
77
166
  let bytes = 0;
78
167
  let timer;
79
- const timeoutMs = 30000; // 30s inactivity timeout
168
+ const timeoutMs = 30000;
80
169
 
81
170
  const resetTimer = () => {
82
171
  if (timer) clearTimeout(timer);
@@ -121,7 +210,6 @@ async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
121
210
  reject(err);
122
211
  });
123
212
 
124
- // Absolute request timeout to avoid hanging forever
125
213
  req.setTimeout(120000, () => {
126
214
  req.destroy(new Error('download timed out'));
127
215
  });
@@ -173,102 +261,74 @@ function validateDownloadedBinary(p) {
173
261
  }
174
262
  }
175
263
 
176
- async function main() {
177
- // Detect potential PATH conflict with an existing `code` command (e.g., VS Code)
178
- // Only relevant for global installs; skip for npx/local installs to keep postinstall fast.
264
+ export async function runPostinstall(options = {}) {
265
+ const { skipGlobalAlias = false, invokedByRuntime = false } = options;
266
+ if (process.env.DEV_POSTINSTALL_DRY_RUN === '1') {
267
+ return { skipped: true };
268
+ }
269
+
270
+ if (invokedByRuntime) {
271
+ process.env.DEV_RUNTIME_POSTINSTALL = process.env.DEV_RUNTIME_POSTINSTALL || '1';
272
+ }
273
+
179
274
  const ua = process.env.npm_config_user_agent || '';
180
275
  const isNpx = ua.includes('npx');
181
276
  const isGlobal = process.env.npm_config_global === 'true';
182
- if (isGlobal && !isNpx) {
183
- try {
184
- const whichCmd = process.platform === 'win32' ? 'where code' : 'command -v code || which code || true';
185
- const resolved = execSync(whichCmd, { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform !== 'win32' }).toString().split(/\r?\n/).filter(Boolean)[0];
186
- if (resolved) {
187
- let contents = '';
188
- try {
189
- contents = readFileSync(resolved, 'utf8');
190
- } catch {
191
- contents = '';
192
- }
193
- const looksLikeOurs = contents.includes('@just-every/code') || contents.includes('bin/coder.js');
194
- if (!looksLikeOurs) {
195
- console.warn('[notice] Found an existing `code` on PATH at:');
196
- console.warn(` ${resolved}`);
197
- console.warn('[notice] We will still install our CLI, also available as `coder`.');
198
- console.warn(' If `code` runs another tool, prefer using: coder');
199
- console.warn(' Or run our CLI explicitly via: npx -y @just-every/code');
200
- }
201
- }
202
- } catch {
203
- // Ignore detection failures; proceed with install.
204
- }
205
- }
206
277
 
207
278
  const targetTriple = getTargetTriple();
208
279
  const isWindows = platform() === 'win32';
209
280
  const binaryExt = isWindows ? '.exe' : '';
210
-
281
+
211
282
  const binDir = join(__dirname, 'bin');
212
283
  if (!existsSync(binDir)) {
213
284
  mkdirSync(binDir, { recursive: true });
214
285
  }
215
-
216
- // Get package version - use readFileSync for compatibility
286
+
217
287
  const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
218
288
  const version = packageJson.version;
219
-
220
- // Download only the primary binary; we'll create wrappers for legacy names.
289
+
290
+ // The release produces 'code-*' binaries; we'll download and rename to 'dev-*'
221
291
  const binaries = ['code'];
222
-
223
- console.log(`Installing @just-every/code v${version} for ${targetTriple}...`);
224
-
292
+
293
+ console.log(`Installing @hanzo/dev v${version} for ${targetTriple}...`);
294
+
225
295
  for (const binary of binaries) {
226
296
  const binaryName = `${binary}-${targetTriple}${binaryExt}`;
227
- const localPath = join(binDir, binaryName);
297
+ const devBinaryName = `dev-${targetTriple}${binaryExt}`;
298
+ const localPath = join(binDir, devBinaryName);
228
299
  const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
229
-
230
- // Skip if already exists and has correct permissions
231
- if (existsSync(localPath)) {
232
- // Always try to fix permissions on Unix-like systems
233
- if (!isWindows) {
234
- try {
235
- chmodSync(localPath, 0o755);
236
- console.log(`✓ ${binaryName} already exists (permissions fixed)`);
237
- } catch (e) {
238
- console.log(`✓ ${binaryName} already exists`);
239
- }
240
- } else {
241
- console.log(`✓ ${binaryName} already exists`);
242
- }
243
- continue;
244
- }
245
300
 
246
- // Fast path: if a valid cached binary exists for this version+triple, reuse it.
301
+ // Fast path: if a valid cached binary exists, reuse it
247
302
  try {
248
303
  if (existsSync(cachePath)) {
249
304
  const valid = validateDownloadedBinary(cachePath);
250
305
  if (valid.ok) {
251
- copyFileSync(cachePath, localPath);
252
- if (!isWindows) chmodSync(localPath, 0o755);
253
- console.log(`✓ Installed ${binaryName} from user cache`);
254
- continue; // next binary
306
+ const wsl = isWSL();
307
+ const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
308
+ const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
309
+ if (mirrorToLocal) {
310
+ copyFileSync(cachePath, localPath);
311
+ try { chmodSync(localPath, 0o755); } catch {}
312
+ }
313
+ console.log(`✓ ${devBinaryName} ready from user cache`);
314
+ continue;
255
315
  }
256
316
  }
257
317
  } catch {
258
- // Ignore cache errors and fall through to normal paths
318
+ // Ignore cache errors
259
319
  }
260
-
261
- // First try platform package via npm optionalDependencies (fast path on npm CDN).
320
+
321
+ // Try platform package via npm optionalDependencies
262
322
  const require = createRequire(import.meta.url);
263
323
  const platformPkg = (() => {
264
324
  const name = (() => {
265
- if (isWindows) return '@just-every/code-win32-x64';
325
+ if (isWindows) return '@hanzo/dev-win32-x64';
266
326
  const plt = platform();
267
327
  const cpu = arch();
268
- if (plt === 'darwin' && cpu === 'arm64') return '@just-every/code-darwin-arm64';
269
- if (plt === 'darwin' && cpu === 'x64') return '@just-every/code-darwin-x64';
270
- if (plt === 'linux' && cpu === 'x64') return '@just-every/code-linux-x64-musl';
271
- if (plt === 'linux' && cpu === 'arm64') return '@just-every/code-linux-arm64-musl';
328
+ if (plt === 'darwin' && cpu === 'arm64') return '@hanzo/dev-darwin-arm64';
329
+ if (plt === 'darwin' && cpu === 'x64') return '@hanzo/dev-darwin-x64';
330
+ if (plt === 'linux' && cpu === 'x64') return '@hanzo/dev-linux-x64-musl';
331
+ if (plt === 'linux' && cpu === 'arm64') return '@hanzo/dev-linux-arm64-musl';
272
332
  return null;
273
333
  })();
274
334
  if (!name) return null;
@@ -283,30 +343,30 @@ async function main() {
283
343
 
284
344
  if (platformPkg) {
285
345
  try {
286
- // Expect binary inside platform package bin directory
287
- const src = join(platformPkg.dir, 'bin', binaryName);
346
+ const src = join(platformPkg.dir, 'bin', devBinaryName);
288
347
  if (!existsSync(src)) {
289
348
  throw new Error(`platform package missing binary: ${platformPkg.name}`);
290
349
  }
291
- copyFileSync(src, localPath);
292
- if (!isWindows) chmodSync(localPath, 0o755);
293
- console.log(`✓ Installed ${binaryName} from ${platformPkg.name}`);
294
- // Populate cache for future npx runs
295
- try {
296
- if (!existsSync(cachePath)) {
297
- copyFileSync(localPath, cachePath);
298
- }
299
- } catch {}
300
- continue; // next binary
350
+ await writeCacheAtomic(src, cachePath);
351
+ const wsl = isWSL();
352
+ const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
353
+ const mirrorToLocal = !(isWindows || (wsl && isPathOnWindowsFs(binDirReal)));
354
+ if (mirrorToLocal) {
355
+ copyFileSync(cachePath, localPath);
356
+ try { chmodSync(localPath, 0o755); } catch {}
357
+ }
358
+ console.log(`✓ Installed ${devBinaryName} from ${platformPkg.name} (cached)`);
359
+ continue;
301
360
  } catch (e) {
302
361
  console.warn(`⚠ Failed platform package install (${e.message}), falling back to GitHub download`);
303
362
  }
304
363
  }
305
364
 
306
- // Decide archive format per OS with fallback on macOS/Linux:
307
- // - Windows: .zip
308
- // - macOS/Linux: prefer .zst if `zstd` CLI is available; otherwise use .tar.gz
365
+ // Download from GitHub release
309
366
  const isWin = isWindows;
367
+ const detectedWSL = isWSL();
368
+ const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
369
+ const mirrorToLocal = !(isWin || (detectedWSL && isPathOnWindowsFs(binDirReal)));
310
370
  let useZst = false;
311
371
  if (!isWin) {
312
372
  try {
@@ -317,35 +377,61 @@ async function main() {
317
377
  }
318
378
  }
319
379
  const archiveName = isWin ? `${binaryName}.zip` : (useZst ? `${binaryName}.zst` : `${binaryName}.tar.gz`);
320
- const downloadUrl = `https://github.com/just-every/code/releases/download/v${version}/${archiveName}`;
380
+ const downloadUrl = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
321
381
 
322
382
  console.log(`Downloading ${archiveName}...`);
323
383
  try {
324
- const tmpPath = join(binDir, `.${archiveName}.part`);
384
+ const needsIsolation = isWin || (!isWin && !mirrorToLocal);
385
+ let safeTempDir = needsIsolation ? join(tmpdir(), 'hanzo', 'dev', version) : binDir;
386
+ if (needsIsolation) {
387
+ try {
388
+ if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
389
+ } catch {
390
+ try {
391
+ safeTempDir = getCacheDir(version);
392
+ if (!existsSync(safeTempDir)) mkdirSync(safeTempDir, { recursive: true });
393
+ } catch {}
394
+ }
395
+ }
396
+ const tmpPath = join(needsIsolation ? safeTempDir : binDir, `.${archiveName}.part`);
325
397
  await downloadBinary(downloadUrl, tmpPath);
326
398
 
327
399
  if (isWin) {
328
- // Unzip the single-file archive using PowerShell (built-in)
400
+ const unzipDest = safeTempDir;
329
401
  try {
330
- const psCmd = `powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmpPath}' -DestinationPath '${binDir}' -Force"`;
331
- execSync(psCmd, { stdio: 'ignore' });
402
+ const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
403
+ const psFull = join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
404
+ const psCmd = `Expand-Archive -Path '${tmpPath}' -DestinationPath '${unzipDest}' -Force`;
405
+ let ok = false;
406
+ try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
407
+ if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
408
+ if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
409
+ if (!ok) { execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, { stdio: 'ignore', shell: true }); }
332
410
  } catch (e) {
333
411
  throw new Error(`failed to unzip archive: ${e.message}`);
334
412
  } finally {
335
413
  try { unlinkSync(tmpPath); } catch {}
336
414
  }
415
+ try {
416
+ const extractedPath = join(unzipDest, binaryName);
417
+ // Rename code-* to dev-* in cache
418
+ const devCachePath = cachePath;
419
+ await writeCacheAtomic(extractedPath, devCachePath);
420
+ try { unlinkSync(extractedPath); } catch {}
421
+ } catch (e) {
422
+ throw new Error(`failed to move binary to cache: ${e.message}`);
423
+ }
337
424
  } else {
425
+ const downloadedPath = join(binDir, binaryName);
338
426
  if (useZst) {
339
- // Decompress .zst via system zstd
340
427
  try {
341
- execSync(`zstd -d '${tmpPath}' -o '${localPath}'`, { stdio: 'ignore', shell: true });
428
+ execSync(`zstd -d '${tmpPath}' -o '${downloadedPath}'`, { stdio: 'ignore', shell: true });
342
429
  } catch (e) {
343
430
  try { unlinkSync(tmpPath); } catch {}
344
431
  throw new Error(`failed to decompress .zst (need zstd CLI): ${e.message}`);
345
432
  }
346
433
  try { unlinkSync(tmpPath); } catch {}
347
434
  } else {
348
- // Extract .tar.gz using system tar
349
435
  try {
350
436
  execSync(`tar -xzf '${tmpPath}' -C '${binDir}'`, { stdio: 'ignore', shell: true });
351
437
  } catch (e) {
@@ -354,43 +440,47 @@ async function main() {
354
440
  }
355
441
  try { unlinkSync(tmpPath); } catch {}
356
442
  }
443
+ // Rename code-* to dev-*
444
+ if (existsSync(downloadedPath)) {
445
+ if (mirrorToLocal) {
446
+ renameSync(downloadedPath, localPath);
447
+ } else {
448
+ const extractedPath = downloadedPath;
449
+ await writeCacheAtomic(extractedPath, cachePath);
450
+ try { unlinkSync(extractedPath); } catch {}
451
+ }
452
+ }
357
453
  }
358
454
 
359
- // Validate header to avoid corrupt binaries causing spawn EFTYPE/ENOEXEC
360
- const valid = validateDownloadedBinary(localPath);
455
+ const valid = validateDownloadedBinary(isWin ? cachePath : (mirrorToLocal ? localPath : cachePath));
361
456
  if (!valid.ok) {
362
- try { unlinkSync(localPath); } catch {}
457
+ try { (isWin || !mirrorToLocal) ? unlinkSync(cachePath) : unlinkSync(localPath); } catch {}
363
458
  throw new Error(`invalid binary (${valid.reason})`);
364
459
  }
365
460
 
366
- // Make executable on Unix-like systems
367
- if (!isWindows) {
461
+ if (!isWin && mirrorToLocal) {
368
462
  chmodSync(localPath, 0o755);
369
463
  }
370
-
371
- console.log(`✓ Installed ${binaryName}`);
372
- // Save into persistent cache for future fast installs
373
- try {
374
- copyFileSync(localPath, cachePath);
375
- } catch {}
464
+
465
+ console.log(`✓ Installed ${devBinaryName}${(isWin || !mirrorToLocal) ? ' (cached)' : ''}`);
466
+ if (!isWin && mirrorToLocal) {
467
+ try { await writeCacheAtomic(localPath, cachePath); } catch {}
468
+ }
376
469
  } catch (error) {
377
- console.error(`✗ Failed to install ${binaryName}: ${error.message}`);
470
+ console.error(`✗ Failed to install ${devBinaryName}: ${error.message}`);
378
471
  console.error(` Downloaded from: ${downloadUrl}`);
379
- // Continue with other binaries even if one fails
380
472
  }
381
473
  }
382
474
 
383
- // Create platform-specific symlink/copy for main binary
384
- const mainBinary = `code-${targetTriple}${binaryExt}`;
475
+ const mainBinary = `dev-${targetTriple}${binaryExt}`;
385
476
  const mainBinaryPath = join(binDir, mainBinary);
386
-
387
- if (existsSync(mainBinaryPath)) {
477
+
478
+ if (existsSync(mainBinaryPath) || existsSync(getCachedBinaryPath(version, targetTriple, platform() === 'win32'))) {
388
479
  try {
389
- const stats = statSync(mainBinaryPath);
390
- if (!stats.size) {
391
- throw new Error('binary is empty (download likely failed)');
392
- }
393
- const valid = validateDownloadedBinary(mainBinaryPath);
480
+ const probePath = existsSync(mainBinaryPath) ? mainBinaryPath : getCachedBinaryPath(version, targetTriple, platform() === 'win32');
481
+ const stats = statSync(probePath);
482
+ if (!stats.size) throw new Error('binary is empty (download likely failed)');
483
+ const valid = validateDownloadedBinary(probePath);
394
484
  if (!valid.ok) {
395
485
  console.warn(`⚠ Main dev binary appears invalid: ${valid.reason}`);
396
486
  console.warn(' Try reinstalling or check your network/proxy settings.');
@@ -399,191 +489,25 @@ async function main() {
399
489
  console.warn(`⚠ Main dev binary appears invalid: ${e.message}`);
400
490
  console.warn(' Try reinstalling or check your network/proxy settings.');
401
491
  }
402
- console.log('Setting up main dev binary...');
403
-
404
- // On Windows, we can't use symlinks easily, so update the JS wrapper
405
- // On Unix, the JS wrapper will find the correct binary
406
492
  console.log('✓ Installation complete!');
407
493
  } else {
408
- console.warn('⚠ Main code binary not found. You may need to build from source.');
494
+ console.warn('⚠ Main dev binary not found. You may need to build from source.');
409
495
  }
496
+ }
410
497
 
411
- // Handle collisions (e.g., VS Code) and add wrappers. We no longer publish a
412
- // `code` bin in package.json. Instead, for global installs we create a `code`
413
- // wrapper only when there is no conflicting `code` earlier on PATH. This avoids
414
- // hijacking the VS Code CLI while still giving users a friendly name when safe.
415
- // For upgrades from older versions that published a `code` bin, we also remove
416
- // our old shim if a conflict is detected.
417
- if (isGlobal && !isNpx) try {
418
- const isTTY = process.stdout && process.stdout.isTTY;
419
- const isWindows = platform() === 'win32';
420
- const ua = process.env.npm_config_user_agent || '';
421
- const isBun = ua.includes('bun') || !!process.env.BUN_INSTALL;
422
-
423
- const installedCmds = new Set(['coder']); // global install always exposes coder via package manager
424
- const skippedCmds = [];
425
-
426
- // Helper to resolve all 'code' on PATH
427
- const resolveAllOnPath = () => {
428
- try {
429
- if (isWindows) {
430
- const out = execSync('where code', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
431
- return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
432
- }
433
- let out = '';
434
- try {
435
- out = execSync('bash -lc "which -a code 2>/dev/null"', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
436
- } catch {
437
- try {
438
- out = execSync('command -v code || true', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
439
- } catch { out = ''; }
440
- }
441
- return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
442
- } catch {
443
- return [];
444
- }
445
- };
446
-
447
- if (isBun) {
448
- // Bun creates shims for every bin; if another 'code' exists elsewhere on PATH, remove Bun's shim
449
- let bunBin = '';
450
- try {
451
- const home = process.env.HOME || process.env.USERPROFILE || '';
452
- const bunBase = process.env.BUN_INSTALL || join(home, '.bun');
453
- bunBin = join(bunBase, 'bin');
454
- } catch {}
455
-
456
- const bunShim = join(bunBin || '', isWindows ? 'code.cmd' : 'code');
457
- const candidates = resolveAllOnPath();
458
- const other = candidates.find(p => p && (!bunBin || !p.startsWith(bunBin)));
459
- if (other && existsSync(bunShim)) {
460
- try {
461
- unlinkSync(bunShim);
462
- console.log(`✓ Skipped global 'code' shim under Bun (existing: ${other})`);
463
- skippedCmds.push({ name: 'code', reason: `existing: ${other}` });
464
- } catch (e) {
465
- console.log(`⚠ Could not remove Bun shim '${bunShim}': ${e.message}`);
466
- }
467
- } else if (!other) {
468
- // No conflict: create a wrapper that forwards to `coder`
469
- try {
470
- const wrapperPath = bunShim;
471
- if (isWindows) {
472
- const content = `@echo off\r\n"%~dp0coder" %*\r\n`;
473
- writeFileSync(wrapperPath, content);
474
- } else {
475
- const content = `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
476
- writeFileSync(wrapperPath, content);
477
- chmodSync(wrapperPath, 0o755);
478
- }
479
- console.log("✓ Created 'code' wrapper -> coder (bun)");
480
- installedCmds.add('code');
481
- } catch (e) {
482
- console.log(`⚠ Failed to create 'code' wrapper (bun): ${e.message}`);
483
- }
484
- }
485
-
486
- // Print summary for Bun
487
- const list = Array.from(installedCmds).sort().join(', ');
488
- console.log(`Commands installed (bun): ${list}`);
489
- if (skippedCmds.length) {
490
- for (const s of skippedCmds) console.error(`Commands skipped: ${s.name} (${s.reason})`);
491
- console.error('→ Use `coder` to run this tool.');
492
- }
493
- // Final friendly usage hint
494
- if (installedCmds.has('code')) {
495
- console.log("Use 'dev' to launch Hanzo Dev.");
496
- } else {
497
- console.log("Use 'dev' to launch Hanzo Dev.");
498
- }
499
- } else {
500
- // npm/pnpm/yarn path
501
- let globalBin = '';
502
- try {
503
- globalBin = execSync('npm bin -g', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
504
- } catch {}
505
-
506
- const ourShim = join(globalBin || '', isWindows ? 'code.cmd' : 'code');
507
- const candidates = resolveAllOnPath();
508
- const others = candidates.filter(p => p && (!ourShim || p !== ourShim));
509
- const collision = others.length > 0;
510
-
511
- const ensureWrapper = (name, args) => {
512
- if (!globalBin) return;
513
- try {
514
- const wrapperPath = join(globalBin, isWindows ? `${name}.cmd` : name);
515
- if (isWindows) {
516
- const content = `@echo off\r\n"%~dp0${collision ? 'coder' : 'code'}" ${args} %*\r\n`;
517
- writeFileSync(wrapperPath, content);
518
- } else {
519
- const content = `#!/bin/sh\nexec "$(dirname \"$0\")/${collision ? 'coder' : 'code'}" ${args} "$@"\n`;
520
- writeFileSync(wrapperPath, content);
521
- chmodSync(wrapperPath, 0o755);
522
- }
523
- console.log(`✓ Created wrapper '${name}' -> ${collision ? 'coder' : 'code'} ${args}`);
524
- installedCmds.add(name);
525
- } catch (e) {
526
- console.log(`⚠ Failed to create '${name}' wrapper: ${e.message}`);
527
- }
528
- };
529
-
530
- // Always create legacy wrappers so existing scripts keep working
531
- ensureWrapper('code-tui', '');
532
- ensureWrapper('code-exec', 'exec');
533
-
534
- if (collision) {
535
- console.error('⚠ Detected existing `code` on PATH:');
536
- for (const p of others) console.error(` - ${p}`);
537
- if (globalBin) {
538
- try {
539
- if (existsSync(ourShim)) {
540
- unlinkSync(ourShim);
541
- console.error(`✓ Skipped global 'code' shim (removed ${ourShim})`);
542
- skippedCmds.push({ name: 'code', reason: `existing: ${others[0]}` });
543
- }
544
- } catch (e) {
545
- console.error(`⚠ Could not remove npm shim '${ourShim}': ${e.message}`);
546
- }
547
- console.error('→ Use `coder` to run this tool.');
548
- } else {
549
- console.log('Note: could not determine npm global bin; skipping alias creation.');
550
- }
551
- } else {
552
- // No collision; ensure a 'code' wrapper exists forwarding to 'coder'
553
- if (globalBin) {
554
- try {
555
- const content = isWindows
556
- ? `@echo off\r\n"%~dp0coder" %*\r\n`
557
- : `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
558
- writeFileSync(ourShim, content);
559
- if (!isWindows) chmodSync(ourShim, 0o755);
560
- console.log("✓ Created 'code' wrapper -> coder");
561
- installedCmds.add('code');
562
- } catch (e) {
563
- console.log(`⚠ Failed to create 'code' wrapper: ${e.message}`);
564
- }
565
- }
566
- }
567
-
568
- // Print summary for npm/pnpm/yarn
569
- const list = Array.from(installedCmds).sort().join(', ');
570
- console.log(`Commands installed: ${list}`);
571
- if (skippedCmds.length) {
572
- for (const s of skippedCmds) console.log(`Commands skipped: ${s.name} (${s.reason})`);
573
- }
574
- // Final friendly usage hint
575
- if (installedCmds.has('code')) {
576
- console.log("Use 'dev' to launch Hanzo Dev.");
577
- } else {
578
- console.log("Use 'dev' to launch Hanzo Dev.");
579
- }
580
- }
498
+ function isExecutedDirectly() {
499
+ const entry = process.argv[1];
500
+ if (!entry) return false;
501
+ try {
502
+ return resolve(entry) === fileURLToPath(import.meta.url);
581
503
  } catch {
582
- // non-fatal
504
+ return false;
583
505
  }
584
506
  }
585
507
 
586
- main().catch(error => {
587
- console.error('Installation failed:', error);
588
- process.exit(1);
589
- });
508
+ if (isExecutedDirectly()) {
509
+ runPostinstall().catch(error => {
510
+ console.error('Installation failed:', error);
511
+ process.exit(1);
512
+ });
513
+ }