@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.
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // Unified entry point for the Hanzo Dev CLI.
2
+ // Unified entry point for the Code CLI.
3
3
 
4
4
  import path from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { platform as nodePlatform, arch as nodeArch } from "os";
7
7
  import { execSync } from "child_process";
8
8
  import { get as httpsGet } from "https";
9
- import { runPostinstall } from "../postinstall.js";
10
9
 
11
10
  // __dirname equivalent in ESM
12
11
  const __filename = fileURLToPath(import.meta.url);
@@ -14,7 +13,11 @@ const __dirname = path.dirname(__filename);
14
13
 
15
14
  const { platform, arch } = process;
16
15
 
17
- function isWSL() {
16
+ // Important: Never delegate to another system's `code` binary (e.g., VS Code).
17
+ // When users run via `npx @hanzo/dev`, we must always execute our
18
+ // packaged native binary by absolute path to avoid PATH collisions.
19
+
20
+ const isWSL = () => {
18
21
  if (platform !== "linux") return false;
19
22
  try {
20
23
  const txt = readFileSync("/proc/version", "utf8").toLowerCase();
@@ -22,7 +25,7 @@ function isWSL() {
22
25
  } catch {
23
26
  return false;
24
27
  }
25
- }
28
+ };
26
29
 
27
30
  let targetTriple = null;
28
31
  switch (platform) {
@@ -70,10 +73,11 @@ if (!targetTriple) {
70
73
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
71
74
  }
72
75
 
73
- // Binary names for Hanzo dev
76
+ // Prefer new 'dev-*' binary names; fall back to legacy 'coder-*' if missing.
74
77
  let binaryPath = path.join(__dirname, "..", "bin", `dev-${targetTriple}`);
75
- let legacyBinaryPath = path.join(__dirname, "..", "bin", `dev-${targetTriple}`);
78
+ let legacyBinaryPath = path.join(__dirname, "..", "bin", `coder-${targetTriple}`);
76
79
 
80
+ // --- Bootstrap helper (runs if the binary is missing, e.g. Bun blocked postinstall) ---
77
81
  import { existsSync, chmodSync, statSync, openSync, readSync, closeSync, mkdirSync, copyFileSync, readFileSync, unlinkSync, createWriteStream } from "fs";
78
82
 
79
83
  const validateBinary = (p) => {
@@ -116,12 +120,14 @@ const getCacheDir = (version) => {
116
120
  } else {
117
121
  base = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
118
122
  }
119
- const dir = path.join(base, "hanzo", "dev", version);
123
+ const dir = path.join(base, "hanzoai", "dev", version);
120
124
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
121
125
  return dir;
122
126
  };
123
127
 
124
128
  const getCachedBinaryPath = (version) => {
129
+ // targetTriple already includes the proper extension on Windows ("...msvc.exe").
130
+ // Do not append another suffix; just use the exact targetTriple-derived name.
125
131
  const cacheDir = getCacheDir(version);
126
132
  return path.join(cacheDir, `dev-${targetTriple}`);
127
133
  };
@@ -132,6 +138,7 @@ const httpsDownload = (url, dest) => new Promise((resolve, reject) => {
132
138
  const req = httpsGet(url, (res) => {
133
139
  const status = res.statusCode || 0;
134
140
  if (status >= 300 && status < 400 && res.headers.location) {
141
+ // follow one redirect recursively
135
142
  return resolve(httpsDownload(res.headers.location, dest));
136
143
  }
137
144
  if (status !== 200) {
@@ -156,17 +163,19 @@ const httpsDownload = (url, dest) => new Promise((resolve, reject) => {
156
163
 
157
164
  const tryBootstrapBinary = async () => {
158
165
  try {
166
+ // 1) Read our published version
159
167
  const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
160
168
  const version = pkg.version;
161
169
 
162
170
  const binDir = path.join(__dirname, "..", "bin");
163
171
  if (!existsSync(binDir)) mkdirSync(binDir, { recursive: true });
164
172
 
165
- // Fast path: user cache
173
+ // 2) Fast path: user cache
166
174
  const cachePath = getCachedBinaryPath(version);
167
175
  if (existsSync(cachePath)) {
168
176
  const v = validateBinary(cachePath);
169
177
  if (v.ok) {
178
+ // Prefer running directly from cache; mirror into node_modules on Unix
170
179
  if (platform !== "win32") {
171
180
  copyFileSync(cachePath, binaryPath);
172
181
  try { chmodSync(binaryPath, 0o755); } catch {}
@@ -175,11 +184,11 @@ const tryBootstrapBinary = async () => {
175
184
  }
176
185
  }
177
186
 
178
- // Try platform package (if present)
187
+ // 3) Try platform package (if present)
179
188
  try {
180
189
  const req = (await import("module")).createRequire(import.meta.url);
181
190
  const name = (() => {
182
- if (platform === "win32") return "@hanzo/dev-win32-x64";
191
+ if (platform === "win32") return "@hanzo/dev-win32-x64"; // may be unpublished; falls through
183
192
  const plt = nodePlatform();
184
193
  const cpu = nodeArch();
185
194
  if (plt === "darwin" && cpu === "arm64") return "@hanzo/dev-darwin-arm64";
@@ -194,6 +203,7 @@ const tryBootstrapBinary = async () => {
194
203
  const pkgDir = path.dirname(pkgJson);
195
204
  const src = path.join(pkgDir, "bin", `dev-${targetTriple}`);
196
205
  if (existsSync(src)) {
206
+ // Always ensure cache has the binary; on Unix mirror into node_modules
197
207
  copyFileSync(src, cachePath);
198
208
  if (platform !== "win32") {
199
209
  copyFileSync(cachePath, binaryPath);
@@ -205,22 +215,21 @@ const tryBootstrapBinary = async () => {
205
215
  }
206
216
  } catch { /* ignore */ }
207
217
 
208
- // Download from GitHub release
218
+ // 4) Download from GitHub release
209
219
  const isWin = platform === "win32";
210
- // Use 'dev-*' binary names matching the release artifacts
211
- const binaryName = `dev-${targetTriple}`;
212
220
  const archiveName = isWin
213
- ? `${binaryName}.zip`
214
- : (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `${binaryName}.zst`; } catch { return `${binaryName}.tar.gz`; } })();
221
+ ? `dev-${targetTriple}.zip`
222
+ : (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `dev-${targetTriple}.zst`; } catch { return `dev-${targetTriple}.tar.gz`; } })();
215
223
  const url = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
216
224
  const tmp = path.join(binDir, `.${archiveName}.part`);
217
225
  return httpsDownload(url, tmp)
218
226
  .then(() => {
219
227
  if (isWin) {
228
+ // Extract zip with robust fallbacks and use a safe temp dir, then move to cache
220
229
  try {
221
- const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
230
+ const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\\Windows';
222
231
  const psFull = path.join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
223
- const unzipDest = getCacheDir(version);
232
+ const unzipDest = getCacheDir(version); // extract directly to cache location
224
233
  const psCmd = `Expand-Archive -Path '${tmp}' -DestinationPath '${unzipDest}' -Force`;
225
234
  let ok = false;
226
235
  try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
@@ -241,7 +250,11 @@ const tryBootstrapBinary = async () => {
241
250
  try { unlinkSync(tmp); } catch {}
242
251
  }
243
252
  }
244
- if (platform !== "win32") {
253
+ // On Windows, the file was extracted directly into the cache dir
254
+ if (platform === "win32") {
255
+ // Ensure the expected filename exists in cache; Expand-Archive extracts exact name
256
+ // No action required here; validation occurs below against cachePath
257
+ } else {
245
258
  try { copyFileSync(binaryPath, cachePath); } catch {}
246
259
  }
247
260
 
@@ -256,26 +269,13 @@ const tryBootstrapBinary = async () => {
256
269
  }
257
270
  };
258
271
 
259
- // If missing, attempt to bootstrap into place
260
- let binaryReady = existsSync(binaryPath) || existsSync(legacyBinaryPath);
261
- if (!binaryReady) {
262
- let runtimePostinstallError = null;
263
- try {
264
- await runPostinstall({ invokedByRuntime: true, skipGlobalAlias: true });
265
- } catch (err) {
266
- runtimePostinstallError = err;
267
- }
268
-
269
- binaryReady = existsSync(binaryPath) || existsSync(legacyBinaryPath);
270
- if (!binaryReady) {
271
- const ok = await tryBootstrapBinary();
272
- if (!ok) {
273
- if (runtimePostinstallError && !lastBootstrapError) {
274
- lastBootstrapError = runtimePostinstallError;
275
- }
276
- if (existsSync(legacyBinaryPath) && !existsSync(binaryPath)) {
277
- binaryPath = legacyBinaryPath;
278
- }
272
+ // If missing, attempt to bootstrap into place (helps when Bun blocks postinstall)
273
+ if (!existsSync(binaryPath) && !existsSync(legacyBinaryPath)) {
274
+ const ok = await tryBootstrapBinary();
275
+ if (!ok) {
276
+ // retry legacy name in case archive provided coder-*
277
+ if (existsSync(legacyBinaryPath) && !existsSync(binaryPath)) {
278
+ binaryPath = legacyBinaryPath;
279
279
  }
280
280
  }
281
281
  }
@@ -295,14 +295,17 @@ try {
295
295
  // ignore
296
296
  }
297
297
 
298
+ // Check if binary exists and try to fix permissions if needed
299
+ // fs imports are above; keep for readability if tree-shaken by bundlers
298
300
  import { spawnSync } from "child_process";
299
301
  if (existsSync(binaryPath)) {
300
302
  try {
303
+ // Ensure binary is executable on Unix-like systems
301
304
  if (platform !== "win32") {
302
305
  chmodSync(binaryPath, 0o755);
303
306
  }
304
307
  } catch (e) {
305
- // Ignore permission errors
308
+ // Ignore permission errors, will be caught below if it's a real problem
306
309
  }
307
310
  } else {
308
311
  console.error(`Binary not found: ${binaryPath}`);
@@ -321,6 +324,9 @@ if (existsSync(binaryPath)) {
321
324
  process.exit(1);
322
325
  }
323
326
 
327
+ // Lightweight header validation to provide clearer errors before spawn
328
+ // Reuse the validateBinary helper defined above in the bootstrap section.
329
+
324
330
  const validation = validateBinary(binaryPath);
325
331
  if (!validation.ok) {
326
332
  console.error(`The native binary at ${binaryPath} appears invalid: ${validation.reason}`);
@@ -339,25 +345,47 @@ if (!validation.ok) {
339
345
  process.exit(1);
340
346
  }
341
347
 
342
- // If running under npx/npm, emit a concise notice
348
+ // If running under npx/npm, emit a concise notice about which binary path is used
343
349
  try {
344
350
  const ua = process.env.npm_config_user_agent || "";
345
351
  const isNpx = ua.includes("npx");
346
352
  if (isNpx && process.stderr && process.stderr.isTTY) {
347
- console.error(`@hanzo/dev: running bundled binary -> ${binaryPath}`);
353
+ // Best-effort discovery of another 'code' on PATH for user clarity
354
+ let otherCode = "";
355
+ try {
356
+ const cmd = process.platform === "win32" ? "where code" : "command -v code || which code || true";
357
+ const out = spawnSync(process.platform === "win32" ? "cmd" : "bash", [
358
+ process.platform === "win32" ? "/c" : "-lc",
359
+ cmd,
360
+ ], { encoding: "utf8" });
361
+ const line = (out.stdout || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
362
+ if (line && !line.includes("@hanzo/dev")) {
363
+ otherCode = line;
364
+ }
365
+ } catch {}
366
+ if (otherCode) {
367
+ console.error(`@hanzo/dev: running bundled binary -> ${binaryPath}`);
368
+ console.error(`Note: a different 'code' exists at ${otherCode}; not delegating.`);
369
+ } else {
370
+ console.error(`@hanzo/dev: running bundled binary -> ${binaryPath}`);
371
+ }
348
372
  }
349
373
  } catch {}
350
374
 
375
+ // Use an asynchronous spawn instead of spawnSync so that Node is able to
376
+ // respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
377
+ // executing. This allows us to forward those signals to the child process
378
+ // and guarantees that when either the child terminates or the parent
379
+ // receives a fatal signal, both processes exit in a predictable manner.
351
380
  const { spawn } = await import("child_process");
352
381
 
353
- process.env.DEV_BINARY_PATH = binaryPath;
354
-
355
382
  const child = spawn(binaryPath, process.argv.slice(2), {
356
383
  stdio: "inherit",
357
- env: { ...process.env, DEV_MANAGED_BY_NPM: "1", DEV_BINARY_PATH: binaryPath },
384
+ env: { ...process.env, CODER_MANAGED_BY_NPM: "1", CODEX_MANAGED_BY_NPM: "1" },
358
385
  });
359
386
 
360
387
  child.on("error", (err) => {
388
+ // Typically triggered when the binary is missing or not executable.
361
389
  const code = err && err.code;
362
390
  if (code === 'EACCES') {
363
391
  console.error(`Permission denied: ${binaryPath}`);
@@ -381,6 +409,10 @@ child.on("error", (err) => {
381
409
  process.exit(1);
382
410
  });
383
411
 
412
+ // Forward common termination signals to the child so that it shuts down
413
+ // gracefully. In the handler we temporarily disable the default behavior of
414
+ // exiting immediately; once the child has been signaled we simply wait for
415
+ // its exit event which will in turn terminate the parent (see below).
384
416
  const forwardSignal = (signal) => {
385
417
  if (child.killed) {
386
418
  return;
@@ -396,6 +428,11 @@ const forwardSignal = (signal) => {
396
428
  process.on(sig, () => forwardSignal(sig));
397
429
  });
398
430
 
431
+ // When the child exits, mirror its termination reason in the parent so that
432
+ // shell scripts and other tooling observe the correct exit status.
433
+ // Wrap the lifetime of the child process in a Promise so that we can await
434
+ // its termination in a structured way. The Promise resolves with an object
435
+ // describing how the child exited: either via exit code or due to a signal.
399
436
  const childResult = await new Promise((resolve) => {
400
437
  child.on("exit", (code, signal) => {
401
438
  if (signal) {
@@ -407,6 +444,8 @@ const childResult = await new Promise((resolve) => {
407
444
  });
408
445
 
409
446
  if (childResult.type === "signal") {
447
+ // Re-emit the same signal so that the parent terminates with the expected
448
+ // semantics (this also sets the correct exit code of 128 + n).
410
449
  process.kill(process.pid, childResult.signal);
411
450
  } else {
412
451
  process.exit(childResult.exitCode);
package/bin/codex.js ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // Unified entry point for the Codex CLI.
3
+
4
+ import { spawn } from "node:child_process";
5
+ import { existsSync } from "fs";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ // __dirname equivalent in ESM
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ const { platform, arch } = process;
14
+
15
+ let targetTriple = null;
16
+ switch (platform) {
17
+ case "linux":
18
+ case "android":
19
+ switch (arch) {
20
+ case "x64":
21
+ targetTriple = "x86_64-unknown-linux-musl";
22
+ break;
23
+ case "arm64":
24
+ targetTriple = "aarch64-unknown-linux-musl";
25
+ break;
26
+ default:
27
+ break;
28
+ }
29
+ break;
30
+ case "darwin":
31
+ switch (arch) {
32
+ case "x64":
33
+ targetTriple = "x86_64-apple-darwin";
34
+ break;
35
+ case "arm64":
36
+ targetTriple = "aarch64-apple-darwin";
37
+ break;
38
+ default:
39
+ break;
40
+ }
41
+ break;
42
+ case "win32":
43
+ switch (arch) {
44
+ case "x64":
45
+ targetTriple = "x86_64-pc-windows-msvc";
46
+ break;
47
+ case "arm64":
48
+ targetTriple = "aarch64-pc-windows-msvc";
49
+ break;
50
+ default:
51
+ break;
52
+ }
53
+ break;
54
+ default:
55
+ break;
56
+ }
57
+
58
+ if (!targetTriple) {
59
+ throw new Error(`Unsupported platform: ${platform} (${arch})`);
60
+ }
61
+
62
+ const vendorRoot = path.join(__dirname, "..", "vendor");
63
+ const archRoot = path.join(vendorRoot, targetTriple);
64
+ const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
65
+ const binaryPath = path.join(archRoot, "codex", codexBinaryName);
66
+
67
+ // Use an asynchronous spawn instead of spawnSync so that Node is able to
68
+ // respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
69
+ // executing. This allows us to forward those signals to the child process
70
+ // and guarantees that when either the child terminates or the parent
71
+ // receives a fatal signal, both processes exit in a predictable manner.
72
+
73
+ function getUpdatedPath(newDirs) {
74
+ const pathSep = process.platform === "win32" ? ";" : ":";
75
+ const existingPath = process.env.PATH || "";
76
+ const updatedPath = [
77
+ ...newDirs,
78
+ ...existingPath.split(pathSep).filter(Boolean),
79
+ ].join(pathSep);
80
+ return updatedPath;
81
+ }
82
+
83
+ const additionalDirs = [];
84
+ const pathDir = path.join(archRoot, "path");
85
+ if (existsSync(pathDir)) {
86
+ additionalDirs.push(pathDir);
87
+ }
88
+ const updatedPath = getUpdatedPath(additionalDirs);
89
+
90
+ const child = spawn(binaryPath, process.argv.slice(2), {
91
+ stdio: "inherit",
92
+ env: { ...process.env, PATH: updatedPath, CODEX_MANAGED_BY_NPM: "1" },
93
+ });
94
+
95
+ child.on("error", (err) => {
96
+ // Typically triggered when the binary is missing or not executable.
97
+ // Re-throwing here will terminate the parent with a non-zero exit code
98
+ // while still printing a helpful stack trace.
99
+ // eslint-disable-next-line no-console
100
+ console.error(err);
101
+ process.exit(1);
102
+ });
103
+
104
+ // Forward common termination signals to the child so that it shuts down
105
+ // gracefully. In the handler we temporarily disable the default behavior of
106
+ // exiting immediately; once the child has been signaled we simply wait for
107
+ // its exit event which will in turn terminate the parent (see below).
108
+ const forwardSignal = (signal) => {
109
+ if (child.killed) {
110
+ return;
111
+ }
112
+ try {
113
+ child.kill(signal);
114
+ } catch {
115
+ /* ignore */
116
+ }
117
+ };
118
+
119
+ ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
120
+ process.on(sig, () => forwardSignal(sig));
121
+ });
122
+
123
+ // When the child exits, mirror its termination reason in the parent so that
124
+ // shell scripts and other tooling observe the correct exit status.
125
+ // Wrap the lifetime of the child process in a Promise so that we can await
126
+ // its termination in a structured way. The Promise resolves with an object
127
+ // describing how the child exited: either via exit code or due to a signal.
128
+ const childResult = await new Promise((resolve) => {
129
+ child.on("exit", (code, signal) => {
130
+ if (signal) {
131
+ resolve({ type: "signal", signal });
132
+ } else {
133
+ resolve({ type: "code", exitCode: code ?? 1 });
134
+ }
135
+ });
136
+ });
137
+
138
+ if (childResult.type === "signal") {
139
+ // Re-emit the same signal so that the parent terminates with the expected
140
+ // semantics (this also sets the correct exit code of 128 + n).
141
+ process.kill(process.pid, childResult.signal);
142
+ } else {
143
+ process.exit(childResult.exitCode);
144
+ }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@hanzo/dev",
3
- "version": "0.6.90",
3
+ "version": "0.6.92",
4
4
  "license": "Apache-2.0",
5
- "description": "Hanzo AI coding assistant - intelligent CLI for developers",
5
+ "description": "Local AI coding agent for your terminal, powered by Hanzo AI",
6
6
  "bin": {
7
- "dev": "bin/dev.js",
8
- "hanzo": "bin/dev.js"
7
+ "dev": "bin/coder.js"
9
8
  },
10
9
  "type": "module",
11
10
  "engines": {
12
11
  "node": ">=16"
13
12
  },
14
13
  "files": [
15
- "bin/dev.js",
14
+ "bin/coder.js",
15
+ "bin/codex.js",
16
16
  "postinstall.js",
17
17
  "scripts/preinstall.js",
18
18
  "scripts/windows-cleanup.ps1",
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "preinstall": "node scripts/preinstall.js",
23
23
  "postinstall": "node postinstall.js",
24
- "prepublishOnly": "node -e \"const fs=require('fs'),path=require('path'); const repoGit=path.join(__dirname,'..','.git'); const inCi=process.env.GITHUB_ACTIONS==='true'||process.env.CI==='true'; if(fs.existsSync(repoGit) && !inCi){ console.error('Refusing to publish from dev-cli. Publishing happens via release.yml.'); process.exit(1);} else { console.log(inCi ? 'CI publish detected.' : 'Publishing staged package...'); }\""
24
+ "prepublishOnly": "node -e \"const fs=require('fs'),path=require('path'); const repoGit=path.join(__dirname,'..','.git'); const inCi=process.env.GITHUB_ACTIONS==='true'||process.env.CI==='true'; if(fs.existsSync(repoGit) && !inCi){ console.error('Refusing to publish from codex-cli. Publishing happens via release.yml.'); process.exit(1);} else { console.log(inCi ? 'CI publish detected.' : 'Publishing staged package...'); }\""
25
25
  },
26
26
  "repository": {
27
27
  "type": "git",
@@ -35,10 +35,10 @@
35
35
  "prettier": "^3.3.3"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@hanzo/dev-darwin-arm64": "0.6.90",
39
- "@hanzo/dev-darwin-x64": "0.6.90",
40
- "@hanzo/dev-linux-x64-musl": "0.6.90",
41
- "@hanzo/dev-linux-arm64-musl": "0.6.90",
42
- "@hanzo/dev-win32-x64": "0.6.90"
38
+ "@hanzo/dev-darwin-arm64": "0.6.92",
39
+ "@hanzo/dev-darwin-x64": "0.6.92",
40
+ "@hanzo/dev-linux-x64-musl": "0.6.92",
41
+ "@hanzo/dev-linux-arm64-musl": "0.6.92",
42
+ "@hanzo/dev-win32-x64": "0.6.92"
43
43
  }
44
44
  }