mirror of
https://github.com/0xMassi/webclaw.git
synced 2026-06-28 03:29:38 +02:00
fix(create-webclaw): repair binary install on Windows (and all platforms)
`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-<target>`,
but release assets are `webclaw-<tag>-<target>` (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-<tag>-<target>/` 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) <noreply@anthropic.com>
This commit is contained in:
parent
1137419a09
commit
9af55c2a2d
2 changed files with 112 additions and 65 deletions
|
|
@ -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-<tag>-<target>.<ext>` (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-<version>-<target>/` 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue