From aa3a8ea86914ba8d6a6f74df3dc9e27946bc7720 Mon Sep 17 00:00:00 2001 From: Yurii Chukhlib Date: Fri, 30 Jan 2026 21:59:27 +0100 Subject: [PATCH] fix(infra): resolve control-ui assets on npm global install (#4855) --- src/infra/control-ui-assets.ts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/infra/control-ui-assets.ts b/src/infra/control-ui-assets.ts index bd705ebf3..e60b14443 100644 --- a/src/infra/control-ui-assets.ts +++ b/src/infra/control-ui-assets.ts @@ -37,9 +37,39 @@ export function resolveControlUiDistIndexPath( ): string | null { if (!argv1) return null; const normalized = path.resolve(argv1); + + // Case 1: entrypoint is directly inside dist/ (e.g., dist/entry.js) const distDir = path.dirname(normalized); - if (path.basename(distDir) !== "dist") return null; - return path.join(distDir, "control-ui", "index.html"); + if (path.basename(distDir) === "dist") { + return path.join(distDir, "control-ui", "index.html"); + } + + // Case 2: npm global install - entrypoint is at package root (e.g., openclaw.mjs) + // or in node_modules/.bin/. Walk up to find package.json with dist/control-ui/ + const parts = normalized.split(path.sep); + + // Handle .bin symlink: node_modules/.bin/openclaw -> node_modules/openclaw/... + const binIndex = parts.lastIndexOf(".bin"); + if (binIndex > 0 && parts[binIndex - 1] === "node_modules") { + const binName = path.basename(normalized); + const nodeModulesDir = parts.slice(0, binIndex).join(path.sep); + const pkgPath = path.join(nodeModulesDir, binName, "dist", "control-ui", "index.html"); + if (fs.existsSync(pkgPath)) return pkgPath; + } + + // Walk up from entrypoint looking for package with dist/control-ui/ + let dir = path.dirname(normalized); + for (let i = 0; i < 8; i++) { + const candidate = path.join(dir, "dist", "control-ui", "index.html"); + if (fs.existsSync(path.join(dir, "package.json")) && fs.existsSync(candidate)) { + return candidate; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + + return null; } export type EnsureControlUiAssetsResult = {