mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
- hanzo-tunnel v0.1.0 published to crates.io (was path dep to ../../../tunnel) - cli: tunnel + tunnel-bridge deps now optional behind `tunnel` feature flag - cli: cloud module (ZT + relay) gated with #[cfg(feature = "tunnel")] - tunnel-bridge: all hanzo_tunnel usage gated behind `tunnel` feature - CI: fix prettier by scoping JS globs, excluding symlinked CLAUDE.md - CI: format all workflows and docs with prettier - Workspace compiles clean without tunnel feature (CI default) - Local builds with `--features tunnel` still work with crates.io dep
1071 lines
33 KiB
JavaScript
1071 lines
33 KiB
JavaScript
#!/usr/bin/env node
|
|
// Non-functional change to trigger release workflow
|
|
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
createWriteStream,
|
|
chmodSync,
|
|
readFileSync,
|
|
readSync,
|
|
writeFileSync,
|
|
unlinkSync,
|
|
statSync,
|
|
openSync,
|
|
closeSync,
|
|
copyFileSync,
|
|
fsyncSync,
|
|
renameSync,
|
|
realpathSync,
|
|
} from "fs";
|
|
import { join, dirname, resolve } from "path";
|
|
import { fileURLToPath } from "url";
|
|
import { get } from "https";
|
|
import { platform, arch, tmpdir } from "os";
|
|
import { execSync } from "child_process";
|
|
import { createRequire } from "module";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Map Node.js platform/arch to Rust target triples
|
|
function getTargetTriple() {
|
|
const platformMap = {
|
|
darwin: "apple-darwin",
|
|
linux: "unknown-linux-musl", // Default to musl for better compatibility
|
|
win32: "pc-windows-msvc",
|
|
};
|
|
|
|
const archMap = {
|
|
x64: "x86_64",
|
|
arm64: "aarch64",
|
|
};
|
|
|
|
const rustArch = archMap[arch()] || arch();
|
|
const rustPlatform = platformMap[platform()] || platform();
|
|
|
|
return `${rustArch}-${rustPlatform}`;
|
|
}
|
|
|
|
// Resolve a persistent user cache directory for binaries so that repeated
|
|
// npx installs can reuse a previously downloaded artifact and skip work.
|
|
function getCacheDir(version) {
|
|
const plt = platform();
|
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
let base = "";
|
|
if (plt === "win32") {
|
|
base = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
|
|
} else if (plt === "darwin") {
|
|
base = join(home, "Library", "Caches");
|
|
} else {
|
|
base = process.env.XDG_CACHE_HOME || join(home, ".cache");
|
|
}
|
|
const dir = join(base, "hanzo", "dev", version);
|
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
function getCachedBinaryPath(version, targetTriple, isWindows) {
|
|
const ext = isWindows ? ".exe" : "";
|
|
const cacheDir = getCacheDir(version);
|
|
return join(cacheDir, `code-${targetTriple}${ext}`);
|
|
}
|
|
|
|
const CODE_SHIM_SIGNATURES = [
|
|
"@hanzo/dev",
|
|
"bin/coder.js",
|
|
'$(dirname "$0")/coder',
|
|
"%~dp0coder",
|
|
];
|
|
|
|
function shimContentsLookOurs(contents) {
|
|
return CODE_SHIM_SIGNATURES.some((sig) => contents.includes(sig));
|
|
}
|
|
|
|
function looksLikeOurCodeShim(path) {
|
|
try {
|
|
const contents = readFileSync(path, "utf8");
|
|
return shimContentsLookOurs(contents);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isWSL() {
|
|
if (platform() !== "linux") return false;
|
|
try {
|
|
const ver = readFileSync("/proc/version", "utf8").toLowerCase();
|
|
return ver.includes("microsoft") || !!process.env.WSL_DISTRO_NAME;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isPathOnWindowsFs(p) {
|
|
try {
|
|
const mounts = readFileSync("/proc/mounts", "utf8")
|
|
.split(/\n/)
|
|
.filter(Boolean);
|
|
let best = { mount: "/", type: "unknown", len: 1 };
|
|
for (const line of mounts) {
|
|
const parts = line.split(" ");
|
|
if (parts.length < 3) continue;
|
|
const mnt = parts[1];
|
|
const typ = parts[2];
|
|
if (p.startsWith(mnt) && mnt.length > best.len)
|
|
best = { mount: mnt, type: typ, len: mnt.length };
|
|
}
|
|
return best.type === "drvfs" || best.type === "cifs";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function writeCacheAtomic(srcPath, cachePath) {
|
|
try {
|
|
if (existsSync(cachePath)) {
|
|
const ok = validateDownloadedBinary(cachePath).ok;
|
|
if (ok) return;
|
|
}
|
|
} catch {}
|
|
const dir = dirname(cachePath);
|
|
if (!existsSync(dir)) {
|
|
try {
|
|
mkdirSync(dir, { recursive: true });
|
|
} catch {}
|
|
}
|
|
const tmp = cachePath + ".tmp-" + Math.random().toString(36).slice(2, 8);
|
|
copyFileSync(srcPath, tmp);
|
|
try {
|
|
const fd = openSync(tmp, "r");
|
|
try {
|
|
fsyncSync(fd);
|
|
} finally {
|
|
closeSync(fd);
|
|
}
|
|
} catch {}
|
|
// Retry with exponential backoff up to ~1.6s total
|
|
const delays = [100, 200, 400, 800, 1200, 1600];
|
|
for (let i = 0; i < delays.length; i++) {
|
|
try {
|
|
if (existsSync(cachePath)) {
|
|
try {
|
|
unlinkSync(cachePath);
|
|
} catch {}
|
|
}
|
|
renameSync(tmp, cachePath);
|
|
return;
|
|
} catch {
|
|
await new Promise((r) => setTimeout(r, delays[i]));
|
|
}
|
|
}
|
|
if (existsSync(cachePath)) {
|
|
try {
|
|
unlinkSync(cachePath);
|
|
} catch {}
|
|
}
|
|
renameSync(tmp, cachePath);
|
|
}
|
|
|
|
function resolveGlobalBinDir() {
|
|
const plt = platform();
|
|
const userAgent = process.env.npm_config_user_agent || "";
|
|
|
|
const fromPrefix = (prefixPath) => {
|
|
if (!prefixPath) return "";
|
|
return plt === "win32" ? prefixPath : join(prefixPath, "bin");
|
|
};
|
|
|
|
const prefixEnv = process.env.npm_config_prefix || process.env.PREFIX || "";
|
|
const direct = fromPrefix(prefixEnv);
|
|
if (direct) return direct;
|
|
|
|
const tryExec = (command) => {
|
|
try {
|
|
return execSync(command, {
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
shell: true,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
};
|
|
|
|
const prefixFromNpm = fromPrefix(tryExec("npm prefix -g"));
|
|
if (prefixFromNpm) return prefixFromNpm;
|
|
|
|
const binFromNpm = tryExec("npm bin -g");
|
|
if (binFromNpm) return binFromNpm;
|
|
|
|
if (userAgent.includes("pnpm")) {
|
|
const pnpmBin = tryExec("pnpm bin --global");
|
|
if (pnpmBin) return pnpmBin;
|
|
const pnpmPrefix = fromPrefix(tryExec("pnpm env get prefix"));
|
|
if (pnpmPrefix) return pnpmPrefix;
|
|
}
|
|
|
|
if (userAgent.includes("yarn")) {
|
|
const yarnBin = tryExec("yarn global bin");
|
|
if (yarnBin) return yarnBin;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
const doAttempt = () =>
|
|
new Promise((resolve, reject) => {
|
|
const attempt = (currentUrl, redirectsLeft) => {
|
|
const req = get(currentUrl, (response) => {
|
|
const status = response.statusCode || 0;
|
|
const location = response.headers.location;
|
|
|
|
if (
|
|
(status === 301 ||
|
|
status === 302 ||
|
|
status === 303 ||
|
|
status === 307 ||
|
|
status === 308) &&
|
|
location
|
|
) {
|
|
if (redirectsLeft <= 0) {
|
|
reject(
|
|
new Error(`Too many redirects while downloading ${currentUrl}`),
|
|
);
|
|
return;
|
|
}
|
|
attempt(location, redirectsLeft - 1);
|
|
return;
|
|
}
|
|
|
|
if (status === 200) {
|
|
const expected =
|
|
parseInt(response.headers["content-length"] || "0", 10) || 0;
|
|
let bytes = 0;
|
|
let timer;
|
|
const timeoutMs = 30000; // 30s inactivity timeout
|
|
|
|
const resetTimer = () => {
|
|
if (timer) clearTimeout(timer);
|
|
timer = setTimeout(() => {
|
|
req.destroy(new Error("download stalled"));
|
|
}, timeoutMs);
|
|
};
|
|
|
|
resetTimer();
|
|
response.on("data", (chunk) => {
|
|
bytes += chunk.length;
|
|
resetTimer();
|
|
});
|
|
|
|
const file = createWriteStream(dest);
|
|
response.pipe(file);
|
|
file.on("finish", () => {
|
|
if (timer) clearTimeout(timer);
|
|
file.close();
|
|
if (expected && bytes !== expected) {
|
|
try {
|
|
unlinkSync(dest);
|
|
} catch {}
|
|
reject(
|
|
new Error(
|
|
`incomplete download: got ${bytes} of ${expected} bytes`,
|
|
),
|
|
);
|
|
} else if (bytes === 0) {
|
|
try {
|
|
unlinkSync(dest);
|
|
} catch {}
|
|
reject(new Error("empty download"));
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
file.on("error", (err) => {
|
|
if (timer) clearTimeout(timer);
|
|
try {
|
|
unlinkSync(dest);
|
|
} catch {}
|
|
reject(err);
|
|
});
|
|
} else {
|
|
reject(new Error(`Failed to download: HTTP ${status}`));
|
|
}
|
|
});
|
|
|
|
req.on("error", (err) => {
|
|
try {
|
|
unlinkSync(dest);
|
|
} catch {}
|
|
reject(err);
|
|
});
|
|
|
|
// Absolute request timeout to avoid hanging forever
|
|
req.setTimeout(120000, () => {
|
|
req.destroy(new Error("download timed out"));
|
|
});
|
|
};
|
|
|
|
attempt(url, maxRedirects);
|
|
});
|
|
|
|
let attemptNum = 0;
|
|
while (true) {
|
|
try {
|
|
return await doAttempt();
|
|
} catch (e) {
|
|
attemptNum += 1;
|
|
if (attemptNum > maxRetries) throw e;
|
|
const backoff = Math.min(2000, 200 * attemptNum);
|
|
await sleep(backoff);
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateDownloadedBinary(p) {
|
|
try {
|
|
const st = statSync(p);
|
|
if (!st.isFile() || st.size === 0) {
|
|
return { ok: false, reason: "empty or not a regular file" };
|
|
}
|
|
const fd = openSync(p, "r");
|
|
try {
|
|
const buf = Buffer.alloc(4);
|
|
const n = readSync(fd, buf, 0, 4, 0);
|
|
if (n < 2) return { ok: false, reason: "too short" };
|
|
const plt = platform();
|
|
if (plt === "win32") {
|
|
if (!(buf[0] === 0x4d && buf[1] === 0x5a))
|
|
return { ok: false, reason: "invalid PE header (missing MZ)" };
|
|
} else if (plt === "linux" || plt === "android") {
|
|
if (
|
|
!(
|
|
buf[0] === 0x7f &&
|
|
buf[1] === 0x45 &&
|
|
buf[2] === 0x4c &&
|
|
buf[3] === 0x46
|
|
)
|
|
)
|
|
return { ok: false, reason: "invalid ELF header" };
|
|
} else if (plt === "darwin") {
|
|
const isMachO =
|
|
(buf[0] === 0xcf &&
|
|
buf[1] === 0xfa &&
|
|
buf[2] === 0xed &&
|
|
buf[3] === 0xfe) ||
|
|
(buf[0] === 0xca &&
|
|
buf[1] === 0xfe &&
|
|
buf[2] === 0xba &&
|
|
buf[3] === 0xbe);
|
|
if (!isMachO) return { ok: false, reason: "invalid Mach-O header" };
|
|
}
|
|
return { ok: true };
|
|
} finally {
|
|
closeSync(fd);
|
|
}
|
|
} catch (e) {
|
|
return { ok: false, reason: e.message };
|
|
}
|
|
}
|
|
|
|
export async function runPostinstall(options = {}) {
|
|
const { skipGlobalAlias = false, invokedByRuntime = false } = options;
|
|
if (process.env.CODE_POSTINSTALL_DRY_RUN === "1") {
|
|
return { skipped: true };
|
|
}
|
|
|
|
if (invokedByRuntime) {
|
|
process.env.CODE_RUNTIME_POSTINSTALL =
|
|
process.env.CODE_RUNTIME_POSTINSTALL || "1";
|
|
}
|
|
// Detect potential PATH conflict with an existing `code` command (e.g., VS Code)
|
|
// Only relevant for global installs; skip for npx/local installs to keep postinstall fast.
|
|
const ua = process.env.npm_config_user_agent || "";
|
|
const isNpx = ua.includes("npx");
|
|
const isGlobal = process.env.npm_config_global === "true";
|
|
if (!skipGlobalAlias && isGlobal && !isNpx) {
|
|
try {
|
|
const whichCmd =
|
|
process.platform === "win32"
|
|
? "where code"
|
|
: "command -v code || which code || true";
|
|
const resolved = execSync(whichCmd, {
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
shell: process.platform !== "win32",
|
|
})
|
|
.toString()
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)[0];
|
|
if (resolved) {
|
|
let contents = "";
|
|
try {
|
|
contents = readFileSync(resolved, "utf8");
|
|
} catch {
|
|
contents = "";
|
|
}
|
|
const looksLikeOurs = shimContentsLookOurs(contents);
|
|
if (!looksLikeOurs) {
|
|
console.warn("[notice] Found an existing `code` on PATH at:");
|
|
console.warn(` ${resolved}`);
|
|
console.warn(
|
|
"[notice] We will still install our CLI, also available as `coder`.",
|
|
);
|
|
console.warn(
|
|
" If `code` runs another tool, prefer using: coder",
|
|
);
|
|
console.warn(
|
|
" Or run our CLI explicitly via: npx -y @hanzo/dev",
|
|
);
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore detection failures; proceed with install.
|
|
}
|
|
}
|
|
|
|
const targetTriple = getTargetTriple();
|
|
const isWindows = platform() === "win32";
|
|
const binaryExt = isWindows ? ".exe" : "";
|
|
|
|
const binDir = join(__dirname, "bin");
|
|
if (!existsSync(binDir)) {
|
|
mkdirSync(binDir, { recursive: true });
|
|
}
|
|
|
|
// Get package version - use readFileSync for compatibility
|
|
const packageJson = JSON.parse(
|
|
readFileSync(join(__dirname, "package.json"), "utf8"),
|
|
);
|
|
const version = packageJson.version;
|
|
|
|
// Download only the primary binary; we'll create wrappers for legacy names.
|
|
const binaries = ["code"];
|
|
|
|
console.log(`Installing @hanzo/dev v${version} for ${targetTriple}...`);
|
|
|
|
for (const binary of binaries) {
|
|
const binaryName = `${binary}-${targetTriple}${binaryExt}`;
|
|
const localPath = join(binDir, binaryName);
|
|
const cachePath = getCachedBinaryPath(version, targetTriple, isWindows);
|
|
|
|
// On Windows we avoid placing the executable inside node_modules to prevent
|
|
// EBUSY/EPERM during global upgrades when the binary is in use.
|
|
// We treat the user cache path as the canonical home of the native binary.
|
|
// For macOS/Linux we keep previous behavior and also place a copy in binDir
|
|
// for convenience.
|
|
|
|
// Fast path: if a valid cached binary exists for this version+triple, reuse it.
|
|
try {
|
|
if (existsSync(cachePath)) {
|
|
const valid = validateDownloadedBinary(cachePath);
|
|
if (valid.ok) {
|
|
// Avoid mirroring into node_modules on Windows or WSL-on-NTFS.
|
|
const wsl = isWSL();
|
|
const binDirReal = (() => {
|
|
try {
|
|
return realpathSync(binDir);
|
|
} catch {
|
|
return binDir;
|
|
}
|
|
})();
|
|
const mirrorToLocal = !(
|
|
isWindows ||
|
|
(wsl && isPathOnWindowsFs(binDirReal))
|
|
);
|
|
if (mirrorToLocal) {
|
|
copyFileSync(cachePath, localPath);
|
|
try {
|
|
chmodSync(localPath, 0o755);
|
|
} catch {}
|
|
}
|
|
console.log(`✓ ${binaryName} ready from user cache`);
|
|
continue; // next binary
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore cache errors and fall through to normal paths
|
|
}
|
|
|
|
// First try platform package via npm optionalDependencies (fast path on npm CDN).
|
|
const require = createRequire(import.meta.url);
|
|
const platformPkg = (() => {
|
|
const name = (() => {
|
|
if (isWindows) return "@hanzo/dev-win32-x64";
|
|
const plt = platform();
|
|
const cpu = arch();
|
|
if (plt === "darwin" && cpu === "arm64")
|
|
return "@hanzo/dev-darwin-arm64";
|
|
if (plt === "darwin" && cpu === "x64") return "@hanzo/dev-darwin-x64";
|
|
if (plt === "linux" && cpu === "x64")
|
|
return "@hanzo/dev-linux-x64-musl";
|
|
if (plt === "linux" && cpu === "arm64")
|
|
return "@hanzo/dev-linux-arm64-musl";
|
|
return null;
|
|
})();
|
|
if (!name) return null;
|
|
try {
|
|
const pkgJsonPath = require.resolve(`${name}/package.json`);
|
|
const pkgDir = dirname(pkgJsonPath);
|
|
return { name, dir: pkgDir };
|
|
} catch {
|
|
return null;
|
|
}
|
|
})();
|
|
|
|
if (platformPkg) {
|
|
try {
|
|
// Expect binary inside platform package bin directory
|
|
const src = join(platformPkg.dir, "bin", binaryName);
|
|
if (!existsSync(src)) {
|
|
throw new Error(
|
|
`platform package missing binary: ${platformPkg.name}`,
|
|
);
|
|
}
|
|
// Populate cache first (canonical location) atomically
|
|
await writeCacheAtomic(src, cachePath);
|
|
// Mirror into local bin only on Unix-like filesystems (not Windows/WSL-on-NTFS)
|
|
const wsl = isWSL();
|
|
const binDirReal = (() => {
|
|
try {
|
|
return realpathSync(binDir);
|
|
} catch {
|
|
return binDir;
|
|
}
|
|
})();
|
|
const mirrorToLocal = !(
|
|
isWindows ||
|
|
(wsl && isPathOnWindowsFs(binDirReal))
|
|
);
|
|
if (mirrorToLocal) {
|
|
copyFileSync(cachePath, localPath);
|
|
try {
|
|
chmodSync(localPath, 0o755);
|
|
} catch {}
|
|
}
|
|
console.log(
|
|
`✓ Installed ${binaryName} from ${platformPkg.name} (cached)`,
|
|
);
|
|
continue; // next binary
|
|
} catch (e) {
|
|
console.warn(
|
|
`⚠ Failed platform package install (${e.message}), falling back to GitHub download`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Decide archive format per OS with fallback on macOS/Linux:
|
|
// - Windows: .zip
|
|
// - macOS/Linux: prefer .zst if `zstd` CLI is available; otherwise use .tar.gz
|
|
const isWin = isWindows;
|
|
const detectedWSL = (() => {
|
|
if (platform() !== "linux") return false;
|
|
try {
|
|
const ver = readFileSync("/proc/version", "utf8").toLowerCase();
|
|
return ver.includes("microsoft") || !!process.env.WSL_DISTRO_NAME;
|
|
} catch {
|
|
return false;
|
|
}
|
|
})();
|
|
const binDirReal = (() => {
|
|
try {
|
|
return realpathSync(binDir);
|
|
} catch {
|
|
return binDir;
|
|
}
|
|
})();
|
|
const mirrorToLocal = !(
|
|
isWin ||
|
|
(detectedWSL && isPathOnWindowsFs(binDirReal))
|
|
);
|
|
let useZst = false;
|
|
if (!isWin) {
|
|
try {
|
|
execSync("zstd --version", { stdio: "ignore", shell: true });
|
|
useZst = true;
|
|
} catch {
|
|
useZst = false;
|
|
}
|
|
}
|
|
const archiveName = isWin
|
|
? `${binaryName}.zip`
|
|
: useZst
|
|
? `${binaryName}.zst`
|
|
: `${binaryName}.tar.gz`;
|
|
const downloadUrl = `https://github.com/hanzoai/dev/releases/download/v${version}/${archiveName}`;
|
|
|
|
console.log(`Downloading ${archiveName}...`);
|
|
try {
|
|
const needsIsolation = isWin || (!isWin && !mirrorToLocal); // Windows or WSL-on-NTFS
|
|
let safeTempDir = needsIsolation
|
|
? join(tmpdir(), "hanzo", "dev", version)
|
|
: binDir;
|
|
// Ensure staging dir exists; if tmp fails (permissions/space), fall back to user cache.
|
|
if (needsIsolation) {
|
|
try {
|
|
if (!existsSync(safeTempDir))
|
|
mkdirSync(safeTempDir, { recursive: true });
|
|
} catch {
|
|
try {
|
|
safeTempDir = getCacheDir(version);
|
|
if (!existsSync(safeTempDir))
|
|
mkdirSync(safeTempDir, { recursive: true });
|
|
} catch {}
|
|
}
|
|
}
|
|
const tmpPath = join(
|
|
needsIsolation ? safeTempDir : binDir,
|
|
`.${archiveName}.part`,
|
|
);
|
|
await downloadBinary(downloadUrl, tmpPath);
|
|
|
|
if (isWin) {
|
|
// Unzip to a temp directory, then move into the per-user cache.
|
|
const unzipDest = safeTempDir;
|
|
try {
|
|
const sysRoot =
|
|
process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
|
const psFull = join(
|
|
sysRoot,
|
|
"System32",
|
|
"WindowsPowerShell",
|
|
"v1.0",
|
|
"powershell.exe",
|
|
);
|
|
const psCmd = `Expand-Archive -Path '${tmpPath}' -DestinationPath '${unzipDest}' -Force`;
|
|
let ok = false;
|
|
// Attempt full-path powershell.exe
|
|
try {
|
|
execSync(
|
|
`"${psFull}" -NoProfile -NonInteractive -Command "${psCmd}"`,
|
|
{ stdio: "ignore" },
|
|
);
|
|
ok = true;
|
|
} catch {}
|
|
// Fallback to powershell in PATH
|
|
if (!ok) {
|
|
try {
|
|
execSync(
|
|
`powershell -NoProfile -NonInteractive -Command "${psCmd}"`,
|
|
{ stdio: "ignore" },
|
|
);
|
|
ok = true;
|
|
} catch {}
|
|
}
|
|
// Fallback to pwsh (PowerShell 7)
|
|
if (!ok) {
|
|
try {
|
|
execSync(`pwsh -NoProfile -NonInteractive -Command "${psCmd}"`, {
|
|
stdio: "ignore",
|
|
});
|
|
ok = true;
|
|
} catch {}
|
|
}
|
|
// Final fallback: bsdtar can extract .zip
|
|
if (!ok) {
|
|
execSync(`tar -xf "${tmpPath}" -C "${unzipDest}"`, {
|
|
stdio: "ignore",
|
|
shell: true,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`failed to unzip archive: ${e.message}`);
|
|
} finally {
|
|
try {
|
|
unlinkSync(tmpPath);
|
|
} catch {}
|
|
}
|
|
// Move the extracted file from temp to cache; do not leave a copy in node_modules
|
|
try {
|
|
const extractedPath = join(unzipDest, binaryName);
|
|
await writeCacheAtomic(extractedPath, cachePath);
|
|
try {
|
|
unlinkSync(extractedPath);
|
|
} catch {}
|
|
} catch (e) {
|
|
throw new Error(`failed to move binary to cache: ${e.message}`);
|
|
}
|
|
} else {
|
|
if (useZst) {
|
|
// Decompress .zst via system zstd
|
|
try {
|
|
const outPath = mirrorToLocal
|
|
? localPath
|
|
: join(safeTempDir, binaryName);
|
|
execSync(`zstd -d '${tmpPath}' -o '${outPath}'`, {
|
|
stdio: "ignore",
|
|
shell: true,
|
|
});
|
|
} catch (e) {
|
|
try {
|
|
unlinkSync(tmpPath);
|
|
} catch {}
|
|
throw new Error(
|
|
`failed to decompress .zst (need zstd CLI): ${e.message}`,
|
|
);
|
|
}
|
|
try {
|
|
unlinkSync(tmpPath);
|
|
} catch {}
|
|
} else {
|
|
// Extract .tar.gz using system tar
|
|
try {
|
|
const dest = mirrorToLocal ? binDir : safeTempDir;
|
|
execSync(`tar -xzf '${tmpPath}' -C '${dest}'`, {
|
|
stdio: "ignore",
|
|
shell: true,
|
|
});
|
|
} catch (e) {
|
|
try {
|
|
unlinkSync(tmpPath);
|
|
} catch {}
|
|
throw new Error(`failed to extract .tar.gz: ${e.message}`);
|
|
}
|
|
try {
|
|
unlinkSync(tmpPath);
|
|
} catch {}
|
|
}
|
|
if (!mirrorToLocal) {
|
|
try {
|
|
const extractedPath = join(safeTempDir, binaryName);
|
|
await writeCacheAtomic(extractedPath, cachePath);
|
|
try {
|
|
unlinkSync(extractedPath);
|
|
} catch {}
|
|
} catch (e) {
|
|
throw new Error(`failed to move binary to cache: ${e.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate header to avoid corrupt binaries causing spawn EFTYPE/ENOEXEC
|
|
|
|
const valid = validateDownloadedBinary(
|
|
isWin ? cachePath : mirrorToLocal ? localPath : cachePath,
|
|
);
|
|
if (!valid.ok) {
|
|
try {
|
|
isWin || !mirrorToLocal
|
|
? unlinkSync(cachePath)
|
|
: unlinkSync(localPath);
|
|
} catch {}
|
|
throw new Error(`invalid binary (${valid.reason})`);
|
|
}
|
|
|
|
// Make executable on Unix-like systems
|
|
if (!isWin && mirrorToLocal) {
|
|
chmodSync(localPath, 0o755);
|
|
}
|
|
|
|
console.log(
|
|
`✓ Installed ${binaryName}${isWin || !mirrorToLocal ? " (cached)" : ""}`,
|
|
);
|
|
// Ensure persistent cache holds the binary (already true for Windows path)
|
|
if (!isWin && mirrorToLocal) {
|
|
try {
|
|
await writeCacheAtomic(localPath, cachePath);
|
|
} catch {}
|
|
}
|
|
} catch (error) {
|
|
console.error(`✗ Failed to install ${binaryName}: ${error.message}`);
|
|
console.error(` Downloaded from: ${downloadUrl}`);
|
|
// Continue with other binaries even if one fails
|
|
}
|
|
}
|
|
|
|
// Create platform-specific symlink/copy for main binary
|
|
const mainBinary = `code-${targetTriple}${binaryExt}`;
|
|
const mainBinaryPath = join(binDir, mainBinary);
|
|
|
|
if (
|
|
existsSync(mainBinaryPath) ||
|
|
existsSync(
|
|
getCachedBinaryPath(version, targetTriple, platform() === "win32"),
|
|
)
|
|
) {
|
|
try {
|
|
const probePath = existsSync(mainBinaryPath)
|
|
? mainBinaryPath
|
|
: getCachedBinaryPath(version, targetTriple, platform() === "win32");
|
|
const stats = statSync(probePath);
|
|
if (!stats.size)
|
|
throw new Error("binary is empty (download likely failed)");
|
|
const valid = validateDownloadedBinary(probePath);
|
|
if (!valid.ok) {
|
|
console.warn(`⚠ Main code binary appears invalid: ${valid.reason}`);
|
|
console.warn(
|
|
" Try reinstalling or check your network/proxy settings.",
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.warn(`⚠ Main code binary appears invalid: ${e.message}`);
|
|
console.warn(" Try reinstalling or check your network/proxy settings.");
|
|
}
|
|
console.log("Setting up main code binary...");
|
|
|
|
// On Windows, we can't use symlinks easily, so update the JS wrapper
|
|
// On Unix, the JS wrapper will find the correct binary
|
|
console.log("✓ Installation complete!");
|
|
} else {
|
|
console.warn(
|
|
"⚠ Main code binary not found. You may need to build from source.",
|
|
);
|
|
}
|
|
|
|
// Handle collisions (e.g., VS Code) and add wrappers. We no longer publish a
|
|
// `code` bin in package.json. Instead, for global installs we create a `code`
|
|
// wrapper only when there is no conflicting `code` earlier on PATH. This avoids
|
|
// hijacking the VS Code CLI while still giving users a friendly name when safe.
|
|
// For upgrades from older versions that published a `code` bin, we also remove
|
|
// our old shim if a conflict is detected.
|
|
if (isGlobal && !isNpx)
|
|
try {
|
|
const isTTY = process.stdout && process.stdout.isTTY;
|
|
const isWindows = platform() === "win32";
|
|
const ua = process.env.npm_config_user_agent || "";
|
|
const isBun = ua.includes("bun") || !!process.env.BUN_INSTALL;
|
|
|
|
const installedCmds = new Set(["coder"]); // global install always exposes coder via package manager
|
|
const skippedCmds = [];
|
|
|
|
// Helper to resolve all 'code' on PATH
|
|
const resolveAllOnPath = () => {
|
|
try {
|
|
if (isWindows) {
|
|
const out = execSync("where code", {
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
}).toString();
|
|
return out
|
|
.split(/\r?\n/)
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
let out = "";
|
|
try {
|
|
out = execSync('bash -lc "which -a code 2>/dev/null"', {
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
}).toString();
|
|
} catch {
|
|
try {
|
|
out = execSync("command -v code || true", {
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
}).toString();
|
|
} catch {
|
|
out = "";
|
|
}
|
|
}
|
|
return out
|
|
.split(/\r?\n/)
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
} catch {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
if (isBun) {
|
|
// Bun creates shims for every bin; if another 'code' exists elsewhere on PATH, remove Bun's shim
|
|
let bunBin = "";
|
|
try {
|
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
const bunBase = process.env.BUN_INSTALL || join(home, ".bun");
|
|
bunBin = join(bunBase, "bin");
|
|
} catch {}
|
|
|
|
const bunShim = join(bunBin || "", isWindows ? "code.cmd" : "code");
|
|
const candidates = resolveAllOnPath();
|
|
const other = candidates.find(
|
|
(p) => p && (!bunBin || !p.startsWith(bunBin)),
|
|
);
|
|
if (other && existsSync(bunShim)) {
|
|
try {
|
|
unlinkSync(bunShim);
|
|
console.log(
|
|
`✓ Skipped global 'code' shim under Bun (existing: ${other})`,
|
|
);
|
|
skippedCmds.push({ name: "code", reason: `existing: ${other}` });
|
|
} catch (e) {
|
|
console.log(
|
|
`⚠ Could not remove Bun shim '${bunShim}': ${e.message}`,
|
|
);
|
|
}
|
|
} else if (!other) {
|
|
// No conflict: create a wrapper that forwards to `coder`
|
|
try {
|
|
const wrapperPath = bunShim;
|
|
if (isWindows) {
|
|
const content = `@echo off\r\n"%~dp0coder" %*\r\n`;
|
|
writeFileSync(wrapperPath, content);
|
|
} else {
|
|
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
|
|
writeFileSync(wrapperPath, content);
|
|
chmodSync(wrapperPath, 0o755);
|
|
}
|
|
console.log("✓ Created 'code' wrapper -> coder (bun)");
|
|
installedCmds.add("code");
|
|
} catch (e) {
|
|
console.log(
|
|
`⚠ Failed to create 'code' wrapper (bun): ${e.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Print summary for Bun
|
|
const list = Array.from(installedCmds).sort().join(", ");
|
|
console.log(`Commands installed (bun): ${list}`);
|
|
if (skippedCmds.length) {
|
|
for (const s of skippedCmds)
|
|
console.error(`Commands skipped: ${s.name} (${s.reason})`);
|
|
console.error("→ Use `coder` to run this tool.");
|
|
}
|
|
// Final friendly usage hint
|
|
if (installedCmds.has("code")) {
|
|
console.log("Use 'code' to launch Code.");
|
|
} else {
|
|
console.log("Use 'coder' to launch Code.");
|
|
}
|
|
} else {
|
|
// npm/pnpm/yarn path
|
|
const globalBin = resolveGlobalBinDir();
|
|
const ourShim = globalBin
|
|
? join(globalBin, isWindows ? "code.cmd" : "code")
|
|
: "";
|
|
const candidates = resolveAllOnPath();
|
|
const others = candidates.filter(
|
|
(p) => p && (!ourShim || p !== ourShim),
|
|
);
|
|
const ourShimExists = ourShim && existsSync(ourShim);
|
|
const shimLooksOurs = ourShimExists && looksLikeOurCodeShim(ourShim);
|
|
const conflictPaths = [
|
|
...others,
|
|
...(ourShimExists && !shimLooksOurs ? [ourShim] : []),
|
|
];
|
|
const collision = conflictPaths.length > 0;
|
|
|
|
const ensureWrapper = (name, args) => {
|
|
if (!globalBin) return;
|
|
try {
|
|
const wrapperPath = join(
|
|
globalBin,
|
|
isWindows ? `${name}.cmd` : name,
|
|
);
|
|
if (isWindows) {
|
|
const content = `@echo off\r\n"%~dp0${collision ? "coder" : "code"}" ${args} %*\r\n`;
|
|
writeFileSync(wrapperPath, content);
|
|
} else {
|
|
const content = `#!/bin/sh\nexec "$(dirname \"$0\")/${collision ? "coder" : "code"}" ${args} "$@"\n`;
|
|
writeFileSync(wrapperPath, content);
|
|
chmodSync(wrapperPath, 0o755);
|
|
}
|
|
console.log(
|
|
`✓ Created wrapper '${name}' -> ${collision ? "coder" : "code"} ${args}`,
|
|
);
|
|
installedCmds.add(name);
|
|
} catch (e) {
|
|
console.log(`⚠ Failed to create '${name}' wrapper: ${e.message}`);
|
|
}
|
|
};
|
|
|
|
// Always create legacy wrappers so existing scripts keep working
|
|
ensureWrapper("code-tui", "");
|
|
ensureWrapper("code-exec", "exec");
|
|
|
|
if (collision) {
|
|
console.error("⚠ Detected existing `code` on PATH:");
|
|
for (const p of conflictPaths) console.error(` - ${p}`);
|
|
if (globalBin) {
|
|
try {
|
|
if (ourShimExists) {
|
|
if (shimLooksOurs && others.length > 0) {
|
|
unlinkSync(ourShim);
|
|
console.error(
|
|
`✓ Removed global 'code' shim (ours) at ${ourShim}`,
|
|
);
|
|
const reason = others[0] || ourShim;
|
|
skippedCmds.push({
|
|
name: "code",
|
|
reason: `existing: ${reason}`,
|
|
});
|
|
} else if (!shimLooksOurs) {
|
|
console.error(
|
|
`✓ Skipped global 'code' shim (different CLI at ${ourShim})`,
|
|
);
|
|
const reason = conflictPaths[0] || ourShim;
|
|
skippedCmds.push({
|
|
name: "code",
|
|
reason: `existing: ${reason}`,
|
|
});
|
|
}
|
|
} else {
|
|
const reason = conflictPaths[0] || "another command on PATH";
|
|
skippedCmds.push({
|
|
name: "code",
|
|
reason: `existing: ${reason}`,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error(
|
|
`⚠ Could not remove npm shim '${ourShim}': ${e.message}`,
|
|
);
|
|
}
|
|
console.error("→ Use `coder` to run this tool.");
|
|
} else {
|
|
console.log(
|
|
"Note: could not determine npm global bin; skipping alias creation.",
|
|
);
|
|
}
|
|
} else {
|
|
// No collision; ensure a 'code' wrapper exists forwarding to 'coder'
|
|
if (globalBin) {
|
|
try {
|
|
const content = isWindows
|
|
? `@echo off\r\n"%~dp0coder" %*\r\n`
|
|
: `#!/bin/sh\nexec "$(dirname \"$0\")/coder" "$@"\n`;
|
|
writeFileSync(ourShim, content);
|
|
if (!isWindows) chmodSync(ourShim, 0o755);
|
|
console.log("✓ Created 'code' wrapper -> coder");
|
|
installedCmds.add("code");
|
|
} catch (e) {
|
|
console.log(`⚠ Failed to create 'code' wrapper: ${e.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Print summary for npm/pnpm/yarn
|
|
const list = Array.from(installedCmds).sort().join(", ");
|
|
console.log(`Commands installed: ${list}`);
|
|
if (skippedCmds.length) {
|
|
for (const s of skippedCmds)
|
|
console.log(`Commands skipped: ${s.name} (${s.reason})`);
|
|
}
|
|
// Final friendly usage hint
|
|
if (installedCmds.has("code")) {
|
|
console.log("Use 'code' to launch Code.");
|
|
} else {
|
|
console.log("Use 'coder' to launch Code.");
|
|
}
|
|
}
|
|
} catch {
|
|
// non-fatal
|
|
}
|
|
}
|
|
|
|
function isExecutedDirectly() {
|
|
const entry = process.argv[1];
|
|
if (!entry) return false;
|
|
try {
|
|
return resolve(entry) === fileURLToPath(import.meta.url);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isExecutedDirectly()) {
|
|
runPostinstall().catch((error) => {
|
|
console.error("Installation failed:", error);
|
|
process.exit(1);
|
|
});
|
|
}
|