@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,521 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WslDistroProbe,
|
|
3
|
+
WslInstalledDistro,
|
|
4
|
+
WslJob,
|
|
5
|
+
WslOnlineDistro,
|
|
6
|
+
WslNeurocodeCheck,
|
|
7
|
+
WslRuntimeCheck,
|
|
8
|
+
WslServerConfig,
|
|
9
|
+
WslServerItem,
|
|
10
|
+
WslServerRuntime,
|
|
11
|
+
WslServersEvent,
|
|
12
|
+
WslServersState,
|
|
13
|
+
} from "../../preload/types"
|
|
14
|
+
import { WSL_SERVERS_KEY } from "../store-keys"
|
|
15
|
+
import { getStore } from "../store"
|
|
16
|
+
import { expectNeurocodeVersion, pendingRestartAfterWslInstall, wslServerIdsToStartOnInitialize } from "./startup"
|
|
17
|
+
import { clearWslDistroState, wslServerIdToRestart } from "./policy"
|
|
18
|
+
import {
|
|
19
|
+
installWslDistro,
|
|
20
|
+
installWslNeurocode,
|
|
21
|
+
installWslRuntimeElevated,
|
|
22
|
+
listInstalledWslDistros,
|
|
23
|
+
listOnlineWslDistros,
|
|
24
|
+
openWslTerminal,
|
|
25
|
+
probeWslDistro,
|
|
26
|
+
probeWslRuntime,
|
|
27
|
+
readWslCommandVersion,
|
|
28
|
+
resolveWslNeurocode,
|
|
29
|
+
summarize,
|
|
30
|
+
} from "./runtime"
|
|
31
|
+
|
|
32
|
+
type RunningSidecar = {
|
|
33
|
+
listener: { stop: () => void; onExit: (cb: (code: number | null, signal: NodeJS.Signals | null) => void) => void }
|
|
34
|
+
url: string
|
|
35
|
+
username: string | null
|
|
36
|
+
password: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type SpawnSidecar = (distro: string) => Promise<RunningSidecar>
|
|
40
|
+
|
|
41
|
+
type ControllerLogger = {
|
|
42
|
+
log: (message: string, meta?: unknown) => void
|
|
43
|
+
error: (message: string, meta?: unknown) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type WslServersControllerOptions = {
|
|
47
|
+
logger?: ControllerLogger
|
|
48
|
+
readServers?: () => WslServerConfig[]
|
|
49
|
+
writeServers?: (servers: WslServerConfig[]) => void
|
|
50
|
+
probeDistro?: typeof probeWslDistro
|
|
51
|
+
resolveNeurocode?: typeof resolveWslNeurocode
|
|
52
|
+
readCommandVersion?: typeof readWslCommandVersion
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type WslServersController = ReturnType<typeof createWslServersController>
|
|
56
|
+
|
|
57
|
+
export function wslServerIdForDistro(distro: string) {
|
|
58
|
+
return `wsl:${distro}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createWslServersController(
|
|
62
|
+
appVersion: string,
|
|
63
|
+
spawnSidecar: SpawnSidecar,
|
|
64
|
+
options?: WslServersControllerOptions,
|
|
65
|
+
) {
|
|
66
|
+
let state: WslServersState = initialState()
|
|
67
|
+
const listeners = new Set<(event: WslServersEvent) => void>()
|
|
68
|
+
const sidecars = new Map<string, RunningSidecar>()
|
|
69
|
+
const startAttempts = new Map<string, number>()
|
|
70
|
+
let jobAbort: AbortController | undefined
|
|
71
|
+
const logger = options?.logger
|
|
72
|
+
const readServers = options?.readServers ?? readPersistedServers
|
|
73
|
+
const writeServers = options?.writeServers ?? writePersistedServers
|
|
74
|
+
const probeDistro = options?.probeDistro ?? probeWslDistro
|
|
75
|
+
|
|
76
|
+
const emit = () => {
|
|
77
|
+
for (const listener of listeners) listener({ type: "state", state })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const setState = (next: Partial<WslServersState>) => {
|
|
81
|
+
state = { ...state, ...next }
|
|
82
|
+
emit()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const persistServers = (servers: WslServerConfig[]) => {
|
|
86
|
+
writeServers(servers)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const updateServer = (id: string, update: (item: WslServerItem) => WslServerItem) => {
|
|
90
|
+
const next = state.servers.map((item) => (item.config.id === id ? update(item) : item))
|
|
91
|
+
setState({ servers: next })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const beginJob = (job: WslJob): AbortController => {
|
|
95
|
+
jobAbort?.abort()
|
|
96
|
+
const abort = new AbortController()
|
|
97
|
+
jobAbort = abort
|
|
98
|
+
setState({ job })
|
|
99
|
+
return abort
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const endJob = (abort: AbortController) => {
|
|
103
|
+
if (jobAbort !== abort) return
|
|
104
|
+
jobAbort = undefined
|
|
105
|
+
setState({ job: null })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const refreshFromStore = () => {
|
|
109
|
+
const persisted = readServers()
|
|
110
|
+
const items: WslServerItem[] = persisted.map((config) => {
|
|
111
|
+
const existing = state.servers.find((item) => item.config.id === config.id)
|
|
112
|
+
return {
|
|
113
|
+
config,
|
|
114
|
+
runtime: existing?.runtime ?? { kind: "stopped" },
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
setState({ servers: items })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const setRuntime = (id: string, runtime: WslServerRuntime) => {
|
|
121
|
+
updateServer(id, (item) => ({ ...item, runtime }))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const setNeurocodeCheck = (distro: string, check: WslNeurocodeCheck) => {
|
|
125
|
+
setState({
|
|
126
|
+
neurocodeChecks: {
|
|
127
|
+
...state.neurocodeChecks,
|
|
128
|
+
[distro]: check,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const checkNeurocode = async (distro: string, opts?: { signal?: AbortSignal }) => {
|
|
134
|
+
const resolved = await (options?.resolveNeurocode ?? resolveWslNeurocode)(distro, opts)
|
|
135
|
+
const version = resolved
|
|
136
|
+
? await (options?.readCommandVersion ?? readWslCommandVersion)(resolved, distro, opts)
|
|
137
|
+
: null
|
|
138
|
+
return neurocodeCheck(distro, resolved, version, appVersion)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const refreshNeurocodeCheck = async (distro: string, opts?: { signal?: AbortSignal }) => {
|
|
142
|
+
setNeurocodeCheck(distro, await checkNeurocode(distro, opts))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const probeAddableDistros = async (distros: string[], opts?: { signal?: AbortSignal }) => {
|
|
146
|
+
const unique = [...new Set(distros)]
|
|
147
|
+
const distroProbes = await Promise.all(
|
|
148
|
+
unique
|
|
149
|
+
.filter((distro) => !state.distroProbes[distro])
|
|
150
|
+
.map(async (distro) => [distro, await probeDistro(distro, opts)] as const),
|
|
151
|
+
)
|
|
152
|
+
if (distroProbes.length) {
|
|
153
|
+
setState({ distroProbes: { ...state.distroProbes, ...Object.fromEntries(distroProbes) } })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const neurocodeChecks = await Promise.all(
|
|
157
|
+
unique
|
|
158
|
+
.filter((distro) => distroProbeReady(state.distroProbes[distro]))
|
|
159
|
+
.filter((distro) => !state.neurocodeChecks[distro])
|
|
160
|
+
.map(async (distro) => [distro, await checkNeurocode(distro, opts)] as const),
|
|
161
|
+
)
|
|
162
|
+
if (neurocodeChecks.length) {
|
|
163
|
+
setState({ neurocodeChecks: { ...state.neurocodeChecks, ...Object.fromEntries(neurocodeChecks) } })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const hasServer = (id: string, distro: string) => {
|
|
168
|
+
return state.servers.some((item) => item.config.id === id && item.config.distro === distro)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const refreshNeurocodeCheckBackground = (id: string, distro: string) => {
|
|
172
|
+
void checkNeurocode(distro)
|
|
173
|
+
.then((check) => {
|
|
174
|
+
if (!hasServer(id, distro)) return
|
|
175
|
+
setNeurocodeCheck(distro, check)
|
|
176
|
+
})
|
|
177
|
+
.catch((error) => {
|
|
178
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
179
|
+
logger?.error("wsl neurocode check failed", { id, distro, message })
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const refreshNeurocodeChecks = async () => {
|
|
184
|
+
await Promise.all(
|
|
185
|
+
state.servers.map((item) =>
|
|
186
|
+
checkNeurocode(item.config.distro)
|
|
187
|
+
.then((check) => {
|
|
188
|
+
if (!hasServer(item.config.id, item.config.distro)) return
|
|
189
|
+
setNeurocodeCheck(item.config.distro, check)
|
|
190
|
+
})
|
|
191
|
+
.catch((error) => {
|
|
192
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
193
|
+
logger?.error("wsl neurocode check failed", {
|
|
194
|
+
id: item.config.id,
|
|
195
|
+
distro: item.config.distro,
|
|
196
|
+
message,
|
|
197
|
+
})
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const refreshDistroLists = async (opts: { signal?: AbortSignal }) => {
|
|
204
|
+
const [installed, online] = await Promise.all([listInstalledWslDistros(opts), listOnlineWslDistros(opts)])
|
|
205
|
+
return { installed, online }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const nextStartAttempt = (id: string) => {
|
|
209
|
+
const next = (startAttempts.get(id) ?? 0) + 1
|
|
210
|
+
startAttempts.set(id, next)
|
|
211
|
+
return next
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const invalidateStartAttempt = (id: string) => {
|
|
215
|
+
startAttempts.set(id, (startAttempts.get(id) ?? 0) + 1)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const isCurrentStartAttempt = (id: string, attempt: number) => {
|
|
219
|
+
return startAttempts.get(id) === attempt && state.servers.some((item) => item.config.id === id)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const startServer = async (id: string) => {
|
|
223
|
+
const item = state.servers.find((x) => x.config.id === id)
|
|
224
|
+
if (!item) return
|
|
225
|
+
const attempt = nextStartAttempt(id)
|
|
226
|
+
await stopServerInternal(id)
|
|
227
|
+
if (!isCurrentStartAttempt(id, attempt)) return
|
|
228
|
+
setRuntime(id, { kind: "starting" })
|
|
229
|
+
logger?.log("wsl sidecar starting", { id, distro: item.config.distro })
|
|
230
|
+
try {
|
|
231
|
+
const sidecar = await spawnSidecar(item.config.distro)
|
|
232
|
+
if (!isCurrentStartAttempt(id, attempt)) {
|
|
233
|
+
try {
|
|
234
|
+
sidecar.listener.stop()
|
|
235
|
+
} catch {
|
|
236
|
+
// ignore stop errors for stale sidecars
|
|
237
|
+
}
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
sidecars.set(id, sidecar)
|
|
241
|
+
setRuntime(id, {
|
|
242
|
+
kind: "ready",
|
|
243
|
+
url: sidecar.url,
|
|
244
|
+
username: sidecar.username,
|
|
245
|
+
password: sidecar.password,
|
|
246
|
+
})
|
|
247
|
+
sidecar.listener.onExit((code, signal) => {
|
|
248
|
+
if (sidecars.get(id) !== sidecar) return
|
|
249
|
+
sidecars.delete(id)
|
|
250
|
+
const message = startupFailure(code, signal)
|
|
251
|
+
setRuntime(id, { kind: "failed", message })
|
|
252
|
+
logger?.error("wsl sidecar exited", { id, distro: item.config.distro, code, signal })
|
|
253
|
+
})
|
|
254
|
+
refreshNeurocodeCheckBackground(id, item.config.distro)
|
|
255
|
+
logger?.log("wsl sidecar ready", { id, distro: item.config.distro, url: sidecar.url })
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
258
|
+
if (!isCurrentStartAttempt(id, attempt)) return
|
|
259
|
+
setRuntime(id, { kind: "failed", message })
|
|
260
|
+
// Without this, an Ubuntu-style silent failure leaves no trace in
|
|
261
|
+
// main.log — the controller captures the message in its state but
|
|
262
|
+
// nothing surfaces unless the user opens the WSL servers dialog.
|
|
263
|
+
logger?.error("wsl sidecar failed to start", { id, distro: item.config.distro, message })
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const stopServerInternal = async (id: string) => {
|
|
268
|
+
const existing = sidecars.get(id)
|
|
269
|
+
if (!existing) return
|
|
270
|
+
sidecars.delete(id)
|
|
271
|
+
try {
|
|
272
|
+
existing.listener.stop()
|
|
273
|
+
} catch {
|
|
274
|
+
// ignore stop errors
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const runJob = async <T>(job: WslJob, runner: (abort: AbortController) => Promise<T>) => {
|
|
279
|
+
const abort = beginJob(job)
|
|
280
|
+
try {
|
|
281
|
+
const value = await runner(abort)
|
|
282
|
+
endJob(abort)
|
|
283
|
+
return value
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
286
|
+
endJob(abort)
|
|
287
|
+
return undefined
|
|
288
|
+
}
|
|
289
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
290
|
+
endJob(abort)
|
|
291
|
+
throw err
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
getState() {
|
|
297
|
+
return state
|
|
298
|
+
},
|
|
299
|
+
subscribe(listener: (event: WslServersEvent) => void) {
|
|
300
|
+
listeners.add(listener)
|
|
301
|
+
return () => listeners.delete(listener)
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
async initialize() {
|
|
305
|
+
refreshFromStore()
|
|
306
|
+
void refreshNeurocodeChecks()
|
|
307
|
+
for (const id of wslServerIdsToStartOnInitialize(state.servers.map((item) => item.config))) void startServer(id)
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
async probeRuntime() {
|
|
311
|
+
await runJob({ kind: "runtime", startedAt: Date.now() }, async (abort) => {
|
|
312
|
+
const runtime = await probeWslRuntime({ signal: abort.signal })
|
|
313
|
+
setState({
|
|
314
|
+
runtime,
|
|
315
|
+
pendingRestart: state.pendingRestart && !runtime.available ? state.pendingRestart : false,
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
async refreshDistros() {
|
|
321
|
+
await runJob({ kind: "distros", startedAt: Date.now() }, async (abort) => {
|
|
322
|
+
setState(await refreshDistroLists({ signal: abort.signal }))
|
|
323
|
+
})
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
async installWsl() {
|
|
327
|
+
await runJob({ kind: "install-wsl", startedAt: Date.now() }, async (abort) => {
|
|
328
|
+
const result = await installWslRuntimeElevated({ signal: abort.signal })
|
|
329
|
+
if (result.code !== 0) {
|
|
330
|
+
const message = summarize(result.stderr || result.stdout) || "WSL installation failed"
|
|
331
|
+
throw new Error(message)
|
|
332
|
+
}
|
|
333
|
+
const runtime = await probeWslRuntime({ signal: abort.signal })
|
|
334
|
+
setState({ runtime, pendingRestart: pendingRestartAfterWslInstall(runtime) })
|
|
335
|
+
})
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
async installDistro(name: string) {
|
|
339
|
+
await runJob({ kind: "install-distro", distro: name, startedAt: Date.now() }, async (abort) => {
|
|
340
|
+
const result = await installWslDistro(name, { signal: abort.signal })
|
|
341
|
+
if (result.code !== 0) {
|
|
342
|
+
const message = summarize(result.stderr || result.stdout) || `Failed to install distro: ${name}`
|
|
343
|
+
throw new Error(message)
|
|
344
|
+
}
|
|
345
|
+
const distros = await refreshDistroLists({ signal: abort.signal })
|
|
346
|
+
const probe = await probeDistro(name, { signal: abort.signal })
|
|
347
|
+
setState({
|
|
348
|
+
...distros,
|
|
349
|
+
distroProbes: { ...state.distroProbes, [name]: probe },
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
async probeAddable(distros: string[]) {
|
|
355
|
+
if (!distros.length) return
|
|
356
|
+
await runJob({ kind: "probe-addable", distros, startedAt: Date.now() }, async (abort) => {
|
|
357
|
+
await probeAddableDistros(distros, { signal: abort.signal })
|
|
358
|
+
})
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
async installNeurocode(name: string) {
|
|
362
|
+
await runJob({ kind: "install-neurocode", distro: name, startedAt: Date.now() }, async (abort) => {
|
|
363
|
+
const result = await installWslNeurocode(appVersion, name, { signal: abort.signal })
|
|
364
|
+
if (result.code !== 0) {
|
|
365
|
+
throw new Error(summarize(result.stderr || result.stdout) || "NeuroCode installation failed")
|
|
366
|
+
}
|
|
367
|
+
await refreshNeurocodeCheck(name, { signal: abort.signal })
|
|
368
|
+
expectNeurocodeVersion(state.neurocodeChecks[name]?.version ?? null, appVersion, name)
|
|
369
|
+
const id = wslServerIdToRestart(state.servers, name)
|
|
370
|
+
if (id) await startServer(id)
|
|
371
|
+
})
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
async openTerminal(name: string) {
|
|
375
|
+
await openWslTerminal(name)
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
async addServer(distro: string): Promise<WslServerConfig> {
|
|
379
|
+
const id = wslServerIdForDistro(distro)
|
|
380
|
+
if (state.servers.some((item) => item.config.id === id)) {
|
|
381
|
+
throw new Error(`${distro} is already added`)
|
|
382
|
+
}
|
|
383
|
+
const config: WslServerConfig = {
|
|
384
|
+
id,
|
|
385
|
+
distro,
|
|
386
|
+
}
|
|
387
|
+
persistServers([...readServers(), config])
|
|
388
|
+
setState({
|
|
389
|
+
servers: [...state.servers, { config, runtime: { kind: "starting" } }],
|
|
390
|
+
})
|
|
391
|
+
void startServer(id)
|
|
392
|
+
return config
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
async removeServer(id: string) {
|
|
396
|
+
const distro = state.servers.find((item) => item.config.id === id)?.config.distro
|
|
397
|
+
invalidateStartAttempt(id)
|
|
398
|
+
await stopServerInternal(id)
|
|
399
|
+
const remaining = readServers().filter((item) => item.id !== id)
|
|
400
|
+
persistServers(remaining)
|
|
401
|
+
setState({
|
|
402
|
+
servers: state.servers.filter((item) => item.config.id !== id),
|
|
403
|
+
...(distro ? clearWslDistroState(state.distroProbes, state.neurocodeChecks, distro) : {}),
|
|
404
|
+
})
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
startServer,
|
|
408
|
+
|
|
409
|
+
stopAll() {
|
|
410
|
+
for (const item of state.servers) invalidateStartAttempt(item.config.id)
|
|
411
|
+
for (const existing of sidecars.values()) {
|
|
412
|
+
try {
|
|
413
|
+
existing.listener.stop()
|
|
414
|
+
} catch {
|
|
415
|
+
// ignore
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
sidecars.clear()
|
|
419
|
+
},
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function initialState(): WslServersState {
|
|
424
|
+
return {
|
|
425
|
+
runtime: null,
|
|
426
|
+
installed: [],
|
|
427
|
+
online: [],
|
|
428
|
+
distroProbes: {},
|
|
429
|
+
neurocodeChecks: {},
|
|
430
|
+
pendingRestart: false,
|
|
431
|
+
servers: [],
|
|
432
|
+
job: null,
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function readPersistedServers(): WslServerConfig[] {
|
|
437
|
+
const store = getStore()
|
|
438
|
+
const existing = store.get(WSL_SERVERS_KEY)
|
|
439
|
+
if (existing && typeof existing === "object") {
|
|
440
|
+
const record = existing as { servers?: unknown }
|
|
441
|
+
const list = Array.isArray(record.servers) ? record.servers : []
|
|
442
|
+
return list.flatMap(normalizePersistedServer)
|
|
443
|
+
}
|
|
444
|
+
return []
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function writePersistedServers(servers: WslServerConfig[]) {
|
|
448
|
+
getStore().set(WSL_SERVERS_KEY, { servers })
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function normalizePersistedServer(value: unknown): WslServerConfig[] {
|
|
452
|
+
if (!value || typeof value !== "object") return []
|
|
453
|
+
const record = value as Record<string, unknown>
|
|
454
|
+
const distro = typeof record.distro === "string" && record.distro.length > 0 ? record.distro : null
|
|
455
|
+
if (!distro) return []
|
|
456
|
+
const id = typeof record.id === "string" && record.id.length > 0 ? record.id : wslServerIdForDistro(distro)
|
|
457
|
+
return [
|
|
458
|
+
{
|
|
459
|
+
id,
|
|
460
|
+
distro,
|
|
461
|
+
},
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function neurocodeCheck(
|
|
466
|
+
distro: string,
|
|
467
|
+
resolvedPath: string | null,
|
|
468
|
+
version: string | null,
|
|
469
|
+
expectedVersion: string,
|
|
470
|
+
): WslNeurocodeCheck {
|
|
471
|
+
if (!resolvedPath) {
|
|
472
|
+
return {
|
|
473
|
+
distro,
|
|
474
|
+
resolvedPath: null,
|
|
475
|
+
version: null,
|
|
476
|
+
expectedVersion,
|
|
477
|
+
matchesDesktop: null,
|
|
478
|
+
error: "neurocode is not installed in this distro",
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (!version) {
|
|
482
|
+
return {
|
|
483
|
+
distro,
|
|
484
|
+
resolvedPath,
|
|
485
|
+
version: null,
|
|
486
|
+
expectedVersion,
|
|
487
|
+
matchesDesktop: null,
|
|
488
|
+
error: "neurocode is installed but could not run",
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
distro,
|
|
493
|
+
resolvedPath,
|
|
494
|
+
version,
|
|
495
|
+
expectedVersion,
|
|
496
|
+
matchesDesktop: version === expectedVersion,
|
|
497
|
+
error: null,
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function distroProbeReady(probe: WslDistroProbe | undefined) {
|
|
502
|
+
return !!probe?.canExecute && probe.hasBash && probe.hasCurl
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function startupFailure(code: number | null, signal: NodeJS.Signals | null) {
|
|
506
|
+
return `WSL server exited after startup (code=${code ?? "null"} signal=${signal ?? "null"})`
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Re-export types used by callers
|
|
510
|
+
export type {
|
|
511
|
+
WslInstalledDistro,
|
|
512
|
+
WslOnlineDistro,
|
|
513
|
+
WslRuntimeCheck,
|
|
514
|
+
WslDistroProbe,
|
|
515
|
+
WslNeurocodeCheck,
|
|
516
|
+
WslServerConfig,
|
|
517
|
+
WslServerItem,
|
|
518
|
+
WslServerRuntime,
|
|
519
|
+
WslServersEvent,
|
|
520
|
+
WslServersState,
|
|
521
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { randomUUID } from "node:crypto"
|
|
3
|
+
import { createServer } from "node:net"
|
|
4
|
+
import { app } from "electron"
|
|
5
|
+
import { checkHealth } from "../server"
|
|
6
|
+
import { type WslCommandLine, resolveWslNeurocode, shellEscape, wslArgs } from "./runtime"
|
|
7
|
+
import { pollWslHealth } from "./startup"
|
|
8
|
+
|
|
9
|
+
export type WslSidecar = {
|
|
10
|
+
listener: { stop: () => void; onExit: (cb: (code: number | null, signal: NodeJS.Signals | null) => void) => void }
|
|
11
|
+
url: string
|
|
12
|
+
username: string | null
|
|
13
|
+
password: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function spawnWslSidecar(
|
|
17
|
+
distro: string,
|
|
18
|
+
opts: { onLine?: (line: WslCommandLine) => void; healthTimeoutMs?: number } = {},
|
|
19
|
+
): Promise<WslSidecar> {
|
|
20
|
+
const neurocodePath = await resolveWslNeurocode(distro)
|
|
21
|
+
if (!neurocodePath) throw new Error(`NeuroCode is not installed in ${distro}`)
|
|
22
|
+
|
|
23
|
+
const port = await allocatePort()
|
|
24
|
+
const password = randomUUID()
|
|
25
|
+
const username = "neurocode"
|
|
26
|
+
const script = [
|
|
27
|
+
"set -euo pipefail",
|
|
28
|
+
'cd "$HOME" || cd /',
|
|
29
|
+
'PATH=$(awk -v RS=: -v ORS=: \'$0 !~ /^\\/mnt\\//\' <<<"$PATH" | sed "s/:$//")',
|
|
30
|
+
"export PATH",
|
|
31
|
+
"export WSLENV=",
|
|
32
|
+
"export NEUROCODE_EXPERIMENTAL_DISABLE_FILEWATCHER=true",
|
|
33
|
+
"export NEUROCODE_CLIENT=desktop",
|
|
34
|
+
`export NEUROCODE_SERVER_USERNAME=${shellEscape(username)}`,
|
|
35
|
+
`export NEUROCODE_SERVER_PASSWORD=${shellEscape(password)}`,
|
|
36
|
+
'export XDG_STATE_HOME="$HOME/.local/state"',
|
|
37
|
+
`exec ${shellEscape(neurocodePath)} --print-logs --log-level ${app.isPackaged ? "WARN" : "INFO"} serve --hostname 0.0.0.0 --port ${port}`,
|
|
38
|
+
].join("\n")
|
|
39
|
+
const child = spawn("wsl", wslArgs(["bash", "-se"], distro), {
|
|
40
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
})
|
|
43
|
+
child.stdin.end(script)
|
|
44
|
+
|
|
45
|
+
const recentOutput: string[] = []
|
|
46
|
+
const emit = (line: WslCommandLine) => {
|
|
47
|
+
if (!line.text.trim()) return
|
|
48
|
+
recentOutput.push(`[${line.stream}] ${line.text}`)
|
|
49
|
+
if (recentOutput.length > 12) recentOutput.shift()
|
|
50
|
+
opts.onLine?.(line)
|
|
51
|
+
}
|
|
52
|
+
forwardLines(child.stdout, "stdout", emit)
|
|
53
|
+
forwardLines(child.stderr, "stderr", emit)
|
|
54
|
+
|
|
55
|
+
const exit = new Promise<never>((_, reject) => {
|
|
56
|
+
child.once("error", reject)
|
|
57
|
+
child.once("exit", (code, signal) => reject(new Error(startupFailure(code, signal, recentOutput))))
|
|
58
|
+
})
|
|
59
|
+
const url = `http://127.0.0.1:${port}`
|
|
60
|
+
const startup = new AbortController()
|
|
61
|
+
const health = pollWslHealth(() => checkHealth(url, password), startup.signal)
|
|
62
|
+
const timeoutMs = opts.healthTimeoutMs ?? 30_000
|
|
63
|
+
let timeout: ReturnType<typeof setTimeout>
|
|
64
|
+
const timedOut = new Promise<never>(
|
|
65
|
+
(_, reject) =>
|
|
66
|
+
(timeout = setTimeout(
|
|
67
|
+
() => reject(new Error(`Sidecar for ${distro} health check timed out after ${timeoutMs}ms`)),
|
|
68
|
+
timeoutMs,
|
|
69
|
+
)),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
await Promise.race([health, exit, timedOut])
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
child.kill()
|
|
75
|
+
throw error
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
clearTimeout(timeout)
|
|
79
|
+
startup.abort()
|
|
80
|
+
})
|
|
81
|
+
return {
|
|
82
|
+
listener: {
|
|
83
|
+
stop: () => child.kill(),
|
|
84
|
+
onExit: (cb) => child.once("exit", cb),
|
|
85
|
+
},
|
|
86
|
+
url,
|
|
87
|
+
username,
|
|
88
|
+
password,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function allocatePort() {
|
|
93
|
+
return new Promise<number>((resolve, reject) => {
|
|
94
|
+
const server = createServer()
|
|
95
|
+
server.on("error", reject)
|
|
96
|
+
server.listen(0, "127.0.0.1", () => {
|
|
97
|
+
const address = server.address()
|
|
98
|
+
if (typeof address !== "object" || !address) {
|
|
99
|
+
server.close()
|
|
100
|
+
reject(new Error("Failed to get port"))
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
server.close(() => resolve(address.port))
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function forwardLines(
|
|
109
|
+
stream: NodeJS.ReadableStream,
|
|
110
|
+
source: WslCommandLine["stream"],
|
|
111
|
+
onLine: (line: WslCommandLine) => void,
|
|
112
|
+
) {
|
|
113
|
+
let pending = ""
|
|
114
|
+
stream.setEncoding("utf8")
|
|
115
|
+
stream.on("data", (chunk: string) => {
|
|
116
|
+
pending += chunk
|
|
117
|
+
const lines = pending.split(/\r?\n/g)
|
|
118
|
+
pending = lines.pop() ?? ""
|
|
119
|
+
lines.forEach((text) => onLine({ stream: source, text }))
|
|
120
|
+
})
|
|
121
|
+
stream.on("end", () => {
|
|
122
|
+
if (pending) onLine({ stream: source, text: pending })
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function startupFailure(code: number | null, signal: NodeJS.Signals | null, recentOutput: string[]) {
|
|
127
|
+
const suffix = recentOutput.length ? `\n${recentOutput.join("\n")}` : ""
|
|
128
|
+
return `WSL server exited before becoming healthy (code=${code ?? "null"} signal=${signal ?? "null"})${suffix}`
|
|
129
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function wslServerIdsToStartOnInitialize(servers: { id: string }[]) {
|
|
2
|
+
return servers.map((server) => server.id)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function expectNeurocodeVersion(installed: string | null, expected: string, distro = "Debian") {
|
|
6
|
+
if (installed === expected) return
|
|
7
|
+
throw new Error(
|
|
8
|
+
`Neurocode update finished but ${distro} still reports ${installed ?? "no version"}; expected ${expected}`,
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const pendingRestartAfterWslInstall = (runtime: { available: boolean }) => !runtime.available
|
|
13
|
+
|
|
14
|
+
export async function pollWslHealth(check: () => Promise<boolean>, signal: AbortSignal, interval = 100) {
|
|
15
|
+
while (!signal.aborted) {
|
|
16
|
+
if (await check()) return
|
|
17
|
+
await abortableDelay(interval, signal)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function abortableDelay(duration: number, signal: AbortSignal) {
|
|
22
|
+
return new Promise<void>((resolve) => {
|
|
23
|
+
const done = () => {
|
|
24
|
+
clearTimeout(timeout)
|
|
25
|
+
signal.removeEventListener("abort", done)
|
|
26
|
+
resolve()
|
|
27
|
+
}
|
|
28
|
+
const timeout = setTimeout(done, duration)
|
|
29
|
+
signal.addEventListener("abort", done, { once: true })
|
|
30
|
+
})
|
|
31
|
+
}
|