diff --git a/src/entry.ts b/src/entry.ts index ba4dcaaa0287..e42bfb60aa94 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -7,7 +7,6 @@ import { format } from "node:util"; import { resolveCliArgvInvocation } from "./cli/argv-invocation.js"; import { isRootHelpInvocation } from "./cli/argv.js"; import { parseCliContainerArgs, resolveCliContainerTarget } from "./cli/container-target.js"; -import { requestExitAfterOneShotOutput, runCliWithExitFinalization } from "./cli/one-shot-exit.js"; import { tryOutputPrecomputedCommandHelp, type PrecomputedCommandHelpDeps, @@ -19,7 +18,6 @@ import { isUpdateAdmissionInvocation, tryRunUpdateAdmissionBeforeStartup, } from "./cli/run-main-update-admission.js"; -import { withCliProcessScope } from "./cli/runtime-cleanup-scope.js"; import { configureGatewayStartupTraceConsoleFormatting, createGatewayDispatchStartupTrace, @@ -240,8 +238,10 @@ if ( gatewayEntryStartupTrace.mark("argv"); if (!tryHandleRootVersionFastPath(process.argv)) { - const run = (finalize?: () => Promise) => - withCliProcessScope(() => runMainOrRootHelp(process.argv, { finalize })); + const run = async (finalize?: () => Promise) => { + const { withCliProcessScope } = await import("./cli/runtime-cleanup-scope.js"); + return withCliProcessScope(() => runMainOrRootHelp(process.argv, { finalize })); + }; const managedNodeStatePath = getManagedNodeHostStatePath(); if (managedNodeStatePath) { const { withExistingOpenClawStateSchema } = @@ -371,6 +371,8 @@ export async function runMainOrRootHelp( // mode so the envelope is written here. Only failures before runCli are startup failures. let commandStarted = false; let failureHandler: Awaited> | undefined; + const { runCliWithExitFinalization, requestExitAfterOneShotOutput } = + await import("./cli/one-shot-exit.js"); await runCliWithExitFinalization({ finalize: deps.finalize, run: async () => { diff --git a/src/index.entrypoint.test.ts b/src/index.entrypoint.test.ts index 2d7ddf01d4f1..7d3f5c0e3e4a 100644 --- a/src/index.entrypoint.test.ts +++ b/src/index.entrypoint.test.ts @@ -13,9 +13,19 @@ vi.mock("node:fs", async (importOriginal) => ({ vi.mock("./cli/run-main.js", () => ({ runCli: vi.fn(async () => undefined), })); +const lifecycleImports = vi.hoisted(() => ({ failureOutput: vi.fn() })); + vi.mock("./cli/one-shot-exit.js", () => ({ runCliWithExitFinalization: vi.fn(), })); +vi.mock("./cli/failure-output.js", () => { + lifecycleImports.failureOutput(); + return { + formatCliFailureLines: vi.fn(() => []), + formatCliJsonFailure: vi.fn(), + isExpectedCliError: vi.fn(() => false), + }; +}); vi.mock("./entry.version-fast-path.js", () => ({ tryHandleRootVersionFastPath: vi.fn(() => false), })); @@ -25,6 +35,26 @@ vi.mock("./infra/is-main.js", () => ({ vi.mock("./infra/package-lifecycle.js", () => ({ completePendingPackageLifecycle: vi.fn(async () => true), })); +vi.mock("./library.js", () => ({ + applyTemplate: vi.fn(), + createDefaultDeps: vi.fn(), + deriveSessionKey: vi.fn(), + describePortOwner: vi.fn(), + ensureBinary: vi.fn(), + ensurePortAvailable: vi.fn(), + getReplyFromConfig: vi.fn(), + handlePortError: vi.fn(), + loadConfig: vi.fn(), + monitorWebChannel: vi.fn(), + normalizeE164: vi.fn(), + PortInUseError: class PortInUseError extends Error {}, + promptYesNo: vi.fn(), + resolveSessionKey: vi.fn(), + resolveStorePath: vi.fn(), + runCommandWithTimeout: vi.fn(), + runExec: vi.fn(), + waitForever: vi.fn(), +})); const originalArgv = process.argv; const originalExitCode = process.exitCode; @@ -37,6 +67,7 @@ describe("legacy package executable entrypoint", () => { vi.mocked(tryHandleRootVersionFastPath).mockReturnValue(false); vi.mocked(existsSync).mockReturnValue(false); vi.mocked(completePendingPackageLifecycle).mockResolvedValue(true); + lifecycleImports.failureOutput.mockClear(); process.argv = ["node", "dist/index.js", "status"]; }); @@ -58,6 +89,24 @@ describe("legacy package executable entrypoint", () => { expect(tryHandleRootVersionFastPath).toHaveBeenCalledWith(process.argv); expect(runMain.runCli).not.toHaveBeenCalled(); expect(exitFinalization.runCliWithExitFinalization).not.toHaveBeenCalled(); + expect(lifecycleImports.failureOutput).not.toHaveBeenCalled(); + }); + + it("loads CLI failure modules only after the version fast path declines", async () => { + process.argv = ["node", "dist/index.js", "status"]; + + await import("./index.js?legacy-cli-start" as "./index.js"); + + expect(lifecycleImports.failureOutput).toHaveBeenCalledOnce(); + }); + + it("keeps library imports free of CLI failure modules", async () => { + vi.mocked(isMainModule).mockReturnValue(false); + + const entry = await import("./index.js?legacy-library-entry" as "./index.js"); + + expect(typeof entry.loadConfig).toBe("function"); + expect(lifecycleImports.failureOutput).not.toHaveBeenCalled(); }); it("completes pending lifecycle before loading the CLI entry graph", async () => { diff --git a/src/index.ts b/src/index.ts index 70bf8d82c893..ca10a937326c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import process from "node:process"; import { fileURLToPath } from "node:url"; import { resolveCliArgvInvocation } from "./cli/argv-invocation.js"; import { tryRunUpdateAdmissionBeforeStartup } from "./cli/run-main-update-admission.js"; +import { tryHandleRootVersionFastPath } from "./entry.version-fast-path.js"; import { isMainModule } from "./infra/is-main.js"; const isMain = isMainModule({ @@ -31,27 +32,8 @@ if ( } } -const [ - { formatCliFailureLines, formatCliJsonFailure, isExpectedCliError }, - { isJsonOutputModeActive }, - { runCliWithExitFinalization }, - { withCliProcessScope }, - { installDistEsmResolveFastPath }, - { tryHandleRootVersionFastPath }, - { formatUncaughtError }, - { runFatalErrorHooks }, - { installUnhandledRejectionHandler, isBenignUncaughtExceptionError, isUncaughtExceptionHandled }, -] = await Promise.all([ - import("./cli/failure-output.js"), - import("./cli/json-output-mode.js"), - import("./cli/one-shot-exit.js"), - import("./cli/runtime-cleanup-scope.js"), - import("./entry.esm-resolve-fast-path.js"), - import("./entry.version-fast-path.js"), - import("./infra/errors.js"), - import("./infra/fatal-error-hooks.js"), - import("./infra/unhandled-rejections.js"), -]); +const handledRootVersion = + isMain && !handledAdmission && tryHandleRootVersionFastPath(process.argv); type LegacyCliDeps = { runCli: ( @@ -102,12 +84,6 @@ export async function runLegacyCliEntry( await runCli(argv, options); } -if (isMain && !handledAdmission) { - installDistEsmResolveFastPath(import.meta.url); -} -const handledRootVersion = - isMain && !handledAdmission && tryHandleRootVersionFastPath(process.argv); - if (!isMain) { ({ applyTemplate, @@ -132,6 +108,31 @@ if (!isMain) { } if (isMain && !handledRootVersion && !handledAdmission) { + const [ + { formatCliFailureLines, formatCliJsonFailure, isExpectedCliError }, + { isJsonOutputModeActive }, + { runCliWithExitFinalization }, + { withCliProcessScope }, + { installDistEsmResolveFastPath: installFastPath }, + { formatUncaughtError }, + { runFatalErrorHooks }, + { + installUnhandledRejectionHandler, + isBenignUncaughtExceptionError, + isUncaughtExceptionHandled, + }, + ] = await Promise.all([ + import("./cli/failure-output.js"), + import("./cli/json-output-mode.js"), + import("./cli/one-shot-exit.js"), + import("./cli/runtime-cleanup-scope.js"), + import("./entry.esm-resolve-fast-path.js"), + import("./infra/errors.js"), + import("./infra/fatal-error-hooks.js"), + import("./infra/unhandled-rejections.js"), + ]); + installFastPath(import.meta.url); + const { defaultRuntime, restoreRuntimeTerminalState } = await import("./runtime.js"); // Global error handlers to prevent silent crashes from unhandled rejections/exceptions.