Files
cline/apps/cli/src/index.ts
T
2377a20973 fix(core): load embedded plugin and dashboard resources (#15080)
* fix(core): load embedded plugin and dashboard resources

* fix(core): isolate subprocess Bun mode and retain system CA trust

* fix(core): load compiled plugin resources directly from bunfs

* test(desktop): compare native paths and tolerate Hub cleanup on Windows

The standalone plugin test compared the child's process.execPath to
realpathSync(cli), which keeps Windows 8.3 short names (RUNNER~1) while the
child reports the long path. Use realpathSync.native. The dashboard test's
cleanup could hit EBUSY while the stopped Hub released its cwd; retry it like
the startup scenario cleanup does.

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): package every declared SDK runtime export

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-10-19 15:37:08 -07:00

143 lines
4.8 KiB
TypeScript
Executable File

#!/usr/bin/env bun
import { isMainThread } from "node:worker_threads";
import { setPluginSandboxLauncher } from "@cline/core";
import {
buildTelemetrySelfcheckReport,
claimHubDaemonProcess,
claimSupervisedConnectorProcess,
createClineTelemetryServiceConfig,
disableCurrentDirectoryExecutableSearch,
disposeAll,
ensureLoopbackProxyBypass,
initVcr,
setConnectorCliLaunchSpec,
} from "@cline/shared";
import { logCliProcessError } from "./logging/errors";
import {
abortActiveRuntime,
cleanupActiveRuntime,
isAbortInProgress,
} from "./runtime/active-runtime";
import { registerClineClientIdentity } from "./utils/cline-client-identity";
import { resolveCliLaunchSpec } from "./utils/internal-launch";
import { writeErr } from "./utils/output";
// Initialize VCR before any HTTP requests are made.
// Set CLINE_VCR=record|playback and CLINE_VCR_CASSETTE=<path> to enable.
initVcr(process.env.CLINE_VCR);
// Before any personality below can spawn a child with the workspace as cwd.
disableCurrentDirectoryExecutableSearch();
// Before any personality probes the local hub over 127.0.0.1.
ensureLoopbackProxyBypass();
if (Bun.isStandaloneExecutable) {
setPluginSandboxLauncher({
command: process.execPath,
argsPrefix: ["--plugin-sandbox"],
});
}
if (!isMainThread) {
// Worker imports of the bundled CLI entrypoint should not start the CLI.
} else if (process.argv.includes("--plugin-sandbox")) {
// Boot as the compiled host so the child mounts the same read-only resources.
const bootstrap = process.argv[process.argv.indexOf("--plugin-sandbox") + 1];
if (!bootstrap) throw new Error("Missing plugin sandbox bootstrap path");
await import(bootstrap);
} else if (process.argv.includes("--telemetry-selfcheck")) {
// Inspect the config embedded in this executable without starting a hub,
// loading user settings, or initializing telemetry exporters.
process.stdout.write(
`${JSON.stringify(buildTelemetrySelfcheckReport(createClineTelemetryServiceConfig()))}\n`,
);
} else if (claimHubDaemonProcess()) {
// Claim rather than read: the sentinel is consumed here so the processes a
// daemon-hosted session spawns do not inherit it and try to become daemons.
// The hub daemon owns its process-level abort handling. Installing the CLI's
// fatal rejection handler first would make expected abort rejections exit it.
registerClineClientIdentity("cline-cli");
void import("@cline/core/hub/daemon-entry");
} else {
// Same reasoning as the daemon sentinel above: consume the supervised-connector
// marker so the processes an agent session spawns cannot inherit it and mistake
// themselves for the connector the hub is tracking.
claimSupervisedConnectorProcess();
const cliLaunchSpec = resolveCliLaunchSpec({ debugRole: "connector" });
if (cliLaunchSpec) {
setConnectorCliLaunchSpec({
launcher: cliLaunchSpec.launcher,
connectArgsPrefix: [...cliLaunchSpec.childArgsPrefix, "connect"],
cwd: process.cwd(),
});
}
let shuttingDown = false;
let handlingFatalProcessError = false;
const forwardSignalToRuntime = () => {
if (shuttingDown) {
process.exit(1);
}
shuttingDown = true;
abortActiveRuntime();
};
process.on("SIGINT", forwardSignalToRuntime);
process.on("SIGTERM", forwardSignalToRuntime);
const handleFatalProcessError = (kind: string, error: unknown) => {
if (handlingFatalProcessError) {
process.exit(1);
}
handlingFatalProcessError = true;
logCliProcessError(kind, error);
writeErr(
error instanceof Error ? (error.stack ?? error.message) : String(error),
);
cleanupActiveRuntime();
abortActiveRuntime();
void disposeAll().finally(() => {
process.exit(1);
});
};
process.on("uncaughtException", (error) => {
handleFatalProcessError("uncaughtException", error);
});
process.on("unhandledRejection", (reason, promise) => {
if (isAbortInProgress()) {
// Mark the promise as handled so OpenTUI's error overlay
// does not surface expected abort-related rejections.
promise.catch(() => {});
return;
}
handleFatalProcessError("unhandledRejection", reason);
});
void (async () => {
let exitCode = 0;
try {
const { runCli } = await import("./main");
await runCli();
} catch (err) {
logCliProcessError("runCli", err);
writeErr(err instanceof Error ? err.message : String(err));
cleanupActiveRuntime();
abortActiveRuntime();
exitCode = 1;
} finally {
await disposeAll();
}
// The explicit process.exit below means beforeExit never fires, so a
// startup-recorded auto-update must be applied here, after all runtime
// teardown. It spawns detached and only when no other CLI is attached.
try {
const { applyDeferredUpdate } = await import("./commands/update");
await applyDeferredUpdate();
} catch {
// Best-effort; never block exit on the updater.
}
process.exit(exitCode || (process.exitCode as number) || 0);
})();
}