From 49ec53f4aed15887bf36cc2135ce8ec41ae9f0b8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 20 Dec 2025 18:39:02 +0100 Subject: [PATCH] fix: detect main module under PM2 --- src/index.ts | 6 ++-- src/infra/is-main.test.ts | 38 +++++++++++++++++++++++ src/infra/is-main.ts | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/infra/is-main.test.ts create mode 100644 src/infra/is-main.ts diff --git a/src/index.ts b/src/index.ts index 24df05eac..2dbedba63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { saveSessionStore, } from "./config/sessions.js"; import { ensureBinary } from "./infra/binaries.js"; +import { isMainModule } from "./infra/is-main.js"; import { ensureClawdisCliOnPath } from "./infra/path-env.js"; import { describePortOwner, @@ -68,8 +69,9 @@ export { waitForever, }; -const isMain = - process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; +const isMain = isMainModule({ + currentFile: fileURLToPath(import.meta.url), +}); if (isMain) { // Global error handlers to prevent silent crashes from unhandled rejections/exceptions. diff --git a/src/infra/is-main.test.ts b/src/infra/is-main.test.ts new file mode 100644 index 000000000..c54a1d681 --- /dev/null +++ b/src/infra/is-main.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { isMainModule } from "./is-main.js"; + +describe("isMainModule", () => { + it("returns true when argv[1] matches current file", () => { + expect( + isMainModule({ + currentFile: "/repo/dist/index.js", + argv: ["node", "/repo/dist/index.js"], + cwd: "/repo", + env: {}, + }), + ).toBe(true); + }); + + it("returns true under PM2 when pm_exec_path matches current file", () => { + expect( + isMainModule({ + currentFile: "/repo/dist/index.js", + argv: ["node", "/pm2/lib/ProcessContainerFork.js"], + cwd: "/repo", + env: { pm_exec_path: "/repo/dist/index.js", pm_id: "0" }, + }), + ).toBe(true); + }); + + it("returns false when running under PM2 but this module is imported", () => { + expect( + isMainModule({ + currentFile: "/repo/node_modules/clawdis/dist/index.js", + argv: ["node", "/repo/app.js"], + cwd: "/repo", + env: { pm_exec_path: "/repo/app.js", pm_id: "0" }, + }), + ).toBe(false); + }); +}); diff --git a/src/infra/is-main.ts b/src/infra/is-main.ts new file mode 100644 index 000000000..0c753f019 --- /dev/null +++ b/src/infra/is-main.ts @@ -0,0 +1,63 @@ +import fs from "node:fs"; +import path from "node:path"; + +type IsMainModuleOptions = { + currentFile: string; + argv?: string[]; + env?: NodeJS.ProcessEnv; + cwd?: string; +}; + +function normalizePathCandidate( + candidate: string | undefined, + cwd: string, +): string | undefined { + if (!candidate) return undefined; + + const resolved = path.resolve(cwd, candidate); + try { + return fs.realpathSync.native(resolved); + } catch { + return resolved; + } +} + +export function isMainModule({ + currentFile, + argv = process.argv, + env = process.env, + cwd = process.cwd(), +}: IsMainModuleOptions): boolean { + const normalizedCurrent = normalizePathCandidate(currentFile, cwd); + const normalizedArgv1 = normalizePathCandidate(argv[1], cwd); + + if ( + normalizedCurrent && + normalizedArgv1 && + normalizedCurrent === normalizedArgv1 + ) { + return true; + } + + // PM2 runs the script via an internal wrapper; `argv[1]` points at the wrapper. + // PM2 exposes the actual script path in `pm_exec_path`. + const normalizedPmExecPath = normalizePathCandidate(env.pm_exec_path, cwd); + if ( + normalizedCurrent && + normalizedPmExecPath && + normalizedCurrent === normalizedPmExecPath + ) { + return true; + } + + // Fallback: basename match (relative paths, symlinked bins). + if ( + normalizedCurrent && + normalizedArgv1 && + path.basename(normalizedCurrent) === path.basename(normalizedArgv1) + ) { + return true; + } + + return false; +}