Files
cline/sdk/apps/cli/bun.mts
T
BeeandSaoud Rizwan 386ded5126 feat(cli): bundle and serve Cline Hub dashboard with cline dashboard (#11195)
* feat(cli): bundle and serve Cline Hub dashboard with cline dashboard

Add the @cline/cline-hub workspace dependency to the CLI and build the
Hub webview as part of CLI packaging. Copy the generated dashboard assets
into platform-specific CLI distributions so the dashboard is available in
built artifacts.

Refactor the Cline Hub server startup into an exported function so the CLI
can start and stop the dashboard server programmatically.

* fix(cli): resolve dashboard webview in wrapper installs

Detect the platform-specific CLI package from the published wrapper layout
and use its bundled cline-hub webview assets when no explicit dist path is
set. Add test coverage for resolving assets via CLINE_WRAPPER_PATH.

* patches

* patch

* fix server detachHub on stop

Imported detachHub.
Changed ClineHubDashboardServer.stop to () => Promise<void>.
Made stop() idempotent with a stopped guard.
Clears the health interval.
Calls server.stop(true).
Always calls await detachHub(ctx) in a finally, so hub client teardown still happens if the HTTP server stop throws.

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-06-01 17:35:24 -07:00

134 lines
3.8 KiB
TypeScript

import {
copyFileSync,
cpSync,
existsSync,
mkdirSync,
readdirSync,
statSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { $ } from "bun";
function defineProcessEnv(name: string): string {
return JSON.stringify(process.env[name] ?? "");
}
const sourcemap = Bun.env.CLINE_SOURCEMAPS === "1" ? "linked" : "none";
const rootDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(rootDir, "../../..");
const hubWebviewSourcePath = join(rootDir, "../cline-hub/src/webview");
const hubWebviewDistPath = join(rootDir, "../cline-hub/dist/webview");
const hubWebviewIndexPath = join(hubWebviewDistPath, "index.html");
const cliHubWebviewDistPath = join(rootDir, "dist/cline-hub/webview");
function newestFileMtimeMs(dir: string): number {
let newest = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (
entry.name === "node_modules" ||
entry.name === "dist" ||
entry.name === ".turbo"
) {
continue;
}
const path = join(dir, entry.name);
if (entry.isDirectory()) {
newest = Math.max(newest, newestFileMtimeMs(path));
} else if (entry.isFile()) {
newest = Math.max(newest, statSync(path).mtimeMs);
}
}
return newest;
}
function shouldBuildHubWebview(): boolean {
if (!existsSync(hubWebviewIndexPath)) {
return true;
}
try {
return (
newestFileMtimeMs(hubWebviewSourcePath) >
statSync(hubWebviewIndexPath).mtimeMs
);
} catch {
return true;
}
}
if (shouldBuildHubWebview()) {
console.log("Building Cline Hub webview...");
await $`bun -F @cline/cline-hub build:webview`.cwd(repoRoot);
}
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "node",
format: "esm",
sourcemap,
packages: "bundle", // Keep private workspace packages bundled so npm consumers do not need @cline/* at runtime.
external: [
// OpenTUI resolves a platform-specific native package at runtime.
// Bundling through that resolution path rewrites the import in a way that
// breaks Linux e2e runs from dist/. Keep React external too so OpenTUI and
// the CLI share one React runtime instead of ending up with duplicate hook
// dispatchers in the bundle.
"@opentui/core",
"@opentui/react",
"@opentui-ui/dialog",
"opentui-spinner",
"react",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"react-devtools-core",
],
define: {
"process.env.NODE_ENV": '"production"',
"process.env.OTEL_TELEMETRY_ENABLED": defineProcessEnv(
"OTEL_TELEMETRY_ENABLED",
),
"process.env.OTEL_EXPORTER_OTLP_ENDPOINT": defineProcessEnv(
"OTEL_EXPORTER_OTLP_ENDPOINT",
),
"process.env.OTEL_METRICS_EXPORTER": defineProcessEnv(
"OTEL_METRICS_EXPORTER",
),
"process.env.OTEL_LOGS_EXPORTER": defineProcessEnv("OTEL_LOGS_EXPORTER"),
"process.env.OTEL_EXPORTER_OTLP_PROTOCOL": defineProcessEnv(
"OTEL_EXPORTER_OTLP_PROTOCOL",
),
"process.env.OTEL_METRIC_EXPORT_INTERVAL": defineProcessEnv(
"OTEL_METRIC_EXPORT_INTERVAL",
),
"process.env.OTEL_EXPORTER_OTLP_HEADERS": defineProcessEnv(
"OTEL_EXPORTER_OTLP_HEADERS",
),
},
env: "OTEL_*",
banner:
'import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
});
if (result.logs.length > 0) {
for (const log of result.logs) {
console.warn(log);
}
}
const coreBootstrapPath = join(
rootDir,
"../../packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
);
const cliBootstrapPath = join(
rootDir,
"./dist/extensions/plugin-sandbox-bootstrap.js",
);
mkdirSync(dirname(cliBootstrapPath), { recursive: true });
copyFileSync(coreBootstrapPath, cliBootstrapPath);
if (existsSync(hubWebviewDistPath)) {
mkdirSync(dirname(cliHubWebviewDistPath), { recursive: true });
cpSync(hubWebviewDistPath, cliHubWebviewDistPath, { recursive: true });
}