| author | |
| committer | |
| log | 42ea7cfae16dd55eb004314781403386cd6965dc |
| tree | fa23a0525e3da3b7833525f794a44e766116b0c6 |
| parent | d2e6c82709230e8b2772cee2640d36d9b745274e |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 29 insertions(+), 31 deletions(-)
apps/server/src/processRunner.ts+1-1| ... | ... | @@ -37,7 +37,7 @@ function normalizeSpawnError(command: string, args: readonly string[], error: un |
| 37 | 37 | return new Error(`Failed to run ${commandLabel(command, args)}: ${error.message}`); |
| 38 | 38 | } |
| 39 | 39 | |
| 40 | function isWindowsCommandNotFound(code: number | null, stderr: string): boolean { | |
| 40 | export function isWindowsCommandNotFound(code: number | null, stderr: string): boolean { | |
| 41 | 41 | if (process.platform !== "win32") return false; |
| 42 | 42 | if (code === 9009) return true; |
| 43 | 43 | return /is not recognized as an internal or external command/i.test(stderr); |
apps/server/src/provider/Layers/ClaudeProvider.ts+3-15| ... | ... | @@ -13,13 +13,13 @@ import { resolveContextWindow, resolveEffort } from "@t3tools/shared/model"; |
| 13 | 13 | |
| 14 | 14 | import { |
| 15 | 15 | buildServerProvider, |
| 16 | collectStreamAsString, | |
| 17 | 16 | DEFAULT_TIMEOUT_MS, |
| 18 | 17 | detailFromResult, |
| 19 | 18 | extractAuthBoolean, |
| 20 | 19 | isCommandMissingCause, |
| 21 | 20 | parseGenericCliVersion, |
| 22 | 21 | providerModelsFromSettings, |
| 22 | spawnAndCollect, | |
| 23 | 23 | type CommandResult, |
| 24 | 24 | } from "../providerSnapshot"; |
| 25 | 25 | import { makeManagedServerProvider } from "../makeManagedServerProvider"; |
| ... | ... | @@ -198,7 +198,6 @@ export function parseClaudeAuthStatusFromOutput(result: CommandResult): { |
| 198 | 198 | |
| 199 | 199 | const runClaudeCommand = (args: ReadonlyArray<string>) => |
| 200 | 200 | Effect.gen(function* () { |
| 201 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 202 | 201 | const claudeSettings = yield* Effect.service(ServerSettingsService).pipe( |
| 203 | 202 | Effect.flatMap((service) => service.getSettings), |
| 204 | 203 | Effect.map((settings) => settings.providers.claudeAgent), |
| ... | ... | @@ -206,19 +205,8 @@ const runClaudeCommand = (args: ReadonlyArray<string>) => |
| 206 | 205 | const command = ChildProcess.make(claudeSettings.binaryPath, [...args], { |
| 207 | 206 | shell: process.platform === "win32", |
| 208 | 207 | }); |
| 209 | ||
| 210 | const child = yield* spawner.spawn(command); | |
| 211 | const [stdout, stderr, exitCode] = yield* Effect.all( | |
| 212 | [ | |
| 213 | collectStreamAsString(child.stdout), | |
| 214 | collectStreamAsString(child.stderr), | |
| 215 | child.exitCode.pipe(Effect.map(Number)), | |
| 216 | ], | |
| 217 | { concurrency: "unbounded" }, | |
| 218 | ); | |
| 219 | ||
| 220 | return { stdout, stderr, code: exitCode } satisfies CommandResult; | |
| 221 | }).pipe(Effect.scoped); | |
| 208 | return yield* spawnAndCollect(claudeSettings.binaryPath, command); | |
| 209 | }); | |
| 222 | 210 | |
| 223 | 211 | export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 224 | 212 | function* (): Effect.fn.Return< |
apps/server/src/provider/Layers/CodexProvider.ts+3-15| ... | ... | @@ -14,13 +14,13 @@ import { resolveEffort } from "@t3tools/shared/model"; |
| 14 | 14 | |
| 15 | 15 | import { |
| 16 | 16 | buildServerProvider, |
| 17 | collectStreamAsString, | |
| 18 | 17 | DEFAULT_TIMEOUT_MS, |
| 19 | 18 | detailFromResult, |
| 20 | 19 | extractAuthBoolean, |
| 21 | 20 | isCommandMissingCause, |
| 22 | 21 | parseGenericCliVersion, |
| 23 | 22 | providerModelsFromSettings, |
| 23 | spawnAndCollect, | |
| 24 | 24 | type CommandResult, |
| 25 | 25 | } from "../providerSnapshot"; |
| 26 | 26 | import { makeManagedServerProvider } from "../makeManagedServerProvider"; |
| ... | ... | @@ -292,7 +292,6 @@ export const hasCustomModelProvider = readCodexConfigModelProvider().pipe( |
| 292 | 292 | |
| 293 | 293 | const runCodexCommand = (args: ReadonlyArray<string>) => |
| 294 | 294 | Effect.gen(function* () { |
| 295 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 296 | 295 | const settingsService = yield* ServerSettingsService; |
| 297 | 296 | const codexSettings = yield* settingsService.getSettings.pipe( |
| 298 | 297 | Effect.map((settings) => settings.providers.codex), |
| ... | ... | @@ -304,19 +303,8 @@ const runCodexCommand = (args: ReadonlyArray<string>) => |
| 304 | 303 | ...(codexSettings.homePath ? { CODEX_HOME: codexSettings.homePath } : {}), |
| 305 | 304 | }, |
| 306 | 305 | }); |
| 307 | ||
| 308 | const child = yield* spawner.spawn(command); | |
| 309 | const [stdout, stderr, exitCode] = yield* Effect.all( | |
| 310 | [ | |
| 311 | collectStreamAsString(child.stdout), | |
| 312 | collectStreamAsString(child.stderr), | |
| 313 | child.exitCode.pipe(Effect.map(Number)), | |
| 314 | ], | |
| 315 | { concurrency: "unbounded" }, | |
| 316 | ); | |
| 317 | ||
| 318 | return { stdout, stderr, code: exitCode } satisfies CommandResult; | |
| 319 | }).pipe(Effect.scoped); | |
| 306 | return yield* spawnAndCollect(codexSettings.binaryPath, command); | |
| 307 | }); | |
| 320 | 308 | |
| 321 | 309 | export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")( |
| 322 | 310 | function* (): Effect.fn.Return< |
apps/server/src/provider/providerSnapshot.ts+22| ... | ... | @@ -5,7 +5,9 @@ import type { |
| 5 | 5 | ServerProviderState, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | import { Effect, Stream } from "effect"; |
| 8 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | |
| 8 | 9 | import { normalizeModelSlug } from "@t3tools/shared/model"; |
| 10 | import { isWindowsCommandNotFound } from "../processRunner"; | |
| 9 | 11 | |
| 10 | 12 | export const DEFAULT_TIMEOUT_MS = 4_000; |
| 11 | 13 | |
| ... | ... | @@ -35,6 +37,26 @@ export function isCommandMissingCause(error: unknown): boolean { |
| 35 | 37 | return lower.includes("enoent") || lower.includes("notfound"); |
| 36 | 38 | } |
| 37 | 39 | |
| 40 | export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Command) => | |
| 41 | Effect.gen(function* () { | |
| 42 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 43 | const child = yield* spawner.spawn(command); | |
| 44 | const [stdout, stderr, exitCode] = yield* Effect.all( | |
| 45 | [ | |
| 46 | collectStreamAsString(child.stdout), | |
| 47 | collectStreamAsString(child.stderr), | |
| 48 | child.exitCode.pipe(Effect.map(Number)), | |
| 49 | ], | |
| 50 | { concurrency: "unbounded" }, | |
| 51 | ); | |
| 52 | ||
| 53 | const result: CommandResult = { stdout, stderr, code: exitCode }; | |
| 54 | if (isWindowsCommandNotFound(exitCode, stderr)) { | |
| 55 | return yield* Effect.fail(new Error(`spawn ${binaryPath} ENOENT`)); | |
| 56 | } | |
| 57 | return result; | |
| 58 | }).pipe(Effect.scoped); | |
| 59 | ||
| 38 | 60 | export function detailFromResult( |
| 39 | 61 | result: CommandResult & { readonly timedOut?: boolean }, |
| 40 | 62 | ): string | undefined { |