diff --git a/src/application/ProcessCaptureLifecycle.ts b/src/application/ProcessCaptureLifecycle.ts index 9ef9a0a424..f38661a97f 100644 --- a/src/application/ProcessCaptureLifecycle.ts +++ b/src/application/ProcessCaptureLifecycle.ts @@ -26,10 +26,8 @@ import { PRODUCT_IDENTITY } from "../identity.js"; import type { SnapshotResult } from "./FilesystemSnapshot.js"; import { snapshotRoots } from "./FilesystemSnapshot.js"; import { classifyFilesystemEffects } from "./ProcessFilesystemEffects.js"; -import { - cleanupOwnedProcessGroup, - observeOwnedProcessGroup, -} from "../process/ProcessOwnership.js"; +import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js"; +import { observeOwnedProcessGroup } from "../process/ProcessOwnershipObservation.js"; import { ProcessCaptureError } from "./ProcessCaptureError.js"; import { assertNotCancelled } from "./ProcessScenarioRuntimeValidation.js"; import { diff --git a/src/application/ProcessHarness.ts b/src/application/ProcessHarness.ts index 24c256d631..b162c918d4 100644 --- a/src/application/ProcessHarness.ts +++ b/src/application/ProcessHarness.ts @@ -25,7 +25,7 @@ import { snapshotRoots } from "./FilesystemSnapshot.js"; import { TerminalRenderer } from "./TerminalRenderer.js"; import type { ProcessTimer } from "./ProcessTimer.js"; import { normalizeProcessText } from "./ProcessNormalization.js"; -import { selectCapturedProcessGroupIds } from "../process/ProcessOwnership.js"; +import { selectCapturedProcessGroupIds } from "../process/ProcessOwnershipProcessTree.js"; import { awaitTerminalExit, buildCaptureResult, diff --git a/src/application/Setup.configuration.test.ts b/src/application/Setup.configuration.test.ts index dc93a0266a..cf63381d6b 100644 --- a/src/application/Setup.configuration.test.ts +++ b/src/application/Setup.configuration.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it } from "vitest"; import { resolve } from "node:path"; import { FakeSetupHost, options } from "./Setup.fixture.js"; -import { runSetup, setupRegistrationCommand } from "./Setup.js"; +import { runSetup } from "./Setup.js"; +import { setupRegistrationCommand } from "./SetupHost.js"; describe("setup workflow", () => { it("omits an aligned managed skill from an otherwise empty plan", async () => { diff --git a/src/application/Setup.discovery.test.ts b/src/application/Setup.discovery.test.ts index 93fda1beb6..b2c8054d5f 100644 --- a/src/application/Setup.discovery.test.ts +++ b/src/application/Setup.discovery.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { detectClients } from "./Setup.js"; +import { detectClients } from "./SetupHost.js"; describe("setup client discovery", () => { let temporaryHome: string | undefined; diff --git a/src/application/Setup.fixture.ts b/src/application/Setup.fixture.ts index 1e7d182ad1..f119ec5816 100644 --- a/src/application/Setup.fixture.ts +++ b/src/application/Setup.fixture.ts @@ -1,12 +1,12 @@ import { type ClientConfigurationInspection, type ClientConfigurationResult, - type SetupClient, type SetupHost, type SetupOptions, - type SetupHopperInstallResult, type SetupProviderEnvironment, -} from "./Setup.js"; +} from "./SetupTypes.js"; +import type { SetupClient } from "./SupportedClients.js"; +import type { SetupHopperInstallResult } from "./SetupInstallFailure.js"; import type { DoctorCheck, DoctorReport, DoctorScope } from "./Doctor.js"; import type { LinuxDistribution } from "./LinuxHopper.js"; import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js"; diff --git a/src/application/Setup.planning.test.ts b/src/application/Setup.planning.test.ts index c830346942..3654c2dcf2 100644 --- a/src/application/Setup.planning.test.ts +++ b/src/application/Setup.planning.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; -import { runSetup, type SetupProgressEvent } from "./Setup.js"; +import { runSetup } from "./Setup.js"; +import type { SetupProgressEvent } from "./SetupTypes.js"; import { FakeSetupHost, options } from "./Setup.fixture.js"; describe("setup workflow", () => { diff --git a/src/application/Setup.ts b/src/application/Setup.ts index d24c396971..c99a25ea72 100644 --- a/src/application/Setup.ts +++ b/src/application/Setup.ts @@ -1,235 +1,27 @@ -import { access } from "node:fs/promises"; -import { homedir } from "node:os"; -import { resolve } from "node:path"; - -import { PRODUCT_IDENTITY } from "../identity.js"; -import { supportsNodeVersion } from "../domain/runtimeVersion.js"; -import { - runDoctor, - systemDoctorHost, - type DoctorHost, - type DoctorReport, - type DoctorScope, -} from "./Doctor.js"; -import { - installLinuxHopper, - readLinuxDistribution, - type LinuxDistribution, -} from "./LinuxHopper.js"; -import { installMacHopper } from "./MacHopper.js"; +import type { + ClientConfigurationResult, + SetupAction, + SetupClientState, + SetupConfirmation, + SetupConfirmationDecision, + SetupDoctorSummary, + SetupHost, + SetupOptions, + SetupProgressEvent, + SetupProviderEnvironment, + SetupResult, +} from "./SetupTypes.js"; +import type { SetupClient } from "./SupportedClients.js"; +import type { DoctorReport, DoctorScope } from "./Doctor.js"; import { configureDetectedClients } from "./SetupClients.js"; -import { supportedClients, type SetupClient } from "./SupportedClients.js"; -export type { SetupClient } from "./SupportedClients.js"; -import { - canonicalSkillNeedsInstall, - installCanonicalSkill, -} from "./SetupSkill.js"; import { discoverSetupState, planSetupActions } from "./SetupPlan.js"; import { - clientConfigurationAligned, - configureClientConfiguration, - configureJsonClient, - configureTomlClient, - inspectClientConfiguration, -} from "./SetupClientConfiguration.js"; -export { configureJsonClient, configureTomlClient }; -import { - setupInstallFailure, - type SetupFailureCode, - type SetupHopperInstallResult, -} from "./SetupInstallFailure.js"; -import { providerRegistrationEnvironment } from "./SetupRegistrationEnvironment.js"; -export type { SetupHopperInstallResult } from "./SetupInstallFailure.js"; - -/** Exact non-secret provider variables propagated into managed registrations. */ -export type SetupProviderEnvironment = Readonly>; - -export { canonicalSkillNeedsInstall, installCanonicalSkill }; - -/** Resolve the executable and arguments used in a managed MCP registration. */ -export const setupRegistrationCommand = ( - platform: NodeJS.Platform, - useNpmRunner: boolean = process.env.npm_command === "exec", -): readonly string[] => - useNpmRunner - ? PRODUCT_IDENTITY.mcpCommand.split(" ") - : platform === "win32" - ? [ - process.execPath, - resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), - "mcp", - ] - : [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"]; - -/** Result of one backup/write/readback transaction. */ -export type ClientConfigurationResult = - | { - readonly status: "unchanged" | "configured" | "skipped"; - readonly backupPath?: string; - } - | { - readonly status: "failed"; - readonly reason: "path" | "backup" | "write" | "readback"; - }; - -/** Read-only state used to block unsafe setup plans before any mutation. */ -export type ClientConfigurationInspection = - | { readonly status: "already_current" } - | { - readonly status: "create" | "update"; - readonly backupPath?: string; - } - | { - readonly status: "invalid"; - readonly remediation: string; - }; -/** Effects required by the idempotent, non-blocking setup workflow. */ -export interface SetupHost { - readonly platform: NodeJS.Platform; - readonly nodeVersion: string; - macosVersion(): Promise; - linuxDistribution(): Promise; - hopperPath(): Promise; - installHopper(replaceExisting: boolean): Promise; - providerEnvironment?(): Promise; - detectedClients(): Promise; - supportedClients?(): Promise; - configureClient( - client: SetupClient, - providerEnvironment: SetupProviderEnvironment, - command: readonly string[], - ): Promise; - clientNeedsConfigure( - client: SetupClient, - providerEnvironment: SetupProviderEnvironment, - command: readonly string[], - ): Promise; - inspectClientConfiguration?( - client: SetupClient, - providerEnvironment: SetupProviderEnvironment, - command: readonly string[], - ): Promise; - skillNeedsInstall(): Promise; - installSkill(): Promise<"installed" | "unchanged" | "failed">; - doctor(scope?: DoctorScope): Promise>>; -} -/** Structured setup outcome carrying remediation instead of prompting. */ -export interface SetupResult { - readonly status: - | "planned" - | "cancelled" - | "needs_confirmation" - | "ready" - | "needs_human"; - readonly plannedActions: readonly SetupAction[]; - readonly appliedActions: readonly string[]; - readonly clients: Readonly>; - readonly doctor: SetupDoctorSummary; - /** Supported-client discovery state captured before setup mutations. */ - readonly clientStates: readonly SetupClientState[]; - readonly remediation?: string; - readonly code?: SetupFailureCode; -} - -/** Whether the requested setup actions still need approval or human repair. */ -export const isSetupFailure = (result: SetupResult): boolean => { - const status = result.status; - switch (status) { - case "planned": - case "cancelled": - case "ready": - return false; - case "needs_confirmation": - case "needs_human": - return true; - default: { - const exhaustive: never = status; - throw new TypeError( - `Unhandled setup result status: ${String(exhaustive)}`, - ); - } - } -}; - -/** Relevant health and identity evidence for the selected setup scope. */ -export interface SetupDoctorSummary { - readonly healthy: boolean; - readonly environment_healthy: boolean; - readonly scope: DoctorReport["scope"]; - readonly hopperPath?: string; - readonly availableProviders: readonly string[]; - readonly providerInspections?: DoctorReport["providerInspections"]; - readonly identity?: Pick< - NonNullable, - "skill" | "registrations" - >; -} - -/** Read-only state for one supported client location. */ -export interface SetupClientState { - readonly client: SetupClient; - readonly detected: boolean; - readonly configured: boolean; - readonly status: "configured" | "missing" | "needs_configuration" | "invalid"; -} - -/** One concrete setup mutation disclosed before approval. */ -export interface SetupAction { - readonly id: string; - readonly kind: "install_hopper" | "configure_client" | "install_skill"; - readonly label: string; - readonly target: string; - readonly detail: string; - readonly external: boolean; - readonly operation: "create" | "update" | "install"; - readonly backupPath?: string; - readonly networkOrigins?: readonly string[]; - readonly commands?: readonly string[]; - readonly integrity?: string; -} - -/** Explicit authorization supplied by an interactive or unattended CLI adapter. */ -export interface SetupOptions { - readonly approved: boolean; - readonly installHopper: boolean; - readonly structured: boolean; - readonly proposeHopper?: boolean; - readonly clientIds?: readonly string[]; - readonly installSkill?: boolean; - readonly readinessScope?: DoctorScope; - readonly dryRun?: boolean; - readonly allDetectedClients?: boolean; - readonly onProgress?: (event: SetupProgressEvent) => void; -} - -/** One settled or active setup operation projected to the interactive adapter. */ -export interface SetupProgressEvent { - readonly actionId: string; - readonly label: string; - readonly state: "started" | "completed" | "warning" | "failed"; - readonly detail?: string; -} - -/** Interactive selection and final consent returned by the CLI adapter. */ -export interface SetupConfirmationDecision { - readonly approved: boolean; - readonly selectedActionIds: readonly string[]; - readonly cancelled?: boolean; -} - -/** Read-only context shown while selecting clients or reviewing exact actions. */ -export interface SetupConfirmationContext { - readonly stage: "select" | "confirm"; - readonly clientStates: readonly SetupClientState[]; - readonly selectedClientIds: readonly string[]; - readonly clientSelectionAllowed: boolean; -} - -/** Adapter-owned selection and confirmation for the setup workflow. */ -export type SetupConfirmation = ( - actions: readonly SetupAction[], - context?: SetupConfirmationContext, -) => Promise; + filterClientsNeedingConfigure, + hostRemediation, + initialProviderEnvironment, + setupRegistrationCommand, + systemSetupHost, +} from "./SetupHost.js"; /** Discover, approve, and apply setup actions idempotently. */ export const runSetup = async ( @@ -581,28 +373,6 @@ const setupFailure = async ( const emitProgress = (options: SetupOptions, event: SetupProgressEvent): void => options.onProgress?.(event); -const filterClientsNeedingConfigure = async ( - host: SetupHost, - detectedClients: readonly SetupClient[], - providerEnvironment: SetupProviderEnvironment, - command: readonly string[], -): Promise => { - const needs = await Promise.all( - detectedClients.map((client) => - host.clientNeedsConfigure(client, providerEnvironment, command), - ), - ); - return detectedClients.filter((_, index) => needs[index]); -}; - -const initialProviderEnvironment = async ( - host: SetupHost, - hopperPath: string | undefined, -): Promise => ({ - ...(await host.providerEnvironment?.()), - ...(hopperPath === undefined ? {} : { HOPPER_LAUNCHER_PATH: hopperPath }), -}); - const finalSetupRemediation = ( platform: NodeJS.Platform, installedHopper: boolean, @@ -679,93 +449,3 @@ const summarizeDoctor = (report: DoctorReport): SetupDoctorSummary => ({ }, }), }); - -const hostRemediation = async ( - host: SetupHost, - installHopper: boolean, -): Promise => { - if (!supportsNodeVersion(host.nodeVersion)) - return "Install Node.js 22.19+ or 24.11+ and rerun setup."; - if (!installHopper) return undefined; - if (host.platform !== "darwin" && host.platform !== "linux") - return "REA supports Hopper on macOS and selected 64-bit Linux distributions."; - if (host.platform === "darwin") { - const version = await host.macosVersion(); - return version === undefined || major(version) < 12 - ? "Upgrade to macOS 12 or newer." - : undefined; - } - if ((await host.linuxDistribution())?.supported === true) return undefined; - return "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux; configure an existing supported provider instead."; -}; - -/** Production setup effects for Hopper, agent configuration, and the canonical skill directory. */ -export const systemSetupHost = ( - doctorHost: DoctorHost = systemDoctorHost(), -): SetupHost => { - const platform = doctorHost.platform; - return { - platform, - nodeVersion: process.versions.node, - macosVersion: () => doctorHost.macosVersion(), - linuxDistribution: readLinuxDistribution, - hopperPath: async () => (await runDoctor(undefined, doctorHost)).hopperPath, - providerEnvironment: async () => { - const diagnosis = await runDoctor(undefined, doctorHost); - return { - ...providerRegistrationEnvironment(diagnosis.providerInspections ?? []), - ...(diagnosis.hopperPath === undefined - ? {} - : { HOPPER_LAUNCHER_PATH: diagnosis.hopperPath }), - }; - }, - installHopper: async (replaceExisting) => { - const result = - platform === "linux" - ? await installLinuxHopper() - : await installMacHopper({ replaceExisting }); - if (result.status === "installed") return result; - return setupInstallFailure(result.reason); - }, - detectedClients: () => detectClients(homedir()), - supportedClients: () => Promise.resolve(supportedClients(homedir())), - configureClient: (client, providerEnvironment, command) => - client.format === "unsupported" - ? Promise.resolve({ status: "skipped" }) - : configureClientConfiguration(client, providerEnvironment, command), - clientNeedsConfigure: (client, providerEnvironment, command) => - clientConfigurationAligned(client, providerEnvironment, command).then( - (aligned) => !aligned, - ), - inspectClientConfiguration: inspectClientConfiguration, - skillNeedsInstall: () => canonicalSkillNeedsInstall(homedir()), - installSkill: () => installCanonicalSkill(homedir()), - doctor: (scope) => runDoctor(undefined, doctorHost, scope), - }; -}; - -/** Detect supported agents from their config files or stable installation markers. */ -export const detectClients = async ( - home: string, -): Promise => { - const detected: SetupClient[] = []; - for (const candidate of supportedClients(home)) { - const [hasConfig, hasMarker] = await Promise.all([ - exists(candidate.configPath), - candidate.markerPath === undefined ? false : exists(candidate.markerPath), - ]); - if (hasConfig || hasMarker) detected.push(candidate); - } - return detected; -}; - -const major = (version: string): number => - Number.parseInt(version.split(".")[0] ?? "0", 10); -const exists = async (path: string): Promise => { - try { - await access(path); - return true; - } catch { - return false; - } -}; diff --git a/src/application/SetupClientConfiguration.ts b/src/application/SetupClientConfiguration.ts index 7608eb257f..87d13aaa99 100644 --- a/src/application/SetupClientConfiguration.ts +++ b/src/application/SetupClientConfiguration.ts @@ -17,7 +17,7 @@ import type { ClientConfigurationInspection, ClientConfigurationResult, SetupProviderEnvironment, -} from "./Setup.js"; +} from "./SetupTypes.js"; import type { SetupClient } from "./SupportedClients.js"; const defaultCommand = (): readonly string[] => [ diff --git a/src/application/SetupClients.ts b/src/application/SetupClients.ts index 67f6fed95e..bbeeedf3f3 100644 --- a/src/application/SetupClients.ts +++ b/src/application/SetupClients.ts @@ -1,10 +1,10 @@ import type { ClientConfigurationResult, - SetupClient, SetupHost, SetupProviderEnvironment, SetupProgressEvent, -} from "./Setup.js"; +} from "./SetupTypes.js"; +import type { SetupClient } from "./SupportedClients.js"; const failedConfigurationMessage = ( reason: "path" | "backup" | "write" | "readback", diff --git a/src/application/SetupHost.ts b/src/application/SetupHost.ts new file mode 100644 index 0000000000..46cb53a73f --- /dev/null +++ b/src/application/SetupHost.ts @@ -0,0 +1,149 @@ +import { access } from "node:fs/promises"; +import { homedir } from "node:os"; +import { resolve } from "node:path"; + +import { PRODUCT_IDENTITY } from "../identity.js"; +import { supportsNodeVersion } from "../domain/runtimeVersion.js"; +import { runDoctor, systemDoctorHost, type DoctorHost } from "./Doctor.js"; +import { installLinuxHopper, readLinuxDistribution } from "./LinuxHopper.js"; +import { installMacHopper } from "./MacHopper.js"; +import { supportedClients, type SetupClient } from "./SupportedClients.js"; +import { + canonicalSkillNeedsInstall, + installCanonicalSkill, +} from "./SetupSkill.js"; +import { + clientConfigurationAligned, + configureClientConfiguration, + inspectClientConfiguration, +} from "./SetupClientConfiguration.js"; +import { setupInstallFailure } from "./SetupInstallFailure.js"; +import { providerRegistrationEnvironment } from "./SetupRegistrationEnvironment.js"; +import type { SetupHost, SetupProviderEnvironment } from "./SetupTypes.js"; + +/** Resolve the executable and arguments used in a managed MCP registration. */ +export const setupRegistrationCommand = ( + platform: NodeJS.Platform, + useNpmRunner: boolean = process.env.npm_command === "exec", +): readonly string[] => + useNpmRunner + ? PRODUCT_IDENTITY.mcpCommand.split(" ") + : platform === "win32" + ? [ + process.execPath, + resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), + "mcp", + ] + : [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"]; + +export const filterClientsNeedingConfigure = async ( + host: SetupHost, + detectedClients: readonly SetupClient[], + providerEnvironment: SetupProviderEnvironment, + command: readonly string[], +): Promise => { + const needs = await Promise.all( + detectedClients.map((client) => + host.clientNeedsConfigure(client, providerEnvironment, command), + ), + ); + return detectedClients.filter((_, index) => needs[index]); +}; + +export const initialProviderEnvironment = async ( + host: SetupHost, + hopperPath: string | undefined, +): Promise => ({ + ...(await host.providerEnvironment?.()), + ...(hopperPath === undefined ? {} : { HOPPER_LAUNCHER_PATH: hopperPath }), +}); + +export const hostRemediation = async ( + host: SetupHost, + installHopper: boolean, +): Promise => { + if (!supportsNodeVersion(host.nodeVersion)) + return "Install Node.js 22.19+ or 24.11+ and rerun setup."; + if (!installHopper) return undefined; + if (host.platform !== "darwin" && host.platform !== "linux") + return "REA supports Hopper on macOS and selected 64-bit Linux distributions."; + if (host.platform === "darwin") { + const version = await host.macosVersion(); + return version === undefined || major(version) < 12 + ? "Upgrade to macOS 12 or newer." + : undefined; + } + if ((await host.linuxDistribution())?.supported === true) return undefined; + return "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux; configure an existing supported provider instead."; +}; + +/** Production setup effects for Hopper, agent configuration, and the canonical skill directory. */ +export const systemSetupHost = ( + doctorHost: DoctorHost = systemDoctorHost(), +): SetupHost => { + const platform = doctorHost.platform; + return { + platform, + nodeVersion: process.versions.node, + macosVersion: () => doctorHost.macosVersion(), + linuxDistribution: readLinuxDistribution, + hopperPath: async () => (await runDoctor(undefined, doctorHost)).hopperPath, + providerEnvironment: async () => { + const diagnosis = await runDoctor(undefined, doctorHost); + return { + ...providerRegistrationEnvironment(diagnosis.providerInspections ?? []), + ...(diagnosis.hopperPath === undefined + ? {} + : { HOPPER_LAUNCHER_PATH: diagnosis.hopperPath }), + }; + }, + installHopper: async (replaceExisting) => { + const result = + platform === "linux" + ? await installLinuxHopper() + : await installMacHopper({ replaceExisting }); + if (result.status === "installed") return result; + return setupInstallFailure(result.reason); + }, + detectedClients: () => detectClients(homedir()), + supportedClients: () => Promise.resolve(supportedClients(homedir())), + configureClient: (client, providerEnvironment, command) => + client.format === "unsupported" + ? Promise.resolve({ status: "skipped" }) + : configureClientConfiguration(client, providerEnvironment, command), + clientNeedsConfigure: (client, providerEnvironment, command) => + clientConfigurationAligned(client, providerEnvironment, command).then( + (aligned) => !aligned, + ), + inspectClientConfiguration: inspectClientConfiguration, + skillNeedsInstall: () => canonicalSkillNeedsInstall(homedir()), + installSkill: () => installCanonicalSkill(homedir()), + doctor: (scope) => runDoctor(undefined, doctorHost, scope), + }; +}; + +/** Detect supported agents from their config files or stable installation markers. */ +export const detectClients = async ( + home: string, +): Promise => { + const detected: SetupClient[] = []; + for (const candidate of supportedClients(home)) { + const [hasConfig, hasMarker] = await Promise.all([ + exists(candidate.configPath), + candidate.markerPath === undefined ? false : exists(candidate.markerPath), + ]); + if (hasConfig || hasMarker) detected.push(candidate); + } + return detected; +}; + +const major = (version: string): number => + Number.parseInt(version.split(".")[0] ?? "0", 10); +const exists = async (path: string): Promise => { + try { + await access(path); + return true; + } catch { + return false; + } +}; diff --git a/src/application/SetupPlan.ts b/src/application/SetupPlan.ts index 85b5cb97fe..e89e072293 100644 --- a/src/application/SetupPlan.ts +++ b/src/application/SetupPlan.ts @@ -10,11 +10,11 @@ import { import { macHopperInstallDisclosure } from "./MacHopper.js"; import type { SetupAction, - SetupClient, SetupClientState, SetupHost, SetupProviderEnvironment, -} from "./Setup.js"; +} from "./SetupTypes.js"; +import type { SetupClient } from "./SupportedClients.js"; import type { DoctorScope } from "./Doctor.js"; /** Read-only setup facts shared by client selection and preflight. */ diff --git a/src/application/SetupTypes.ts b/src/application/SetupTypes.ts new file mode 100644 index 0000000000..ac4da9d7bc --- /dev/null +++ b/src/application/SetupTypes.ts @@ -0,0 +1,180 @@ +import type { DoctorReport, DoctorScope, runDoctor } from "./Doctor.js"; +import type { LinuxDistribution } from "./LinuxHopper.js"; +import type { SetupClient } from "./SupportedClients.js"; +import type { + SetupFailureCode, + SetupHopperInstallResult, +} from "./SetupInstallFailure.js"; + +/** Exact non-secret provider variables propagated into managed registrations. */ +export type SetupProviderEnvironment = Readonly>; + +/** Result of one backup/write/readback transaction. */ +export type ClientConfigurationResult = + | { + readonly status: "unchanged" | "configured" | "skipped"; + readonly backupPath?: string; + } + | { + readonly status: "failed"; + readonly reason: "path" | "backup" | "write" | "readback"; + }; + +/** Read-only state used to block unsafe setup plans before any mutation. */ +export type ClientConfigurationInspection = + | { readonly status: "already_current" } + | { + readonly status: "create" | "update"; + readonly backupPath?: string; + } + | { + readonly status: "invalid"; + readonly remediation: string; + }; +/** Effects required by the idempotent, non-blocking setup workflow. */ +export interface SetupHost { + readonly platform: NodeJS.Platform; + readonly nodeVersion: string; + macosVersion(): Promise; + linuxDistribution(): Promise; + hopperPath(): Promise; + installHopper(replaceExisting: boolean): Promise; + providerEnvironment?(): Promise; + detectedClients(): Promise; + supportedClients?(): Promise; + configureClient( + client: SetupClient, + providerEnvironment: SetupProviderEnvironment, + command: readonly string[], + ): Promise; + clientNeedsConfigure( + client: SetupClient, + providerEnvironment: SetupProviderEnvironment, + command: readonly string[], + ): Promise; + inspectClientConfiguration?( + client: SetupClient, + providerEnvironment: SetupProviderEnvironment, + command: readonly string[], + ): Promise; + skillNeedsInstall(): Promise; + installSkill(): Promise<"installed" | "unchanged" | "failed">; + doctor(scope?: DoctorScope): Promise>>; +} +/** Structured setup outcome carrying remediation instead of prompting. */ +export interface SetupResult { + readonly status: + | "planned" + | "cancelled" + | "needs_confirmation" + | "ready" + | "needs_human"; + readonly plannedActions: readonly SetupAction[]; + readonly appliedActions: readonly string[]; + readonly clients: Readonly>; + readonly doctor: SetupDoctorSummary; + /** Supported-client discovery state captured before setup mutations. */ + readonly clientStates: readonly SetupClientState[]; + readonly remediation?: string; + readonly code?: SetupFailureCode; +} + +/** Whether the requested setup actions still need approval or human repair. */ +export const isSetupFailure = (result: SetupResult): boolean => { + const status = result.status; + switch (status) { + case "planned": + case "cancelled": + case "ready": + return false; + case "needs_confirmation": + case "needs_human": + return true; + default: { + const exhaustive: never = status; + throw new TypeError( + `Unhandled setup result status: ${String(exhaustive)}`, + ); + } + } +}; + +/** Relevant health and identity evidence for the selected setup scope. */ +export interface SetupDoctorSummary { + readonly healthy: boolean; + readonly environment_healthy: boolean; + readonly scope: DoctorReport["scope"]; + readonly hopperPath?: string; + readonly availableProviders: readonly string[]; + readonly providerInspections?: DoctorReport["providerInspections"]; + readonly identity?: Pick< + NonNullable, + "skill" | "registrations" + >; +} + +/** Read-only state for one supported client location. */ +export interface SetupClientState { + readonly client: SetupClient; + readonly detected: boolean; + readonly configured: boolean; + readonly status: "configured" | "missing" | "needs_configuration" | "invalid"; +} + +/** One concrete setup mutation disclosed before approval. */ +export interface SetupAction { + readonly id: string; + readonly kind: "install_hopper" | "configure_client" | "install_skill"; + readonly label: string; + readonly target: string; + readonly detail: string; + readonly external: boolean; + readonly operation: "create" | "update" | "install"; + readonly backupPath?: string; + readonly networkOrigins?: readonly string[]; + readonly commands?: readonly string[]; + readonly integrity?: string; +} + +/** Explicit authorization supplied by an interactive or unattended CLI adapter. */ +export interface SetupOptions { + readonly approved: boolean; + readonly installHopper: boolean; + readonly structured: boolean; + readonly proposeHopper?: boolean; + readonly clientIds?: readonly string[]; + readonly installSkill?: boolean; + readonly readinessScope?: DoctorScope; + readonly dryRun?: boolean; + readonly allDetectedClients?: boolean; + readonly onProgress?: (event: SetupProgressEvent) => void; +} + +/** One settled or active setup operation projected to the interactive adapter. */ +export interface SetupProgressEvent { + readonly actionId: string; + readonly label: string; + readonly state: "started" | "completed" | "warning" | "failed"; + readonly detail?: string; +} + +/** Interactive selection and final consent returned by the CLI adapter. */ +export interface SetupConfirmationDecision { + readonly approved: boolean; + readonly selectedActionIds: readonly string[]; + readonly cancelled?: boolean; +} + +/** Read-only context shown while selecting clients or reviewing exact actions. */ +export interface SetupConfirmationContext { + readonly stage: "select" | "confirm"; + readonly clientStates: readonly SetupClientState[]; + readonly selectedClientIds: readonly string[]; + readonly clientSelectionAllowed: boolean; +} + +/** Adapter-owned selection and confirmation for the setup workflow. */ +export type SetupConfirmation = ( + actions: readonly SetupAction[], + context?: SetupConfirmationContext, +) => Promise; diff --git a/src/application/UpdateMaintenance.ts b/src/application/UpdateMaintenance.ts index 64a6f10dfd..78e1197b0e 100644 --- a/src/application/UpdateMaintenance.ts +++ b/src/application/UpdateMaintenance.ts @@ -7,7 +7,7 @@ import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.j import { readClientRegistrationStatuses } from "./ClientRegistrationStatus.js"; import { parseClientConfiguration } from "./ClientConfigurationDocument.js"; import { supportedClients } from "./SupportedClients.js"; -import type { SetupAction } from "./Setup.js"; +import type { SetupAction } from "./SetupTypes.js"; import type { Result } from "../domain/result.js"; /** Existing REA integrations selected for maintenance, without discovering new targets. */ diff --git a/src/browser/PlaywrightElectronActiveProvider.ts b/src/browser/PlaywrightElectronActiveProvider.ts index 75f436ced3..ab20ae7a00 100644 --- a/src/browser/PlaywrightElectronActiveProvider.ts +++ b/src/browser/PlaywrightElectronActiveProvider.ts @@ -24,13 +24,13 @@ import { err, ok, type Result } from "../domain/result.js"; import { cleanupOwnedProcessGroup, cleanupWindowsProcessTree, - observeOwnedProcessLineage, - selectCapturedProcessGroupIds, verifyNoTokenOwnedProcesses, type OwnedProcessGroup, type ProcessCleanupResult, type ProcessLineageObservation, } from "../process/ProcessOwnership.js"; +import { observeOwnedProcessLineage } from "../process/ProcessOwnershipObservation.js"; +import { selectCapturedProcessGroupIds } from "../process/ProcessOwnershipProcessTree.js"; import { runElectronActions, readApplicationState, diff --git a/src/cli/setupCommands.ts b/src/cli/setupCommands.ts index 8753d612fa..6648aa26a1 100644 --- a/src/cli/setupCommands.ts +++ b/src/cli/setupCommands.ts @@ -5,12 +5,12 @@ import { runDoctor, type DoctorScope, } from "../application/Doctor.js"; +import { runSetup } from "../application/Setup.js"; +import { systemSetupHost } from "../application/SetupHost.js"; import { - runSetup, isSetupFailure, - systemSetupHost, type SetupOptions, -} from "../application/Setup.js"; +} from "../application/SetupTypes.js"; import { isUninstallFailure, runUninstall } from "../application/Uninstall.js"; import { isUpdateFailure, runUpdate } from "../application/Update.js"; import { systemUpdateHost } from "../application/UpdateRuntime.js"; diff --git a/src/cliLogging.test.ts b/src/cliLogging.test.ts index 5d93b19ea4..f8b3a1dbc6 100644 --- a/src/cliLogging.test.ts +++ b/src/cliLogging.test.ts @@ -8,11 +8,8 @@ import { import { isCliOperationFailure, logCliCommand } from "./cliLogging.js"; import { silentLogger } from "./logger.js"; -import { - isSetupFailure, - runSetup, - type SetupResult, -} from "./application/Setup.js"; +import { runSetup } from "./application/Setup.js"; +import { isSetupFailure, type SetupResult } from "./application/SetupTypes.js"; import { FakeSetupHost, options as setupOptions, diff --git a/src/cliSetup.ts b/src/cliSetup.ts index 9bd412ee9f..e378985f6b 100644 --- a/src/cliSetup.ts +++ b/src/cliSetup.ts @@ -14,7 +14,7 @@ import type { SetupClientState, SetupProgressEvent, SetupResult, -} from "./application/Setup.js"; +} from "./application/SetupTypes.js"; import { SUPPORTED_CLIENT_DEFINITIONS } from "./application/SupportedClients.js"; const clientDisplayNames: ReadonlyMap = new Map( diff --git a/src/process/ProcessOwnership.part2.test.ts b/src/process/ProcessOwnership.part2.test.ts index 7dab7d98c7..7e6e81b4cb 100644 --- a/src/process/ProcessOwnership.part2.test.ts +++ b/src/process/ProcessOwnership.part2.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from "vitest"; import { cleanupOwnedProcessGroup, - observeOwnedProcessLineage, verifyNoTokenOwnedProcesses, type ProcessOwnershipHost, } from "./ProcessOwnership.js"; +import { observeOwnedProcessLineage } from "./ProcessOwnershipObservation.js"; const ownership = { runId: "run-token", leaderPid: 100, diff --git a/src/process/ProcessOwnership.part3.test.ts b/src/process/ProcessOwnership.part3.test.ts index d8b1ae2a4a..b782c6f77a 100644 --- a/src/process/ProcessOwnership.part3.test.ts +++ b/src/process/ProcessOwnership.part3.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { - observeOwnedProcessLineage, - type ProcessOwnershipHost, -} from "./ProcessOwnership.js"; +import { type ProcessOwnershipHost } from "./ProcessOwnership.js"; +import { observeOwnedProcessLineage } from "./ProcessOwnershipObservation.js"; const ownership = { runId: "run-token", leaderPid: 100, diff --git a/src/process/ProcessOwnership.test.ts b/src/process/ProcessOwnership.test.ts index c1e86cf805..d565f0494d 100644 --- a/src/process/ProcessOwnership.test.ts +++ b/src/process/ProcessOwnership.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from "vitest"; import { cleanupOwnedProcessGroup, - parseProcessEnvironment, - selectCapturedProcessGroupIds, type ProcessOwnershipHost, } from "./ProcessOwnership.js"; +import { parseProcessEnvironment } from "./ProcessOwnershipObservation.js"; +import { selectCapturedProcessGroupIds } from "./ProcessOwnershipProcessTree.js"; import { matchesOwnedProcessCommand } from "./ProcessCommandIdentity.js"; const ownership = { runId: "run-token", diff --git a/src/process/ProcessOwnership.ts b/src/process/ProcessOwnership.ts index 8fbaa1022c..9b00dd3fd8 100644 --- a/src/process/ProcessOwnership.ts +++ b/src/process/ProcessOwnership.ts @@ -1,14 +1,12 @@ import { execFile } from "node:child_process"; -import { readFile } from "node:fs/promises"; import { promisify } from "node:util"; import { - observeOwnedProcessGroupWithHost, - observeOwnedProcessLineageWithHost, + createSystemProcessOwnershipHost, + errorMessage, } from "./ProcessOwnershipObservation.js"; import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js"; import { descendantsOf, liveProcesses } from "./ProcessOwnershipProcessTree.js"; -import { execFileOutput } from "./ExecFileOutput.js"; const execFileAsync = promisify(execFile); @@ -127,86 +125,6 @@ export const cleanupWindowsProcessTree = async ( } }; -/** Select the PTY root group and groups led by an observed captured process. */ -export const selectCapturedProcessGroupIds = ( - rootPid: number, - samples: readonly { - readonly pid: number; - readonly process_group_id: number | null; - }[], -): readonly number[] => { - const processGroupIds = new Set([rootPid]); - for (const sample of samples) { - // A sampled member may transiently inherit an unrelated group. POSIX group - // leaders have pid === pgid, so only that observation establishes that the - // group leader itself belonged to the captured tree. Token checks below - // remain the final authority immediately before observation or signaling. - if (sample.pid === sample.process_group_id) processGroupIds.add(sample.pid); - } - return [...processGroupIds]; -}; - -/** Parse the NUL-delimited Linux process environment without nameless keys. */ -export const parseProcessEnvironment = ( - value: string, -): Readonly> => - Object.fromEntries( - value - .split("\0") - .filter((entry) => entry.indexOf("=") > 0) - .map((entry) => { - const separator = entry.indexOf("="); - return [entry.slice(0, separator), entry.slice(separator + 1)]; - }), - ); - -/** Create the operating-system process inspector for an explicit host context. */ -export const createSystemProcessOwnershipHost = ( - platform: NodeJS.Platform = process.platform, - hostEnvironment: NodeJS.ProcessEnv = process.env, -): ProcessOwnershipHost => ({ - platform, - async listProcesses() { - if (platform === "win32") return []; - const { stdout } = await execFileOutput( - "ps", - ["-axo", "pid=,ppid=,pgid=,stat=,command="], - { env: hostEnvironment }, - ); - return stdout - .split("\n") - .map((line) => /\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)/u.exec(line)) - .filter((match): match is RegExpExecArray => match !== null) - .map((match) => ({ - pid: Number(match[1]), - parentPid: Number(match[2]), - processGroupId: Number(match[3]), - state: match[4] ?? "", - command: match[5] ?? "", - })); - }, - async environment(pid) { - if (platform === "linux") - return parseProcessEnvironment( - await readFile(`/proc/${pid}/environ`, "utf8"), - ); - const { stdout } = await execFileOutput("ps", ["eww", "-p", String(pid)], { - env: hostEnvironment, - }); - const observedEnvironment: Record = {}; - for (const match of stdout.matchAll( - /(?:^|\s)([A-Za-z_][A-Za-z0-9_]*)=([^\s]*)/gu, - )) { - const name = match[1]; - if (name !== undefined) observedEnvironment[name] = match[2] ?? ""; - } - return observedEnvironment; - }, - signalGroup(processGroupId, signal) { - process.kill(-processGroupId, signal); - }, -}); - const systemHost = createSystemProcessOwnershipHost(); const systemWindowsProcessTreeHost: WindowsProcessTreeHost = { @@ -519,26 +437,3 @@ const scanTokenOwnedProcesses = async ( } return { owned, failures }; }; - -const errorMessage = (cause: unknown): string => - cause instanceof Error ? cause.message : String(cause); - -/** Observe one group without signaling it, failing closed on identity doubt. */ -export const observeOwnedProcessGroup = async ( - ownership: OwnedProcessGroup, - host: ProcessOwnershipHost = systemHost, -): Promise => - observeOwnedProcessGroupWithHost(ownership, host); - -/** - * Record the live launcher and descendant lineage after run-token validation. - * - * The observation is intentionally point-in-time. A verified empty descendant - * list means no descendants were live during this observation, not that the - * run never created a short-lived child. - */ -export const observeOwnedProcessLineage = async ( - ownership: OwnedProcessGroup, - host: ProcessOwnershipHost = systemHost, -): Promise => - observeOwnedProcessLineageWithHost(ownership, host); diff --git a/src/process/ProcessOwnership.validation.test.ts b/src/process/ProcessOwnership.validation.test.ts index e22a27a84e..0ba1aa2072 100644 --- a/src/process/ProcessOwnership.validation.test.ts +++ b/src/process/ProcessOwnership.validation.test.ts @@ -2,12 +2,14 @@ import { describe, expect, it, vi } from "vitest"; import { cleanupOwnedProcessGroup, cleanupWindowsProcessTree, - createSystemProcessOwnershipHost, - observeOwnedProcessGroup, - observeOwnedProcessLineage, type ProcessOwnershipHost, type WindowsProcessTreeHost, } from "./ProcessOwnership.js"; +import { + createSystemProcessOwnershipHost, + observeOwnedProcessGroup, + observeOwnedProcessLineage, +} from "./ProcessOwnershipObservation.js"; import { host, ownership } from "./ProcessOwnership.fixture.js"; describe("owned process-group cleanup validation: ownership and lineage", () => { diff --git a/src/process/ProcessOwnershipObservation.ts b/src/process/ProcessOwnershipObservation.ts index 4c1f80b735..f1cd15e731 100644 --- a/src/process/ProcessOwnershipObservation.ts +++ b/src/process/ProcessOwnershipObservation.ts @@ -1,5 +1,8 @@ +import { readFile } from "node:fs/promises"; + import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js"; import { descendantsOf, liveProcesses } from "./ProcessOwnershipProcessTree.js"; +import { execFileOutput } from "./ExecFileOutput.js"; import type { OwnedProcessGroup, ProcessGroupObservation, @@ -127,6 +130,89 @@ const unavailableLineage = ( reason, }); +/** Parse the NUL-delimited Linux process environment without nameless keys. */ +export const parseProcessEnvironment = ( + value: string, +): Readonly> => + Object.fromEntries( + value + .split("\0") + .filter((entry) => entry.indexOf("=") > 0) + .map((entry) => { + const separator = entry.indexOf("="); + return [entry.slice(0, separator), entry.slice(separator + 1)]; + }), + ); + +/** Create the operating-system process inspector for an explicit host context. */ +export const createSystemProcessOwnershipHost = ( + platform: NodeJS.Platform = process.platform, + hostEnvironment: NodeJS.ProcessEnv = process.env, +): ProcessOwnershipHost => ({ + platform, + async listProcesses() { + if (platform === "win32") return []; + const { stdout } = await execFileOutput( + "ps", + ["-axo", "pid=,ppid=,pgid=,stat=,command="], + { env: hostEnvironment }, + ); + return stdout + .split("\n") + .map((line) => /\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)/u.exec(line)) + .filter((match): match is RegExpExecArray => match !== null) + .map((match) => ({ + pid: Number(match[1]), + parentPid: Number(match[2]), + processGroupId: Number(match[3]), + state: match[4] ?? "", + command: match[5] ?? "", + })); + }, + async environment(pid) { + if (platform === "linux") + return parseProcessEnvironment( + await readFile(`/proc/${pid}/environ`, "utf8"), + ); + const { stdout } = await execFileOutput("ps", ["eww", "-p", String(pid)], { + env: hostEnvironment, + }); + const observedEnvironment: Record = {}; + for (const match of stdout.matchAll( + /(?:^|\s)([A-Za-z_][A-Za-z0-9_]*)=([^\s]*)/gu, + )) { + const name = match[1]; + if (name !== undefined) observedEnvironment[name] = match[2] ?? ""; + } + return observedEnvironment; + }, + signalGroup(processGroupId, signal) { + process.kill(-processGroupId, signal); + }, +}); + +const systemHost = createSystemProcessOwnershipHost(); + +/** Observe one group without signaling it, failing closed on identity doubt. */ +export const observeOwnedProcessGroup = async ( + ownership: OwnedProcessGroup, + host: ProcessOwnershipHost = systemHost, +): Promise => + observeOwnedProcessGroupWithHost(ownership, host); + +/** + * Record the live launcher and descendant lineage after run-token validation. + * + * The observation is intentionally point-in-time. A verified empty descendant + * list means no descendants were live during this observation, not that the + * run never created a short-lived child. + */ +export const observeOwnedProcessLineage = async ( + ownership: OwnedProcessGroup, + host: ProcessOwnershipHost = systemHost, +): Promise => + observeOwnedProcessLineageWithHost(ownership, host); + const processIsGone = async ( host: ProcessOwnershipHost, pid: number, @@ -140,5 +226,5 @@ const processIsGone = async ( } }; -const errorMessage = (cause: unknown): string => +export const errorMessage = (cause: unknown): string => cause instanceof Error ? cause.message : String(cause); diff --git a/src/process/ProcessOwnershipProcessTree.ts b/src/process/ProcessOwnershipProcessTree.ts index bdab7af251..fd57038d7e 100644 --- a/src/process/ProcessOwnershipProcessTree.ts +++ b/src/process/ProcessOwnershipProcessTree.ts @@ -28,3 +28,22 @@ export const descendantsOf = ( } return descendants; }; + +/** Select the PTY root group and groups led by an observed captured process. */ +export const selectCapturedProcessGroupIds = ( + rootPid: number, + samples: readonly { + readonly pid: number; + readonly process_group_id: number | null; + }[], +): readonly number[] => { + const processGroupIds = new Set([rootPid]); + for (const sample of samples) { + // A sampled member may transiently inherit an unrelated group. POSIX group + // leaders have pid === pgid, so only that observation establishes that the + // group leader itself belonged to the captured tree. Token checks below + // remain the final authority immediately before observation or signaling. + if (sample.pid === sample.process_group_id) processGroupIds.add(sample.pid); + } + return [...processGroupIds]; +}; diff --git a/src/process/ProviderRunLineage.ts b/src/process/ProviderRunLineage.ts index 893c6956b9..b97c9961a1 100644 --- a/src/process/ProviderRunLineage.ts +++ b/src/process/ProviderRunLineage.ts @@ -1,5 +1,5 @@ import type { ProcessLineageObservation } from "./ProcessOwnership.js"; -import { observeOwnedProcessLineage } from "./ProcessOwnership.js"; +import { observeOwnedProcessLineage } from "./ProcessOwnershipObservation.js"; import type { ProviderProcessLaunch } from "./ProviderProcess.js"; /** Retain the latest truthful process-lineage observation for one client. */ diff --git a/tests/boundary/filesystem/clientLifecycle.test.ts b/tests/boundary/filesystem/clientLifecycle.test.ts index b76cca95f5..410554f57e 100644 --- a/tests/boundary/filesystem/clientLifecycle.test.ts +++ b/tests/boundary/filesystem/clientLifecycle.test.ts @@ -14,10 +14,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { supportedClients } from "../../../src/application/SupportedClients.js"; -import { - configureTomlClient, - detectClients, -} from "../../../src/application/Setup.js"; +import { configureTomlClient } from "../../../src/application/SetupClientConfiguration.js"; +import { detectClients } from "../../../src/application/SetupHost.js"; import { runUninstall, systemUninstallHost, diff --git a/tests/boundary/filesystem/setupConfig.test.ts b/tests/boundary/filesystem/setupConfig.test.ts index 9161aa6f8b..4019e5cf2b 100644 --- a/tests/boundary/filesystem/setupConfig.test.ts +++ b/tests/boundary/filesystem/setupConfig.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { resolveClientConfigTransactionPath } from "../../../src/application/ClientConfigPath.js"; -import { configureJsonClient } from "../../../src/application/Setup.js"; +import { configureJsonClient } from "../../../src/application/SetupClientConfiguration.js"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; const pinnedPackage = PRODUCT_IDENTITY.registrationPackageSpecifier; diff --git a/tests/boundary/filesystem/setupSkill.test.ts b/tests/boundary/filesystem/setupSkill.test.ts index 9e3b6d41e2..a2ec56993c 100644 --- a/tests/boundary/filesystem/setupSkill.test.ts +++ b/tests/boundary/filesystem/setupSkill.test.ts @@ -6,7 +6,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { canonicalSkillNeedsInstall, installCanonicalSkill, -} from "../../../src/application/Setup.js"; +} from "../../../src/application/SetupSkill.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; describe("canonical skill transaction", () => { diff --git a/tests/boundary/process/providerProcess.test.ts b/tests/boundary/process/providerProcess.test.ts index 2eca8d8a72..b3b1788c6f 100644 --- a/tests/boundary/process/providerProcess.test.ts +++ b/tests/boundary/process/providerProcess.test.ts @@ -7,10 +7,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { PendingOperations } from "../../../src/process/PendingOperations.js"; import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; import { ProviderStartupDeadline } from "../../../src/process/ProviderDeadline.js"; -import { - cleanupOwnedProcessGroup, - observeOwnedProcessLineage, -} from "../../../src/process/ProcessOwnership.js"; +import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js"; +import { observeOwnedProcessLineage } from "../../../src/process/ProcessOwnershipObservation.js"; import { ProviderProcessSupervisor, spawnOwnedProviderProcess, diff --git a/tests/process-global/cliLogging.test.ts b/tests/process-global/cliLogging.test.ts index 3258adfe04..591f5a040e 100644 --- a/tests/process-global/cliLogging.test.ts +++ b/tests/process-global/cliLogging.test.ts @@ -6,7 +6,8 @@ import { ArtifactOperationError, projectAnalysisError, } from "../../src/domain/errors.js"; -import { isSetupFailure, runSetup } from "../../src/application/Setup.js"; +import { runSetup } from "../../src/application/Setup.js"; +import { isSetupFailure } from "../../src/application/SetupTypes.js"; import { FakeSetupHost, options as setupOptions,