diff --git a/packages/provider-grok/src/server/driver.test.ts b/packages/provider-grok/src/server/driver.test.ts index 3a080ec60582a..87f60732b65e5 100644 --- a/packages/provider-grok/src/server/driver.test.ts +++ b/packages/provider-grok/src/server/driver.test.ts @@ -172,6 +172,33 @@ it.layer(layerTest)("GrokDriver", (it) => { }); } + it.effect("probes a ~/ binary path from the home directory", () => { + const argv: Array = []; + return Effect.gen(function* () { + const path = yield* Path.Path; + const instance = yield* GrokDriver.create({ + instanceId: ProviderInstanceId.make("grok-home"), + displayName: "Grok test", + enabled: true, + environment: [], + config: { ...GrokDriver.defaultConfig(), binaryPath: "~/bin/grok" }, + }).pipe(Effect.provideService(HostProcess.HomeDirectory, "/test-home")); + yield* instance.snapshot.refresh; + // Agent launches may be wrapped in /bin/sh, so look at every argv element. + expect(argv).toContain(path.join("/test-home", "bin", "grok")); + expect(argv).not.toContain("~/bin/grok"); + }).pipe( + Effect.provideService( + ChildProcessSpawner.ChildProcessSpawner, + stdoutSpawner((command, args) => { + argv.push(command, ...args); + return "grok 1.0.0\n"; + }), + ), + Effect.scoped, + ); + }); + it.effect("stays manual-only when the configured executable does not exist", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/packages/provider-grok/src/server/driver.ts b/packages/provider-grok/src/server/driver.ts index acb8690509e2d..0ff0707e83609 100644 --- a/packages/provider-grok/src/server/driver.ts +++ b/packages/provider-grok/src/server/driver.ts @@ -29,6 +29,8 @@ import { } from "@t3tools/provider-core/server/driver"; import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { discoverGrokSkills } from "./skills.ts"; import { homebrewOwnershipFromCommandPath, @@ -137,7 +139,11 @@ export const GrokDriver: ProviderDriver accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); - const effectiveConfig = { ...config, enabled } satisfies GrokSettings; + const effectiveConfig = { + ...config, + enabled, + binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), + } satisfies GrokSettings; const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution( resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, @@ -154,7 +160,7 @@ export const GrokDriver: ProviderDriver accentColor, environment, enabled, - config, + config: effectiveConfig, }).pipe( Effect.mapError( (cause) => diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index fe0826e7a0026..296fd6ecd09d9 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -88,6 +88,30 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => { }).pipe(Effect.scoped), ); + it.effect("probes and starts a ~/ binary path from the home directory", () => + Effect.gen(function* () { + const binaryPaths: Array = []; + const record = ({ binaryPath }: { readonly binaryPath: string }) => + Effect.sync(() => binaryPaths.push(binaryPath)); + const instance = yield* create({ binaryPath: "~/bin/opencode" }, noHttp).pipe( + Effect.provideService(HostProcess.HomeDirectory, "/test-home"), + Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, { + ...openCode2Runtime, + runOpenCodeCommand: (input) => + record(input).pipe(Effect.andThen(openCode2Runtime.runOpenCodeCommand(input))), + startOpenCodeServerProcess: (input) => + record(input).pipe(Effect.andThen(reachedServer("start"))), + }), + ); + yield* instance.snapshot.refresh; + assert.isNotEmpty(binaryPaths); + assert.deepStrictEqual( + [...new Set(binaryPaths)], + [NodePath.join("/test-home", "bin", "opencode")], + ); + }).pipe(Effect.scoped), + ); + it.effect("answers capability reads without waiting on an unreachable server", () => Effect.gen(function* () { const hang = HttpClient.make(() => Effect.never); diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index 95da30946e138..fbfcc0fcfbcfd 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -50,6 +50,8 @@ import { } from "@t3tools/provider-core/server/driver"; import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { enrichProviderSnapshotWithVersionAdvisory, makeCachedProviderMaintenanceResolution, @@ -207,7 +209,11 @@ export const OpenCodeDriver: ProviderDriver< accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); - const effectiveConfig = { ...config, enabled } satisfies OpenCodeSettings; + const effectiveConfig = { + ...config, + enabled, + binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), + } satisfies OpenCodeSettings; const runtimeProbe = yield* makeOpenCodeRuntimeProbe( probeOpenCodeRuntime(effectiveConfig, processEnv).pipe( Effect.provideService(HttpClient.HttpClient, httpClient), @@ -252,7 +258,7 @@ export const OpenCodeDriver: ProviderDriver< accentColor, environment, enabled, - config, + config: effectiveConfig, }).pipe( Effect.mapError( (cause) => diff --git a/packages/provider-pi/src/server/driver.test.ts b/packages/provider-pi/src/server/driver.test.ts index 087a8089aaee8..6e2e8dec432af 100644 --- a/packages/provider-pi/src/server/driver.test.ts +++ b/packages/provider-pi/src/server/driver.test.ts @@ -1,9 +1,11 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { DEFAULT_SERVER_SETTINGS, ProviderInstanceId } from "@t3tools/contracts"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; @@ -172,6 +174,23 @@ it.layer(layerTest)("PiDriver workspace discovery", (it) => { }).pipe(Effect.scoped), ); + it.effect("launches a ~/ binary path from the home directory", () => + Effect.gen(function* () { + const { spawner, launches } = yield* makePiSpawner(); + const instance = yield* create({ binaryPath: "~/bin/pi" }).pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(HostProcess.HomeDirectory, "/test-home"), + ); + yield* instance.snapshot.refresh; + const path = yield* Path.Path; + assert.isNotEmpty(launches); + assert.deepEqual( + [...new Set(launches.map((launch) => launch.command))], + [path.join("/test-home", "bin", "pi")], + ); + }).pipe(Effect.scoped), + ); + it.effect("does not run a disabled provider's workspace probe", () => Effect.gen(function* () { const instance = yield* create({}, false).pipe( diff --git a/packages/provider-pi/src/server/driver.ts b/packages/provider-pi/src/server/driver.ts index c37e5d757f382..5bd999dd131b4 100644 --- a/packages/provider-pi/src/server/driver.ts +++ b/packages/provider-pi/src/server/driver.ts @@ -28,6 +28,8 @@ import { } from "@t3tools/provider-core/server/driver"; import type { ServerProviderDraft } from "@t3tools/provider-core/server/snapshotProbe"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import { makeCachedProviderMaintenanceResolution, makePackageManagedProviderMaintenanceResolver, @@ -108,7 +110,11 @@ export const PiDriver: ProviderDriver = { accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); - const effectiveConfig = { ...config, enabled } satisfies PiSettings; + const effectiveConfig = { + ...config, + enabled, + binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory), + } satisfies PiSettings; const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution( resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, @@ -126,7 +132,7 @@ export const PiDriver: ProviderDriver = { accentColor, environment, enabled, - config, + config: effectiveConfig, }).pipe( Effect.mapError( (cause) =>