@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/bin/dev.js CHANGED
@@ -6,6 +6,7 @@ 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";
9
10
 
10
11
  // __dirname equivalent in ESM
11
12
  const __filename = fileURLToPath(import.meta.url);
@@ -13,23 +14,15 @@ const __dirname = path.dirname(__filename);
13
14
 
14
15
  const { platform, arch } = process;
15
16
 
16
- // Important: Never delegate to another system's `dev` binary.
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 = () => {
17
+ function isWSL() {
21
18
  if (platform !== "linux") return false;
22
19
  try {
23
- const os = require("os");
24
- const rel = os.release().toLowerCase();
25
- if (rel.includes("microsoft")) return true;
26
- const fs = require("fs");
27
- const txt = fs.readFileSync("/proc/version", "utf8").toLowerCase();
20
+ const txt = readFileSync("/proc/version", "utf8").toLowerCase();
28
21
  return txt.includes("microsoft");
29
22
  } catch {
30
23
  return false;
31
24
  }
32
- };
25
+ }
33
26
 
34
27
  let targetTriple = null;
35
28
  switch (platform) {
@@ -77,12 +70,11 @@ if (!targetTriple) {
77
70
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
78
71
  }
79
72
 
80
- // Use 'dev-*' binary names
73
+ // Binary names for Hanzo dev
81
74
  let binaryPath = path.join(__dirname, "..", "bin", `dev-${targetTriple}`);
82
- let legacyBinaryPath = path.join(__dirname, "..", "bin", `dev-${targetTriple}`);
75
+ let legacyBinaryPath = path.join(__dirname, "..", "bin", `code-${targetTriple}`);
83
76
 
84
- // --- Bootstrap helper (runs if the binary is missing, e.g. Bun blocked postinstall) ---
85
- import { existsSync, chmodSync, statSync, openSync, readSync, closeSync, mkdirSync, copyFileSync, readFileSync, unlinkSync } from "fs";
77
+ import { existsSync, chmodSync, statSync, openSync, readSync, closeSync, mkdirSync, copyFileSync, readFileSync, unlinkSync, createWriteStream } from "fs";
86
78
 
87
79
  const validateBinary = (p) => {
88
80
  try {
@@ -130,23 +122,22 @@ const getCacheDir = (version) => {
130
122
  };
131
123
 
132
124
  const getCachedBinaryPath = (version) => {
133
- const isWin = nodePlatform() === "win32";
134
- const ext = isWin ? ".exe" : "";
135
125
  const cacheDir = getCacheDir(version);
136
- return path.join(cacheDir, `dev-${targetTriple}${ext}`);
126
+ return path.join(cacheDir, `dev-${targetTriple}`);
137
127
  };
138
128
 
129
+ let lastBootstrapError = null;
130
+
139
131
  const httpsDownload = (url, dest) => new Promise((resolve, reject) => {
140
132
  const req = httpsGet(url, (res) => {
141
133
  const status = res.statusCode || 0;
142
134
  if (status >= 300 && status < 400 && res.headers.location) {
143
- // follow one redirect recursively
144
135
  return resolve(httpsDownload(res.headers.location, dest));
145
136
  }
146
137
  if (status !== 200) {
147
138
  return reject(new Error(`HTTP ${status}`));
148
139
  }
149
- const out = require("fs").createWriteStream(dest);
140
+ const out = createWriteStream(dest);
150
141
  res.pipe(out);
151
142
  out.on("finish", () => out.close(resolve));
152
143
  out.on("error", (e) => {
@@ -165,29 +156,30 @@ const httpsDownload = (url, dest) => new Promise((resolve, reject) => {
165
156
 
166
157
  const tryBootstrapBinary = async () => {
167
158
  try {
168
- // 1) Read our published version
169
159
  const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
170
160
  const version = pkg.version;
171
161
 
172
162
  const binDir = path.join(__dirname, "..", "bin");
173
163
  if (!existsSync(binDir)) mkdirSync(binDir, { recursive: true });
174
164
 
175
- // 2) Fast path: user cache
165
+ // Fast path: user cache
176
166
  const cachePath = getCachedBinaryPath(version);
177
167
  if (existsSync(cachePath)) {
178
168
  const v = validateBinary(cachePath);
179
169
  if (v.ok) {
180
- copyFileSync(cachePath, binaryPath);
181
- if (platform !== "win32") chmodSync(binaryPath, 0o755);
182
- return existsSync(binaryPath);
170
+ if (platform !== "win32") {
171
+ copyFileSync(cachePath, binaryPath);
172
+ try { chmodSync(binaryPath, 0o755); } catch {}
173
+ }
174
+ return true;
183
175
  }
184
176
  }
185
177
 
186
- // 3) Try platform package (if present)
178
+ // Try platform package (if present)
187
179
  try {
188
180
  const req = (await import("module")).createRequire(import.meta.url);
189
181
  const name = (() => {
190
- if (platform === "win32") return "@just-every/code-win32-x64"; // may be unpublished; falls through
182
+ if (platform === "win32") return "@hanzo/dev-win32-x64";
191
183
  const plt = nodePlatform();
192
184
  const cpu = nodeArch();
193
185
  if (plt === "darwin" && cpu === "arm64") return "@hanzo/dev-darwin-arm64";
@@ -200,31 +192,41 @@ const tryBootstrapBinary = async () => {
200
192
  try {
201
193
  const pkgJson = req.resolve(`${name}/package.json`);
202
194
  const pkgDir = path.dirname(pkgJson);
203
- const src = path.join(pkgDir, "bin", `dev-${targetTriple}${platform === "win32" ? ".exe" : ""}`);
195
+ const src = path.join(pkgDir, "bin", `dev-${targetTriple}`);
204
196
  if (existsSync(src)) {
205
- copyFileSync(src, binaryPath);
206
- if (platform !== "win32") chmodSync(binaryPath, 0o755);
207
- // refresh cache
208
- try { copyFileSync(binaryPath, cachePath); } catch {}
209
- return existsSync(binaryPath);
197
+ copyFileSync(src, cachePath);
198
+ if (platform !== "win32") {
199
+ copyFileSync(cachePath, binaryPath);
200
+ try { chmodSync(binaryPath, 0o755); } catch {}
201
+ }
202
+ return true;
210
203
  }
211
204
  } catch { /* ignore and fall back */ }
212
205
  }
213
206
  } catch { /* ignore */ }
214
207
 
215
- // 4) Download from GitHub release
208
+ // Download from GitHub release
216
209
  const isWin = platform === "win32";
210
+ // Use 'code-*' binary names since that's what the release produces
211
+ const binaryName = `code-${targetTriple}`;
217
212
  const archiveName = isWin
218
- ? `dev-${targetTriple}.zip`
219
- : (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `dev-${targetTriple}.zst`; } catch { return `dev-${targetTriple}.tar.gz`; } })();
213
+ ? `${binaryName}.zip`
214
+ : (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `${binaryName}.zst`; } catch { return `${binaryName}.tar.gz`; } })();
220
215
  const url = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
221
216
  const tmp = path.join(binDir, `.${archiveName}.part`);
222
217
  return httpsDownload(url, tmp)
223
218
  .then(() => {
224
219
  if (isWin) {
225
220
  try {
226
- const ps = `powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmp}' -DestinationPath '${binDir}' -Force"`;
227
- execSync(ps, { stdio: "ignore" });
221
+ const sysRoot = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
222
+ const psFull = path.join(sysRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
223
+ const unzipDest = getCacheDir(version);
224
+ const psCmd = `Expand-Archive -Path '${tmp}' -DestinationPath '${unzipDest}' -Force`;
225
+ let ok = false;
226
+ try { execSync(`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {}
227
+ if (!ok) { try { execSync(`powershell -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
228
+ if (!ok) { try { execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, { stdio: 'ignore' }); ok = true; } catch {} }
229
+ if (!ok) { execSync(`tar -xf "${tmp}" -C "${unzipDest}"`, { stdio: 'ignore', shell: true }); }
228
230
  } catch (e) {
229
231
  throw new Error(`failed to unzip: ${e.message}`);
230
232
  } finally { try { unlinkSync(tmp); } catch {} }
@@ -239,48 +241,75 @@ const tryBootstrapBinary = async () => {
239
241
  try { unlinkSync(tmp); } catch {}
240
242
  }
241
243
  }
242
- const v = validateBinary(binaryPath);
244
+ if (platform !== "win32") {
245
+ try { copyFileSync(binaryPath, cachePath); } catch {}
246
+ }
247
+
248
+ const v = validateBinary(platform === "win32" ? cachePath : binaryPath);
243
249
  if (!v.ok) throw new Error(`invalid binary (${v.reason})`);
244
- if (platform !== "win32") chmodSync(binaryPath, 0o755);
245
- try { copyFileSync(binaryPath, cachePath); } catch {}
250
+ if (platform !== "win32") try { chmodSync(binaryPath, 0o755); } catch {}
246
251
  return true;
247
252
  })
248
- .catch((_e) => false);
253
+ .catch((e) => { lastBootstrapError = e; return false; });
249
254
  } catch {
250
255
  return false;
251
256
  }
252
257
  };
253
258
 
254
- // If missing, attempt to bootstrap into place (helps when Bun blocks postinstall)
255
- if (!existsSync(binaryPath) && !existsSync(legacyBinaryPath)) {
256
- const ok = await tryBootstrapBinary();
257
- if (!ok) {
258
- // retry legacy name in case archive provided coder-*
259
- if (existsSync(legacyBinaryPath) && !existsSync(binaryPath)) {
260
- binaryPath = legacyBinaryPath;
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
+ }
261
279
  }
262
280
  }
263
281
  }
264
282
 
265
- // Fall back to legacy name if primary is still missing
266
- if (!existsSync(binaryPath) && existsSync(legacyBinaryPath)) {
267
- binaryPath = legacyBinaryPath;
283
+ // Prefer cached binary when available
284
+ try {
285
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
286
+ const version = pkg.version;
287
+ const cached = getCachedBinaryPath(version);
288
+ const v = existsSync(cached) ? validateBinary(cached) : { ok: false };
289
+ if (v.ok) {
290
+ binaryPath = cached;
291
+ } else if (!existsSync(binaryPath) && existsSync(legacyBinaryPath)) {
292
+ binaryPath = legacyBinaryPath;
293
+ }
294
+ } catch {
295
+ // ignore
268
296
  }
269
297
 
270
- // Check if binary exists and try to fix permissions if needed
271
- // fs imports are above; keep for readability if tree-shaken by bundlers
272
298
  import { spawnSync } from "child_process";
273
299
  if (existsSync(binaryPath)) {
274
300
  try {
275
- // Ensure binary is executable on Unix-like systems
276
301
  if (platform !== "win32") {
277
302
  chmodSync(binaryPath, 0o755);
278
303
  }
279
304
  } catch (e) {
280
- // Ignore permission errors, will be caught below if it's a real problem
305
+ // Ignore permission errors
281
306
  }
282
307
  } else {
283
308
  console.error(`Binary not found: ${binaryPath}`);
309
+ if (lastBootstrapError) {
310
+ const msg = (lastBootstrapError && (lastBootstrapError.message || String(lastBootstrapError))) || 'unknown bootstrap error';
311
+ console.error(`Bootstrap error: ${msg}`);
312
+ }
284
313
  console.error(`Please try reinstalling the package:`);
285
314
  console.error(` npm uninstall -g @hanzo/dev`);
286
315
  console.error(` npm install -g @hanzo/dev`);
@@ -292,9 +321,6 @@ if (existsSync(binaryPath)) {
292
321
  process.exit(1);
293
322
  }
294
323
 
295
- // Lightweight header validation to provide clearer errors before spawn
296
- // Reuse the validateBinary helper defined above in the bootstrap section.
297
-
298
324
  const validation = validateBinary(binaryPath);
299
325
  if (!validation.ok) {
300
326
  console.error(`The native binary at ${binaryPath} appears invalid: ${validation.reason}`);
@@ -313,47 +339,25 @@ if (!validation.ok) {
313
339
  process.exit(1);
314
340
  }
315
341
 
316
- // If running under npx/npm, emit a concise notice about which binary path is used
342
+ // If running under npx/npm, emit a concise notice
317
343
  try {
318
344
  const ua = process.env.npm_config_user_agent || "";
319
345
  const isNpx = ua.includes("npx");
320
346
  if (isNpx && process.stderr && process.stderr.isTTY) {
321
- // Best-effort discovery of another 'code' on PATH for user clarity
322
- let otherCode = "";
323
- try {
324
- const cmd = process.platform === "win32" ? "where code" : "command -v code || which code || true";
325
- const out = spawnSync(process.platform === "win32" ? "cmd" : "bash", [
326
- process.platform === "win32" ? "/c" : "-lc",
327
- cmd,
328
- ], { encoding: "utf8" });
329
- const line = (out.stdout || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
330
- if (line && !line.includes("@hanzo/dev")) {
331
- otherCode = line;
332
- }
333
- } catch {}
334
- if (otherCode) {
335
- console.error(`@hanzo/dev: running bundled binary -> ${binaryPath}`);
336
- console.error(`Note: a different 'dev' exists at ${otherCode}; not delegating.`);
337
- } else {
338
- console.error(`@hanzo/dev: running bundled binary -> ${binaryPath}`);
339
- }
347
+ console.error(`@hanzo/dev: running bundled binary -> ${binaryPath}`);
340
348
  }
341
349
  } catch {}
342
350
 
343
- // Use an asynchronous spawn instead of spawnSync so that Node is able to
344
- // respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
345
- // executing. This allows us to forward those signals to the child process
346
- // and guarantees that when either the child terminates or the parent
347
- // receives a fatal signal, both processes exit in a predictable manner.
348
351
  const { spawn } = await import("child_process");
349
352
 
353
+ process.env.DEV_BINARY_PATH = binaryPath;
354
+
350
355
  const child = spawn(binaryPath, process.argv.slice(2), {
351
356
  stdio: "inherit",
352
- env: { ...process.env, CODER_MANAGED_BY_NPM: "1", CODEX_MANAGED_BY_NPM: "1" },
357
+ env: { ...process.env, DEV_MANAGED_BY_NPM: "1", DEV_BINARY_PATH: binaryPath },
353
358
  });
354
359
 
355
360
  child.on("error", (err) => {
356
- // Typically triggered when the binary is missing or not executable.
357
361
  const code = err && err.code;
358
362
  if (code === 'EACCES') {
359
363
  console.error(`Permission denied: ${binaryPath}`);
@@ -377,10 +381,6 @@ child.on("error", (err) => {
377
381
  process.exit(1);
378
382
  });
379
383
 
380
- // Forward common termination signals to the child so that it shuts down
381
- // gracefully. In the handler we temporarily disable the default behavior of
382
- // exiting immediately; once the child has been signaled we simply wait for
383
- // its exit event which will in turn terminate the parent (see below).
384
384
  const forwardSignal = (signal) => {
385
385
  if (child.killed) {
386
386
  return;
@@ -396,11 +396,6 @@ const forwardSignal = (signal) => {
396
396
  process.on(sig, () => forwardSignal(sig));
397
397
  });
398
398
 
399
- // When the child exits, mirror its termination reason in the parent so that
400
- // shell scripts and other tooling observe the correct exit status.
401
- // Wrap the lifetime of the child process in a Promise so that we can await
402
- // its termination in a structured way. The Promise resolves with an object
403
- // describing how the child exited: either via exit code or due to a signal.
404
399
  const childResult = await new Promise((resolve) => {
405
400
  child.on("exit", (code, signal) => {
406
401
  if (signal) {
@@ -412,8 +407,6 @@ const childResult = await new Promise((resolve) => {
412
407
  });
413
408
 
414
409
  if (childResult.type === "signal") {
415
- // Re-emit the same signal so that the parent terminates with the expected
416
- // semantics (this also sets the correct exit code of 128 + n).
417
410
  process.kill(process.pid, childResult.signal);
418
411
  } else {
419
412
  process.exit(childResult.exitCode);
package/package.json CHANGED
@@ -1,44 +1,32 @@
1
1
  {
2
2
  "name": "@hanzo/dev",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "license": "Apache-2.0",
5
- "description": "Lightweight coding agent that runs in your terminal - Hanzo AI developer tools",
5
+ "description": "Hanzo AI coding assistant - intelligent CLI for developers",
6
6
  "bin": {
7
- "dev": "bin/dev.js"
7
+ "dev": "bin/dev.js",
8
+ "hanzo": "bin/dev.js"
8
9
  },
9
10
  "type": "module",
10
11
  "engines": {
11
- "node": ">=20"
12
+ "node": ">=16"
12
13
  },
13
14
  "files": [
14
15
  "bin/dev.js",
15
16
  "postinstall.js",
17
+ "scripts/preinstall.js",
18
+ "scripts/windows-cleanup.ps1",
16
19
  "dist"
17
20
  ],
18
21
  "scripts": {
19
- "postinstall": "node postinstall.js"
22
+ "preinstall": "node scripts/preinstall.js",
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...'); }\""
20
25
  },
21
26
  "repository": {
22
27
  "type": "git",
23
28
  "url": "git+https://github.com/hanzoai/dev.git"
24
29
  },
25
- "keywords": [
26
- "ai",
27
- "cli",
28
- "dev",
29
- "developer-tools",
30
- "coding-assistant",
31
- "hanzo",
32
- "llm",
33
- "chatgpt",
34
- "claude",
35
- "code-generation"
36
- ],
37
- "author": "Hanzo AI",
38
- "homepage": "https://github.com/hanzoai/dev#readme",
39
- "bugs": {
40
- "url": "https://github.com/hanzoai/dev/issues"
41
- },
42
30
  "publishConfig": {
43
31
  "access": "public"
44
32
  },
@@ -47,10 +35,10 @@
47
35
  "prettier": "^3.3.3"
48
36
  },
49
37
  "optionalDependencies": {
50
- "@hanzo/dev-darwin-arm64": "3.0.1",
51
- "@hanzo/dev-darwin-x64": "3.0.1",
52
- "@hanzo/dev-linux-x64": "3.0.1",
53
- "@hanzo/dev-linux-arm64": "3.0.1",
54
- "@hanzo/dev-win32-x64": "3.0.1"
38
+ "@hanzo/dev-darwin-arm64": "3.0.2",
39
+ "@hanzo/dev-darwin-x64": "3.0.2",
40
+ "@hanzo/dev-linux-x64-musl": "3.0.2",
41
+ "@hanzo/dev-linux-arm64-musl": "3.0.2",
42
+ "@hanzo/dev-win32-x64": "3.0.2"
55
43
  }
56
44
  }