Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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"
Expand Down Expand Up @@ -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
26 changes: 23 additions & 3 deletions npm/bin/hanzo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" : "";
Expand All @@ -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;
}
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion npm/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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,
Expand All @@ -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;
Expand Down
Loading