Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
362 lines
9.2 KiB
JavaScript
362 lines
9.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Routes UI package commands through the repo's Node/pnpm wrappers.
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
|
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(here, "..");
|
|
const uiDir = path.join(repoRoot, "ui");
|
|
|
|
const WINDOWS_CMD_EXE_EXTENSIONS = new Set([".cmd", ".bat"]);
|
|
|
|
function usage() {
|
|
// keep this tiny; it's invoked from npm scripts too
|
|
process.stderr.write("Usage: node scripts/ui.js <install|dev|build|test> [...args]\n");
|
|
}
|
|
|
|
/**
|
|
* Returns whether Windows needs cmd.exe for a command shim.
|
|
*/
|
|
export function shouldUseCmdExeForCommand(cmd, platform = process.platform) {
|
|
if (platform !== "win32") {
|
|
return false;
|
|
}
|
|
const extension = path.extname(cmd).toLowerCase();
|
|
return WINDOWS_CMD_EXE_EXTENSIONS.has(extension);
|
|
}
|
|
|
|
/**
|
|
* Builds the spawn call for a UI command, including Windows cmd.exe wrapping.
|
|
*/
|
|
export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
|
|
const platform = params.platform ?? process.platform;
|
|
const options = {
|
|
cwd: params.cwd ?? uiDir,
|
|
stdio: "inherit",
|
|
env: envOverride ?? process.env,
|
|
shell: false,
|
|
};
|
|
|
|
if (shouldUseCmdExeForCommand(cmd, platform)) {
|
|
const comSpec = params.comSpec ?? resolveWindowsCmdExePath(options.env);
|
|
return {
|
|
command: comSpec,
|
|
args: ["/d", "/s", "/c", buildCmdExeCommandLine(cmd, args)],
|
|
options: {
|
|
...options,
|
|
windowsVerbatimArguments: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
command: cmd,
|
|
args,
|
|
options,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Builds the pnpm-backed spawn call for UI package scripts.
|
|
*/
|
|
export function resolvePnpmSpawnCall(pnpmArgs, envOverride, params = {}) {
|
|
const env = envOverride ?? process.env;
|
|
const platform = params.platform ?? process.platform;
|
|
const cwd = params.cwd ?? uiDir;
|
|
const runner = resolvePnpmRunner({
|
|
cwd,
|
|
env,
|
|
pnpmArgs,
|
|
nodeExecPath: params.nodeExecPath ?? process.execPath,
|
|
npmExecPath: params.npmExecPath ?? env.npm_execpath,
|
|
comSpec: params.comSpec,
|
|
platform,
|
|
});
|
|
return {
|
|
command: runner.command,
|
|
args: runner.args,
|
|
options: {
|
|
cwd,
|
|
stdio: "inherit",
|
|
env,
|
|
shell: runner.shell,
|
|
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
|
},
|
|
};
|
|
}
|
|
|
|
function runSpawnCall(spawnCall, label) {
|
|
const { command, args: spawnArgs, options } = spawnCall;
|
|
let child;
|
|
try {
|
|
child = spawn(command, spawnArgs, options);
|
|
} catch (err) {
|
|
console.error(`Failed to launch ${label}:`, err);
|
|
process.exit(1);
|
|
return;
|
|
}
|
|
|
|
let forwardedSignal = null;
|
|
let forwardedSignalPids = [];
|
|
let forceKillTimer = null;
|
|
let forwardedSignalDrainTimer = null;
|
|
const clearForwardedSignalTimers = () => {
|
|
if (forceKillTimer) {
|
|
clearTimeout(forceKillTimer);
|
|
forceKillTimer = null;
|
|
}
|
|
if (forwardedSignalDrainTimer) {
|
|
clearInterval(forwardedSignalDrainTimer);
|
|
forwardedSignalDrainTimer = null;
|
|
}
|
|
};
|
|
const finishForwardedSignal = () => {
|
|
cleanupSignalHandlers();
|
|
process.kill(process.pid, forwardedSignal);
|
|
};
|
|
const waitForForwardedSignalChildren = () => {
|
|
if (!forwardedSignal || processTreeIsAlive(forwardedSignalPids)) {
|
|
return;
|
|
}
|
|
finishForwardedSignal();
|
|
};
|
|
// Keep UI dev children in the foreground process group for native TTY
|
|
// resize/job-control behavior. Forward wrapper shutdown signals to the
|
|
// captured child tree instead of using a detached process group.
|
|
const forwardedSignals = ["SIGTERM", "SIGHUP"];
|
|
const signalHandlers = new Map(
|
|
forwardedSignals.map((signal) => [
|
|
signal,
|
|
() => {
|
|
if (!forwardedSignal) {
|
|
forwardedSignal = signal;
|
|
forwardedSignalPids = collectChildProcessTreePids(child);
|
|
signalProcessTree(child, signal, forwardedSignalPids);
|
|
forwardedSignalDrainTimer = setInterval(waitForForwardedSignalChildren, 25);
|
|
forceKillTimer = setTimeout(() => {
|
|
signalProcessTree(child, "SIGKILL", forwardedSignalPids);
|
|
}, 5_000);
|
|
forceKillTimer.unref?.();
|
|
}
|
|
},
|
|
]),
|
|
);
|
|
const cleanupSignalHandlers = () => {
|
|
for (const [signal, handler] of signalHandlers) {
|
|
process.off(signal, handler);
|
|
}
|
|
clearForwardedSignalTimers();
|
|
};
|
|
for (const [signal, handler] of signalHandlers) {
|
|
process.on(signal, handler);
|
|
}
|
|
|
|
child.on("error", (err) => {
|
|
cleanupSignalHandlers();
|
|
console.error(`Failed to launch ${label}:`, err);
|
|
process.exit(1);
|
|
});
|
|
child.on("exit", (code, signal) => {
|
|
if (forwardedSignal) {
|
|
waitForForwardedSignalChildren();
|
|
return;
|
|
}
|
|
cleanupSignalHandlers();
|
|
if (signal) {
|
|
process.kill(process.pid, signal);
|
|
return;
|
|
}
|
|
if (code !== 0) {
|
|
process.exit(code ?? 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
function collectChildProcessTreePids(child) {
|
|
if (process.platform === "win32" || typeof child.pid !== "number") {
|
|
return typeof child.pid === "number" ? [child.pid] : [];
|
|
}
|
|
const ps = spawnSync("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" });
|
|
if (ps.status !== 0) {
|
|
return [child.pid];
|
|
}
|
|
const childrenByParent = new Map();
|
|
for (const line of ps.stdout.split("\n")) {
|
|
const match = line.trim().match(/^(\d+)\s+(\d+)$/u);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
const pid = Number(match[1]);
|
|
const ppid = Number(match[2]);
|
|
const siblings = childrenByParent.get(ppid) ?? [];
|
|
siblings.push(pid);
|
|
childrenByParent.set(ppid, siblings);
|
|
}
|
|
const pids = [child.pid];
|
|
for (const parentPid of pids) {
|
|
for (const pid of childrenByParent.get(parentPid) ?? []) {
|
|
pids.push(pid);
|
|
}
|
|
}
|
|
return [...new Set(pids)];
|
|
}
|
|
|
|
function processTreeIsAlive(pids) {
|
|
return pids.some((pid) => {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (error) {
|
|
return error?.code === "EPERM";
|
|
}
|
|
});
|
|
}
|
|
|
|
function signalProcessTree(child, signal, pids) {
|
|
if (process.platform === "win32") {
|
|
child.kill(signal);
|
|
return;
|
|
}
|
|
if (pids.length === 0) {
|
|
child.kill(signal);
|
|
return;
|
|
}
|
|
for (const pid of pids.toReversed()) {
|
|
try {
|
|
process.kill(pid, signal);
|
|
} catch (error) {
|
|
if (error?.code !== "ESRCH") {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function run(cmd, args) {
|
|
runSpawnCall(resolveSpawnCall(cmd, args), cmd);
|
|
}
|
|
|
|
function runPnpm(args, envOverride) {
|
|
runSpawnCall(resolvePnpmSpawnCall(args, envOverride), "pnpm");
|
|
}
|
|
|
|
function runSpawnCallSync(spawnCall, label) {
|
|
const { command, args: spawnArgs, options } = spawnCall;
|
|
let result;
|
|
try {
|
|
result = spawnSync(command, spawnArgs, options);
|
|
} catch (err) {
|
|
console.error(`Failed to launch ${label}:`, err);
|
|
process.exit(1);
|
|
return;
|
|
}
|
|
if (result.signal) {
|
|
process.kill(process.pid, result.signal);
|
|
return;
|
|
}
|
|
if ((result.status ?? 1) !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
function runPnpmSync(args, envOverride) {
|
|
runSpawnCallSync(resolvePnpmSpawnCall(args, envOverride), "pnpm");
|
|
}
|
|
|
|
function depsInstalled(kind) {
|
|
try {
|
|
const require = createRequire(path.join(uiDir, "package.json"));
|
|
require.resolve("vite");
|
|
require.resolve("dompurify");
|
|
if (kind === "test") {
|
|
require.resolve("vitest");
|
|
require.resolve("@vitest/browser-playwright");
|
|
require.resolve("playwright");
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function resolveScriptAction(action) {
|
|
if (action === "install") {
|
|
return null;
|
|
}
|
|
if (action === "dev") {
|
|
return "dev";
|
|
}
|
|
if (action === "build") {
|
|
return "build";
|
|
}
|
|
if (action === "test") {
|
|
return "test";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function main(argv = process.argv.slice(2)) {
|
|
const [action, ...rest] = argv;
|
|
if (!action) {
|
|
usage();
|
|
process.exit(2);
|
|
}
|
|
|
|
const script = resolveScriptAction(action);
|
|
if (action !== "install" && !script) {
|
|
usage();
|
|
process.exit(2);
|
|
}
|
|
|
|
if (process.env.OPENCLAW_BUILD_ALL_NO_PNPM === "1" && action === "build") {
|
|
run(process.execPath, [path.join(repoRoot, "node_modules/vite/bin/vite.js"), "build", ...rest]);
|
|
return;
|
|
}
|
|
|
|
if (action === "install") {
|
|
runPnpm(["install", ...rest]);
|
|
return;
|
|
}
|
|
|
|
if (!depsInstalled(action === "test" ? "test" : "build")) {
|
|
const installEnv = process.env;
|
|
const installArgs = ["install"];
|
|
runPnpmSync(installArgs, installEnv);
|
|
}
|
|
|
|
runPnpm(["run", script, ...rest]);
|
|
}
|
|
|
|
export function resolveDirectExecutionPath(entry, realpath = fs.realpathSync.native) {
|
|
const resolved = path.resolve(entry);
|
|
try {
|
|
return realpath(resolved);
|
|
} catch {
|
|
return resolved;
|
|
}
|
|
}
|
|
|
|
export function isDirectScriptExecution(
|
|
entry = process.argv[1],
|
|
scriptPath = fileURLToPath(import.meta.url),
|
|
realpath = fs.realpathSync.native,
|
|
) {
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
return (
|
|
resolveDirectExecutionPath(entry, realpath) === resolveDirectExecutionPath(scriptPath, realpath)
|
|
);
|
|
}
|
|
|
|
const isDirectExecution = isDirectScriptExecution();
|
|
|
|
if (isDirectExecution) {
|
|
main();
|
|
}
|