diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 7a62c6278..5461a336b 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -22,6 +22,14 @@ jobs: steps: - uses: actions/checkout@v4 + # The unified `hanzo` front door (hanzo-js/hanzo) ships beside @hanzo/cli: + # it depends on it, so the two go out together. A plain `git clone` on the + # runner prompts for credentials even for a public repo; checkout does not. + - uses: actions/checkout@v4 + with: + repository: hanzo-js/hanzo + path: .hanzo-pkg + - uses: actions/setup-node@v4 with: node-version: '22' @@ -41,8 +49,7 @@ jobs: echo "publishing @hanzo/cli@$NEW (current on npm: $OLD)" echo "cli=true" >> "$GITHUB_OUTPUT" fi - git clone --depth 1 https://github.com/hanzo-js/hanzo.git "$RUNNER_TEMP/hanzo-pkg" - HNEW=$(node -p 'require(process.argv[1]).version' "$RUNNER_TEMP/hanzo-pkg/package.json") + HNEW=$(node -p 'require(process.argv[1]).version' "$GITHUB_WORKSPACE/.hanzo-pkg/package.json") HOLD=$(npm view hanzo version --registry https://registry.npmjs.org 2>/dev/null || echo 0.0.0) if curl -fsSL "https://registry.npmjs.org/hanzo/$HNEW" >/dev/null 2>&1; then echo "npm already serves hanzo@$HNEW" @@ -77,7 +84,7 @@ jobs: - name: Publish hanzo if: steps.ahead.outputs.hanzo == 'true' - working-directory: ${{ runner.temp }}/hanzo-pkg + working-directory: .hanzo-pkg env: NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/.npmrc run: npm publish --access public --ignore-scripts --no-provenance --registry https://registry.npmjs.org diff --git a/npm/bin/hanzo.js b/npm/bin/hanzo.js index 074ab025b..8a2b7828f 100755 --- a/npm/bin/hanzo.js +++ b/npm/bin/hanzo.js @@ -6,7 +6,7 @@ // `hanzo-vm`, `zt`, `hanzod`) are always discoverable, and lazily fetches any // missing companion binary on first invocation via `../install.js`. const { spawnSync } = require("node:child_process"); -const { existsSync } = require("node:fs"); +const { existsSync, realpathSync } = require("node:fs"); const { basename, delimiter, join } = require("node:path"); const ext = process.platform === "win32" ? ".exe" : ""; @@ -29,10 +29,28 @@ const TOOL_BINS = { node: "hanzod", }; +// A tool counts as present when its native binary sits beside this launcher, +// or anywhere on PATH as a REAL binary. npm's own global shims (`hanzo-mcp`, +// `hanzo-dev`, … all symlinks to this very file) are on PATH too, and must not +// count: treating one as the tool skips the download and then execs nothing. +function realBinary(p) { + try { + return !realpathSync(p).endsWith(".js"); + } catch { + return false; + } +} + +function hasLocal(tool) { + return existsSync(join(__dirname, (TOOL_BINS[tool] || tool) + ext)); +} + function hasTool(tool) { + if (hasLocal(tool)) return true; const binName = (TOOL_BINS[tool] || tool) + ext; for (const dir of (env[pathKey] || "").split(delimiter)) { - if (dir && existsSync(join(dir, binName))) return true; + const p = dir && join(dir, binName); + if (p && existsSync(p) && realBinary(p)) return true; } return false; } @@ -92,7 +110,9 @@ if (rawName === "hanzo-dev" || rawName === "dev") { } } -const missing = required.filter((t) => !hasTool(t)); +// The binary this launcher execs must be HERE (it is run by absolute path); +// companions only need to be findable on PATH, which starts here. +const missing = required.filter((t) => (t === targetTool ? !hasLocal(t) : !hasTool(t))); if (missing.length > 0 && existsSync(installScript) && env.HANZO_NO_LAZY_INSTALL !== "1") { spawnSync(process.execPath, [installScript, ...missing], { stdio: "inherit", diff --git a/npm/install.js b/npm/install.js index 40a5a91af..1348b5aa5 100644 --- a/npm/install.js +++ b/npm/install.js @@ -193,6 +193,7 @@ async function fetchVm({ key, dest, ext }) { async function installOne(name, { key, target, dest, ext, optional }) { const tool = TOOLS[name]; if (!tool) return; + let fellBack = false; try { if (tool.kind === "engine") { await fetchEngine({ key, dest, ext }); @@ -213,6 +214,7 @@ async function installOne(name, { key, target, dest, ext, optional }) { }); } catch (err) { if (tool.versioned && String(err.message || err).startsWith("404 ")) { + fellBack = true; await fetchStandardTool({ base: `https://github.com/${tool.repo}/releases/latest/download`, bin: tool.bin, @@ -226,7 +228,7 @@ async function installOne(name, { key, target, dest, ext, optional }) { } } } - const label = tool.versioned ? `${tool.bin} ${VERSION}` : tool.bin; + const label = tool.versioned && !fellBack ? `${tool.bin} ${VERSION}` : `${tool.bin} (latest release)`; console.error(`@hanzo/cli: installed ${label} (${target})`); } catch (err) { if (!optional || String(err.message || err).includes("checksum mismatch")) throw err;