@nimbus-sh/worker 0.7.0 → 0.9.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.
- package/dist/facets/compose.d.ts +3 -0
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +1 -1
- package/dist/facets/manager.d.ts +26 -15
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +154 -112
- package/dist/facets/real-vite-hmr.d.ts +3 -1
- package/dist/facets/real-vite-hmr.d.ts.map +1 -1
- package/dist/facets/real-vite-hmr.js +18 -8
- package/dist/facets/ws-terminal.d.ts +9 -31
- package/dist/facets/ws-terminal.d.ts.map +1 -1
- package/dist/facets/ws-terminal.js +58 -51
- package/dist/hosted/commands.d.ts +12 -0
- package/dist/hosted/commands.d.ts.map +1 -0
- package/dist/hosted/commands.js +1647 -0
- package/dist/hosted/runtime.d.ts +135 -0
- package/dist/hosted/runtime.d.ts.map +1 -0
- package/dist/hosted/runtime.js +344 -0
- package/dist/hosted/services.d.ts +44 -0
- package/dist/hosted/services.d.ts.map +1 -0
- package/dist/hosted/services.js +522 -0
- package/dist/loaders/generated-workers.d.ts +1 -1
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +2 -2
- package/dist/node-shims-artifact.generated.js +3 -3
- package/dist/runtime/bash-repl.d.ts +6 -4
- package/dist/runtime/bash-repl.d.ts.map +1 -1
- package/dist/runtime/bash-repl.js +46 -7
- package/dist/runtime/bun-runner.js +1 -1
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +1 -3
- package/dist/runtime/node-runner.d.ts +1 -1
- package/dist/runtime/node-runner.d.ts.map +1 -1
- package/dist/runtime/node-runner.js +2 -2
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +14 -1
- package/dist/runtime/opencode-facet-runner.js +14 -14
- package/dist/runtime/package-manager.d.ts +35 -122
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +61 -596
- package/dist/runtime/process-logs-api.d.ts +1 -1
- package/dist/runtime/process-logs-api.d.ts.map +1 -1
- package/dist/runtime/process-logs-api.js +1 -1
- package/dist/runtime/python-repl.d.ts +6 -4
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +92 -67
- package/dist/runtime/repl-session.d.ts +9 -47
- package/dist/runtime/repl-session.d.ts.map +1 -1
- package/dist/runtime/repl-session.js +105 -105
- package/dist/runtime/ruby-repl.d.ts +3 -2
- package/dist/runtime/ruby-repl.d.ts.map +1 -1
- package/dist/runtime/ruby-repl.js +7 -6
- package/dist/runtime/runtime-catalog.d.ts +13 -0
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +88 -0
- package/dist/runtime/wasi/preamble.d.ts.map +1 -1
- package/dist/runtime/wasi/preamble.js +74 -56
- package/dist/runtime-catalog.generated.js +1 -1
- package/dist/session/agent.d.ts +1 -21
- package/dist/session/agent.d.ts.map +1 -1
- package/dist/session/ai.d.ts +5 -1
- package/dist/session/ai.d.ts.map +1 -1
- package/dist/session/ai.js +7 -0
- package/dist/session/hibernation.d.ts +2 -1
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +7 -4
- package/dist/session/init.d.ts +1 -36
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +18 -1773
- package/dist/session/loopback.d.ts +2 -29
- package/dist/session/loopback.d.ts.map +1 -1
- package/dist/session/loopback.js +3 -27
- package/dist/session/nimbus-session.d.ts +28 -34
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +78 -518
- package/dist/session/npm-install-port.d.ts +3 -1
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/port-capability.d.ts.map +1 -1
- package/dist/session/port-capability.js +11 -1
- package/dist/session/programmatic.d.ts +5 -38
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +17 -49
- package/dist/session/rpc.d.ts +3 -43
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +8 -67
- package/dist/session/supervisor-op.d.ts +10 -5
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +21 -48
- package/dist/session/supervisor-rpc.d.ts +33 -16
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +49 -4
- package/dist/vfs/facet-resident-store.d.ts.map +1 -1
- package/dist/vfs/facet-resident-store.js +4 -2
- package/dist/workspace-host.d.ts +7 -0
- package/dist/workspace-host.d.ts.map +1 -0
- package/dist/workspace-host.js +5 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +1 -0
- package/package.json +17 -12
- package/public/_assets/runtime/{node-shims-8f8c64edd2a1f0f5.js → node-shims-acaa037e6d26ec0d.js} +14 -1
- package/scripts/bundle-runtime.mjs +35 -8
|
@@ -1,113 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* package-manager.ts —
|
|
2
|
+
* package-manager.ts — the Worker's edge of runtime installation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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,
|
|
29
|
-
import {
|
|
30
|
-
import { CRED_KERNEL
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
49
|
+
spec,
|
|
50
|
+
exitCode,
|
|
51
|
+
stdout: stdout.join(''),
|
|
52
|
+
stderr: stderr.join(''),
|
|
40
53
|
};
|
|
41
54
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
}
|