diff --git a/CHANGELOG.md b/CHANGELOG.md
index 080b92faaa..a15bdf3fb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,21 @@ file is the human-readable summary of what changed and why.
## Unreleased
+### Desktop / Web UI
+
+- **WSL setup no longer depends on reaching Microsoft's servers** — on
+ Windows, **Install WSL** now downloads a pinned install bundle from the
+ CoDev storage host (the one host whitelist-only corporate proxies already
+ allow), enables the Virtual Machine Platform feature offline, and installs
+ the WSL runtime MSI quietly behind a single elevation prompt; the previous
+ `wsl --install` path against Microsoft's servers remains as an automatic
+ fallback when the bundle cannot be downloaded. Installing the
+ **Ubuntu-24.04** distro likewise imports the bundled image
+ (`wsl --install --from-file`) and only falls back to `--web-download` for
+ other distros or when the bundle is unavailable. Genuine install failures
+ still surface as errors rather than being retried behind a second
+ elevation prompt.
+
## 1.18.4-8
### Desktop / Web UI
diff --git a/DIVERGENCES.md b/DIVERGENCES.md
index bc577a15d3..4198d32727 100644
--- a/DIVERGENCES.md
+++ b/DIVERGENCES.md
@@ -188,6 +188,7 @@ from the squash message first, or use a plain merge.
| `packages/desktop/src/main/index.ts`, `packages/desktop/src/main/windows.ts` | APP_NAMES + window title → "CoDev Code" (Dev/Beta variants too) | Desktop app display name. | Keep our names. |
| `packages/desktop/electron-builder.config.ts` | `productName` ×3 → "CoDev Code…"; artifactName → `codev-code-desktop-…`; protocol display name. Auto-update `publish` repointed off GitHub entirely: upstream's beta `anomalyco/opencode-beta` and prod `anomalyco/opencode` collapse to one shared `generic` config (`const publish`) pointing at the fork's MinIO bucket — `url` from `process.env.CODEV_UPDATE_URL` falling back to a base64 `UPDATE_URL` const (same `atob` convention as `packages/codev-brand/src/index.ts`), plus two fields that are load-bearing rather than cosmetic: `channel: "latest"` and `useMultipleRangeRequest: false`. | Installer/product branding, and a packaged build must not offer upstream OpenCode releases as CoDev updates. GitHub could never have worked here: `quickbeard/codev-code` is private and `GitHubProvider.getLatestTagName` requests `/releases/latest` unauthenticated, and the repo has never carried a desktop asset. A bucket also splits nothing — one channel, one prefix. **`channel: "latest"` is required, not a default restated**: `detectUpdateChannel` is on by default, so with it absent `getResolvedPublishConfig` takes the channel from `AppInfo.channel`, which is the version's first semver prerelease component — `8` for `1.18.4-8` — and `getUpdateInfoFileName` then writes `8.yml`/`8-mac.yml`/`8-linux.yml` while the app requests `latest.yml` (`autoUpdater.channel` in `src/main/updater.ts`), i.e. a permanent `ERR_UPDATER_CHANNEL_FILE_NOT_FOUND`. **`useMultipleRangeRequest: false` is required for MinIO**, which answers a multipart range request with the whole object; electron-updater hardcodes the same `false` for its own `s3` provider citing minio/minio#5285 but defaults it to `true` for any `generic` URL that is not `s3.amazonaws.com`. Beta keeps a `publish` block despite the fork shipping no beta installers because `UPDATER_ENABLED` covers that channel and a build with no publish config gets no `app-update.yml` at all, so every check would fail with ENOENT. `${channel}` is deliberately NOT used as a URL macro — `macroExpander` expands it to that same `AppInfo.channel` (`8`), not `CODEV_CHANNEL`. appId (`ai.opencode.desktop`) and the `opencode://` deep-link scheme stay upstream — identity plumbing wired into main/index.ts and app deep-links; do not rename casually. `rpm.packageName` (`opencode`/`opencode-beta`/`opencode-dev`) also stays — it names the installed Linux package, not a publish target. | Keep our names and the whole `publish` const; take upstream's other config. Never drop `channel` or `useMultipleRangeRequest` to "match upstream" — both are covered by `electron-builder.config.test.ts`. If upstream reworks its publish targets, re-point ours rather than merging theirs. Note `generic` has no publisher (`createPublisher` returns null), so installers reach the bucket by hand; build with `--publish never`. |
| `packages/desktop/src/main/updater.ts` | `autoUpdater.allowDowngrade` `true` → `false`. | Upstream's `true` is safe against immutable GitHub release tags. The fork's feed is a mutable bucket path, where re-uploading or failing to replace a stale `latest.yml` would actively downgrade every client on the next 10-minute poll. | Keep `false`; take upstream's other updater settings. `channel = "latest"` and `allowPrerelease = false` stay as upstream has them — the first must match `publish.channel` in `electron-builder.config.ts`, the second is inert for the `generic` provider (it only steers GitHub tag resolution). |
+| Dual-path WSL install: `packages/desktop/src/main/wsl/{runtime,servers}.ts` | `runtime.ts`: three internals gain `export` (`runPowerShell`, `runInteractiveCommand`, `resolveSystem32Command`) — no behavior change. `servers.ts`: the `install-wsl` job calls `installWslRuntimeDualPath` and the `install-distro` job calls `installWslDistroDualPath` (both from the fork-only `install-bundle.ts`, which holds all new logic) instead of `installWslRuntimeElevated` / `installWslDistro` directly; both return the same `WslCommandResult` shape, so the surrounding job/error handling is untouched. | The customer fleet sits behind whitelist-only proxies that cannot reach Microsoft's servers. The dual path downloads the pinned bundle (`wsl.zip`: runtime MSI + Ubuntu-24.04 image) from the codev storage host, enables VirtualMachinePlatform via offline dism + installs the MSI behind one UAC prompt, and falls back to upstream's `wsl --install` / `--web-download` paths only when the bundle cannot be *obtained* — a completed-but-failed elevated install is returned as-is, never retried behind a second UAC prompt. | Keep the two call swaps and the three `export` keywords; take upstream's surrounding changes. If upstream reworks `installWslRuntimeElevated` / `installWslDistro`, keep them exported from `runtime.ts` — `install-bundle.ts` calls them as the fallback path. |
| `script/publish.ts` | The `if (Script.release)` block calling `packages/desktop/scripts/finalize-latest-{json,yml}.ts` removed, replaced by a comment. | Both finalizers existed to assemble update metadata into a GitHub release, which the fork no longer publishes to. electron-builder already writes a correct per-platform `latest*.yml` next to each installer, and the fork builds one arch per platform (macOS is universal — see the universal-builds row), so there is nothing left to merge. | Keep removed. If upstream adds further desktop release steps here, drop them unless they target the bucket. |
| `packages/desktop/scripts/finalize-latest-json.ts`, `packages/desktop/scripts/finalize-latest-yml.ts` | Deleted. | Both `gh release upload` into a GitHub release the fork does not use. `finalize-latest-yml.ts` merged per-arch `latest*.yml` from an upstream build matrix the fork does not run; `finalize-latest-json.ts` additionally built a Tauri-style `latest.json` signed with `@tauri-apps/cli signer` that nothing in the Electron updater path ever reads, and had gone stale besides — it hardcoded `opencode-desktop-mac-{x64,arm64}.app.tar.gz`, an artifact name and format this fork never produces. Leaving them in place is a trap: they look like the release path and upload to the wrong place. | Keep deleted (`git rm` the upstream side of modify/delete conflicts). If a future build matrix produces two `latest.yml` files for one platform (Windows x64 + arm64), write a bucket-targeted merge step rather than restoring these. |
| `packages/desktop/src/renderer/index.tsx` | Desktop notification icon `https://opencode.ai/favicon-96x96-v3.png` → the relative `./favicon-96x96-v3.png`. The renderer's `publicDir` is `packages/app/public`, whose `favicon-96x96-v3.png` symlinks to the rebranded `packages/ui/src/assets/favicon/` mark, so the file is emitted next to `index.html` and resolves against the renderer origin (`oc://renderer/` packaged, the vite dev server otherwise) — the same asset and the same relative form `src/renderer/index.html` already uses for ``. | A desktop app should not reach out to a remote host to draw its own notification icon (it fails offline and leaks a request per notification), and the remote file is upstream-branded while the bundled one is the codev mark. | Keep our relative path. If upstream changes the URL, re-point it at the bundled asset rather than taking a new remote host. `packages/app/src/entry.tsx` and `pages/layout/helpers.ts` keep their remote `opencode.ai/favicon*` sources — same bug class, not yet fixed. |
diff --git a/bun.lock b/bun.lock
index 9a05c3df09..41a809b14d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -417,6 +417,7 @@
},
"devDependencies": {
"@actions/artifact": "4.0.0",
+ "@codev/sandbox": "workspace:*",
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@sentry/solid": "catalog:",
diff --git a/packages/codev-sandbox/src/const.ts b/packages/codev-sandbox/src/const.ts
index d2e1c724ac..d206734dc6 100644
--- a/packages/codev-sandbox/src/const.ts
+++ b/packages/codev-sandbox/src/const.ts
@@ -10,3 +10,13 @@ export const SANDBOX_MANIFEST_URL = `${SANDBOX_IMAGES_BASE_URL}/manifest.json`
// WSL MSI installers mirrored for corporate networks where only the storage
// host is whitelisted; the Microsoft `wsl --install` path stays as fallback.
export const INSTALLERS_BASE_URL = `${STORAGE_BASE}/installers`
+
+// The WSL install bundle is one hand-uploaded zip so a corporate whitelist
+// rule covers a single URL. The member names are part of the contract: the
+// install flow extracts them from the zip by exact name. The MSI carries the
+// pinned WSL runtime version in its filename; the .wsl file is the Ubuntu
+// distro image for the WSL servers feature (`wsl --install --from-file`),
+// not the sandbox rootfs, which ships via the image manifest instead.
+export const WSL_INSTALLER_ZIP_URL = `${INSTALLERS_BASE_URL}/wsl.zip`
+export const WSL_INSTALLER_MSI_NAME = "wsl.2.7.11.0.x64.msi"
+export const WSL_DISTRO_IMAGE_NAME = "ubuntu-24.04.4-wsl-amd64.wsl"
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 34a1e5324c..87ed66e6ce 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -36,6 +36,7 @@
},
"devDependencies": {
"@actions/artifact": "4.0.0",
+ "@codev/sandbox": "workspace:*",
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@sentry/solid": "catalog:",
diff --git a/packages/desktop/src/main/wsl/install-bundle.test.ts b/packages/desktop/src/main/wsl/install-bundle.test.ts
new file mode 100644
index 0000000000..4e41d55e32
--- /dev/null
+++ b/packages/desktop/src/main/wsl/install-bundle.test.ts
@@ -0,0 +1,65 @@
+import { expect, test } from "bun:test"
+import { mkdtemp, readFile, readdir } from "node:fs/promises"
+import { tmpdir } from "node:os"
+import { join } from "node:path"
+import { WSL_DISTRO_IMAGE_NAME, WSL_INSTALLER_MSI_NAME } from "@codev/sandbox"
+import { bundleProvidesDistro, ensureWslInstallBundle, wslRuntimeInstallScript } from "./install-bundle"
+
+// Built with zip.js rather than checked in as a fixture: the test only cares
+// that members are found and extracted by exact name, not about zip bytes.
+async function fixtureZip() {
+ const { BlobWriter, TextReader, ZipWriter, configure } = await import("@zip.js/zip.js")
+ configure({ useWebWorkers: false })
+ const writer = new ZipWriter(new BlobWriter("application/zip"))
+ await writer.add(WSL_INSTALLER_MSI_NAME, new TextReader("msi-bytes"))
+ await writer.add(WSL_DISTRO_IMAGE_NAME, new TextReader("distro-bytes"))
+ return writer.close()
+}
+
+test("downloads, extracts by member name, and never fetches twice", async () => {
+ const dir = await mkdtemp(join(tmpdir(), "wsl-bundle-"))
+ const zip = await fixtureZip()
+ let fetches = 0
+ const fetchImpl = (async () => {
+ fetches += 1
+ return new Response(zip)
+ }) as typeof fetch
+
+ const first = await ensureWslInstallBundle({ dir, fetchImpl })
+ expect(await readFile(first.msiPath, "utf8")).toBe("msi-bytes")
+ expect(await readFile(first.distroImagePath, "utf8")).toBe("distro-bytes")
+ // The zip is deleted after extraction and nothing half-written survives.
+ expect((await readdir(dir)).toSorted()).toEqual([WSL_DISTRO_IMAGE_NAME, WSL_INSTALLER_MSI_NAME].toSorted())
+
+ const second = await ensureWslInstallBundle({ dir, fetchImpl })
+ expect(second).toEqual(first)
+ expect(fetches).toBe(1)
+})
+
+test("fails loudly when the zip is missing a member", async () => {
+ const dir = await mkdtemp(join(tmpdir(), "wsl-bundle-"))
+ const { BlobWriter, TextReader, ZipWriter, configure } = await import("@zip.js/zip.js")
+ configure({ useWebWorkers: false })
+ const writer = new ZipWriter(new BlobWriter("application/zip"))
+ await writer.add(WSL_INSTALLER_MSI_NAME, new TextReader("msi-bytes"))
+ const zip = await writer.close()
+ const fetchImpl = (async () => new Response(zip)) as typeof fetch
+
+ expect(ensureWslInstallBundle({ dir, fetchImpl })).rejects.toThrow(WSL_DISTRO_IMAGE_NAME)
+})
+
+test("elevated script tolerates reboot-required exits and quotes the MSI path", () => {
+ const script = wslRuntimeInstallScript(`C:\\Users\\O'Brien\\wsl.msi`)
+ expect(script).toContain("/featurename:VirtualMachinePlatform")
+ expect(script).toContain("'C:\\Users\\O''Brien\\wsl.msi'")
+ // 3010 = ERROR_SUCCESS_REBOOT_REQUIRED, expected from both dism and msiexec.
+ expect(script).toContain("($dism -ne 0) -and ($dism -ne 3010)")
+ expect(script).toContain("($msi.ExitCode -ne 0) -and ($msi.ExitCode -ne 3010)")
+})
+
+test("only Ubuntu-24.04 is served from the bundle", () => {
+ expect(bundleProvidesDistro("Ubuntu-24.04")).toBe(true)
+ expect(bundleProvidesDistro("ubuntu-24.04")).toBe(true)
+ expect(bundleProvidesDistro("Ubuntu-22.04")).toBe(false)
+ expect(bundleProvidesDistro("Debian")).toBe(false)
+})
diff --git a/packages/desktop/src/main/wsl/install-bundle.ts b/packages/desktop/src/main/wsl/install-bundle.ts
new file mode 100644
index 0000000000..6475035822
--- /dev/null
+++ b/packages/desktop/src/main/wsl/install-bundle.ts
@@ -0,0 +1,174 @@
+import { createWriteStream, existsSync } from "node:fs"
+import { mkdir, rename, rm, writeFile } from "node:fs/promises"
+import { dirname, join } from "node:path"
+import { Writable } from "node:stream"
+import electron from "electron"
+import { WSL_DISTRO_IMAGE_NAME, WSL_INSTALLER_MSI_NAME, WSL_INSTALLER_ZIP_URL } from "@codev/sandbox"
+import type { RunWslOptions, WslCommandResult } from "./runtime"
+import {
+ installWslDistro,
+ installWslRuntimeElevated,
+ resolveSystem32Command,
+ runInteractiveCommand,
+ runPowerShell,
+} from "./runtime"
+
+// The customer fleet sits behind whitelist-only proxies that cannot reach
+// Microsoft's servers, so WSL setup prefers the hand-uploaded bundle on the
+// codev storage host and only falls back to the original `wsl --install`
+// paths when the bundle cannot be obtained. Genuine install failures are NOT
+// retried on the fallback: both paths need the same elevation, and a second
+// UAC prompt right after a failed one helps nobody.
+
+// What the distro image registers itself as (its embedded
+// /etc/wsl-distribution.conf shortname), and also the name Microsoft's online
+// catalog lists — which is what makes the by-name fallback equivalent. Must
+// move together with WSL_DISTRO_IMAGE_NAME.
+export const BUNDLED_DISTRO_NAME = "Ubuntu-24.04"
+
+const INSTALL_TIMEOUT_MS = 15 * 60_000
+// 643MB over a slow corporate link; observed as low as ~35 KB/s.
+const DOWNLOAD_TIMEOUT_MS = 6 * 60 * 60_000
+
+type InstallLogger = {
+ log: (message: string, meta?: unknown) => void
+ error: (message: string, meta?: unknown) => void
+}
+
+export type InstallBundleOptions = RunWslOptions & {
+ dir?: string
+ url?: string
+ fetchImpl?: typeof fetch
+}
+
+export async function installWslRuntimeDualPath(
+ opts?: InstallBundleOptions,
+ logger?: InstallLogger,
+): Promise {
+ const primary = bundleSupportsThisMachine()
+ ? await installWslRuntimeFromBundle(opts).catch(intoError)
+ : new Error("the WSL install bundle is x64-only")
+ if (!(primary instanceof Error)) return primary
+ if (opts?.signal?.aborted) throw primary
+ logger?.error("bundled WSL install unavailable; falling back to wsl --install", { message: primary.message })
+ return installWslRuntimeElevated(opts)
+}
+
+export async function installWslDistroDualPath(
+ name: string,
+ opts?: InstallBundleOptions,
+ logger?: InstallLogger,
+): Promise {
+ const bundle =
+ bundleProvidesDistro(name) && bundleSupportsThisMachine()
+ ? await ensureWslInstallBundle(opts).catch(intoError)
+ : new Error(`the WSL install bundle does not provide ${name}`)
+ if (!(bundle instanceof Error)) {
+ return runInteractiveCommand(
+ resolveSystem32Command("wsl.exe"),
+ ["--install", "--from-file", bundle.distroImagePath, "--no-launch"],
+ { ...opts, timeoutMs: opts?.timeoutMs ?? INSTALL_TIMEOUT_MS },
+ INSTALL_TIMEOUT_MS,
+ )
+ }
+ if (opts?.signal?.aborted) throw bundle
+ logger?.error("bundled distro install unavailable; falling back to --web-download", { message: bundle.message })
+ return installWslDistro(name, opts)
+}
+
+export function bundleProvidesDistro(name: string) {
+ return name.toLowerCase() === BUNDLED_DISTRO_NAME.toLowerCase()
+}
+
+// Acquisition problems (download, extraction) throw and let the caller fall
+// back; the elevated install itself ran once we return a result, even a
+// failing one.
+export async function installWslRuntimeFromBundle(opts?: InstallBundleOptions): Promise {
+ const bundle = await ensureWslInstallBundle(opts)
+ const scriptPath = join(dirname(bundle.msiPath), "install-wsl-runtime.ps1")
+ await writeFile(scriptPath, wslRuntimeInstallScript(bundle.msiPath))
+ const command = [
+ "$ErrorActionPreference = 'Stop'",
+ `$process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @('-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File',('"{0}"' -f ${psQuote(scriptPath)})) -Wait -PassThru`,
+ "if ($null -ne $process.ExitCode) { exit $process.ExitCode }",
+ ].join("; ")
+ return runPowerShell(command, { ...opts, timeoutMs: opts?.timeoutMs ?? INSTALL_TIMEOUT_MS })
+}
+
+// One elevated script covers both steps so the user sees a single UAC prompt.
+// 3010 is ERROR_SUCCESS_REBOOT_REQUIRED from both dism and msiexec — the
+// expected outcome on a machine that never had VirtualMachinePlatform — and
+// the pending-restart handling downstream already covers it.
+export function wslRuntimeInstallScript(msiPath: string) {
+ return [
+ "$ErrorActionPreference = 'Continue'",
+ "& dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart",
+ "$dism = $LASTEXITCODE",
+ "if (($dism -ne 0) -and ($dism -ne 3010)) { exit $dism }",
+ `$msi = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/i', ('"{0}"' -f ${psQuote(msiPath)}), '/qn', '/norestart') -Wait -PassThru`,
+ "if (($msi.ExitCode -ne 0) -and ($msi.ExitCode -ne 3010)) { exit $msi.ExitCode }",
+ "exit 0",
+ ].join("\r\n")
+}
+
+export async function ensureWslInstallBundle(opts?: InstallBundleOptions) {
+ const dir = opts?.dir ?? join(electron.app.getPath("userData"), "wsl-installer")
+ const msiPath = join(dir, WSL_INSTALLER_MSI_NAME)
+ const distroImagePath = join(dir, WSL_DISTRO_IMAGE_NAME)
+ if (existsSync(msiPath) && existsSync(distroImagePath)) return { msiPath, distroImagePath }
+
+ await mkdir(dir, { recursive: true })
+ const zipPath = join(dir, "wsl.zip")
+ if (!existsSync(zipPath)) await downloadTo(zipPath, opts)
+ await extractZipMembers(zipPath, dir, [WSL_INSTALLER_MSI_NAME, WSL_DISTRO_IMAGE_NAME])
+ if (!existsSync(msiPath) || !existsSync(distroImagePath))
+ throw new Error(`wsl.zip did not contain ${WSL_INSTALLER_MSI_NAME} and ${WSL_DISTRO_IMAGE_NAME}`)
+ // Members extracted, so the zip is 643MB of dead weight from here on.
+ await rm(zipPath, { force: true })
+ return { msiPath, distroImagePath }
+}
+
+async function downloadTo(zipPath: string, opts?: InstallBundleOptions) {
+ const fetchImpl = opts?.fetchImpl ?? fetch
+ const url = opts?.url ?? WSL_INSTALLER_ZIP_URL
+ const signal = AbortSignal.any(
+ [opts?.signal, AbortSignal.timeout(opts?.timeoutMs ?? DOWNLOAD_TIMEOUT_MS)].filter(
+ (value): value is AbortSignal => value !== undefined,
+ ),
+ )
+ const response = await fetchImpl(url, { signal })
+ if (!response.ok || !response.body) throw new Error(`downloading ${url} failed: HTTP ${response.status}`)
+ const partial = `${zipPath}.partial`
+ await response.body.pipeTo(Writable.toWeb(createWriteStream(partial)), { signal })
+ await rename(partial, zipPath)
+}
+
+// zip.js instead of PowerShell Expand-Archive: streams each member to disk
+// without buffering it, and keeps this file testable off Windows.
+async function extractZipMembers(zipPath: string, dir: string, names: string[]) {
+ const { openAsBlob } = await import("node:fs")
+ const { BlobReader, ZipReader, configure } = await import("@zip.js/zip.js")
+ configure({ useWebWorkers: false })
+ const reader = new ZipReader(new BlobReader(await openAsBlob(zipPath)))
+ const entries = await reader.getEntries()
+ for (const name of names) {
+ const entry = entries.find((item) => item.filename === name || item.filename.endsWith(`/${name}`))
+ if (!entry?.getData) throw new Error(`wsl.zip has no ${name}`)
+ const partial = join(dir, `${name}.partial`)
+ await entry.getData(Writable.toWeb(createWriteStream(partial)))
+ await rename(partial, join(dir, name))
+ }
+ await reader.close()
+}
+
+function bundleSupportsThisMachine() {
+ return process.arch === "x64"
+}
+
+function psQuote(value: string) {
+ return `'${value.replace(/'/g, "''")}'`
+}
+
+function intoError(error: unknown) {
+ return error instanceof Error ? error : new Error(String(error))
+}
diff --git a/packages/desktop/src/main/wsl/runtime.ts b/packages/desktop/src/main/wsl/runtime.ts
index 0f6dc00f38..d64744e37d 100644
--- a/packages/desktop/src/main/wsl/runtime.ts
+++ b/packages/desktop/src/main/wsl/runtime.ts
@@ -41,7 +41,7 @@ export function runWsl(args: string[], opts: RunWslOptions = {}) {
return runCommand("wsl", args, opts)
}
-function runPowerShell(command: string, opts: RunWslOptions = {}) {
+export function runPowerShell(command: string, opts: RunWslOptions = {}) {
return runCommand(
"powershell.exe",
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
@@ -110,7 +110,12 @@ function runCommand(command: string, args: string[], opts: RunWslOptions = {}) {
})
}
-function runInteractiveCommand(command: string, args: string[], opts: RunWslOptions = {}, defaultTimeoutMs: number) {
+export function runInteractiveCommand(
+ command: string,
+ args: string[],
+ opts: RunWslOptions = {},
+ defaultTimeoutMs: number,
+) {
return new Promise((resolve, reject) => {
const child = pty.spawn(command, args, {
name: "xterm-color",
@@ -385,7 +390,7 @@ export function shellEscape(value: string) {
return `'${value.replace(/'/g, `'"'"'`)}'`
}
-function resolveSystem32Command(command: string) {
+export function resolveSystem32Command(command: string) {
const root = process.env.SystemRoot ?? process.env.windir
if (!root) return command
const resolved = join(root, "System32", command)
diff --git a/packages/desktop/src/main/wsl/servers.ts b/packages/desktop/src/main/wsl/servers.ts
index deba0598e1..be1e7570dc 100644
--- a/packages/desktop/src/main/wsl/servers.ts
+++ b/packages/desktop/src/main/wsl/servers.ts
@@ -15,10 +15,9 @@ import { WSL_SERVERS_KEY } from "../store-keys"
import { getStore } from "../store"
import { expectCodevVersion, pendingRestartAfterWslInstall, wslServerIdsToStartOnInitialize } from "./startup"
import { clearWslDistroState, wslServerIdToRestart } from "./policy"
+import { installWslDistroDualPath, installWslRuntimeDualPath } from "./install-bundle"
import {
- installWslDistro,
installWslCodev,
- installWslRuntimeElevated,
listInstalledWslDistros,
listOnlineWslDistros,
openWslTerminal,
@@ -325,7 +324,7 @@ export function createWslServersController(
async installWsl() {
await runJob({ kind: "install-wsl", startedAt: Date.now() }, async (abort) => {
- const result = await installWslRuntimeElevated({ signal: abort.signal })
+ const result = await installWslRuntimeDualPath({ signal: abort.signal }, logger)
if (result.code !== 0) {
const message = summarize(result.stderr || result.stdout) || "WSL installation failed"
throw new Error(message)
@@ -337,7 +336,7 @@ export function createWslServersController(
async installDistro(name: string) {
await runJob({ kind: "install-distro", distro: name, startedAt: Date.now() }, async (abort) => {
- const result = await installWslDistro(name, { signal: abort.signal })
+ const result = await installWslDistroDualPath(name, { signal: abort.signal }, logger)
if (result.code !== 0) {
const message = summarize(result.stderr || result.stdout) || `Failed to install distro: ${name}`
throw new Error(message)