mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 11:55:46 +00:00
feat: add local server install actions
This commit is contained in:
@@ -255,6 +255,8 @@ registerIpcHandlers({
|
||||
setLocalServerConfig: (config) => localServer.setConfig(config),
|
||||
runLocalServerStep: (step) => localServer.runStep(step),
|
||||
cancelLocalServerJob: () => localServer.cancelJob(),
|
||||
installLocalServerWsl: () => localServer.installWsl(),
|
||||
installLocalServerDistro: (name) => localServer.installDistro(name),
|
||||
openLocalServerTerminal: () => localServer.openTerminal(),
|
||||
onLocalServerEvent: (listener) => localServer.subscribe(listener),
|
||||
getDefaultServerUrl: () => getDefaultServerUrl(),
|
||||
|
||||
@@ -27,6 +27,8 @@ type Deps = {
|
||||
setLocalServerConfig: (config: LocalServerConfig) => Promise<void> | void
|
||||
runLocalServerStep: (step: LocalServerStep) => Promise<void> | void
|
||||
cancelLocalServerJob: () => Promise<void> | void
|
||||
installLocalServerWsl: () => Promise<void> | void
|
||||
installLocalServerDistro: (name: string) => Promise<void> | void
|
||||
openLocalServerTerminal: () => Promise<void> | void
|
||||
onLocalServerEvent: (listener: (event: LocalServerEvent) => void) => () => void
|
||||
getDefaultServerUrl: () => Promise<string | null> | string | null
|
||||
@@ -66,6 +68,10 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
deps.runLocalServerStep(step),
|
||||
)
|
||||
ipcMain.handle("local-server-cancel-job", () => deps.cancelLocalServerJob())
|
||||
ipcMain.handle("local-server-install-wsl", () => deps.installLocalServerWsl())
|
||||
ipcMain.handle("local-server-install-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
deps.installLocalServerDistro(name),
|
||||
)
|
||||
ipcMain.handle("local-server-open-terminal", () => deps.openLocalServerTerminal())
|
||||
ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
|
||||
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
|
||||
|
||||
@@ -8,7 +8,16 @@ import type {
|
||||
} from "../preload/types"
|
||||
import { LOCAL_SERVER_KEY } from "./constants"
|
||||
import { store } from "./store"
|
||||
import { listInstalledWslDistros, listOnlineWslDistros, openWslTerminal, probeWslDistro, probeWslRuntime } from "./wsl"
|
||||
import {
|
||||
installWslDistro,
|
||||
installWslRuntimeElevated,
|
||||
listInstalledWslDistros,
|
||||
listOnlineWslDistros,
|
||||
openWslTerminal,
|
||||
probeWslDistro,
|
||||
probeWslRuntime,
|
||||
wslNeedsRestart,
|
||||
} from "./wsl"
|
||||
|
||||
export function defaultLocalServerConfig(): LocalServerConfig {
|
||||
return {
|
||||
@@ -54,17 +63,22 @@ export function createLocalServerController() {
|
||||
})
|
||||
}
|
||||
|
||||
const persistConfig = (config: LocalServerConfig) => {
|
||||
const next = normalizeLocalServerConfig(config)
|
||||
store.set(LOCAL_SERVER_KEY, next)
|
||||
update({
|
||||
...state,
|
||||
config: next,
|
||||
})
|
||||
return next
|
||||
}
|
||||
|
||||
return {
|
||||
getState() {
|
||||
return state
|
||||
},
|
||||
setConfig(config: LocalServerConfig) {
|
||||
const next = normalizeLocalServerConfig(config)
|
||||
store.set(LOCAL_SERVER_KEY, next)
|
||||
update({
|
||||
...state,
|
||||
config: next,
|
||||
})
|
||||
persistConfig(config)
|
||||
},
|
||||
subscribe(listener: (event: LocalServerEvent) => void) {
|
||||
listeners.add(listener)
|
||||
@@ -180,6 +194,201 @@ export function createLocalServerController() {
|
||||
status: { kind: "idle" },
|
||||
})
|
||||
},
|
||||
async installWsl() {
|
||||
jobAbort?.abort()
|
||||
const abort = new AbortController()
|
||||
jobAbort = abort
|
||||
clearTranscript()
|
||||
appendTranscript({ stream: "system", text: "Installing WSL runtime" })
|
||||
persistConfig({
|
||||
...state.config,
|
||||
mode: "wsl",
|
||||
onboarding: {
|
||||
...state.config.onboarding,
|
||||
step: "wsl",
|
||||
complete: false,
|
||||
pendingRestart: false,
|
||||
},
|
||||
})
|
||||
update({
|
||||
...state,
|
||||
job: { step: "wsl", startedAt: Date.now() },
|
||||
status: { kind: "running", step: "wsl" },
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await installWslRuntimeElevated({
|
||||
signal: abort.signal,
|
||||
onLine: (line) => appendTranscript(line),
|
||||
})
|
||||
if (jobAbort !== abort) return
|
||||
if (result.code !== 0) throw new Error(commandFailure(result, "WSL installation failed"))
|
||||
|
||||
const pendingRestart = wslNeedsRestart(result)
|
||||
const nextConfig = persistConfig({
|
||||
...state.config,
|
||||
mode: "wsl",
|
||||
onboarding: {
|
||||
...state.config.onboarding,
|
||||
step: pendingRestart ? "wsl" : "distro",
|
||||
complete: false,
|
||||
pendingRestart,
|
||||
},
|
||||
})
|
||||
|
||||
if (pendingRestart) {
|
||||
const message = "Windows restart required to finish WSL installation"
|
||||
update({
|
||||
...state,
|
||||
config: nextConfig,
|
||||
job: null,
|
||||
status: { kind: "failed", step: "wsl", message },
|
||||
checks: {
|
||||
...state.checks,
|
||||
wsl: {
|
||||
available: false,
|
||||
version: null,
|
||||
status: null,
|
||||
error: message,
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const wsl = await probeWslRuntime({
|
||||
signal: abort.signal,
|
||||
onLine: (line) => appendTranscript(line),
|
||||
})
|
||||
if (jobAbort !== abort) return
|
||||
update({
|
||||
...state,
|
||||
config: nextConfig,
|
||||
job: null,
|
||||
status: wsl.available
|
||||
? { kind: "ready" }
|
||||
: { kind: "failed", step: "wsl", message: wsl.error ?? "WSL is unavailable" },
|
||||
checks: {
|
||||
...state.checks,
|
||||
wsl,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (jobAbort !== abort) return
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: { kind: "idle" },
|
||||
})
|
||||
return
|
||||
}
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: { kind: "failed", step: "wsl", message: error instanceof Error ? error.message : String(error) },
|
||||
})
|
||||
} finally {
|
||||
if (jobAbort === abort) jobAbort = undefined
|
||||
}
|
||||
},
|
||||
async installDistro(name: string) {
|
||||
jobAbort?.abort()
|
||||
const abort = new AbortController()
|
||||
jobAbort = abort
|
||||
clearTranscript()
|
||||
appendTranscript({ stream: "system", text: `Installing WSL distro: ${name}` })
|
||||
persistConfig({
|
||||
...state.config,
|
||||
mode: "wsl",
|
||||
distro: name,
|
||||
onboarding: {
|
||||
...state.config.onboarding,
|
||||
step: "distro",
|
||||
complete: false,
|
||||
pendingRestart: false,
|
||||
},
|
||||
})
|
||||
update({
|
||||
...state,
|
||||
job: { step: "distro", startedAt: Date.now() },
|
||||
status: { kind: "running", step: "distro" },
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await installWslDistro(name, {
|
||||
signal: abort.signal,
|
||||
onLine: (line) => appendTranscript(line),
|
||||
})
|
||||
if (jobAbort !== abort) return
|
||||
if (result.code !== 0) throw new Error(commandFailure(result, `Failed to install distro: ${name}`))
|
||||
|
||||
const [installedResult, onlineResult] = await Promise.allSettled([
|
||||
listInstalledWslDistros({
|
||||
signal: abort.signal,
|
||||
onLine: (line) => appendTranscript(line),
|
||||
}),
|
||||
listOnlineWslDistros({
|
||||
signal: abort.signal,
|
||||
onLine: (line) => appendTranscript(line),
|
||||
}),
|
||||
])
|
||||
if (jobAbort !== abort) return
|
||||
|
||||
const installed = installedResult.status === "fulfilled" ? installedResult.value : []
|
||||
const online = onlineResult.status === "fulfilled" ? onlineResult.value : []
|
||||
const selected = await probeWslDistro(name, {
|
||||
signal: abort.signal,
|
||||
onLine: (line) => appendTranscript(line),
|
||||
})
|
||||
if (jobAbort !== abort) return
|
||||
|
||||
const error = distroError(name, installed, selected, installedResult, onlineResult)
|
||||
const nextConfig = persistConfig({
|
||||
...state.config,
|
||||
mode: "wsl",
|
||||
distro: name,
|
||||
onboarding: {
|
||||
...state.config.onboarding,
|
||||
step: error ? "distro" : "opencode",
|
||||
complete: false,
|
||||
pendingRestart: false,
|
||||
},
|
||||
})
|
||||
update({
|
||||
...state,
|
||||
config: nextConfig,
|
||||
job: null,
|
||||
status: error ? { kind: "failed", step: "distro", message: error } : { kind: "ready" },
|
||||
checks: {
|
||||
...state.checks,
|
||||
distro: {
|
||||
installed,
|
||||
online,
|
||||
selected,
|
||||
error,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (jobAbort !== abort) return
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: { kind: "idle" },
|
||||
})
|
||||
return
|
||||
}
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: { kind: "failed", step: "distro", message: error instanceof Error ? error.message : String(error) },
|
||||
})
|
||||
} finally {
|
||||
if (jobAbort === abort) jobAbort = undefined
|
||||
}
|
||||
},
|
||||
async openTerminal() {
|
||||
if (!state.config.distro) throw new Error("No WSL distro selected")
|
||||
await openWslTerminal(state.config.distro)
|
||||
@@ -299,3 +508,12 @@ function distroError(
|
||||
if (selected?.error) return selected.error
|
||||
return null
|
||||
}
|
||||
|
||||
function commandFailure(result: { stdout: string; stderr: string }, fallback: string) {
|
||||
const output = `${result.stderr}\n${result.stdout}`
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
return output || fallback
|
||||
}
|
||||
|
||||
@@ -29,8 +29,20 @@ export function wslArgs(args: string[], distro?: string | null) {
|
||||
}
|
||||
|
||||
export function runWsl(args: string[], opts: RunWslOptions = {}) {
|
||||
return runCommand("wsl", args, opts)
|
||||
}
|
||||
|
||||
function runPowerShell(command: string, opts: RunWslOptions = {}) {
|
||||
return runCommand(
|
||||
"powershell.exe",
|
||||
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
function runCommand(command: string, args: string[], opts: RunWslOptions = {}) {
|
||||
return new Promise<WslCommandResult>((resolve, reject) => {
|
||||
const child = spawn("wsl", args, {
|
||||
const child = spawn(command, args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
signal: opts.signal,
|
||||
@@ -136,10 +148,23 @@ export async function installWslRuntime(opts?: RunWslOptions) {
|
||||
return runWsl(["--install", "--no-distribution"], opts)
|
||||
}
|
||||
|
||||
export async function installWslRuntimeElevated(opts?: RunWslOptions) {
|
||||
const script = [
|
||||
"$ErrorActionPreference = 'Stop'",
|
||||
"$process = Start-Process -FilePath 'wsl.exe' -Verb RunAs -ArgumentList @('--install','--no-distribution') -Wait -PassThru",
|
||||
"if ($null -ne $process.ExitCode) { exit $process.ExitCode }",
|
||||
].join("; ")
|
||||
return runPowerShell(script, opts)
|
||||
}
|
||||
|
||||
export async function installWslDistro(name: string, opts?: RunWslOptions) {
|
||||
return runWsl(["--install", "-d", name, "--web-download", "--no-launch"], opts)
|
||||
}
|
||||
|
||||
export function wslNeedsRestart(result: WslCommandResult) {
|
||||
return /restart|reboot/i.test(`${result.stdout}\n${result.stderr}`)
|
||||
}
|
||||
|
||||
export async function probeWslDistro(name: string, opts?: RunWslOptions): Promise<LocalServerDistroProbe> {
|
||||
const executable = await runWslInDistro(["/bin/true"], name, opts).catch((error) => ({
|
||||
code: 1,
|
||||
|
||||
@@ -16,6 +16,8 @@ const api: ElectronAPI = {
|
||||
setConfig: (config) => ipcRenderer.invoke("local-server-set-config", config),
|
||||
runStep: (step) => ipcRenderer.invoke("local-server-run-step", step),
|
||||
cancelJob: () => ipcRenderer.invoke("local-server-cancel-job"),
|
||||
installWsl: () => ipcRenderer.invoke("local-server-install-wsl"),
|
||||
installDistro: (name) => ipcRenderer.invoke("local-server-install-distro", name),
|
||||
openTerminal: () => ipcRenderer.invoke("local-server-open-terminal"),
|
||||
subscribe: (cb) => {
|
||||
const handler = (_: unknown, event: LocalServerEvent) => cb(event)
|
||||
|
||||
@@ -92,6 +92,8 @@ export type LocalServerAPI = {
|
||||
setConfig: (config: LocalServerConfig) => Promise<void>
|
||||
runStep: (step: LocalServerStep) => Promise<void>
|
||||
cancelJob: () => Promise<void>
|
||||
installWsl: () => Promise<void>
|
||||
installDistro: (name: string) => Promise<void>
|
||||
openTerminal: () => Promise<void>
|
||||
subscribe: (cb: (event: LocalServerEvent) => void) => () => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user