Skip to content

Commit e84adeb

Browse files
committed
fix(server): detect OpenCode version via spawn spec
1 parent d45a1ff commit e84adeb

3 files changed

Lines changed: 74 additions & 53 deletions

File tree

packages/server/src/server/routes/settings.ts

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { FastifyInstance } from "fastify"
22
import { z } from "zod"
3-
import { spawnSync } from "child_process"
4-
import { buildSpawnSpec } from "../../workspaces/runtime"
3+
import { probeBinaryVersion } from "../../workspaces/runtime"
54
import type { SettingsService } from "../../settings/service"
65
import type { Logger } from "../../logger"
76

@@ -15,37 +14,8 @@ const ValidateBinarySchema = z.object({
1514
})
1615

1716
function validateBinaryPath(binaryPath: string): { valid: boolean; version?: string; error?: string } {
18-
if (!binaryPath) {
19-
return { valid: false, error: "Missing binary path" }
20-
}
21-
22-
const spec = buildSpawnSpec(binaryPath, ["--version"])
23-
try {
24-
const result = spawnSync(spec.command, spec.args, {
25-
encoding: "utf8",
26-
windowsVerbatimArguments: Boolean((spec.options as { windowsVerbatimArguments?: boolean }).windowsVerbatimArguments),
27-
})
28-
29-
if (result.error) {
30-
return { valid: false, error: result.error.message }
31-
}
32-
if (result.status !== 0) {
33-
const stderr = result.stderr?.trim()
34-
const stdout = result.stdout?.trim()
35-
const combined = stderr || stdout
36-
const error = combined ? `Exited with code ${result.status}: ${combined}` : `Exited with code ${result.status}`
37-
return { valid: false, error }
38-
}
39-
40-
const stdout = (result.stdout ?? "").trim()
41-
const firstLine = stdout.split(/\r?\n/).find((line) => line.trim().length > 0)
42-
const normalized = firstLine?.trim()
43-
const versionMatch = normalized?.match(/([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/)
44-
const version = versionMatch?.[1]
45-
return { valid: true, version }
46-
} catch (error) {
47-
return { valid: false, error: error instanceof Error ? error.message : String(error) }
48-
}
17+
const result = probeBinaryVersion(binaryPath)
18+
return { valid: result.valid, version: result.version, error: result.error }
4919
}
5020

5121
export function registerSettingsRoutes(app: FastifyInstance, deps: RouteDeps) {

packages/server/src/workspaces/manager.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FileSystemBrowser } from "../filesystem/browser"
88
import { searchWorkspaceFiles, WorkspaceFileSearchOptions } from "../filesystem/search"
99
import { clearWorkspaceSearchCache } from "../filesystem/search-cache"
1010
import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types"
11-
import { WorkspaceRuntime, ProcessExitInfo } from "./runtime"
11+
import { WorkspaceRuntime, ProcessExitInfo, probeBinaryVersion } from "./runtime"
1212
import { Logger } from "../logger"
1313
import { getOpencodeConfigDir } from "../opencode-config.js"
1414
import {
@@ -283,28 +283,22 @@ export class WorkspaceManager {
283283
return undefined
284284
}
285285

286-
try {
287-
const result = spawnSync(resolvedPath, ["--version"], { encoding: "utf8" })
288-
if (result.status === 0 && result.stdout) {
289-
const line = result.stdout.split(/\r?\n/).find((entry) => entry.trim().length > 0)
290-
if (line) {
291-
const normalized = line.trim()
292-
const versionMatch = normalized.match(/([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/)
293-
if (versionMatch) {
294-
const version = versionMatch[1]
295-
this.options.logger.debug({ binary: resolvedPath, version }, "Detected binary version")
296-
return version
297-
}
298-
this.options.logger.debug({ binary: resolvedPath, reported: normalized }, "Binary reported version string")
299-
return normalized
300-
}
301-
} else if (result.error) {
302-
this.options.logger.warn({ binary: resolvedPath, err: result.error }, "Failed to read binary version")
286+
const result = probeBinaryVersion(resolvedPath)
287+
if (result.valid) {
288+
if (result.version) {
289+
this.options.logger.debug({ binary: resolvedPath, version: result.version }, "Detected binary version")
290+
return result.version
303291
}
304-
} catch (error) {
305-
this.options.logger.warn({ binary: resolvedPath, err: error }, "Failed to detect binary version")
292+
if (result.reported) {
293+
this.options.logger.debug({ binary: resolvedPath, reported: result.reported }, "Binary reported version string")
294+
return result.reported
295+
}
296+
return undefined
306297
}
307298

299+
if (result.error) {
300+
this.options.logger.warn({ binary: resolvedPath, err: result.error }, "Failed to detect binary version")
301+
}
308302
return undefined
309303
}
310304

packages/server/src/workspaces/runtime.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { Logger } from "../logger"
88
export const WINDOWS_CMD_EXTENSIONS = new Set([".cmd", ".bat"])
99
export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"])
1010

11+
const VERSION_REGEX = /([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/
12+
1113
export function buildSpawnSpec(binaryPath: string, args: string[]) {
1214
if (process.platform !== "win32") {
1315
return { command: binaryPath, args, options: {} as const }
@@ -40,6 +42,61 @@ export function buildSpawnSpec(binaryPath: string, args: string[]) {
4042
return { command: binaryPath, args, options: {} as const }
4143
}
4244

45+
export function probeBinaryVersion(binaryPath: string): {
46+
valid: boolean
47+
version?: string
48+
reported?: string
49+
error?: string
50+
} {
51+
if (!binaryPath) {
52+
return { valid: false, error: "Missing binary path" }
53+
}
54+
55+
const spec = buildSpawnSpec(binaryPath, ["--version"])
56+
57+
try {
58+
const result = spawnSync(spec.command, spec.args, {
59+
encoding: "utf8",
60+
windowsVerbatimArguments: Boolean(
61+
(spec.options as { windowsVerbatimArguments?: boolean }).windowsVerbatimArguments,
62+
),
63+
})
64+
65+
if (result.error) {
66+
return { valid: false, error: result.error.message }
67+
}
68+
69+
if (result.status !== 0) {
70+
const stderr = result.stderr?.trim()
71+
const stdout = result.stdout?.trim()
72+
const combined = stderr || stdout
73+
const error = combined ? `Exited with code ${result.status}: ${combined}` : `Exited with code ${result.status}`
74+
return { valid: false, error }
75+
}
76+
77+
const stdoutLines = String(result.stdout ?? "")
78+
.split(/\r?\n/)
79+
.map((line) => line.trim())
80+
.filter((line) => line.length > 0)
81+
const stderrLines = String(result.stderr ?? "")
82+
.split(/\r?\n/)
83+
.map((line) => line.trim())
84+
.filter((line) => line.length > 0)
85+
86+
// Prefer stdout; fall back to stderr (some tools report version there).
87+
const reported = stdoutLines[0] ?? stderrLines[0]
88+
if (!reported) {
89+
return { valid: true }
90+
}
91+
92+
const versionMatch = reported.match(VERSION_REGEX)
93+
const version = versionMatch?.[1]
94+
return { valid: true, version, reported }
95+
} catch (error) {
96+
return { valid: false, error: error instanceof Error ? error.message : String(error) }
97+
}
98+
}
99+
43100
const SENSITIVE_ENV_KEY = /(PASSWORD|TOKEN|SECRET)/i
44101

45102
function redactEnvironment(env: Record<string, string | undefined>): Record<string, string | undefined> {

0 commit comments

Comments
 (0)