From 9af55c2a2dc68291f7d5a11bf8e13e966ab28a68 Mon Sep 17 00:00:00 2001 From: Valerio Date: Sat, 27 Jun 2026 11:58:14 +0200 Subject: [PATCH] fix(create-webclaw): repair binary install on Windows (and all platforms) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npx create-webclaw` never used the prebuilt binary on any platform and silently fell back to `cargo install`, which fails with "'cargo' is not recognized" / "cargo: not found" unless Rust is installed. Four bugs: 1. Asset name mismatch: getAssetName() hardcoded `webclaw-mcp-`, but release assets are `webclaw--` (versioned, no `mcp-` infix). The `find()` always returned undefined, so the prebuilt path was never taken — on every OS, not just Windows. Now the asset name is built from the release tag_name + a platform→target map. 2. `unzip` is absent on Windows. The `.zip` branch now uses PowerShell `Expand-Archive` (ships with Windows 10/11) and keeps `unzip` only for the non-Windows case. 3. The prebuilt failure was swallowed by a bare `catch {}`, hiding the real cause (a 403 is almost always a GitHub API rate limit). The error is now surfaced, with a rate-limit hint + GITHUB_TOKEN support on the api.github.com request (token dropped on CDN redirects). 4. (missed by the report's own suggested fix) Archives extract into a `webclaw--/` subdirectory holding three binaries, so the old `chmod(BINARY_PATH)` hit a nonexistent path. webclaw-mcp is now lifted out of that subdir to BINARY_PATH and the rest is cleaned up. BINARY_NAME/BINARY_PATH also gain the `.exe` suffix on Windows so the written MCP config points at a real file. Tested in Docker (no Windows machine available): - Linux amd64 + arm64 on Debian trixie: full flow installs the binary and it answers a real MCP initialize handshake (serverInfo webclaw-mcp 0.6.13, 12 tools). - Windows .zip path validated against the real release zip: Expand-Archive equivalent extraction, nested `.exe` resolved + lifted, PE header `MZ`. Executing the .exe needs Windows (the reporter confirmed that on Win11). - Bug 3: with the GitHub API blocked, the new build prints the real reason instead of "No pre-built binary found". Closes #71 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/create-webclaw/index.mjs | 175 +++++++++++++++++---------- packages/create-webclaw/package.json | 2 +- 2 files changed, 112 insertions(+), 65 deletions(-) diff --git a/packages/create-webclaw/index.mjs b/packages/create-webclaw/index.mjs index 8a9cb1c..2bb04a4 100644 --- a/packages/create-webclaw/index.mjs +++ b/packages/create-webclaw/index.mjs @@ -1,6 +1,13 @@ #!/usr/bin/env node -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { + existsSync, + mkdirSync, + readFileSync, + writeFileSync, + copyFileSync, + rmSync, +} from "fs"; import { createInterface } from "readline"; import { homedir, platform, arch } from "os"; import { join, dirname } from "path"; @@ -13,10 +20,10 @@ import http from "http"; // ── Constants ── const REPO = "0xMassi/webclaw"; -const BINARY_NAME = "webclaw-mcp"; +const IS_WINDOWS = platform() === "win32"; +const BINARY_NAME = IS_WINDOWS ? "webclaw-mcp.exe" : "webclaw-mcp"; const INSTALL_DIR = join(homedir(), ".webclaw"); const BINARY_PATH = join(INSTALL_DIR, BINARY_NAME); -const VERSION = "latest"; const COLORS = { reset: "\x1b[0m", @@ -166,12 +173,14 @@ function ask(question) { }); } -function download(url) { +function download(url, extraHeaders = {}) { return new Promise((resolve, reject) => { const client = url.startsWith("https") ? https : http; + const headers = { "User-Agent": "create-webclaw", ...extraHeaders }; client - .get(url, { headers: { "User-Agent": "create-webclaw" } }, (res) => { - // Follow redirects + .get(url, { headers }, (res) => { + // Follow redirects, dropping extra headers so an Authorization token + // never leaks to the release CDN (its signed URLs reject it anyway). if ( res.statusCode >= 300 && res.statusCode < 400 && @@ -220,22 +229,19 @@ async function downloadFile(url, dest) { }); } -function getAssetName() { - const os = platform(); - const a = arch(); - - if (os === "darwin" && a === "arm64") - return `webclaw-mcp-aarch64-apple-darwin.tar.gz`; - if (os === "darwin" && a === "x64") - return `webclaw-mcp-x86_64-apple-darwin.tar.gz`; - if (os === "linux" && a === "x64") - return `webclaw-mcp-x86_64-unknown-linux-gnu.tar.gz`; - if (os === "linux" && a === "arm64") - return `webclaw-mcp-aarch64-unknown-linux-gnu.tar.gz`; - if (os === "win32" && a === "x64") - return `webclaw-mcp-x86_64-pc-windows-msvc.zip`; - - return null; +// Map the current platform to its Rust release target triple. Release assets +// are named `webclaw--.` (e.g. +// webclaw-v0.6.13-x86_64-unknown-linux-gnu.tar.gz), so the asset name is built +// from the release's tag_name at fetch time — it can't be hardcoded here. +function getTarget() { + const targets = { + "darwin-arm64": "aarch64-apple-darwin", + "darwin-x64": "x86_64-apple-darwin", + "linux-x64": "x86_64-unknown-linux-gnu", + "linux-arm64": "aarch64-unknown-linux-gnu", + "win32-x64": "x86_64-pc-windows-msvc", + }; + return targets[`${platform()}-${arch()}`] || null; } function readJsonFile(path) { @@ -439,8 +445,8 @@ async function main() { // 3. Download binary console.log(c("bold", " Downloading webclaw-mcp...")); - const assetName = getAssetName(); - if (!assetName) { + const target = getTarget(); + if (!target) { console.log(c("red", ` Unsupported platform: ${platform()}-${arch()}`)); console.log( c( @@ -456,62 +462,103 @@ async function main() { } let downloaded = false; + let prebuiltError = null; try { - // Get latest release URL - const releaseData = await download( - `https://api.github.com/repos/${REPO}/releases/latest`, + // Resolve the latest release. Its tag_name drives the asset name. An + // unauthenticated GitHub API call is rate-limited to 60/hour per IP, so + // honour GITHUB_TOKEN when set — but only on this api.github.com request, + // never on the asset download (which redirects to a CDN). + const apiHeaders = process.env.GITHUB_TOKEN + ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } + : {}; + const release = JSON.parse( + ( + await download( + `https://api.github.com/repos/${REPO}/releases/latest`, + apiHeaders, + ) + ).toString(), ); - const release = JSON.parse(releaseData.toString()); + + const version = release.tag_name; // e.g. "v0.6.13" + const ext = IS_WINDOWS ? "zip" : "tar.gz"; + const assetName = `webclaw-${version}-${target}.${ext}`; const asset = release.assets?.find((a) => a.name === assetName); - - if (asset) { - const tarPath = join(INSTALL_DIR, assetName); - await downloadFile(asset.browser_download_url, tarPath); - - // Extract - if (assetName.endsWith(".tar.gz")) { - execSync(`tar xzf "${tarPath}" -C "${INSTALL_DIR}"`, { - stdio: "ignore", - }); - } else if (assetName.endsWith(".zip")) { - execSync(`unzip -o "${tarPath}" -d "${INSTALL_DIR}"`, { - stdio: "ignore", - }); - } - - // Make executable - await chmod(BINARY_PATH, 0o755); - - // Cleanup archive - try { - execSync(`rm "${tarPath}"`, { stdio: "ignore" }); - } catch {} - - console.log(c("green", ` ✓ Installed to ${BINARY_PATH}`)); - downloaded = true; + if (!asset) { + throw new Error(`asset ${assetName} not found in release ${version}`); } + + const archivePath = join(INSTALL_DIR, assetName); + await downloadFile(asset.browser_download_url, archivePath); + + // Each archive holds a top-level `webclaw--/` directory + // containing webclaw, webclaw-mcp, webclaw-server, and docs. + if (ext === "tar.gz") { + execSync(`tar xzf "${archivePath}" -C "${INSTALL_DIR}"`, { + stdio: "ignore", + }); + } else if (IS_WINDOWS) { + // Windows ships no `unzip`; Expand-Archive comes with PowerShell 5+. + execSync( + `powershell -NoProfile -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${INSTALL_DIR}' -Force"`, + { stdio: "ignore" }, + ); + } else { + execSync(`unzip -o "${archivePath}" -d "${INSTALL_DIR}"`, { + stdio: "ignore", + }); + } + + // Lift webclaw-mcp out of the extracted directory to BINARY_PATH, then + // drop the rest (the other two binaries + docs). + const extractedDir = join(INSTALL_DIR, `webclaw-${version}-${target}`); + const extractedBin = join(extractedDir, BINARY_NAME); + if (!existsSync(extractedBin)) { + throw new Error(`binary missing after extract: ${extractedBin}`); + } + copyFileSync(extractedBin, BINARY_PATH); + if (!IS_WINDOWS) await chmod(BINARY_PATH, 0o755); + + try { + rmSync(extractedDir, { recursive: true, force: true }); + rmSync(archivePath, { force: true }); + } catch {} + + console.log(c("green", ` ✓ Installed to ${BINARY_PATH}`)); + downloaded = true; } catch (e) { - // Release not available yet — expected before first release + prebuiltError = e; } if (!downloaded) { - // Try cargo install as fallback - console.log( - c("yellow", " No pre-built binary found. Trying cargo install..."), - ); + // Surface why the prebuilt path failed instead of hiding it — a 403 here + // is almost always a GitHub API rate limit, which Rust can't fix. + if (prebuiltError) { + const m = prebuiltError.message || String(prebuiltError); + if (m.includes("403") || /rate limit/i.test(m)) { + console.log( + c( + "yellow", + " GitHub API rate limit hit. Retry in a few minutes, or set GITHUB_TOKEN.", + ), + ); + } else { + console.log(c("yellow", ` Prebuilt binary unavailable (${m}).`)); + } + } + + // Fall back to building from source. + console.log(c("yellow", " Trying cargo install...")); try { execSync( `cargo install --git https://github.com/${REPO} webclaw-mcp --root "${INSTALL_DIR}"`, { stdio: "inherit" }, ); - // cargo install puts binary in INSTALL_DIR/bin/ + // cargo install puts the binary in INSTALL_DIR/bin/ const cargoPath = join(INSTALL_DIR, "bin", BINARY_NAME); if (existsSync(cargoPath)) { - // Move to expected location - execSync(`mv "${cargoPath}" "${BINARY_PATH}"`, { - stdio: "ignore", - }); + copyFileSync(cargoPath, BINARY_PATH); console.log(c("green", ` ✓ Built and installed to ${BINARY_PATH}`)); downloaded = true; } diff --git a/packages/create-webclaw/package.json b/packages/create-webclaw/package.json index ede4cb3..d410447 100644 --- a/packages/create-webclaw/package.json +++ b/packages/create-webclaw/package.json @@ -1,6 +1,6 @@ { "name": "create-webclaw", - "version": "0.1.4", + "version": "0.1.5", "mcpName": "io.github.0xMassi/webclaw", "description": "Set up webclaw MCP server for AI agents (Claude, Cursor, Windsurf, OpenCode, Codex, Antigravity)", "bin": {