@namzu/sandbox 23.0.1 → 25.0.0

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.
Files changed (158) hide show
  1. package/CHANGELOG.md +290 -0
  2. package/dist/backends/http-worker-client.d.ts +3 -1
  3. package/dist/backends/http-worker-client.d.ts.map +1 -1
  4. package/dist/backends/http-worker-client.js +15 -4
  5. package/dist/backends/http-worker-client.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/local-virtual-computer/client.d.ts +21 -0
  11. package/dist/local-virtual-computer/client.d.ts.map +1 -0
  12. package/dist/local-virtual-computer/client.js +503 -0
  13. package/dist/local-virtual-computer/client.js.map +1 -0
  14. package/dist/local-virtual-computer/detached-process.d.ts +15 -0
  15. package/dist/local-virtual-computer/detached-process.d.ts.map +1 -0
  16. package/dist/local-virtual-computer/detached-process.js +111 -0
  17. package/dist/local-virtual-computer/detached-process.js.map +1 -0
  18. package/dist/local-virtual-computer/detached-worker.d.ts +11 -0
  19. package/dist/local-virtual-computer/detached-worker.d.ts.map +1 -0
  20. package/dist/local-virtual-computer/detached-worker.js +97 -0
  21. package/dist/local-virtual-computer/detached-worker.js.map +1 -0
  22. package/dist/local-virtual-computer/dialect.d.ts +26 -0
  23. package/dist/local-virtual-computer/dialect.d.ts.map +1 -0
  24. package/dist/local-virtual-computer/dialect.js +61 -0
  25. package/dist/local-virtual-computer/dialect.js.map +1 -0
  26. package/dist/local-virtual-computer/engine.d.ts +63 -0
  27. package/dist/local-virtual-computer/engine.d.ts.map +1 -0
  28. package/dist/local-virtual-computer/engine.js +125 -0
  29. package/dist/local-virtual-computer/engine.js.map +1 -0
  30. package/dist/local-virtual-computer/guest-stdio.d.ts +15 -0
  31. package/dist/local-virtual-computer/guest-stdio.d.ts.map +1 -0
  32. package/dist/local-virtual-computer/guest-stdio.js +330 -0
  33. package/dist/local-virtual-computer/guest-stdio.js.map +1 -0
  34. package/dist/local-virtual-computer/index.d.ts +36 -0
  35. package/dist/local-virtual-computer/index.d.ts.map +1 -0
  36. package/dist/local-virtual-computer/index.js +341 -0
  37. package/dist/local-virtual-computer/index.js.map +1 -0
  38. package/dist/local-virtual-computer/podman-machine.d.ts +17 -0
  39. package/dist/local-virtual-computer/podman-machine.d.ts.map +1 -0
  40. package/dist/local-virtual-computer/podman-machine.js +181 -0
  41. package/dist/local-virtual-computer/podman-machine.js.map +1 -0
  42. package/dist/local-virtual-computer/setup/contract.d.ts +360 -0
  43. package/dist/local-virtual-computer/setup/contract.d.ts.map +1 -0
  44. package/dist/local-virtual-computer/setup/contract.js +3 -0
  45. package/dist/local-virtual-computer/setup/contract.js.map +1 -0
  46. package/dist/local-virtual-computer/setup/elevated.d.ts +78 -0
  47. package/dist/local-virtual-computer/setup/elevated.d.ts.map +1 -0
  48. package/dist/local-virtual-computer/setup/elevated.js +548 -0
  49. package/dist/local-virtual-computer/setup/elevated.js.map +1 -0
  50. package/dist/local-virtual-computer/setup/host.d.ts +12 -0
  51. package/dist/local-virtual-computer/setup/host.d.ts.map +1 -0
  52. package/dist/local-virtual-computer/setup/host.js +93 -0
  53. package/dist/local-virtual-computer/setup/host.js.map +1 -0
  54. package/dist/local-virtual-computer/setup/idle.d.ts +18 -0
  55. package/dist/local-virtual-computer/setup/idle.d.ts.map +1 -0
  56. package/dist/local-virtual-computer/setup/idle.js +38 -0
  57. package/dist/local-virtual-computer/setup/idle.js.map +1 -0
  58. package/dist/local-virtual-computer/setup/image.d.ts +7 -0
  59. package/dist/local-virtual-computer/setup/image.d.ts.map +1 -0
  60. package/dist/local-virtual-computer/setup/image.js +124 -0
  61. package/dist/local-virtual-computer/setup/image.js.map +1 -0
  62. package/dist/local-virtual-computer/setup/index.d.ts +17 -0
  63. package/dist/local-virtual-computer/setup/index.d.ts.map +1 -0
  64. package/dist/local-virtual-computer/setup/index.js +17 -0
  65. package/dist/local-virtual-computer/setup/index.js.map +1 -0
  66. package/dist/local-virtual-computer/setup/limits.d.ts +30 -0
  67. package/dist/local-virtual-computer/setup/limits.d.ts.map +1 -0
  68. package/dist/local-virtual-computer/setup/limits.js +58 -0
  69. package/dist/local-virtual-computer/setup/limits.js.map +1 -0
  70. package/dist/local-virtual-computer/setup/plan.d.ts +8 -0
  71. package/dist/local-virtual-computer/setup/plan.d.ts.map +1 -0
  72. package/dist/local-virtual-computer/setup/plan.js +131 -0
  73. package/dist/local-virtual-computer/setup/plan.js.map +1 -0
  74. package/dist/local-virtual-computer/setup/readiness.d.ts +11 -0
  75. package/dist/local-virtual-computer/setup/readiness.d.ts.map +1 -0
  76. package/dist/local-virtual-computer/setup/readiness.js +320 -0
  77. package/dist/local-virtual-computer/setup/readiness.js.map +1 -0
  78. package/dist/local-virtual-computer/setup/registry.d.ts +18 -0
  79. package/dist/local-virtual-computer/setup/registry.d.ts.map +1 -0
  80. package/dist/local-virtual-computer/setup/registry.js +50 -0
  81. package/dist/local-virtual-computer/setup/registry.js.map +1 -0
  82. package/dist/local-virtual-computer/setup/restart.d.ts +8 -0
  83. package/dist/local-virtual-computer/setup/restart.d.ts.map +1 -0
  84. package/dist/local-virtual-computer/setup/restart.js +60 -0
  85. package/dist/local-virtual-computer/setup/restart.js.map +1 -0
  86. package/dist/local-virtual-computer/setup/resume.d.ts +14 -0
  87. package/dist/local-virtual-computer/setup/resume.d.ts.map +1 -0
  88. package/dist/local-virtual-computer/setup/resume.js +58 -0
  89. package/dist/local-virtual-computer/setup/resume.js.map +1 -0
  90. package/dist/local-virtual-computer/setup/run.d.ts +8 -0
  91. package/dist/local-virtual-computer/setup/run.d.ts.map +1 -0
  92. package/dist/local-virtual-computer/setup/run.js +200 -0
  93. package/dist/local-virtual-computer/setup/run.js.map +1 -0
  94. package/dist/local-virtual-computer/setup/windows-folder.d.ts +11 -0
  95. package/dist/local-virtual-computer/setup/windows-folder.d.ts.map +1 -0
  96. package/dist/local-virtual-computer/setup/windows-folder.js +14 -0
  97. package/dist/local-virtual-computer/setup/windows-folder.js.map +1 -0
  98. package/dist/local-virtual-computer/setup/wsl-package.d.ts +19 -0
  99. package/dist/local-virtual-computer/setup/wsl-package.d.ts.map +1 -0
  100. package/dist/local-virtual-computer/setup/wsl-package.js +242 -0
  101. package/dist/local-virtual-computer/setup/wsl-package.js.map +1 -0
  102. package/dist/local-virtual-computer/setup/wslc-shared.d.ts +27 -0
  103. package/dist/local-virtual-computer/setup/wslc-shared.d.ts.map +1 -0
  104. package/dist/local-virtual-computer/setup/wslc-shared.js +137 -0
  105. package/dist/local-virtual-computer/setup/wslc-shared.js.map +1 -0
  106. package/dist/local-virtual-computer/ssh-forward.d.ts +19 -0
  107. package/dist/local-virtual-computer/ssh-forward.d.ts.map +1 -0
  108. package/dist/local-virtual-computer/ssh-forward.js +179 -0
  109. package/dist/local-virtual-computer/ssh-forward.js.map +1 -0
  110. package/dist/local-virtual-computer/wslc-engine.d.ts +27 -0
  111. package/dist/local-virtual-computer/wslc-engine.d.ts.map +1 -0
  112. package/dist/local-virtual-computer/wslc-engine.js +98 -0
  113. package/dist/local-virtual-computer/wslc-engine.js.map +1 -0
  114. package/local-computer/Dockerfile +99 -0
  115. package/local-computer/app-catalog.cjs +240 -0
  116. package/local-computer/app-native-host.cjs +144 -0
  117. package/local-computer/browser-home.cjs +249 -0
  118. package/local-computer/build-image.mjs +310 -0
  119. package/local-computer/desktop-worker.cjs +477 -0
  120. package/local-computer/entrypoint.sh +161 -0
  121. package/local-computer/home.html +28 -0
  122. package/local-computer/new-tab/home.css +255 -0
  123. package/local-computer/new-tab/home.js +65 -0
  124. package/local-computer/new-tab/manifest.json +12 -0
  125. package/local-computer/new-tab/wordmark.svg +33 -0
  126. package/local-computer/openbox-rc.xml +41 -0
  127. package/local-computer/prepare-home.cjs +121 -0
  128. package/local-computer/tint2rc +44 -0
  129. package/package.json +13 -4
  130. package/src/backends/http-worker-client.ts +31 -3
  131. package/src/index.ts +13 -0
  132. package/src/local-virtual-computer/client.ts +573 -0
  133. package/src/local-virtual-computer/detached-process.ts +133 -0
  134. package/src/local-virtual-computer/detached-worker.ts +124 -0
  135. package/src/local-virtual-computer/dialect.ts +83 -0
  136. package/src/local-virtual-computer/engine.ts +197 -0
  137. package/src/local-virtual-computer/guest-stdio.ts +332 -0
  138. package/src/local-virtual-computer/index.ts +447 -0
  139. package/src/local-virtual-computer/podman-machine.ts +264 -0
  140. package/src/local-virtual-computer/setup/contract.ts +395 -0
  141. package/src/local-virtual-computer/setup/elevated.ts +655 -0
  142. package/src/local-virtual-computer/setup/host.ts +100 -0
  143. package/src/local-virtual-computer/setup/idle.ts +39 -0
  144. package/src/local-virtual-computer/setup/image.ts +159 -0
  145. package/src/local-virtual-computer/setup/index.ts +16 -0
  146. package/src/local-virtual-computer/setup/limits.ts +68 -0
  147. package/src/local-virtual-computer/setup/plan.ts +144 -0
  148. package/src/local-virtual-computer/setup/readiness.ts +386 -0
  149. package/src/local-virtual-computer/setup/registry.ts +50 -0
  150. package/src/local-virtual-computer/setup/restart.ts +64 -0
  151. package/src/local-virtual-computer/setup/resume.ts +65 -0
  152. package/src/local-virtual-computer/setup/run.ts +225 -0
  153. package/src/local-virtual-computer/setup/windows-folder.ts +13 -0
  154. package/src/local-virtual-computer/setup/wsl-package.ts +242 -0
  155. package/src/local-virtual-computer/setup/wslc-shared.ts +147 -0
  156. package/src/local-virtual-computer/ssh-forward.ts +195 -0
  157. package/src/local-virtual-computer/wslc-engine.ts +124 -0
  158. package/worker/server.js +1499 -0
@@ -0,0 +1,264 @@
1
+ import { lstat, realpath } from 'node:fs/promises'
2
+ import { createConnection } from 'node:net'
3
+ import { win32 } from 'node:path'
4
+ import {
5
+ type LocalComputerCommandRunner,
6
+ LocalComputerSetupError,
7
+ type LocalEngine,
8
+ nodeCommandRunner,
9
+ } from './engine.js'
10
+ import { startLocalSshForward } from './ssh-forward.js'
11
+
12
+ interface PodmanMachineOptions {
13
+ readonly podmanBinary?: string
14
+ readonly podmanMachine?: string
15
+ readonly podmanConnection?: string
16
+ readonly engineEndpoint?: string
17
+ readonly runner?: LocalComputerCommandRunner
18
+ }
19
+
20
+ async function confirmLocalPipe(path: string, signal?: AbortSignal): Promise<void> {
21
+ signal?.throwIfAborted()
22
+ await new Promise<void>((resolve, reject) => {
23
+ const socket = createConnection(path)
24
+ const abort = () => {
25
+ socket.destroy()
26
+ reject(signal?.reason)
27
+ }
28
+ const finish = (error?: Error) => {
29
+ signal?.removeEventListener('abort', abort)
30
+ socket.destroy()
31
+ if (error)
32
+ reject(
33
+ new LocalComputerSetupError(
34
+ 'The selected local Podman machine pipe is unavailable; start that machine explicitly',
35
+ ),
36
+ )
37
+ else resolve()
38
+ }
39
+ socket.once('connect', () => finish())
40
+ socket.once('error', finish)
41
+ signal?.addEventListener('abort', abort, { once: true })
42
+ if (signal?.aborted) abort()
43
+ })
44
+ }
45
+
46
+ function insideProfile(path: string, profile: string): boolean {
47
+ const relative = win32.relative(profile, path)
48
+ return !win32.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..\\')
49
+ }
50
+
51
+ /**
52
+ * Admit only a registered Windows WSL Podman machine. Its SSH connection is a
53
+ * local machine transport, not permission to select an arbitrary remote daemon.
54
+ * Capture its verified URL and identity so later named-connection edits cannot
55
+ * redirect an allocation. Never start, initialize or configure the machine.
56
+ */
57
+ export async function resolveLocalPodmanEngine(
58
+ options: PodmanMachineOptions,
59
+ signal?: AbortSignal,
60
+ platform: NodeJS.Platform = process.platform,
61
+ ): Promise<LocalEngine> {
62
+ if (platform !== 'win32')
63
+ throw new LocalComputerSetupError(
64
+ 'The Podman computer adapter requires a locally registered Windows WSL machine',
65
+ )
66
+ const machineName = options.podmanMachine
67
+ const connectionName = options.podmanConnection
68
+ if (
69
+ !machineName ||
70
+ !connectionName ||
71
+ !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(machineName) ||
72
+ !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(connectionName)
73
+ )
74
+ throw new LocalComputerSetupError(
75
+ 'Select an existing Podman machine and its registered connection explicitly',
76
+ )
77
+ if (options.engineEndpoint)
78
+ throw new LocalComputerSetupError(
79
+ 'Podman machine transport is verified from local registration; an arbitrary engine endpoint is refused',
80
+ )
81
+ const selectedBinary = options.podmanBinary ?? 'podman'
82
+ const binary = /[\\/]|^[a-zA-Z]:/.test(selectedBinary)
83
+ ? win32.resolve(process.cwd(), selectedBinary)
84
+ : selectedBinary
85
+ // Native Podman probes /proc/self/uid_map even in remote mode. A WSL UNC
86
+ // caller directory produces ERROR_DIRECTORY rather than a missing file,
87
+ // which Podman treats as fatal. Pin this transport to a native directory
88
+ // without changing the embedding application's working directory.
89
+ const profile = process.env.USERPROFILE
90
+ if (!profile || !/^[a-zA-Z]:[\\/]/.test(profile))
91
+ throw new LocalComputerSetupError(
92
+ 'The Podman machine requires an existing native Windows operator profile directory',
93
+ )
94
+ const realProfile = await realpath(profile)
95
+ if (!/^[a-zA-Z]:[\\/]/.test(realProfile) || !(await lstat(realProfile)).isDirectory())
96
+ throw new LocalComputerSetupError(
97
+ 'The Podman machine requires an existing native Windows operator profile directory',
98
+ )
99
+ const runner = options.runner ?? nodeCommandRunner
100
+ const machines = JSON.parse(
101
+ await runner.run(binary, ['machine', 'list', '--format', 'json'], {
102
+ signal,
103
+ cwd: realProfile,
104
+ }),
105
+ ) as { Name?: string; VMType?: string }[]
106
+ if (machines.find((machine) => machine.Name === machineName)?.VMType !== 'wsl')
107
+ throw new LocalComputerSetupError(
108
+ 'The selected Podman machine must be registered locally with the Windows WSL provider',
109
+ )
110
+ const inspected = JSON.parse(
111
+ await runner.run(binary, ['machine', 'inspect', machineName, '--format', '{{json .}}'], {
112
+ signal,
113
+ cwd: realProfile,
114
+ }),
115
+ ) as {
116
+ Name?: string
117
+ State?: string
118
+ Rootful?: boolean
119
+ ConfigDir?: { Path?: string }
120
+ SSHConfig?: { IdentityPath?: string; Port?: number; RemoteUsername?: string }
121
+ ConnectionInfo?: { PodmanPipe?: { Path?: string } }
122
+ }
123
+ if (inspected.Name !== machineName || inspected.State !== 'running')
124
+ throw new LocalComputerSetupError(
125
+ 'The selected Podman machine is stopped or unavailable; start it explicitly',
126
+ )
127
+ const registrations = await runner.run(
128
+ win32.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'wsl.exe'),
129
+ ['--list', '--quiet'],
130
+ { signal, cwd: realProfile },
131
+ )
132
+ if (
133
+ !registrations
134
+ .split(/\r?\n/)
135
+ .some((name) => name.trim().toLowerCase() === machineName.toLowerCase())
136
+ )
137
+ throw new LocalComputerSetupError(
138
+ 'The selected Podman machine is not a registered local WSL distribution',
139
+ )
140
+ const pipe = inspected.ConnectionInfo?.PodmanPipe?.Path
141
+ if (pipe !== `\\\\.\\pipe\\${machineName}`)
142
+ throw new LocalComputerSetupError(
143
+ 'The Podman machine must own its matching local Windows named pipe',
144
+ )
145
+ const identity = inspected.SSHConfig?.IdentityPath
146
+ const configDir = inspected.ConfigDir?.Path
147
+ if (!identity || !configDir || !win32.isAbsolute(identity) || !win32.isAbsolute(configDir))
148
+ throw new LocalComputerSetupError(
149
+ 'The Podman machine must belong to the current Windows operator profile',
150
+ )
151
+ const [realIdentity, realConfig] = await Promise.all([
152
+ realpath(win32.normalize(identity)),
153
+ realpath(win32.normalize(configDir)),
154
+ ])
155
+ if (
156
+ !insideProfile(realIdentity, realProfile) ||
157
+ !insideProfile(realConfig, realProfile) ||
158
+ !(await lstat(realIdentity)).isFile()
159
+ )
160
+ throw new LocalComputerSetupError(
161
+ 'The Podman machine identity and configuration must remain inside the current Windows operator profile',
162
+ )
163
+ const connections = JSON.parse(
164
+ await runner.run(binary, ['system', 'connection', 'list', '--format', 'json'], {
165
+ signal,
166
+ cwd: realProfile,
167
+ }),
168
+ ) as {
169
+ Name?: string
170
+ URI?: string
171
+ Identity?: string
172
+ IsMachine?: boolean
173
+ ReadWrite?: boolean
174
+ }[]
175
+ const connection = connections.find((candidate) => candidate.Name === connectionName)
176
+ let url: URL
177
+ try {
178
+ url = new URL(connection?.URI ?? '')
179
+ } catch {
180
+ throw new LocalComputerSetupError('The selected Podman machine connection is invalid')
181
+ }
182
+ const expectedUser = inspected.Rootful ? 'root' : inspected.SSHConfig?.RemoteUsername
183
+ const port = inspected.SSHConfig?.Port
184
+ if (
185
+ !connection?.IsMachine ||
186
+ connection.ReadWrite !== true ||
187
+ !connection.Identity ||
188
+ win32.normalize(connection.Identity).toLowerCase() !==
189
+ win32.normalize(realIdentity).toLowerCase() ||
190
+ !Number.isSafeInteger(port) ||
191
+ Number(port) < 1 ||
192
+ Number(port) > 65535 ||
193
+ url.protocol !== 'ssh:' ||
194
+ url.hostname !== '127.0.0.1' ||
195
+ Number(url.port) !== port ||
196
+ !expectedUser ||
197
+ url.username !== expectedUser ||
198
+ url.password ||
199
+ url.search ||
200
+ url.hash ||
201
+ (inspected.Rootful
202
+ ? url.pathname !== '/run/podman/podman.sock'
203
+ : !/^\/run\/user\/\d+\/podman\/podman\.sock$/.test(url.pathname))
204
+ )
205
+ throw new LocalComputerSetupError(
206
+ 'The Podman connection must match the selected local WSL machine identity, port, user and socket; remote connections are refused',
207
+ )
208
+ await confirmLocalPipe(pipe, signal)
209
+ const endpoint = `npipe:////./pipe/${machineName}`
210
+ const run: LocalEngine['run'] = (args, call) => {
211
+ const env = { ...process.env, ...call?.env }
212
+ for (const name of [
213
+ 'CONTAINER_HOST',
214
+ 'CONTAINER_CONNECTION',
215
+ 'CONTAINER_SSHKEY',
216
+ 'DOCKER_HOST',
217
+ 'DOCKER_CONTEXT',
218
+ ])
219
+ env[name] = undefined
220
+ return runner.run(binary, ['--url', url.href, '--identity', realIdentity, ...args], {
221
+ ...call,
222
+ env,
223
+ cwd: realProfile,
224
+ })
225
+ }
226
+ const info = JSON.parse(await run(['info', '--format', 'json'], { signal })) as {
227
+ host?: { os?: string }
228
+ }
229
+ if (info.host?.os !== 'linux')
230
+ throw new LocalComputerSetupError('The selected local Podman machine must run Linux containers')
231
+ return {
232
+ endpoint,
233
+ run,
234
+ async forwardPorts(ports, call) {
235
+ const key = (
236
+ await runner.run(
237
+ win32.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'wsl.exe'),
238
+ [
239
+ '--distribution',
240
+ machineName,
241
+ '--user',
242
+ 'root',
243
+ '--exec',
244
+ 'cat',
245
+ '/etc/ssh/ssh_host_ed25519_key.pub',
246
+ ],
247
+ { signal: call.signal, cwd: realProfile },
248
+ )
249
+ )
250
+ .trim()
251
+ .split(/\s+/)
252
+ .slice(0, 2)
253
+ .join(' ')
254
+ return await startLocalSshForward({
255
+ identity: realIdentity,
256
+ port: Number(port),
257
+ user: expectedUser,
258
+ hostPublicKey: key,
259
+ ports,
260
+ ...call,
261
+ })
262
+ },
263
+ }
264
+ }
@@ -0,0 +1,395 @@
1
+ /**
2
+ * The contract of the Windows Pal computer setup: what a readiness inspection
3
+ * reports, the plan a person consents to, and how running that plan reports
4
+ * progress and ends. The provider never installs, updates, loads or starts
5
+ * anything; everything here is a separate operation an application runs after
6
+ * the person agrees.
7
+ */
8
+ import type { LocalComputerCommandRunner } from '../engine.js'
9
+
10
+ /**
11
+ * One state per machine, checked in this order: the first that applies wins.
12
+ * - `unsupported`: not Windows, not x64, or a Windows build below 19041.
13
+ * - `blocked-by-policy`: `AllowWSL` or `AllowWSLContainer` is 0 under
14
+ * `HKLM\Software\Policies\WSL`.
15
+ * - `restart-pending`: setup recorded that Windows needs a restart, and the PC
16
+ * has not restarted since; or Virtual Machine Platform is on, no hypervisor
17
+ * runs yet, and Windows itself reports a restart pending (no marker then).
18
+ * A marker that cannot be read, or a boot time that cannot be fetched while
19
+ * a marker exists, also reads `restart-pending`: the steps still to run
20
+ * after the restart are then unknown and treated as none. So does a marker
21
+ * that can be neither read nor dated (no file date): `restart` is then
22
+ * absent, since there is no record time to report.
23
+ * - `platform-off`: Virtual Machine Platform is off, or (Windows 10 only) the
24
+ * Windows Subsystem for Linux optional component is.
25
+ * - `wsl-missing`: no WSL package with wslc.
26
+ * - `wsl-outdated`: WSL is present, but wslc is missing or below the floor.
27
+ * - `sdk-mismatch`: the installed WSL refuses this client (`0x8004060B`).
28
+ * - `virtualization-off`: the session's virtual machine cannot start
29
+ * (`0x80370102`), or (without a session probe) no hypervisor runs although
30
+ * the platform is on and Windows reports no restart pending.
31
+ * - `image-missing`: wslc works, but the Pal computer image is not loaded.
32
+ * - `ready`: wslc works and the image is loaded and matches its manifest.
33
+ */
34
+ export type WslcReadinessState =
35
+ | 'unsupported'
36
+ | 'blocked-by-policy'
37
+ | 'restart-pending'
38
+ | 'platform-off'
39
+ | 'wsl-missing'
40
+ | 'wsl-outdated'
41
+ | 'sdk-mismatch'
42
+ | 'virtualization-off'
43
+ | 'image-missing'
44
+ | 'ready'
45
+
46
+ /** Why a machine is `unsupported`. */
47
+ export type WslcUnsupportedReason = 'platform' | 'architecture' | 'windows-build'
48
+
49
+ /** What a readiness inspection found. Plain data; safe to print as JSON. */
50
+ export interface WslcReadiness {
51
+ readonly state: WslcReadinessState
52
+ /** Set only when `state` is `unsupported`. */
53
+ readonly unsupportedReason?: WslcUnsupportedReason
54
+ readonly windows?: {
55
+ readonly build: number
56
+ readonly architecture: 'x64' | 'arm64' | 'other'
57
+ readonly isWindows11: boolean
58
+ }
59
+ readonly policy: {
60
+ readonly allowWsl: boolean
61
+ readonly allowWslContainer: boolean
62
+ }
63
+ readonly components: {
64
+ readonly virtualMachinePlatform: boolean | 'unknown'
65
+ /** Windows 11 does not need the optional component. */
66
+ readonly wslOptionalComponent: boolean | 'unknown' | 'not-needed'
67
+ readonly hypervisorPresent: boolean | 'unknown'
68
+ }
69
+ readonly wsl: {
70
+ /** `x.y.z.w` from `wsl.exe --version`, when a WSL package answers. */
71
+ readonly version?: string
72
+ /** The wslc binary found, if any. */
73
+ readonly wslcBinary?: string
74
+ /** `x.y.z` from `wslc version`. */
75
+ readonly wslcVersion?: string
76
+ }
77
+ readonly image: {
78
+ /** The reference the inspection looked for, e.g. `namzu-local-computer:2`. */
79
+ readonly ref: string
80
+ readonly loaded: boolean
81
+ /** A manifest and archive the application carries, if it has them. */
82
+ readonly bundled?: { readonly manifestPath: string; readonly archivePath: string }
83
+ }
84
+ /** Set when setup recorded a required restart (see {@link WslcRestartMarker}). */
85
+ readonly restart?: { readonly recordedAt: string; readonly bootTime?: string }
86
+ /** ISO time of the inspection. */
87
+ readonly checkedAt: string
88
+ }
89
+
90
+ /**
91
+ * The image manifest `local-computer/build-image.mjs` writes and the Windows
92
+ * installer carries in `resources/pal-computer/manifest.json`.
93
+ */
94
+ export interface WslcImageManifest {
95
+ readonly revision: number
96
+ readonly stage: 'base' | 'creative'
97
+ /** `namzu-local-computer:<revision>`; a test tag is never bundled. */
98
+ readonly imageRef: string
99
+ readonly imageId: string
100
+ /** The image's `org.namzu.local-computer.protocol` label. */
101
+ readonly protocol: string
102
+ /** The archive's file name, beside the manifest. */
103
+ readonly file: string
104
+ /** SHA-256 of the archive as shipped. */
105
+ readonly sha256: string
106
+ /** SHA-256 of the uncompressed tar. */
107
+ readonly tarSha256: string
108
+ /** Size of the uncompressed tar. */
109
+ readonly sizeBytes: number
110
+ /** Size of the archive. */
111
+ readonly compressedBytes: number
112
+ readonly format: 'zst' | 'xz' | 'gz' | 'tar'
113
+ }
114
+
115
+ /** The WSL release setup installs when WSL is missing or too old. */
116
+ export interface PinnedWslPackage {
117
+ /** `x.y.z.w`, for example `3.0.1.0`. */
118
+ readonly version: string
119
+ readonly architecture: 'x64'
120
+ /** An `https://github.com/microsoft/WSL/releases/download/...` URL. */
121
+ readonly url: string
122
+ readonly sha256: string
123
+ readonly sizeBytes: number
124
+ }
125
+
126
+ /**
127
+ * One step of a plan, in the order it runs.
128
+ * - `download-wsl`: fetch the pinned WSL package into the setup directory and
129
+ * check its size and SHA-256. Not elevated.
130
+ * - `enable-windows-features`: turn on Virtual Machine Platform (and on
131
+ * Windows 10 the WSL optional component). Elevated.
132
+ * - `install-wsl`: install the downloaded WSL package. Elevated.
133
+ * - `restart`: the person restarts the PC; setup continues after sign-in.
134
+ * - `load-image`: check the bundled archive against its manifest and load it
135
+ * with `wslc load -i`. Not elevated.
136
+ *
137
+ * Every elevated step of one plan runs in ONE elevated process, so a plan
138
+ * causes at most one Windows permission prompt.
139
+ */
140
+ export type WslcSetupStepKind =
141
+ | 'download-wsl'
142
+ | 'enable-windows-features'
143
+ | 'install-wsl'
144
+ | 'restart'
145
+ | 'load-image'
146
+
147
+ export interface WslcSetupStep {
148
+ readonly kind: WslcSetupStepKind
149
+ readonly elevated: boolean
150
+ /** Bytes this step downloads, if any. */
151
+ readonly downloadBytes?: number
152
+ /** Bytes this step adds on disk, if known. */
153
+ readonly diskBytes?: number
154
+ /** A rough duration for the sentence the person reads. */
155
+ readonly estimatedSeconds: number
156
+ readonly mayRequireRestart: boolean
157
+ }
158
+
159
+ /** Why a plan has nothing Namzu can do. */
160
+ export type WslcSetupBlock =
161
+ | 'unsupported'
162
+ | 'blocked-by-policy'
163
+ | 'virtualization-off'
164
+ | 'sdk-mismatch'
165
+ /** No bundled image and no other way to get one. */
166
+ | 'image-unavailable'
167
+
168
+ export interface WslcSetupPlan {
169
+ /**
170
+ * A digest of the readiness state and the steps. Consent names it, and
171
+ * running refuses a plan whose id no longer matches a fresh inspection.
172
+ */
173
+ readonly id: string
174
+ readonly state: WslcReadinessState
175
+ readonly steps: readonly WslcSetupStep[]
176
+ /** 0 or 1: all elevated steps share one elevated process. */
177
+ readonly elevationPrompts: 0 | 1
178
+ readonly mayRequireRestart: boolean
179
+ /** Set when setup cannot proceed; `steps` is then empty. */
180
+ readonly blocked?: WslcSetupBlock
181
+ }
182
+
183
+ /** The person's agreement to one specific plan. */
184
+ export interface WslcSetupConsent {
185
+ readonly planId: string
186
+ /** ISO time of the click or the typed answer. */
187
+ readonly approvedAt: string
188
+ }
189
+
190
+ /** Progress, emitted in order. Byte counts are raw; the caller humanises them. */
191
+ export type WslcSetupProgress =
192
+ | { readonly step: WslcSetupStepKind; readonly phase: 'started' }
193
+ | {
194
+ readonly step: WslcSetupStepKind
195
+ readonly phase: 'progress'
196
+ readonly done: number
197
+ readonly total?: number
198
+ readonly unit: 'bytes' | 'percent'
199
+ }
200
+ | {
201
+ readonly step: WslcSetupStepKind
202
+ readonly phase: 'finished'
203
+ readonly result: 'ok' | 'restart-required'
204
+ }
205
+
206
+ /** Stable reasons a failed setup step carries. */
207
+ export type WslcSetupFailureCode =
208
+ | 'download-failed'
209
+ | 'download-corrupt'
210
+ | 'elevation-failed'
211
+ | 'feature-enable-failed'
212
+ | 'wsl-install-failed'
213
+ | 'image-corrupt'
214
+ | 'image-load-failed'
215
+ | 'image-mismatch'
216
+ | 'engine-failed'
217
+
218
+ /** How running a plan ended. */
219
+ export type WslcSetupResult =
220
+ | { readonly outcome: 'ready' }
221
+ | { readonly outcome: 'restart-required' }
222
+ /** The person declined the Windows permission prompt. */
223
+ | { readonly outcome: 'cancelled' }
224
+ /** The account cannot elevate and no administrator approved. */
225
+ | { readonly outcome: 'needs-administrator' }
226
+ /** The consent names a plan that no longer matches the machine. */
227
+ | { readonly outcome: 'plan-changed'; readonly plan: WslcSetupPlan }
228
+ | { readonly outcome: 'blocked'; readonly blocked: WslcSetupBlock }
229
+ | {
230
+ readonly outcome: 'failed'
231
+ readonly step: WslcSetupStepKind
232
+ readonly code: WslcSetupFailureCode
233
+ /** Plain words for a Details fold. Never engine output or paths with user names. */
234
+ readonly detail?: string
235
+ /** The Windows exit code of the failed step, when Windows gave one. */
236
+ readonly exitCode?: number
237
+ }
238
+
239
+ /**
240
+ * Written to `<stateDirectory>/restart.json` when an elevated step reports
241
+ * that Windows needs a restart; cleared once readiness sees a boot later than
242
+ * `recordedAt`.
243
+ */
244
+ export interface WslcRestartMarker {
245
+ readonly recordedAt: string
246
+ readonly planId: string
247
+ /** Steps still to run after the restart. */
248
+ readonly remaining: readonly WslcSetupStepKind[]
249
+ }
250
+
251
+ /** Exit status of the elevated process, before it is mapped to a result. */
252
+ export type WslcElevatedOutcome =
253
+ | { readonly outcome: 'ok' }
254
+ | { readonly outcome: 'restart-required' }
255
+ | { readonly outcome: 'cancelled' }
256
+ | { readonly outcome: 'needs-administrator' }
257
+ | {
258
+ readonly outcome: 'failed'
259
+ readonly step: 'enable-windows-features' | 'install-wsl'
260
+ readonly code: 'feature-enable-failed' | 'wsl-install-failed' | 'elevation-failed'
261
+ /** The Windows exit code, for a Details fold. */
262
+ readonly exitCode?: number
263
+ }
264
+
265
+ /**
266
+ * Everything the setup touches on the machine, injected so tests can stand in
267
+ * for Windows. Implementations for a real Windows host live beside the
268
+ * functions that use them.
269
+ */
270
+ export interface WslcSetupHost {
271
+ readonly platform: NodeJS.Platform
272
+ readonly arch: string
273
+ /** Windows build number, e.g. 19045; undefined off Windows. */
274
+ readonly windowsBuild: number | undefined
275
+ readonly env: NodeJS.ProcessEnv
276
+ readonly runner: LocalComputerCommandRunner
277
+ /** A DWORD from the registry; undefined when the value or key is absent. */
278
+ readDword(key: string, name: string): Promise<number | undefined>
279
+ /** Whether a registry key exists; `unknown` when the check failed or timed out. */
280
+ keyExists(key: string): Promise<boolean | 'unknown'>
281
+ /** The last boot time of Windows. */
282
+ bootTime(): Promise<Date>
283
+ now(): Date
284
+ /** `%LOCALAPPDATA%\Namzu\setup` unless the caller chooses otherwise. */
285
+ readonly stateDirectory: string
286
+ /**
287
+ * The registry key the resume command is written under. Defaults to
288
+ * {@link RESUME_AT_SIGN_IN_KEY}; a trial run on a real PC points it at a
289
+ * scratch key so nothing runs at the next sign-in.
290
+ */
291
+ readonly resumeRegistryKey: string
292
+ }
293
+
294
+ /** Where Windows keeps the command it runs once at the next sign-in. */
295
+ export const RESUME_AT_SIGN_IN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce'
296
+
297
+ /** Options every setup entry point takes. */
298
+ export interface WslcSetupOptions {
299
+ /** The image the application runs, e.g. `namzu-local-computer:2`. */
300
+ readonly imageRef: string
301
+ /** The manifest the application carries, with its archive beside it. */
302
+ readonly bundledManifestPath?: string
303
+ /** Defaults to the release pinned in this package. */
304
+ readonly wslPackage?: PinnedWslPackage
305
+ readonly host?: Partial<WslcSetupHost>
306
+ readonly signal?: AbortSignal
307
+ /**
308
+ * Start the wslc session to check the loaded image and the virtual machine
309
+ * itself. Off by default, so opening a panel never starts a virtual machine
310
+ * of about 2 GB; without it, `image.loaded` comes from the record setup
311
+ * writes after a successful load (`<stateDirectory>/image.json`).
312
+ * `runWslcSetup` always probes.
313
+ */
314
+ readonly probeSession?: boolean
315
+ }
316
+
317
+ /** Run the one elevated process for the elevated steps of a plan. */
318
+ export type RunElevatedWslcSteps = (request: {
319
+ readonly steps: readonly ('enable-windows-features' | 'install-wsl')[]
320
+ /** Required when `install-wsl` is among the steps; re-verified while elevated. */
321
+ readonly wslPackage?: { readonly path: string; readonly sha256: string }
322
+ readonly host: WslcSetupHost
323
+ readonly onProgress?: (progress: WslcSetupProgress) => void
324
+ readonly signal?: AbortSignal
325
+ }) => Promise<WslcElevatedOutcome>
326
+
327
+ /** The entry points, as the CLI and Desktop call them. */
328
+ export type InspectWslcReadiness = (options: WslcSetupOptions) => Promise<WslcReadiness>
329
+ export type PlanWslcSetup = (
330
+ readiness: WslcReadiness,
331
+ options: Pick<WslcSetupOptions, 'wslPackage'> & { readonly manifest?: WslcImageManifest },
332
+ ) => WslcSetupPlan
333
+ export type RunWslcSetup = (
334
+ consent: WslcSetupConsent,
335
+ options: WslcSetupOptions & {
336
+ readonly onProgress?: (progress: WslcSetupProgress) => void
337
+ /** Defaults to the real elevated runner; tests pass a stand-in. */
338
+ readonly runElevated?: RunElevatedWslcSteps
339
+ /** The command Windows runs once at the next sign-in to continue setup. */
340
+ readonly resumeCommand?: string
341
+ },
342
+ ) => Promise<WslcSetupResult>
343
+ /**
344
+ * Stop the wslc session's virtual machine when nothing runs in it. Answers
345
+ * `busy` and does nothing while any container runs, Namzu's or not.
346
+ */
347
+ export type StopIdleWslcSession = (
348
+ options: Pick<WslcSetupOptions, 'host' | 'signal'>,
349
+ ) => Promise<{ readonly outcome: 'stopped' | 'busy' | 'not-running' }>
350
+
351
+ /** Check the bundled archive against its manifest, load it, and check what loaded. */
352
+ export type LoadWslcImage = (options: {
353
+ readonly manifestPath: string
354
+ readonly host?: Partial<WslcSetupHost>
355
+ readonly onProgress?: (progress: WslcSetupProgress) => void
356
+ readonly signal?: AbortSignal
357
+ }) => Promise<
358
+ | { readonly outcome: 'loaded' | 'already-loaded' }
359
+ | {
360
+ readonly outcome: 'failed'
361
+ readonly code: 'image-corrupt' | 'image-load-failed' | 'image-mismatch'
362
+ readonly detail?: string
363
+ }
364
+ >
365
+
366
+ /**
367
+ * Fetch the pinned WSL package into `directory`, resuming a partial file, and
368
+ * check its size and SHA-256. A file that fails the check is deleted.
369
+ */
370
+ export type DownloadWslPackage = (options: {
371
+ readonly pinned: PinnedWslPackage
372
+ readonly directory: string
373
+ readonly fetch?: typeof fetch
374
+ readonly onProgress?: (progress: WslcSetupProgress) => void
375
+ readonly signal?: AbortSignal
376
+ }) => Promise<
377
+ | { readonly outcome: 'ok'; readonly path: string }
378
+ | {
379
+ readonly outcome: 'failed'
380
+ readonly code: 'download-failed' | 'download-corrupt'
381
+ readonly detail?: string
382
+ }
383
+ >
384
+
385
+ /** The restart marker in `<stateDirectory>/restart.json`. */
386
+ export type ReadRestartMarker = (host: WslcSetupHost) => Promise<WslcRestartMarker | undefined>
387
+ export type WriteRestartMarker = (host: WslcSetupHost, marker: WslcRestartMarker) => Promise<void>
388
+ export type ClearRestartMarker = (host: WslcSetupHost) => Promise<void>
389
+
390
+ /** Ask Windows to run `command` once at the next sign-in, and take that back. */
391
+ export type RegisterResumeAtSignIn = (host: WslcSetupHost, command: string) => Promise<void>
392
+ export type ClearResumeAtSignIn = (host: WslcSetupHost) => Promise<void>
393
+
394
+ /** The real Windows host, with `overrides` applied. */
395
+ export type CreateWslcSetupHost = (overrides?: Partial<WslcSetupHost>) => WslcSetupHost