From c27dd75135686612fd266abfb6965ed795bf22a9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 09:08:25 +0100 Subject: [PATCH] build(control-ui): prefer bun for UI build --- CHANGELOG.md | 2 +- README.md | 10 ++-- docs/control-ui.md | 10 ++-- docs/web.md | 4 +- package.json | 6 +- scripts/package-mac-app.sh | 4 +- scripts/ui.js | 102 +++++++++++++++++++++++++++++++++ src/gateway/control-ui.ts | 2 +- src/infra/control-ui-assets.ts | 44 +++++++------- 9 files changed, 141 insertions(+), 43 deletions(-) create mode 100644 scripts/ui.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 021462476..94fed3878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,7 +133,7 @@ - Env: load global `$CLAWDBOT_STATE_DIR/.env` (`~/.clawdbot/.env`) as a fallback after CWD `.env`. - Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env). - Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas). -- Onboarding: when running from source, auto-build missing Control UI assets (`pnpm ui:build`). +- Onboarding: when running from source, auto-build missing Control UI assets (`bun run ui:build`). - Discord/Slack: route reaction + system notifications to the correct session (no main-session bleed). - Agent tools: honor `agent.tools` allow/deny policy even when sandbox is off. - Discord: avoid duplicate replies when OpenAI emits repeated `message_end` events. diff --git a/README.md b/README.md index dd1fb17ca..195fad9eb 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,10 @@ Do **not** download prebuilt binaries. Build from source. git clone https://github.com/clawdbot/clawdbot.git cd clawdbot -pnpm install -pnpm build -pnpm ui:build -pnpm clawdbot onboard +bun install +bun run build +bun run ui:build +bun run clawdbot onboard ``` ## Quick start (from source) @@ -442,5 +442,5 @@ Thanks to all clawtributors: scald sreekaransrinath ratulsarna osolmaz conhecendocontato hrdwdmrbl jayhickey jamesgroat gtsifrikas djangonavarro220 azade-c andranik-sahakyan adamgall jalehman jarvis-medmatic mneves75 regenrek tobiasbischoff MSch obviyus dbhurley - Asleep123 Iamadig imfing kitze nachoiacovino VACInc + Asleep123 Iamadig imfing kitze nachoiacovino VACInc cash-echo-bot claude kiranjd pcty-nextgen-service-account

diff --git a/docs/control-ui.md b/docs/control-ui.md index 315e1415c..0e4fe0c32 100644 --- a/docs/control-ui.md +++ b/docs/control-ui.md @@ -63,21 +63,21 @@ Paste the token into the UI settings (sent as `connect.params.auth.token`). The Gateway serves static files from `dist/control-ui`. Build them with: ```bash -pnpm ui:install -pnpm ui:build +bun run ui:install +bun run ui:build ``` Optional absolute base (when you want fixed asset URLs): ```bash -CLAWDBOT_CONTROL_UI_BASE_PATH=/clawdbot/ pnpm ui:build +CLAWDBOT_CONTROL_UI_BASE_PATH=/clawdbot/ bun run ui:build ``` For local development (separate dev server): ```bash -pnpm ui:install -pnpm ui:dev +bun run ui:install +bun run ui:dev ``` Then point the UI at your Gateway WS URL (e.g. `ws://127.0.0.1:18789`). diff --git a/docs/web.md b/docs/web.md index aeb0967f1..6e1a7274a 100644 --- a/docs/web.md +++ b/docs/web.md @@ -110,6 +110,6 @@ Open: The Gateway serves static files from `dist/control-ui`. Build them with: ```bash -pnpm ui:install -pnpm ui:build +bun run ui:install +bun run ui:build ``` diff --git a/package.json b/package.json index 4e30de34a..1e2ba8f6d 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,9 @@ "docs:build": "cd docs && pnpm dlx mint broken-links", "build": "tsc -p tsconfig.json && bun scripts/canvas-a2ui-copy.ts", "release:check": "bun scripts/release-check.ts", - "ui:install": "pnpm -C ui install", - "ui:dev": "pnpm -C ui dev", - "ui:build": "pnpm -C ui build", + "ui:install": "node scripts/ui.js install", + "ui:dev": "node scripts/ui.js dev", + "ui:build": "node scripts/ui.js build", "start": "bun src/entry.ts", "clawdbot": "bun src/entry.ts", "gateway:watch": "bun --watch src/entry.ts gateway --force", diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh index 7c7fe1b1f..b60a6cb75 100755 --- a/scripts/package-mac-app.sh +++ b/scripts/package-mac-app.sh @@ -146,8 +146,8 @@ else fi if [[ "${SKIP_UI_BUILD:-0}" != "1" ]]; then - echo "🖥 Building Control UI (pnpm ui:build)" - (cd "$ROOT_DIR" && pnpm ui:build) + echo "🖥 Building Control UI (ui:build)" + (cd "$ROOT_DIR" && node scripts/ui.js build) else echo "🖥 Skipping Control UI build (SKIP_UI_BUILD=1)" fi diff --git a/scripts/ui.js b/scripts/ui.js new file mode 100644 index 000000000..bb84ebbff --- /dev/null +++ b/scripts/ui.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(here, ".."); +const uiDir = path.join(repoRoot, "ui"); + +function usage() { + // keep this tiny; it's invoked from npm scripts too + process.stderr.write( + "Usage: node scripts/ui.js [...args]\n", + ); +} + +function which(cmd) { + try { + const key = process.platform === "win32" ? "Path" : "PATH"; + const paths = (process.env[key] ?? process.env.PATH ?? "") + .split(path.delimiter) + .filter(Boolean); + const extensions = + process.platform === "win32" + ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM") + .split(";") + .filter(Boolean) + : [""]; + for (const entry of paths) { + for (const ext of extensions) { + const candidate = path.join(entry, process.platform === "win32" ? `${cmd}${ext}` : cmd); + try { + if (fs.existsSync(candidate)) return candidate; + } catch { + // ignore + } + } + } + } catch { + // ignore + } + return null; +} + +function resolveRunner() { + const bun = which("bun"); + if (bun) return { cmd: bun, kind: "bun" }; + const pnpm = which("pnpm"); + if (pnpm) return { cmd: pnpm, kind: "pnpm" }; + return null; +} + +function run(cmd, args) { + const child = spawn(cmd, args, { + cwd: uiDir, + stdio: "inherit", + env: process.env, + }); + child.on("exit", (code, signal) => { + if (signal) process.exit(1); + process.exit(code ?? 1); + }); +} + +const [, , action, ...rest] = process.argv; +if (!action) { + usage(); + process.exit(2); +} + +const runner = resolveRunner(); +if (!runner) { + process.stderr.write( + "Missing UI runner: install bun or pnpm, then retry.\n", + ); + process.exit(1); +} + +const script = + action === "install" + ? null + : action === "dev" + ? "dev" + : action === "build" + ? "build" + : action === "test" + ? "test" + : null; + +if (action !== "install" && !script) { + usage(); + process.exit(2); +} + +if (runner.kind === "bun") { + if (action === "install") run(runner.cmd, ["install", ...rest]); + else run(runner.cmd, ["run", script, ...rest]); +} else { + if (action === "install") run(runner.cmd, ["install", ...rest]); + else run(runner.cmd, ["run", script, ...rest]); +} diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index e53f4e352..8f69e806a 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -157,7 +157,7 @@ export function handleControlUiHttpRequest( res.statusCode = 503; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end( - "Control UI assets not found. Build them with `pnpm ui:build` (or run `pnpm ui:dev` during development).", + "Control UI assets not found. Build them with `bun run ui:build` (or run `bun run ui:dev` during development).", ); return true; } diff --git a/src/infra/control-ui-assets.ts b/src/infra/control-ui-assets.ts index e005789dc..0b995a0cd 100644 --- a/src/infra/control-ui-assets.ts +++ b/src/infra/control-ui-assets.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { runCommandWithTimeout, runExec } from "../process/exec.js"; +import { runCommandWithTimeout } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; export function resolveControlUiRepoRoot( @@ -76,7 +76,7 @@ export async function ensureControlUiAssetsBuilt( return { ok: false, built: false, - message: `${hint}. Build them with \`pnpm ui:build\`.`, + message: `${hint}. Build them with \`bun run ui:build\`.`, }; } @@ -85,35 +85,28 @@ export async function ensureControlUiAssetsBuilt( return { ok: true, built: false }; } - const pnpmWhich = process.platform === "win32" ? "where" : "which"; - const pnpm = await runExec(pnpmWhich, ["pnpm"]) - .then( - (r) => - r.stdout - .split(/\r?\n/g) - .map((l) => l.trim()) - .find(Boolean) ?? "", - ) - .catch(() => ""); - if (!pnpm) { + const uiScript = path.join(repoRoot, "scripts", "ui.js"); + if (!fs.existsSync(uiScript)) { return { ok: false, built: false, - message: - "Control UI assets not found and pnpm missing. Install pnpm, then run `pnpm ui:build`.", + message: `Control UI assets missing but ${uiScript} is unavailable.`, }; } - runtime.log("Control UI assets missing; building (pnpm ui:build)…"); + runtime.log("Control UI assets missing; building (ui:build)…"); const ensureInstalled = !fs.existsSync( path.join(repoRoot, "ui", "node_modules"), ); if (ensureInstalled) { - const install = await runCommandWithTimeout([pnpm, "ui:install"], { - cwd: repoRoot, - timeoutMs: opts?.timeoutMs ?? 10 * 60_000, - }); + const install = await runCommandWithTimeout( + [process.execPath, uiScript, "install"], + { + cwd: repoRoot, + timeoutMs: opts?.timeoutMs ?? 10 * 60_000, + }, + ); if (install.code !== 0) { return { ok: false, @@ -123,10 +116,13 @@ export async function ensureControlUiAssetsBuilt( } } - const build = await runCommandWithTimeout([pnpm, "ui:build"], { - cwd: repoRoot, - timeoutMs: opts?.timeoutMs ?? 10 * 60_000, - }); + const build = await runCommandWithTimeout( + [process.execPath, uiScript, "build"], + { + cwd: repoRoot, + timeoutMs: opts?.timeoutMs ?? 10 * 60_000, + }, + ); if (build.code !== 0) { return { ok: false,