From 061cb226e22a11bd24f02c60a803045ffc28701c Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sun, 11 Oct 2026 03:58:58 -0700 Subject: [PATCH] refactor(orcad): shared serve-option validation; no daemon barrel (#27443) orcad now checks its flags with the same validator Electron serve and the CLI use, so contradictory pairing flags fail with the same message instead of one silently winning. The CLI launcher drops its duplicate recipe check (the CLI handler already ran the shared validator). Removes the daemon-init and cli/runtime/index re-export barrels and the parsePtySessionId / OrcadInstanceLockError re-exports; callers and mocks import the defining modules. Co-authored-by: m4air Co-authored-by: Claude Opus 5.5 --- .../scripts/verify-packaged-daemon-entry.cjs | 2 +- src/cli/runtime-client.ts | 13 +++---- src/cli/runtime/index.ts | 10 ------ src/cli/runtime/launch.ts | 3 -- src/main/daemon/daemon-init-fresh-import.ts | 8 ++++- .../daemon-init-wedged-daemon-grace.test.ts | 2 +- src/main/daemon/daemon-init.ts | 23 ------------ .../daemon/daemon-pty-process-inspection.ts | 2 +- .../daemon/daemon-pty-session-inventory.ts | 2 +- src/main/daemon/pty-session-id.test.ts | 2 +- src/main/daemon/pty-session-id.ts | 6 ---- .../foreground-process-tracker.ts | 2 +- src/main/daemon/wsl-session-context.ts | 2 +- src/main/ipc/pty-management.test.ts | 6 ++-- src/main/ipc/pty-management.ts | 6 ++-- .../local-builds/local-build-compatibility.ts | 2 +- ...cal-pty-registry-folder-workspaces.test.ts | 2 +- .../memory/hydrate-local-pty-registry.test.ts | 2 +- src/main/memory/hydrate-local-pty-registry.ts | 2 +- src/main/orcad/orcad-command-arguments.ts | 15 +++++--- .../orcad/orcad-daemon-retirement.test.ts | 2 +- src/main/orcad/orcad-daemon-retirement.ts | 2 +- .../orcad/orcad-daemon-supervision.test.ts | 6 ++-- src/main/orcad/orcad-daemon-supervision.ts | 4 +-- src/main/orcad/orcad-entry.ts | 2 +- src/main/orcad/orcad-exit-code.ts | 2 +- src/main/orcad/orcad-health.test.ts | 2 +- src/main/orcad/orcad-health.ts | 2 +- src/main/orcad/orcad-instance-lock.test.ts | 2 +- src/main/orcad/orcad-instance-lock.ts | 2 -- src/main/orcad/orcad-launch-contract.test.ts | 35 +++++++++++-------- .../orcad/orcad-managed-idle-exit-host.ts | 2 +- src/main/orcad/orcad-push-startup.test.ts | 2 +- .../desktop-profile-instance-lock.test.ts | 3 +- .../startup/desktop-profile-instance-lock.ts | 7 ++-- src/main/startup/main-process-pty-startup.ts | 7 ++-- .../main-process-quit-pending-writer.test.ts | 2 +- .../main-process-quit-update-veto.test.ts | 2 +- src/main/startup/main-process-quit.ts | 2 +- .../startup/main-process-runtime-service.ts | 2 +- src/shared/daemon-lifecycle-telemetry.ts | 2 +- .../daemon-generation-desktop-discovery.ts | 2 +- .../tools/win-update-e2e/daemon-processes.mjs | 2 +- 43 files changed, 88 insertions(+), 120 deletions(-) delete mode 100644 src/cli/runtime/index.ts delete mode 100644 src/main/daemon/daemon-init.ts diff --git a/config/scripts/verify-packaged-daemon-entry.cjs b/config/scripts/verify-packaged-daemon-entry.cjs index 8f5e802825c..006f5715012 100644 --- a/config/scripts/verify-packaged-daemon-entry.cjs +++ b/config/scripts/verify-packaged-daemon-entry.cjs @@ -4,7 +4,7 @@ const { join } = require('node:path') // Why: `asarUnpack` in config/electron-builder.config.cjs lists // out/main/daemon-entry.js on every platform, and the packaged daemon fork -// (src/main/daemon/daemon-init.ts) resolves exactly this unpacked path. A +// (src/main/daemon/daemon-launch-paths.ts) resolves exactly this unpacked path. A // missing entry means the package layout regressed, so the check throws // instead of skipping — a silent skip false-passed exactly the layout bug // this gate exists to catch. diff --git a/src/cli/runtime-client.ts b/src/cli/runtime-client.ts index f88b116981b..981759a005a 100644 --- a/src/cli/runtime-client.ts +++ b/src/cli/runtime-client.ts @@ -1,14 +1,11 @@ -// Why: the runtime client used to live here as a single file. It was split -// into ./runtime/{types,metadata,transport,status,launch,client}.ts so each -// concern can be tested in isolation. This barrel preserves the original -// import surface so call sites (src/cli/index.ts, tests) remain unchanged. +// Why: one import surface (and one vi.mock target) for CLI handlers over ./runtime/*. +export { RuntimeClient } from './runtime/client' +export { serveOrcaApp } from './runtime/launch' +export { getDefaultUserDataPath } from './runtime/metadata' export { - RuntimeClient, RuntimeClientError, RuntimeRpcFailureError, - serveOrcaApp, - getDefaultUserDataPath, type RuntimeRpcFailure, type RuntimeRpcResponse, type RuntimeRpcSuccess -} from './runtime/index' +} from './runtime/types' diff --git a/src/cli/runtime/index.ts b/src/cli/runtime/index.ts deleted file mode 100644 index 81d9fdf16c6..00000000000 --- a/src/cli/runtime/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { RuntimeClient } from './client' -export { serveOrcaApp } from './launch' -export { getDefaultUserDataPath } from './metadata' -export { - RuntimeClientError, - RuntimeRpcFailureError, - type RuntimeRpcFailure, - type RuntimeRpcResponse, - type RuntimeRpcSuccess -} from './types' diff --git a/src/cli/runtime/launch.ts b/src/cli/runtime/launch.ts index a4d24de8bd2..0b88807f827 100644 --- a/src/cli/runtime/launch.ts +++ b/src/cli/runtime/launch.ts @@ -108,9 +108,6 @@ function spawnDetached(command: string, args: string[], options: SpawnOptions): export function serveOrcaApp(args: ServeOrcaAppArgs = {}): Promise { const executable = resolveForegroundOrcaExecutable() - if (args.recipeJson && !args.projectRoot) { - throw new RuntimeClientError('invalid_argument', 'Recipe JSON output requires --project-root.') - } // Why one value: the selector, orcad and Electron must all serve the profile the caller chose. const userDataPath = resolveLaunchUserDataPath() // Why synchronous on the opt-out: it must spawn Electron exactly as before, without asking. diff --git a/src/main/daemon/daemon-init-fresh-import.ts b/src/main/daemon/daemon-init-fresh-import.ts index 39f3625c063..d63815e88ee 100644 --- a/src/main/daemon/daemon-init-fresh-import.ts +++ b/src/main/daemon/daemon-init-fresh-import.ts @@ -125,5 +125,11 @@ export async function importFreshDaemonInit(state: DaemonInitMockState) { getAppMetrics: () => [] } as unknown as AppEnvironment) // Why: import after resetModules so module-level spawner/adapter/restartInFlight start fresh — needed to test first-init and the coalescer. - return import('./daemon-init') + const [init, restart, providerState, cleanup] = await Promise.all([ + import('./daemon-provider-init'), + import('./daemon-provider-restart'), + import('./daemon-provider-state'), + import('./daemon-protocol-cleanup') + ]) + return { ...init, ...restart, ...providerState, ...cleanup } } diff --git a/src/main/daemon/daemon-init-wedged-daemon-grace.test.ts b/src/main/daemon/daemon-init-wedged-daemon-grace.test.ts index 9cd55946112..3c0f9ef8e43 100644 --- a/src/main/daemon/daemon-init-wedged-daemon-grace.test.ts +++ b/src/main/daemon/daemon-init-wedged-daemon-grace.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { WEDGED_DAEMON_GRACE_RETRIES } from './daemon-init' +import { WEDGED_DAEMON_GRACE_RETRIES } from './daemon-replacement-preflight' import { FAKE_RUNTIME_DIR } from './daemon-init-test-harness' import { DAEMON_RECOVERY_BUDGET_MS, diff --git a/src/main/daemon/daemon-init.ts b/src/main/daemon/daemon-init.ts deleted file mode 100644 index 6f0bd4eefc5..00000000000 --- a/src/main/daemon/daemon-init.ts +++ /dev/null @@ -1,23 +0,0 @@ -export { createLegacyDaemonAdapters } from './daemon-legacy-adapters' -export { initDaemonPtyProvider } from './daemon-provider-init' -export { restartDaemon, type RestartDaemonResult } from './daemon-provider-restart' -export { - daemonOwnsFreshPersistentPtys, - disconnectDaemon, - getCurrentDaemonMacTccAttributionHealth, - getDaemonEndpointFacts, - getDaemonProvider, - listLiveDaemonPtyIds, - listLiveDaemonSessions, - requestIdleDaemonRetirement, - releaseDaemonRetirementFence, - readDaemonPidRecord, - replaceDaemonProvider, - shutdownDaemon, - type DaemonEndpointFacts -} from './daemon-provider-state' -export { - cleanupDaemonForProtocol, - type OrphanedDaemonCleanupResult -} from './daemon-protocol-cleanup' -export { WEDGED_DAEMON_GRACE_RETRIES } from './daemon-replacement-preflight' diff --git a/src/main/daemon/daemon-pty-process-inspection.ts b/src/main/daemon/daemon-pty-process-inspection.ts index 335c641da62..0c10a2a2c85 100644 --- a/src/main/daemon/daemon-pty-process-inspection.ts +++ b/src/main/daemon/daemon-pty-process-inspection.ts @@ -1,6 +1,6 @@ import { isShellProcess } from '../../shared/agent-detection' import { DaemonPtyBufferSnapshots } from './daemon-pty-buffer-snapshots' -import { parsePtySessionId } from './pty-session-id' +import { parsePtySessionId } from '../../shared/pty-session-id-format' import { COMPLETION_PROCESS_INSPECTION_PROTOCOL_VERSION, GET_FOREGROUND_PROCESS_PROTOCOL_VERSION, diff --git a/src/main/daemon/daemon-pty-session-inventory.ts b/src/main/daemon/daemon-pty-session-inventory.ts index 39d0bdafcce..7f1a4f6cc47 100644 --- a/src/main/daemon/daemon-pty-session-inventory.ts +++ b/src/main/daemon/daemon-pty-session-inventory.ts @@ -11,7 +11,7 @@ import { recordAuthenticatedInventory } from './daemon-audit-classifier' import { isMissingWindowsNamedPipeError } from './daemon-endpoint-errors' import { DaemonPtyProcessInspection } from './daemon-pty-process-inspection' import { remainingDaemonRequestTimeoutMs } from './daemon-request-deadline' -import { parsePtySessionId } from './pty-session-id' +import { parsePtySessionId } from '../../shared/pty-session-id-format' import type { ListSessionsResult, SessionInfo } from './types' import { PtyProcessListAdmission } from '../providers/pty-process-list-admission' import type { PtyProcessInfo } from '../providers/types' diff --git a/src/main/daemon/pty-session-id.test.ts b/src/main/daemon/pty-session-id.test.ts index c8e7077c8a0..6d8f7c1b8ea 100644 --- a/src/main/daemon/pty-session-id.test.ts +++ b/src/main/daemon/pty-session-id.test.ts @@ -2,9 +2,9 @@ import { describe, expect, it } from 'vitest' import { isSafePtySessionId, mintPtySessionId, - parsePtySessionId, ptySessionIdForAgentCreateOperation } from './pty-session-id' +import { parsePtySessionId } from '../../shared/pty-session-id-format' const USER_DATA = '/tmp/orca-userdata' diff --git a/src/main/daemon/pty-session-id.ts b/src/main/daemon/pty-session-id.ts index 69324104af6..dab9c6ab3c3 100644 --- a/src/main/daemon/pty-session-id.ts +++ b/src/main/daemon/pty-session-id.ts @@ -2,12 +2,6 @@ import { randomUUID } from 'node:crypto' import { isAbsolute, join, relative, resolve, sep } from 'node:path' import { PTY_SESSION_ID_SEPARATOR } from '../../shared/pty-session-id-format' -// Why: re-exported here so main-side callers can keep importing -// `parsePtySessionId` from this module (next to `mintPtySessionId`). The -// implementation lives in `src/shared/` because the renderer-side merge -// helper also needs it and cannot import node-only modules. -export { parsePtySessionId } from '../../shared/pty-session-id-format' - /** * Session IDs use the format `${worktreeId}@@${shortUuid}` so that * DaemonPtyAdapter.reconcileOnStartup (see daemon-pty-adapter.ts) can diff --git a/src/main/daemon/pty-subprocess/foreground-process-tracker.ts b/src/main/daemon/pty-subprocess/foreground-process-tracker.ts index 7a96877c204..1a7277903c3 100644 --- a/src/main/daemon/pty-subprocess/foreground-process-tracker.ts +++ b/src/main/daemon/pty-subprocess/foreground-process-tracker.ts @@ -23,7 +23,7 @@ import { } from '../../../shared/foreground-wrapper-agent' import { isShellProcess } from '../../../shared/shell-process-detection' import { resolveFallbackForegroundProcess } from './foreground-fallback-process' -import { parsePtySessionId } from '../pty-session-id' +import { parsePtySessionId } from '../../../shared/pty-session-id-format' import { ptyProcessNameIsSpawnFile, createPtyForegroundResolver, diff --git a/src/main/daemon/wsl-session-context.ts b/src/main/daemon/wsl-session-context.ts index 0cf419872f9..a594d6d9344 100644 --- a/src/main/daemon/wsl-session-context.ts +++ b/src/main/daemon/wsl-session-context.ts @@ -1,7 +1,7 @@ import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { isWslShellName } from '../../shared/local-windows-terminal-runtime' import { getDefaultWslDistro, parseWslPath } from '../wsl' -import { parsePtySessionId } from './pty-session-id' +import { parsePtySessionId } from '../../shared/pty-session-id-format' import { parseWslUncPath } from '../../shared/wsl-paths' export type WslSessionContext = { diff --git a/src/main/ipc/pty-management.test.ts b/src/main/ipc/pty-management.test.ts index 0e46f2ab6bb..17787a3c5a0 100644 --- a/src/main/ipc/pty-management.test.ts +++ b/src/main/ipc/pty-management.test.ts @@ -45,9 +45,11 @@ vi.mock('../daemon/daemon-folder-access-reset', () => ({ resetFolderAccessForDaemon: resetFolderAccessForDaemonMock })) -vi.mock('../daemon/daemon-init', () => ({ +vi.mock('../daemon/daemon-provider-restart', () => ({ + restartDaemon: restartDaemonMock +})) +vi.mock('../daemon/daemon-provider-state', () => ({ getDaemonProvider: getDaemonProviderMock, - restartDaemon: restartDaemonMock, getCurrentDaemonMacTccAttributionHealth: getCurrentDaemonMacTccAttributionHealthMock })) diff --git a/src/main/ipc/pty-management.ts b/src/main/ipc/pty-management.ts index 2503ced8bbd..871545d8adf 100644 --- a/src/main/ipc/pty-management.ts +++ b/src/main/ipc/pty-management.ts @@ -3,9 +3,9 @@ import { DegradedDaemonPtyProvider } from '../daemon/degraded-daemon-pty-provide import type { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' import { getCurrentDaemonMacTccAttributionHealth, - getDaemonProvider, - restartDaemon -} from '../daemon/daemon-init' + getDaemonProvider +} from '../daemon/daemon-provider-state' +import { restartDaemon } from '../daemon/daemon-provider-restart' import { getAllDaemonAdapters, getCurrentDaemonAdapter } from '../daemon/daemon-provider-routing' import { getDaemonFolderAccessMismatch, diff --git a/src/main/local-builds/local-build-compatibility.ts b/src/main/local-builds/local-build-compatibility.ts index 482d7a4f643..f36fcf4678c 100644 --- a/src/main/local-builds/local-build-compatibility.ts +++ b/src/main/local-builds/local-build-compatibility.ts @@ -4,7 +4,7 @@ import { type LocalBuildCompatibility } from '../../shared/local-build-compatibility' import { DegradedDaemonPtyProvider } from '../daemon/degraded-daemon-pty-provider' -import { getDaemonProvider } from '../daemon/daemon-init' +import { getDaemonProvider } from '../daemon/daemon-provider-state' import { getAllDaemonAdapters } from '../daemon/daemon-provider-routing' import { getLocalPtyProvider } from '../ipc/pty' import { LocalPtyProvider } from '../providers/local-pty-provider' diff --git a/src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts b/src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts index c67e4772bee..2464817783d 100644 --- a/src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts +++ b/src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts @@ -4,7 +4,7 @@ import type { SessionInfo } from '../daemon/types' import type { Store } from '../persistence' const getDaemonProviderMock = vi.fn() -vi.mock('../daemon/daemon-init', () => ({ +vi.mock('../daemon/daemon-provider-state', () => ({ getDaemonProvider: () => getDaemonProviderMock() })) vi.mock('../project-runtime-git-options', () => ({ diff --git a/src/main/memory/hydrate-local-pty-registry.test.ts b/src/main/memory/hydrate-local-pty-registry.test.ts index a9e8e312a75..02094b17ac3 100644 --- a/src/main/memory/hydrate-local-pty-registry.test.ts +++ b/src/main/memory/hydrate-local-pty-registry.test.ts @@ -29,7 +29,7 @@ const LARGE_SESSION_COUNT = 150_000 // getter. Stubbing it lets us drive the offline / throwing / live paths // without spinning up real sockets. const getDaemonProviderMock = vi.fn() -vi.mock('../daemon/daemon-init', () => ({ +vi.mock('../daemon/daemon-provider-state', () => ({ getDaemonProvider: () => getDaemonProviderMock() })) diff --git a/src/main/memory/hydrate-local-pty-registry.ts b/src/main/memory/hydrate-local-pty-registry.ts index fa1c176728a..9e4e97bceb2 100644 --- a/src/main/memory/hydrate-local-pty-registry.ts +++ b/src/main/memory/hydrate-local-pty-registry.ts @@ -5,7 +5,7 @@ import { parsePtySessionId } from '../../shared/pty-session-id-format' import { isFolderRepo } from '../../shared/repo-kind' import type { Repo } from '../../shared/repo-types' import { splitWorktreeId, worktreeIdComparisonKey } from '../../shared/worktree/id' -import { getDaemonProvider } from '../daemon/daemon-init' +import { getDaemonProvider } from '../daemon/daemon-provider-state' import { getAllDaemonAdapters, type DaemonProvider } from '../daemon/daemon-provider-routing' import type { SessionInfo } from '../daemon/types' import { isFolderWorkspaceIdForRepo } from '../ipc/worktrees/folder-workspace-model' diff --git a/src/main/orcad/orcad-command-arguments.ts b/src/main/orcad/orcad-command-arguments.ts index 03e4eb0b128..2f9ca1d4519 100644 --- a/src/main/orcad/orcad-command-arguments.ts +++ b/src/main/orcad/orcad-command-arguments.ts @@ -1,6 +1,7 @@ import type { OrcadOptions } from './orcad-entry' import { DEFAULT_WS_PORT } from '../runtime/runtime-rpc/runtime-rpc-pairing-types' import { ORCAD_LOOPBACK_BIND_HOST } from './orcad-bind-address' +import { getServeOptionValidationError } from '../../shared/serve-option-validation' export const ORCAD_USAGE = `orcad — the Orca runtime served without Electron. @@ -82,11 +83,15 @@ export function parseArgs(argv: string[]): OrcadCommandArguments { throw new Error(`Unknown argument: ${arg}`) } } - if (options.grantDesktopControl && (options.noPairing || options.mobilePairing)) { - throw new Error('--grant-desktop-control applies only to the default runtime pairing offer') - } - if (options.recipeJson && !options.projectRoot) { - throw new Error('--recipe-json requires --project-root') + const validationError = getServeOptionValidationError({ + noPairing: options.noPairing === true, + mobilePairing: options.mobilePairing === true, + grantDesktopControl: options.grantDesktopControl, + recipeJson: options.recipeJson === true, + projectRoot: options.projectRoot + }) + if (validationError) { + throw new Error(validationError) } return options } diff --git a/src/main/orcad/orcad-daemon-retirement.test.ts b/src/main/orcad/orcad-daemon-retirement.test.ts index 3c952cf9b6a..5414f4000c0 100644 --- a/src/main/orcad/orcad-daemon-retirement.test.ts +++ b/src/main/orcad/orcad-daemon-retirement.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -vi.mock('../daemon/daemon-init', () => ({ +vi.mock('../daemon/daemon-provider-state', () => ({ requestIdleDaemonRetirement: vi.fn(), listLiveDaemonSessions: vi.fn(), releaseDaemonRetirementFence: vi.fn() diff --git a/src/main/orcad/orcad-daemon-retirement.ts b/src/main/orcad/orcad-daemon-retirement.ts index 0cc73c99e52..70cc62745d9 100644 --- a/src/main/orcad/orcad-daemon-retirement.ts +++ b/src/main/orcad/orcad-daemon-retirement.ts @@ -9,7 +9,7 @@ import { listLiveDaemonSessions, releaseDaemonRetirementFence, requestIdleDaemonRetirement -} from '../daemon/daemon-init' +} from '../daemon/daemon-provider-state' import type { DaemonIdleRetirementResult } from '../daemon/daemon-pty-runtime-state' import type { OrcadDaemonRetirementVerdict } from '../../shared/orcad-stop-request' import { withTimeout } from '../../shared/promise-timeout-fallback' diff --git a/src/main/orcad/orcad-daemon-supervision.test.ts b/src/main/orcad/orcad-daemon-supervision.test.ts index d301b715a9e..562717c0783 100644 --- a/src/main/orcad/orcad-daemon-supervision.test.ts +++ b/src/main/orcad/orcad-daemon-supervision.test.ts @@ -14,8 +14,10 @@ const { readDaemonPidRecordMock: vi.fn<() => { pid: number } | null>() })) -vi.mock('../daemon/daemon-init', () => ({ - initDaemonPtyProvider: initDaemonPtyProviderMock, +vi.mock('../daemon/daemon-provider-init', () => ({ + initDaemonPtyProvider: initDaemonPtyProviderMock +})) +vi.mock('../daemon/daemon-provider-state', () => ({ disconnectDaemon: disconnectDaemonMock, // Exported here purely so the test can prove it is never reached — killing the daemon on // orcad shutdown is what would make an orcad restart destructive again. diff --git a/src/main/orcad/orcad-daemon-supervision.ts b/src/main/orcad/orcad-daemon-supervision.ts index 79ec4f3ed28..6a87db81f70 100644 --- a/src/main/orcad/orcad-daemon-supervision.ts +++ b/src/main/orcad/orcad-daemon-supervision.ts @@ -14,9 +14,9 @@ import { disconnectDaemon, daemonOwnsFreshPersistentPtys, - initDaemonPtyProvider, readDaemonPidRecord -} from '../daemon/daemon-init' +} from '../daemon/daemon-provider-state' +import { initDaemonPtyProvider } from '../daemon/daemon-provider-init' import { errorMessage } from '../../shared/error-message' const WINDOWS_DAEMON_STARTUP_TIMEOUT_MS = 30_000 diff --git a/src/main/orcad/orcad-entry.ts b/src/main/orcad/orcad-entry.ts index 5d8eb854b7c..9a04f531731 100644 --- a/src/main/orcad/orcad-entry.ts +++ b/src/main/orcad/orcad-entry.ts @@ -169,7 +169,7 @@ async function startOrcadRuntime( const { buildOrcadServeReadiness } = await import('./orcad-serve-readiness') const { createOrcadProfileStateStartup } = await import('./orcad-profile-state-startup') const { startOrcadDaemon, stopOrcadDaemon } = await import('./orcad-daemon-supervision') - const { daemonOwnsFreshPersistentPtys } = await import('../daemon/daemon-init') + const { daemonOwnsFreshPersistentPtys } = await import('../daemon/daemon-provider-state') const { collectOrcadHealth } = await import('./orcad-health') // Why importable here: the singleton's module tree never reaches Electron, and orcad supplies // its persistence and endpoint paths explicitly below. diff --git a/src/main/orcad/orcad-exit-code.ts b/src/main/orcad/orcad-exit-code.ts index 6ae468fe936..8259c5f7f5f 100644 --- a/src/main/orcad/orcad-exit-code.ts +++ b/src/main/orcad/orcad-exit-code.ts @@ -1,6 +1,6 @@ import { OrcadBindAddressError } from './orcad-bind-address' import { OrcadBundledRuntimeError } from './orcad-bundled-runtime' -import { OrcadInstanceLockError } from './orcad-instance-lock' +import { OrcadInstanceLockError } from './orcad-data-root-privacy' import { ProfileStateAccessError } from '../persistence/profile-state/profile-state-access' import { ORCAD_EXIT_CONFIGURATION } from './orcad-configuration-exit' diff --git a/src/main/orcad/orcad-health.test.ts b/src/main/orcad/orcad-health.test.ts index 4f75c7c9025..dd581a02b56 100644 --- a/src/main/orcad/orcad-health.test.ts +++ b/src/main/orcad/orcad-health.test.ts @@ -26,7 +26,7 @@ vi.mock('../daemon/daemon-health', () => ({ reportedCoverage.value.shift() ?? (process.platform === 'win32' ? 'handshake' : 'pty-spawn') }) })) -vi.mock('../daemon/daemon-init', () => ({ +vi.mock('../daemon/daemon-provider-state', () => ({ getDaemonEndpointFacts: getDaemonEndpointFactsMock, readDaemonPidRecord: readDaemonPidRecordMock, daemonOwnsFreshPersistentPtys: daemonOwnsFreshPersistentPtysMock diff --git a/src/main/orcad/orcad-health.ts b/src/main/orcad/orcad-health.ts index d8cf2f705a2..4f891395f6e 100644 --- a/src/main/orcad/orcad-health.ts +++ b/src/main/orcad/orcad-health.ts @@ -16,7 +16,7 @@ import { daemonOwnsFreshPersistentPtys, getDaemonEndpointFacts, readDaemonPidRecord -} from '../daemon/daemon-init' +} from '../daemon/daemon-provider-state' import type { OrcadProfileStateAuthoritySelection } from './orcad-profile-state-telemetry' import { ORCAD_STOP_REQUESTS_CAPABILITY } from '../../shared/orcad-stop-request' import type { OrcadIdleStopRecord } from '../../shared/orcad-idle-exit' diff --git a/src/main/orcad/orcad-instance-lock.test.ts b/src/main/orcad/orcad-instance-lock.test.ts index 36dc3700f23..0d97ccbdaf8 100644 --- a/src/main/orcad/orcad-instance-lock.test.ts +++ b/src/main/orcad/orcad-instance-lock.test.ts @@ -16,10 +16,10 @@ import { afterEach, describe, expect, it } from 'vitest' import { acquireOrcadInstanceLock, ORCAD_LOCK_FILE_NAME, - OrcadInstanceLockError, type OrcadInstanceLockHooks, type OrcadLockRecord } from './orcad-instance-lock' +import { OrcadInstanceLockError } from './orcad-data-root-privacy' const roots: string[] = [] diff --git a/src/main/orcad/orcad-instance-lock.ts b/src/main/orcad/orcad-instance-lock.ts index 72fdb8263b7..ab152d2a2d8 100644 --- a/src/main/orcad/orcad-instance-lock.ts +++ b/src/main/orcad/orcad-instance-lock.ts @@ -35,8 +35,6 @@ const MAX_ORCAD_LOCK_BYTES = 64 * 1024 // A writer finishes in milliseconds; a garbled record older than this has no live writer. const GARBLED_LOCK_GRACE_MS = 10_000 -export { OrcadInstanceLockError, type OrcadInstanceLockCode } from './orcad-data-root-privacy' - const OrcadLockRecordSchema = z.object({ pid: z.number().int().positive().max(Number.MAX_SAFE_INTEGER), /** Null where the platform cannot read it; PID alone is then the (weaker) fence. */ diff --git a/src/main/orcad/orcad-launch-contract.test.ts b/src/main/orcad/orcad-launch-contract.test.ts index 93937ef49b7..09241e5372e 100644 --- a/src/main/orcad/orcad-launch-contract.test.ts +++ b/src/main/orcad/orcad-launch-contract.test.ts @@ -15,7 +15,7 @@ import { readAgentSessionRuntimeEnds } from '../runtime/agent-session-runtime-end-record' import { OrcadBindAddressError } from './orcad-bind-address' -import { OrcadInstanceLockError } from './orcad-instance-lock' +import { OrcadInstanceLockError } from './orcad-data-root-privacy' import { ProfileStateAccessError } from '../persistence/profile-state/profile-state-access' import { OrcadBundledRuntimeError } from './orcad-bundled-runtime' @@ -42,7 +42,7 @@ describe('parseArgs', () => { expect(() => parseArgs(['--help', '--port', 'bad'])).toThrow('--port expects an integer') expect(() => parseArgs(['--help', '--bind'])).toThrow('--bind expects a value') expect(() => parseArgs(['--help', '--recipe-json'])).toThrow( - '--recipe-json requires --project-root' + 'Recipe JSON output requires --project-root.' ) for (const flag of ['--no-pairing', '--mobile-pairing']) { expect(() => parseArgs(['--help', '--grant-desktop-control', flag])).toThrow( @@ -62,24 +62,29 @@ describe('parseArgs', () => { }) it('takes the desktop serve flags orca serve forwards', () => { - expect( - parseArgs([ - '--mobile-pairing', - '--recipe-json', - '--project-root', - '/work/app', - '--no-pairing' - ]) - ).toEqual({ - mobilePairing: true, + expect(parseArgs(['--recipe-json', '--project-root', '/work/app'])).toEqual({ recipeJson: true, - projectRoot: '/work/app', - noPairing: true + projectRoot: '/work/app' }) - expect(() => parseArgs(['--recipe-json'])).toThrow('--recipe-json requires --project-root') + expect(parseArgs(['--mobile-pairing'])).toEqual({ mobilePairing: true }) + expect(parseArgs(['--no-pairing'])).toEqual({ noPairing: true }) + expect(() => parseArgs(['--recipe-json'])).toThrow( + 'Recipe JSON output requires --project-root.' + ) expect(() => parseArgs(['--project-root'])).toThrow('--project-root expects a value') }) + it('rejects contradictory pairing flags with the desktop serve message', () => { + expect(() => parseArgs(['--no-pairing', '--mobile-pairing'])).toThrow( + 'Use either --mobile-pairing or --no-pairing, not both.' + ) + for (const flag of ['--no-pairing', '--mobile-pairing']) { + expect(() => parseArgs(['--recipe-json', '--project-root', '/work/app', flag])).toThrow( + `Recipe JSON output requires runtime pairing; remove ${flag}.` + ) + } + }) + it('rejects --bind with no value rather than silently binding the default', () => { expect(() => parseArgs(['--bind'])).toThrow('--bind expects a value') expect(() => parseArgs(['--bind', '--json'])).not.toThrow() diff --git a/src/main/orcad/orcad-managed-idle-exit-host.ts b/src/main/orcad/orcad-managed-idle-exit-host.ts index 7ea30102d18..f7a6d455d3b 100644 --- a/src/main/orcad/orcad-managed-idle-exit-host.ts +++ b/src/main/orcad/orcad-managed-idle-exit-host.ts @@ -63,7 +63,7 @@ async function startOrcadManagedIdleExit( } ): Promise { const { getLocalPtyProvider } = await import('../ipc/pty') - const { getDaemonEndpointFacts } = await import('../daemon/daemon-init') + const { getDaemonEndpointFacts } = await import('../daemon/daemon-provider-state') const { countLiveOrcadDaemonSessions, retireOrcadDaemonIfIdle } = await import('./orcad-daemon-retirement') const idleRecord = createIdleStopRecordOwnership(input.userDataPath) diff --git a/src/main/orcad/orcad-push-startup.test.ts b/src/main/orcad/orcad-push-startup.test.ts index b2349b560b1..140def0eb6a 100644 --- a/src/main/orcad/orcad-push-startup.test.ts +++ b/src/main/orcad/orcad-push-startup.test.ts @@ -79,7 +79,7 @@ vi.mock('./orcad-automations', () => ({ vi.mock('../runtime/agent-state-rules/agent-state-rules-live-update', () => ({ startAgentStateRulesLiveUpdates: () => {} })) -vi.mock('../daemon/daemon-init', () => ({ daemonOwnsFreshPersistentPtys: () => false })) +vi.mock('../daemon/daemon-provider-state', () => ({ daemonOwnsFreshPersistentPtys: () => false })) vi.mock('../ipc/pty', () => ({ registerHeadlessPtyRuntime: async () => {}, getLocalPtyProvider: () => null, diff --git a/src/main/startup/desktop-profile-instance-lock.test.ts b/src/main/startup/desktop-profile-instance-lock.test.ts index 8bf0ec121cf..938ff74d174 100644 --- a/src/main/startup/desktop-profile-instance-lock.test.ts +++ b/src/main/startup/desktop-profile-instance-lock.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import { OrcadInstanceLockError, type OrcadInstanceLock } from '../orcad/orcad-instance-lock' +import type { OrcadInstanceLock } from '../orcad/orcad-instance-lock' +import { OrcadInstanceLockError } from '../orcad/orcad-data-root-privacy' import { acquireDesktopProfileInstanceLock } from './desktop-profile-instance-lock' describe('the desktop share of the profile instance lock', () => { diff --git a/src/main/startup/desktop-profile-instance-lock.ts b/src/main/startup/desktop-profile-instance-lock.ts index 076e5b627dc..b16c5c4563a 100644 --- a/src/main/startup/desktop-profile-instance-lock.ts +++ b/src/main/startup/desktop-profile-instance-lock.ts @@ -5,11 +5,8 @@ * Electron's single-instance lock only fences other Electron launches; orcad is a plain Node * process that cannot see it. Sharing orcad's lock file gives both hosts one owner record. */ -import { - acquireOrcadInstanceLock, - OrcadInstanceLockError, - type OrcadInstanceLock -} from '../orcad/orcad-instance-lock' +import { acquireOrcadInstanceLock, type OrcadInstanceLock } from '../orcad/orcad-instance-lock' +import { OrcadInstanceLockError } from '../orcad/orcad-data-root-privacy' import { writeStartupDiagnosticLine, type StartupDiagnosticSink } from './startup-diagnostics' export type DesktopProfileLockResult = diff --git a/src/main/startup/main-process-pty-startup.ts b/src/main/startup/main-process-pty-startup.ts index 3e76bb855de..20136d4e9d5 100644 --- a/src/main/startup/main-process-pty-startup.ts +++ b/src/main/startup/main-process-pty-startup.ts @@ -2,11 +2,8 @@ import { app } from 'electron' import { classifyError } from '../telemetry/classify-error' import { track } from '../telemetry/client' import { getPtyIdForPaneKey } from '../ipc/pty' -import { - getDaemonProvider, - initDaemonPtyProvider, - listLiveDaemonPtyIds -} from '../daemon/daemon-init' +import { getDaemonProvider, listLiveDaemonPtyIds } from '../daemon/daemon-provider-state' +import { initDaemonPtyProvider } from '../daemon/daemon-provider-init' import { getCodexPaneAccount, hasAnyRecordedLegacyWslCodexPane, diff --git a/src/main/startup/main-process-quit-pending-writer.test.ts b/src/main/startup/main-process-quit-pending-writer.test.ts index cd2ddf84062..8bf383b8adf 100644 --- a/src/main/startup/main-process-quit-pending-writer.test.ts +++ b/src/main/startup/main-process-quit-pending-writer.test.ts @@ -11,7 +11,7 @@ const dependencies: Record> = { '../ipc/worktree-base-directory-watcher': { disposeWorktreeBaseDirectoryWatchers: settled() }, '../ipc/folder-repo-git-upgrade': { stopFolderRepoGitUpgradeWatch: idle() }, '../ipc/pty': { killAllPty: idle() }, - '../daemon/daemon-init': { disconnectDaemon: settled(), shutdownDaemon: settled() }, + '../daemon/daemon-provider-state': { disconnectDaemon: settled(), shutdownDaemon: settled() }, '../ipc/ssh-shutdown-drain': { beginSshShutdown: settled() }, '../agent-hooks/server': { agentHookServer: { stop: idle() } }, '../agent-hooks/wsl-hook-relay-manager': { wslHookRelayManager: { disposeAll: idle() } }, diff --git a/src/main/startup/main-process-quit-update-veto.test.ts b/src/main/startup/main-process-quit-update-veto.test.ts index ea46417fe40..4164f2b039a 100644 --- a/src/main/startup/main-process-quit-update-veto.test.ts +++ b/src/main/startup/main-process-quit-update-veto.test.ts @@ -6,7 +6,7 @@ const dependencyExports: [string, string[]][] = [ ['../ipc/worktree-base-directory-watcher', ['disposeWorktreeBaseDirectoryWatchers']], ['../ipc/folder-repo-git-upgrade', ['stopFolderRepoGitUpgradeWatch']], ['../ipc/pty', ['killAllPty']], - ['../daemon/daemon-init', ['disconnectDaemon', 'shutdownDaemon']], + ['../daemon/daemon-provider-state', ['disconnectDaemon', 'shutdownDaemon']], ['../ipc/ssh-shutdown-drain', ['beginSshShutdown']], ['../agent-hooks/server', ['agentHookServer']], ['../agent-hooks/wsl-hook-relay-manager', ['wslHookRelayManager']], diff --git a/src/main/startup/main-process-quit.ts b/src/main/startup/main-process-quit.ts index eb2f35bfa99..7e7af4c773e 100644 --- a/src/main/startup/main-process-quit.ts +++ b/src/main/startup/main-process-quit.ts @@ -3,7 +3,7 @@ import { closeAllWatchers } from '../ipc/filesystem-watcher' import { disposeWorktreeBaseDirectoryWatchers } from '../ipc/worktree-base-directory-watcher' import { stopFolderRepoGitUpgradeWatch } from '../ipc/folder-repo-git-upgrade' import { killAllPty } from '../ipc/pty' -import { disconnectDaemon, shutdownDaemon } from '../daemon/daemon-init' +import { disconnectDaemon, shutdownDaemon } from '../daemon/daemon-provider-state' import { beginSshShutdown } from '../ipc/ssh-shutdown-drain' import { agentHookServer } from '../agent-hooks/server' import { wslHookRelayManager } from '../agent-hooks/wsl-hook-relay-manager' diff --git a/src/main/startup/main-process-runtime-service.ts b/src/main/startup/main-process-runtime-service.ts index 61e36137688..a003e5b42cc 100644 --- a/src/main/startup/main-process-runtime-service.ts +++ b/src/main/startup/main-process-runtime-service.ts @@ -16,7 +16,7 @@ import { prepareCodexAiVaultSessionResume } from '../codex/codex-ai-vault-sessio import { prepareCodexPinnedLaunchHome } from './codex-session-resume-launch' import { resolveHostCodexSessionSourceHome } from '../codex/codex-session-source-home' import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' -import { getDaemonProvider } from '../daemon/daemon-init' +import { getDaemonProvider } from '../daemon/daemon-provider-state' import type { TerminalSideEffectBatch } from '../../shared/terminal-side-effect-facts' import type { OrchestrationEnvironmentTransport } from '../runtime/orchestration/environment-transport' import { resolveEnvironment } from '../../shared/runtime-environment-store' diff --git a/src/shared/daemon-lifecycle-telemetry.ts b/src/shared/daemon-lifecycle-telemetry.ts index 88b6f8f287f..afa9c71ec7c 100644 --- a/src/shared/daemon-lifecycle-telemetry.ts +++ b/src/shared/daemon-lifecycle-telemetry.ts @@ -3,7 +3,7 @@ // subprocess and never crosses into the app, so every reason here is one the app // itself decides: a startup replace, or an observed death→respawn. -// Startup launcher replaced a still-connectable daemon (each maps 1:1 to a `daemon-init.ts` decision). +// Startup launcher replaced a still-connectable daemon (each maps 1:1 to a `daemon-replacement-preflight.ts` decision). export const DAEMON_REPLACE_REASONS = [ 'unhealthy_resolver', 'stale_bundle', diff --git a/tests/e2e/fixtures/daemon-generation-desktop-discovery.ts b/tests/e2e/fixtures/daemon-generation-desktop-discovery.ts index 92743a443dd..e208ded947b 100644 --- a/tests/e2e/fixtures/daemon-generation-desktop-discovery.ts +++ b/tests/e2e/fixtures/daemon-generation-desktop-discovery.ts @@ -1,5 +1,5 @@ import { DaemonPtyAdapter } from '../../../src/main/daemon/daemon-pty-adapter' -import { createLegacyDaemonAdapters } from '../../../src/main/daemon/daemon-init' +import { createLegacyDaemonAdapters } from '../../../src/main/daemon/daemon-legacy-adapters' import { DaemonPtyRouter } from '../../../src/main/daemon/daemon-pty-router' export type DesktopDiscoveryGeneration = { diff --git a/tests/tools/win-update-e2e/daemon-processes.mjs b/tests/tools/win-update-e2e/daemon-processes.mjs index d292c0f24b8..950068a6de4 100644 --- a/tests/tools/win-update-e2e/daemon-processes.mjs +++ b/tests/tools/win-update-e2e/daemon-processes.mjs @@ -5,7 +5,7 @@ // matched by executable name. The only reliable discriminators are the // command-line markers the fork always passes: the daemon entry script // (daemon-entry.js) and its --socket / --token arguments. See -// src/main/daemon/daemon-init.ts (createOutOfProcessLauncher). +// src/main/daemon/daemon-out-of-process-launcher.ts (createOutOfProcessLauncher). // // The daemon also writes a PID file at /daemon/daemon-v.pid whose // JSON carries { pid, startedAtMs, entryPath, appVersion } — the appVersion