diff --git a/openclaw.mjs b/openclaw.mjs index 099c7f6a406..432ee961fb0 100755 --- a/openclaw.mjs +++ b/openclaw.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env node +import { access } from "node:fs/promises"; import module from "node:module"; import { fileURLToPath } from "node:url"; @@ -59,7 +60,11 @@ const isDirectModuleNotFoundError = (err, specifier) => { } const message = "message" in err && typeof err.message === "string" ? err.message : ""; - return message.includes(fileURLToPath(expectedUrl)); + const expectedPath = fileURLToPath(expectedUrl); + return ( + message.includes(`Cannot find module '${expectedPath}'`) || + message.includes(`Cannot find module "${expectedPath}"`) + ); }; const installProcessWarningFilter = async () => { @@ -95,10 +100,36 @@ const tryImport = async (specifier) => { } }; +const exists = async (specifier) => { + try { + await access(new URL(specifier, import.meta.url)); + return true; + } catch { + return false; + } +}; + +const buildMissingEntryErrorMessage = async () => { + const lines = ["openclaw: missing dist/entry.(m)js (build output)."]; + if (!(await exists("./src/entry.ts"))) { + return lines.join("\n"); + } + + lines.push("This install looks like an unbuilt source tree or GitHub source archive."); + lines.push( + "Build locally with `pnpm install && pnpm build`, or install a built package instead.", + ); + lines.push( + "For pinned GitHub installs, use `npm install -g github:openclaw/openclaw#` instead of a raw `/archive/.tar.gz` URL.", + ); + lines.push("For releases, use `npm install -g openclaw@latest`."); + return lines.join("\n"); +}; + if (await tryImport("./dist/entry.js")) { // OK } else if (await tryImport("./dist/entry.mjs")) { // OK } else { - throw new Error("openclaw: missing dist/entry.(m)js (build output)."); + throw new Error(await buildMissingEntryErrorMessage()); } diff --git a/test/openclaw-launcher.e2e.test.ts b/test/openclaw-launcher.e2e.test.ts index ab9400da5db..53a6d14d8d4 100644 --- a/test/openclaw-launcher.e2e.test.ts +++ b/test/openclaw-launcher.e2e.test.ts @@ -15,6 +15,11 @@ async function makeLauncherFixture(fixtureRoots: string[]): Promise { return fixtureRoot; } +async function addSourceTreeMarker(fixtureRoot: string): Promise { + await fs.mkdir(path.join(fixtureRoot, "src"), { recursive: true }); + await fs.writeFile(path.join(fixtureRoot, "src", "entry.ts"), "export {};\n", "utf8"); +} + describe("openclaw launcher", () => { const fixtureRoots: string[] = []; @@ -55,4 +60,20 @@ describe("openclaw launcher", () => { expect(result.status).not.toBe(0); expect(result.stderr).toContain("missing dist/entry.(m)js"); }); + + it("explains how to recover from an unbuilt source install", async () => { + const fixtureRoot = await makeLauncherFixture(fixtureRoots); + await addSourceTreeMarker(fixtureRoot); + + const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], { + cwd: fixtureRoot, + encoding: "utf8", + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("missing dist/entry.(m)js"); + expect(result.stderr).toContain("unbuilt source tree or GitHub source archive"); + expect(result.stderr).toContain("pnpm install && pnpm build"); + expect(result.stderr).toContain("github:openclaw/openclaw#"); + }); });