@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.
Files changed (102) hide show
  1. package/dist/facets/compose.d.ts +3 -0
  2. package/dist/facets/compose.d.ts.map +1 -1
  3. package/dist/facets/compose.js +1 -1
  4. package/dist/facets/manager.d.ts +26 -15
  5. package/dist/facets/manager.d.ts.map +1 -1
  6. package/dist/facets/manager.js +154 -112
  7. package/dist/facets/real-vite-hmr.d.ts +3 -1
  8. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  9. package/dist/facets/real-vite-hmr.js +18 -8
  10. package/dist/facets/ws-terminal.d.ts +9 -31
  11. package/dist/facets/ws-terminal.d.ts.map +1 -1
  12. package/dist/facets/ws-terminal.js +58 -51
  13. package/dist/hosted/commands.d.ts +12 -0
  14. package/dist/hosted/commands.d.ts.map +1 -0
  15. package/dist/hosted/commands.js +1647 -0
  16. package/dist/hosted/runtime.d.ts +135 -0
  17. package/dist/hosted/runtime.d.ts.map +1 -0
  18. package/dist/hosted/runtime.js +344 -0
  19. package/dist/hosted/services.d.ts +44 -0
  20. package/dist/hosted/services.d.ts.map +1 -0
  21. package/dist/hosted/services.js +522 -0
  22. package/dist/loaders/generated-workers.d.ts +1 -1
  23. package/dist/loaders/generated-workers.d.ts.map +1 -1
  24. package/dist/loaders/generated-workers.js +2 -2
  25. package/dist/node-shims-artifact.generated.js +3 -3
  26. package/dist/runtime/bash-repl.d.ts +6 -4
  27. package/dist/runtime/bash-repl.d.ts.map +1 -1
  28. package/dist/runtime/bash-repl.js +46 -7
  29. package/dist/runtime/bun-runner.js +1 -1
  30. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  31. package/dist/runtime/facet-loader-host.js +1 -3
  32. package/dist/runtime/node-runner.d.ts +1 -1
  33. package/dist/runtime/node-runner.d.ts.map +1 -1
  34. package/dist/runtime/node-runner.js +2 -2
  35. package/dist/runtime/node-shims.d.ts.map +1 -1
  36. package/dist/runtime/node-shims.js +14 -1
  37. package/dist/runtime/opencode-facet-runner.js +14 -14
  38. package/dist/runtime/package-manager.d.ts +35 -122
  39. package/dist/runtime/package-manager.d.ts.map +1 -1
  40. package/dist/runtime/package-manager.js +61 -596
  41. package/dist/runtime/process-logs-api.d.ts +1 -1
  42. package/dist/runtime/process-logs-api.d.ts.map +1 -1
  43. package/dist/runtime/process-logs-api.js +1 -1
  44. package/dist/runtime/python-repl.d.ts +6 -4
  45. package/dist/runtime/python-repl.d.ts.map +1 -1
  46. package/dist/runtime/python-repl.js +92 -67
  47. package/dist/runtime/repl-session.d.ts +9 -47
  48. package/dist/runtime/repl-session.d.ts.map +1 -1
  49. package/dist/runtime/repl-session.js +105 -105
  50. package/dist/runtime/ruby-repl.d.ts +3 -2
  51. package/dist/runtime/ruby-repl.d.ts.map +1 -1
  52. package/dist/runtime/ruby-repl.js +7 -6
  53. package/dist/runtime/runtime-catalog.d.ts +13 -0
  54. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  55. package/dist/runtime/runtime-catalog.js +88 -0
  56. package/dist/runtime/wasi/preamble.d.ts.map +1 -1
  57. package/dist/runtime/wasi/preamble.js +74 -56
  58. package/dist/runtime-catalog.generated.js +1 -1
  59. package/dist/session/agent.d.ts +1 -21
  60. package/dist/session/agent.d.ts.map +1 -1
  61. package/dist/session/ai.d.ts +5 -1
  62. package/dist/session/ai.d.ts.map +1 -1
  63. package/dist/session/ai.js +7 -0
  64. package/dist/session/hibernation.d.ts +2 -1
  65. package/dist/session/hibernation.d.ts.map +1 -1
  66. package/dist/session/hibernation.js +7 -4
  67. package/dist/session/init.d.ts +1 -36
  68. package/dist/session/init.d.ts.map +1 -1
  69. package/dist/session/init.js +18 -1773
  70. package/dist/session/loopback.d.ts +2 -29
  71. package/dist/session/loopback.d.ts.map +1 -1
  72. package/dist/session/loopback.js +3 -27
  73. package/dist/session/nimbus-session.d.ts +28 -34
  74. package/dist/session/nimbus-session.d.ts.map +1 -1
  75. package/dist/session/nimbus-session.js +78 -518
  76. package/dist/session/npm-install-port.d.ts +3 -1
  77. package/dist/session/npm-install-port.d.ts.map +1 -1
  78. package/dist/session/port-capability.d.ts.map +1 -1
  79. package/dist/session/port-capability.js +11 -1
  80. package/dist/session/programmatic.d.ts +5 -38
  81. package/dist/session/programmatic.d.ts.map +1 -1
  82. package/dist/session/programmatic.js +17 -49
  83. package/dist/session/rpc.d.ts +3 -43
  84. package/dist/session/rpc.d.ts.map +1 -1
  85. package/dist/session/rpc.js +8 -67
  86. package/dist/session/supervisor-op.d.ts +10 -5
  87. package/dist/session/supervisor-op.d.ts.map +1 -1
  88. package/dist/session/supervisor-op.js +21 -48
  89. package/dist/session/supervisor-rpc.d.ts +33 -16
  90. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  91. package/dist/session/supervisor-rpc.js +49 -4
  92. package/dist/vfs/facet-resident-store.d.ts.map +1 -1
  93. package/dist/vfs/facet-resident-store.js +4 -2
  94. package/dist/workspace-host.d.ts +7 -0
  95. package/dist/workspace-host.d.ts.map +1 -0
  96. package/dist/workspace-host.js +5 -0
  97. package/dist/workspace.d.ts +2 -0
  98. package/dist/workspace.d.ts.map +1 -0
  99. package/dist/workspace.js +1 -0
  100. package/package.json +17 -12
  101. package/public/_assets/runtime/{node-shims-8f8c64edd2a1f0f5.js → node-shims-acaa037e6d26ec0d.js} +14 -1
  102. package/scripts/bundle-runtime.mjs +35 -8
@@ -0,0 +1,1647 @@
1
+ import { Shell, createCurlCommand, createNpmCommand, NPM_VERSION, createTopCommand, createWatchCommand, createHelpCommand, rehydrateGlobalPackages } from '@nimbus-sh/core/substrate/lifo/index.js';
2
+ import { createKillCommand } from '@nimbus-sh/core/substrate/lifo/commands/system/kill.js';
3
+ import { textSink } from '@nimbus-sh/core/_shared/bytes.js';
4
+ import { BASH_RUNNER, CRED_KERNEL, requireVfsCred } from '@nimbus-sh/core/runtime/os-contracts.js';
5
+ import { ExecutionFs } from '@nimbus-sh/core/shell/execution-fs.js';
6
+ import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
7
+ import { runFresh } from '../runtime/node-runner.js';
8
+ import { runBunScript, BUN_VERSION } from '../runtime/bun-runner.js';
9
+ import { buildRuntimeHandler, resolveRuntimeScriptPath } from '@nimbus-sh/core/runtime/runtime-registry.js';
10
+ import { normalizeVfsPath, parentVfsPath, resolveVfsPath } from '@nimbus-sh/core/vfs/path.js';
11
+ import { NimbusWrangler } from '../wrangler/nimbus-wrangler.js';
12
+ import { filterWranglerFlags, detectBundlerBin, checkNodeModulesGuard, detectUnsupportedWranglerConfig, refusedNextSubcommand, NEXT_REFUSAL_MESSAGE } from '../session/helpers.js';
13
+ import { createViteCommand } from '../session/vite-command.js';
14
+ import { HeredocHandler, LineEditorExtender } from '@nimbus-sh/core/shell/features.js';
15
+ import { registerShellEntrypointCommands } from '@nimbus-sh/core/shell/shell-entrypoints.js';
16
+ import { makeChshCommand } from '@nimbus-sh/core/substrate/lifo/shell/default-shell.js';
17
+ import { installNpmBinFallbackResolver } from '../shell/npm-bin-entrypoints.js';
18
+ import { createNpmInstallPort } from '../session/npm-install-port.js';
19
+ import { createRuntimeCommandHintResolver } from '../runtime/package-manager.js';
20
+ import { rpcExposeApp, rpcListApps, rpcRemoveApp, rpcRotateLink } from '../session/programmatic.js';
21
+ import { listInstalledRuntimes } from '@nimbus-sh/core/runtime/installed-runtimes.js';
22
+ import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
23
+ import { stripAnsi } from '@nimbus-sh/core/runtime/process-logs.js';
24
+ import { NODE_VERSION } from '@nimbus-sh/core/constants.js';
25
+ import { VITE_CONFIG_KEY } from '../session/keys.js';
26
+ import { HeadlessTerminal } from '@nimbus-sh/core/substrate/lifo/index.js';
27
+ import { makeNimbusVerbHandler } from '@nimbus-sh/core/runtime/nimbus-command.js';
28
+ export async function registerHostedCommands(self, workspace) {
29
+ const sqliteFs = workspace.vfs;
30
+ const kernelFs = sqliteFs.as(CRED_KERNEL);
31
+ const kernel = workspace.kernel;
32
+ const shell = workspace.shell;
33
+ const registry = workspace.registry;
34
+ const processRegistry = kernel.processRegistry;
35
+ const env = workspace.env;
36
+ const terminal = self.terminal;
37
+ if (!terminal)
38
+ throw new Error('runtime terminal is not initialized');
39
+ const facetMgr = self.ensureFacetManager().manager;
40
+ const runAsProcess = shell.getRunAsHost();
41
+ const commandIdentityFor = (pid) => ({
42
+ pid,
43
+ get cred() { return self.processes.cred(pid); },
44
+ setUmask: (mask) => self.processes.setUmask(pid, mask),
45
+ runAs: runAsProcess,
46
+ });
47
+ self._setCpRegistry(registry);
48
+ registry.register('chsh', makeChshCommand({
49
+ isBashInstalled: async (home) => (await listInstalledRuntimes(sqliteFs, home)).some((runtime) => runtime.name === 'bash'),
50
+ }));
51
+ // ── Git integration (isomorphic-git) ──
52
+ // ctx + env are passed for clone/fetch/pull which run in a facet to avoid
53
+ // exhausting the supervisor DO's CPU budget on large repos. The command
54
+ // module (+ its ~106 KB network-facet dependency) is loaded lazily on the
55
+ // first `git` invocation so it stays out of the cold script-eval graph.
56
+ registry.register('git', async (ctx) => {
57
+ const { runGitCommand } = await import('../git/commands.js');
58
+ return runGitCommand(ctx, sqliteFs, self.ctx, self.env);
59
+ });
60
+ // ── runtime package manager: `nimbus install` package manager + runner registry.
61
+ //
62
+ // 1. Register the clang-runner factory FIRST so the rehydration step
63
+ // below can re-bind `clang` / `wasm-ld` from an already-installed
64
+ // manifest.
65
+ // 2. Register the `nimbus` shell verb (install/uninstall/list/available).
66
+ // 3. Rehydrate any previously-installed runtimes from VFS so their
67
+ // bins reappear in the registry after DO eviction or WS reconnect.
68
+ workspace.runtimes.registerRunner('clang-runner', (manifest, installRoot, binName, binKind) => async (ctx) => {
69
+ const { makeClangRunnerFactory } = await import('@nimbus-sh/core/runtime/clang-runner.js');
70
+ const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
71
+ return await makeClangRunnerFactory({ facets: facetHostForManager(facetMgr), filesystem: workspace.filesystem })(manifest, installRoot, binName, binKind)(ctx);
72
+ });
73
+ // Pyodide v1 — Python 3.13 via the same R2-package-manager
74
+ // substrate that ships clang. Manifest entrypoints `python` and
75
+ // `python3` both bind to this factory; the runner ferries
76
+ // pyodide.asm.wasm via LOADER modules-map and pyodide.asm.js +
77
+ // python_stdlib.zip via the loader-pool context channel.
78
+ //
79
+ // REPL-W1: wrap the one-shot factory so `python` with NO args (and
80
+ // no flags that would consume args) drops into an interactive
81
+ // REPL. The wrap is purely additive — args-bearing invocations
82
+ // pass through to the existing handler unchanged.
83
+ workspace.runtimes.registerRunner('cpython-runner', (manifest, installRoot, binName, binKind) => async function pythonReplOrOneShot(ctx) {
84
+ const argv = ctx.args || [];
85
+ // No args at all → REPL session. Hand off to runPythonRepl
86
+ // which builds its own IsolatePool (separate from the
87
+ // one-shot dispatch's pool) and drives a ReplSession.
88
+ if (argv.length === 0 && terminal) {
89
+ const { runPythonRepl } = await import('../runtime/python-repl.js');
90
+ return await runPythonRepl({
91
+ facetMgr,
92
+ authority: workspace.filesystem,
93
+ terminal: terminal,
94
+ installRoot,
95
+ manifest,
96
+ // REPL-R7-1: thread the shell so ReplSession can drain
97
+ // shell.pasteQueue on attach (multi-line WS frames like
98
+ // `python\nexit(7)` would otherwise drop the tail input).
99
+ shell: shell,
100
+ // The supervisor derives the write credential from this; without
101
+ // it the prompt cannot write to the session filesystem.
102
+ pid: ctx.pid,
103
+ });
104
+ }
105
+ // Args present (one-shot mode: -c, script, -m, -). Fall through
106
+ // to the canonical handler (imported lazily on first use).
107
+ const { makeCPythonRunnerFactory } = await import('@nimbus-sh/core/runtime/cpython-runner.js');
108
+ const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
109
+ const { cpythonResidentStart } = await import('../runtime/cpython-resident.js');
110
+ return await makeCPythonRunnerFactory({
111
+ facets: facetHostForManager(facetMgr),
112
+ startResident: cpythonResidentStart(facetMgr),
113
+ })(manifest, installRoot, binName, binKind)(ctx);
114
+ });
115
+ // Ruby v1 — Ruby 3.3.4 via ruby.wasm 2.9.3-2.9.4. Same architecture
116
+ // as python-runner: ruby+stdlib.wasm rides via LOADER modules-map,
117
+ // bootstrap runs at child-facet module-init time, per-call
118
+ // __rubyRun drives rb-eval-string-protect with a wrapper that
119
+ // catches SystemExit. See src/runtime/ruby-runner.ts +
120
+ //
121
+ // REPL Stream A: wrap the one-shot factory so `ruby` with NO args
122
+ // drops into an interactive REPL. The wrap is purely additive —
123
+ // args-bearing invocations pass through to the existing handler.
124
+ workspace.runtimes.registerRunner('ruby-runner', (manifest, installRoot, binName, binKind) => async function rubyReplOrOneShot(ctx) {
125
+ const argv = ctx.args || [];
126
+ if (argv.length === 0 && terminal) {
127
+ const { runRubyRepl } = await import('../runtime/ruby-repl.js');
128
+ return await runRubyRepl({
129
+ facetMgr,
130
+ authority: workspace.filesystem,
131
+ terminal: terminal,
132
+ installRoot,
133
+ });
134
+ }
135
+ const { makeRubyRunnerFactory } = await import('@nimbus-sh/core/runtime/ruby-runner.js');
136
+ const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
137
+ const { rubyResidentStart } = await import('../runtime/ruby-resident.js');
138
+ const runner = await makeRubyRunnerFactory({
139
+ facets: facetHostForManager(facetMgr),
140
+ filesystem: workspace.filesystem,
141
+ registry,
142
+ startResident: rubyResidentStart(facetMgr),
143
+ })(manifest, installRoot, binName, binKind);
144
+ return runner(ctx);
145
+ });
146
+ // GNU bash 5.2.37 (wasm32-wasi, asyncified) — dedicated facet
147
+ // runner driving the fork/pipe/exec/setjmp scheduler (fork M1-M3
148
+ // mechanisms). Interactive terminal invocations use the shared
149
+ // ReplSession line editor; -c, scripts, and piped stdin retain the
150
+ // canonical one-shot handler.
151
+ workspace.runtimes.registerRunner(BASH_RUNNER, (manifest, installRoot, binName, binKind) => async (ctx) => {
152
+ const argv = ctx.args || [];
153
+ const explicitInteractive = argv.includes('-i');
154
+ const terminalStdin = ctx.isFdTerminal?.(0) ?? !ctx.stdin;
155
+ if (terminal && (explicitInteractive || (argv.length === 0 && terminalStdin))) {
156
+ const { runBashRepl } = await import('../runtime/bash-repl.js');
157
+ return await runBashRepl({
158
+ facetMgr,
159
+ authority: workspace.filesystem,
160
+ terminal: terminal,
161
+ installRoot,
162
+ manifest,
163
+ cred: ctx.cred,
164
+ pid: ctx.pid,
165
+ filesystem: workspace.filesystem.bind({ pid: ctx.pid, cred: ctx.cred, signal: ctx.signal }),
166
+ env: ctx.env,
167
+ cwd: ctx.cwd || '/home/user',
168
+ shell: shell ?? undefined,
169
+ });
170
+ }
171
+ const { makeBashRunnerFactory } = await import('@nimbus-sh/core/runtime/bash-runner.js');
172
+ const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
173
+ return await makeBashRunnerFactory({ facets: facetHostForManager(facetMgr), filesystem: workspace.filesystem })(manifest, installRoot, binName, binKind)(ctx);
174
+ });
175
+ {
176
+ // Cast registry to the minimal package-manager shape. CommandRegistry
177
+ // CommandRegistry has register(name, handler) which matches.
178
+ registry.register('nimbus', makeNimbusVerbHandler({
179
+ runtimes: workspace.runtimes,
180
+ vfs: kernelFs,
181
+ registry: registry,
182
+ // The application verbs are the session's own (programmatic.ts):
183
+ // the shell, the SDK and the Agent all reach the same policy.
184
+ apps: {
185
+ expose: (target, options) => rpcExposeApp(self, target, options),
186
+ list: () => rpcListApps(self),
187
+ rotateLink: (target) => rpcRotateLink(self, target),
188
+ remove: (target) => rpcRemoveApp(self, target),
189
+ },
190
+ warmRuntime: async (target, ctx) => {
191
+ // The runtime name, which is what `nimbus install python` installs
192
+ // — not the name the user typed. Left as 'python' through the
193
+ // migration, this matched only the superseded Pyodide entry, so the
194
+ // interpreter people actually install was never warmed and the
195
+ // first invocation after installing paid the wasm compile.
196
+ if (target.name !== 'cpython')
197
+ return;
198
+ ctx.stdout.write(`[${target.name}] warming runtime...\n`);
199
+ const stdout = { write(_s) { } };
200
+ const stderrText = [];
201
+ const stderr = { write: (s) => { stderrText.push(String(s)); } };
202
+ const py = await registry.resolve('python');
203
+ if (py) {
204
+ const pid = ctx.pid;
205
+ const cred = requireVfsCred(ctx.cred, 'python warm-up');
206
+ const code = await py({
207
+ ...ctx,
208
+ args: ['-c', 'pass'],
209
+ pid,
210
+ cred,
211
+ setUmask: (mask) => ctx.setUmask(mask),
212
+ runAs: (targetCred, argv) => ctx.runAs(targetCred, argv),
213
+ vfs: new ExecutionFs(workspace.filesystem.bind({ pid, cred })),
214
+ signal: new AbortController().signal,
215
+ stdout,
216
+ stderr,
217
+ });
218
+ if (typeof code === 'number' && code !== 0) {
219
+ throw new Error(stderrText.join('').trim() || `python warm-up exited ${code}`);
220
+ }
221
+ }
222
+ const { warmPythonRepl } = await import('../runtime/python-repl.js');
223
+ await warmPythonRepl({
224
+ facetMgr,
225
+ authority: workspace.filesystem,
226
+ installRoot: target.root,
227
+ manifest: target.manifest,
228
+ });
229
+ ctx.stdout.write('[python] ready\n');
230
+ },
231
+ }));
232
+ // Rehydration runs here even though the shell hasn't been built
233
+ // yet — that's fine: it accesses VFS + registry which are both
234
+ // already initialised. Runs O(installed-runtimes); typically 0
235
+ // on a fresh session.
236
+ try {
237
+ const rehydration = await workspace.runtimes.rehydrate();
238
+ if (rehydration.count > 0) {
239
+ // Surface to terminal via the standard MOTD-style line so
240
+ // users see what's been auto-rebound. Not an error path.
241
+ terminal?.write?.(`\x1b[2m[nimbus] rehydrated ${rehydration.count} runtime bin(s): ${rehydration.bins.join(', ')}\x1b[0m\r\n`);
242
+ }
243
+ }
244
+ catch { /* fail-soft: rehydration must not block session boot */ }
245
+ }
246
+ // ── node command: facet-based execution ─────────────────────────────
247
+ // Parses args, reads script from VFS, delegates to FacetManager.
248
+ // The facet creates a dynamic worker where new Function() is allowed
249
+ // during module startup.
250
+ // ── node command (runtime registry refactor: refactored to use runtime-registry) ──
251
+ //
252
+ // Behaviour preserved exactly:
253
+ // - primitive #1 nodeFlagSpan (--version/-v/-h scan only flags
254
+ // before script path)
255
+ // - primitive #1 shebang strip
256
+ // - .ts/.tsx/.jsx esbuild auto-transform
257
+ // - G4 binSpawn ctx propagation (when the .bin handler set
258
+ // ctx.__nimbusBinSpawn, runFresh reuses the caller's PID
259
+ // instead of double-spawning)
260
+ // - --watch/--inspect/--inspect-brk routing via runFresh →
261
+ // isLongRunningInvocation
262
+ //
263
+ // The registry encodes the shared shape; per-runtime overrides
264
+ // live in the spec object.
265
+ const nodeSpec = {
266
+ name: 'node',
267
+ // CLN-1b: was stale literal 'v20.0.0'; pull from src/constants.ts
268
+ // canonical (src/runtime/node-shims.ts already does the same).
269
+ version: NODE_VERSION,
270
+ helpText: 'Usage: node [options] [script.js] [arguments]\n' +
271
+ ' node -e "code"\n\n' +
272
+ 'Options:\n' +
273
+ ' -e, --eval <code> Evaluate code\n' +
274
+ ' -v, --version Print version\n' +
275
+ ' -h, --help Print help\n' +
276
+ '\nExecution via DO Facets (isolated V8 isolate)',
277
+ run: (code, opts) => runFresh(facetMgr, code, opts),
278
+ supportsBinSpawn: true,
279
+ };
280
+ {
281
+ const oneShotNode = buildRuntimeHandler(nodeSpec, {
282
+ getEsbuild: () => {
283
+ if (!self.esbuildService) {
284
+ self.ensureSqliteFs();
285
+ self.esbuildService = new EsbuildService(kernelFs);
286
+ }
287
+ return self.esbuildService;
288
+ },
289
+ registry,
290
+ });
291
+ // REPL Stream A: no-args invocation → drop into REPL session.
292
+ registry.register('node', async function nodeReplOrOneShot(ctx) {
293
+ const argv = ctx.args || [];
294
+ if (argv.length === 0 && terminal) {
295
+ const { runNodeRepl } = await import('../runtime/node-repl.js');
296
+ return await runNodeRepl({ facetMgr, terminal: terminal });
297
+ }
298
+ return await oneShotNode(ctx);
299
+ });
300
+ }
301
+ // ── bun command (runtime registry refactor: refactored to use runtime-registry) ──
302
+ //
303
+ // Behaviour preserved exactly:
304
+ // - --version / --help
305
+ // - install / i / add → delegate to npm
306
+ // - run <script> → look up package.json#scripts and shell.execute
307
+ // - -e / --eval flow
308
+ // - script-path flow with .ts/.tsx/.jsx auto-transform
309
+ // - BUN_SHIM_PREAMBLE prepend (handled inside runBunScript itself)
310
+ //
311
+ // Bun does NOT use binSpawn ctx propagation today (its runFresh
312
+ // chain doesn't share PID state with the .bin handler — the .bin
313
+ // handler always dispatches through `node`, not `bun`). So
314
+ // supportsBinSpawn=false (default).
315
+ /** `scripts` from the cwd's package.json; empty when there is none. */
316
+ const readPackageScripts = (cwd) => {
317
+ try {
318
+ const pkg = JSON.parse(kernelFs.readFileString(cwd + '/package.json'));
319
+ const scripts = pkg?.scripts;
320
+ return scripts && typeof scripts === 'object' ? scripts : {};
321
+ }
322
+ catch {
323
+ return {};
324
+ }
325
+ };
326
+ const bunSpec = {
327
+ name: 'bun',
328
+ version: BUN_VERSION,
329
+ helpText: 'Usage: bun [options] [script.[js|ts|tsx]] [args...]\n' +
330
+ ' bun -e "code"\n' +
331
+ ' bun install [pkg ...]\n' +
332
+ ' bun run <script>\n\n' +
333
+ 'Bun-runtime shim provides Bun.file/Bun.write/\n' +
334
+ 'Bun.spawn/Bun.password/Bun.gunzip backed by Workers-native\n' +
335
+ 'primitives. Bun.serve / Bun.sql / Bun.S3 throw with supported alternatives.\n' +
336
+ 'Execution via DO Facets (isolated V8 isolate per call).',
337
+ run: (code, opts) => runBunScript(facetMgr, code, opts),
338
+ subcommands: {
339
+ // bun install / i / add → npm install (same VFS, same R2 caches).
340
+ install: async (ctx, reg) => {
341
+ const npmCmd = await reg.resolve('npm');
342
+ if (npmCmd) {
343
+ return await npmCmd({ ...ctx, args: ['install', ...(ctx.args || []).slice(1)] });
344
+ }
345
+ ctx.stderr.write('bun install: npm handler unavailable\n');
346
+ return 1;
347
+ },
348
+ i: async (ctx, reg) => {
349
+ const npmCmd = await reg.resolve('npm');
350
+ if (npmCmd) {
351
+ return await npmCmd({ ...ctx, args: ['install', ...(ctx.args || []).slice(1)] });
352
+ }
353
+ ctx.stderr.write('bun i: npm handler unavailable\n');
354
+ return 1;
355
+ },
356
+ add: async (ctx, reg) => {
357
+ const npmCmd = await reg.resolve('npm');
358
+ if (npmCmd) {
359
+ return await npmCmd({ ...ctx, args: ['install', ...(ctx.args || []).slice(1)] });
360
+ }
361
+ ctx.stderr.write('bun add: npm handler unavailable\n');
362
+ return 1;
363
+ },
364
+ // bun run <target> — a package.json script, or a file.
365
+ //
366
+ // Real bun's precedence, and the reason this is not just a script
367
+ // lookup: a path-shaped target (`./cli.ts`, `sub/x`, `.`) is ALWAYS
368
+ // a file and never consults scripts, while a bare name checks
369
+ // scripts first and only then falls back to a file. The file case
370
+ // delegates to the standard runtime flow, so `bun run ./cli.ts`
371
+ // and `bun cli.ts` execute through one path — TypeScript transform,
372
+ // facet dispatch and all.
373
+ run: async (ctx, _reg, runAsBun) => {
374
+ const args = ctx.args || [];
375
+ // bun's own run flags sit between the verb and the target.
376
+ let targetIdx = 1;
377
+ while (targetIdx < args.length && args[targetIdx].startsWith('-'))
378
+ targetIdx++;
379
+ const target = args[targetIdx];
380
+ const cwd = normalizeVfsPath(ctx.cwd || '/home/user');
381
+ const scripts = readPackageScripts(cwd);
382
+ if (!target) {
383
+ ctx.stdout.write('Usage: bun run [flags] <file or script>\n');
384
+ const names = Object.keys(scripts);
385
+ if (names.length) {
386
+ ctx.stdout.write(`\npackage.json scripts (${names.length} found):\n\n`);
387
+ for (const n of names)
388
+ ctx.stdout.write(` bun run ${n}\n ${scripts[n]}\n\n`);
389
+ }
390
+ return 0;
391
+ }
392
+ const pathShaped = target === '.' || target === '..' || target.includes('/');
393
+ const pkgScript = pathShaped ? undefined : scripts[target];
394
+ if (pkgScript) {
395
+ // Trailing args append to the script command; bun drops a single
396
+ // `--` separator between the script name and them.
397
+ const extra = args.slice(targetIdx + 1);
398
+ if (extra[0] === '--')
399
+ extra.shift();
400
+ const command = extra.length ? `${pkgScript} ${extra.join(' ')}` : pkgScript;
401
+ try {
402
+ const shellResult = await shell.execute(command, {
403
+ cwd: ctx.cwd,
404
+ env: ctx.env,
405
+ onStdout: textSink((d) => ctx.stdout.write(d)),
406
+ onStderr: textSink((d) => ctx.stderr.write(d)),
407
+ });
408
+ return shellResult.exitCode;
409
+ }
410
+ catch (e) {
411
+ ctx.stderr.write(`bun run: ${e?.message ?? String(e)}\n`);
412
+ return 1;
413
+ }
414
+ }
415
+ const resolved = (await resolveRuntimeScriptPath(kernelFs, cwd, target, {
416
+ preferModuleField: true,
417
+ }));
418
+ if (resolved === null) {
419
+ ctx.stderr.write(pathShaped
420
+ ? `error: Module not found "${target}"\n`
421
+ : `error: Script not found "${target}"\n`);
422
+ return 1;
423
+ }
424
+ return runAsBun([...args.slice(1, targetIdx), '/' + resolved, ...args.slice(targetIdx + 1)]);
425
+ },
426
+ },
427
+ };
428
+ {
429
+ const oneShotBun = buildRuntimeHandler(bunSpec, {
430
+ getEsbuild: () => {
431
+ if (!self.esbuildService) {
432
+ self.ensureSqliteFs();
433
+ self.esbuildService = new EsbuildService(kernelFs);
434
+ }
435
+ return self.esbuildService;
436
+ },
437
+ registry,
438
+ });
439
+ // REPL Stream A: no-args invocation → drop into REPL session.
440
+ registry.register('bun', async function bunReplOrOneShot(ctx) {
441
+ const argv = ctx.args || [];
442
+ if (argv.length === 0 && terminal) {
443
+ const { runBunRepl } = await import('../runtime/bun-repl.js');
444
+ return await runBunRepl({ facetMgr, terminal: terminal });
445
+ }
446
+ return await oneShotBun(ctx);
447
+ });
448
+ }
449
+ // ── wasm-runner: native WebAssembly, on this session's facet host ──
450
+ //
451
+ // Bytes ride INSIDE the inner worker's code blob, the one phase where
452
+ // workerd permits wasm code generation; request-time
453
+ // WebAssembly.instantiate(bytes) is CSP-blocked and avoided.
454
+ //
455
+ // wasm-csp/findings.md — add(3,4)===7 in 11ms warm against the
456
+ // deployed Cloudflare fleet.
457
+ //
458
+ // Lazy: the wasm-runner module (WASI instance preamble + snapshot
459
+ // machinery) is imported on first `wasm-runner` invocation and the
460
+ // built handler memoized, so its module-eval cost stays off the cold
461
+ // Worker Startup Time path.
462
+ {
463
+ let wasmHandler = null;
464
+ registry.register('wasm-runner', async (ctx) => {
465
+ if (!wasmHandler) {
466
+ const { wasmRunnerSpec } = await import('@nimbus-sh/core/runtime/wasm-runner.js');
467
+ const { loaderFacetHost } = await import('../runtime/facet-loader-host.js');
468
+ const wasmSpec = wasmRunnerSpec({
469
+ filesystem: workspace.filesystem,
470
+ facets: loaderFacetHost(self.env, self.ctx),
471
+ processes: self.processes,
472
+ });
473
+ wasmHandler = buildRuntimeHandler(wasmSpec, {
474
+ getEsbuild: () => {
475
+ if (!self.esbuildService) {
476
+ self.ensureSqliteFs();
477
+ self.esbuildService = new EsbuildService(kernelFs);
478
+ }
479
+ return self.esbuildService;
480
+ },
481
+ registry,
482
+ });
483
+ }
484
+ return await wasmHandler(ctx);
485
+ });
486
+ }
487
+ kernel.routeLoopback = (port, request) => self.routeLoopback(port, request);
488
+ try {
489
+ registry.register('curl', createCurlCommand(kernel));
490
+ }
491
+ catch { }
492
+ // ── df with SQLite stats + cache + process metrics ──────────────────
493
+ registry.register('df', async (ctx) => {
494
+ const stats = sqliteFs.getStats();
495
+ const pstats = facetMgr.stats;
496
+ const used = stats.usedBytes;
497
+ const cap = stats.capacityBytes;
498
+ const avail = cap - used;
499
+ const pct = ((used / cap) * 100).toFixed(0);
500
+ const fmt = (b) => {
501
+ if (b >= 1e9)
502
+ return (b / 1e9).toFixed(1) + 'G';
503
+ if (b >= 1e6)
504
+ return (b / 1e6).toFixed(1) + 'M';
505
+ if (b >= 1e3)
506
+ return (b / 1e3).toFixed(1) + 'K';
507
+ return b + 'B';
508
+ };
509
+ ctx.stdout.write('Filesystem Size Used Avail Use% Mounted on\n');
510
+ ctx.stdout.write('sqlite ' + fmt(cap).padStart(5) + ' ' + fmt(used).padStart(5) +
511
+ ' ' + fmt(avail).padStart(5) + ' ' + pct.padStart(3) + '% /\n');
512
+ ctx.stdout.write('\nCache: ' + stats.cache.entries + '/' + stats.cache.maxEntries +
513
+ ' slots | hit rate: ' + stats.cache.hitRate +
514
+ '% | evictions: ' + stats.cache.evictions + '\n');
515
+ ctx.stdout.write('Procs: ' + pstats.running + ' running, ' +
516
+ pstats.exited + ' exited, ' +
517
+ pstats.total + ' total (next PID: ' + pstats.nextPid + ')\n');
518
+ return 0;
519
+ });
520
+ // ── esbuild command: transform/bundle via esbuild facet ───────────────
521
+ // Lazy-creates the EsbuildService on first use (esbuild-wasm is ~10MB).
522
+ registry.register('esbuild', async (ctx) => {
523
+ const args = ctx.args || [];
524
+ if (args.includes('--version')) {
525
+ ctx.stdout.write('0.24.2 (esbuild-wasm, bundled)\n');
526
+ return 0;
527
+ }
528
+ if (args.includes('--help') || args.length === 0) {
529
+ ctx.stdout.write('Usage: esbuild [options] [entry points]\n\n');
530
+ ctx.stdout.write('Options:\n');
531
+ ctx.stdout.write(' --bundle Bundle all dependencies into output\n');
532
+ ctx.stdout.write(' --outfile=<path> Write output to a file\n');
533
+ ctx.stdout.write(' --outdir=<path> Write output to a directory\n');
534
+ ctx.stdout.write(' --format=esm|cjs Output format (default: esm)\n');
535
+ ctx.stdout.write(' --platform=browser|node Target platform\n');
536
+ ctx.stdout.write(' --minify Minify output\n');
537
+ ctx.stdout.write(' --sourcemap Generate source maps\n');
538
+ ctx.stdout.write(' --target=<target> JS target (default: esnext)\n');
539
+ ctx.stdout.write(' --loader=<loader> Force file loader (ts, tsx, jsx, css)\n');
540
+ ctx.stdout.write(' --version Show version\n');
541
+ ctx.stdout.write('\nPowered by esbuild-wasm (bundled in supervisor).\n');
542
+ return 0;
543
+ }
544
+ // Lazy-init esbuild service
545
+ if (!self.esbuildService) {
546
+ self.ensureSqliteFs();
547
+ self.esbuildService = new EsbuildService(kernelFs);
548
+ }
549
+ // Parse flags
550
+ const flags = {};
551
+ const entryPoints = [];
552
+ for (const arg of args) {
553
+ if (arg.startsWith('--')) {
554
+ const eqIdx = arg.indexOf('=');
555
+ if (eqIdx > 0) {
556
+ flags[arg.substring(2, eqIdx)] = arg.substring(eqIdx + 1);
557
+ }
558
+ else {
559
+ flags[arg.substring(2)] = 'true';
560
+ }
561
+ }
562
+ else {
563
+ entryPoints.push(arg);
564
+ }
565
+ }
566
+ // Transform-only mode (single file, no --bundle)
567
+ if (entryPoints.length === 1 && !flags['bundle']) {
568
+ // Read the file and transform it
569
+ const filePath = resolveVfsPath(entryPoints[0], ctx.cwd || '/home/user');
570
+ let code;
571
+ try {
572
+ code = kernelFs.readFileString(filePath);
573
+ }
574
+ catch {
575
+ ctx.stderr.write(`esbuild: could not read file: ${entryPoints[0]}\n`);
576
+ return 1;
577
+ }
578
+ try {
579
+ ctx.stderr.write('Transforming...\n');
580
+ const result = await self.esbuildService.transform(code, {
581
+ loader: flags['loader'] || (() => {
582
+ const ext = filePath.split('.').pop()?.toLowerCase();
583
+ return { ts: 'ts', tsx: 'tsx', jsx: 'jsx', js: 'js', mts: 'ts', mjs: 'js', css: 'css', json: 'json' }[ext || ''];
584
+ })(),
585
+ format: flags['format'] || 'esm',
586
+ target: flags['target'] || 'esnext',
587
+ sourcemap: flags['sourcemap'] === 'true',
588
+ minify: flags['minify'] === 'true',
589
+ });
590
+ if (flags['outfile']) {
591
+ const outPath = resolveVfsPath(flags['outfile'], ctx.cwd || '/home/user');
592
+ const parent = parentVfsPath(outPath);
593
+ if (parent && !kernelFs.exists(parent))
594
+ kernelFs.mkdir(parent, { recursive: true });
595
+ kernelFs.writeFile(outPath, result.code);
596
+ ctx.stdout.write(` ${outPath} ${result.code.length} bytes\n`);
597
+ }
598
+ else {
599
+ ctx.stdout.write(result.code);
600
+ }
601
+ for (const w of result.warnings || []) {
602
+ ctx.stderr.write(`warning: ${w.text}\n`);
603
+ }
604
+ return 0;
605
+ }
606
+ catch (e) {
607
+ ctx.stderr.write(`esbuild error: ${e?.message || e}\n`);
608
+ return 1;
609
+ }
610
+ }
611
+ // Bundle mode
612
+ if (entryPoints.length === 0) {
613
+ ctx.stderr.write('esbuild: no entry points specified\n');
614
+ return 1;
615
+ }
616
+ // Resolve entry points relative to cwd
617
+ const resolvedEntryPoints = entryPoints.map(ep => resolveVfsPath(ep, ctx.cwd || '/home/user'));
618
+ try {
619
+ ctx.stderr.write('Bundling...\n');
620
+ const result = await self.esbuildService.build(resolvedEntryPoints, {
621
+ bundle: flags['bundle'] === 'true',
622
+ format: flags['format'] || 'esm',
623
+ target: flags['target'] || 'esnext',
624
+ platform: flags['platform'] || 'browser',
625
+ outdir: flags['outfile'] ? undefined : (flags['outdir'] || '/dist'),
626
+ outfile: flags['outfile'],
627
+ sourcemap: flags['sourcemap'] === 'true',
628
+ minify: flags['minify'] === 'true',
629
+ external: flags['external']?.split(','),
630
+ });
631
+ for (const e of result.errors || []) {
632
+ ctx.stderr.write(`error: ${e.text}\n`);
633
+ }
634
+ for (const w of result.warnings || []) {
635
+ ctx.stderr.write(`warning: ${w.text}\n`);
636
+ }
637
+ if (result.errors?.length)
638
+ return 1;
639
+ // Write output files to VFS
640
+ for (const f of result.outputFiles || []) {
641
+ const outPath = normalizeVfsPath(f.path);
642
+ const parent = parentVfsPath(outPath);
643
+ if (parent && !kernelFs.exists(parent))
644
+ kernelFs.mkdir(parent, { recursive: true });
645
+ kernelFs.writeFile(outPath, f.contents);
646
+ ctx.stdout.write(` ${outPath} ${f.contents.length} bytes\n`);
647
+ }
648
+ ctx.stderr.write(`Done (${result.outputFiles?.length || 0} output files)\n`);
649
+ return 0;
650
+ }
651
+ catch (e) {
652
+ ctx.stderr.write(`esbuild error: ${e?.message || e}\n`);
653
+ return 1;
654
+ }
655
+ });
656
+ // ── vite command: start/stop the dev server ──────────────────────────
657
+ registry.register('vite', createViteCommand(self));
658
+ // ── nimbus-wrangler / wrangler command: Worker dev server ─────────────
659
+ //
660
+ // `wrangler` is registered as a transparent alias for `nimbus-wrangler`
661
+ // so projects with `"dev": "wrangler dev"` in package.json Just Work.
662
+ // The shared handler below takes an extra `invokedAs` flag so we can
663
+ // - print a one-shot "DO-in-DO mode" banner on the first wrangler
664
+ // invocation per session (so users know they're getting a compat
665
+ // layer, not real wrangler)
666
+ // - silently strip wrangler-specific flags (--ip, --port, etc.) that
667
+ // have no meaning inside a DO.
668
+ const wranglerHandler = (invokedAs) => async (ctx) => {
669
+ const rawArgs = ctx.args || [];
670
+ // Filter wrangler-only flags early (works for both invocation paths;
671
+ // a no-op for nimbus-wrangler since it doesn't accept them anyway).
672
+ const { args, ignored } = filterWranglerFlags(rawArgs);
673
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
674
+ ctx.stdout.write(`Usage: ${invokedAs} dev [options]\n\n`);
675
+ ctx.stdout.write('Run your Cloudflare Worker locally on the actual CF runtime\n');
676
+ ctx.stdout.write('(DO-in-DO via env.LOADER — workerd in a workerd).\n\n');
677
+ ctx.stdout.write('Commands:\n');
678
+ ctx.stdout.write(' dev Start the dev server\n');
679
+ ctx.stdout.write(' stop Stop the dev server\n\n');
680
+ ctx.stdout.write('Options:\n');
681
+ ctx.stdout.write(' --root <dir> Project root (default: cwd)\n\n');
682
+ if (invokedAs === 'wrangler') {
683
+ ctx.stdout.write('Note: \x1b[2minside Nimbus, `wrangler` is an alias for\x1b[0m \x1b[36mnimbus-wrangler\x1b[0m.\n');
684
+ ctx.stdout.write('Most real-wrangler flags (--ip, --port, --local, --log-level, ...)\n');
685
+ ctx.stdout.write('are silently ignored because the DO provides its own routing.\n');
686
+ }
687
+ return 0;
688
+ }
689
+ if (args[0] === 'stop') {
690
+ if (self.nimbusWrangler?.isRunning) {
691
+ self.nimbusWrangler.stop();
692
+ ctx.stdout.write('\x1b[33mWorker dev server stopped.\x1b[0m\n');
693
+ }
694
+ else {
695
+ ctx.stdout.write('No Worker dev server running.\n');
696
+ }
697
+ return 0;
698
+ }
699
+ if (args[0] !== 'dev') {
700
+ ctx.stderr.write(`Unknown command: ${args[0]}. Use "${invokedAs} dev" or "${invokedAs} --help".\n`);
701
+ return 1;
702
+ }
703
+ // First-run banner — only when invoked as `wrangler`, and only once
704
+ // per session. Makes it OBVIOUS to the user that they're not running
705
+ // real wrangler, and that Nimbus is doing something different.
706
+ if (invokedAs === 'wrangler' && !self.wranglerAliasBannerShown) {
707
+ ctx.stdout.write('\x1b[2m\u2388 wrangler (Nimbus DO-in-DO mode) — bundling via esbuild-wasm, running via env.LOADER\x1b[0m\n');
708
+ self.wranglerAliasBannerShown = true;
709
+ }
710
+ // Report ignored flags (also one-shot — if user sees it once per
711
+ // session that's enough to spot a typo; no need to spam on rebuilds).
712
+ if (ignored.length > 0 && invokedAs === 'wrangler') {
713
+ ctx.stdout.write('\x1b[2m ignored wrangler flags: ' + ignored.join(' ') + '\x1b[0m\n');
714
+ }
715
+ // Lazy-init esbuild
716
+ if (!self.esbuildService) {
717
+ self.ensureSqliteFs();
718
+ self.esbuildService = new EsbuildService(kernelFs);
719
+ }
720
+ // Parse --root flag; default to the shell cwd so `npm run dev` from
721
+ // a project directory picks up that project's wrangler.jsonc.
722
+ let root = ctx.cwd || '/home/user';
723
+ for (let i = 1; i < args.length; i++) {
724
+ if (args[i] === '--root' && args[i + 1])
725
+ root = args[i + 1];
726
+ }
727
+ // Stop existing
728
+ if (self.nimbusWrangler?.isRunning)
729
+ self.nimbusWrangler.stop();
730
+ const vfsRoot = resolveVfsPath(root, ctx.cwd || '/home/user');
731
+ // Pre-flight: read the wrangler config ourselves and call out any
732
+ // binding fields nimbus-wrangler can't provide. NimbusWrangler will
733
+ // still try to bundle + load, but user sees up-front why their
734
+ // Worker may fail when it tries to access a missing binding.
735
+ const unsupportedFields = detectUnsupportedWranglerConfig(kernelFs, vfsRoot);
736
+ ctx.stdout.write('\n');
737
+ ctx.stdout.write('\x1b[1;35m ' + (invokedAs === 'wrangler' ? 'Wrangler' : 'Nimbus Wrangler') + ' Dev\x1b[0m\n\n');
738
+ if (unsupportedFields.length > 0) {
739
+ ctx.stderr.write('\x1b[33m\u26A0\x1b[0m \x1b[1mNimbus-incompatible wrangler.jsonc fields detected:\x1b[0m\n');
740
+ for (const f of unsupportedFields) {
741
+ ctx.stderr.write(' - \x1b[33m' + f + '\x1b[0m\n');
742
+ }
743
+ ctx.stderr.write(' These bindings are NOT provisioned inside nimbus-wrangler. Your Worker\n' +
744
+ ' will get \x1b[2mundefined\x1b[0m when it tries to access them, which typically\n' +
745
+ ' causes a runtime TypeError. The bundle will still build and load.\n' +
746
+ ' \x1b[2mDeploy with real wrangler to get the real bindings.\x1b[0m\n\n');
747
+ }
748
+ self.nimbusWrangler = new NimbusWrangler({
749
+ vfs: kernelFs,
750
+ vfsEvents: sqliteFs.events,
751
+ esbuild: self.esbuildService,
752
+ env: self.env,
753
+ // Supervisor DO ctx — required for ctx.facets.get() when
754
+ // synthesizing durable_objects bindings on the inner Worker.
755
+ ctx: self.ctx,
756
+ root: vfsRoot,
757
+ onLog: (msg) => {
758
+ if (terminal) {
759
+ try {
760
+ terminal.write(msg);
761
+ }
762
+ catch { }
763
+ }
764
+ },
765
+ onHmrMessage: (msg) => {
766
+ if (terminal) {
767
+ try {
768
+ terminal.ws?.send(JSON.stringify({ type: 'hmr', data: msg }));
769
+ }
770
+ catch { }
771
+ }
772
+ },
773
+ });
774
+ const ok = await self.nimbusWrangler.start();
775
+ if (!ok) {
776
+ ctx.stderr.write(' \x1b[31mFailed to start Worker dev server.\x1b[0m\n');
777
+ return 1;
778
+ }
779
+ const cfg = self.nimbusWrangler.stats;
780
+ // runtime primitive support (P5): banner advertises the canonical
781
+ // `/__nimbus/worker/` route. The legacy `/worker/` URL is still
782
+ // accepted for one release (Sunset 2027-01-01) but new sessions
783
+ // are pointed at the namespaced form.
784
+ const workerBase = (self.sessionBasePath || '') + '/__nimbus/worker';
785
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Worker: \x1b[36m' + workerBase + '/\x1b[0m\n');
786
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Name: ' + cfg.name + '\n');
787
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Entry: ' + cfg.main + '\n');
788
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Root: ' + cfg.root + '\n');
789
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Reload: on file change\n\n');
790
+ ctx.stdout.write(' \x1b[2mRun \x1b[0m' + invokedAs + ' stop\x1b[2m to stop.\x1b[0m\n\n');
791
+ return 0;
792
+ };
793
+ registry.register('nimbus-wrangler', wranglerHandler('nimbus-wrangler'));
794
+ registry.register('wrangler', wranglerHandler('wrangler'));
795
+ // ── npm-fast command: parallel npm install (v2 — batched writes) ────
796
+ registry.register('npm-fast', async (ctx) => {
797
+ const args = ctx.args || [];
798
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
799
+ ctx.stdout.write('Usage: npm-fast install <packages...>\n\n');
800
+ ctx.stdout.write('Nimbus npm v2 — batched VFS writes, content-addressed cache.\n');
801
+ ctx.stdout.write('Handles 100+ dependency projects without crashing.\n');
802
+ return 0;
803
+ }
804
+ if (args[0] !== 'install' && args[0] !== 'i') {
805
+ ctx.stderr.write('Only "npm-fast install" is supported. Use "npm" for other commands.\n');
806
+ return 1;
807
+ }
808
+ const packages = args.slice(1).filter((a) => !a.startsWith('-'));
809
+ if (packages.length === 0) {
810
+ ctx.stderr.write('Specify packages to install: npm-fast install react react-dom\n');
811
+ return 1;
812
+ }
813
+ self.ensureSqliteFs();
814
+ const cwd = normalizeVfsPath(ctx.cwd || '/home/user');
815
+ // Ensure package.json exists
816
+ const pkgJsonPath = cwd + '/package.json';
817
+ if (!kernelFs.exists(pkgJsonPath)) {
818
+ kernelFs.writeFile(pkgJsonPath, '{"name":"project","version":"1.0.0","dependencies":{}}\n');
819
+ }
820
+ ctx.stdout.write('\x1b[36mNimbus npm v2 (batched writes)\x1b[0m\n');
821
+ await self.ensureNpmInstaller((msg) => {
822
+ ctx.stdout.write('[npm] ' + msg + '\n');
823
+ });
824
+ const result = await self.npmInstaller.install(cwd, { packages, pid: ctx.pid });
825
+ if (result.failed.length > 0) {
826
+ ctx.stderr.write('\x1b[31mFailed: ' + result.failed.join(', ') + '\x1b[0m\n');
827
+ }
828
+ // [HONEST INSTALL MESSAGE P0a] Yellow + "(N failed, see above)"
829
+ // when partial. Green only when failed.length === 0. Pre-fix the
830
+ // green line printed unconditionally — see user transcript line
831
+ // 831 ("added 264 packages" with 353 silent failures above).
832
+ const partial = result.failed.length > 0;
833
+ const color = partial ? '\x1b[33m' : '\x1b[32m';
834
+ const suffix = partial ? ` (${result.failed.length} failed, see above)` : '';
835
+ ctx.stdout.write(`\n${color}added ${result.installed.length} packages (${result.totalFiles} files) in ${(result.elapsed / 1000).toFixed(1)}s${suffix}\x1b[0m\n`);
836
+ if (result.cachedHits > 0) {
837
+ ctx.stdout.write(`\x1b[2m (${result.cachedHits} from cache)\x1b[0m\n`);
838
+ }
839
+ return result.failed.length > 0 ? 1 : 0;
840
+ });
841
+ HeredocHandler.install(shell, terminal);
842
+ // ── Readline-parity keybindings (Ctrl+K, Ctrl+W, Alt+B, Alt+F, Alt+D,
843
+ // Ctrl+Y, Ctrl+T, Ctrl+L, Ctrl+R, Alt+. , Ctrl+←/→, Alt+←/→, Linux
844
+ // Home/End variants, Ctrl+B/F/N/P, …). Installed AFTER Heredoc so
845
+ // its handleInput wrapper takes precedence when heredoc mode is
846
+ // active. ──
847
+ LineEditorExtender.install(shell, terminal);
848
+ // ── Wire npm/npx with shellExecute ──
849
+ const shellExecute = async (cmd, cmdCtx) => {
850
+ const stdin = cmdCtx.stdin && cmdCtx.stdin !== cmdCtx.terminalStdin
851
+ ? await cmdCtx.stdin.readAll()
852
+ : undefined;
853
+ const result = await shell.execute(cmd, {
854
+ cwd: cmdCtx.cwd,
855
+ env: cmdCtx.env,
856
+ onStdout: textSink((d) => cmdCtx.stdout.write(d)),
857
+ onStderr: textSink((d) => cmdCtx.stderr.write(d)),
858
+ stdin,
859
+ terminalStdin: cmdCtx.terminalStdin,
860
+ // `npm run x` runs x on npm's own fds. Handing the nested execution a
861
+ // sink without saying where that sink leads would make every fd look
862
+ // redirected, and a runtime whose stdout is redirected stops streaming.
863
+ terminalFds: {
864
+ stdin: cmdCtx.isFdTerminal?.(0) ?? false,
865
+ stdout: cmdCtx.isFdTerminal?.(1) ?? false,
866
+ stderr: cmdCtx.isFdTerminal?.(2) ?? false,
867
+ },
868
+ commandContext: {
869
+ pid: cmdCtx.pid,
870
+ cred: cmdCtx.cred,
871
+ setUmask: cmdCtx.setUmask,
872
+ },
873
+ runAs: runAsProcess,
874
+ });
875
+ return result.exitCode;
876
+ };
877
+ const shellEntrypointExecutor = {
878
+ execute: async (cmd, options) => {
879
+ const parentPid = options?.commandContext?.['pid'];
880
+ if (typeof parentPid !== 'number') {
881
+ throw new Error('shell entrypoint requires a parent process');
882
+ }
883
+ const childProcess = self.processes.spawn('sh', ['sh'], options?.cwd || '/home/user', { parentPid });
884
+ let exitCode = 1;
885
+ try {
886
+ const identity = commandIdentityFor(childProcess.pid);
887
+ const terminal = new HeadlessTerminal();
888
+ const childShell = new Shell(terminal, workspace.filesystem, registry, { ...env, ...(options?.env || {}) }, processRegistry, identity);
889
+ HeredocHandler.install(childShell, terminal);
890
+ if (options?.cwd)
891
+ childShell.setCwd(options.cwd);
892
+ const result = await childShell.execute(cmd, {
893
+ ...options,
894
+ commandContext: {
895
+ ...options?.commandContext,
896
+ pid: identity.pid,
897
+ cred: identity.cred,
898
+ setUmask: identity.setUmask,
899
+ },
900
+ runAs: runAsProcess,
901
+ });
902
+ exitCode = result.exitCode;
903
+ return result;
904
+ }
905
+ finally {
906
+ self.processes.exit(childProcess.pid, exitCode);
907
+ }
908
+ },
909
+ };
910
+ registerShellEntrypointCommands(registry, shellEntrypointExecutor);
911
+ // Shell scripts that execute through the local shell still need the same
912
+ // process-table and log-store contract as facet-backed processes.
913
+ const shellExecuteTracked = async (cmd, cmdCtx, opts = {}) => {
914
+ const entry = self.processes.spawn(cmd, [cmd], cmdCtx.cwd || '/home/user', { parentPid: cmdCtx.pid });
915
+ const pid = entry.pid;
916
+ if (opts.longRunning)
917
+ self.processes.setLongRunning(pid);
918
+ const startedAt = Date.now();
919
+ // Spawn banner — matches facet-manager.ts onSpawn format.
920
+ if (terminal) {
921
+ const label = opts.longRunning ? 'started (long-running)' : 'started';
922
+ terminal.write(`\x1b[2m[shell ${label}: pid=${pid} cmd="${cmd}"]\x1b[0m\r\n`);
923
+ }
924
+ // Structured spawn event for the tabs UI (mirrors the facet-manager
925
+ // onSpawn hook). Long-running shell commands like `vite` and
926
+ // `wrangler dev` trigger auto-open of a log tab.
927
+ notifyTerminalEvent(terminal, {
928
+ type: 'spawn', pid, command: cmd, longRunning: !!opts.longRunning, attachedTty: false,
929
+ });
930
+ // Wrap the caller-supplied streams so every chunk is both displayed
931
+ // AND captured in the ring buffer keyed by this PID.
932
+ const tee = (stream, target) => {
933
+ const toTarget = textSink((text) => target.write(text));
934
+ return (d) => {
935
+ try {
936
+ self.processes.appendOutputBytes(pid, stream, d);
937
+ }
938
+ catch { }
939
+ try {
940
+ toTarget(d);
941
+ }
942
+ catch { }
943
+ };
944
+ };
945
+ let exitCode = 1;
946
+ try {
947
+ const result = await shell.execute(cmd, {
948
+ cwd: cmdCtx.cwd,
949
+ env: cmdCtx.env,
950
+ onStdout: tee('stdout', cmdCtx.stdout),
951
+ onStderr: tee('stderr', cmdCtx.stderr),
952
+ // The tee is a sink, not a redirection: the script's fds are still
953
+ // whatever the caller's were, and a runtime asks that question before
954
+ // deciding whether it may stream past the shell.
955
+ terminalFds: {
956
+ stdin: cmdCtx.isFdTerminal?.(0) ?? false,
957
+ stdout: cmdCtx.isFdTerminal?.(1) ?? false,
958
+ stderr: cmdCtx.isFdTerminal?.(2) ?? false,
959
+ },
960
+ // Single spawn path for long-running handoff: a registry command
961
+ // (vite/wrangler/serve) ADOPTS this wrapper pid via the bin-spawn
962
+ // contract instead of allocating a second one, and suppresses its
963
+ // own `[started (long-running)]` notice.
964
+ commandContext: {
965
+ pid,
966
+ cred: entry.cred,
967
+ setUmask: (mask) => self.processes.setUmask(pid, mask),
968
+ ...(opts.longRunning
969
+ ? {
970
+ __nimbusBinSpawn: {
971
+ skipSpawn: true,
972
+ callerPid: pid,
973
+ command: cmd,
974
+ forceLongRunning: true,
975
+ },
976
+ }
977
+ : {}),
978
+ },
979
+ runAs: runAsProcess,
980
+ });
981
+ exitCode = result.exitCode;
982
+ }
983
+ catch (e) {
984
+ // Surface the error in the terminal and the ring buffer.
985
+ const msg = (e && (e.stack || e.message)) || String(e);
986
+ const line = 'shellExecuteTracked error: ' + msg + '\n';
987
+ try {
988
+ self.processes.appendOutput(pid, 'stderr', line);
989
+ }
990
+ catch { }
991
+ try {
992
+ cmdCtx.stderr.write(line);
993
+ }
994
+ catch { }
995
+ exitCode = 1;
996
+ }
997
+ finally {
998
+ // When a long-running script handed off to a live server (the registry
999
+ // command adopted this pid and returned 0), the process stays running;
1000
+ // emitting an immediate exit would print a false `[shell exited]` and
1001
+ // flip the process to terminated in /api/processes. Mirror the npm-bin
1002
+ // resolver's `handedOffToLongRunningFacet` contract.
1003
+ const handedOffToLongRunningFacet = opts.longRunning === true && exitCode === 0;
1004
+ if (!handedOffToLongRunningFacet) {
1005
+ try {
1006
+ self.processes.exit(pid, exitCode);
1007
+ }
1008
+ catch { }
1009
+ try {
1010
+ if (!self.processes.getExit(pid)) {
1011
+ self.processes.markExit(pid, exitCode);
1012
+ }
1013
+ }
1014
+ catch { }
1015
+ // Structured exit for the tabs UI. Always fires (the UI doesn't
1016
+ // know which tabs are open, and client-side dedupe is trivial).
1017
+ // Include the command so the UI can backfill a tab for pids it
1018
+ // never saw a spawn event for (e.g. evals routed past onSpawn).
1019
+ notifyTerminalEvent(terminal, { type: 'exit', pid, code: exitCode, command: cmd });
1020
+ // Keep shell execution diagnostics on the same session helper used by
1021
+ // the rest of the process subsystem.
1022
+ try {
1023
+ self._emitShellExecDone(pid, cmd, exitCode, Date.now() - startedAt);
1024
+ }
1025
+ catch { }
1026
+ }
1027
+ }
1028
+ return exitCode;
1029
+ };
1030
+ const runtimeCommandHint = createRuntimeCommandHintResolver(self.env);
1031
+ installNpmBinFallbackResolver(registry, {
1032
+ vfs: kernelFs,
1033
+ getCwd: () => shell?.cwd || '/home/user',
1034
+ processes: self.processes,
1035
+ getFacetManager: () => {
1036
+ self.ensureFacetManager();
1037
+ return facetMgr;
1038
+ },
1039
+ terminal: terminal,
1040
+ notifyTerminalEvent: (event) => notifyTerminalEvent(terminal, event),
1041
+ runtimeCommandHint,
1042
+ emitShellExecDone: (pid, command, exitCode, durationMs) => {
1043
+ try {
1044
+ self._emitShellExecDone(pid, command, exitCode, durationMs);
1045
+ }
1046
+ catch { }
1047
+ },
1048
+ });
1049
+ // Register core npm — the install work itself goes through the port,
1050
+ // which owns the NpmInstaller, prefix dirs and bin materialisation.
1051
+ const coreNpmCmd = createNpmCommand(registry, shellExecute, kernel, {
1052
+ installer: createNpmInstallPort(self),
1053
+ });
1054
+ registry.register('npm', async (ctx) => {
1055
+ const args = ctx.args || [];
1056
+ const sub = args[0];
1057
+ const cwdKey = normalizeVfsPath(ctx.cwd || '/home/user');
1058
+ // npm run <script> / npm test / npm start — parse package.json and execute
1059
+ if (sub === 'run' || sub === 'run-script' || sub === 'test' || sub === 'start') {
1060
+ const scriptName = sub === 'test' ? 'test' : sub === 'start' ? 'start' : args[1];
1061
+ if (!scriptName) {
1062
+ // npm run (no script) — list available scripts
1063
+ const pkgPath = cwdKey + '/package.json';
1064
+ try {
1065
+ const pkg = JSON.parse(kernelFs.readFileString(pkgPath));
1066
+ if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
1067
+ ctx.stdout.write('Lifecycle scripts:\n');
1068
+ for (const [name, cmd] of Object.entries(pkg.scripts)) {
1069
+ ctx.stdout.write(` ${name}\n ${cmd}\n`);
1070
+ }
1071
+ }
1072
+ else {
1073
+ ctx.stdout.write('No scripts found in package.json\n');
1074
+ }
1075
+ }
1076
+ catch {
1077
+ ctx.stderr.write('npm ERR! no package.json found\n');
1078
+ return 1;
1079
+ }
1080
+ return 0;
1081
+ }
1082
+ const pkgPath = cwdKey + '/package.json';
1083
+ try {
1084
+ const pkg = JSON.parse(kernelFs.readFileString(pkgPath));
1085
+ const script = pkg.scripts?.[scriptName];
1086
+ if (!script) {
1087
+ ctx.stderr.write(`npm ERR! Missing script: "${scriptName}"\n`);
1088
+ if (pkg.scripts) {
1089
+ ctx.stderr.write('npm ERR! Available scripts:\n');
1090
+ for (const name of Object.keys(pkg.scripts))
1091
+ ctx.stderr.write(` - ${name}\n`);
1092
+ }
1093
+ return 1;
1094
+ }
1095
+ // ── node_modules preflight ────────────────────────────────────
1096
+ // If the script invokes a known bundler/framework CLI (vite, next,
1097
+ // webpack, tsc, ...) and node_modules is missing, HARD-FAIL before
1098
+ // running it — the tool would crash with a cryptic "command not
1099
+ // found" / "cannot find package" error that's less helpful.
1100
+ // For custom/unknown scripts (e.g. `echo hi`), emit a warning but
1101
+ // continue — the user's intent might not need deps at all.
1102
+ // Bypass with --force / --no-install-check in the script args, or
1103
+ // by setting NIMBUS_SKIP_INSTALL_CHECK=1 in env.
1104
+ const scriptArgs = args.slice(sub === 'run' || sub === 'run-script' ? 2 : 1);
1105
+ const bypassRunCheck = scriptArgs.includes('--force') ||
1106
+ scriptArgs.includes('--no-install-check') ||
1107
+ ctx.env?.NIMBUS_SKIP_INSTALL_CHECK === '1';
1108
+ if (!bypassRunCheck) {
1109
+ const guard = checkNodeModulesGuard(kernelFs, cwdKey);
1110
+ if (guard.missing) {
1111
+ const bundler = detectBundlerBin(script);
1112
+ if (bundler) {
1113
+ // Hard fail: script needs a bundler binary that lives in node_modules/.bin.
1114
+ ctx.stderr.write('\x1b[31m\u2718\x1b[0m \x1b[1mnode_modules/ not found\x1b[0m — ' +
1115
+ `script "${scriptName}" runs \x1b[36m${bundler}\x1b[0m which needs installed dependencies ` +
1116
+ `(${guard.depCount} declared).\n` +
1117
+ ' Run \x1b[36mnpm install\x1b[0m first,\n' +
1118
+ ' or re-run with \x1b[36mnpm run ' + scriptName + ' -- --force\x1b[0m to skip this check.\n');
1119
+ return 1;
1120
+ }
1121
+ // Soft warning: script might not need deps; let it try.
1122
+ ctx.stderr.write('\x1b[33m\u26A0\x1b[0m node_modules/ not found (' + guard.depCount + ' deps declared) — ' +
1123
+ 'proceeding anyway. Run \x1b[36mnpm install\x1b[0m if the script fails.\n\n');
1124
+ }
1125
+ }
1126
+ ctx.stdout.write(`\n> ${pkg.name || 'project'}@${pkg.version || '1.0.0'} ${scriptName}\n`);
1127
+ ctx.stdout.write(`> ${script}\n\n`);
1128
+ // ── Next.js loud-block (was W11 per-framework substrate) ──────
1129
+ //
1130
+ // runtime primitive support (P12): the previous wave shipped this as a
1131
+ // 45-line standalone module at src/frameworks/next.ts that
1132
+ // exported nothing but the strings used here. Per the
1133
+ // "no per-framework substrate" rule, the message is inlined
1134
+ // (the only thing the module did was hold these strings) and
1135
+ // the file is deleted.
1136
+ //
1137
+ // Next.js dev/start/build/export all need the same pipeline —
1138
+ // custom http.Server + child_process.fork with v8-IPC +
1139
+ // webpack/Turbopack — none of which Nimbus ships. We surface a
1140
+ // deterministic refusal rather than letting the script hang or
1141
+ // (build) reset the isolate during spawn. The block remains a
1142
+ // one-off symbol-detection guard, NOT a generic per-framework
1143
+ // code path. Any future similar guard belongs alongside this
1144
+ // one — not in its own src/frameworks/<name>.ts file.
1145
+ const refused = refusedNextSubcommand(scriptName, script, pkg, scriptArgs);
1146
+ if (refused) {
1147
+ ctx.stderr.write(NEXT_REFUSAL_MESSAGE);
1148
+ return 127;
1149
+ }
1150
+ const scriptTrim = script.trim();
1151
+ const longRunning = scriptName === 'dev' || scriptName === 'start' ||
1152
+ scriptName === 'serve' || scriptName === 'watch';
1153
+ return await shellExecuteTracked(scriptTrim, {
1154
+ ...ctx,
1155
+ env: { ...ctx.env, npm_lifecycle_event: scriptName, npm_package_name: pkg.name || '' },
1156
+ }, { longRunning });
1157
+ }
1158
+ catch (e) {
1159
+ ctx.stderr.write(`npm ERR! ${e?.message || e}\n`);
1160
+ return 1;
1161
+ }
1162
+ }
1163
+ // npm ls — list installed packages
1164
+ if (sub === 'ls' || sub === 'list') {
1165
+ const pkgPath = cwdKey + '/package.json';
1166
+ const nmDir = cwdKey + '/node_modules';
1167
+ try {
1168
+ const pkg = JSON.parse(kernelFs.readFileString(pkgPath));
1169
+ ctx.stdout.write(`${pkg.name || 'project'}@${pkg.version || '1.0.0'} ${ctx.cwd}\n`);
1170
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
1171
+ const names = Object.keys(deps);
1172
+ for (let i = 0; i < names.length; i++) {
1173
+ const isLast = i === names.length - 1;
1174
+ const prefix = isLast ? '└── ' : '├── ';
1175
+ const name = names[i];
1176
+ let version = deps[name];
1177
+ // Try to read actual installed version
1178
+ try {
1179
+ const installed = JSON.parse(kernelFs.readFileString(nmDir + '/' + name + '/package.json'));
1180
+ version = installed.version;
1181
+ }
1182
+ catch { }
1183
+ ctx.stdout.write(`${prefix}${name}@${version}\n`);
1184
+ }
1185
+ }
1186
+ catch {
1187
+ ctx.stderr.write('npm ERR! no package.json found\n');
1188
+ return 1;
1189
+ }
1190
+ return 0;
1191
+ }
1192
+ // npm init / npm init -y
1193
+ if (sub === 'init') {
1194
+ const cwd = cwdKey;
1195
+ const pkgPath = cwd + '/package.json';
1196
+ if (kernelFs.exists(pkgPath) && !args.includes('-y') && !args.includes('--yes')) {
1197
+ ctx.stderr.write('package.json already exists. Use -y to overwrite.\n');
1198
+ return 1;
1199
+ }
1200
+ const name = cwd.split('/').pop() || 'project';
1201
+ const pkg = {
1202
+ name, version: '1.0.0', description: '', main: 'index.js',
1203
+ type: 'module',
1204
+ scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview', test: 'echo "no test"' },
1205
+ keywords: [], author: '', license: 'MIT', dependencies: {}, devDependencies: {},
1206
+ };
1207
+ kernelFs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
1208
+ ctx.stdout.write('Wrote to ' + pkgPath + '\n');
1209
+ return 0;
1210
+ }
1211
+ // npm uninstall <pkg>
1212
+ if (sub === 'uninstall' || sub === 'un' || sub === 'remove' || sub === 'rm') {
1213
+ const packages = args.slice(1).filter(a => !a.startsWith('-'));
1214
+ if (packages.length === 0) {
1215
+ ctx.stderr.write('Usage: npm uninstall <pkg>\n');
1216
+ return 1;
1217
+ }
1218
+ const nmDir = cwdKey + '/node_modules';
1219
+ for (const pkg of packages) {
1220
+ const pkgDir = nmDir + '/' + pkg;
1221
+ // Recursively delete package directory
1222
+ const deleteRecursive = (dir) => {
1223
+ try {
1224
+ for (const e of kernelFs.readdir(dir)) {
1225
+ const fp = dir + '/' + e.name;
1226
+ if (e.type === 'directory')
1227
+ deleteRecursive(fp);
1228
+ else
1229
+ try {
1230
+ kernelFs.unlink(fp);
1231
+ }
1232
+ catch { }
1233
+ }
1234
+ try {
1235
+ kernelFs.rmdir(dir);
1236
+ }
1237
+ catch { }
1238
+ }
1239
+ catch { }
1240
+ };
1241
+ deleteRecursive(pkgDir);
1242
+ ctx.stdout.write('removed ' + pkg + '\n');
1243
+ }
1244
+ // Update package.json
1245
+ const pkgPath = cwdKey + '/package.json';
1246
+ try {
1247
+ const pkgJson = JSON.parse(kernelFs.readFileString(pkgPath));
1248
+ for (const pkg of packages) {
1249
+ delete pkgJson.dependencies?.[pkg];
1250
+ delete pkgJson.devDependencies?.[pkg];
1251
+ }
1252
+ kernelFs.writeFile(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
1253
+ }
1254
+ catch { }
1255
+ return 0;
1256
+ }
1257
+ // ── npm create <pkg> / npm init <pkg> → npx create-<pkg> ─────────
1258
+ //
1259
+ // Per the npm spec, `npm create X args...` and `npm init X args...`
1260
+ // (when X is supplied) are sugar for invoking the `create-X`
1261
+ // initializer package via npx. Specifically:
1262
+ //
1263
+ // npm create foo args... → npx create-foo args...
1264
+ // npm create foo@1.2 args... → npx create-foo@1.2 args...
1265
+ // npm create @scope/foo args... → npx @scope/create-foo args...
1266
+ // npm create @scope args... → npx @scope/create args...
1267
+ //
1268
+ // `npm init` (no args) is a different beast — it scaffolds a
1269
+ // package.json interactively. The `sub === 'init'` branch above
1270
+ // handles the no-arg case; here we only intercept the
1271
+ // initializer-package case (1+ args after `init`).
1272
+ //
1273
+ // Without this routing, `npm create vite@latest mvp -- --template
1274
+ // react-ts` hits the base npm dispatch which only knows
1275
+ // {init, install/i/add, uninstall/remove/rm/un, list/ls,
1276
+ // run/run-script, start, test, info/view/show, search, version}
1277
+ // — and emits "npm: unknown command 'create'". Every modern
1278
+ // framework's `create-*` flow (create-vite, create-next-app
1279
+ // routed via "npm create", create-cloudflare, create-astro, etc.)
1280
+ // depends on this.
1281
+ //
1282
+ // This is a primitive: one fix, every framework wins.
1283
+ if (sub === 'create' || (sub === 'init' && args.length >= 2 && !args[1].startsWith('-'))) {
1284
+ const arg1 = args[1];
1285
+ if (!arg1) {
1286
+ ctx.stderr.write('npm create: missing package name\n');
1287
+ ctx.stderr.write('Usage: npm create <pkg> [args...]\n');
1288
+ return 1;
1289
+ }
1290
+ // Parse pkg + version. Scope-aware:
1291
+ // @scope → @scope/create
1292
+ // @scope/foo → @scope/create-foo
1293
+ // foo → create-foo
1294
+ // foo@1.2.3 → create-foo@1.2.3
1295
+ // foo@latest → create-foo@latest
1296
+ function rewriteToCreatePkg(spec) {
1297
+ // Strip an optional version range and re-append after the rewrite.
1298
+ const atIdx = spec.lastIndexOf('@');
1299
+ const hasVersion = atIdx > 0; // a leading @ is the scope; not a version
1300
+ const bare = hasVersion ? spec.slice(0, atIdx) : spec;
1301
+ const version = hasVersion ? spec.slice(atIdx) : '';
1302
+ let pkg;
1303
+ if (bare.startsWith('@')) {
1304
+ const slash = bare.indexOf('/');
1305
+ if (slash < 0) {
1306
+ // @scope → @scope/create
1307
+ pkg = bare + '/create';
1308
+ }
1309
+ else {
1310
+ // @scope/foo → @scope/create-foo
1311
+ const scope = bare.slice(0, slash);
1312
+ const name = bare.slice(slash + 1);
1313
+ pkg = scope + '/create-' + name;
1314
+ }
1315
+ }
1316
+ else {
1317
+ pkg = 'create-' + bare;
1318
+ }
1319
+ return pkg + version;
1320
+ }
1321
+ const createPkg = rewriteToCreatePkg(arg1);
1322
+ const passThrough = args.slice(2);
1323
+ // `npm create` accepts an optional `--` separator to push the
1324
+ // remaining args to the create script; npx doesn't need a
1325
+ // separator (positional args after the package name go to the
1326
+ // package). We strip a single literal `--` token if present so
1327
+ // `npm create vite@latest mvp -- --template react-ts` becomes
1328
+ // `npx --yes create-vite@latest mvp --template react-ts`.
1329
+ const stripped = passThrough.filter((a, i, arr) => !(a === '--' && i < arr.length - 1) && !(a === '--' && arr.indexOf('--') === i));
1330
+ // Inform the user what we're routing to — matches npm's own
1331
+ // visible "npx" line so the create flow is honest.
1332
+ ctx.stdout.write(`> npx --yes ${createPkg}${stripped.length ? ' ' + stripped.join(' ') : ''}\n`);
1333
+ // Dispatch through the npx registry entry. `--yes` skips the
1334
+ // "Ok to proceed? (y)" prompt.
1335
+ const npxHandler = await registry.resolve('npx');
1336
+ if (!npxHandler) {
1337
+ ctx.stderr.write('npm create: npx command unavailable\n');
1338
+ return 1;
1339
+ }
1340
+ return await npxHandler({
1341
+ ...ctx,
1342
+ args: ['--yes', createPkg, ...stripped],
1343
+ });
1344
+ }
1345
+ // Fall through to core npm for other subcommands
1346
+ return coreNpmCmd(ctx);
1347
+ });
1348
+ // npx: check registered commands first, then resolve/install through
1349
+ // Nimbus's NpmInstaller and execute the package bin via the Node runtime.
1350
+ registry.register('npx', async (ctx) => {
1351
+ const npxArgs = ctx.args || [];
1352
+ const { describeNpxSelfInvocation, formatNpxHelp, getNpxCommandArgs, getNpxCommandWord, resolveNpxBinary, } = await import('../npm/npx-install.js');
1353
+ const selfInvocation = describeNpxSelfInvocation(npxArgs);
1354
+ if (selfInvocation === 'missing') {
1355
+ ctx.stderr.write('Usage: npx <command> [args...]\n');
1356
+ return 1;
1357
+ }
1358
+ if (selfInvocation === 'version') {
1359
+ ctx.stdout.write(NPM_VERSION + '\n');
1360
+ return 0;
1361
+ }
1362
+ if (selfInvocation === 'help') {
1363
+ ctx.stdout.write(formatNpxHelp());
1364
+ return 0;
1365
+ }
1366
+ const cmd = getNpxCommandWord(npxArgs);
1367
+ // Check if it's a built-in command (vite, esbuild, etc.)
1368
+ const resolved = cmd ? await registry.resolve(cmd) : null;
1369
+ if (resolved) {
1370
+ return await resolved({ ...ctx, args: getNpxCommandArgs(npxArgs) });
1371
+ }
1372
+ // Nimbus-native npx install + run path. Routes package installation
1373
+ // through NpmInstaller's full-packument resolver.
1374
+ await self.ensureNpmInstaller((msg) => ctx.stdout.write('[npm] ' + msg + '\n'));
1375
+ self.ensureSqliteFs();
1376
+ const installer = self.npmInstaller;
1377
+ const resolveResult = await resolveNpxBinary(installer, sqliteFs.as(requireVfsCred(ctx.cred, 'npx')), ctx.cwd || '/home/user', npxArgs, (msg) => ctx.stdout.write(msg + '\n'), ctx.pid);
1378
+ if (resolveResult.ok && resolveResult.binPath) {
1379
+ const nodeCmd = await registry.resolve('node');
1380
+ if (nodeCmd) {
1381
+ return await nodeCmd({
1382
+ ...ctx,
1383
+ args: [resolveResult.binPath, ...(resolveResult.binArgs || [])],
1384
+ __nimbusBundleProfile: resolveResult.bundleProfile,
1385
+ });
1386
+ }
1387
+ ctx.stderr.write('npx: node runtime is not registered\n');
1388
+ return 1;
1389
+ }
1390
+ ctx.stderr.write((resolveResult.error || 'npx: could not resolve binary') + '\n');
1391
+ return 1;
1392
+ });
1393
+ // ── Register process commands (enhanced with facet process tracking) ──
1394
+ registry.register('ps', async (ctx) => {
1395
+ // Pids are generation-strided (see PID_GEN_STRIDE) so they can be 7+
1396
+ // digits; size the column to the widest pid in this listing.
1397
+ const procs = self.processes.getAll();
1398
+ const pidWidth = Math.max(3, ...procs.map((p) => String(p.pid).length));
1399
+ ctx.stdout.write(` ${'PID'.padStart(pidWidth)} STATUS COMMAND\n`);
1400
+ for (const proc of procs) {
1401
+ // Prefer log-store exit info over ProcessTable's: the store has
1402
+ // the authoritative code and survives reap. For `running`, rely
1403
+ // on ProcessTable (store has no "running" concept).
1404
+ let status;
1405
+ if (proc.state === 'running') {
1406
+ status = '\x1b[32mrunning\x1b[0m';
1407
+ }
1408
+ else if (proc.state === 'killed') {
1409
+ status = `\x1b[33mkilled(${proc.exitCode ?? 137})\x1b[0m`;
1410
+ }
1411
+ else {
1412
+ // 'exited' — distinguish clean vs crashed.
1413
+ const code = proc.exitCode ?? 0;
1414
+ status = code === 0
1415
+ ? `\x1b[2mexited(0)\x1b[0m`
1416
+ : `\x1b[31mcrashed(${code})\x1b[0m`;
1417
+ }
1418
+ ctx.stdout.write(` ${String(proc.pid).padStart(pidWidth)} ${status.padEnd(26)} ${proc.command}\n`);
1419
+ }
1420
+ // Show vite dev server
1421
+ if (self.viteDevServer?.isRunning) {
1422
+ ctx.stdout.write(` \x1b[33m${'---'.padStart(pidWidth)}\x1b[0m \x1b[32mrunning\x1b[0m vite dev server (${self.viteBasePath}/)\n`);
1423
+ }
1424
+ if (self.processes.getAll().length === 0 && !self.viteDevServer?.isRunning) {
1425
+ ctx.stdout.write(' (no processes)\n');
1426
+ }
1427
+ return 0;
1428
+ });
1429
+ // ── `logs <pid>` — tail per-process ring buffer ──
1430
+ // Flags:
1431
+ // -f / --follow stream new chunks until the process exits
1432
+ // -n / --lines N number of lines from the tail (default 200)
1433
+ // --bytes N max bytes from the tail (overrides --lines)
1434
+ // --plain strip ANSI escapes on output (keeps buffer raw)
1435
+ registry.register('logs', async (ctx) => {
1436
+ const args = ctx.args || [];
1437
+ const follow = args.includes('-f') || args.includes('--follow');
1438
+ const plain = args.includes('--plain');
1439
+ let lines = 200;
1440
+ let bytes;
1441
+ for (let i = 0; i < args.length; i++) {
1442
+ const a = args[i];
1443
+ if ((a === '-n' || a === '--lines') && args[i + 1]) {
1444
+ const n = parseInt(args[i + 1], 10);
1445
+ if (!isNaN(n) && n > 0)
1446
+ lines = n;
1447
+ i++;
1448
+ }
1449
+ else if (a === '--bytes' && args[i + 1]) {
1450
+ const n = parseInt(args[i + 1], 10);
1451
+ if (!isNaN(n) && n > 0)
1452
+ bytes = n;
1453
+ i++;
1454
+ }
1455
+ }
1456
+ const pidArg = args.find(a => /^\d+$/.test(a));
1457
+ if (!pidArg) {
1458
+ ctx.stderr.write('usage: logs [-f] [-n LINES | --bytes N] [--plain] <pid>\n');
1459
+ return 1;
1460
+ }
1461
+ const pid = parseInt(pidArg, 10);
1462
+ if (!self.processes.hasLogs(pid)) {
1463
+ ctx.stderr.write(`no logs for pid ${pid}\n`);
1464
+ return 1;
1465
+ }
1466
+ // Paint a single chunk for live-stream (follow-mode) rendering.
1467
+ // `--plain` strips ANSI per chunk — safe for live output because
1468
+ // individual streamed chunks from the RPC layer never split an
1469
+ // escape sequence (the RPC boundary always delivers a complete
1470
+ // write call). Backfill is different (see below).
1471
+ const renderChunk = (c) => {
1472
+ let data = c.data;
1473
+ if (plain)
1474
+ data = stripAnsi(data);
1475
+ if (c.stream === 'stderr' && !plain) {
1476
+ return `\x1b[31m${data}\x1b[0m`;
1477
+ }
1478
+ return data;
1479
+ };
1480
+ // Backfill. Concatenate same-stream consecutive chunks BEFORE
1481
+ // stripping so that any ANSI escape split across chunk boundaries
1482
+ // (by the 4 KB splitter inside ProcessLogStore) gets rejoined and
1483
+ // stripped cleanly instead of leaking `1m` / `[31m` fragments.
1484
+ const tailOpts = bytes !== undefined ? { bytes } : { lines };
1485
+ const chunks = self.processes.tailLogs(pid, tailOpts);
1486
+ let group = [];
1487
+ const flushGroup = () => {
1488
+ if (group.length === 0)
1489
+ return;
1490
+ const stream = group[0].stream;
1491
+ let data = group.map(c => c.data).join('');
1492
+ if (plain)
1493
+ data = stripAnsi(data);
1494
+ if (stream === 'stderr' && !plain) {
1495
+ ctx.stdout.write(`\x1b[31m${data}\x1b[0m`);
1496
+ }
1497
+ else {
1498
+ ctx.stdout.write(data);
1499
+ }
1500
+ group = [];
1501
+ };
1502
+ for (const c of chunks) {
1503
+ if (group.length > 0 && group[group.length - 1].stream !== c.stream) {
1504
+ flushGroup();
1505
+ }
1506
+ group.push(c);
1507
+ }
1508
+ flushGroup();
1509
+ if (!follow) {
1510
+ // Footer only when process has exited already.
1511
+ const exit = self.processes.getExit(pid);
1512
+ if (exit) {
1513
+ ctx.stdout.write(`\r\n\x1b[2m[process exited with code ${exit.code}${exit.reason ? ` (${exit.reason})` : ''}]\x1b[0m\r\n`);
1514
+ }
1515
+ return 0;
1516
+ }
1517
+ // Follow mode: subscribe to live appends, poll for exit.
1518
+ const entry = self.processes.get(pid);
1519
+ const alreadyExited = !entry || entry.state !== 'running' || self.processes.getExit(pid);
1520
+ if (alreadyExited) {
1521
+ const exit = self.processes.getExit(pid);
1522
+ if (exit) {
1523
+ ctx.stdout.write(`\r\n\x1b[2m[process exited with code ${exit.code}${exit.reason ? ` (${exit.reason})` : ''}]\x1b[0m\r\n`);
1524
+ }
1525
+ return 0;
1526
+ }
1527
+ return await new Promise((resolve) => {
1528
+ let done = false;
1529
+ const finish = (code) => {
1530
+ if (done)
1531
+ return;
1532
+ done = true;
1533
+ unsub();
1534
+ unsubExit();
1535
+ resolve(code);
1536
+ };
1537
+ const unsub = self.processes.subscribeLogs(pid, (c) => {
1538
+ ctx.stdout.write(renderChunk(c));
1539
+ });
1540
+ const unsubExit = self.processes.subscribeExit(pid, (exit) => {
1541
+ ctx.stdout.write(`\r\n\x1b[2m[process exited with code ${exit.code}${exit.reason ? ` (${exit.reason})` : ''}]\x1b[0m\r\n`);
1542
+ finish(0);
1543
+ });
1544
+ // TOCTOU: the process may have exited between our `alreadyExited`
1545
+ // check above and these subscribe calls. Re-check now that the
1546
+ // exit subscriber is wired — if exit already set, the subscribe
1547
+ // callback never fires, so synthesize the footer ourselves.
1548
+ const exitNow = self.processes.getExit(pid);
1549
+ if (exitNow) {
1550
+ ctx.stdout.write(`\r\n\x1b[2m[process exited with code ${exitNow.code}${exitNow.reason ? ` (${exitNow.reason})` : ''}]\x1b[0m\r\n`);
1551
+ finish(0);
1552
+ return;
1553
+ }
1554
+ // If ctx exposes an AbortSignal (Ctrl+C wired by the shell),
1555
+ // honor it. Otherwise, follow-mode ends only on process exit.
1556
+ if (ctx.signal && typeof ctx.signal.addEventListener === 'function') {
1557
+ ctx.signal.addEventListener('abort', () => finish(130));
1558
+ }
1559
+ });
1560
+ });
1561
+ registry.register('jobs', async (ctx) => {
1562
+ const running = self.processes.getRunning();
1563
+ if (running.length === 0 && !self.viteDevServer?.isRunning) {
1564
+ ctx.stdout.write('No background jobs.\n');
1565
+ return 0;
1566
+ }
1567
+ for (let i = 0; i < running.length; i++) {
1568
+ ctx.stdout.write(`[${i + 1}] Running ${running[i].command} (pid ${running[i].pid})\n`);
1569
+ }
1570
+ if (self.viteDevServer?.isRunning) {
1571
+ ctx.stdout.write(`[${running.length + 1}] Running vite dev server\n`);
1572
+ }
1573
+ return 0;
1574
+ });
1575
+ const shellKillCommand = createKillCommand(processRegistry);
1576
+ registry.register('kill', async (ctx) => {
1577
+ const pidArg = ctx.args[0];
1578
+ if (!pidArg) {
1579
+ ctx.stderr.write('Usage: kill <pid>\n');
1580
+ return 1;
1581
+ }
1582
+ if (pidArg.startsWith('-') || pidArg.startsWith('%')) {
1583
+ return shellKillCommand(ctx);
1584
+ }
1585
+ const pid = parseInt(pidArg);
1586
+ if (isNaN(pid)) {
1587
+ ctx.stderr.write('kill: invalid pid\n');
1588
+ return 1;
1589
+ }
1590
+ if (processRegistry.get(pid)) {
1591
+ return shellKillCommand(ctx);
1592
+ }
1593
+ // runtime primitive support (P11): if the target is the vite shim PID
1594
+ // (registered by P5's long-running spawn), tear down the
1595
+ // in-process viteDevServer / cirrusReal too — facetManager.kill
1596
+ // only handles real Worker-Loader facets, and would leave the
1597
+ // shim running with its port registered against a dead PID.
1598
+ if (self._viteShimPid === pid) {
1599
+ try {
1600
+ if (self.cirrusReal?.isRunning) {
1601
+ self.cirrusReal.stop(self.ctx);
1602
+ self.cirrusReal = null;
1603
+ }
1604
+ if (self.viteDevServer?.isRunning) {
1605
+ self.viteDevServer.stop();
1606
+ self.viteDevServer = null;
1607
+ try {
1608
+ await self.ctx.storage.delete(VITE_CONFIG_KEY);
1609
+ }
1610
+ catch { }
1611
+ }
1612
+ }
1613
+ catch (e) {
1614
+ ctx.stderr.write('kill: while stopping vite shim: ' + (e?.message || e) + '\n');
1615
+ }
1616
+ try {
1617
+ self.portRegistry.unregisterByPid(pid);
1618
+ }
1619
+ catch { }
1620
+ try {
1621
+ self.processes.kill(pid);
1622
+ }
1623
+ catch { }
1624
+ notifyTerminalEvent(terminal, {
1625
+ type: 'exit', pid, code: 137, command: 'vite',
1626
+ });
1627
+ self._viteShimPid = null;
1628
+ self._viteShimPort = null;
1629
+ ctx.stdout.write(`Process ${pid} killed.\n`);
1630
+ return 0;
1631
+ }
1632
+ if (facetMgr?.kill(pid)) {
1633
+ ctx.stdout.write(`Process ${pid} killed.\n`);
1634
+ return 0;
1635
+ }
1636
+ ctx.stderr.write(`kill: no such process: ${pid}\n`);
1637
+ return 1;
1638
+ });
1639
+ registry.register('top', createTopCommand(processRegistry));
1640
+ registry.register('watch', createWatchCommand(registry));
1641
+ registry.register('help', createHelpCommand(registry));
1642
+ // ── Rehydrate globally-installed npm packages ──
1643
+ try {
1644
+ rehydrateGlobalPackages(kernel.vfs, registry);
1645
+ }
1646
+ catch { }
1647
+ }