Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/server/src/processRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function normalizeSpawnError(command: string, args: readonly string[], error: un
return new Error(`Failed to run ${commandLabel(command, args)}: ${error.message}`);
}

function isWindowsCommandNotFound(code: number | null, stderr: string): boolean {
export function isWindowsCommandNotFound(code: number | null, stderr: string): boolean {
if (process.platform !== "win32") return false;
if (code === 9009) return true;
return /is not recognized as an internal or external command/i.test(stderr);
Expand Down
18 changes: 3 additions & 15 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import { resolveContextWindow, resolveEffort } from "@t3tools/shared/model";

import {
buildServerProvider,
collectStreamAsString,
DEFAULT_TIMEOUT_MS,
detailFromResult,
extractAuthBoolean,
isCommandMissingCause,
parseGenericCliVersion,
providerModelsFromSettings,
spawnAndCollect,
type CommandResult,
} from "../providerSnapshot";
import { makeManagedServerProvider } from "../makeManagedServerProvider";
Expand Down Expand Up @@ -198,27 +198,15 @@ export function parseClaudeAuthStatusFromOutput(result: CommandResult): {

const runClaudeCommand = (args: ReadonlyArray<string>) =>
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const claudeSettings = yield* Effect.service(ServerSettingsService).pipe(
Effect.flatMap((service) => service.getSettings),
Effect.map((settings) => settings.providers.claudeAgent),
);
const command = ChildProcess.make(claudeSettings.binaryPath, [...args], {
shell: process.platform === "win32",
});

const child = yield* spawner.spawn(command);
const [stdout, stderr, exitCode] = yield* Effect.all(
[
collectStreamAsString(child.stdout),
collectStreamAsString(child.stderr),
child.exitCode.pipe(Effect.map(Number)),
],
{ concurrency: "unbounded" },
);

return { stdout, stderr, code: exitCode } satisfies CommandResult;
}).pipe(Effect.scoped);
return yield* spawnAndCollect(claudeSettings.binaryPath, command);
});

export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
function* (): Effect.fn.Return<
Expand Down
18 changes: 3 additions & 15 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import { resolveEffort } from "@t3tools/shared/model";

import {
buildServerProvider,
collectStreamAsString,
DEFAULT_TIMEOUT_MS,
detailFromResult,
extractAuthBoolean,
isCommandMissingCause,
parseGenericCliVersion,
providerModelsFromSettings,
spawnAndCollect,
type CommandResult,
} from "../providerSnapshot";
import { makeManagedServerProvider } from "../makeManagedServerProvider";
Expand Down Expand Up @@ -292,7 +292,6 @@ export const hasCustomModelProvider = readCodexConfigModelProvider().pipe(

const runCodexCommand = (args: ReadonlyArray<string>) =>
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const settingsService = yield* ServerSettingsService;
const codexSettings = yield* settingsService.getSettings.pipe(
Effect.map((settings) => settings.providers.codex),
Expand All @@ -304,19 +303,8 @@ const runCodexCommand = (args: ReadonlyArray<string>) =>
...(codexSettings.homePath ? { CODEX_HOME: codexSettings.homePath } : {}),
},
});

const child = yield* spawner.spawn(command);
const [stdout, stderr, exitCode] = yield* Effect.all(
[
collectStreamAsString(child.stdout),
collectStreamAsString(child.stderr),
child.exitCode.pipe(Effect.map(Number)),
],
{ concurrency: "unbounded" },
);

return { stdout, stderr, code: exitCode } satisfies CommandResult;
}).pipe(Effect.scoped);
return yield* spawnAndCollect(codexSettings.binaryPath, command);
});

export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(
function* (): Effect.fn.Return<
Expand Down
22 changes: 22 additions & 0 deletions apps/server/src/provider/providerSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import type {
ServerProviderState,
} from "@t3tools/contracts";
import { Effect, Stream } from "effect";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import { normalizeModelSlug } from "@t3tools/shared/model";
import { isWindowsCommandNotFound } from "../processRunner";

export const DEFAULT_TIMEOUT_MS = 4_000;

Expand Down Expand Up @@ -35,6 +37,26 @@ export function isCommandMissingCause(error: unknown): boolean {
return lower.includes("enoent") || lower.includes("notfound");
}

export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Command) =>
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const child = yield* spawner.spawn(command);
const [stdout, stderr, exitCode] = yield* Effect.all(
[
collectStreamAsString(child.stdout),
collectStreamAsString(child.stderr),
child.exitCode.pipe(Effect.map(Number)),
],
{ concurrency: "unbounded" },
);

const result: CommandResult = { stdout, stderr, code: exitCode };
if (isWindowsCommandNotFound(exitCode, stderr)) {
return yield* Effect.fail(new Error(`spawn ${binaryPath} ENOENT`));
}
return result;
}).pipe(Effect.scoped);

export function detailFromResult(
result: CommandResult & { readonly timedOut?: boolean },
): string | undefined {
Expand Down