ad6e80192d
Right-click menus: one portal shell (components/ContextMenu.tsx) for every item with actions - sidebar/Recents/archived session rows (SessionMenu), console tabs, pane headers, the Changes panel's files/tree/commits, the workspace home, sessions outside a workspace (SessionRowMenu: Sessions table, Dashboard, Activity, Workflow runs), prompt images, every Settings list and every Claude Code/Codex config list (FileItemMenu). Buttons stay; a delete keeps its button's confirm; the shell stops portal clicks from reaching the card that opened it. Status bar: Workspace $ and Session $ on the Workspace page, from ?lane= on GET /api/pricing/cost and the focused/replayed session's cost. Database, no data dropped: synchronous=NORMAL, journal_size_limit 64 MB, hourly PASSIVE checkpoint + PRAGMA optimize, idx_events_session_created (replacing the redundant idx_events_session), watchdog reads sessions.transcript_path, transcript parsed before the hook's write transaction, GET /api/sessions/:id?events=0 for the web page (reloads only on its own session, debounced), streamed export, transactional clear-data and chunked cleanup, subagent import reads tool_use_ids once, runtime logs rotated past 5 MB.
137 lines
5.8 KiB
JavaScript
137 lines
5.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* @file plugin-open.js
|
|
* @description Builds the dashboard UI bundle for a plugin install and prints
|
|
* the dashboard URL. `scripts/plugin-bootstrap.js` calls `buildClient` eagerly
|
|
* on session start, so client-only routes (e.g. `/run`) work the moment
|
|
* `claude` is started, not just the API. `/agenthub-open` re-runs
|
|
* this standalone — a no-op when the bundle already matches — as a fallback
|
|
* for `--force` rebuilds or a first build that failed during bootstrap, and it
|
|
* re-runs bootstrap first when the live server belongs to a retired plugin root
|
|
* (a mid-session `/plugin` update) so the new bundle is not left calling routes
|
|
* the running server never had.
|
|
*
|
|
* The client source is copied out of the (replaced-on-update) plugin cache into
|
|
* the runtime dir before building, so neither the cache nor the checkout is
|
|
* written to, and the output lands where the server already serves from
|
|
* (`DASHBOARD_CLIENT_DIST`).
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const fs = require("fs");
|
|
const { rotateIfLarge } = require("./log-rotate");
|
|
const path = require("path");
|
|
const { spawnSync } = require("child_process");
|
|
|
|
const boot = require("./plugin-bootstrap");
|
|
const { resolveDashboardPort } = require("../server/lib/server-info");
|
|
|
|
const PLUGIN_ROOT = path.resolve(__dirname, "..");
|
|
const NPM = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
|
|
function run(cmd, args, cwd, stdio) {
|
|
const res = spawnSync(cmd, args, { cwd, stdio, shell: process.platform === "win32" });
|
|
return res.status === 0;
|
|
}
|
|
|
|
/**
|
|
* Build the bundle into `<runtime>/client-dist`. Idempotent: an existing bundle
|
|
* is left alone unless `force` is set.
|
|
*
|
|
* `logPath`, when given, redirects npm's output there instead of inheriting the
|
|
* caller's stdio — used when this runs inside the detached bootstrap worker,
|
|
* whose own stdout is discarded, so a build failure is still debuggable.
|
|
*
|
|
* `exec` is the command runner, swappable for tests.
|
|
*
|
|
* @returns {"ready"|"built"|"failed"}
|
|
*/
|
|
function buildClient({
|
|
rt = boot.runtimeDir(),
|
|
root = PLUGIN_ROOT,
|
|
force = false,
|
|
logPath,
|
|
exec = run,
|
|
} = {}) {
|
|
const dist = path.join(rt, "client-dist");
|
|
if (!force && fs.existsSync(path.join(dist, "index.html"))) return "ready";
|
|
|
|
if (logPath) rotateIfLarge(logPath);
|
|
const logFd = logPath ? fs.openSync(logPath, "a") : null;
|
|
const stdio = logFd === null ? "inherit" : ["ignore", logFd, logFd];
|
|
try {
|
|
const src = path.join(rt, "client-src");
|
|
fs.rmSync(src, { recursive: true, force: true });
|
|
fs.cpSync(path.join(root, "client"), src, {
|
|
recursive: true,
|
|
filter: (p) => !/[\\/](node_modules|dist)$/.test(p),
|
|
});
|
|
|
|
// `npm install` reuses whatever the cache and a previous run left behind and
|
|
// still reports success, so a truncated cache entry or an interrupted install
|
|
// yields a tree that is missing files nobody notices until `tsc` dies with
|
|
// TS2318 (every global type gone, because typescript's own lib/*.d.ts never
|
|
// landed). `npm ci` starts from an empty node_modules and checks every
|
|
// tarball against the lockfile's integrity hash, so a bad cache entry is
|
|
// refetched instead of silently reused. It needs a lockfile that matches
|
|
// package.json — fall back to `install` when the copy has none or has drifted.
|
|
//
|
|
// `--include=dev` is not optional here: the whole build toolchain (vite,
|
|
// typescript, @vitejs/plugin-react, tailwindcss, @types/*) lives in the
|
|
// client's devDependencies, and npm reads `NODE_ENV=production` as
|
|
// `--omit=dev`. Every bootstrap descended from the running server inherits
|
|
// that NODE_ENV — the self-update chain and any SessionStart fired from a
|
|
// tmux pane the server started — so the install silently dropped `vite`,
|
|
// `npm run build` failed, and the PREVIOUS bundle stayed in place: the
|
|
// dashboard kept serving the old version's UI while the API was already the
|
|
// new one. An explicit CLI flag outranks the env, which is the point.
|
|
const npmFlags = ["--no-audit", "--no-fund", "--include=dev"];
|
|
if (
|
|
!exec(NPM, ["ci", ...npmFlags], src, stdio) &&
|
|
!exec(NPM, ["install", ...npmFlags], src, stdio)
|
|
) {
|
|
return "failed";
|
|
}
|
|
if (!exec(NPM, ["run", "build"], src, stdio)) return "failed";
|
|
|
|
fs.rmSync(dist, { recursive: true, force: true });
|
|
fs.cpSync(path.join(src, "dist"), dist, { recursive: true });
|
|
return "built";
|
|
} finally {
|
|
if (logFd !== null) fs.closeSync(logFd);
|
|
}
|
|
}
|
|
|
|
function dashboardUrl() {
|
|
try {
|
|
return `http://localhost:${resolveDashboardPort()}`;
|
|
} catch {
|
|
return "http://localhost:4820";
|
|
}
|
|
}
|
|
|
|
// Exported before the entry block below, not after it: `bootstrap()` requires
|
|
// this module back for `buildClient`, and when plugin-open is itself the entry
|
|
// that lazy require resolves to whatever is on module.exports at that moment.
|
|
module.exports = { buildClient, dashboardUrl };
|
|
|
|
if (require.main === module) {
|
|
// A plugin update applied mid-session (/plugin + /reload-plugins) fires no
|
|
// SessionStart, so the server keeps executing a cache dir Claude Code has
|
|
// already retired: a bundle rebuilt from the new root then calls routes that
|
|
// old server never had. Hand the version handover back to bootstrap, which
|
|
// owns stop/start and the dependency install the new root may need.
|
|
const stale = (boot.readState() || {}).pluginRoot !== PLUGIN_ROOT || !boot.serverIsLive();
|
|
if (stale) boot.bootstrap();
|
|
|
|
const result = buildClient({ force: process.argv.includes("--force") });
|
|
if (result === "failed") {
|
|
console.error("Client build failed — the API still works.");
|
|
process.exitCode = 1;
|
|
} else {
|
|
if (result === "built") console.log("Dashboard UI built.");
|
|
if (stale) console.log("Server restarted on the current plugin version.");
|
|
console.log(dashboardUrl());
|
|
}
|
|
}
|