fix(cli/install): bootstrap native binary on first run; drop unpublished win optional dep\n\n- Add self-bootstrap in bin/coder.js to fetch validated platform binary when postinstall is blocked (e.g., Bun global)\n- Prefer cached binary, then optional platform pkg, then GitHub release download\n- Remove @just-every/code-win32-x64 from optionalDependencies to avoid Bun 404 noise on non-Windows installs\n- Build verified with ./build-fast.sh

This commit is contained in:
James Peter
2025-08-30 13:39:39 +10:00
parent 4f3690d05e
commit 27a0b4e49e
4 changed files with 217 additions and 10 deletions
+190 -3
View File
@@ -3,6 +3,9 @@
import path from "path";
import { fileURLToPath } from "url";
import { platform as nodePlatform, arch as nodeArch } from "os";
import { execSync } from "child_process";
import { get as httpsGet } from "https";
// __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
@@ -76,12 +79,196 @@ if (!targetTriple) {
// Prefer new 'code-*' binary names; fall back to legacy 'coder-*' if missing.
let binaryPath = path.join(__dirname, "..", "bin", `code-${targetTriple}`);
if (!existsSync(binaryPath)) {
binaryPath = path.join(__dirname, "..", "bin", `coder-${targetTriple}`);
let legacyBinaryPath = path.join(__dirname, "..", "bin", `coder-${targetTriple}`);
// --- Bootstrap helper (runs if the binary is missing, e.g. Bun blocked postinstall) ---
import { existsSync, chmodSync, statSync, openSync, readSync, closeSync, mkdirSync, copyFileSync, readFileSync, unlinkSync } from "fs";
const validateBinary = (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" };
if (platform === "win32") {
if (!(buf[0] === 0x4d && buf[1] === 0x5a)) return { ok: false, reason: "invalid PE header (missing MZ)" };
} else if (platform === "linux" || platform === "android") {
if (!(buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46)) return { ok: false, reason: "invalid ELF header" };
} else if (platform === "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" };
}
} finally {
closeSync(fd);
}
return { ok: true };
} catch (e) {
return { ok: false, reason: e.message };
}
};
const getCacheDir = (version) => {
const plt = nodePlatform();
const home = process.env.HOME || process.env.USERPROFILE || "";
let base = "";
if (plt === "win32") {
base = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
} else if (plt === "darwin") {
base = path.join(home, "Library", "Caches");
} else {
base = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
}
const dir = path.join(base, "just-every", "code", version);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
return dir;
};
const getCachedBinaryPath = (version) => {
const isWin = nodePlatform() === "win32";
const ext = isWin ? ".exe" : "";
const cacheDir = getCacheDir(version);
return path.join(cacheDir, `code-${targetTriple}${ext}`);
};
const httpsDownload = (url, dest) => new Promise((resolve, reject) => {
const req = httpsGet(url, (res) => {
const status = res.statusCode || 0;
if (status >= 300 && status < 400 && res.headers.location) {
// follow one redirect recursively
return resolve(httpsDownload(res.headers.location, dest));
}
if (status !== 200) {
return reject(new Error(`HTTP ${status}`));
}
const out = require("fs").createWriteStream(dest);
res.pipe(out);
out.on("finish", () => out.close(resolve));
out.on("error", (e) => {
try { unlinkSync(dest); } catch {}
reject(e);
});
});
req.on("error", (e) => {
try { unlinkSync(dest); } catch {}
reject(e);
});
req.setTimeout(120000, () => {
req.destroy(new Error("download timed out"));
});
});
const tryBootstrapBinary = () => {
try {
// 1) Read our published version
const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
const version = pkg.version;
const binDir = path.join(__dirname, "..", "bin");
if (!existsSync(binDir)) mkdirSync(binDir, { recursive: true });
// 2) Fast path: user cache
const cachePath = getCachedBinaryPath(version);
if (existsSync(cachePath)) {
const v = validateBinary(cachePath);
if (v.ok) {
copyFileSync(cachePath, binaryPath);
if (platform !== "win32") chmodSync(binaryPath, 0o755);
return existsSync(binaryPath);
}
}
// 3) Try platform package (if present)
try {
const req = (await import("module")).createRequire(import.meta.url);
const name = (() => {
if (platform === "win32") return "@just-every/code-win32-x64"; // may be unpublished; falls through
const plt = nodePlatform();
const cpu = nodeArch();
if (plt === "darwin" && cpu === "arm64") return "@just-every/code-darwin-arm64";
if (plt === "darwin" && cpu === "x64") return "@just-every/code-darwin-x64";
if (plt === "linux" && cpu === "x64") return "@just-every/code-linux-x64-musl";
if (plt === "linux" && cpu === "arm64") return "@just-every/code-linux-arm64-musl";
return null;
})();
if (name) {
try {
const pkgJson = req.resolve(`${name}/package.json`);
const pkgDir = path.dirname(pkgJson);
const src = path.join(pkgDir, "bin", `code-${targetTriple}${platform === "win32" ? ".exe" : ""}`);
if (existsSync(src)) {
copyFileSync(src, binaryPath);
if (platform !== "win32") chmodSync(binaryPath, 0o755);
// refresh cache
try { copyFileSync(binaryPath, cachePath); } catch {}
return existsSync(binaryPath);
}
} catch { /* ignore and fall back */ }
}
} catch { /* ignore */ }
// 4) Download from GitHub release
const isWin = platform === "win32";
const archiveName = isWin
? `code-${targetTriple}.zip`
: (() => { try { execSync("zstd --version", { stdio: "ignore", shell: true }); return `code-${targetTriple}.zst`; } catch { return `code-${targetTriple}.tar.gz`; } })();
const url = `https://github.com/just-every/code/releases/download/v${version}/${archiveName}`;
const tmp = path.join(binDir, `.${archiveName}.part`);
return httpsDownload(url, tmp)
.then(() => {
if (isWin) {
try {
const ps = `powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmp}' -DestinationPath '${binDir}' -Force"`;
execSync(ps, { stdio: "ignore" });
} catch (e) {
throw new Error(`failed to unzip: ${e.message}`);
} finally { try { unlinkSync(tmp); } catch {} }
} else {
if (archiveName.endsWith(".zst")) {
try { execSync(`zstd -d '${tmp}' -o '${binaryPath}'`, { stdio: 'ignore', shell: true }); }
catch (e) { try { unlinkSync(tmp); } catch {}; throw new Error(`failed to decompress zst: ${e.message}`); }
try { unlinkSync(tmp); } catch {}
} else {
try { execSync(`tar -xzf '${tmp}' -C '${binDir}'`, { stdio: 'ignore', shell: true }); }
catch (e) { try { unlinkSync(tmp); } catch {}; throw new Error(`failed to extract tar.gz: ${e.message}`); }
try { unlinkSync(tmp); } catch {}
}
}
const v = validateBinary(binaryPath);
if (!v.ok) throw new Error(`invalid binary (${v.reason})`);
if (platform !== "win32") chmodSync(binaryPath, 0o755);
try { copyFileSync(binaryPath, cachePath); } catch {}
return true;
})
.catch((_e) => false);
} catch {
return false;
}
};
// If missing, attempt to bootstrap into place (helps when Bun blocks postinstall)
if (!existsSync(binaryPath) && !existsSync(legacyBinaryPath)) {
const ok = await tryBootstrapBinary();
if (!ok) {
// retry legacy name in case archive provided coder-*
if (existsSync(legacyBinaryPath) && !existsSync(binaryPath)) {
binaryPath = legacyBinaryPath;
}
}
}
// Fall back to legacy name if primary is still missing
if (!existsSync(binaryPath) && existsSync(legacyBinaryPath)) {
binaryPath = legacyBinaryPath;
}
// Check if binary exists and try to fix permissions if needed
import { existsSync, chmodSync, statSync, openSync, readSync, closeSync } from "fs";
// fs imports are above; keep for readability if tree-shaken by bundlers
import { spawnSync } from "child_process";
if (existsSync(binaryPath)) {
try {
+1 -2
View File
@@ -37,7 +37,6 @@
"@just-every/code-darwin-arm64": "0.2.47",
"@just-every/code-darwin-x64": "0.2.47",
"@just-every/code-linux-x64-musl": "0.2.47",
"@just-every/code-linux-arm64-musl": "0.2.47",
"@just-every/code-win32-x64": "0.2.47"
"@just-every/code-linux-arm64-musl": "0.2.47"
}
}
+18 -5
View File
@@ -148,6 +148,16 @@ impl App<'_> {
crossterm::event::Event::Resize(_, _) => {
app_event_tx.send(AppEvent::RequestRedraw);
}
// When the terminal/tab regains focus, issue a redraw.
// Some terminals clear the alt‑screen buffer on focus switches,
// which can leave the status bar and inline images blank until
// the next resize. A focus‑gain repaint fixes this immediately.
crossterm::event::Event::FocusGained => {
app_event_tx.send(AppEvent::RequestRedraw);
}
crossterm::event::Event::FocusLost => {
// No action needed; keep state as‑is.
}
crossterm::event::Event::Paste(pasted) => {
// Many terminals convert newlines to \r when pasting (e.g., iTerm2),
// but tui-textarea expects \n. Normalize CR to LF.
@@ -159,9 +169,7 @@ impl App<'_> {
crossterm::event::Event::Mouse(mouse_event) => {
app_event_tx.send(AppEvent::MouseEvent(mouse_event));
}
_ => {
// Ignore any other events.
}
// All other event variants are explicitly handled above.
}
}
} else {
@@ -744,7 +752,11 @@ impl App<'_> {
widget.set_theme(new_theme);
}
// Request a redraw to apply the new theme
// Force a full redraw on the next frame so the entire
// ratatui back buffer is cleared and repainted with the
// new theme. This avoids any stale cells lingering on
// terminals that preserve previous cell attributes.
self.clear_on_first_frame = true;
self.schedule_redraw();
}
AppEvent::PreviewTheme(new_theme) => {
@@ -772,7 +784,8 @@ impl App<'_> {
}
// Don't update config or add to history for previews
// Request a redraw to apply the new theme
// Force a full redraw so previews repaint cleanly as you cycle
self.clear_on_first_frame = true;
self.schedule_redraw();
}
AppEvent::ComposerExpanded => {
+8
View File
@@ -7,7 +7,9 @@ use codex_core::config::Config;
use crossterm::cursor::MoveTo;
use crossterm::event::DisableBracketedPaste;
use crossterm::event::DisableMouseCapture;
use crossterm::event::DisableFocusChange;
use crossterm::event::EnableBracketedPaste;
use crossterm::event::EnableFocusChange;
use crossterm::event::KeyboardEnhancementFlags;
use crossterm::event::PopKeyboardEnhancementFlags;
use crossterm::event::PushKeyboardEnhancementFlags;
@@ -52,6 +54,10 @@ pub fn init(config: &Config) -> Result<(Tui, TerminalInfo)> {
crate::syntax_highlight::init_highlight_from_config(&config.tui.highlight);
execute!(stdout(), EnableBracketedPaste)?;
// Enable focus change events so we can detect when the terminal window/tab
// regains focus and proactively repaint the UI (helps terminals that clear
// their alt‑screen buffer while unfocused).
let _ = execute!(stdout(), EnableFocusChange);
// Enter alternate screen mode for full screen TUI
execute!(stdout(), crossterm::terminal::EnterAlternateScreen)?;
@@ -162,6 +168,8 @@ pub fn restore() -> Result<()> {
// Pop may fail on platforms that didn't support the push; ignore errors.
let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
execute!(stdout(), DisableBracketedPaste)?;
// Best‑effort: disable focus change notifications if supported.
let _ = execute!(stdout(), DisableFocusChange);
execute!(stdout(), DisableMouseCapture)?;
disable_raw_mode()?;
// Leave alternate screen mode