mirror of
https://github.com/cline/cline.git
synced 2026-09-12 00:50:27 +08:00
* fix(core): bridge protections for updates landing under pre-3.0.55 clients Three pieces, each proven against real released artifacts: - postinstall shield: CLI versions <= 3.0.54 restart the hub daemon after a background auto-update even while it serves live sessions, and their fingerprint check then rejects every replacement hub, bricking the running TUI. That code is on users' machines and cannot be patched — but it runs only after the install completes, and it bails out harmlessly when no hub discovery record exists. The newly installed package's postinstall sets the record aside so the old updater never fires. - superseded-record fallback: the set-aside record is also the only source of the auth token and pid the next new-build launch needs to retire the displaced hub (a port probe carries neither); ensure reads it back. - bind retry: a hub retired on the fixed port can hold it ~2s after acking shutdown (watchdog force-exit); the replacement daemon retries EADDRINUSE for up to 5s instead of dying and leaving no hub at all. * fix(cli): defer auto-update install until no CLI is attached to the hub Installing while cline processes run swaps the npm package under them: their respawn paths break on the new build fingerprint, and the updater then restarted the hub daemon out from under live sessions (the 'Hub connection closed (code=1006)' incident). Guarding the restart treats the symptom; the fix is to never install under a running process. The startup check now only records that an update is available. The install runs at process exit, and only when the hub confirms no other cli* client is attached — desktop sidecars and connectors ship their own binaries, so only cli* clients make the swap unsafe. With nothing old running at install time, no hub restart is needed at all: the next launch retires the stale hub through the existing ensure path. Deletes restartHubServerIfRunning, ensureCliHubServerAfterUpdate, and their support code; manual 'cline update' still installs immediately and now just notes that the update applies on next start. * fix(cli): apply deferred update from the entrypoint exit sequence The CLI entrypoint always terminates with an explicit process.exit(), which never emits beforeExit — the hook the deferred installer waited on, so it would never have run (caught by review). Invoke applyDeferredUpdate directly from the entrypoint's exit sequence after disposeAll(), where every normal termination passes; crash paths deliberately skip it. Also clear the pending update once an install spawns so the apply is idempotent. * test(cli): isolate unit tests from the real ~/.cline A full vitest run could leave a real hub daemon running against the developer's actual ~/.cline discovery record (observed while validating this PR: a daemon spawned from the globally installed cline binary, attached to the real data dir). Point CLINE_DIR, CLINE_DATA_DIR, and CLINE_HUB_DISCOVERY_PATH at a per-worker temp dir and disable auto-update before any test file loads; subprocesses inherit the isolation via env. * fix(core): discard the superseded discovery record once consumed The set-aside record is one-shot recovery metadata, but nothing deleted it, and it feeds a pid into retireDiscoveredHub's SIGTERM. Weeks later a launch that finds no live record (routine after any retirement) could read the stale file and signal whatever process the OS recycled that pid onto (review finding by @abeatrix). Unlink it at every ensure resolution that ends with a live, verified hub; failure paths keep it for the next attempt. * fix(cli): harden the exit-time update gate Three review findings on the deferred-apply path: - A wedged hub could stall an otherwise-finished CLI for tens of seconds via the hub client's default timeouts; the whole exit-time query is now bounded to 3s, with timeout counting as attached (never install unless the hub positively confirms). - Sub-second commands exited before the startup version check resolved and silently dropped the update every time for one-shot-only usage; exit now grants the in-flight check a 250ms grace. - client.list can lose a TUI's registration during transport churn while its session connection survives, so an empty client list is not proof of safety; cross-check sessions with participants. Participants rather than session status: finished sessions linger idle forever and must not pin updates, and participant-less scheduled runs live in the hub process, which the binary swap does not touch. Verified live: a session-holding client invisible to client.list defers the install, and the gate opens once it disconnects. * docs(cli): fix stale beforeExit reference in the exit-gate comment * style(cli): apply biome formatting to update deferral code * fix(cli): let doctor see a hub whose record the update shield set aside During the shielded update window the discovery record is renamed to .superseded so pre-3.0.55 updaters cannot restart a busy hub. Doctor read only the primary record, so in that window it reported the live daemon - the one serving the user's still-open old session - as a stale hub daemon and advised 'cline doctor fix', which kills it and reproduces the exact 1006 incident the shield exists to prevent (found by QA). Doctor now falls back to the set-aside record the same way the ensure path does, and doctor fix clears the set-aside file along with the primary record so a deliberate reset does not leave stale retirement metadata pointing at a recyclable pid. * fix(core): keep shielded sessions on one Hub authority (#13244) * fix(core): recover shielded busy hub discovery * chore(core): instrument shielded hub recovery * fix(core): recover shielded hubs with attached clients * fix(cli): recognize shielded hubs in doctor * refactor(core): keep shield recovery minimal * fix(core): retain shared Hub idle helper semantics * chore(core): align busyness helper with the #13231 wording The participants-only hasActiveHubSessions here duplicates the change on bee/hub-lifecycle (this branch needs its semantics for the participant gate). Matching that version byte for byte lets the two merges resolve cleanly instead of conflicting. Also restores the module-registry reset comment this branch dropped - it documents a real local-vs-CI gotcha.
127 lines
4.1 KiB
JavaScript
127 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Post-install script for Cline CLI.
|
|
//
|
|
// Creates a hard link (or copy fallback) from the platform-specific binary
|
|
// to bin/.cline for fast startup on subsequent runs.
|
|
//
|
|
// This script must use only Node.js APIs (no Bun) since it runs via
|
|
// "node script/postinstall.mjs" in the npm lifecycle.
|
|
|
|
import fs from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const require = createRequire(import.meta.url);
|
|
|
|
// CLI versions <= 3.0.54 restart the hub daemon after a background
|
|
// auto-update even while it is serving live sessions, killing those sessions
|
|
// mid-turn — and their build-fingerprint check then rejects every replacement
|
|
// hub, bricking the running TUI. That restart code is the *old* version's, so
|
|
// it cannot be patched here; but it bails out harmlessly when no hub
|
|
// discovery record exists, and it runs only after this install (and this
|
|
// script) completes. Setting the record aside protects any attached clients:
|
|
// a running hub keeps serving its established connections, clients that share
|
|
// its build fingerprint rebuild the record from a port probe, and the next
|
|
// fresh launch retires stale hubs regardless of the record.
|
|
function shieldRunningHubDiscovery() {
|
|
const explicitPath = process.env.CLINE_HUB_DISCOVERY_PATH?.trim();
|
|
const dataDir =
|
|
process.env.CLINE_DATA_DIR?.trim() ||
|
|
path.join(
|
|
process.env.CLINE_DIR?.trim() || path.join(os.homedir(), ".cline"),
|
|
"data",
|
|
);
|
|
const recordPath =
|
|
explicitPath || path.join(dataDir, "locks", "hub", "production.json");
|
|
if (!fs.existsSync(recordPath)) {
|
|
return;
|
|
}
|
|
const asidePath = `${recordPath}.superseded`;
|
|
fs.rmSync(asidePath, { force: true });
|
|
fs.renameSync(recordPath, asidePath);
|
|
console.log("Set aside hub discovery record for the updated CLI");
|
|
}
|
|
|
|
function main() {
|
|
if (os.platform() === "win32") {
|
|
// On Windows, npm creates .cmd shims from the bin field.
|
|
// The resolver script handles binary lookup at runtime.
|
|
console.log("Windows detected: skipping binary cache setup");
|
|
return;
|
|
}
|
|
|
|
const platformMap = {
|
|
darwin: "darwin",
|
|
linux: "linux",
|
|
};
|
|
const platform = platformMap[os.platform()] || os.platform();
|
|
const arch = os.arch();
|
|
const packageName = `@cline/cli-${platform}-${arch}`;
|
|
const binaryName = "cline";
|
|
|
|
let binaryPath;
|
|
try {
|
|
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
const packageDir = path.dirname(packageJsonPath);
|
|
binaryPath = path.join(packageDir, "bin", binaryName);
|
|
|
|
if (!fs.existsSync(binaryPath)) {
|
|
throw new Error(`Binary not found at ${binaryPath}`);
|
|
}
|
|
} catch (_error) {
|
|
// Platform package not available. The resolver script will find
|
|
// it at runtime by walking node_modules. This is expected on
|
|
// platforms we don't ship binaries for.
|
|
console.log(`Note: ${packageName} not found, skipping binary cache`);
|
|
return;
|
|
}
|
|
|
|
const binDir =
|
|
path.basename(__dirname) === "script"
|
|
? path.join(__dirname, "..", "bin")
|
|
: path.join(__dirname, "bin");
|
|
const target = path.join(binDir, ".cline");
|
|
|
|
// Ensure bin directory exists
|
|
if (!fs.existsSync(binDir)) {
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
}
|
|
|
|
// Remove existing cached binary
|
|
if (fs.existsSync(target)) {
|
|
fs.unlinkSync(target);
|
|
}
|
|
|
|
// Hard link preferred (shares disk space), copy as fallback
|
|
// (hard links fail on some filesystems like NFS or cross-device)
|
|
try {
|
|
fs.linkSync(binaryPath, target);
|
|
} catch {
|
|
fs.copyFileSync(binaryPath, target);
|
|
}
|
|
|
|
fs.chmodSync(target, 0o755);
|
|
console.log(`Cached cline binary at ${target}`);
|
|
}
|
|
|
|
try {
|
|
shieldRunningHubDiscovery();
|
|
} catch (error) {
|
|
// Best-effort: without the shield the worst case is the pre-3.0.55
|
|
// restart-while-busy behavior, never a broken install.
|
|
console.error(`postinstall: hub discovery shield skipped: ${error.message}`);
|
|
}
|
|
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
// postinstall failures should never block npm install.
|
|
// The resolver script will find the binary at runtime.
|
|
console.error(`postinstall: ${error.message}`);
|
|
process.exit(0);
|
|
}
|