@nimbus-sh/worker 0.6.0 → 0.8.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 (184) hide show
  1. package/README.md +2 -2
  2. package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
  3. package/dist/cirrus-npm-cjs.generated.js +1 -1
  4. package/dist/facets/cirrus-real.d.ts.map +1 -1
  5. package/dist/facets/cirrus-real.js +6 -5
  6. package/dist/facets/compose.d.ts +148 -0
  7. package/dist/facets/compose.d.ts.map +1 -0
  8. package/dist/facets/compose.js +146 -0
  9. package/dist/facets/durable-images.d.ts +9 -5
  10. package/dist/facets/durable-images.d.ts.map +1 -1
  11. package/dist/facets/durable-images.js +17 -7
  12. package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
  13. package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
  14. package/dist/facets/esbuild-bundle-pool.js +122 -0
  15. package/dist/facets/exec-telemetry.d.ts +8 -0
  16. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  17. package/dist/facets/manager.d.ts +240 -109
  18. package/dist/facets/manager.d.ts.map +1 -1
  19. package/dist/facets/manager.js +677 -342
  20. package/dist/facets/process.d.ts +20 -16
  21. package/dist/facets/process.d.ts.map +1 -1
  22. package/dist/facets/process.js +59 -28
  23. package/dist/facets/real-vite-fs-shim.js +2 -2
  24. package/dist/facets/real-vite-hmr.d.ts +3 -1
  25. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  26. package/dist/facets/real-vite-hmr.js +18 -8
  27. package/dist/facets/vite-dev-server.d.ts +25 -47
  28. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  29. package/dist/facets/vite-dev-server.js +185 -219
  30. package/dist/facets/wasm-image-digest.d.ts +25 -0
  31. package/dist/facets/wasm-image-digest.d.ts.map +1 -0
  32. package/dist/facets/wasm-image-digest.js +25 -0
  33. package/dist/facets/wasm-swap-registry.d.ts +51 -92
  34. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  35. package/dist/facets/wasm-swap-registry.js +68 -227
  36. package/dist/facets/ws-terminal.d.ts +9 -31
  37. package/dist/facets/ws-terminal.d.ts.map +1 -1
  38. package/dist/facets/ws-terminal.js +58 -51
  39. package/dist/git/network-facet.d.ts.map +1 -1
  40. package/dist/git/network-facet.js +6 -5
  41. package/dist/hosted/commands.d.ts +12 -0
  42. package/dist/hosted/commands.d.ts.map +1 -0
  43. package/dist/hosted/commands.js +1658 -0
  44. package/dist/hosted/runtime.d.ts +114 -0
  45. package/dist/hosted/runtime.d.ts.map +1 -0
  46. package/dist/hosted/runtime.js +288 -0
  47. package/dist/hosted/services.d.ts +40 -0
  48. package/dist/hosted/services.d.ts.map +1 -0
  49. package/dist/hosted/services.js +512 -0
  50. package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
  51. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  52. package/dist/loaders/child-process/spawn-pool.js +3 -2
  53. package/dist/loaders/generated-workers.d.ts +2 -2
  54. package/dist/loaders/generated-workers.d.ts.map +1 -1
  55. package/dist/loaders/generated-workers.js +3 -3
  56. package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
  57. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  58. package/dist/loaders/npm-resolve-preamble.js +34 -108
  59. package/dist/node-shims-artifact.generated.js +3 -3
  60. package/dist/npm/install-batch-facet.d.ts +3 -2
  61. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  62. package/dist/npm/install-batch-facet.js +45 -15
  63. package/dist/npm/installer.d.ts +22 -17
  64. package/dist/npm/installer.d.ts.map +1 -1
  65. package/dist/npm/installer.js +209 -193
  66. package/dist/npm/pre-bundle-facet.d.ts +1 -1
  67. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  68. package/dist/npm/pre-bundle-facet.js +1 -1
  69. package/dist/npm/resolve-facet.d.ts +6 -0
  70. package/dist/npm/resolve-facet.d.ts.map +1 -1
  71. package/dist/npm/resolve-one-facet.d.ts +21 -20
  72. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  73. package/dist/npm/resolve-one-facet.js +85 -84
  74. package/dist/npm/semver.d.ts +59 -0
  75. package/dist/npm/semver.d.ts.map +1 -0
  76. package/dist/npm/semver.js +226 -0
  77. package/dist/router/remote-api.js +31 -1
  78. package/dist/runtime/bash-repl.d.ts +1 -1
  79. package/dist/runtime/bash-repl.d.ts.map +1 -1
  80. package/dist/runtime/bash-repl.js +38 -4
  81. package/dist/runtime/bun-runner.js +1 -1
  82. package/dist/runtime/html-entrypoint.d.ts +7 -0
  83. package/dist/runtime/html-entrypoint.d.ts.map +1 -1
  84. package/dist/runtime/html-entrypoint.js +18 -2
  85. package/dist/runtime/node-runner.d.ts +4 -1
  86. package/dist/runtime/node-runner.d.ts.map +1 -1
  87. package/dist/runtime/node-runner.js +9 -2
  88. package/dist/runtime/node-shims.d.ts.map +1 -1
  89. package/dist/runtime/node-shims.js +305 -29
  90. package/dist/runtime/opencode-facet-runner.js +16 -16
  91. package/dist/runtime/package-manager.d.ts +35 -122
  92. package/dist/runtime/package-manager.d.ts.map +1 -1
  93. package/dist/runtime/package-manager.js +61 -596
  94. package/dist/runtime/process-logs-api.d.ts +1 -1
  95. package/dist/runtime/process-logs-api.d.ts.map +1 -1
  96. package/dist/runtime/process-logs-api.js +1 -1
  97. package/dist/runtime/python-repl.d.ts +2 -1
  98. package/dist/runtime/python-repl.d.ts.map +1 -1
  99. package/dist/runtime/python-repl.js +82 -59
  100. package/dist/runtime/repl-session.d.ts +9 -47
  101. package/dist/runtime/repl-session.d.ts.map +1 -1
  102. package/dist/runtime/repl-session.js +105 -105
  103. package/dist/runtime/runtime-catalog.d.ts +13 -0
  104. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  105. package/dist/runtime/runtime-catalog.js +88 -0
  106. package/dist/session/agent.d.ts +1 -21
  107. package/dist/session/agent.d.ts.map +1 -1
  108. package/dist/session/ai.d.ts +5 -1
  109. package/dist/session/ai.d.ts.map +1 -1
  110. package/dist/session/ai.js +7 -0
  111. package/dist/session/helpers.d.ts +38 -0
  112. package/dist/session/helpers.d.ts.map +1 -1
  113. package/dist/session/helpers.js +76 -0
  114. package/dist/session/hibernation.d.ts +2 -1
  115. package/dist/session/hibernation.d.ts.map +1 -1
  116. package/dist/session/hibernation.js +7 -4
  117. package/dist/session/init-phases.d.ts +1 -1
  118. package/dist/session/init-phases.d.ts.map +1 -1
  119. package/dist/session/init-phases.js +2 -12
  120. package/dist/session/init.d.ts +15 -36
  121. package/dist/session/init.d.ts.map +1 -1
  122. package/dist/session/init.js +50 -2327
  123. package/dist/session/loopback.d.ts +2 -29
  124. package/dist/session/loopback.d.ts.map +1 -1
  125. package/dist/session/loopback.js +3 -27
  126. package/dist/session/nimbus-session.d.ts +77 -34
  127. package/dist/session/nimbus-session.d.ts.map +1 -1
  128. package/dist/session/nimbus-session.js +126 -514
  129. package/dist/session/npm-install-port.d.ts +18 -0
  130. package/dist/session/npm-install-port.d.ts.map +1 -0
  131. package/dist/session/npm-install-port.js +37 -0
  132. package/dist/session/port-capability.d.ts +44 -0
  133. package/dist/session/port-capability.d.ts.map +1 -1
  134. package/dist/session/port-capability.js +110 -0
  135. package/dist/session/programmatic.d.ts +36 -43
  136. package/dist/session/programmatic.d.ts.map +1 -1
  137. package/dist/session/programmatic.js +109 -115
  138. package/dist/session/routes.d.ts +30 -17
  139. package/dist/session/routes.d.ts.map +1 -1
  140. package/dist/session/routes.js +42 -131
  141. package/dist/session/rpc.d.ts +15 -15
  142. package/dist/session/rpc.d.ts.map +1 -1
  143. package/dist/session/rpc.js +98 -49
  144. package/dist/session/serving-port.d.ts +27 -0
  145. package/dist/session/serving-port.d.ts.map +1 -0
  146. package/dist/session/serving-port.js +27 -0
  147. package/dist/session/start-real-vite.d.ts +12 -0
  148. package/dist/session/start-real-vite.d.ts.map +1 -1
  149. package/dist/session/start-real-vite.js +4 -4
  150. package/dist/session/supervisor-op.d.ts +4 -4
  151. package/dist/session/supervisor-op.d.ts.map +1 -1
  152. package/dist/session/supervisor-op.js +24 -10
  153. package/dist/session/supervisor-rpc.d.ts +7 -7
  154. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  155. package/dist/session/supervisor-rpc.js +4 -0
  156. package/dist/session/vite-command.d.ts +18 -0
  157. package/dist/session/vite-command.d.ts.map +1 -0
  158. package/dist/session/vite-command.js +609 -0
  159. package/dist/session/ws.d.ts +61 -0
  160. package/dist/session/ws.d.ts.map +1 -1
  161. package/dist/session/ws.js +122 -12
  162. package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
  163. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  164. package/dist/shell/npm-bin-entrypoints.js +12 -85
  165. package/dist/workspace-host.d.ts +6 -0
  166. package/dist/workspace-host.d.ts.map +1 -0
  167. package/dist/workspace-host.js +5 -0
  168. package/dist/workspace.d.ts +2 -0
  169. package/dist/workspace.d.ts.map +1 -0
  170. package/dist/workspace.js +1 -0
  171. package/package.json +34 -9
  172. package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
  173. package/public/index.html +1 -1
  174. package/scripts/bundle-facet-workers.mjs +1 -1
  175. package/scripts/bundle-npm-cjs.mjs +2 -2
  176. package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
  177. package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
  178. package/dist/facets/on-demand-bundle-gate.js +0 -125
  179. package/dist/npm/install-args.d.ts +0 -21
  180. package/dist/npm/install-args.d.ts.map +0 -1
  181. package/dist/npm/install-args.js +0 -70
  182. package/dist/npm/npm-log.d.ts +0 -31
  183. package/dist/npm/npm-log.d.ts.map +0 -1
  184. package/dist/npm/npm-log.js +0 -51
@@ -1,113 +1,73 @@
1
1
  /**
2
- * package-manager.ts — `nimbus install <runtime>` shell verb.
2
+ * package-manager.ts — the Worker's edge of runtime installation.
3
3
  *
4
- * Subcommands (all under `nimbus install`):
4
+ * The policy moved: installs, singleflight, bin registration and the `nimbus`
5
+ * verb itself live in `@nimbus-sh/core` (`runtime/runtime-manager.ts`,
6
+ * `runtime/nimbus-command.ts`) because a library workspace owns them too.
7
+ * What remains here is what only a Cloudflare host can answer:
5
8
  *
6
- * nimbus install <name> install latest of <name>
7
- * nimbus install <name>@<version> install specific version
8
- * nimbus install --list show installed runtimes
9
- * nimbus install --available show catalog of installables
10
- * nimbus install --reinstall <name> force refetch
11
- * nimbus uninstall <name> remove installed runtime
12
- *
13
- * Per-user VFS layout:
14
- *
15
- * ~/.nimbus/runtimes/<name>/<version>/
16
- * manifest.json
17
- * bin/<entry>
18
- * share/<name>/...
19
- * LICENSE
20
- *
21
- * Survives DO eviction (SqliteFS-backed). Boot-time rehydration reads
22
- * every `~/.nimbus/runtimes/STAR/STAR/manifest.json` and re-registers
23
- * each `entrypoints[].binName` as a shell command pointing at the
24
- * named runner factory.
25
- *
26
- * Errors throw and bubble up to the user as a single diagnostic line.
9
+ * - `runtimeCatalogSource(env)` — the R2-backed RuntimeSource (the digest
10
+ * chain stays in runtime-catalog.ts beside this file's fetchers);
11
+ * - the programmatic entry points the SDK surface calls;
12
+ * - the command-not-found hint resolver init.ts wires into exec dispatch.
27
13
  */
28
- import { fetchCatalog, fetchManifest, fetchBlob } from './runtime-catalog.js';
29
- import { parseRuntimeManifest } from '@nimbus-sh/core/runtime/runtime-manifest.js';
30
- import { CRED_KERNEL, NIMBUS_RUNTIME_ABIS, NATIVE_UNSUPPORTED_ABI } from '@nimbus-sh/core/runtime/os-contracts.js';
31
- import { installRoot, listInstalledManifestsView, rehydrateInstalledRuntimesView, runnerFactoryFor, runtimeAbiForManifest, runtimeEntrypoints, } from '@nimbus-sh/core/runtime/installed-runtimes.js';
32
- function runtimeAbiForCatalogName(name) {
33
- return NIMBUS_RUNTIME_ABIS[name] ?? NATIVE_UNSUPPORTED_ABI;
34
- }
35
- function splitRuntimeSpec(spec) {
36
- const atIdx = spec.indexOf('@');
14
+ import { fetchCatalog, fetchManifest, runtimeCatalogSource, } from './runtime-catalog.js';
15
+ import { runtimeEntrypoints } from '@nimbus-sh/core/runtime/installed-runtimes.js';
16
+ import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
17
+ import { runNimbusInstall, } from '@nimbus-sh/core/runtime/nimbus-command.js';
18
+ export { runtimeCatalogSource };
19
+ /**
20
+ * `nimbus install` without a shell: the same `runNimbusInstall` path the verb
21
+ * runs, with output captured instead of written to a terminal. Deps take the
22
+ * workspace's RuntimeManager — which already holds the source, the registry
23
+ * and the runner factories — rather than a catalog env.
24
+ */
25
+ export async function installRuntimeProgrammatic(deps, spec, opts = {}) {
26
+ const stdout = [];
27
+ const stderr = [];
28
+ let programmaticCred = CRED_KERNEL;
29
+ const ctx = {
30
+ pid: 0,
31
+ get cred() { return programmaticCred; },
32
+ setUmask: (umask) => { programmaticCred = { ...programmaticCred, umask }; },
33
+ runAs: async () => 126,
34
+ args: [],
35
+ env: {},
36
+ cwd: deps.getHome(),
37
+ stdout: { write: (s) => stdout.push(String(s)) },
38
+ stderr: { write: (s) => stderr.push(String(s)) },
39
+ };
40
+ const verbDeps = {
41
+ runtimes: deps.runtimes,
42
+ registry: deps.registry,
43
+ vfs: deps.vfs,
44
+ warmRuntime: deps.warmRuntime,
45
+ };
46
+ const args = opts.force ? ['--reinstall', spec] : [spec];
47
+ const exitCode = await runNimbusInstall(args, ctx, verbDeps);
37
48
  return {
38
- name: atIdx >= 0 ? spec.slice(0, atIdx) : spec,
39
- versionOverride: atIdx >= 0 ? spec.slice(atIdx + 1) : null,
49
+ spec,
50
+ exitCode,
51
+ stdout: stdout.join(''),
52
+ stderr: stderr.join(''),
40
53
  };
41
54
  }
42
- /**
43
- * Runtimes replaced by another implementation, keyed by the name users type.
44
- *
45
- * `python` was Pyodide (CPython on Emscripten, with its own filesystem) and is
46
- * now CPython built for wasm32-wasi. Both are in the catalog, and the
47
- * redirection lives HERE rather than in the catalog's `default` because the
48
- * catalog is shared with production: flipping it would repoint deployed Workers
49
- * that still register the old runner, and `python` would become "command not
50
- * found" for everyone until they redeployed. In code, the cutover ships with
51
- * the Worker that can serve it.
52
- *
53
- * `nimbus install python@0.29.4` still reaches Pyodide — an explicit version is
54
- * a deliberate request and is left alone.
55
- */
56
- const SUPERSEDED_RUNTIMES = { python: 'cpython' };
57
- async function resolveRuntimeInstallTarget(env, catalog, spec) {
58
- const parsed = splitRuntimeSpec(spec);
59
- const superseding = parsed.versionOverride === null ? SUPERSEDED_RUNTIMES[parsed.name] : undefined;
60
- if (superseding && catalog.runtimes[superseding]) {
61
- return {
62
- runtimeName: superseding,
63
- versionOverride: null,
64
- // The name the user typed, so the installer's output still says `python`.
65
- requestedName: parsed.name,
66
- };
67
- }
68
- if (catalog.runtimes[parsed.name]) {
69
- return {
70
- runtimeName: parsed.name,
71
- versionOverride: parsed.versionOverride,
72
- requestedName: parsed.name,
73
- };
74
- }
75
- // Command-name aliasing is catalog-driven: any command a runtime
76
- // provides (manifest entrypoints + RUNTIME_EXTRA_ENTRYPOINTS) resolves
77
- // to that runtime, so `nimbus install python3|pip|gem|wasm-ld` all
78
- // work without a hand-maintained alias map.
79
- for (const [runtimeName, entry] of Object.entries(catalog.runtimes)) {
80
- // A superseded runtime does not answer for a command name either. It still
81
- // declares `python` and `python3`, it comes first in the catalog, and its
82
- // runner is no longer registered — so `nimbus install python3` installed
83
- // Pyodide and left two bins nothing could invoke, while `nimbus install
84
- // python` next to it installed CPython. The successor declares the same
85
- // commands and is reached further down this same loop.
86
- if (parsed.versionOverride === null
87
- && SUPERSEDED_RUNTIMES[runtimeName]
88
- && catalog.runtimes[SUPERSEDED_RUNTIMES[runtimeName]])
89
- continue;
90
- const version = entry.default;
91
- const versionEntry = entry.versions[version];
92
- if (!versionEntry)
93
- continue;
94
- try {
95
- const manifest = await fetchManifest(env, versionEntry);
96
- if (runtimeEntrypoints(manifest).some((ep) => ep.binName === parsed.name)) {
97
- return {
98
- runtimeName,
99
- versionOverride: parsed.versionOverride,
100
- requestedName: parsed.name,
101
- };
102
- }
103
- }
104
- catch {
105
- // A bad manifest should not prevent canonical catalog names from
106
- // resolving; it only suppresses bin-name aliasing for that runtime.
107
- }
55
+ export async function ensureRuntimesProgrammatic(deps, specs, opts = {}) {
56
+ const results = [];
57
+ for (const spec of specs) {
58
+ results.push(await installRuntimeProgrammatic(deps, spec, opts));
108
59
  }
109
- return null;
60
+ return results;
61
+ }
62
+ export async function listAvailableRuntimes(env) {
63
+ return runtimeCatalogSource(env).list();
110
64
  }
65
+ /**
66
+ * Command-not-found hints, catalog-driven: a bare name the shell could not
67
+ * resolve is answered with the runtime that provides it, so `python3` hints
68
+ * at cpython and `wasm-ld` at clang. Loaded once and memoized; a fetch
69
+ * failure drops the memo so the next unknown command retries.
70
+ */
111
71
  export function createRuntimeCommandHintResolver(env) {
112
72
  let hintsPromise = null;
113
73
  const loadHints = async () => {
@@ -152,498 +112,3 @@ export function createRuntimeCommandHintResolver(env) {
152
112
  return hints.get(command) ?? null;
153
113
  };
154
114
  }
155
- export async function listAvailableRuntimes(env) {
156
- const catalog = await fetchCatalog(env);
157
- return Object.entries(catalog.runtimes)
158
- .sort(([a], [b]) => a.localeCompare(b))
159
- .map(([name, entry]) => ({
160
- name,
161
- abi: runtimeAbiForCatalogName(name),
162
- defaultVersion: entry.default,
163
- versions: Object.entries(entry.versions)
164
- .sort(([a], [b]) => a.localeCompare(b))
165
- .map(([version, v]) => ({
166
- version,
167
- sizeBytes: v.size_bytes,
168
- license: v.license,
169
- })),
170
- }));
171
- }
172
- export async function installRuntimeProgrammatic(deps, spec, opts = {}) {
173
- const stdout = [];
174
- const stderr = [];
175
- let programmaticCred = CRED_KERNEL;
176
- const ctx = {
177
- pid: 0,
178
- get cred() { return programmaticCred; },
179
- setUmask: (umask) => { programmaticCred = { ...programmaticCred, umask }; },
180
- runAs: async () => 126,
181
- args: [],
182
- env: {},
183
- cwd: deps.getHome(),
184
- stdout: { write: (s) => stdout.push(String(s)) },
185
- stderr: { write: (s) => stderr.push(String(s)) },
186
- };
187
- const args = opts.force ? ['--reinstall', spec] : [spec];
188
- const exitCode = await runInstall(args, ctx, { ...deps, vfs: deps.vfs.as(CRED_KERNEL) });
189
- return {
190
- spec,
191
- exitCode,
192
- stdout: stdout.join(''),
193
- stderr: stderr.join(''),
194
- };
195
- }
196
- export async function ensureRuntimesProgrammatic(deps, specs, opts = {}) {
197
- const results = [];
198
- for (const spec of specs) {
199
- results.push(await installRuntimeProgrammatic(deps, spec, opts));
200
- }
201
- return results;
202
- }
203
- const NIMBUS_USAGE = [
204
- 'usage: nimbus install <name>[@<version>] | nimbus install --list | nimbus install --available | nimbus uninstall <name>',
205
- ' nimbus expose <port|pid> [--public] [--name <name>]',
206
- ' nimbus app list | url <name|port> | rotate <name|port> | remove <name|port>',
207
- ' nimbus start [--restart never|on-failure] <command> [args...]',
208
- ].join('\n');
209
- /**
210
- * Build the shell-command handler that implements `nimbus install …`,
211
- * `nimbus uninstall …`, `nimbus expose …`, `nimbus app …` and
212
- * `nimbus start …`. Registered under the name `nimbus`.
213
- */
214
- export function makeNimbusVerbHandler(deps) {
215
- const { env, registry, getHome, warmRuntime, apps } = deps;
216
- const vfs = deps.vfs.as(CRED_KERNEL);
217
- return async function nimbus(ctx) {
218
- const argv = ctx.args || [];
219
- const verb = argv[0];
220
- const rest = argv.slice(1);
221
- if (verb === 'install') {
222
- return runInstall(rest, ctx, { env, vfs, registry, getHome, warmRuntime });
223
- }
224
- if (verb === 'uninstall') {
225
- return runUninstall(rest, ctx, { vfs, registry, getHome });
226
- }
227
- if (verb === 'expose' || verb === 'app' || verb === 'start') {
228
- if (verb === 'start')
229
- return runStart(rest, ctx, registry);
230
- if (!apps) {
231
- ctx.stderr.write(`nimbus ${verb}: this host has no session to address applications on\n`);
232
- return 1;
233
- }
234
- try {
235
- return verb === 'expose' ? await runExpose(rest, ctx, apps) : await runApp(rest, ctx, apps);
236
- }
237
- catch (e) {
238
- ctx.stderr.write(`nimbus ${verb}: ${e instanceof Error ? e.message : String(e)}\n`);
239
- return 1;
240
- }
241
- }
242
- // Unknown verb.
243
- ctx.stderr.write(`nimbus: unknown subcommand '${verb || '(none)'}'\n`);
244
- ctx.stderr.write(`${NIMBUS_USAGE}\n`);
245
- return 2;
246
- };
247
- }
248
- // ── expose / app / start ─────────────────────────────────────────────
249
- /** `<port|pid|name>` as the verbs take it: digits are a port or pid, anything else a name. */
250
- function appTargetArg(raw) {
251
- return /^\d+$/.test(raw) ? Number(raw) : raw;
252
- }
253
- async function runExpose(args, ctx, apps) {
254
- let target;
255
- let name;
256
- let visibility;
257
- for (let i = 0; i < args.length; i++) {
258
- const arg = args[i];
259
- if (arg === '--public')
260
- visibility = 'public';
261
- else if (arg === '--scoped')
262
- visibility = 'scoped';
263
- else if (arg === '--name') {
264
- name = args[++i];
265
- }
266
- else if (arg.startsWith('--name='))
267
- name = arg.slice('--name='.length);
268
- else if (arg.startsWith('--')) {
269
- ctx.stderr.write(`nimbus expose: unknown flag ${arg}\n`);
270
- return 2;
271
- }
272
- else if (target === undefined)
273
- target = arg;
274
- else {
275
- ctx.stderr.write('nimbus expose: one target only\n');
276
- return 2;
277
- }
278
- }
279
- if (target === undefined) {
280
- ctx.stderr.write('usage: nimbus expose <port|pid> [--public] [--name <name>]\n');
281
- return 2;
282
- }
283
- const exposed = await apps.expose(appTargetArg(target), {
284
- ...(visibility !== undefined ? { visibility } : {}),
285
- ...(name !== undefined ? { name } : {}),
286
- });
287
- ctx.stdout.write(`${exposed.url ?? `(no URL: port ${exposed.port}, capability ${exposed.capability ?? 'none'})`}\n`);
288
- ctx.stdout.write(` ${exposed.visibility} · ${exposed.name ?? exposed.owner} · port ${exposed.port}\n`);
289
- return 0;
290
- }
291
- async function runApp(args, ctx, apps) {
292
- const [sub, target] = args;
293
- if (sub === 'list' || sub === 'ls') {
294
- const rows = await apps.list();
295
- if (rows.length === 0) {
296
- ctx.stdout.write('(no applications)\n');
297
- return 0;
298
- }
299
- for (const app of rows) {
300
- const label = app.name ?? app.owner;
301
- const where = app.port === null ? '-' : String(app.port);
302
- const pid = app.pid === null ? '-' : String(app.pid);
303
- const status = app.diagnostic ? `${app.status} (${app.diagnostic})` : app.status;
304
- ctx.stdout.write(`${label}\t${where}\t${pid}\t${status}\t${app.visibility}\t${app.restart}\t${app.url ?? '-'}\n`);
305
- }
306
- return 0;
307
- }
308
- if (target === undefined || (sub !== 'url' && sub !== 'rotate' && sub !== 'remove')) {
309
- ctx.stderr.write('usage: nimbus app list | url <name|port> | rotate <name|port> | remove <name|port>\n');
310
- return 2;
311
- }
312
- const t = appTargetArg(target);
313
- if (sub === 'url') {
314
- const app = (await apps.list()).find((row) => row.name === t || row.owner === t || row.port === t || row.pid === t);
315
- if (!app) {
316
- ctx.stderr.write(`nimbus app url: no application matches ${target}\n`);
317
- return 1;
318
- }
319
- ctx.stdout.write(`${app.url ?? `(no URL: port ${app.port ?? '-'})`}\n`);
320
- return 0;
321
- }
322
- if (sub === 'rotate') {
323
- const rotated = await apps.rotateLink(t);
324
- ctx.stdout.write(`${rotated.url ?? `(no URL: port ${rotated.port}, capability ${rotated.capability ?? 'none'})`}\n`);
325
- return 0;
326
- }
327
- const removed = await apps.remove(t);
328
- ctx.stdout.write(removed.removed
329
- ? `removed ${removed.owner}${removed.port !== null ? ` (port ${removed.port} released)` : ''}\n`
330
- : `nothing to remove for ${removed.owner}\n`);
331
- return removed.removed ? 0 : 1;
332
- }
333
- /**
334
- * `nimbus start [--restart <policy>] <command> [args...]` — run a
335
- * registered command with the restart policy in its environment, which is
336
- * where the resident it starts reads the policy from (`NIMBUS_RESTART`),
337
- * exactly as the SDK's `startProcess({ restart })` carries it.
338
- */
339
- async function runStart(args, ctx, registry) {
340
- let restart = 'never';
341
- let i = 0;
342
- for (; i < args.length; i++) {
343
- const arg = args[i];
344
- if (arg === '--restart') {
345
- const policy = args[++i];
346
- if (policy !== 'never' && policy !== 'on-failure') {
347
- ctx.stderr.write(`nimbus start: --restart must be never or on-failure, got ${policy ?? '(none)'}\n`);
348
- return 2;
349
- }
350
- restart = policy;
351
- }
352
- else if (arg.startsWith('--restart=')) {
353
- const policy = arg.slice('--restart='.length);
354
- if (policy !== 'never' && policy !== 'on-failure') {
355
- ctx.stderr.write(`nimbus start: --restart must be never or on-failure, got ${policy}\n`);
356
- return 2;
357
- }
358
- restart = policy;
359
- }
360
- else if (arg === '--') {
361
- i += 1;
362
- break;
363
- }
364
- else if (arg.startsWith('--')) {
365
- ctx.stderr.write(`nimbus start: unknown flag ${arg}\n`);
366
- return 2;
367
- }
368
- else
369
- break;
370
- }
371
- const [command, ...commandArgs] = args.slice(i);
372
- if (!command) {
373
- ctx.stderr.write('usage: nimbus start [--restart never|on-failure] <command> [args...]\n');
374
- return 2;
375
- }
376
- const handler = registry.resolve ? await registry.resolve(command) : null;
377
- if (!handler) {
378
- ctx.stderr.write(`nimbus start: ${command}: command not found\n`);
379
- return 127;
380
- }
381
- // The shell hands `nimbus` its full CommandContext; ShellCtx is only the
382
- // slice this module reads, so the spread carries everything the command
383
- // needs (signal, vfs, stdin, ...) through untouched.
384
- const full = ctx;
385
- return handler({
386
- ...full,
387
- args: commandArgs,
388
- env: { ...full.env, NIMBUS_RESTART: restart },
389
- });
390
- }
391
- // ── install ──────────────────────────────────────────────────────────
392
- async function runInstall(args, ctx, deps) {
393
- // Flag parsing.
394
- const listOnly = args.includes('--list');
395
- const availOnly = args.includes('--available');
396
- const force = args.includes('--reinstall') || args.includes('--force');
397
- const positional = args.filter((a) => !a.startsWith('--'));
398
- if (listOnly)
399
- return runList(ctx, deps);
400
- if (availOnly)
401
- return runAvailable(ctx, deps);
402
- if (positional.length === 0) {
403
- ctx.stderr.write('nimbus install: missing runtime name\n');
404
- ctx.stderr.write('usage: nimbus install <name>[@<version>]\n');
405
- return 2;
406
- }
407
- const spec = positional[0];
408
- // Fetch catalog.
409
- let catalog;
410
- try {
411
- catalog = await fetchCatalog(deps.env);
412
- }
413
- catch (e) {
414
- ctx.stderr.write(`nimbus install: ${e?.message || e}\n`);
415
- return 1;
416
- }
417
- const target = await resolveRuntimeInstallTarget(deps.env, catalog, spec);
418
- if (!target) {
419
- const parsed = splitRuntimeSpec(spec);
420
- ctx.stderr.write(`nimbus install: '${parsed.name}' is not in catalog\n`);
421
- ctx.stderr.write(`nimbus install: try 'nimbus install --available' to see installable runtimes\n`);
422
- return 1;
423
- }
424
- const name = target.runtimeName;
425
- const runtimeEntry = catalog.runtimes[name];
426
- const version = target.versionOverride || runtimeEntry.default;
427
- const versionEntry = runtimeEntry.versions[version];
428
- if (!versionEntry) {
429
- ctx.stderr.write(`nimbus install: '${name}@${version}' not in catalog\n`);
430
- return 1;
431
- }
432
- const home = deps.getHome();
433
- const root = installRoot(home, name, version);
434
- // Idempotent: if manifest already on disk + sha-equivalent, skip.
435
- if (!force && deps.vfs.exists(`${root}/manifest.json`)) {
436
- // We could re-verify all blob sha256s, but that's expensive. The
437
- // manifest's presence implies the install completed; we trust it.
438
- ctx.stdout.write(`[${name}] already installed at ${root} (use --reinstall to refetch)\n`);
439
- // Still re-register bins in case the registry lost them — idempotent.
440
- rehydrateInstalledRuntimesView(deps.vfs, deps.registry, home, runnerFactoryFor);
441
- let manifest = null;
442
- try {
443
- manifest = parseRuntimeManifest(JSON.parse(deps.vfs.readFileString(`${root}/manifest.json`)));
444
- }
445
- catch {
446
- manifest = null;
447
- }
448
- if (manifest) {
449
- await warmRuntimeIfConfigured(ctx, deps, {
450
- name,
451
- version,
452
- root,
453
- manifest,
454
- });
455
- }
456
- return 0;
457
- }
458
- // Fetch manifest from R2.
459
- ctx.stdout.write(`[${name}] fetching manifest...\n`);
460
- let manifest;
461
- try {
462
- manifest = await fetchManifest(deps.env, versionEntry);
463
- }
464
- catch (e) {
465
- ctx.stderr.write(`nimbus install: ${e?.message || e}\n`);
466
- return 1;
467
- }
468
- // Pre-flight: budget check.
469
- const totalBytes = manifest.files.reduce((a, f) => a + f.size, 0);
470
- ctx.stdout.write(`[${name}] manifest: ${manifest.files.length} files, ${(totalBytes / 1024 / 1024).toFixed(1)} MiB\n`);
471
- // Create install root.
472
- deps.vfs.mkdir(root, { recursive: true });
473
- // Write manifest.json first (so a partial install can be retried
474
- // and the manifest is the source of truth).
475
- deps.vfs.writeFile(`${root}/manifest.json`, JSON.stringify(manifest, null, 2));
476
- // W2: bounded-parallel blob fetch (concurrency=3).
477
- //
478
- // Pre-W2 this was a serial for-loop awaiting fetchBlob one at a time.
479
- // For multi-blob bundles (clang has 5 blobs with the two largest
480
- // dominating wall-clock at ~31 MB + ~19 MB), overlap of network/L2
481
- // reads cuts cold-install wall-clock by 30-50%. Concurrency capped
482
- // at 3 to bound memory peak: worst-case clang holds 3 in-flight
483
- // blobs ≈ ~50-60 MB; well under the DO's 128 MB cap.
484
- //
485
- // mkdir-parent is hoisted OUT of the worker body so concurrent
486
- // workers don't race against vfs.mkdir for the same parent. Each
487
- // unique parent dir is created exactly once, synchronously, BEFORE
488
- // any worker starts.
489
- //
490
- // Manifest-first invariant preserved: this loop runs AFTER the
491
- // manifest.json write above. Partial-install detection unchanged.
492
- //
493
- // Progress UX: with parallel workers, lines arrive in completion
494
- // order, not start order. The final "[name] installed at … (Y MiB)"
495
- // line is still the authoritative completion signal.
496
- const FETCH_CONCURRENCY = 3;
497
- // Pre-compute + create all unique parent dirs sync. Avoids a
498
- // mkdir race between workers and is faster than per-blob exists()
499
- // checks.
500
- const uniqueParents = new Set();
501
- for (const f of manifest.files) {
502
- const target = `${root}/${f.path}`;
503
- const lastSlash = target.lastIndexOf('/');
504
- if (lastSlash > 0) {
505
- uniqueParents.add(target.slice(0, lastSlash));
506
- }
507
- }
508
- for (const parent of uniqueParents) {
509
- if (!deps.vfs.exists(parent))
510
- deps.vfs.mkdir(parent, { recursive: true });
511
- }
512
- // Hand-rolled worker pool: N workers consume indices off a shared
513
- // cursor until the queue is empty. Avoids head-of-line blocking
514
- // that chunked Promise.all suffers (a slow blob in batch i doesn't
515
- // delay blob i+N from starting).
516
- const files = manifest.files;
517
- const total = files.length;
518
- let nextIdx = 0;
519
- let completed = 0;
520
- const workers = Array.from({ length: Math.min(FETCH_CONCURRENCY, total) }, async () => {
521
- while (true) {
522
- const i = nextIdx++;
523
- if (i >= total)
524
- return;
525
- const f = files[i];
526
- const target = `${root}/${f.path}`;
527
- const bytes = await fetchBlob(deps.env, f);
528
- deps.vfs.writeFile(target, bytes);
529
- completed++;
530
- ctx.stdout.write(`[${name}] fetched ${f.path} (${(f.size / 1024 / 1024).toFixed(2)} MiB) ${completed}/${total}\n`);
531
- }
532
- });
533
- await Promise.all(workers);
534
- // Register entrypoints.
535
- for (const ep of runtimeEntrypoints(manifest)) {
536
- const factory = runnerFactoryFor(ep.runner);
537
- if (!factory) {
538
- ctx.stderr.write(`[${name}] warning: runner '${ep.runner}' not registered; bin '${ep.binName}' will not be invokable\n`);
539
- continue;
540
- }
541
- const handler = factory(manifest, root, ep.binName, ep.kind);
542
- deps.registry.register(ep.binName, handler);
543
- }
544
- ctx.stdout.write(`[${name}] installed at ${root} (${(totalBytes / 1024 / 1024).toFixed(1)} MiB)\n`);
545
- await warmRuntimeIfConfigured(ctx, deps, {
546
- name,
547
- version,
548
- root,
549
- manifest,
550
- });
551
- return 0;
552
- }
553
- async function warmRuntimeIfConfigured(ctx, deps, target) {
554
- if (!deps.warmRuntime)
555
- return;
556
- try {
557
- await deps.warmRuntime(target, ctx);
558
- }
559
- catch (e) {
560
- ctx.stderr.write(`[${target.name}] warning: runtime warm-up failed: ${e?.message || e}\n`);
561
- }
562
- }
563
- // ── --list ───────────────────────────────────────────────────────────
564
- async function runList(ctx, deps) {
565
- const home = deps.getHome();
566
- const installed = listInstalledManifestsView(deps.vfs, home);
567
- if (installed.length === 0) {
568
- ctx.stdout.write('(no runtimes installed)\n');
569
- return 0;
570
- }
571
- ctx.stdout.write(`installed runtimes (${installed.length}):\n`);
572
- for (const { root, manifest } of installed) {
573
- const totalBytes = manifest.files.reduce((a, f) => a + f.size, 0);
574
- const bins = runtimeEntrypoints(manifest).map((e) => e.binName).join(', ');
575
- ctx.stdout.write(` ${manifest.name}@${manifest.version} abi=${runtimeAbiForManifest(manifest)} ${(totalBytes / 1024 / 1024).toFixed(1)} MiB bins=[${bins}] ${root}\n`);
576
- }
577
- return 0;
578
- }
579
- // ── --available ──────────────────────────────────────────────────────
580
- async function runAvailable(ctx, deps) {
581
- let catalog;
582
- try {
583
- catalog = await fetchCatalog(deps.env);
584
- }
585
- catch (e) {
586
- ctx.stderr.write(`nimbus install --available: ${e?.message || e}\n`);
587
- return 1;
588
- }
589
- const names = Object.keys(catalog.runtimes).sort();
590
- if (names.length === 0) {
591
- ctx.stdout.write('(no runtimes in catalog)\n');
592
- return 0;
593
- }
594
- ctx.stdout.write(`available runtimes (${names.length}):\n`);
595
- for (const name of names) {
596
- const r = catalog.runtimes[name];
597
- const versions = Object.keys(r.versions);
598
- ctx.stdout.write(` ${name} abi=${runtimeAbiForCatalogName(name)} default=${r.default} versions=[${versions.join(', ')}]\n`);
599
- for (const v of versions) {
600
- const ve = r.versions[v];
601
- ctx.stdout.write(` ${v} ${(ve.size_bytes / 1024 / 1024).toFixed(1)} MiB license=${ve.license}\n`);
602
- }
603
- }
604
- return 0;
605
- }
606
- // ── uninstall ────────────────────────────────────────────────────────
607
- async function runUninstall(args, ctx, deps) {
608
- if (args.length === 0 || args[0].startsWith('--')) {
609
- ctx.stderr.write('nimbus uninstall: missing runtime name\n');
610
- return 2;
611
- }
612
- const spec = args[0];
613
- const atIdx = spec.indexOf('@');
614
- const name = atIdx >= 0 ? spec.slice(0, atIdx) : spec;
615
- const versionOverride = atIdx >= 0 ? spec.slice(atIdx + 1) : null;
616
- const home = deps.getHome();
617
- const installed = listInstalledManifestsView(deps.vfs, home);
618
- const matches = installed.filter((x) => x.manifest.name === name && (!versionOverride || x.manifest.version === versionOverride));
619
- if (matches.length === 0) {
620
- ctx.stderr.write(`nimbus uninstall: '${name}' is not installed\n`);
621
- return 1;
622
- }
623
- for (const m of matches) {
624
- // Unregister bins.
625
- for (const ep of runtimeEntrypoints(m.manifest)) {
626
- // We don't have a guaranteed `unregister`; bins shadowing is OK
627
- // because boot-rehydration only re-registers what's still on disk.
628
- if (typeof deps.registry.unregister === 'function') {
629
- deps.registry.unregister(ep.binName);
630
- }
631
- }
632
- if (deps.vfs.exists(m.root))
633
- deps.vfs.removeRecursive(m.root);
634
- ctx.stdout.write(`[${m.manifest.name}@${m.manifest.version}] uninstalled (removed ${m.root})\n`);
635
- }
636
- // Clean up empty parent dirs (~/.nimbus/runtimes/<name>/ if no
637
- // versions left; runtimes/ if no runtimes left).
638
- const runtimeDir = `${home.replace(/^\/+/, '').replace(/\/+$/, '')}/.nimbus/runtimes/${name}`;
639
- cleanupEmpty(deps.vfs, runtimeDir);
640
- cleanupEmpty(deps.vfs, `${home.replace(/^\/+/, '').replace(/\/+$/, '')}/.nimbus/runtimes`);
641
- return 0;
642
- }
643
- function cleanupEmpty(vfs, path) {
644
- if (!vfs.exists(path))
645
- return;
646
- if (vfs.readdir(path).length === 0) {
647
- vfs.rmdir(path);
648
- }
649
- }
@@ -39,7 +39,7 @@ export interface LogsWebSocketDeps {
39
39
  * cycle and lets tests pass a plain stub.
40
40
  */
41
41
  export interface TerminalLike {
42
- ws: WebSocket;
42
+ ws: WebSocket | null;
43
43
  }
44
44
  /**
45
45
  * Send a structured JSON event to the main terminal WebSocket, if one is