mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
126 lines
4.5 KiB
TypeScript
126 lines
4.5 KiB
TypeScript
import { execFile } from "node:child_process"
|
|
import { existsSync } from "node:fs"
|
|
import { chmod, copyFile, mkdir, rename, rm } from "node:fs/promises"
|
|
import { dirname, join } from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
import { promisify } from "node:util"
|
|
import { app } from "electron"
|
|
|
|
const execFileAsync = promisify(execFile)
|
|
const root = dirname(fileURLToPath(import.meta.url))
|
|
const stateHome = process.env.XDG_STATE_HOME
|
|
const desktopStateNames = ["ai.opencode.desktop.dev", "ai.opencode.desktop.beta", "ai.opencode.desktop"]
|
|
|
|
type Logger = {
|
|
log(message: string, meta?: Record<string, unknown>): void
|
|
error(message: string, meta?: Record<string, unknown>): void
|
|
}
|
|
|
|
export async function startBackgroundCli(logger: Logger, shellStateHome?: string) {
|
|
const bundled = app.isPackaged
|
|
? join(process.resourcesPath, executableName())
|
|
: join(root, "../../resources", executableName())
|
|
logger.log("v2 CLI executable resolved", { bundled, packaged: app.isPackaged })
|
|
const version = await run(bundled, ["--version"], logger)
|
|
const binary = app.isPackaged ? await installCli(bundled, version, logger) : bundled
|
|
|
|
const candidates = [
|
|
...new Set([stateHome, shellStateHome, ...desktopStateNames.map((name) => join(app.getPath("appData"), name))]),
|
|
].filter((candidate) => candidate === undefined || existsSync(candidate))
|
|
const discovered = await Promise.all(
|
|
candidates.map(async (candidate) => ({
|
|
stateHome: candidate,
|
|
url: serviceUrl(await run(binary, ["service", "status"], logger, { stateHome: candidate })),
|
|
})),
|
|
)
|
|
const found = discovered.find((candidate) => candidate.url !== undefined)
|
|
logger.log("v2 CLI background instance checked", {
|
|
detected: Boolean(found),
|
|
...endpoint(found?.url),
|
|
})
|
|
|
|
const daemonStateHome = found?.stateHome ?? stateHome
|
|
const url = await run(binary, ["service", "start"], logger, { stateHome: daemonStateHome })
|
|
const password = await run(binary, ["service", "get", "password"], logger, {
|
|
redact: true,
|
|
stateHome: daemonStateHome,
|
|
})
|
|
logger.log("v2 CLI background service ready", {
|
|
existing: Boolean(found),
|
|
username: "opencode",
|
|
...endpoint(url),
|
|
})
|
|
return {
|
|
url,
|
|
username: "opencode",
|
|
password,
|
|
}
|
|
}
|
|
|
|
async function installCli(source: string, version: string, logger: Logger) {
|
|
const directory = join(app.getPath("userData"), "cli", version.replace(/[^a-zA-Z0-9._-]/g, "-"))
|
|
const destination = join(directory, executableName())
|
|
if (existsSync(destination)) {
|
|
logger.log("v2 CLI staged executable reused", { path: destination, version })
|
|
return destination
|
|
}
|
|
|
|
const temp = destination + `.${process.pid}.tmp`
|
|
await mkdir(directory, { recursive: true })
|
|
await copyFile(source, temp)
|
|
if (process.platform !== "win32") await chmod(temp, 0o755)
|
|
await rename(temp, destination).catch(async (error) => {
|
|
await rm(temp, { force: true })
|
|
throw error
|
|
})
|
|
logger.log("v2 CLI executable staged", { source, path: destination, version })
|
|
return destination
|
|
}
|
|
|
|
async function run(
|
|
binary: string,
|
|
args: string[],
|
|
logger: Logger,
|
|
options: { redact?: boolean; stateHome?: string } = {},
|
|
) {
|
|
logger.log("v2 CLI command started", { binary, args })
|
|
const env = { ...process.env }
|
|
if (options.stateHome === undefined) delete env.XDG_STATE_HOME
|
|
else env.XDG_STATE_HOME = options.stateHome
|
|
return execFileAsync(binary, args, { env, windowsHide: true }).then(
|
|
(result) => {
|
|
const stdout = result.stdout.trim()
|
|
const stderr = result.stderr.trim()
|
|
logger.log("v2 CLI command completed", { args, stdout: options.redact ? "[redacted]" : stdout, stderr })
|
|
return stdout
|
|
},
|
|
(error: unknown) => {
|
|
const output = error as { stdout?: string; stderr?: string }
|
|
logger.error("v2 CLI command failed", {
|
|
args,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stdout: options.redact && output.stdout ? "[redacted]" : (output.stdout?.trim() ?? ""),
|
|
stderr: output.stderr?.trim() ?? "",
|
|
})
|
|
throw error
|
|
},
|
|
)
|
|
}
|
|
|
|
function serviceUrl(status: string) {
|
|
if (URL.canParse(status)) return status
|
|
if (!status.startsWith("running ")) return
|
|
const url = status.slice("running ".length).trim()
|
|
return URL.canParse(url) ? url : undefined
|
|
}
|
|
|
|
function endpoint(url: string | undefined) {
|
|
if (!url || !URL.canParse(url)) return {}
|
|
const parsed = new URL(url)
|
|
return { url, hostname: parsed.hostname, port: parsed.port }
|
|
}
|
|
|
|
function executableName() {
|
|
return process.platform === "win32" ? "opencode-cli.exe" : "opencode-cli"
|
|
}
|