From 1c427364f946776dbba83bb045639f4c38e63479 Mon Sep 17 00:00:00 2001 From: edwin Date: Wed, 29 Jul 2026 11:43:28 -0500 Subject: [PATCH 1/3] fix(server): update Codex through its CLI --- .../src/provider/Drivers/CodexDriver.ts | 12 ++-- .../src/provider/providerMaintenance.test.ts | 59 +++++++++++++++++++ .../src/provider/providerMaintenance.ts | 55 ++++++++++++++++- .../providerMaintenanceRunner.test.ts | 42 +++++++++++++ .../src/provider/providerMaintenanceRunner.ts | 14 ++++- 5 files changed, 173 insertions(+), 9 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 15d7a1ff021..55b140c2c44 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -44,7 +44,7 @@ import type { ServerProviderDraft } from "../providerSnapshot.ts"; import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; import { enrichProviderSnapshotWithVersionAdvisory, - makePackageManagedProviderMaintenanceResolver, + makeSelfUpdatingProviderMaintenanceResolver, resolveProviderMaintenanceCapabilitiesEffect, } from "../providerMaintenance.ts"; import { @@ -60,11 +60,13 @@ import { const decodeCodexSettings = Schema.decodeSync(CodexSettings); const DRIVER_KIND = ProviderDriverKind.make("codex"); -const UPDATE = makePackageManagedProviderMaintenanceResolver({ +const UPDATE = makeSelfUpdatingProviderMaintenanceResolver({ provider: DRIVER_KIND, - npmPackageName: "@openai/codex", - homebrewFormula: "codex", - nativeUpdate: null, + packageName: "@openai/codex", + executable: "codex", + args: ["update"], + lockKey: "codex-self-update", + minimumVersion: "0.126.0", }); /** diff --git a/apps/server/src/provider/providerMaintenance.test.ts b/apps/server/src/provider/providerMaintenance.test.ts index 8937844f613..cc6119a47c8 100644 --- a/apps/server/src/provider/providerMaintenance.test.ts +++ b/apps/server/src/provider/providerMaintenance.test.ts @@ -14,6 +14,7 @@ import { enrichProviderSnapshotWithVersionAdvisory, makePackageManagedProviderMaintenanceResolver, makeProviderMaintenanceCapabilities, + makeSelfUpdatingProviderMaintenanceResolver, makeStaticProviderMaintenanceResolver, normalizeCommandPath, ProviderVersionCache, @@ -68,6 +69,14 @@ const staticToolUpdate = makeStaticProviderMaintenanceResolver( updateLockKey: "static-tool", }), ); +const selfUpdatingTool = makeSelfUpdatingProviderMaintenanceResolver({ + provider: driver("selfUpdatingTool"), + packageName: "@example/self-updating-tool", + executable: "self-updating-tool", + args: ["update"], + lockKey: "self-updating-tool", + minimumVersion: "2.0.0", +}); const installedPackageToolProvider: ServerProvider = { instanceId: ProviderInstanceId.make("packageTool"), driver: driver("packageTool"), @@ -198,6 +207,56 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { }); }); + it("uses the resolved provider binary for self-updates", () => { + expect( + selfUpdatingTool.resolve({ + binaryPath: "self-updating-tool", + resolvedCommandPath: "/opt/tools/self-updating-tool", + }), + ).toEqual({ + provider: driver("selfUpdatingTool"), + packageName: "@example/self-updating-tool", + update: { + command: "/opt/tools/self-updating-tool update", + executable: "/opt/tools/self-updating-tool", + args: ["update"], + lockKey: "self-updating-tool", + minimumVersion: "2.0.0", + }, + }); + }); + + it("keeps self-update guidance manual below the supported provider version", () => { + expect( + createProviderVersionAdvisory({ + driver: driver("selfUpdatingTool"), + currentVersion: "1.9.9", + latestVersion: "2.1.0", + maintenanceCapabilities: selfUpdatingTool.resolve(), + }), + ).toMatchObject({ + status: "behind_latest", + updateCommand: null, + canUpdate: false, + message: "Update this provider manually, then refresh provider status.", + }); + }); + + it("enables self-updates at the minimum supported provider version", () => { + expect( + createProviderVersionAdvisory({ + driver: driver("selfUpdatingTool"), + currentVersion: "2.0.0", + latestVersion: "2.1.0", + maintenanceCapabilities: selfUpdatingTool.resolve(), + }), + ).toMatchObject({ + status: "behind_latest", + updateCommand: "self-updating-tool update", + canUpdate: true, + }); + }); + it.effect( "switches package-managed providers to vite-plus updates when the resolved binary lives in vite-plus global bin", () => diff --git a/apps/server/src/provider/providerMaintenance.ts b/apps/server/src/provider/providerMaintenance.ts index 8645f9f943c..b404d08caac 100644 --- a/apps/server/src/provider/providerMaintenance.ts +++ b/apps/server/src/provider/providerMaintenance.ts @@ -48,6 +48,7 @@ export interface ProviderMaintenanceCommandAction { readonly executable: string; readonly args: ReadonlyArray; readonly lockKey: string; + readonly minimumVersion?: string; } export interface ProviderMaintenanceCapabilityResolutionOptions { @@ -100,6 +101,7 @@ export function makeProviderMaintenanceCapabilities(input: { readonly updateExecutable: string | null; readonly updateArgs: ReadonlyArray; readonly updateLockKey: string | null; + readonly updateMinimumVersion?: string; }): ProviderMaintenanceCapabilities { const update = input.updateExecutable === null || input.updateLockKey === null @@ -109,6 +111,7 @@ export function makeProviderMaintenanceCapabilities(input: { executable: input.updateExecutable, args: input.updateArgs, lockKey: input.updateLockKey, + ...(input.updateMinimumVersion ? { minimumVersion: input.updateMinimumVersion } : {}), }; return { provider: input.provider, @@ -117,6 +120,30 @@ export function makeProviderMaintenanceCapabilities(input: { }; } +export function makeSelfUpdatingProviderMaintenanceResolver(input: { + readonly provider: ProviderDriverKind; + readonly packageName: string | null; + readonly executable: string; + readonly args: ReadonlyArray; + readonly lockKey: string; + readonly minimumVersion?: string; +}): ProviderMaintenanceCapabilitiesResolver { + return { + resolve: (options) => + makeProviderMaintenanceCapabilities({ + provider: input.provider, + packageName: input.packageName, + updateExecutable: + nonEmptyString(options?.resolvedCommandPath) ?? + nonEmptyString(options?.binaryPath) ?? + input.executable, + updateArgs: input.args, + updateLockKey: input.lockKey, + ...(input.minimumVersion ? { updateMinimumVersion: input.minimumVersion } : {}), + }), + }; +} + export function makeManualOnlyProviderMaintenanceCapabilities(input: { readonly provider: ProviderDriverKind; readonly packageName: string | null; @@ -408,18 +435,40 @@ export function createProviderVersionAdvisory(input: { currentVersion: input.currentVersion, latestVersion, }); + const canUpdate = canRunProviderMaintenanceUpdate(capabilities, input.currentVersion); return { status: advisory.status, currentVersion: input.currentVersion, latestVersion, - updateCommand: capabilities.update?.command ?? null, - canUpdate: capabilities.update !== null, + updateCommand: canUpdate ? (capabilities.update?.command ?? null) : null, + canUpdate, checkedAt: input.checkedAt ?? null, - message: advisory.message, + message: + advisory.status === "behind_latest" && !canUpdate + ? "Update this provider manually, then refresh provider status." + : advisory.message, }; } +export function canRunProviderMaintenanceUpdate( + maintenanceCapabilities: ProviderMaintenanceCapabilities, + currentVersion: string | null | undefined, +): boolean { + const update = maintenanceCapabilities.update; + if (!update) { + return false; + } + if (!update.minimumVersion) { + return true; + } + return ( + currentVersion !== null && + currentVersion !== undefined && + compareSemverVersions(currentVersion, update.minimumVersion) >= 0 + ); +} + const fetchNpmLatestVersion = Effect.fn("fetchNpmLatestVersion")(function* (packageName: string) { const client = yield* HttpClient.HttpClient; const request = HttpClientRequest.get( diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index 641c9b52e56..49df33f8171 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -299,6 +299,48 @@ describe("providerMaintenanceRunner", () => { ); }); + it.effect("requires manual updates below a provider self-update minimum version", () => { + const calls: Array<{ command: string; args: ReadonlyArray }> = []; + return Effect.gen(function* () { + const { registry } = yield* makeRegistry({ + ...baseProvider, + version: "0.125.0", + }); + const updater = yield* makeTestRunner({ + ...registry, + getProviderMaintenanceCapabilitiesForInstance: () => + Effect.succeed( + makeProviderMaintenanceCapabilities({ + provider: CODEX_DRIVER, + packageName: "@openai/codex", + updateExecutable: "codex", + updateArgs: ["update"], + updateLockKey: "codex-self-update", + updateMinimumVersion: "0.126.0", + }), + ), + }); + + const exit = yield* updater.updateProvider(CODEX_DRIVER).pipe(Effect.exit); + assert(Exit.isFailure(exit)); + const error = Cause.squash(exit.cause); + assert(isServerProviderUpdateError(error)); + assert.strictEqual(error.reason, "This installed provider version must be updated manually."); + assert.deepStrictEqual(calls, []); + }).pipe( + Effect.provide( + Layer.mergeAll( + NonWindowsPlatform, + latestVersionHttpClient("0.126.0"), + mockSpawnerLayer((command, args) => { + calls.push({ command, args }); + return { stdout: "updated" }; + }), + ), + ), + ); + }); + it.effect( "runs update commands through Effect ChildProcess when no test runner is injected", () => { diff --git a/apps/server/src/provider/providerMaintenanceRunner.ts b/apps/server/src/provider/providerMaintenanceRunner.ts index 3c114dd83d8..8b41e6ac10f 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.ts @@ -23,7 +23,10 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { ProviderRegistry } from "./Services/ProviderRegistry.ts"; import { makeProviderMaintenanceCommandCoordinator } from "./providerMaintenanceCommandCoordinator.ts"; -import { enrichProviderSnapshotWithVersionAdvisory } from "./providerMaintenance.ts"; +import { + canRunProviderMaintenanceUpdate, + enrichProviderSnapshotWithVersionAdvisory, +} from "./providerMaintenance.ts"; import type { ProviderMaintenanceCapabilities } from "./providerMaintenance.ts"; import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError); @@ -304,6 +307,15 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () { reason: "This provider does not support one-click updates.", }); } + const currentProvider = (yield* providerRegistry.getProviders).find( + (candidate) => candidate.driver === provider && candidate.instanceId === instanceId, + ); + if (!canRunProviderMaintenanceUpdate(capabilities, currentProvider?.version)) { + return yield* new ServerProviderUpdateError({ + provider, + reason: "This installed provider version must be updated manually.", + }); + } const setUpdateState = (state: ServerProviderUpdateState | null) => providerRegistry.setProviderMaintenanceActionState({ From aa7c43211e813c7813ac86a5f7b29ceeaf5eba87 Mon Sep 17 00:00:00 2001 From: edwin Date: Wed, 29 Jul 2026 11:53:44 -0500 Subject: [PATCH 2/3] refactor(server): simplify Codex update command --- .../src/provider/Drivers/CodexDriver.ts | 22 ++++++----- .../src/provider/providerMaintenance.test.ts | 38 +++++-------------- .../src/provider/providerMaintenance.ts | 24 ------------ 3 files changed, 23 insertions(+), 61 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 55b140c2c44..803e46ac7a8 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -44,7 +44,8 @@ import type { ServerProviderDraft } from "../providerSnapshot.ts"; import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; import { enrichProviderSnapshotWithVersionAdvisory, - makeSelfUpdatingProviderMaintenanceResolver, + makeProviderMaintenanceCapabilities, + type ProviderMaintenanceCapabilitiesResolver, resolveProviderMaintenanceCapabilitiesEffect, } from "../providerMaintenance.ts"; import { @@ -60,14 +61,17 @@ import { const decodeCodexSettings = Schema.decodeSync(CodexSettings); const DRIVER_KIND = ProviderDriverKind.make("codex"); -const UPDATE = makeSelfUpdatingProviderMaintenanceResolver({ - provider: DRIVER_KIND, - packageName: "@openai/codex", - executable: "codex", - args: ["update"], - lockKey: "codex-self-update", - minimumVersion: "0.126.0", -}); +const UPDATE: ProviderMaintenanceCapabilitiesResolver = { + resolve: (options) => + makeProviderMaintenanceCapabilities({ + provider: DRIVER_KIND, + packageName: "@openai/codex", + updateExecutable: options?.binaryPath?.trim() || "codex", + updateArgs: ["update"], + updateLockKey: "codex-self-update", + updateMinimumVersion: "0.126.0", + }), +}; /** * Services the driver needs to materialize an instance. Surfaced as the diff --git a/apps/server/src/provider/providerMaintenance.test.ts b/apps/server/src/provider/providerMaintenance.test.ts index cc6119a47c8..aaa38939a50 100644 --- a/apps/server/src/provider/providerMaintenance.test.ts +++ b/apps/server/src/provider/providerMaintenance.test.ts @@ -14,7 +14,6 @@ import { enrichProviderSnapshotWithVersionAdvisory, makePackageManagedProviderMaintenanceResolver, makeProviderMaintenanceCapabilities, - makeSelfUpdatingProviderMaintenanceResolver, makeStaticProviderMaintenanceResolver, normalizeCommandPath, ProviderVersionCache, @@ -69,14 +68,16 @@ const staticToolUpdate = makeStaticProviderMaintenanceResolver( updateLockKey: "static-tool", }), ); -const selfUpdatingTool = makeSelfUpdatingProviderMaintenanceResolver({ - provider: driver("selfUpdatingTool"), - packageName: "@example/self-updating-tool", - executable: "self-updating-tool", - args: ["update"], - lockKey: "self-updating-tool", - minimumVersion: "2.0.0", -}); +const selfUpdatingTool = makeStaticProviderMaintenanceResolver( + makeProviderMaintenanceCapabilities({ + provider: driver("selfUpdatingTool"), + packageName: "@example/self-updating-tool", + updateExecutable: "self-updating-tool", + updateArgs: ["update"], + updateLockKey: "self-updating-tool", + updateMinimumVersion: "2.0.0", + }), +); const installedPackageToolProvider: ServerProvider = { instanceId: ProviderInstanceId.make("packageTool"), driver: driver("packageTool"), @@ -207,25 +208,6 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { }); }); - it("uses the resolved provider binary for self-updates", () => { - expect( - selfUpdatingTool.resolve({ - binaryPath: "self-updating-tool", - resolvedCommandPath: "/opt/tools/self-updating-tool", - }), - ).toEqual({ - provider: driver("selfUpdatingTool"), - packageName: "@example/self-updating-tool", - update: { - command: "/opt/tools/self-updating-tool update", - executable: "/opt/tools/self-updating-tool", - args: ["update"], - lockKey: "self-updating-tool", - minimumVersion: "2.0.0", - }, - }); - }); - it("keeps self-update guidance manual below the supported provider version", () => { expect( createProviderVersionAdvisory({ diff --git a/apps/server/src/provider/providerMaintenance.ts b/apps/server/src/provider/providerMaintenance.ts index b404d08caac..65a31f169dc 100644 --- a/apps/server/src/provider/providerMaintenance.ts +++ b/apps/server/src/provider/providerMaintenance.ts @@ -120,30 +120,6 @@ export function makeProviderMaintenanceCapabilities(input: { }; } -export function makeSelfUpdatingProviderMaintenanceResolver(input: { - readonly provider: ProviderDriverKind; - readonly packageName: string | null; - readonly executable: string; - readonly args: ReadonlyArray; - readonly lockKey: string; - readonly minimumVersion?: string; -}): ProviderMaintenanceCapabilitiesResolver { - return { - resolve: (options) => - makeProviderMaintenanceCapabilities({ - provider: input.provider, - packageName: input.packageName, - updateExecutable: - nonEmptyString(options?.resolvedCommandPath) ?? - nonEmptyString(options?.binaryPath) ?? - input.executable, - updateArgs: input.args, - updateLockKey: input.lockKey, - ...(input.minimumVersion ? { updateMinimumVersion: input.minimumVersion } : {}), - }), - }; -} - export function makeManualOnlyProviderMaintenanceCapabilities(input: { readonly provider: ProviderDriverKind; readonly packageName: string | null; From 69fe6105b14dab792ac1683ae457daeee52991d5 Mon Sep 17 00:00:00 2001 From: edwin Date: Wed, 29 Jul 2026 12:21:05 -0500 Subject: [PATCH 3/3] fix(server): harden Codex self-update --- .../src/provider/Drivers/CodexDriver.ts | 1 + .../src/provider/providerMaintenance.test.ts | 14 ++++ .../src/provider/providerMaintenance.ts | 14 ++-- .../providerMaintenanceRunner.test.ts | 72 ++++++++++++++++++- .../src/provider/providerMaintenanceRunner.ts | 23 ++++-- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 803e46ac7a8..b449a88c3e1 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -68,6 +68,7 @@ const UPDATE: ProviderMaintenanceCapabilitiesResolver = { packageName: "@openai/codex", updateExecutable: options?.binaryPath?.trim() || "codex", updateArgs: ["update"], + ...(options?.env ? { updateEnv: options.env } : {}), updateLockKey: "codex-self-update", updateMinimumVersion: "0.126.0", }), diff --git a/apps/server/src/provider/providerMaintenance.test.ts b/apps/server/src/provider/providerMaintenance.test.ts index aaa38939a50..7866af8c30c 100644 --- a/apps/server/src/provider/providerMaintenance.test.ts +++ b/apps/server/src/provider/providerMaintenance.test.ts @@ -239,6 +239,20 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { }); }); + it("keeps self-update guidance manual when the provider version is not semver", () => { + expect( + createProviderVersionAdvisory({ + driver: driver("selfUpdatingTool"), + currentVersion: "unknown", + latestVersion: "2.1.0", + maintenanceCapabilities: selfUpdatingTool.resolve(), + }), + ).toMatchObject({ + updateCommand: null, + canUpdate: false, + }); + }); + it.effect( "switches package-managed providers to vite-plus updates when the resolved binary lives in vite-plus global bin", () => diff --git a/apps/server/src/provider/providerMaintenance.ts b/apps/server/src/provider/providerMaintenance.ts index 65a31f169dc..8e97a3c4dd9 100644 --- a/apps/server/src/provider/providerMaintenance.ts +++ b/apps/server/src/provider/providerMaintenance.ts @@ -3,7 +3,7 @@ import { type ServerProvider, type ServerProviderVersionAdvisory, } from "@t3tools/contracts"; -import { compareSemverVersions } from "@t3tools/shared/semver"; +import { compareSemverVersions, parseSemver } from "@t3tools/shared/semver"; import { resolveCommandPath } from "@t3tools/shared/shell"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; @@ -47,6 +47,7 @@ export interface ProviderMaintenanceCommandAction { readonly command: string; readonly executable: string; readonly args: ReadonlyArray; + readonly env?: NodeJS.ProcessEnv; readonly lockKey: string; readonly minimumVersion?: string; } @@ -100,6 +101,7 @@ export function makeProviderMaintenanceCapabilities(input: { readonly packageName: string | null; readonly updateExecutable: string | null; readonly updateArgs: ReadonlyArray; + readonly updateEnv?: NodeJS.ProcessEnv; readonly updateLockKey: string | null; readonly updateMinimumVersion?: string; }): ProviderMaintenanceCapabilities { @@ -110,6 +112,7 @@ export function makeProviderMaintenanceCapabilities(input: { command: [input.updateExecutable, ...input.updateArgs].join(" "), executable: input.updateExecutable, args: input.updateArgs, + ...(input.updateEnv ? { env: input.updateEnv } : {}), lockKey: input.updateLockKey, ...(input.updateMinimumVersion ? { minimumVersion: input.updateMinimumVersion } : {}), }; @@ -438,11 +441,10 @@ export function canRunProviderMaintenanceUpdate( if (!update.minimumVersion) { return true; } - return ( - currentVersion !== null && - currentVersion !== undefined && - compareSemverVersions(currentVersion, update.minimumVersion) >= 0 - ); + if (!currentVersion || !parseSemver(currentVersion) || !parseSemver(update.minimumVersion)) { + return false; + } + return compareSemverVersions(currentVersion, update.minimumVersion) >= 0; } const fetchNpmLatestVersion = Effect.fn("fetchNpmLatestVersion")(function* (packageName: string) { diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index 49df33f8171..8eb4e12f87d 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -129,6 +129,10 @@ function mockSpawnerLayer( handler: ( command: string, args: ReadonlyArray, + options: { + readonly env?: Record; + readonly extendEnv?: boolean; + }, ) => { readonly stdout?: string; readonly stderr?: string; @@ -142,8 +146,14 @@ function mockSpawnerLayer( const childProcess = command as unknown as { readonly command: string; readonly args: ReadonlyArray; + readonly options: { + readonly env?: Record; + readonly extendEnv?: boolean; + }; }; - return Effect.succeed(mockHandle(handler(childProcess.command, childProcess.args))); + return Effect.succeed( + mockHandle(handler(childProcess.command, childProcess.args, childProcess.options)), + ); }), ); } @@ -341,6 +351,66 @@ describe("providerMaintenanceRunner", () => { ); }); + it.effect("preserves the provider environment when running a self-update", () => { + const calls: Array<{ + command: string; + args: ReadonlyArray; + env: Record | undefined; + extendEnv: boolean | undefined; + }> = []; + const updateEnv = { + PATH: "/custom/codex/bin", + CODEX_HOME: "/custom/codex/home", + }; + return Effect.gen(function* () { + const { registry } = yield* makeRegistry({ + ...baseProvider, + version: "0.126.0", + }); + const updater = yield* makeTestRunner({ + ...registry, + getProviderMaintenanceCapabilitiesForInstance: () => + Effect.succeed( + makeProviderMaintenanceCapabilities({ + provider: CODEX_DRIVER, + packageName: "@openai/codex", + updateExecutable: "codex", + updateArgs: ["update"], + updateEnv, + updateLockKey: "codex-self-update", + updateMinimumVersion: "0.126.0", + }), + ), + }); + + yield* updater.updateProvider(CODEX_DRIVER); + assert.deepStrictEqual(calls, [ + { + command: "codex", + args: ["update"], + env: updateEnv, + extendEnv: true, + }, + ]); + }).pipe( + Effect.provide( + Layer.mergeAll( + NonWindowsPlatform, + latestVersionHttpClient("0.126.0"), + mockSpawnerLayer((command, args, options) => { + calls.push({ + command, + args, + env: options.env, + extendEnv: options.extendEnv, + }); + return { stdout: "updated" }; + }), + ), + ), + ); + }); + it.effect( "runs update commands through Effect ChildProcess when no test runner is injected", () => { diff --git a/apps/server/src/provider/providerMaintenanceRunner.ts b/apps/server/src/provider/providerMaintenanceRunner.ts index 8b41e6ac10f..1f8215cf491 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.ts @@ -76,6 +76,7 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR readonly spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; readonly command: string; readonly args: ReadonlyArray; + readonly env?: NodeJS.ProcessEnv; }) { const collectCommandResult = Effect.fn("ProviderMaintenanceRunner.collectCommandResult")( function* () { @@ -84,9 +85,18 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR // which a bare ChildProcess.spawn cannot launch (spawn npm ENOENT); // resolveSpawnCommand finds the real `.cmd` and routes it through the // shell. On Linux/macOS (incl. the WSL backend) this is a no-op. - const resolved = yield* resolveSpawnCommand(input.command, input.args); + const resolved = yield* resolveSpawnCommand( + input.command, + input.args, + input.env ? { env: input.env, extendEnv: true } : {}, + ); const child = yield* input.spawner - .spawn(ChildProcess.make(resolved.command, resolved.args, { shell: resolved.shell })) + .spawn( + ChildProcess.make(resolved.command, resolved.args, { + ...(input.env ? { env: input.env, extendEnv: true } : {}), + shell: resolved.shell, + }), + ) .pipe( Effect.mapError( (cause) => @@ -204,11 +214,16 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () { const providerRegistry = yield* ProviderRegistry; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const httpClient = yield* HttpClient.HttpClient; - const runMaintenanceCommand = (command: string, args: ReadonlyArray) => + const runMaintenanceCommand = ( + command: string, + args: ReadonlyArray, + env?: NodeJS.ProcessEnv, + ) => runProviderMaintenanceCommandWithSpawner({ spawner, command, args, + ...(env ? { env } : {}), }); const commandCoordinator = yield* makeProviderMaintenanceCommandCoordinator({ makeAlreadyRunningError: () => @@ -351,7 +366,7 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () { }), ); - const result = yield* runMaintenanceCommand(update.executable, update.args); + const result = yield* runMaintenanceCommand(update.executable, update.args, update.env); const finishedAt = yield* nowIso; if (result.timedOut || result.exitCode !== 0) { return yield* finish(