@neurocode-ai/desktop 1.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +19 -0
- package/electron-builder.config.test.ts +48 -0
- package/electron-builder.config.ts +146 -0
- package/electron.vite.config.ts +106 -0
- package/icons/README.md +14 -0
- package/icons/beta/128x128.png +0 -0
- package/icons/beta/128x128@2x.png +0 -0
- package/icons/beta/32x32.png +0 -0
- package/icons/beta/64x64.png +0 -0
- package/icons/beta/Square107x107Logo.png +0 -0
- package/icons/beta/Square142x142Logo.png +0 -0
- package/icons/beta/Square150x150Logo.png +0 -0
- package/icons/beta/Square284x284Logo.png +0 -0
- package/icons/beta/Square30x30Logo.png +0 -0
- package/icons/beta/Square310x310Logo.png +0 -0
- package/icons/beta/Square44x44Logo.png +0 -0
- package/icons/beta/Square71x71Logo.png +0 -0
- package/icons/beta/Square89x89Logo.png +0 -0
- package/icons/beta/StoreLogo.png +0 -0
- package/icons/beta/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/beta/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/beta/android/values/ic_launcher_background.xml +4 -0
- package/icons/beta/dock.png +0 -0
- package/icons/beta/icon.icns +0 -0
- package/icons/beta/icon.ico +0 -0
- package/icons/beta/icon.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/beta/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/beta/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/beta/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/beta/ios/AppIcon-512@2x.png +0 -0
- package/icons/beta/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/beta/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/beta/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/beta/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/beta/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/icons/dev/128x128.png +0 -0
- package/icons/dev/128x128@2x.png +0 -0
- package/icons/dev/32x32.png +0 -0
- package/icons/dev/64x64.png +0 -0
- package/icons/dev/Square107x107Logo.png +0 -0
- package/icons/dev/Square142x142Logo.png +0 -0
- package/icons/dev/Square150x150Logo.png +0 -0
- package/icons/dev/Square284x284Logo.png +0 -0
- package/icons/dev/Square30x30Logo.png +0 -0
- package/icons/dev/Square310x310Logo.png +0 -0
- package/icons/dev/Square44x44Logo.png +0 -0
- package/icons/dev/Square71x71Logo.png +0 -0
- package/icons/dev/Square89x89Logo.png +0 -0
- package/icons/dev/StoreLogo.png +0 -0
- package/icons/dev/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/dev/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/dev/android/values/ic_launcher_background.xml +4 -0
- package/icons/dev/dock.png +0 -0
- package/icons/dev/icon.icns +0 -0
- package/icons/dev/icon.ico +0 -0
- package/icons/dev/icon.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/dev/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/dev/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/dev/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/dev/ios/AppIcon-512@2x.png +0 -0
- package/icons/dev/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/dev/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/dev/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/dev/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/dev/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/icons/prod/128x128.png +0 -0
- package/icons/prod/128x128@2x.png +0 -0
- package/icons/prod/32x32.png +0 -0
- package/icons/prod/64x64.png +0 -0
- package/icons/prod/Square107x107Logo.png +0 -0
- package/icons/prod/Square142x142Logo.png +0 -0
- package/icons/prod/Square150x150Logo.png +0 -0
- package/icons/prod/Square284x284Logo.png +0 -0
- package/icons/prod/Square30x30Logo.png +0 -0
- package/icons/prod/Square310x310Logo.png +0 -0
- package/icons/prod/Square44x44Logo.png +0 -0
- package/icons/prod/Square71x71Logo.png +0 -0
- package/icons/prod/Square89x89Logo.png +0 -0
- package/icons/prod/StoreLogo.png +0 -0
- package/icons/prod/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/icons/prod/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/icons/prod/android/values/ic_launcher_background.xml +4 -0
- package/icons/prod/dock.png +0 -0
- package/icons/prod/icon.icns +0 -0
- package/icons/prod/icon.ico +0 -0
- package/icons/prod/icon.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@1x.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@2x.png +0 -0
- package/icons/prod/ios/AppIcon-20x20@3x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@1x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@2x.png +0 -0
- package/icons/prod/ios/AppIcon-29x29@3x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@1x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@2x-1.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@2x.png +0 -0
- package/icons/prod/ios/AppIcon-40x40@3x.png +0 -0
- package/icons/prod/ios/AppIcon-512@2x.png +0 -0
- package/icons/prod/ios/AppIcon-60x60@2x.png +0 -0
- package/icons/prod/ios/AppIcon-60x60@3x.png +0 -0
- package/icons/prod/ios/AppIcon-76x76@1x.png +0 -0
- package/icons/prod/ios/AppIcon-76x76@2x.png +0 -0
- package/icons/prod/ios/AppIcon-83.5x83.5@2x.png +0 -0
- package/package.json +75 -0
- package/resources/entitlements.plist +18 -0
- package/resources/linux/opencode-desktop.desktop +10 -0
- package/scripts/copy-bundles.ts +12 -0
- package/scripts/copy-icons.ts +12 -0
- package/scripts/copy-metainfo.ts +47 -0
- package/scripts/finalize-latest-json.ts +219 -0
- package/scripts/finalize-latest-yml.ts +124 -0
- package/scripts/prebuild.ts +10 -0
- package/scripts/predev.ts +5 -0
- package/scripts/prepare.ts +9 -0
- package/scripts/utils.ts +77 -0
- package/src/main/apps.ts +136 -0
- package/src/main/attachment-picker.test.ts +87 -0
- package/src/main/attachment-picker.ts +56 -0
- package/src/main/constants.ts +7 -0
- package/src/main/debug.ts +95 -0
- package/src/main/desktop-menu-actions.ts +84 -0
- package/src/main/env.d.ts +19 -0
- package/src/main/index.test.ts +37 -0
- package/src/main/index.ts +399 -0
- package/src/main/initialization.ts +6 -0
- package/src/main/install-state.test.ts +19 -0
- package/src/main/install-state.ts +8 -0
- package/src/main/ipc.ts +274 -0
- package/src/main/logging.ts +205 -0
- package/src/main/markdown.ts +16 -0
- package/src/main/menu.ts +67 -0
- package/src/main/migrate.ts +91 -0
- package/src/main/onboarding.ts +45 -0
- package/src/main/server.ts +238 -0
- package/src/main/shell-env.test.ts +50 -0
- package/src/main/shell-env.ts +101 -0
- package/src/main/sidecar.ts +157 -0
- package/src/main/store-cleanup.test.ts +93 -0
- package/src/main/store-cleanup.ts +94 -0
- package/src/main/store-keys.ts +7 -0
- package/src/main/store.ts +35 -0
- package/src/main/unresponsive.ts +70 -0
- package/src/main/updater-controller.test.ts +111 -0
- package/src/main/updater-controller.ts +97 -0
- package/src/main/updater-subscriptions.test.ts +16 -0
- package/src/main/updater-subscriptions.ts +20 -0
- package/src/main/updater.ts +85 -0
- package/src/main/window-registry.test.ts +91 -0
- package/src/main/window-registry.ts +47 -0
- package/src/main/window-state.ts +29 -0
- package/src/main/windows.ts +496 -0
- package/src/main/wsl/ipc.ts +103 -0
- package/src/main/wsl/policy.ts +33 -0
- package/src/main/wsl/runtime.ts +400 -0
- package/src/main/wsl/servers.test.ts +231 -0
- package/src/main/wsl/servers.ts +521 -0
- package/src/main/wsl/sidecar.ts +129 -0
- package/src/main/wsl/startup.ts +31 -0
- package/src/preload/index.ts +128 -0
- package/src/preload/types.ts +109 -0
- package/src/renderer/cli.ts +12 -0
- package/src/renderer/env.d.ts +10 -0
- package/src/renderer/html.test.ts +62 -0
- package/src/renderer/i18n/ar.ts +26 -0
- package/src/renderer/i18n/br.ts +27 -0
- package/src/renderer/i18n/bs.ts +28 -0
- package/src/renderer/i18n/da.ts +28 -0
- package/src/renderer/i18n/de.ts +28 -0
- package/src/renderer/i18n/en.ts +27 -0
- package/src/renderer/i18n/es.ts +27 -0
- package/src/renderer/i18n/fr.ts +28 -0
- package/src/renderer/i18n/index.ts +194 -0
- package/src/renderer/i18n/ja.ts +28 -0
- package/src/renderer/i18n/ko.ts +27 -0
- package/src/renderer/i18n/no.ts +28 -0
- package/src/renderer/i18n/pl.ts +28 -0
- package/src/renderer/i18n/ru.ts +27 -0
- package/src/renderer/i18n/uk.ts +28 -0
- package/src/renderer/i18n/zh.ts +26 -0
- package/src/renderer/i18n/zht.ts +26 -0
- package/src/renderer/index.html +21 -0
- package/src/renderer/index.tsx +464 -0
- package/src/renderer/initialization.test.ts +73 -0
- package/src/renderer/initialization.ts +22 -0
- package/src/renderer/onboarding.tsx +53 -0
- package/src/renderer/styles.css +0 -0
- package/src/renderer/webview-zoom.ts +138 -0
- package/src/renderer/wsl/connections.test.ts +43 -0
- package/src/renderer/wsl/connections.ts +28 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { existsSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import * as pty from "@lydell/node-pty"
|
|
5
|
+
import type { WslDistroProbe, WslInstalledDistro, WslOnlineDistro, WslRuntimeCheck } from "../../preload/types"
|
|
6
|
+
import { wslTerminalArgs } from "./policy"
|
|
7
|
+
|
|
8
|
+
export type WslCommandLine = {
|
|
9
|
+
stream: "stdout" | "stderr"
|
|
10
|
+
text: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type WslCommandResult = {
|
|
14
|
+
code: number | null
|
|
15
|
+
signal: NodeJS.Signals | null
|
|
16
|
+
stdout: string
|
|
17
|
+
stderr: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RunWslOptions = {
|
|
21
|
+
signal?: AbortSignal
|
|
22
|
+
/**
|
|
23
|
+
* Ceiling on how long we wait for the child process to exit. When the
|
|
24
|
+
* LXSS service or a specific distro wedges (e.g. Ubuntu-24.04 with a
|
|
25
|
+
* pending first-run prompt), `wsl.exe` never returns and any command
|
|
26
|
+
* that doesn't specify a timeout hangs the entire startup flow. Default
|
|
27
|
+
* is 20s — enough for slow cold-starts, short enough to fail fast on
|
|
28
|
+
* a wedge. Callers can override for longer-running jobs.
|
|
29
|
+
*/
|
|
30
|
+
timeoutMs?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_WSL_TIMEOUT_MS = 20_000
|
|
34
|
+
const DEFAULT_WSL_INSTALL_TIMEOUT_MS = 15 * 60_000
|
|
35
|
+
|
|
36
|
+
export function wslArgs(args: string[], distro?: string | null, user?: string | null) {
|
|
37
|
+
return [...(distro ? ["-d", distro] : []), ...(user ? ["--user", user] : []), "--", ...args]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function runWsl(args: string[], opts: RunWslOptions = {}) {
|
|
41
|
+
return runCommand("wsl", args, opts)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runPowerShell(command: string, opts: RunWslOptions = {}) {
|
|
45
|
+
return runCommand(
|
|
46
|
+
"powershell.exe",
|
|
47
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
48
|
+
opts,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runCommand(command: string, args: string[], opts: RunWslOptions = {}) {
|
|
53
|
+
return new Promise<WslCommandResult>((resolve, reject) => {
|
|
54
|
+
const child = spawn(command, args, {
|
|
55
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
signal: opts.signal,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Guard every wsl.exe invocation with a timeout. When the distro or
|
|
61
|
+
// the LXSS service is wedged (Ubuntu first-run state, Windows update
|
|
62
|
+
// pending, etc.) wsl.exe produces no output and never exits; without
|
|
63
|
+
// this the whole sidecar spawn flow stalls the app forever.
|
|
64
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_WSL_TIMEOUT_MS
|
|
65
|
+
const timeoutId = setTimeout(() => {
|
|
66
|
+
try {
|
|
67
|
+
child.kill()
|
|
68
|
+
} catch {
|
|
69
|
+
/* ignore */
|
|
70
|
+
}
|
|
71
|
+
reject(new Error(`${command} ${args.join(" ")} timed out after ${timeoutMs}ms`))
|
|
72
|
+
}, timeoutMs)
|
|
73
|
+
|
|
74
|
+
let stdout = ""
|
|
75
|
+
let stderr = ""
|
|
76
|
+
const stdoutDecoder = createOutputDecoder()
|
|
77
|
+
const stderrDecoder = createOutputDecoder()
|
|
78
|
+
|
|
79
|
+
const append = (stream: WslCommandLine["stream"], chunk: string) => {
|
|
80
|
+
if (!chunk) return
|
|
81
|
+
if (stream === "stdout") {
|
|
82
|
+
stdout += chunk
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
stderr += chunk
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
89
|
+
append("stdout", stdoutDecoder.decode(chunk))
|
|
90
|
+
})
|
|
91
|
+
child.stdout.on("end", () => {
|
|
92
|
+
append("stdout", stdoutDecoder.flush())
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
96
|
+
append("stderr", stderrDecoder.decode(chunk))
|
|
97
|
+
})
|
|
98
|
+
child.stderr.on("end", () => {
|
|
99
|
+
append("stderr", stderrDecoder.flush())
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
child.once("error", (error) => {
|
|
103
|
+
clearTimeout(timeoutId)
|
|
104
|
+
reject(error)
|
|
105
|
+
})
|
|
106
|
+
child.once("close", (code, signal) => {
|
|
107
|
+
clearTimeout(timeoutId)
|
|
108
|
+
resolve({ code, signal, stdout, stderr })
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runInteractiveCommand(command: string, args: string[], opts: RunWslOptions = {}, defaultTimeoutMs: number) {
|
|
114
|
+
return new Promise<WslCommandResult>((resolve, reject) => {
|
|
115
|
+
const child = pty.spawn(command, args, {
|
|
116
|
+
name: "xterm-color",
|
|
117
|
+
cols: 80,
|
|
118
|
+
rows: 24,
|
|
119
|
+
cwd: process.cwd(),
|
|
120
|
+
env: process.env,
|
|
121
|
+
useConpty: true,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
let settled = false
|
|
125
|
+
let stdout = ""
|
|
126
|
+
|
|
127
|
+
const cleanup = () => {
|
|
128
|
+
clearTimeout(timeoutId)
|
|
129
|
+
abortCleanup?.()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const timeoutMs = opts.timeoutMs ?? defaultTimeoutMs
|
|
133
|
+
const timeoutId = setTimeout(() => {
|
|
134
|
+
try {
|
|
135
|
+
child.kill()
|
|
136
|
+
} catch {
|
|
137
|
+
/* ignore */
|
|
138
|
+
}
|
|
139
|
+
if (settled) return
|
|
140
|
+
settled = true
|
|
141
|
+
cleanup()
|
|
142
|
+
reject(new Error(`${command} ${args.join(" ")} timed out after ${timeoutMs}ms`))
|
|
143
|
+
}, timeoutMs)
|
|
144
|
+
|
|
145
|
+
const abortHandler = () => {
|
|
146
|
+
try {
|
|
147
|
+
child.kill()
|
|
148
|
+
} catch {
|
|
149
|
+
/* ignore */
|
|
150
|
+
}
|
|
151
|
+
if (settled) return
|
|
152
|
+
settled = true
|
|
153
|
+
cleanup()
|
|
154
|
+
reject(new DOMException("Aborted", "AbortError"))
|
|
155
|
+
}
|
|
156
|
+
const abortCleanup = opts.signal
|
|
157
|
+
? (() => {
|
|
158
|
+
opts.signal?.addEventListener("abort", abortHandler, { once: true })
|
|
159
|
+
return () => opts.signal?.removeEventListener("abort", abortHandler)
|
|
160
|
+
})()
|
|
161
|
+
: undefined
|
|
162
|
+
|
|
163
|
+
child.onData((data: string) => {
|
|
164
|
+
stdout += data
|
|
165
|
+
})
|
|
166
|
+
child.onExit((event: { exitCode: number }) => {
|
|
167
|
+
if (settled) return
|
|
168
|
+
settled = true
|
|
169
|
+
cleanup()
|
|
170
|
+
resolve({ code: event.exitCode, signal: null, stdout, stderr: "" })
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createOutputDecoder() {
|
|
176
|
+
let decoder: TextDecoder | undefined
|
|
177
|
+
return {
|
|
178
|
+
decode(chunk: Buffer) {
|
|
179
|
+
decoder ??= new TextDecoder(detectOutputEncoding(chunk))
|
|
180
|
+
return decoder.decode(chunk, { stream: true })
|
|
181
|
+
},
|
|
182
|
+
flush() {
|
|
183
|
+
return decoder?.decode() ?? ""
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function detectOutputEncoding(chunk: Uint8Array) {
|
|
189
|
+
if (chunk[0] === 0xff && chunk[1] === 0xfe) return "utf-16le"
|
|
190
|
+
const pairs = Math.floor(chunk.length / 2)
|
|
191
|
+
if (pairs < 2) return "utf-8"
|
|
192
|
+
const oddZeroes = Array.from({ length: pairs }).filter((_, index) => chunk[index * 2 + 1] === 0).length
|
|
193
|
+
const evenZeroes = Array.from({ length: pairs }).filter((_, index) => chunk[index * 2] === 0).length
|
|
194
|
+
return oddZeroes >= Math.ceil(pairs / 3) && evenZeroes * 2 <= oddZeroes ? "utf-16le" : "utf-8"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function runWslInDistro(args: string[], distro?: string | null, opts?: RunWslOptions) {
|
|
198
|
+
return runWsl(wslArgs(args, distro), opts)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function runWslSh(script: string, distro?: string | null, opts?: RunWslOptions) {
|
|
202
|
+
return runWslInDistro(["sh", "-lc", script], distro, opts)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function probeWslRuntime(opts?: RunWslOptions): Promise<WslRuntimeCheck> {
|
|
206
|
+
const version = await runWsl(["--version"], opts).catch((error) => ({
|
|
207
|
+
code: 1,
|
|
208
|
+
signal: null,
|
|
209
|
+
stdout: "",
|
|
210
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
211
|
+
}))
|
|
212
|
+
|
|
213
|
+
if (version.code !== 0) {
|
|
214
|
+
return {
|
|
215
|
+
available: false,
|
|
216
|
+
version: null,
|
|
217
|
+
error: summarize(version.stderr || version.stdout) || "WSL is unavailable",
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
available: true,
|
|
223
|
+
version: firstLine(version.stdout),
|
|
224
|
+
error: null,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function listInstalledWslDistros(opts?: RunWslOptions) {
|
|
229
|
+
const result = await runWsl(["--list", "--verbose"], opts)
|
|
230
|
+
if (result.code !== 0) {
|
|
231
|
+
throw new Error(summarize(result.stderr || result.stdout) || "Failed to list installed WSL distros")
|
|
232
|
+
}
|
|
233
|
+
return parseInstalledDistros(result.stdout)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function listOnlineWslDistros(opts?: RunWslOptions) {
|
|
237
|
+
const result = await runWsl(["--list", "--online"], opts)
|
|
238
|
+
if (result.code !== 0) {
|
|
239
|
+
throw new Error(summarize(result.stderr || result.stdout) || "Failed to list online WSL distros")
|
|
240
|
+
}
|
|
241
|
+
return parseOnlineDistros(result.stdout)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function installWslRuntimeElevated(opts?: RunWslOptions) {
|
|
245
|
+
const script = [
|
|
246
|
+
"$ErrorActionPreference = 'Stop'",
|
|
247
|
+
"$process = Start-Process -FilePath 'wsl.exe' -Verb RunAs -ArgumentList @('--install','--no-distribution') -Wait -PassThru",
|
|
248
|
+
"if ($null -ne $process.ExitCode) { exit $process.ExitCode }",
|
|
249
|
+
].join("; ")
|
|
250
|
+
return runPowerShell(script, withTimeout(opts, DEFAULT_WSL_INSTALL_TIMEOUT_MS))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function installWslDistro(name: string, opts?: RunWslOptions) {
|
|
254
|
+
return runInteractiveCommand(
|
|
255
|
+
resolveSystem32Command("wsl.exe"),
|
|
256
|
+
["--install", "-d", name, "--web-download", "--no-launch"],
|
|
257
|
+
withTimeout(opts, DEFAULT_WSL_INSTALL_TIMEOUT_MS),
|
|
258
|
+
DEFAULT_WSL_INSTALL_TIMEOUT_MS,
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function installWslNeurocode(version: string, distro: string, opts?: RunWslOptions) {
|
|
263
|
+
return runInteractiveCommand(
|
|
264
|
+
resolveSystem32Command("wsl.exe"),
|
|
265
|
+
wslArgs(
|
|
266
|
+
["bash", "-lc", `curl -fsSL https://neurocode.ai/install | bash -s -- --version ${shellEscape(version)}`],
|
|
267
|
+
distro,
|
|
268
|
+
),
|
|
269
|
+
withTimeout(opts, DEFAULT_WSL_INSTALL_TIMEOUT_MS),
|
|
270
|
+
DEFAULT_WSL_INSTALL_TIMEOUT_MS,
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function probeWslDistro(name: string, opts?: RunWslOptions): Promise<WslDistroProbe> {
|
|
275
|
+
const executable = await runWslInDistro(["/bin/true"], name, opts).catch((error) => ({
|
|
276
|
+
code: 1,
|
|
277
|
+
signal: null,
|
|
278
|
+
stdout: "",
|
|
279
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
280
|
+
}))
|
|
281
|
+
if (executable.code !== 0) {
|
|
282
|
+
return {
|
|
283
|
+
name,
|
|
284
|
+
canExecute: false,
|
|
285
|
+
hasBash: false,
|
|
286
|
+
hasCurl: false,
|
|
287
|
+
error: summarize(executable.stderr || executable.stdout) || "Cannot execute commands in distro",
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const [bash, curl] = await Promise.all([
|
|
292
|
+
runWslSh("command -v bash >/dev/null && printf yes || printf no", name, opts),
|
|
293
|
+
runWslSh("command -v curl >/dev/null && printf yes || printf no", name, opts),
|
|
294
|
+
])
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
name,
|
|
298
|
+
canExecute: true,
|
|
299
|
+
hasBash: bash.code === 0 && summarize(bash.stdout) === "yes",
|
|
300
|
+
hasCurl: curl.code === 0 && summarize(curl.stdout) === "yes",
|
|
301
|
+
error: null,
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function resolveWslNeurocode(distro: string, opts?: RunWslOptions) {
|
|
306
|
+
return firstLine(
|
|
307
|
+
(
|
|
308
|
+
await runWslSh(
|
|
309
|
+
'if [ -x "$HOME/.neurocode/bin/neurocode" ]; then printf "%s\\n" "$HOME/.neurocode/bin/neurocode"; fi',
|
|
310
|
+
distro,
|
|
311
|
+
opts,
|
|
312
|
+
)
|
|
313
|
+
).stdout,
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function readWslCommandVersion(command: string, distro: string, opts?: RunWslOptions) {
|
|
318
|
+
const result = await runWslSh(`${shellEscape(command)} --version 2>/dev/null || true`, distro, opts)
|
|
319
|
+
return firstLine(result.stdout)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function openWslTerminal(distro?: string | null) {
|
|
323
|
+
return new Promise<void>((resolve, reject) => {
|
|
324
|
+
const child = spawn("cmd.exe", wslTerminalArgs(distro), {
|
|
325
|
+
detached: true,
|
|
326
|
+
stdio: "ignore",
|
|
327
|
+
windowsHide: true,
|
|
328
|
+
})
|
|
329
|
+
child.once("error", reject)
|
|
330
|
+
child.once("spawn", () => {
|
|
331
|
+
child.unref()
|
|
332
|
+
resolve()
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function parseInstalledDistros(output: string) {
|
|
338
|
+
return output.split(/\r?\n/g).flatMap((line) => {
|
|
339
|
+
const trimmed = line.trim()
|
|
340
|
+
if (!trimmed) return []
|
|
341
|
+
const match = line.match(/^\s*(\*)?\s*(.*?)\s{2,}\S+\s+(\d+)\s*$/)
|
|
342
|
+
if (!match) return []
|
|
343
|
+
const [, marker, name, version] = match
|
|
344
|
+
if (!name || /^name$/i.test(name)) return []
|
|
345
|
+
return [
|
|
346
|
+
{
|
|
347
|
+
name: name.trim(),
|
|
348
|
+
version: Number.isNaN(Number.parseInt(version, 10)) ? null : Number.parseInt(version, 10),
|
|
349
|
+
isDefault: marker === "*",
|
|
350
|
+
} satisfies WslInstalledDistro,
|
|
351
|
+
]
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function parseOnlineDistros(output: string) {
|
|
356
|
+
return output.split(/\r?\n/g).flatMap((line) => {
|
|
357
|
+
const trimmed = line.trim()
|
|
358
|
+
if (!trimmed) return []
|
|
359
|
+
const match = trimmed.match(/^([A-Za-z0-9._-]+)\s{2,}(.+)$/)
|
|
360
|
+
if (!match) return []
|
|
361
|
+
const [, name, label] = match
|
|
362
|
+
if (/^name$/i.test(name)) return []
|
|
363
|
+
return [{ name, label: label.trim() } satisfies WslOnlineDistro]
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function firstLine(value: string) {
|
|
368
|
+
return (
|
|
369
|
+
value
|
|
370
|
+
.split(/\r?\n/g)
|
|
371
|
+
.map((line) => line.trim())
|
|
372
|
+
.find(Boolean) ?? null
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function summarize(value: string) {
|
|
377
|
+
return value
|
|
378
|
+
.split(/\r?\n/g)
|
|
379
|
+
.map((line) => line.trim())
|
|
380
|
+
.filter(Boolean)
|
|
381
|
+
.join("\n")
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function shellEscape(value: string) {
|
|
385
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function resolveSystem32Command(command: string) {
|
|
389
|
+
const root = process.env.SystemRoot ?? process.env.windir
|
|
390
|
+
if (!root) return command
|
|
391
|
+
const resolved = join(root, "System32", command)
|
|
392
|
+
return existsSync(resolved) ? resolved : command
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function withTimeout(opts: RunWslOptions | undefined, timeoutMs: number): RunWslOptions {
|
|
396
|
+
return {
|
|
397
|
+
...opts,
|
|
398
|
+
timeoutMs: opts?.timeoutMs ?? timeoutMs,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
clearWslDistroState,
|
|
4
|
+
requireWslIpcString,
|
|
5
|
+
requireWslIpcStrings,
|
|
6
|
+
wslServerIdToRestart,
|
|
7
|
+
wslTerminalArgs,
|
|
8
|
+
} from "./policy"
|
|
9
|
+
import {
|
|
10
|
+
expectNeurocodeVersion,
|
|
11
|
+
pendingRestartAfterWslInstall,
|
|
12
|
+
pollWslHealth,
|
|
13
|
+
wslServerIdsToStartOnInitialize,
|
|
14
|
+
} from "./startup"
|
|
15
|
+
import { createWslServersController, type WslServerConfig } from "./servers"
|
|
16
|
+
|
|
17
|
+
let persistedServers: WslServerConfig[] = []
|
|
18
|
+
let releaseNeurocodeResolve: (() => void) | undefined
|
|
19
|
+
|
|
20
|
+
test("starts every configured WSL server on initialization", () => {
|
|
21
|
+
expect(
|
|
22
|
+
wslServerIdsToStartOnInitialize([
|
|
23
|
+
{ id: "wsl:Debian", distro: "Debian" },
|
|
24
|
+
{ id: "wsl:Ubuntu-24.04", distro: "Ubuntu-24.04" },
|
|
25
|
+
]),
|
|
26
|
+
).toEqual(["wsl:Debian", "wsl:Ubuntu-24.04"])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("rejects an update that did not install the desktop version", () => {
|
|
30
|
+
expect(() => expectNeurocodeVersion("1.16.2", "1.16.2")).not.toThrow()
|
|
31
|
+
expect(() => expectNeurocodeVersion("1.14.35", "1.16.2")).toThrow(
|
|
32
|
+
"Neurocode update finished but Debian still reports 1.14.35; expected 1.16.2",
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("restarts an existing distro server after updating Neurocode", () => {
|
|
37
|
+
expect(
|
|
38
|
+
wslServerIdToRestart(
|
|
39
|
+
[
|
|
40
|
+
{
|
|
41
|
+
config: { id: "wsl:Debian", distro: "Debian" },
|
|
42
|
+
runtime: { kind: "ready", url: "", username: null, password: null },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
"Debian",
|
|
46
|
+
),
|
|
47
|
+
).toBe("wsl:Debian")
|
|
48
|
+
expect(wslServerIdToRestart([], "Debian")).toBeUndefined()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("clears cached distro probes when removing a WSL server", () => {
|
|
52
|
+
expect(
|
|
53
|
+
clearWslDistroState(
|
|
54
|
+
{ Debian: { name: "Debian", canExecute: true, hasBash: true, hasCurl: true, error: null } },
|
|
55
|
+
{
|
|
56
|
+
Debian: {
|
|
57
|
+
distro: "Debian",
|
|
58
|
+
resolvedPath: "/home/luke/.neurocode/bin/neurocode",
|
|
59
|
+
version: "1.16.2",
|
|
60
|
+
expectedVersion: "1.16.2",
|
|
61
|
+
matchesDesktop: true,
|
|
62
|
+
error: null,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
"Debian",
|
|
66
|
+
),
|
|
67
|
+
).toEqual({ distroProbes: {}, neurocodeChecks: {} })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("opens terminals for distro names containing spaces", () => {
|
|
71
|
+
expect(wslTerminalArgs("Ubuntu Preview")).toEqual(["/c", "start", "", "wsl", "-d", "Ubuntu Preview"])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("stops health polling when sidecar startup settles", async () => {
|
|
75
|
+
const abort = new AbortController()
|
|
76
|
+
let checks = 0
|
|
77
|
+
const polling = pollWslHealth(
|
|
78
|
+
async () => {
|
|
79
|
+
checks++
|
|
80
|
+
return false
|
|
81
|
+
},
|
|
82
|
+
abort.signal,
|
|
83
|
+
1,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
87
|
+
abort.abort()
|
|
88
|
+
await polling
|
|
89
|
+
const settled = checks
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
91
|
+
expect(checks).toBe(settled)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("validates WSL IPC identifiers at the module boundary", () => {
|
|
95
|
+
expect(requireWslIpcString("distro", "Debian")).toBe("Debian")
|
|
96
|
+
expect(requireWslIpcStrings("distro", ["Debian", "Ubuntu"])).toEqual(["Debian", "Ubuntu"])
|
|
97
|
+
expect(() => requireWslIpcString("distro", "")).toThrow("Invalid distro")
|
|
98
|
+
expect(() => requireWslIpcString("server id", undefined)).toThrow("Invalid server id")
|
|
99
|
+
expect(() => requireWslIpcStrings("distro", [])).toThrow("Invalid distro")
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("derives a required Windows restart from the post-install runtime probe", () => {
|
|
103
|
+
expect(pendingRestartAfterWslInstall({ available: false, version: null, error: "WSL unavailable" })).toBe(true)
|
|
104
|
+
expect(pendingRestartAfterWslInstall({ available: true, version: "WSL version: 2.6.1", error: null })).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test("ignores stale background Neurocode checks after removing a WSL server", async () => {
|
|
108
|
+
persistedServers = []
|
|
109
|
+
releaseNeurocodeResolve = undefined
|
|
110
|
+
const controller = createWslServersController(
|
|
111
|
+
"1.16.2",
|
|
112
|
+
async () => ({
|
|
113
|
+
listener: {
|
|
114
|
+
stop: () => undefined,
|
|
115
|
+
onExit: () => undefined,
|
|
116
|
+
},
|
|
117
|
+
url: "http://127.0.0.1:4096",
|
|
118
|
+
username: "neurocode",
|
|
119
|
+
password: "secret",
|
|
120
|
+
}),
|
|
121
|
+
testControllerOptions(),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
await controller.addServer("Debian")
|
|
125
|
+
await waitFor(() => !!releaseNeurocodeResolve)
|
|
126
|
+
await controller.removeServer("wsl:Debian")
|
|
127
|
+
releaseNeurocodeResolve?.()
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
129
|
+
|
|
130
|
+
expect(controller.getState().servers).toEqual([])
|
|
131
|
+
expect(controller.getState().neurocodeChecks).toEqual({})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("ignores stale startup Neurocode checks after removing a WSL server", async () => {
|
|
135
|
+
persistedServers = [{ id: "wsl:Debian", distro: "Debian" }]
|
|
136
|
+
releaseNeurocodeResolve = undefined
|
|
137
|
+
const controller = createWslServersController(
|
|
138
|
+
"1.16.2",
|
|
139
|
+
async () => new Promise<never>(() => undefined),
|
|
140
|
+
testControllerOptions(),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
await controller.initialize()
|
|
144
|
+
await waitFor(() => !!releaseNeurocodeResolve)
|
|
145
|
+
await controller.removeServer("wsl:Debian")
|
|
146
|
+
releaseNeurocodeResolve?.()
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
148
|
+
|
|
149
|
+
expect(controller.getState().servers).toEqual([])
|
|
150
|
+
expect(controller.getState().neurocodeChecks).toEqual({})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("probes addable distros in parallel before checking Neurocode", async () => {
|
|
154
|
+
persistedServers = []
|
|
155
|
+
const started: string[] = []
|
|
156
|
+
const release = new Map<string, () => void>()
|
|
157
|
+
const neurocode: string[] = []
|
|
158
|
+
const controller = createWslServersController("1.16.2", async () => new Promise<never>(() => undefined), {
|
|
159
|
+
...testControllerOptions(),
|
|
160
|
+
probeDistro: async (distro) => {
|
|
161
|
+
started.push(distro)
|
|
162
|
+
await new Promise<void>((resolve) => release.set(distro, resolve))
|
|
163
|
+
return { name: distro, canExecute: true, hasBash: true, hasCurl: true, error: null }
|
|
164
|
+
},
|
|
165
|
+
resolveNeurocode: async (distro) => {
|
|
166
|
+
neurocode.push(distro)
|
|
167
|
+
return "/home/me/.neurocode/bin/neurocode"
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const task = controller.probeAddable(["Debian", "Ubuntu"])
|
|
172
|
+
await waitFor(() => started.length === 2)
|
|
173
|
+
expect(started).toEqual(["Debian", "Ubuntu"])
|
|
174
|
+
expect(neurocode).toEqual([])
|
|
175
|
+
release.get("Debian")?.()
|
|
176
|
+
release.get("Ubuntu")?.()
|
|
177
|
+
await task
|
|
178
|
+
|
|
179
|
+
expect(Object.keys(controller.getState().distroProbes)).toEqual(["Debian", "Ubuntu"])
|
|
180
|
+
expect(neurocode).toEqual(["Debian", "Ubuntu"])
|
|
181
|
+
expect(Object.keys(controller.getState().neurocodeChecks)).toEqual(["Debian", "Ubuntu"])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test("does not check Neurocode in addable distros that cannot execute commands", async () => {
|
|
185
|
+
persistedServers = []
|
|
186
|
+
const neurocode: string[] = []
|
|
187
|
+
const controller = createWslServersController("1.16.2", async () => new Promise<never>(() => undefined), {
|
|
188
|
+
...testControllerOptions(),
|
|
189
|
+
probeDistro: async (distro) => ({
|
|
190
|
+
name: distro,
|
|
191
|
+
canExecute: distro === "Debian",
|
|
192
|
+
hasBash: distro === "Debian",
|
|
193
|
+
hasCurl: distro === "Debian",
|
|
194
|
+
error: distro === "Debian" ? null : "Open Ubuntu once to finish setup",
|
|
195
|
+
}),
|
|
196
|
+
resolveNeurocode: async (distro) => {
|
|
197
|
+
neurocode.push(distro)
|
|
198
|
+
return "/home/me/.neurocode/bin/neurocode"
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
await controller.probeAddable(["Debian", "Ubuntu"])
|
|
203
|
+
|
|
204
|
+
expect(Object.keys(controller.getState().distroProbes)).toEqual(["Debian", "Ubuntu"])
|
|
205
|
+
expect(neurocode).toEqual(["Debian"])
|
|
206
|
+
expect(Object.keys(controller.getState().neurocodeChecks)).toEqual(["Debian"])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
async function waitFor(check: () => boolean) {
|
|
210
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
211
|
+
if (check()) return
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Timed out waiting for condition")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function testControllerOptions() {
|
|
218
|
+
return {
|
|
219
|
+
readServers: () => persistedServers,
|
|
220
|
+
writeServers: (servers: WslServerConfig[]) => {
|
|
221
|
+
persistedServers = servers
|
|
222
|
+
},
|
|
223
|
+
readCommandVersion: async () => "1.16.2",
|
|
224
|
+
resolveNeurocode: async () => {
|
|
225
|
+
await new Promise<void>((resolve) => {
|
|
226
|
+
releaseNeurocodeResolve = resolve
|
|
227
|
+
})
|
|
228
|
+
return "/home/me/.neurocode/bin/neurocode"
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
}
|