@nimbus-sh/worker 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
- package/dist/cirrus-npm-cjs.generated.js +1 -1
- package/dist/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +6 -5
- package/dist/facets/compose.d.ts +148 -0
- package/dist/facets/compose.d.ts.map +1 -0
- package/dist/facets/compose.js +146 -0
- package/dist/facets/durable-images.d.ts +9 -5
- package/dist/facets/durable-images.d.ts.map +1 -1
- package/dist/facets/durable-images.js +17 -7
- package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
- package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
- package/dist/facets/esbuild-bundle-pool.js +122 -0
- package/dist/facets/exec-telemetry.d.ts +8 -0
- package/dist/facets/exec-telemetry.d.ts.map +1 -1
- package/dist/facets/manager.d.ts +240 -109
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +677 -342
- package/dist/facets/process.d.ts +20 -16
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +59 -28
- package/dist/facets/real-vite-fs-shim.js +2 -2
- 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/vite-dev-server.d.ts +25 -47
- package/dist/facets/vite-dev-server.d.ts.map +1 -1
- package/dist/facets/vite-dev-server.js +185 -219
- package/dist/facets/wasm-image-digest.d.ts +25 -0
- package/dist/facets/wasm-image-digest.d.ts.map +1 -0
- package/dist/facets/wasm-image-digest.js +25 -0
- package/dist/facets/wasm-swap-registry.d.ts +51 -92
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
- package/dist/facets/wasm-swap-registry.js +68 -227
- 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/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +6 -5
- package/dist/hosted/commands.d.ts +12 -0
- package/dist/hosted/commands.d.ts.map +1 -0
- package/dist/hosted/commands.js +1658 -0
- package/dist/hosted/runtime.d.ts +114 -0
- package/dist/hosted/runtime.d.ts.map +1 -0
- package/dist/hosted/runtime.js +288 -0
- package/dist/hosted/services.d.ts +40 -0
- package/dist/hosted/services.d.ts.map +1 -0
- package/dist/hosted/services.js +512 -0
- package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-pool.js +3 -2
- package/dist/loaders/generated-workers.d.ts +2 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +3 -3
- package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +34 -108
- package/dist/node-shims-artifact.generated.js +3 -3
- package/dist/npm/install-batch-facet.d.ts +3 -2
- package/dist/npm/install-batch-facet.d.ts.map +1 -1
- package/dist/npm/install-batch-facet.js +45 -15
- package/dist/npm/installer.d.ts +22 -17
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +209 -193
- package/dist/npm/pre-bundle-facet.d.ts +1 -1
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +1 -1
- package/dist/npm/resolve-facet.d.ts +6 -0
- package/dist/npm/resolve-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.d.ts +21 -20
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +85 -84
- package/dist/npm/semver.d.ts +59 -0
- package/dist/npm/semver.d.ts.map +1 -0
- package/dist/npm/semver.js +226 -0
- package/dist/router/remote-api.js +31 -1
- package/dist/runtime/bash-repl.d.ts +1 -1
- package/dist/runtime/bash-repl.d.ts.map +1 -1
- package/dist/runtime/bash-repl.js +38 -4
- package/dist/runtime/bun-runner.js +1 -1
- package/dist/runtime/html-entrypoint.d.ts +7 -0
- package/dist/runtime/html-entrypoint.d.ts.map +1 -1
- package/dist/runtime/html-entrypoint.js +18 -2
- package/dist/runtime/node-runner.d.ts +4 -1
- package/dist/runtime/node-runner.d.ts.map +1 -1
- package/dist/runtime/node-runner.js +9 -2
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +305 -29
- package/dist/runtime/opencode-facet-runner.js +16 -16
- 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 +2 -1
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +82 -59
- 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/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/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/helpers.d.ts +38 -0
- package/dist/session/helpers.d.ts.map +1 -1
- package/dist/session/helpers.js +76 -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-phases.d.ts +1 -1
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +2 -12
- package/dist/session/init.d.ts +15 -36
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +50 -2327
- 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 +77 -34
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +126 -514
- package/dist/session/npm-install-port.d.ts +18 -0
- package/dist/session/npm-install-port.d.ts.map +1 -0
- package/dist/session/npm-install-port.js +37 -0
- package/dist/session/port-capability.d.ts +44 -0
- package/dist/session/port-capability.d.ts.map +1 -1
- package/dist/session/port-capability.js +110 -0
- package/dist/session/programmatic.d.ts +36 -43
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +109 -115
- package/dist/session/routes.d.ts +30 -17
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +42 -131
- package/dist/session/rpc.d.ts +15 -15
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +98 -49
- package/dist/session/serving-port.d.ts +27 -0
- package/dist/session/serving-port.d.ts.map +1 -0
- package/dist/session/serving-port.js +27 -0
- package/dist/session/start-real-vite.d.ts +12 -0
- package/dist/session/start-real-vite.d.ts.map +1 -1
- package/dist/session/start-real-vite.js +4 -4
- package/dist/session/supervisor-op.d.ts +4 -4
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +24 -10
- package/dist/session/supervisor-rpc.d.ts +7 -7
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +4 -0
- package/dist/session/vite-command.d.ts +18 -0
- package/dist/session/vite-command.d.ts.map +1 -0
- package/dist/session/vite-command.js +609 -0
- package/dist/session/ws.d.ts +61 -0
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +122 -12
- package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
- package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
- package/dist/shell/npm-bin-entrypoints.js +12 -85
- package/dist/workspace-host.d.ts +6 -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 +34 -9
- package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
- package/public/index.html +1 -1
- package/scripts/bundle-facet-workers.mjs +1 -1
- package/scripts/bundle-npm-cjs.mjs +2 -2
- package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
- package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
- package/dist/facets/on-demand-bundle-gate.js +0 -125
- package/dist/npm/install-args.d.ts +0 -21
- package/dist/npm/install-args.d.ts.map +0 -1
- package/dist/npm/install-args.js +0 -70
- package/dist/npm/npm-log.d.ts +0 -31
- package/dist/npm/npm-log.d.ts.map +0 -1
- package/dist/npm/npm-log.js +0 -51
package/dist/session/init.js
CHANGED
|
@@ -1,92 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Why this is one big function and not a class:
|
|
5
|
-
* initSession runs once per /ws upgrade and walks the session
|
|
6
|
-
* through Phase R (rehydrate from SQL), Phase B (compose the
|
|
7
|
-
* workspace + register the session's commands), Phase W (attach
|
|
8
|
-
* terminal), and (cold-only) Phase O (MOTD + framework hint). The
|
|
9
|
-
* phases share lots of locals (vfs, kernel, registry, shell) and
|
|
10
|
-
* ordering matters strictly — there's no interesting reuse boundary
|
|
11
|
-
* that a class decomposition would expose.
|
|
12
|
-
*
|
|
13
|
-
* What it no longer does is COMPOSE the operating system. The kernel,
|
|
14
|
-
* the provider mounts, the command registry, the coreutils, the exec
|
|
15
|
-
* resolver, the default environment and the shell come from
|
|
16
|
-
* `NimbusWorkspace` (@nimbus-sh/core/workspace), which an embedder off
|
|
17
|
-
* Cloudflare builds the same way. What stays here is everything that
|
|
18
|
-
* only makes sense with a socket, a Durable Object and a product behind
|
|
19
|
-
* it: the terminal and its scrollback, the phase machine, the persisted
|
|
20
|
-
* shell state, npm, git, vite, wrangler and the facet-backed runtimes.
|
|
21
|
-
*
|
|
22
|
-
* The function is intentionally written so that a reader sees:
|
|
23
|
-
* 1. setPhase('rehydrate') ...
|
|
24
|
-
* 2. setPhase('build') ... (~95% of the LOC)
|
|
25
|
-
* 3. setPhase('online') if (cold)
|
|
26
|
-
* 4. self._b4Phase = 'hydrated'
|
|
27
|
-
*
|
|
28
|
-
* `self` is typed as InitHost, a narrow view of SessionInternal plus readonly
|
|
29
|
-
* ctx/env, so this module can use the session internals it owns without
|
|
30
|
-
* depending on the full Durable Object class surface.
|
|
31
|
-
*
|
|
32
|
-
* Imports and class delegators on NimbusSession preserve back-compat:
|
|
33
|
-
* - acceptShellWebSocket → self.initSession(ws) (S7 will extract).
|
|
34
|
-
* - The class still has `initSession(ws)` as a delegator method.
|
|
35
|
-
*/
|
|
36
|
-
import { Shell, createCurlCommand, createNpmCommand, NPM_VERSION, createTopCommand, createWatchCommand, createHelpCommand, rehydrateGlobalPackages, } from '@nimbus-sh/core/substrate/lifo/index.js';
|
|
37
|
-
import { createKillCommand } from '@nimbus-sh/core/substrate/lifo/commands/system/kill.js';
|
|
1
|
+
import { registerHostedCommands } from '../hosted/commands.js';
|
|
2
|
+
import { runtimeCatalogSource } from '../runtime/runtime-catalog.js';
|
|
38
3
|
import { NimbusWorkspace } from '@nimbus-sh/core/workspace';
|
|
39
|
-
import { CRED_KERNEL
|
|
4
|
+
import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
40
5
|
import { WebSocketTerminal } from '../facets/ws-terminal.js';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import { runBunScript, BUN_VERSION } from '../runtime/bun-runner.js';
|
|
44
|
-
import { buildRuntimeHandler, resolveRuntimeScriptPath, } from '@nimbus-sh/core/runtime/runtime-registry.js';
|
|
45
|
-
import { parseViteConfigSource } from '@nimbus-sh/core/runtime/vite-config-parser.js';
|
|
46
|
-
import { startRealVite } from './start-real-vite.js';
|
|
47
|
-
import { findHtmlScriptEntrypoint, rewriteViteBuildHtml } from '../runtime/html-entrypoint.js';
|
|
48
|
-
import { normalizeVfsPath, parentVfsPath, resolveVfsPath, stripLeadingSlashes } from '@nimbus-sh/core/vfs/path.js';
|
|
49
|
-
import { ViteDevServer } from '../facets/vite-dev-server.js';
|
|
50
|
-
import { shouldUseRealVite } from '../facets/cirrus-real.js';
|
|
51
|
-
import { makeLongRunningPortStub, resolveLongRunningPort, expandArgvShellDefaults, } from '@nimbus-sh/core/runtime/long-running-handle.js';
|
|
52
|
-
import { NimbusWrangler } from '../wrangler/nimbus-wrangler.js';
|
|
53
|
-
import { filterWranglerFlags, detectBundlerBin, checkNodeModulesGuard, detectUnsupportedWranglerConfig, } from './helpers.js';
|
|
54
|
-
import { HeredocHandler, LineEditorExtender } from '@nimbus-sh/core/shell/features.js';
|
|
55
|
-
import { registerShellEntrypointCommands } from '@nimbus-sh/core/shell/shell-entrypoints.js';
|
|
56
|
-
import { makeChshCommand } from '@nimbus-sh/core/substrate/lifo/shell/default-shell.js';
|
|
57
|
-
import { installNpmBinFallbackResolver } from '../shell/npm-bin-entrypoints.js';
|
|
58
|
-
import { parseNpmInstallInvocation } from '../npm/install-args.js';
|
|
59
|
-
import { npmLogEnabled } from '../npm/npm-log.js';
|
|
60
|
-
import { materializeNpmBinShims } from '../npm/bin-links.js';
|
|
61
|
-
import { makeNimbusVerbHandler, createRuntimeCommandHintResolver, } from '../runtime/package-manager.js';
|
|
62
|
-
import { rpcExposeApp, rpcListApps, rpcRemoveApp, rpcRotateLink } from './programmatic.js';
|
|
63
|
-
import { listInstalledRuntimes, rehydrateInstalledRuntimes, registerRunnerFactory, } from '@nimbus-sh/core/runtime/installed-runtimes.js';
|
|
6
|
+
import { parentVfsPath, stripLeadingSlashes } from '@nimbus-sh/core/vfs/path.js';
|
|
7
|
+
import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
|
|
64
8
|
// Runtime factories (clang/python/ruby/bash/wasm) are imported lazily at
|
|
65
9
|
// first-use inside their registered handlers — see the registrations below.
|
|
66
10
|
// Keeping them off the top-level import graph shaves their module-eval cost
|
|
67
11
|
// (zod, embedded socket-kernel/shim sources, wasm loaders) out of the
|
|
68
12
|
// one-time Worker Startup Time paid on every fresh-isolate cold run.
|
|
69
|
-
|
|
70
|
-
|
|
13
|
+
// Runtime factories (clang/python/ruby/bash/wasm) are imported lazily at
|
|
14
|
+
// first-use inside their registered handlers — see the registrations below.
|
|
15
|
+
// Keeping them off the top-level import graph shaves their module-eval cost
|
|
16
|
+
// (zod, embedded socket-kernel/shim sources, wasm loaders) out of the
|
|
17
|
+
// one-time Worker Startup Time paid on every fresh-isolate cold run.
|
|
18
|
+
import { hasSeededProject, SEED_PROJECT_DIR, SEED_PROJECT_NAME, SEED_PROJECT_TILDE } from '@nimbus-sh/core/vfs/seed-project.js';
|
|
71
19
|
import { generation } from '@nimbus-sh/fabric/generation.js';
|
|
72
|
-
import {
|
|
73
|
-
import {
|
|
74
|
-
import { ensureSessionStateSchema, loadShellState, stampHydratedAt, countSessionStateKeys, loadKernelMounts, persistKernelMounts, appendScrollback, loadScrollback, } from './state-store.js';
|
|
20
|
+
import { DEFAULT_MOUNT_POINTS } from '@nimbus-sh/core/constants.js';
|
|
21
|
+
import { ensureSessionStateSchema, loadShellState, stampHydratedAt, countSessionStateKeys, loadKernelMounts, persistKernelMounts, loadScrollback } from './state-store.js';
|
|
75
22
|
import { recordRecoveryEvent } from '@nimbus-sh/platform/oom-discriminator.js';
|
|
76
23
|
import { sessionAiEnv } from './ai.js';
|
|
77
|
-
import { routeSessionLoopback } from './loopback.js';
|
|
78
24
|
import { setPhase } from './init-phases.js';
|
|
79
|
-
import {
|
|
80
|
-
import { clearPortCapability } from './port-capability.js';
|
|
81
|
-
function resolveNpmPrefix(prefix, cwd) {
|
|
82
|
-
return prefix.startsWith('/')
|
|
83
|
-
? normalizeVfsPath(prefix)
|
|
84
|
-
: resolveVfsPath(prefix, cwd || '/home/user');
|
|
85
|
-
}
|
|
25
|
+
import { shellTerminalTee } from './ws.js';
|
|
86
26
|
function quoteShellArgument(value) {
|
|
87
27
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
88
28
|
}
|
|
89
|
-
export async function initSession(self, ws) {
|
|
29
|
+
export async function initSession(self, ws, options = {}) {
|
|
30
|
+
const replayScrollback = options.resume !== 'wake';
|
|
90
31
|
self.ensureSqliteFs();
|
|
91
32
|
const kernelFs = self.sqliteFs.as(CRED_KERNEL);
|
|
92
33
|
self.ensureFacetManager();
|
|
@@ -115,17 +56,7 @@ export async function initSession(self, ws) {
|
|
|
115
56
|
// facet and the scrollback replay below is wire-phase work.
|
|
116
57
|
// Phase B will tag in once we start building the kernel.
|
|
117
58
|
setPhase(self, 'wire', 'init-session');
|
|
118
|
-
self.terminal = new WebSocketTerminal(ws, (
|
|
119
|
-
try {
|
|
120
|
-
appendScrollback(self.ctx, frame, Date.now());
|
|
121
|
-
}
|
|
122
|
-
catch (e) {
|
|
123
|
-
try {
|
|
124
|
-
console.warn('[B\'.3] appendScrollback failed:', e?.message || e);
|
|
125
|
-
}
|
|
126
|
-
catch { }
|
|
127
|
-
}
|
|
128
|
-
});
|
|
59
|
+
self.terminal = new WebSocketTerminal(ws, shellTerminalTee(self));
|
|
129
60
|
// [B'.3] Replay persisted scrollback BEFORE the cold-start UI gate.
|
|
130
61
|
// On rehydrate (hasPersistedState=true) we emit the prior
|
|
131
62
|
// session's terminal contents as a single batched write — the
|
|
@@ -139,16 +70,18 @@ export async function initSession(self, ws) {
|
|
|
139
70
|
// same scrollback both times. The cap eviction keeps total bytes
|
|
140
71
|
// bounded.
|
|
141
72
|
if (persisted.hasPersistedState) {
|
|
142
|
-
|
|
143
|
-
const replay = loadScrollback(self.ctx);
|
|
144
|
-
if (replay.length > 0)
|
|
145
|
-
self.terminal.write(replay);
|
|
146
|
-
}
|
|
147
|
-
catch (e) {
|
|
73
|
+
if (replayScrollback) {
|
|
148
74
|
try {
|
|
149
|
-
|
|
75
|
+
const replay = loadScrollback(self.ctx);
|
|
76
|
+
if (replay.length > 0)
|
|
77
|
+
self.terminal.write(replay);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
try {
|
|
81
|
+
console.warn('[B\'.3] scrollback replay failed:', e?.message || e);
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
150
84
|
}
|
|
151
|
-
catch { }
|
|
152
85
|
}
|
|
153
86
|
// The scrollback stops mid-command when the previous instance was
|
|
154
87
|
// reset under it — the platform kills the isolate without running a
|
|
@@ -234,61 +167,6 @@ export async function initSession(self, ws) {
|
|
|
234
167
|
if (self.shellProcessPid !== null) {
|
|
235
168
|
self.processes.exit(self.shellProcessPid, 0);
|
|
236
169
|
}
|
|
237
|
-
const shellProcess = self.processes.spawn('sh', ['sh'], persisted.cwd || '/home/user');
|
|
238
|
-
self.shellProcessPid = shellProcess.pid;
|
|
239
|
-
const runAsProcess = async (parent, cred, argv) => {
|
|
240
|
-
if (argv.length === 0)
|
|
241
|
-
return 0;
|
|
242
|
-
const child = self.processes.spawn(argv.join(' '), argv, parent.cwd, { parentPid: parent.pid, cred });
|
|
243
|
-
const activeShell = self.shell;
|
|
244
|
-
if (!activeShell) {
|
|
245
|
-
self.processes.exit(child.pid, 1);
|
|
246
|
-
throw new Error('shell is not initialized');
|
|
247
|
-
}
|
|
248
|
-
const identity = commandIdentityFor(child.pid);
|
|
249
|
-
let exitCode = 1;
|
|
250
|
-
try {
|
|
251
|
-
const stdin = parent.stdin && parent.stdin !== parent.terminalStdin
|
|
252
|
-
? await parent.stdin.readAll()
|
|
253
|
-
: undefined;
|
|
254
|
-
const result = await activeShell.execute(argv.map(quoteShellArgument).join(' '), {
|
|
255
|
-
cwd: parent.cwd,
|
|
256
|
-
env: parent.env,
|
|
257
|
-
stdin,
|
|
258
|
-
terminalStdin: parent.terminalStdin,
|
|
259
|
-
signal: parent.signal,
|
|
260
|
-
isolateShellState: true,
|
|
261
|
-
terminalFds: {
|
|
262
|
-
stdin: parent.isFdTerminal?.(0) ?? false,
|
|
263
|
-
stdout: parent.isFdTerminal?.(1) ?? false,
|
|
264
|
-
stderr: parent.isFdTerminal?.(2) ?? false,
|
|
265
|
-
},
|
|
266
|
-
onStdout: (data) => parent.stdout.write(data),
|
|
267
|
-
onStderr: (data) => parent.stderr.write(data),
|
|
268
|
-
commandContext: {
|
|
269
|
-
pid: identity.pid,
|
|
270
|
-
cred: identity.cred,
|
|
271
|
-
setUmask: identity.setUmask,
|
|
272
|
-
},
|
|
273
|
-
runAs: runAsProcess,
|
|
274
|
-
});
|
|
275
|
-
exitCode = result.exitCode;
|
|
276
|
-
return exitCode;
|
|
277
|
-
}
|
|
278
|
-
finally {
|
|
279
|
-
self.processes.exit(child.pid, exitCode);
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
const commandIdentityFor = (pid) => ({
|
|
283
|
-
pid,
|
|
284
|
-
get cred() {
|
|
285
|
-
return self.processes.cred(pid);
|
|
286
|
-
},
|
|
287
|
-
setUmask(mask) {
|
|
288
|
-
self.processes.setUmask(pid, mask);
|
|
289
|
-
},
|
|
290
|
-
runAs: runAsProcess,
|
|
291
|
-
});
|
|
292
170
|
// ── The workspace: one recipe for kernel + mounts + shell + coreutils ──
|
|
293
171
|
//
|
|
294
172
|
// No `facets`: the session registers its own runtime factories below,
|
|
@@ -304,8 +182,11 @@ export async function initSession(self, ws) {
|
|
|
304
182
|
mounts: mountPoints,
|
|
305
183
|
env: envOverlay,
|
|
306
184
|
terminal: self.terminal,
|
|
307
|
-
|
|
185
|
+
processes: self.processes,
|
|
186
|
+
runtimeSource: runtimeCatalogSource(self.env),
|
|
308
187
|
});
|
|
188
|
+
self.runtimeWorkspace = workspace;
|
|
189
|
+
self.shellProcessPid = workspace.shellProcessPid;
|
|
309
190
|
self.kernel = workspace.kernel;
|
|
310
191
|
self.shell = workspace.shell;
|
|
311
192
|
const kernel = workspace.kernel;
|
|
@@ -448,1270 +329,6 @@ export async function initSession(self, ws) {
|
|
|
448
329
|
// W8: hand the registry to the cp broker so child_process.spawn from
|
|
449
330
|
// a parent facet can resolve and dispatch commands the same way the
|
|
450
331
|
// shell does.
|
|
451
|
-
self._setCpRegistry(registry);
|
|
452
|
-
registry.register('chsh', makeChshCommand({
|
|
453
|
-
isBashInstalled: (home) => listInstalledRuntimes(sqliteFs, home).some((runtime) => runtime.name === 'bash'),
|
|
454
|
-
}));
|
|
455
|
-
// ── Git integration (isomorphic-git) ──
|
|
456
|
-
// ctx + env are passed for clone/fetch/pull which run in a facet to avoid
|
|
457
|
-
// exhausting the supervisor DO's CPU budget on large repos. The command
|
|
458
|
-
// module (+ its ~106 KB network-facet dependency) is loaded lazily on the
|
|
459
|
-
// first `git` invocation so it stays out of the cold script-eval graph.
|
|
460
|
-
registry.register('git', async (ctx) => {
|
|
461
|
-
const { runGitCommand } = await import('../git/commands.js');
|
|
462
|
-
return runGitCommand(ctx, sqliteFs, self.ctx, self.env);
|
|
463
|
-
});
|
|
464
|
-
// ── runtime package manager: `nimbus install` package manager + runner registry.
|
|
465
|
-
//
|
|
466
|
-
// 1. Register the clang-runner factory FIRST so the rehydration step
|
|
467
|
-
// below can re-bind `clang` / `wasm-ld` from an already-installed
|
|
468
|
-
// manifest.
|
|
469
|
-
// 2. Register the `nimbus` shell verb (install/uninstall/list/available).
|
|
470
|
-
// 3. Rehydrate any previously-installed runtimes from VFS so their
|
|
471
|
-
// bins reappear in the registry after DO eviction or WS reconnect.
|
|
472
|
-
registerRunnerFactory('clang-runner', (manifest, installRoot, binName, binKind) => async (ctx) => {
|
|
473
|
-
const { makeClangRunnerFactory } = await import('@nimbus-sh/core/runtime/clang-runner.js');
|
|
474
|
-
const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
|
|
475
|
-
return await makeClangRunnerFactory({ facets: facetHostForManager(facetMgr), vfs: sqliteFs })(manifest, installRoot, binName, binKind)(ctx);
|
|
476
|
-
});
|
|
477
|
-
// Pyodide v1 — Python 3.13 via the same R2-package-manager
|
|
478
|
-
// substrate that ships clang. Manifest entrypoints `python` and
|
|
479
|
-
// `python3` both bind to this factory; the runner ferries
|
|
480
|
-
// pyodide.asm.wasm via LOADER modules-map and pyodide.asm.js +
|
|
481
|
-
// python_stdlib.zip via the loader-pool context channel.
|
|
482
|
-
//
|
|
483
|
-
// REPL-W1: wrap the one-shot factory so `python` with NO args (and
|
|
484
|
-
// no flags that would consume args) drops into an interactive
|
|
485
|
-
// REPL. The wrap is purely additive — args-bearing invocations
|
|
486
|
-
// pass through to the existing handler unchanged.
|
|
487
|
-
registerRunnerFactory('cpython-runner', (manifest, installRoot, binName, binKind) => async function pythonReplOrOneShot(ctx) {
|
|
488
|
-
const argv = ctx.args || [];
|
|
489
|
-
// No args at all → REPL session. Hand off to runPythonRepl
|
|
490
|
-
// which builds its own IsolatePool (separate from the
|
|
491
|
-
// one-shot dispatch's pool) and drives a ReplSession.
|
|
492
|
-
if (argv.length === 0 && self.terminal) {
|
|
493
|
-
const { runPythonRepl } = await import('../runtime/python-repl.js');
|
|
494
|
-
return await runPythonRepl({
|
|
495
|
-
facetMgr,
|
|
496
|
-
vfs: sqliteFs,
|
|
497
|
-
terminal: self.terminal,
|
|
498
|
-
installRoot,
|
|
499
|
-
manifest,
|
|
500
|
-
// REPL-R7-1: thread the shell so ReplSession can drain
|
|
501
|
-
// shell.pasteQueue on attach (multi-line WS frames like
|
|
502
|
-
// `python\nexit(7)` would otherwise drop the tail input).
|
|
503
|
-
shell: self.shell,
|
|
504
|
-
// The supervisor derives the write credential from this; without
|
|
505
|
-
// it the prompt cannot write to the session filesystem.
|
|
506
|
-
pid: ctx.pid,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
// Args present (one-shot mode: -c, script, -m, -). Fall through
|
|
510
|
-
// to the canonical handler (imported lazily on first use).
|
|
511
|
-
const { makeCPythonRunnerFactory } = await import('@nimbus-sh/core/runtime/cpython-runner.js');
|
|
512
|
-
const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
|
|
513
|
-
const { cpythonResidentStart } = await import('../runtime/cpython-resident.js');
|
|
514
|
-
return await makeCPythonRunnerFactory({
|
|
515
|
-
facets: facetHostForManager(facetMgr),
|
|
516
|
-
vfs: sqliteFs,
|
|
517
|
-
startResident: cpythonResidentStart(facetMgr),
|
|
518
|
-
})(manifest, installRoot, binName, binKind)(ctx);
|
|
519
|
-
});
|
|
520
|
-
// Ruby v1 — Ruby 3.3.4 via ruby.wasm 2.9.3-2.9.4. Same architecture
|
|
521
|
-
// as python-runner: ruby+stdlib.wasm rides via LOADER modules-map,
|
|
522
|
-
// bootstrap runs at child-facet module-init time, per-call
|
|
523
|
-
// __rubyRun drives rb-eval-string-protect with a wrapper that
|
|
524
|
-
// catches SystemExit. See src/runtime/ruby-runner.ts +
|
|
525
|
-
//
|
|
526
|
-
// REPL Stream A: wrap the one-shot factory so `ruby` with NO args
|
|
527
|
-
// drops into an interactive REPL. The wrap is purely additive —
|
|
528
|
-
// args-bearing invocations pass through to the existing handler.
|
|
529
|
-
registerRunnerFactory('ruby-runner', (manifest, installRoot, binName, binKind) => async function rubyReplOrOneShot(ctx) {
|
|
530
|
-
const argv = ctx.args || [];
|
|
531
|
-
if (argv.length === 0 && self.terminal) {
|
|
532
|
-
const { runRubyRepl } = await import('../runtime/ruby-repl.js');
|
|
533
|
-
return await runRubyRepl({
|
|
534
|
-
facetMgr,
|
|
535
|
-
vfs: sqliteFs,
|
|
536
|
-
terminal: self.terminal,
|
|
537
|
-
installRoot,
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
const { makeRubyRunnerFactory } = await import('@nimbus-sh/core/runtime/ruby-runner.js');
|
|
541
|
-
const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
|
|
542
|
-
const { rubyResidentStart } = await import('../runtime/ruby-resident.js');
|
|
543
|
-
return await makeRubyRunnerFactory({
|
|
544
|
-
facets: facetHostForManager(facetMgr),
|
|
545
|
-
vfs: sqliteFs,
|
|
546
|
-
registry,
|
|
547
|
-
startResident: rubyResidentStart(facetMgr),
|
|
548
|
-
})(manifest, installRoot, binName, binKind)(ctx);
|
|
549
|
-
});
|
|
550
|
-
// GNU bash 5.2.37 (wasm32-wasi, asyncified) — dedicated facet
|
|
551
|
-
// runner driving the fork/pipe/exec/setjmp scheduler (fork M1-M3
|
|
552
|
-
// mechanisms). Interactive terminal invocations use the shared
|
|
553
|
-
// ReplSession line editor; -c, scripts, and piped stdin retain the
|
|
554
|
-
// canonical one-shot handler.
|
|
555
|
-
registerRunnerFactory('bash-runner', (manifest, installRoot, binName, binKind) => async (ctx) => {
|
|
556
|
-
const argv = ctx.args || [];
|
|
557
|
-
const explicitInteractive = argv.includes('-i');
|
|
558
|
-
const terminalStdin = ctx.isFdTerminal?.(0) ?? !ctx.stdin;
|
|
559
|
-
if (self.terminal && (explicitInteractive || (argv.length === 0 && terminalStdin))) {
|
|
560
|
-
const { runBashRepl } = await import('../runtime/bash-repl.js');
|
|
561
|
-
return await runBashRepl({
|
|
562
|
-
facetMgr,
|
|
563
|
-
vfs: sqliteFs,
|
|
564
|
-
terminal: self.terminal,
|
|
565
|
-
installRoot,
|
|
566
|
-
manifest,
|
|
567
|
-
cred: ctx.cred,
|
|
568
|
-
env: ctx.env,
|
|
569
|
-
cwd: ctx.cwd || '/home/user',
|
|
570
|
-
shell: self.shell ?? undefined,
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
const { makeBashRunnerFactory } = await import('@nimbus-sh/core/runtime/bash-runner.js');
|
|
574
|
-
const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
|
|
575
|
-
return await makeBashRunnerFactory({ facets: facetHostForManager(facetMgr), vfs: sqliteFs })(manifest, installRoot, binName, binKind)(ctx);
|
|
576
|
-
});
|
|
577
|
-
{
|
|
578
|
-
// Cast registry to the minimal package-manager shape. CommandRegistry
|
|
579
|
-
// CommandRegistry has register(name, handler) which matches.
|
|
580
|
-
const pkgRegistry = registry;
|
|
581
|
-
const nimbusGetHome = () => {
|
|
582
|
-
// Shell env carries HOME; fall back to Nimbus's default home.
|
|
583
|
-
const envHome = (self.shell && self.shell.env && self.shell.env.HOME)
|
|
584
|
-
? self.shell.env.HOME
|
|
585
|
-
: '/home/user';
|
|
586
|
-
return envHome;
|
|
587
|
-
};
|
|
588
|
-
registry.register('nimbus', makeNimbusVerbHandler({
|
|
589
|
-
env: self.env,
|
|
590
|
-
vfs: sqliteFs,
|
|
591
|
-
registry: pkgRegistry,
|
|
592
|
-
getHome: nimbusGetHome,
|
|
593
|
-
// The application verbs are the session's own (programmatic.ts):
|
|
594
|
-
// the shell, the SDK and the Agent all reach the same policy.
|
|
595
|
-
apps: {
|
|
596
|
-
expose: (target, options) => rpcExposeApp(self, target, options),
|
|
597
|
-
list: () => rpcListApps(self),
|
|
598
|
-
rotateLink: (target) => rpcRotateLink(self, target),
|
|
599
|
-
remove: (target) => rpcRemoveApp(self, target),
|
|
600
|
-
},
|
|
601
|
-
warmRuntime: async (target, ctx) => {
|
|
602
|
-
// The runtime name, which is what `nimbus install python` installs
|
|
603
|
-
// — not the name the user typed. Left as 'python' through the
|
|
604
|
-
// migration, this matched only the superseded Pyodide entry, so the
|
|
605
|
-
// interpreter people actually install was never warmed and the
|
|
606
|
-
// first invocation after installing paid the wasm compile.
|
|
607
|
-
if (target.name !== 'cpython')
|
|
608
|
-
return;
|
|
609
|
-
ctx.stdout.write(`[${target.name}] warming runtime...\n`);
|
|
610
|
-
const stdout = { write(_s) { } };
|
|
611
|
-
const stderrText = [];
|
|
612
|
-
const stderr = { write: (s) => { stderrText.push(String(s)); } };
|
|
613
|
-
const py = await registry.resolve('python');
|
|
614
|
-
if (py) {
|
|
615
|
-
const pid = 'pid' in ctx ? ctx.pid : undefined;
|
|
616
|
-
const setUmask = 'setUmask' in ctx ? ctx.setUmask : undefined;
|
|
617
|
-
const runAs = 'runAs' in ctx ? ctx.runAs : undefined;
|
|
618
|
-
if (typeof pid !== 'number'
|
|
619
|
-
|| typeof setUmask !== 'function'
|
|
620
|
-
|| typeof runAs !== 'function'
|
|
621
|
-
|| self.kernel === null) {
|
|
622
|
-
throw new Error('python warm-up requires a process identity');
|
|
623
|
-
}
|
|
624
|
-
const cred = requireVfsCred('cred' in ctx ? ctx.cred : undefined, 'python warm-up');
|
|
625
|
-
const code = await py({
|
|
626
|
-
...ctx,
|
|
627
|
-
args: ['-c', 'pass'],
|
|
628
|
-
pid,
|
|
629
|
-
cred,
|
|
630
|
-
setUmask: (mask) => setUmask(mask),
|
|
631
|
-
runAs: (targetCred, argv) => runAs(targetCred, argv),
|
|
632
|
-
vfs: self.kernel.vfs,
|
|
633
|
-
signal: new AbortController().signal,
|
|
634
|
-
stdout,
|
|
635
|
-
stderr,
|
|
636
|
-
});
|
|
637
|
-
if (typeof code === 'number' && code !== 0) {
|
|
638
|
-
throw new Error(stderrText.join('').trim() || `python warm-up exited ${code}`);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
const { warmPythonRepl } = await import('../runtime/python-repl.js');
|
|
642
|
-
await warmPythonRepl({
|
|
643
|
-
facetMgr,
|
|
644
|
-
vfs: sqliteFs,
|
|
645
|
-
installRoot: target.root,
|
|
646
|
-
manifest: target.manifest,
|
|
647
|
-
});
|
|
648
|
-
ctx.stdout.write('[python] ready\n');
|
|
649
|
-
},
|
|
650
|
-
}));
|
|
651
|
-
// Rehydration runs here even though the shell hasn't been built
|
|
652
|
-
// yet — that's fine: it accesses VFS + registry which are both
|
|
653
|
-
// already initialised. Runs O(installed-runtimes); typically 0
|
|
654
|
-
// on a fresh session.
|
|
655
|
-
try {
|
|
656
|
-
const rehydration = rehydrateInstalledRuntimes(sqliteFs, pkgRegistry, nimbusGetHome());
|
|
657
|
-
if (rehydration.count > 0) {
|
|
658
|
-
// Surface to terminal via the standard MOTD-style line so
|
|
659
|
-
// users see what's been auto-rebound. Not an error path.
|
|
660
|
-
self.terminal?.write?.(`\x1b[2m[nimbus] rehydrated ${rehydration.count} runtime bin(s): ${rehydration.bins.join(', ')}\x1b[0m\r\n`);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
catch { /* fail-soft: rehydration must not block session boot */ }
|
|
664
|
-
}
|
|
665
|
-
// ── node command: facet-based execution ─────────────────────────────
|
|
666
|
-
// Parses args, reads script from VFS, delegates to FacetManager.
|
|
667
|
-
// The facet creates a dynamic worker where new Function() is allowed
|
|
668
|
-
// during module startup.
|
|
669
|
-
// ── node command (runtime registry refactor: refactored to use runtime-registry) ──
|
|
670
|
-
//
|
|
671
|
-
// Behaviour preserved exactly:
|
|
672
|
-
// - primitive #1 nodeFlagSpan (--version/-v/-h scan only flags
|
|
673
|
-
// before script path)
|
|
674
|
-
// - primitive #1 shebang strip
|
|
675
|
-
// - .ts/.tsx/.jsx esbuild auto-transform
|
|
676
|
-
// - G4 binSpawn ctx propagation (when the .bin handler set
|
|
677
|
-
// ctx.__nimbusBinSpawn, runFresh reuses the caller's PID
|
|
678
|
-
// instead of double-spawning)
|
|
679
|
-
// - --watch/--inspect/--inspect-brk routing via runFresh →
|
|
680
|
-
// isLongRunningInvocation
|
|
681
|
-
//
|
|
682
|
-
// The registry encodes the shared shape; per-runtime overrides
|
|
683
|
-
// live in the spec object.
|
|
684
|
-
const nodeSpec = {
|
|
685
|
-
name: 'node',
|
|
686
|
-
// CLN-1b: was stale literal 'v20.0.0'; pull from src/constants.ts
|
|
687
|
-
// canonical (src/runtime/node-shims.ts already does the same).
|
|
688
|
-
version: NODE_VERSION,
|
|
689
|
-
helpText: 'Usage: node [options] [script.js] [arguments]\n' +
|
|
690
|
-
' node -e "code"\n\n' +
|
|
691
|
-
'Options:\n' +
|
|
692
|
-
' -e, --eval <code> Evaluate code\n' +
|
|
693
|
-
' -v, --version Print version\n' +
|
|
694
|
-
' -h, --help Print help\n' +
|
|
695
|
-
'\nExecution via DO Facets (isolated V8 isolate)',
|
|
696
|
-
run: (code, opts) => runFresh(facetMgr, code, opts),
|
|
697
|
-
supportsBinSpawn: true,
|
|
698
|
-
};
|
|
699
|
-
{
|
|
700
|
-
const oneShotNode = buildRuntimeHandler(nodeSpec, {
|
|
701
|
-
vfs: sqliteFs,
|
|
702
|
-
getEsbuild: () => {
|
|
703
|
-
if (!self.esbuildService) {
|
|
704
|
-
self.ensureSqliteFs();
|
|
705
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
706
|
-
}
|
|
707
|
-
return self.esbuildService;
|
|
708
|
-
},
|
|
709
|
-
registry,
|
|
710
|
-
});
|
|
711
|
-
// REPL Stream A: no-args invocation → drop into REPL session.
|
|
712
|
-
registry.register('node', async function nodeReplOrOneShot(ctx) {
|
|
713
|
-
const argv = ctx.args || [];
|
|
714
|
-
if (argv.length === 0 && self.terminal) {
|
|
715
|
-
const { runNodeRepl } = await import('../runtime/node-repl.js');
|
|
716
|
-
return await runNodeRepl({ facetMgr, terminal: self.terminal });
|
|
717
|
-
}
|
|
718
|
-
return await oneShotNode(ctx);
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
// ── bun command (runtime registry refactor: refactored to use runtime-registry) ──
|
|
722
|
-
//
|
|
723
|
-
// Behaviour preserved exactly:
|
|
724
|
-
// - --version / --help
|
|
725
|
-
// - install / i / add → delegate to npm
|
|
726
|
-
// - run <script> → look up package.json#scripts and shell.execute
|
|
727
|
-
// - -e / --eval flow
|
|
728
|
-
// - script-path flow with .ts/.tsx/.jsx auto-transform
|
|
729
|
-
// - BUN_SHIM_PREAMBLE prepend (handled inside runBunScript itself)
|
|
730
|
-
//
|
|
731
|
-
// Bun does NOT use binSpawn ctx propagation today (its runFresh
|
|
732
|
-
// chain doesn't share PID state with the .bin handler — the .bin
|
|
733
|
-
// handler always dispatches through `node`, not `bun`). So
|
|
734
|
-
// supportsBinSpawn=false (default).
|
|
735
|
-
/** `scripts` from the cwd's package.json; empty when there is none. */
|
|
736
|
-
const readPackageScripts = (cwd) => {
|
|
737
|
-
try {
|
|
738
|
-
const pkg = JSON.parse(kernelFs.readFileString(cwd + '/package.json'));
|
|
739
|
-
const scripts = pkg?.scripts;
|
|
740
|
-
return scripts && typeof scripts === 'object' ? scripts : {};
|
|
741
|
-
}
|
|
742
|
-
catch {
|
|
743
|
-
return {};
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
const bunSpec = {
|
|
747
|
-
name: 'bun',
|
|
748
|
-
version: BUN_VERSION,
|
|
749
|
-
helpText: 'Usage: bun [options] [script.[js|ts|tsx]] [args...]\n' +
|
|
750
|
-
' bun -e "code"\n' +
|
|
751
|
-
' bun install [pkg ...]\n' +
|
|
752
|
-
' bun run <script>\n\n' +
|
|
753
|
-
'Bun-runtime shim provides Bun.file/Bun.write/\n' +
|
|
754
|
-
'Bun.spawn/Bun.password/Bun.gunzip backed by Workers-native\n' +
|
|
755
|
-
'primitives. Bun.serve / Bun.sql / Bun.S3 throw with supported alternatives.\n' +
|
|
756
|
-
'Execution via DO Facets (isolated V8 isolate per call).',
|
|
757
|
-
run: (code, opts) => runBunScript(facetMgr, code, opts),
|
|
758
|
-
subcommands: {
|
|
759
|
-
// bun install / i / add → npm install (same VFS, same R2 caches).
|
|
760
|
-
install: async (ctx, reg) => {
|
|
761
|
-
const npmCmd = await reg.resolve('npm');
|
|
762
|
-
if (npmCmd) {
|
|
763
|
-
return await npmCmd({ ...ctx, args: ['install', ...(ctx.args || []).slice(1)] });
|
|
764
|
-
}
|
|
765
|
-
ctx.stderr.write('bun install: npm handler unavailable\n');
|
|
766
|
-
return 1;
|
|
767
|
-
},
|
|
768
|
-
i: async (ctx, reg) => {
|
|
769
|
-
const npmCmd = await reg.resolve('npm');
|
|
770
|
-
if (npmCmd) {
|
|
771
|
-
return await npmCmd({ ...ctx, args: ['install', ...(ctx.args || []).slice(1)] });
|
|
772
|
-
}
|
|
773
|
-
ctx.stderr.write('bun i: npm handler unavailable\n');
|
|
774
|
-
return 1;
|
|
775
|
-
},
|
|
776
|
-
add: async (ctx, reg) => {
|
|
777
|
-
const npmCmd = await reg.resolve('npm');
|
|
778
|
-
if (npmCmd) {
|
|
779
|
-
return await npmCmd({ ...ctx, args: ['install', ...(ctx.args || []).slice(1)] });
|
|
780
|
-
}
|
|
781
|
-
ctx.stderr.write('bun add: npm handler unavailable\n');
|
|
782
|
-
return 1;
|
|
783
|
-
},
|
|
784
|
-
// bun run <target> — a package.json script, or a file.
|
|
785
|
-
//
|
|
786
|
-
// Real bun's precedence, and the reason this is not just a script
|
|
787
|
-
// lookup: a path-shaped target (`./cli.ts`, `sub/x`, `.`) is ALWAYS
|
|
788
|
-
// a file and never consults scripts, while a bare name checks
|
|
789
|
-
// scripts first and only then falls back to a file. The file case
|
|
790
|
-
// delegates to the standard runtime flow, so `bun run ./cli.ts`
|
|
791
|
-
// and `bun cli.ts` execute through one path — TypeScript transform,
|
|
792
|
-
// facet dispatch and all.
|
|
793
|
-
run: async (ctx, _reg, runAsBun) => {
|
|
794
|
-
const args = ctx.args || [];
|
|
795
|
-
// bun's own run flags sit between the verb and the target.
|
|
796
|
-
let targetIdx = 1;
|
|
797
|
-
while (targetIdx < args.length && args[targetIdx].startsWith('-'))
|
|
798
|
-
targetIdx++;
|
|
799
|
-
const target = args[targetIdx];
|
|
800
|
-
const cwd = normalizeVfsPath(ctx.cwd || '/home/user');
|
|
801
|
-
const scripts = readPackageScripts(cwd);
|
|
802
|
-
if (!target) {
|
|
803
|
-
ctx.stdout.write('Usage: bun run [flags] <file or script>\n');
|
|
804
|
-
const names = Object.keys(scripts);
|
|
805
|
-
if (names.length) {
|
|
806
|
-
ctx.stdout.write(`\npackage.json scripts (${names.length} found):\n\n`);
|
|
807
|
-
for (const n of names)
|
|
808
|
-
ctx.stdout.write(` bun run ${n}\n ${scripts[n]}\n\n`);
|
|
809
|
-
}
|
|
810
|
-
return 0;
|
|
811
|
-
}
|
|
812
|
-
const pathShaped = target === '.' || target === '..' || target.includes('/');
|
|
813
|
-
const pkgScript = pathShaped ? undefined : scripts[target];
|
|
814
|
-
if (pkgScript) {
|
|
815
|
-
// Trailing args append to the script command; bun drops a single
|
|
816
|
-
// `--` separator between the script name and them.
|
|
817
|
-
const extra = args.slice(targetIdx + 1);
|
|
818
|
-
if (extra[0] === '--')
|
|
819
|
-
extra.shift();
|
|
820
|
-
const command = extra.length ? `${pkgScript} ${extra.join(' ')}` : pkgScript;
|
|
821
|
-
try {
|
|
822
|
-
const shellResult = await shell.execute(command, {
|
|
823
|
-
cwd: ctx.cwd,
|
|
824
|
-
env: ctx.env,
|
|
825
|
-
onStdout: (d) => ctx.stdout.write(d),
|
|
826
|
-
onStderr: (d) => ctx.stderr.write(d),
|
|
827
|
-
});
|
|
828
|
-
return shellResult.exitCode;
|
|
829
|
-
}
|
|
830
|
-
catch (e) {
|
|
831
|
-
ctx.stderr.write(`bun run: ${e?.message ?? String(e)}\n`);
|
|
832
|
-
return 1;
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
const resolved = resolveRuntimeScriptPath(kernelFs, cwd, target, {
|
|
836
|
-
preferModuleField: true,
|
|
837
|
-
});
|
|
838
|
-
if (resolved === null) {
|
|
839
|
-
ctx.stderr.write(pathShaped
|
|
840
|
-
? `error: Module not found "${target}"\n`
|
|
841
|
-
: `error: Script not found "${target}"\n`);
|
|
842
|
-
return 1;
|
|
843
|
-
}
|
|
844
|
-
return runAsBun([...args.slice(1, targetIdx), '/' + resolved, ...args.slice(targetIdx + 1)]);
|
|
845
|
-
},
|
|
846
|
-
},
|
|
847
|
-
};
|
|
848
|
-
{
|
|
849
|
-
const oneShotBun = buildRuntimeHandler(bunSpec, {
|
|
850
|
-
vfs: sqliteFs,
|
|
851
|
-
getEsbuild: () => {
|
|
852
|
-
if (!self.esbuildService) {
|
|
853
|
-
self.ensureSqliteFs();
|
|
854
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
855
|
-
}
|
|
856
|
-
return self.esbuildService;
|
|
857
|
-
},
|
|
858
|
-
registry,
|
|
859
|
-
});
|
|
860
|
-
// REPL Stream A: no-args invocation → drop into REPL session.
|
|
861
|
-
registry.register('bun', async function bunReplOrOneShot(ctx) {
|
|
862
|
-
const argv = ctx.args || [];
|
|
863
|
-
if (argv.length === 0 && self.terminal) {
|
|
864
|
-
const { runBunRepl } = await import('../runtime/bun-repl.js');
|
|
865
|
-
return await runBunRepl({ facetMgr, terminal: self.terminal });
|
|
866
|
-
}
|
|
867
|
-
return await oneShotBun(ctx);
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
// ── wasm-runner: native WebAssembly, on this session's facet host ──
|
|
871
|
-
//
|
|
872
|
-
// Bytes ride INSIDE the inner worker's code blob, the one phase where
|
|
873
|
-
// workerd permits wasm code generation; request-time
|
|
874
|
-
// WebAssembly.instantiate(bytes) is CSP-blocked and avoided.
|
|
875
|
-
//
|
|
876
|
-
// wasm-csp/findings.md — add(3,4)===7 in 11ms warm against the
|
|
877
|
-
// deployed Cloudflare fleet.
|
|
878
|
-
//
|
|
879
|
-
// Lazy: the wasm-runner module (WASI instance preamble + snapshot
|
|
880
|
-
// machinery) is imported on first `wasm-runner` invocation and the
|
|
881
|
-
// built handler memoized, so its module-eval cost stays off the cold
|
|
882
|
-
// Worker Startup Time path.
|
|
883
|
-
{
|
|
884
|
-
let wasmHandler = null;
|
|
885
|
-
registry.register('wasm-runner', async (ctx) => {
|
|
886
|
-
if (!wasmHandler) {
|
|
887
|
-
const { wasmRunnerSpec } = await import('@nimbus-sh/core/runtime/wasm-runner.js');
|
|
888
|
-
const { loaderFacetHost } = await import('../runtime/facet-loader-host.js');
|
|
889
|
-
const wasmSpec = wasmRunnerSpec({
|
|
890
|
-
// filesystem WASI: extended VFS surface for WASI file-IO. The
|
|
891
|
-
// wasm-runner snapshots a session subtree into the facet, flushes
|
|
892
|
-
// the diff back via this surface after _start returns.
|
|
893
|
-
vfs: sqliteFs,
|
|
894
|
-
facets: loaderFacetHost(self.env, self.ctx),
|
|
895
|
-
processes: self.processes,
|
|
896
|
-
});
|
|
897
|
-
wasmHandler = buildRuntimeHandler(wasmSpec, {
|
|
898
|
-
vfs: sqliteFs,
|
|
899
|
-
getEsbuild: () => {
|
|
900
|
-
if (!self.esbuildService) {
|
|
901
|
-
self.ensureSqliteFs();
|
|
902
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
903
|
-
}
|
|
904
|
-
return self.esbuildService;
|
|
905
|
-
},
|
|
906
|
-
registry,
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
return await wasmHandler(ctx);
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
kernel.routeLoopback = (port, request) => routeSessionLoopback(self, port, request);
|
|
913
|
-
try {
|
|
914
|
-
registry.register('curl', createCurlCommand(kernel));
|
|
915
|
-
}
|
|
916
|
-
catch { }
|
|
917
|
-
// ── df with SQLite stats + cache + process metrics ──────────────────
|
|
918
|
-
registry.register('df', async (ctx) => {
|
|
919
|
-
const stats = sqliteFs.getStats();
|
|
920
|
-
const pstats = facetMgr.stats;
|
|
921
|
-
const used = stats.usedBytes;
|
|
922
|
-
const cap = stats.capacityBytes;
|
|
923
|
-
const avail = cap - used;
|
|
924
|
-
const pct = ((used / cap) * 100).toFixed(0);
|
|
925
|
-
const fmt = (b) => {
|
|
926
|
-
if (b >= 1e9)
|
|
927
|
-
return (b / 1e9).toFixed(1) + 'G';
|
|
928
|
-
if (b >= 1e6)
|
|
929
|
-
return (b / 1e6).toFixed(1) + 'M';
|
|
930
|
-
if (b >= 1e3)
|
|
931
|
-
return (b / 1e3).toFixed(1) + 'K';
|
|
932
|
-
return b + 'B';
|
|
933
|
-
};
|
|
934
|
-
ctx.stdout.write('Filesystem Size Used Avail Use% Mounted on\n');
|
|
935
|
-
ctx.stdout.write('sqlite ' + fmt(cap).padStart(5) + ' ' + fmt(used).padStart(5) +
|
|
936
|
-
' ' + fmt(avail).padStart(5) + ' ' + pct.padStart(3) + '% /\n');
|
|
937
|
-
ctx.stdout.write('\nCache: ' + stats.cache.entries + '/' + stats.cache.maxEntries +
|
|
938
|
-
' slots | hit rate: ' + stats.cache.hitRate +
|
|
939
|
-
'% | evictions: ' + stats.cache.evictions + '\n');
|
|
940
|
-
ctx.stdout.write('Procs: ' + pstats.running + ' running, ' +
|
|
941
|
-
pstats.exited + ' exited, ' +
|
|
942
|
-
pstats.total + ' total (next PID: ' + pstats.nextPid + ')\n');
|
|
943
|
-
return 0;
|
|
944
|
-
});
|
|
945
|
-
// ── esbuild command: transform/bundle via esbuild facet ───────────────
|
|
946
|
-
// Lazy-creates the EsbuildService on first use (esbuild-wasm is ~10MB).
|
|
947
|
-
registry.register('esbuild', async (ctx) => {
|
|
948
|
-
const args = ctx.args || [];
|
|
949
|
-
if (args.includes('--version')) {
|
|
950
|
-
ctx.stdout.write('0.24.2 (esbuild-wasm, bundled)\n');
|
|
951
|
-
return 0;
|
|
952
|
-
}
|
|
953
|
-
if (args.includes('--help') || args.length === 0) {
|
|
954
|
-
ctx.stdout.write('Usage: esbuild [options] [entry points]\n\n');
|
|
955
|
-
ctx.stdout.write('Options:\n');
|
|
956
|
-
ctx.stdout.write(' --bundle Bundle all dependencies into output\n');
|
|
957
|
-
ctx.stdout.write(' --outfile=<path> Write output to a file\n');
|
|
958
|
-
ctx.stdout.write(' --outdir=<path> Write output to a directory\n');
|
|
959
|
-
ctx.stdout.write(' --format=esm|cjs Output format (default: esm)\n');
|
|
960
|
-
ctx.stdout.write(' --platform=browser|node Target platform\n');
|
|
961
|
-
ctx.stdout.write(' --minify Minify output\n');
|
|
962
|
-
ctx.stdout.write(' --sourcemap Generate source maps\n');
|
|
963
|
-
ctx.stdout.write(' --target=<target> JS target (default: esnext)\n');
|
|
964
|
-
ctx.stdout.write(' --loader=<loader> Force file loader (ts, tsx, jsx, css)\n');
|
|
965
|
-
ctx.stdout.write(' --version Show version\n');
|
|
966
|
-
ctx.stdout.write('\nPowered by esbuild-wasm (bundled in supervisor).\n');
|
|
967
|
-
return 0;
|
|
968
|
-
}
|
|
969
|
-
// Lazy-init esbuild service
|
|
970
|
-
if (!self.esbuildService) {
|
|
971
|
-
self.ensureSqliteFs();
|
|
972
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
973
|
-
}
|
|
974
|
-
// Parse flags
|
|
975
|
-
const flags = {};
|
|
976
|
-
const entryPoints = [];
|
|
977
|
-
for (const arg of args) {
|
|
978
|
-
if (arg.startsWith('--')) {
|
|
979
|
-
const eqIdx = arg.indexOf('=');
|
|
980
|
-
if (eqIdx > 0) {
|
|
981
|
-
flags[arg.substring(2, eqIdx)] = arg.substring(eqIdx + 1);
|
|
982
|
-
}
|
|
983
|
-
else {
|
|
984
|
-
flags[arg.substring(2)] = 'true';
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
else {
|
|
988
|
-
entryPoints.push(arg);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
// Transform-only mode (single file, no --bundle)
|
|
992
|
-
if (entryPoints.length === 1 && !flags['bundle']) {
|
|
993
|
-
// Read the file and transform it
|
|
994
|
-
const filePath = resolveVfsPath(entryPoints[0], ctx.cwd || '/home/user');
|
|
995
|
-
let code;
|
|
996
|
-
try {
|
|
997
|
-
code = kernelFs.readFileString(filePath);
|
|
998
|
-
}
|
|
999
|
-
catch {
|
|
1000
|
-
ctx.stderr.write(`esbuild: could not read file: ${entryPoints[0]}\n`);
|
|
1001
|
-
return 1;
|
|
1002
|
-
}
|
|
1003
|
-
try {
|
|
1004
|
-
ctx.stderr.write('Transforming...\n');
|
|
1005
|
-
const result = await self.esbuildService.transform(code, {
|
|
1006
|
-
loader: flags['loader'] || (() => {
|
|
1007
|
-
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
1008
|
-
return { ts: 'ts', tsx: 'tsx', jsx: 'jsx', js: 'js', mts: 'ts', mjs: 'js', css: 'css', json: 'json' }[ext || ''];
|
|
1009
|
-
})(),
|
|
1010
|
-
format: flags['format'] || 'esm',
|
|
1011
|
-
target: flags['target'] || 'esnext',
|
|
1012
|
-
sourcemap: flags['sourcemap'] === 'true',
|
|
1013
|
-
minify: flags['minify'] === 'true',
|
|
1014
|
-
});
|
|
1015
|
-
if (flags['outfile']) {
|
|
1016
|
-
const outPath = resolveVfsPath(flags['outfile'], ctx.cwd || '/home/user');
|
|
1017
|
-
const parent = parentVfsPath(outPath);
|
|
1018
|
-
if (parent && !kernelFs.exists(parent))
|
|
1019
|
-
kernelFs.mkdir(parent, { recursive: true });
|
|
1020
|
-
kernelFs.writeFile(outPath, result.code);
|
|
1021
|
-
ctx.stdout.write(` ${outPath} ${result.code.length} bytes\n`);
|
|
1022
|
-
}
|
|
1023
|
-
else {
|
|
1024
|
-
ctx.stdout.write(result.code);
|
|
1025
|
-
}
|
|
1026
|
-
for (const w of result.warnings || []) {
|
|
1027
|
-
ctx.stderr.write(`warning: ${w.text}\n`);
|
|
1028
|
-
}
|
|
1029
|
-
return 0;
|
|
1030
|
-
}
|
|
1031
|
-
catch (e) {
|
|
1032
|
-
ctx.stderr.write(`esbuild error: ${e?.message || e}\n`);
|
|
1033
|
-
return 1;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
// Bundle mode
|
|
1037
|
-
if (entryPoints.length === 0) {
|
|
1038
|
-
ctx.stderr.write('esbuild: no entry points specified\n');
|
|
1039
|
-
return 1;
|
|
1040
|
-
}
|
|
1041
|
-
// Resolve entry points relative to cwd
|
|
1042
|
-
const resolvedEntryPoints = entryPoints.map(ep => resolveVfsPath(ep, ctx.cwd || '/home/user'));
|
|
1043
|
-
try {
|
|
1044
|
-
ctx.stderr.write('Bundling...\n');
|
|
1045
|
-
const result = await self.esbuildService.build(resolvedEntryPoints, {
|
|
1046
|
-
bundle: flags['bundle'] === 'true',
|
|
1047
|
-
format: flags['format'] || 'esm',
|
|
1048
|
-
target: flags['target'] || 'esnext',
|
|
1049
|
-
platform: flags['platform'] || 'browser',
|
|
1050
|
-
outdir: flags['outfile'] ? undefined : (flags['outdir'] || '/dist'),
|
|
1051
|
-
outfile: flags['outfile'],
|
|
1052
|
-
sourcemap: flags['sourcemap'] === 'true',
|
|
1053
|
-
minify: flags['minify'] === 'true',
|
|
1054
|
-
external: flags['external']?.split(','),
|
|
1055
|
-
});
|
|
1056
|
-
for (const e of result.errors || []) {
|
|
1057
|
-
ctx.stderr.write(`error: ${e.text}\n`);
|
|
1058
|
-
}
|
|
1059
|
-
for (const w of result.warnings || []) {
|
|
1060
|
-
ctx.stderr.write(`warning: ${w.text}\n`);
|
|
1061
|
-
}
|
|
1062
|
-
if (result.errors?.length)
|
|
1063
|
-
return 1;
|
|
1064
|
-
// Write output files to VFS
|
|
1065
|
-
for (const f of result.outputFiles || []) {
|
|
1066
|
-
const outPath = normalizeVfsPath(f.path);
|
|
1067
|
-
const parent = parentVfsPath(outPath);
|
|
1068
|
-
if (parent && !kernelFs.exists(parent))
|
|
1069
|
-
kernelFs.mkdir(parent, { recursive: true });
|
|
1070
|
-
kernelFs.writeFile(outPath, f.contents);
|
|
1071
|
-
ctx.stdout.write(` ${outPath} ${f.contents.length} bytes\n`);
|
|
1072
|
-
}
|
|
1073
|
-
ctx.stderr.write(`Done (${result.outputFiles?.length || 0} output files)\n`);
|
|
1074
|
-
return 0;
|
|
1075
|
-
}
|
|
1076
|
-
catch (e) {
|
|
1077
|
-
ctx.stderr.write(`esbuild error: ${e?.message || e}\n`);
|
|
1078
|
-
return 1;
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
1081
|
-
// ── vite command: start/stop the dev server ──────────────────────────
|
|
1082
|
-
registry.register('vite', async (ctx) => {
|
|
1083
|
-
const args = ctx.args || [];
|
|
1084
|
-
const cwd = normalizeVfsPath(ctx.cwd || '/home/user');
|
|
1085
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
1086
|
-
ctx.stdout.write('Usage: vite [command] [options]\n\n');
|
|
1087
|
-
ctx.stdout.write('Commands:\n');
|
|
1088
|
-
ctx.stdout.write(' (default) Start dev server\n');
|
|
1089
|
-
ctx.stdout.write(' build Build for production\n');
|
|
1090
|
-
ctx.stdout.write(' preview Serve the built dist/\n');
|
|
1091
|
-
ctx.stdout.write(' stop Stop dev server\n\n');
|
|
1092
|
-
ctx.stdout.write('Options:\n');
|
|
1093
|
-
ctx.stdout.write(' --root <dir> Project root\n');
|
|
1094
|
-
ctx.stdout.write(' --port <n> Server port\n');
|
|
1095
|
-
return 0;
|
|
1096
|
-
}
|
|
1097
|
-
self.ensureSqliteFs();
|
|
1098
|
-
const viteConfig = {};
|
|
1099
|
-
for (const cfgName of ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']) {
|
|
1100
|
-
const cfgPath = cwd + '/' + cfgName;
|
|
1101
|
-
if (kernelFs.exists(cfgPath)) {
|
|
1102
|
-
try {
|
|
1103
|
-
let cfgCode = kernelFs.readFileString(cfgPath);
|
|
1104
|
-
// Transform TS to JS
|
|
1105
|
-
if (cfgName.endsWith('.ts')) {
|
|
1106
|
-
if (!self.esbuildService)
|
|
1107
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
1108
|
-
const t = await self.esbuildService.transform(cfgCode, { loader: 'ts', format: 'esm' });
|
|
1109
|
-
cfgCode = t.code;
|
|
1110
|
-
}
|
|
1111
|
-
Object.assign(viteConfig, parseViteConfigSource(cfgCode));
|
|
1112
|
-
}
|
|
1113
|
-
catch (e) {
|
|
1114
|
-
ctx.stderr.write(`Warning: could not parse ${cfgName}: ${e?.message}\n`);
|
|
1115
|
-
}
|
|
1116
|
-
break;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
// ── vite build ──
|
|
1120
|
-
if (args[0] === 'build') {
|
|
1121
|
-
if (!self.esbuildService)
|
|
1122
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
1123
|
-
const htmlPath = cwd + '/index.html';
|
|
1124
|
-
let entryPoint = cwd + '/src/main.tsx';
|
|
1125
|
-
let origHtml = '';
|
|
1126
|
-
try {
|
|
1127
|
-
origHtml = kernelFs.readFileString(htmlPath);
|
|
1128
|
-
const htmlEntrypoint = await findHtmlScriptEntrypoint(origHtml);
|
|
1129
|
-
if (htmlEntrypoint)
|
|
1130
|
-
entryPoint = cwd + '/' + stripLeadingSlashes(htmlEntrypoint);
|
|
1131
|
-
}
|
|
1132
|
-
catch {
|
|
1133
|
-
ctx.stderr.write('Warning: no index.html\n');
|
|
1134
|
-
}
|
|
1135
|
-
if (!kernelFs.exists(entryPoint)) {
|
|
1136
|
-
const alts = [cwd + '/src/main.tsx', cwd + '/src/main.ts', cwd + '/src/index.tsx', cwd + '/src/index.ts'];
|
|
1137
|
-
entryPoint = alts.find(p => kernelFs.exists(p)) || entryPoint;
|
|
1138
|
-
}
|
|
1139
|
-
ctx.stdout.write('Building for production...\n');
|
|
1140
|
-
ctx.stdout.write(' Entry: ' + entryPoint + '\n');
|
|
1141
|
-
const t0 = Date.now();
|
|
1142
|
-
try {
|
|
1143
|
-
const outDir = viteConfig.outDir || 'dist';
|
|
1144
|
-
const distDir = cwd + '/' + outDir;
|
|
1145
|
-
// Detect which packages are installed vs need CDN
|
|
1146
|
-
const nmDir = cwd + '/node_modules';
|
|
1147
|
-
const externals = [];
|
|
1148
|
-
const cdnPackages = [];
|
|
1149
|
-
for (const pkg of ['react', 'react-dom', 'react/jsx-runtime', 'react-dom/client']) {
|
|
1150
|
-
const pkgBase = pkg.split('/')[0];
|
|
1151
|
-
if (!kernelFs.exists(nmDir + '/' + pkgBase)) {
|
|
1152
|
-
externals.push(pkg);
|
|
1153
|
-
if (!cdnPackages.includes(pkgBase))
|
|
1154
|
-
cdnPackages.push(pkgBase);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
if (viteConfig.alias)
|
|
1158
|
-
externals.push(...Object.keys(viteConfig.alias));
|
|
1159
|
-
// Bundle JS
|
|
1160
|
-
const result = await self.esbuildService.build([entryPoint], {
|
|
1161
|
-
bundle: true, format: 'esm', target: 'es2020', platform: 'browser',
|
|
1162
|
-
minify: true, outdir: '/' + distDir + '/assets',
|
|
1163
|
-
external: externals.length > 0 ? externals : undefined,
|
|
1164
|
-
});
|
|
1165
|
-
if (result.errors?.length) {
|
|
1166
|
-
for (const e of result.errors)
|
|
1167
|
-
ctx.stderr.write(' error: ' + e.text + '\n');
|
|
1168
|
-
return 1;
|
|
1169
|
-
}
|
|
1170
|
-
// Generate content hash for filenames
|
|
1171
|
-
let jsContent = '';
|
|
1172
|
-
for (const f of result.outputFiles || []) {
|
|
1173
|
-
jsContent = f.contents;
|
|
1174
|
-
}
|
|
1175
|
-
const hashNum = jsContent.split('').reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0);
|
|
1176
|
-
const hash = (hashNum >>> 0).toString(36).padStart(6, '0');
|
|
1177
|
-
// Write JS with hashed filename
|
|
1178
|
-
const jsFilename = 'index-' + hash + '.js';
|
|
1179
|
-
const jsPath = distDir + '/assets/' + jsFilename;
|
|
1180
|
-
kernelFs.mkdir(distDir + '/assets', { recursive: true });
|
|
1181
|
-
kernelFs.writeFile(jsPath, jsContent);
|
|
1182
|
-
ctx.stdout.write(' \x1b[2m' + outDir + '/assets/' + jsFilename + '\x1b[0m ' + (jsContent.length / 1024).toFixed(2) + ' kB\n');
|
|
1183
|
-
// Collect all CSS files from src/
|
|
1184
|
-
let allCss = '';
|
|
1185
|
-
const collectCss = (dir) => {
|
|
1186
|
-
try {
|
|
1187
|
-
for (const e of kernelFs.readdir(dir)) {
|
|
1188
|
-
const fp = dir + '/' + e.name;
|
|
1189
|
-
if (e.type === 'directory')
|
|
1190
|
-
collectCss(fp);
|
|
1191
|
-
else if (e.name.endsWith('.css')) {
|
|
1192
|
-
try {
|
|
1193
|
-
allCss += kernelFs.readFileString(fp) + '\n';
|
|
1194
|
-
}
|
|
1195
|
-
catch { }
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
catch { }
|
|
1200
|
-
};
|
|
1201
|
-
collectCss(cwd + '/src');
|
|
1202
|
-
const cssFilename = 'index-' + hash + '.css';
|
|
1203
|
-
if (allCss.trim()) {
|
|
1204
|
-
kernelFs.writeFile(distDir + '/assets/' + cssFilename, allCss);
|
|
1205
|
-
ctx.stdout.write(' \x1b[2m' + outDir + '/assets/' + cssFilename + '\x1b[0m ' + (allCss.length / 1024).toFixed(2) + ' kB\n');
|
|
1206
|
-
}
|
|
1207
|
-
// Generate dist/index.html
|
|
1208
|
-
if (origHtml) {
|
|
1209
|
-
const distHtml = await rewriteViteBuildHtml(origHtml, {
|
|
1210
|
-
jsFilename,
|
|
1211
|
-
cssFilename: allCss.trim() ? cssFilename : undefined,
|
|
1212
|
-
removeImportMap: cdnPackages.length === 0,
|
|
1213
|
-
});
|
|
1214
|
-
kernelFs.writeFile(distDir + '/index.html', distHtml);
|
|
1215
|
-
ctx.stdout.write(' \x1b[2m' + outDir + '/index.html\x1b[0m ' + (distHtml.length / 1024).toFixed(2) + ' kB\n');
|
|
1216
|
-
if (cdnPackages.length > 0) {
|
|
1217
|
-
ctx.stdout.write(' \x1b[33mNote: ' + cdnPackages.join(', ') + ' loaded from CDN (not bundled)\x1b[0m\n');
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
ctx.stdout.write('\n\x1b[32m\u2713 built in ' + ((Date.now() - t0) / 1000).toFixed(2) + 's\x1b[0m\n');
|
|
1221
|
-
return 0;
|
|
1222
|
-
}
|
|
1223
|
-
catch (e) {
|
|
1224
|
-
ctx.stderr.write('Build error: ' + (e?.message || e) + '\n');
|
|
1225
|
-
return 1;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
// ── vite preview ──
|
|
1229
|
-
if (args[0] === 'preview') {
|
|
1230
|
-
ctx.stdout.write('Serving dist/ — open ' + self.viteBasePath + '/\n');
|
|
1231
|
-
const distRoot = cwd + '/' + (viteConfig.outDir || 'dist');
|
|
1232
|
-
if (!kernelFs.exists(distRoot)) {
|
|
1233
|
-
ctx.stderr.write('dist/ not found. Run vite build first.\n');
|
|
1234
|
-
return 1;
|
|
1235
|
-
}
|
|
1236
|
-
// Start vite on the dist directory
|
|
1237
|
-
if (!self.esbuildService)
|
|
1238
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
1239
|
-
if (self.viteDevServer?.isRunning)
|
|
1240
|
-
self.viteDevServer.stop();
|
|
1241
|
-
const previewBasePath = self.viteBasePath;
|
|
1242
|
-
// process metadata support: same long-running treatment as the
|
|
1243
|
-
// dev path, just on the dist/ directory.
|
|
1244
|
-
const previewPort = viteConfig.port || 4173; // vite preview default
|
|
1245
|
-
const previewProcEntry = self.processes.spawn('vite preview (' + distRoot + ')', [], distRoot, { longRunning: true });
|
|
1246
|
-
self.viteDevServer = new ViteDevServer({
|
|
1247
|
-
vfs: self.sqliteFs, esbuild: self.esbuildService, root: distRoot,
|
|
1248
|
-
onHmrMessage: () => { },
|
|
1249
|
-
sql: self.ctx.storage.sql,
|
|
1250
|
-
basePath: previewBasePath,
|
|
1251
|
-
env: self.env,
|
|
1252
|
-
ctx: self.ctx,
|
|
1253
|
-
port: previewPort,
|
|
1254
|
-
pid: previewProcEntry.pid,
|
|
1255
|
-
processes: self.processes,
|
|
1256
|
-
});
|
|
1257
|
-
self.viteDevServer.start();
|
|
1258
|
-
try {
|
|
1259
|
-
const previewStub = makeLongRunningPortStub(self.viteDevServer);
|
|
1260
|
-
self.portRegistry.bindFacetStub(previewProcEntry.pid, previewStub);
|
|
1261
|
-
await clearPortCapability(self, previewPort);
|
|
1262
|
-
self.portRegistry.register(previewPort, previewProcEntry.pid);
|
|
1263
|
-
self._viteShimPid = previewProcEntry.pid;
|
|
1264
|
-
self._viteShimPort = previewPort;
|
|
1265
|
-
}
|
|
1266
|
-
catch { }
|
|
1267
|
-
try {
|
|
1268
|
-
await self.ctx.storage.put(VITE_CONFIG_KEY, { root: distRoot, basePath: previewBasePath, port: previewPort });
|
|
1269
|
-
}
|
|
1270
|
-
catch { }
|
|
1271
|
-
ctx.stdout.write('Serving at ' + previewBasePath + '/ \x1b[2m(pid=' + previewProcEntry.pid + ', port=' + previewPort + ')\x1b[0m\n');
|
|
1272
|
-
return 0;
|
|
1273
|
-
}
|
|
1274
|
-
// ── vite stop ──
|
|
1275
|
-
if (args[0] === 'stop') {
|
|
1276
|
-
let stopped = false;
|
|
1277
|
-
if (self.cirrusReal?.isRunning) {
|
|
1278
|
-
self.cirrusReal.stop(self.ctx);
|
|
1279
|
-
self.cirrusReal = null;
|
|
1280
|
-
stopped = true;
|
|
1281
|
-
}
|
|
1282
|
-
if (self.viteDevServer?.isRunning) {
|
|
1283
|
-
self.viteDevServer.stop();
|
|
1284
|
-
self.viteDevServer = null;
|
|
1285
|
-
try {
|
|
1286
|
-
await self.ctx.storage.delete(VITE_CONFIG_KEY);
|
|
1287
|
-
}
|
|
1288
|
-
catch { }
|
|
1289
|
-
stopped = true;
|
|
1290
|
-
}
|
|
1291
|
-
// Primitive #3 teardown — symmetric with the start path. Always
|
|
1292
|
-
// safe to call: unregisterByPid is idempotent, exit() guards
|
|
1293
|
-
// against re-marking already-terminal entries.
|
|
1294
|
-
if (self._viteShimPid != null) {
|
|
1295
|
-
try {
|
|
1296
|
-
self.portRegistry.unregisterByPid(self._viteShimPid);
|
|
1297
|
-
}
|
|
1298
|
-
catch { }
|
|
1299
|
-
try {
|
|
1300
|
-
self.processes.exit(self._viteShimPid, 0);
|
|
1301
|
-
}
|
|
1302
|
-
catch { }
|
|
1303
|
-
notifyTerminalEvent(self.terminal, {
|
|
1304
|
-
type: 'exit', pid: self._viteShimPid, code: 0, command: 'vite',
|
|
1305
|
-
});
|
|
1306
|
-
self._viteShimPid = null;
|
|
1307
|
-
self._viteShimPort = null;
|
|
1308
|
-
}
|
|
1309
|
-
if (stopped) {
|
|
1310
|
-
ctx.stdout.write('\x1b[33mDev server stopped.\x1b[0m\n');
|
|
1311
|
-
}
|
|
1312
|
-
else {
|
|
1313
|
-
ctx.stdout.write('No dev server running.\n');
|
|
1314
|
-
}
|
|
1315
|
-
return 0;
|
|
1316
|
-
}
|
|
1317
|
-
// ── vite (default: dev server) ──
|
|
1318
|
-
let vfsRoot = cwd;
|
|
1319
|
-
for (let i = 0; i < args.length; i++) {
|
|
1320
|
-
if (args[i] === '--root' && args[i + 1])
|
|
1321
|
-
vfsRoot = resolveVfsPath(args[i + 1], cwd);
|
|
1322
|
-
}
|
|
1323
|
-
if (viteConfig.root && viteConfig.root !== '.') {
|
|
1324
|
-
vfsRoot = resolveVfsPath(viteConfig.root, cwd);
|
|
1325
|
-
}
|
|
1326
|
-
vfsRoot = normalizeVfsPath(vfsRoot);
|
|
1327
|
-
// Argv expansion: package.json scripts commonly write
|
|
1328
|
-
// `--port ${PORT:-3000}`. Resolve it once and feed both Vite
|
|
1329
|
-
// backends so `/api/stats`, `/preview/`, and port tabs agree.
|
|
1330
|
-
const expandedArgs = expandArgvShellDefaults(args, ctx.env || {});
|
|
1331
|
-
const vitePortDefault = 5173;
|
|
1332
|
-
const resolvedPort = resolveLongRunningPort({
|
|
1333
|
-
argv: expandedArgs,
|
|
1334
|
-
env: ctx.env,
|
|
1335
|
-
configPort: viteConfig.port,
|
|
1336
|
-
fallback: vitePortDefault,
|
|
1337
|
-
});
|
|
1338
|
-
// ── Preflight: node_modules guard ────────────────────────────────────
|
|
1339
|
-
// Direct `vite` invocation requires installed deps. Bail loudly BEFORE
|
|
1340
|
-
// spawning a dev server that would just serve broken modules and
|
|
1341
|
-
// confuse the user. --force / --no-install-check bypasses the check.
|
|
1342
|
-
const bypassInstallCheck = expandedArgs.includes('--force') || expandedArgs.includes('--no-install-check');
|
|
1343
|
-
if (!bypassInstallCheck) {
|
|
1344
|
-
const guard = checkNodeModulesGuard(kernelFs, vfsRoot);
|
|
1345
|
-
if (guard.missing) {
|
|
1346
|
-
ctx.stderr.write('\x1b[31m\u2718\x1b[0m \x1b[1mnode_modules/ not found\x1b[0m' +
|
|
1347
|
-
(guard.depCount > 0 ? ` (${guard.depCount} dependencies declared)` : '') + '\n' +
|
|
1348
|
-
' Run \x1b[36mnpm install\x1b[0m in ' + vfsRoot + ' first,\n' +
|
|
1349
|
-
' or re-run with \x1b[36m--force\x1b[0m to skip this check.\n');
|
|
1350
|
-
return 1;
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
if (self.viteDevServer?.isRunning)
|
|
1354
|
-
self.viteDevServer.stop();
|
|
1355
|
-
// ── Real-vite mode (Phase 0 spike, opt-in) ─────────────────────────
|
|
1356
|
-
// NIMBUS_REAL_VITE=1 or `nimbusDevServer: 'real'` in vite.config.ts
|
|
1357
|
-
// routes the session through a dynamic-worker facet running the
|
|
1358
|
-
// real `vite` npm package. The in-process Cirrus shim is bypassed.
|
|
1359
|
-
//
|
|
1360
|
-
// This is EXPERIMENTAL and gated behind an explicit opt-in. Any
|
|
1361
|
-
// error here falls back to Cirrus by the user re-running without
|
|
1362
|
-
// the env flag — we do not silently fall back (fidelity over
|
|
1363
|
-
// magic).
|
|
1364
|
-
const sessionEnv = (ctx && ctx.env) || {};
|
|
1365
|
-
const useReal = shouldUseRealVite({ env: sessionEnv, viteConfig });
|
|
1366
|
-
if (useReal) {
|
|
1367
|
-
const vitePort = resolvedPort;
|
|
1368
|
-
const previewBasePath = self.viteBasePath;
|
|
1369
|
-
// One boot path, shared with hibernation-restore (start-real-vite.ts):
|
|
1370
|
-
// pre-bundles the user's vite.config, boots the facet, registers the
|
|
1371
|
-
// port, and persists the vite-config so a woken session rebuilds the
|
|
1372
|
-
// same real-vite server. Only the banner below is command-specific.
|
|
1373
|
-
const { cirrusReal, userConfigBundle, cfgPath } = await startRealVite(self, {
|
|
1374
|
-
root: vfsRoot,
|
|
1375
|
-
port: vitePort,
|
|
1376
|
-
basePath: previewBasePath,
|
|
1377
|
-
configDir: cwd,
|
|
1378
|
-
signal: ctx.signal,
|
|
1379
|
-
onConfigError: (msg) => {
|
|
1380
|
-
ctx.stderr.write('\x1b[33m!\x1b[0m vite.config bundling failed: ' + msg + '\n');
|
|
1381
|
-
ctx.stderr.write(' Real-vite will run with default config.\n');
|
|
1382
|
-
},
|
|
1383
|
-
});
|
|
1384
|
-
{
|
|
1385
|
-
// ── Boot banner (§4.3 of PHASE2-REAL-VITE-PLAN.md) ──────
|
|
1386
|
-
const snap = cirrusReal.stats.snapshot;
|
|
1387
|
-
ctx.stdout.write('\n\x1b[1;36m Nimbus: real-vite mode\x1b[0m \x1b[2m(experimental, Phase 1-4)\x1b[0m\n\n');
|
|
1388
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Preview: \x1b[36m' + previewBasePath + '/\x1b[0m\n');
|
|
1389
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Vite: ' + cirrusReal.stats.viteVersion + ' (bundled)\n');
|
|
1390
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Root: ' + vfsRoot + '\n');
|
|
1391
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Port: ' + vitePort + ' \x1b[2m(virtual routing key)\x1b[0m\n');
|
|
1392
|
-
if (snap) {
|
|
1393
|
-
const kb = (snap.totalBytes / 1024).toFixed(1);
|
|
1394
|
-
const pkgJson = snap.packageJsonCount;
|
|
1395
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Snapshot: ' + snap.fileCount + ' files / ' +
|
|
1396
|
-
kb + ' KB ' +
|
|
1397
|
-
(pkgJson ? '\x1b[2m(incl. ' + pkgJson + ' package.json, rest lazy)\x1b[0m' : '') + '\n');
|
|
1398
|
-
}
|
|
1399
|
-
if (userConfigBundle) {
|
|
1400
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Config: ' + cfgPath + ' \x1b[2m(' +
|
|
1401
|
-
(userConfigBundle.length / 1024).toFixed(0) + ' KB bundled)\x1b[0m\n');
|
|
1402
|
-
}
|
|
1403
|
-
ctx.stdout.write('\n \x1b[2mWorks:\x1b[0m @vitejs/plugin-react, JSX/TSX transforms, SPA fallback, HMR.\n');
|
|
1404
|
-
ctx.stdout.write(' \x1b[2mPartial:\x1b[0m other plugins (Babel-family generally OK; SWC/Rolldown blocked).\n');
|
|
1405
|
-
ctx.stdout.write(' \x1b[2mBlocked:\x1b[0m vite build (rolldown needs node:wasi). Use cirrus for build.\n');
|
|
1406
|
-
ctx.stdout.write('\n \x1b[2mRun \x1b[0mvite stop\x1b[2m, or \x1b[0mNIMBUS_REAL_VITE=0 vite\x1b[2m for Cirrus.\x1b[0m\n\n');
|
|
1407
|
-
return 0;
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
if (!self.esbuildService)
|
|
1411
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
1412
|
-
const previewBasePath = self.viteBasePath;
|
|
1413
|
-
const viteDefine = viteConfig.define;
|
|
1414
|
-
// Vite dev servers are represented as long-running process-table
|
|
1415
|
-
// entries and port-registry handlers, so `ps`, logs, preview routing,
|
|
1416
|
-
// `vite stop`, and `kill <pid>` share the same lifecycle primitives.
|
|
1417
|
-
// Allocate PID FIRST so we can plumb it into ViteDevServer's
|
|
1418
|
-
// process-log wiring at construction time. The PID stays valid
|
|
1419
|
-
// for the life of this dev-server instance; subsequent log lines
|
|
1420
|
-
// emitted by ViteDevServer flow into the pid's stderr ring,
|
|
1421
|
-
// visible in the Process tab.
|
|
1422
|
-
//
|
|
1423
|
-
// Long-running handoff (bin-spawn contract): when invoked from a
|
|
1424
|
-
// wrapper that already allocated a pid (`npm run dev` via
|
|
1425
|
-
// shellExecuteTracked, or the npm-bin resolver), ADOPT that pid instead
|
|
1426
|
-
// of spawning a second one. One pid, one start banner, no false exit —
|
|
1427
|
-
// the wrapper stays `running` in /api/processes with this port.
|
|
1428
|
-
const binSpawn = ctx.__nimbusBinSpawn;
|
|
1429
|
-
const adoptedEntry = binSpawn?.skipSpawn && binSpawn.callerPid != null
|
|
1430
|
-
? self.processes.get(binSpawn.callerPid)
|
|
1431
|
-
: undefined;
|
|
1432
|
-
const handedOff = adoptedEntry != null;
|
|
1433
|
-
const viteProcEntry = adoptedEntry ?? self.processes.spawn('vite (' + vfsRoot + ')', expandedArgs, vfsRoot, { longRunning: true });
|
|
1434
|
-
if (handedOff)
|
|
1435
|
-
self.processes.setLongRunning(viteProcEntry.pid);
|
|
1436
|
-
self.viteDevServer = new ViteDevServer({
|
|
1437
|
-
vfs: self.sqliteFs,
|
|
1438
|
-
esbuild: self.esbuildService,
|
|
1439
|
-
root: vfsRoot,
|
|
1440
|
-
port: resolvedPort,
|
|
1441
|
-
aliases: viteConfig.alias,
|
|
1442
|
-
define: viteDefine,
|
|
1443
|
-
onHmrMessage: (msg) => {
|
|
1444
|
-
if (self.terminal)
|
|
1445
|
-
try {
|
|
1446
|
-
self.terminal.ws.send(JSON.stringify({ type: 'hmr', data: msg }));
|
|
1447
|
-
}
|
|
1448
|
-
catch { }
|
|
1449
|
-
},
|
|
1450
|
-
sql: self.ctx.storage.sql,
|
|
1451
|
-
injectBasename: viteConfig.injectBasename,
|
|
1452
|
-
basePath: previewBasePath,
|
|
1453
|
-
env: self.env,
|
|
1454
|
-
ctx: self.ctx,
|
|
1455
|
-
// process diagnostics support: wire dev-server diagnostics into the
|
|
1456
|
-
// supervisor's per-PID log store so the Process tab is no
|
|
1457
|
-
// longer silent after the banner.
|
|
1458
|
-
pid: viteProcEntry.pid,
|
|
1459
|
-
processes: self.processes,
|
|
1460
|
-
});
|
|
1461
|
-
self.viteDevServer.start();
|
|
1462
|
-
try {
|
|
1463
|
-
await self.ctx.storage.put(VITE_CONFIG_KEY, {
|
|
1464
|
-
root: vfsRoot, aliases: viteConfig.alias, define: viteDefine,
|
|
1465
|
-
injectBasename: viteConfig.injectBasename, basePath: previewBasePath,
|
|
1466
|
-
port: resolvedPort,
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
catch { }
|
|
1470
|
-
// Register the port and build the long-running stub. The stub
|
|
1471
|
-
// forwards into the in-process viteDevServer through the generic
|
|
1472
|
-
// long-running adapter — same hook every future long-running
|
|
1473
|
-
// facet uses (Express, Bun.serve, http.createServer().listen()).
|
|
1474
|
-
const viteStub = makeLongRunningPortStub(self.viteDevServer);
|
|
1475
|
-
self.portRegistry.bindFacetStub(viteProcEntry.pid, viteStub);
|
|
1476
|
-
await clearPortCapability(self, resolvedPort);
|
|
1477
|
-
self.portRegistry.register(resolvedPort, viteProcEntry.pid);
|
|
1478
|
-
// Track the wiring so `vite stop` and crash-handlers can tear it
|
|
1479
|
-
// down without searching the registry.
|
|
1480
|
-
self._viteShimPid = viteProcEntry.pid;
|
|
1481
|
-
self._viteShimPort = resolvedPort;
|
|
1482
|
-
// Spawn / long-running event for the Process tab UI. Mirrors the
|
|
1483
|
-
// shellExecuteTracked banner so the user sees the same shape no
|
|
1484
|
-
// matter how vite was invoked. Suppressed on handoff — the wrapper
|
|
1485
|
-
// already emitted the single start banner and spawn event for this pid.
|
|
1486
|
-
if (!handedOff) {
|
|
1487
|
-
if (self.terminal) {
|
|
1488
|
-
self.terminal.write(`\x1b[2m[shell started (long-running): pid=${viteProcEntry.pid} cmd="vite ${expandedArgs.join(' ')}"]\x1b[0m\r\n`);
|
|
1489
|
-
}
|
|
1490
|
-
notifyTerminalEvent(self.terminal, {
|
|
1491
|
-
type: 'spawn',
|
|
1492
|
-
pid: viteProcEntry.pid,
|
|
1493
|
-
command: 'vite ' + expandedArgs.join(' '),
|
|
1494
|
-
longRunning: true,
|
|
1495
|
-
attachedTty: false,
|
|
1496
|
-
});
|
|
1497
|
-
}
|
|
1498
|
-
// Banner — reports the resolved port and PID so the user can
|
|
1499
|
-
// verify the multi-target routing.
|
|
1500
|
-
ctx.stdout.write('\n\x1b[1;36m Nimbus Vite Dev Server\x1b[0m\n\n');
|
|
1501
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Preview: \x1b[36m' + previewBasePath + '/\x1b[0m');
|
|
1502
|
-
if (resolvedPort !== vitePortDefault) {
|
|
1503
|
-
ctx.stdout.write(' \x1b[2m(also: ' + previewBasePath + '/?port=' + resolvedPort + ')\x1b[0m');
|
|
1504
|
-
}
|
|
1505
|
-
ctx.stdout.write('\n');
|
|
1506
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Root: ' + vfsRoot + '\n');
|
|
1507
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Port: ' + resolvedPort + ' \x1b[2m(pid=' + viteProcEntry.pid + ')\x1b[0m\n');
|
|
1508
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Transforms: .ts .tsx .jsx (React JSX automatic)\n');
|
|
1509
|
-
if (viteConfig.alias)
|
|
1510
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Aliases: ' + Object.keys(viteConfig.alias).join(', ') + '\n');
|
|
1511
|
-
if (viteDefine)
|
|
1512
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Define: ' + Object.keys(viteDefine).join(', ') + '\n');
|
|
1513
|
-
const twCfg = [vfsRoot + '/tailwind.config.js', vfsRoot + '/tailwind.config.ts'].find(p => kernelFs.exists(p));
|
|
1514
|
-
if (twCfg)
|
|
1515
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Tailwind: edge-vendored Play CDN \x1b[2m(detected)\x1b[0m\n');
|
|
1516
|
-
ctx.stdout.write('\n \x1b[2mRun \x1b[0mvite stop\x1b[2m, or \x1b[0mkill ' + viteProcEntry.pid + '\x1b[2m, to stop.\x1b[0m\n\n');
|
|
1517
|
-
return 0;
|
|
1518
|
-
});
|
|
1519
|
-
// ── nimbus-wrangler / wrangler command: Worker dev server ─────────────
|
|
1520
|
-
//
|
|
1521
|
-
// `wrangler` is registered as a transparent alias for `nimbus-wrangler`
|
|
1522
|
-
// so projects with `"dev": "wrangler dev"` in package.json Just Work.
|
|
1523
|
-
// The shared handler below takes an extra `invokedAs` flag so we can
|
|
1524
|
-
// - print a one-shot "DO-in-DO mode" banner on the first wrangler
|
|
1525
|
-
// invocation per session (so users know they're getting a compat
|
|
1526
|
-
// layer, not real wrangler)
|
|
1527
|
-
// - silently strip wrangler-specific flags (--ip, --port, etc.) that
|
|
1528
|
-
// have no meaning inside a DO.
|
|
1529
|
-
const wranglerHandler = (invokedAs) => async (ctx) => {
|
|
1530
|
-
const rawArgs = ctx.args || [];
|
|
1531
|
-
// Filter wrangler-only flags early (works for both invocation paths;
|
|
1532
|
-
// a no-op for nimbus-wrangler since it doesn't accept them anyway).
|
|
1533
|
-
const { args, ignored } = filterWranglerFlags(rawArgs);
|
|
1534
|
-
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
1535
|
-
ctx.stdout.write(`Usage: ${invokedAs} dev [options]\n\n`);
|
|
1536
|
-
ctx.stdout.write('Run your Cloudflare Worker locally on the actual CF runtime\n');
|
|
1537
|
-
ctx.stdout.write('(DO-in-DO via env.LOADER — workerd in a workerd).\n\n');
|
|
1538
|
-
ctx.stdout.write('Commands:\n');
|
|
1539
|
-
ctx.stdout.write(' dev Start the dev server\n');
|
|
1540
|
-
ctx.stdout.write(' stop Stop the dev server\n\n');
|
|
1541
|
-
ctx.stdout.write('Options:\n');
|
|
1542
|
-
ctx.stdout.write(' --root <dir> Project root (default: cwd)\n\n');
|
|
1543
|
-
if (invokedAs === 'wrangler') {
|
|
1544
|
-
ctx.stdout.write('Note: \x1b[2minside Nimbus, `wrangler` is an alias for\x1b[0m \x1b[36mnimbus-wrangler\x1b[0m.\n');
|
|
1545
|
-
ctx.stdout.write('Most real-wrangler flags (--ip, --port, --local, --log-level, ...)\n');
|
|
1546
|
-
ctx.stdout.write('are silently ignored because the DO provides its own routing.\n');
|
|
1547
|
-
}
|
|
1548
|
-
return 0;
|
|
1549
|
-
}
|
|
1550
|
-
if (args[0] === 'stop') {
|
|
1551
|
-
if (self.nimbusWrangler?.isRunning) {
|
|
1552
|
-
self.nimbusWrangler.stop();
|
|
1553
|
-
ctx.stdout.write('\x1b[33mWorker dev server stopped.\x1b[0m\n');
|
|
1554
|
-
}
|
|
1555
|
-
else {
|
|
1556
|
-
ctx.stdout.write('No Worker dev server running.\n');
|
|
1557
|
-
}
|
|
1558
|
-
return 0;
|
|
1559
|
-
}
|
|
1560
|
-
if (args[0] !== 'dev') {
|
|
1561
|
-
ctx.stderr.write(`Unknown command: ${args[0]}. Use "${invokedAs} dev" or "${invokedAs} --help".\n`);
|
|
1562
|
-
return 1;
|
|
1563
|
-
}
|
|
1564
|
-
// First-run banner — only when invoked as `wrangler`, and only once
|
|
1565
|
-
// per session. Makes it OBVIOUS to the user that they're not running
|
|
1566
|
-
// real wrangler, and that Nimbus is doing something different.
|
|
1567
|
-
if (invokedAs === 'wrangler' && !self.wranglerAliasBannerShown) {
|
|
1568
|
-
ctx.stdout.write('\x1b[2m\u2388 wrangler (Nimbus DO-in-DO mode) — bundling via esbuild-wasm, running via env.LOADER\x1b[0m\n');
|
|
1569
|
-
self.wranglerAliasBannerShown = true;
|
|
1570
|
-
}
|
|
1571
|
-
// Report ignored flags (also one-shot — if user sees it once per
|
|
1572
|
-
// session that's enough to spot a typo; no need to spam on rebuilds).
|
|
1573
|
-
if (ignored.length > 0 && invokedAs === 'wrangler') {
|
|
1574
|
-
ctx.stdout.write('\x1b[2m ignored wrangler flags: ' + ignored.join(' ') + '\x1b[0m\n');
|
|
1575
|
-
}
|
|
1576
|
-
// Lazy-init esbuild
|
|
1577
|
-
if (!self.esbuildService) {
|
|
1578
|
-
self.ensureSqliteFs();
|
|
1579
|
-
self.esbuildService = new EsbuildService(kernelFs);
|
|
1580
|
-
}
|
|
1581
|
-
// Parse --root flag; default to the shell cwd so `npm run dev` from
|
|
1582
|
-
// a project directory picks up that project's wrangler.jsonc.
|
|
1583
|
-
let root = ctx.cwd || '/home/user';
|
|
1584
|
-
for (let i = 1; i < args.length; i++) {
|
|
1585
|
-
if (args[i] === '--root' && args[i + 1])
|
|
1586
|
-
root = args[i + 1];
|
|
1587
|
-
}
|
|
1588
|
-
// Stop existing
|
|
1589
|
-
if (self.nimbusWrangler?.isRunning)
|
|
1590
|
-
self.nimbusWrangler.stop();
|
|
1591
|
-
const vfsRoot = resolveVfsPath(root, ctx.cwd || '/home/user');
|
|
1592
|
-
// Pre-flight: read the wrangler config ourselves and call out any
|
|
1593
|
-
// binding fields nimbus-wrangler can't provide. NimbusWrangler will
|
|
1594
|
-
// still try to bundle + load, but user sees up-front why their
|
|
1595
|
-
// Worker may fail when it tries to access a missing binding.
|
|
1596
|
-
const unsupportedFields = detectUnsupportedWranglerConfig(kernelFs, vfsRoot);
|
|
1597
|
-
ctx.stdout.write('\n');
|
|
1598
|
-
ctx.stdout.write('\x1b[1;35m ' + (invokedAs === 'wrangler' ? 'Wrangler' : 'Nimbus Wrangler') + ' Dev\x1b[0m\n\n');
|
|
1599
|
-
if (unsupportedFields.length > 0) {
|
|
1600
|
-
ctx.stderr.write('\x1b[33m\u26A0\x1b[0m \x1b[1mNimbus-incompatible wrangler.jsonc fields detected:\x1b[0m\n');
|
|
1601
|
-
for (const f of unsupportedFields) {
|
|
1602
|
-
ctx.stderr.write(' - \x1b[33m' + f + '\x1b[0m\n');
|
|
1603
|
-
}
|
|
1604
|
-
ctx.stderr.write(' These bindings are NOT provisioned inside nimbus-wrangler. Your Worker\n' +
|
|
1605
|
-
' will get \x1b[2mundefined\x1b[0m when it tries to access them, which typically\n' +
|
|
1606
|
-
' causes a runtime TypeError. The bundle will still build and load.\n' +
|
|
1607
|
-
' \x1b[2mDeploy with real wrangler to get the real bindings.\x1b[0m\n\n');
|
|
1608
|
-
}
|
|
1609
|
-
self.nimbusWrangler = new NimbusWrangler({
|
|
1610
|
-
vfs: kernelFs,
|
|
1611
|
-
vfsEvents: sqliteFs.events,
|
|
1612
|
-
esbuild: self.esbuildService,
|
|
1613
|
-
env: self.env,
|
|
1614
|
-
// Supervisor DO ctx — required for ctx.facets.get() when
|
|
1615
|
-
// synthesizing durable_objects bindings on the inner Worker.
|
|
1616
|
-
ctx: self.ctx,
|
|
1617
|
-
root: vfsRoot,
|
|
1618
|
-
onLog: (msg) => {
|
|
1619
|
-
if (self.terminal) {
|
|
1620
|
-
try {
|
|
1621
|
-
self.terminal.write(msg);
|
|
1622
|
-
}
|
|
1623
|
-
catch { }
|
|
1624
|
-
}
|
|
1625
|
-
},
|
|
1626
|
-
onHmrMessage: (msg) => {
|
|
1627
|
-
if (self.terminal) {
|
|
1628
|
-
try {
|
|
1629
|
-
self.terminal.ws.send(JSON.stringify({ type: 'hmr', data: msg }));
|
|
1630
|
-
}
|
|
1631
|
-
catch { }
|
|
1632
|
-
}
|
|
1633
|
-
},
|
|
1634
|
-
});
|
|
1635
|
-
const ok = await self.nimbusWrangler.start();
|
|
1636
|
-
if (!ok) {
|
|
1637
|
-
ctx.stderr.write(' \x1b[31mFailed to start Worker dev server.\x1b[0m\n');
|
|
1638
|
-
return 1;
|
|
1639
|
-
}
|
|
1640
|
-
const cfg = self.nimbusWrangler.stats;
|
|
1641
|
-
// runtime primitive support (P5): banner advertises the canonical
|
|
1642
|
-
// `/__nimbus/worker/` route. The legacy `/worker/` URL is still
|
|
1643
|
-
// accepted for one release (Sunset 2027-01-01) but new sessions
|
|
1644
|
-
// are pointed at the namespaced form.
|
|
1645
|
-
const workerBase = (self.sessionBasePath || '') + '/__nimbus/worker';
|
|
1646
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Worker: \x1b[36m' + workerBase + '/\x1b[0m\n');
|
|
1647
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Name: ' + cfg.name + '\n');
|
|
1648
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Entry: ' + cfg.main + '\n');
|
|
1649
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Root: ' + cfg.root + '\n');
|
|
1650
|
-
ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Reload: on file change\n\n');
|
|
1651
|
-
ctx.stdout.write(' \x1b[2mRun \x1b[0m' + invokedAs + ' stop\x1b[2m to stop.\x1b[0m\n\n');
|
|
1652
|
-
return 0;
|
|
1653
|
-
};
|
|
1654
|
-
registry.register('nimbus-wrangler', wranglerHandler('nimbus-wrangler'));
|
|
1655
|
-
registry.register('wrangler', wranglerHandler('wrangler'));
|
|
1656
|
-
// ── npm-fast command: parallel npm install (v2 — batched writes) ────
|
|
1657
|
-
registry.register('npm-fast', async (ctx) => {
|
|
1658
|
-
const args = ctx.args || [];
|
|
1659
|
-
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
1660
|
-
ctx.stdout.write('Usage: npm-fast install <packages...>\n\n');
|
|
1661
|
-
ctx.stdout.write('Nimbus npm v2 — batched VFS writes, content-addressed cache.\n');
|
|
1662
|
-
ctx.stdout.write('Handles 100+ dependency projects without crashing.\n');
|
|
1663
|
-
return 0;
|
|
1664
|
-
}
|
|
1665
|
-
if (args[0] !== 'install' && args[0] !== 'i') {
|
|
1666
|
-
ctx.stderr.write('Only "npm-fast install" is supported. Use "npm" for other commands.\n');
|
|
1667
|
-
return 1;
|
|
1668
|
-
}
|
|
1669
|
-
const packages = args.slice(1).filter((a) => !a.startsWith('-'));
|
|
1670
|
-
if (packages.length === 0) {
|
|
1671
|
-
ctx.stderr.write('Specify packages to install: npm-fast install react react-dom\n');
|
|
1672
|
-
return 1;
|
|
1673
|
-
}
|
|
1674
|
-
self.ensureSqliteFs();
|
|
1675
|
-
const cwd = normalizeVfsPath(ctx.cwd || '/home/user');
|
|
1676
|
-
// Ensure package.json exists
|
|
1677
|
-
const pkgJsonPath = cwd + '/package.json';
|
|
1678
|
-
if (!kernelFs.exists(pkgJsonPath)) {
|
|
1679
|
-
kernelFs.writeFile(pkgJsonPath, '{"name":"project","version":"1.0.0","dependencies":{}}\n');
|
|
1680
|
-
}
|
|
1681
|
-
ctx.stdout.write('\x1b[36mNimbus npm v2 (batched writes)\x1b[0m\n');
|
|
1682
|
-
await self.ensureNpmInstaller((msg) => {
|
|
1683
|
-
ctx.stdout.write('[npm] ' + msg + '\n');
|
|
1684
|
-
});
|
|
1685
|
-
const result = await self.npmInstaller.install(cwd, { packages, pid: ctx.pid });
|
|
1686
|
-
if (result.failed.length > 0) {
|
|
1687
|
-
ctx.stderr.write('\x1b[31mFailed: ' + result.failed.join(', ') + '\x1b[0m\n');
|
|
1688
|
-
}
|
|
1689
|
-
// [HONEST INSTALL MESSAGE P0a] Yellow + "(N failed, see above)"
|
|
1690
|
-
// when partial. Green only when failed.length === 0. Pre-fix the
|
|
1691
|
-
// green line printed unconditionally — see user transcript line
|
|
1692
|
-
// 831 ("added 264 packages" with 353 silent failures above).
|
|
1693
|
-
const partial = result.failed.length > 0;
|
|
1694
|
-
const color = partial ? '\x1b[33m' : '\x1b[32m';
|
|
1695
|
-
const suffix = partial ? ` (${result.failed.length} failed, see above)` : '';
|
|
1696
|
-
ctx.stdout.write(`\n${color}added ${result.installed.length} packages (${result.totalFiles} files) in ${(result.elapsed / 1000).toFixed(1)}s${suffix}\x1b[0m\n`);
|
|
1697
|
-
if (result.cachedHits > 0) {
|
|
1698
|
-
ctx.stdout.write(`\x1b[2m (${result.cachedHits} from cache)\x1b[0m\n`);
|
|
1699
|
-
}
|
|
1700
|
-
return result.failed.length > 0 ? 1 : 0;
|
|
1701
|
-
});
|
|
1702
|
-
// Primitive #7: patch NIMBUS_SESSION_ID into the live shell env.
|
|
1703
|
-
// sessionBasePath is "/s/<sid>" set by the X-Nimbus-Base header on
|
|
1704
|
-
// the first /ws upgrade — by the time initSession runs (after the
|
|
1705
|
-
// ws handshake), it's populated. Older /ws-pre-base callers see
|
|
1706
|
-
// an empty string, which is the safe placeholder (no false id).
|
|
1707
|
-
//
|
|
1708
|
-
// We patch the live env (not the overlay the workspace was built
|
|
1709
|
-
// from) so persisted shell state on warm-rejoin still picks up the
|
|
1710
|
-
// SAME session id — the DO's name is stable across hibernation
|
|
1711
|
-
// cycles. Any user `export NIMBUS_SESSION_ID=...` would have been
|
|
1712
|
-
// persisted to persisted.env and the overlay's spread would have
|
|
1713
|
-
// overridden the empty placeholder; we only set when the live env
|
|
1714
|
-
// is empty (don't clobber a user-set value).
|
|
1715
332
|
const sessionIdFromBase = (self.sessionBasePath || '').replace(/^\/s\//, '');
|
|
1716
333
|
if (sessionIdFromBase) {
|
|
1717
334
|
// Shell.env is declared private but mutable at runtime — there's
|
|
@@ -1726,899 +343,13 @@ export async function initSession(self, ws) {
|
|
|
1726
343
|
shellAny.env.NIMBUS_SESSION_ID = sessionIdFromBase;
|
|
1727
344
|
}
|
|
1728
345
|
}
|
|
1729
|
-
// Rehydrate cwd if persisted. The Shell ctor defaults this.cwd to
|
|
1730
|
-
// env.HOME, and restoring after construction is safe because no
|
|
1731
|
-
// interactive command has run yet.
|
|
1732
346
|
if (persisted.cwd) {
|
|
1733
347
|
try {
|
|
1734
348
|
self.shell.setCwd(persisted.cwd);
|
|
1735
349
|
}
|
|
1736
350
|
catch { /* fail-soft */ }
|
|
1737
351
|
}
|
|
1738
|
-
|
|
1739
|
-
// ── Readline-parity keybindings (Ctrl+K, Ctrl+W, Alt+B, Alt+F, Alt+D,
|
|
1740
|
-
// Ctrl+Y, Ctrl+T, Ctrl+L, Ctrl+R, Alt+. , Ctrl+←/→, Alt+←/→, Linux
|
|
1741
|
-
// Home/End variants, Ctrl+B/F/N/P, …). Installed AFTER Heredoc so
|
|
1742
|
-
// its handleInput wrapper takes precedence when heredoc mode is
|
|
1743
|
-
// active. ──
|
|
1744
|
-
LineEditorExtender.install(self.shell, self.terminal);
|
|
1745
|
-
// ── Wire npm/npx with shellExecute ──
|
|
1746
|
-
const shell = self.shell;
|
|
1747
|
-
const shellExecute = async (cmd, cmdCtx) => {
|
|
1748
|
-
const stdin = cmdCtx.stdin && cmdCtx.stdin !== cmdCtx.terminalStdin
|
|
1749
|
-
? await cmdCtx.stdin.readAll()
|
|
1750
|
-
: undefined;
|
|
1751
|
-
const result = await shell.execute(cmd, {
|
|
1752
|
-
cwd: cmdCtx.cwd,
|
|
1753
|
-
env: cmdCtx.env,
|
|
1754
|
-
onStdout: (d) => cmdCtx.stdout.write(d),
|
|
1755
|
-
onStderr: (d) => cmdCtx.stderr.write(d),
|
|
1756
|
-
stdin,
|
|
1757
|
-
terminalStdin: cmdCtx.terminalStdin,
|
|
1758
|
-
// `npm run x` runs x on npm's own fds. Handing the nested execution a
|
|
1759
|
-
// sink without saying where that sink leads would make every fd look
|
|
1760
|
-
// redirected, and a runtime whose stdout is redirected stops streaming.
|
|
1761
|
-
terminalFds: {
|
|
1762
|
-
stdin: cmdCtx.isFdTerminal?.(0) ?? false,
|
|
1763
|
-
stdout: cmdCtx.isFdTerminal?.(1) ?? false,
|
|
1764
|
-
stderr: cmdCtx.isFdTerminal?.(2) ?? false,
|
|
1765
|
-
},
|
|
1766
|
-
commandContext: {
|
|
1767
|
-
pid: cmdCtx.pid,
|
|
1768
|
-
cred: cmdCtx.cred,
|
|
1769
|
-
setUmask: cmdCtx.setUmask,
|
|
1770
|
-
},
|
|
1771
|
-
runAs: runAsProcess,
|
|
1772
|
-
});
|
|
1773
|
-
return result.exitCode;
|
|
1774
|
-
};
|
|
1775
|
-
const shellEntrypointExecutor = {
|
|
1776
|
-
execute: async (cmd, options) => {
|
|
1777
|
-
const parentPid = options?.commandContext?.['pid'];
|
|
1778
|
-
if (typeof parentPid !== 'number') {
|
|
1779
|
-
throw new Error('shell entrypoint requires a parent process');
|
|
1780
|
-
}
|
|
1781
|
-
const childProcess = self.processes.spawn('sh', ['sh'], options?.cwd || '/home/user', { parentPid });
|
|
1782
|
-
let exitCode = 1;
|
|
1783
|
-
try {
|
|
1784
|
-
const identity = commandIdentityFor(childProcess.pid);
|
|
1785
|
-
const kernel = self.kernel;
|
|
1786
|
-
if (kernel === null)
|
|
1787
|
-
throw new Error('shell kernel is not initialized');
|
|
1788
|
-
const terminal = createHeadlessTerminal();
|
|
1789
|
-
const childShell = new Shell(terminal, kernel.vfs, registry, { ...env, ...(options?.env || {}) }, processRegistry, identity);
|
|
1790
|
-
installShellExecutionFeatures(childShell, terminal);
|
|
1791
|
-
if (options?.cwd)
|
|
1792
|
-
childShell.setCwd(options.cwd);
|
|
1793
|
-
const result = await childShell.execute(cmd, {
|
|
1794
|
-
...options,
|
|
1795
|
-
commandContext: {
|
|
1796
|
-
...options?.commandContext,
|
|
1797
|
-
pid: identity.pid,
|
|
1798
|
-
cred: identity.cred,
|
|
1799
|
-
setUmask: identity.setUmask,
|
|
1800
|
-
},
|
|
1801
|
-
runAs: runAsProcess,
|
|
1802
|
-
});
|
|
1803
|
-
exitCode = result.exitCode;
|
|
1804
|
-
return result;
|
|
1805
|
-
}
|
|
1806
|
-
finally {
|
|
1807
|
-
self.processes.exit(childProcess.pid, exitCode);
|
|
1808
|
-
}
|
|
1809
|
-
},
|
|
1810
|
-
};
|
|
1811
|
-
registerShellEntrypointCommands(registry, shellEntrypointExecutor, kernelFs);
|
|
1812
|
-
// Shell scripts that execute through the local shell still need the same
|
|
1813
|
-
// process-table and log-store contract as facet-backed processes.
|
|
1814
|
-
const shellExecuteTracked = async (cmd, cmdCtx, opts = {}) => {
|
|
1815
|
-
const entry = self.processes.spawn(cmd, [cmd], cmdCtx.cwd || '/home/user', { parentPid: cmdCtx.pid });
|
|
1816
|
-
const pid = entry.pid;
|
|
1817
|
-
if (opts.longRunning)
|
|
1818
|
-
self.processes.setLongRunning(pid);
|
|
1819
|
-
const startedAt = Date.now();
|
|
1820
|
-
// Spawn banner — matches facet-manager.ts onSpawn format.
|
|
1821
|
-
if (self.terminal) {
|
|
1822
|
-
const label = opts.longRunning ? 'started (long-running)' : 'started';
|
|
1823
|
-
self.terminal.write(`\x1b[2m[shell ${label}: pid=${pid} cmd="${cmd}"]\x1b[0m\r\n`);
|
|
1824
|
-
}
|
|
1825
|
-
// Structured spawn event for the tabs UI (mirrors the facet-manager
|
|
1826
|
-
// onSpawn hook). Long-running shell commands like `vite` and
|
|
1827
|
-
// `wrangler dev` trigger auto-open of a log tab.
|
|
1828
|
-
notifyTerminalEvent(self.terminal, {
|
|
1829
|
-
type: 'spawn', pid, command: cmd, longRunning: !!opts.longRunning, attachedTty: false,
|
|
1830
|
-
});
|
|
1831
|
-
// Wrap the caller-supplied streams so every chunk is both displayed
|
|
1832
|
-
// AND captured in the ring buffer keyed by this PID.
|
|
1833
|
-
const tee = (stream, target) => (d) => {
|
|
1834
|
-
try {
|
|
1835
|
-
self.processes.appendOutput(pid, stream, String(d));
|
|
1836
|
-
}
|
|
1837
|
-
catch { }
|
|
1838
|
-
try {
|
|
1839
|
-
target.write(d);
|
|
1840
|
-
}
|
|
1841
|
-
catch { }
|
|
1842
|
-
};
|
|
1843
|
-
let exitCode = 1;
|
|
1844
|
-
try {
|
|
1845
|
-
const result = await shell.execute(cmd, {
|
|
1846
|
-
cwd: cmdCtx.cwd,
|
|
1847
|
-
env: cmdCtx.env,
|
|
1848
|
-
onStdout: tee('stdout', cmdCtx.stdout),
|
|
1849
|
-
onStderr: tee('stderr', cmdCtx.stderr),
|
|
1850
|
-
// The tee is a sink, not a redirection: the script's fds are still
|
|
1851
|
-
// whatever the caller's were, and a runtime asks that question before
|
|
1852
|
-
// deciding whether it may stream past the shell.
|
|
1853
|
-
terminalFds: {
|
|
1854
|
-
stdin: cmdCtx.isFdTerminal?.(0) ?? false,
|
|
1855
|
-
stdout: cmdCtx.isFdTerminal?.(1) ?? false,
|
|
1856
|
-
stderr: cmdCtx.isFdTerminal?.(2) ?? false,
|
|
1857
|
-
},
|
|
1858
|
-
// Single spawn path for long-running handoff: a registry command
|
|
1859
|
-
// (vite/wrangler/serve) ADOPTS this wrapper pid via the bin-spawn
|
|
1860
|
-
// contract instead of allocating a second one, and suppresses its
|
|
1861
|
-
// own `[started (long-running)]` notice.
|
|
1862
|
-
commandContext: {
|
|
1863
|
-
pid,
|
|
1864
|
-
cred: entry.cred,
|
|
1865
|
-
setUmask: (mask) => self.processes.setUmask(pid, mask),
|
|
1866
|
-
...(opts.longRunning
|
|
1867
|
-
? {
|
|
1868
|
-
__nimbusBinSpawn: {
|
|
1869
|
-
skipSpawn: true,
|
|
1870
|
-
callerPid: pid,
|
|
1871
|
-
command: cmd,
|
|
1872
|
-
forceLongRunning: true,
|
|
1873
|
-
},
|
|
1874
|
-
}
|
|
1875
|
-
: {}),
|
|
1876
|
-
},
|
|
1877
|
-
runAs: runAsProcess,
|
|
1878
|
-
});
|
|
1879
|
-
exitCode = result.exitCode;
|
|
1880
|
-
}
|
|
1881
|
-
catch (e) {
|
|
1882
|
-
// Surface the error in the terminal and the ring buffer.
|
|
1883
|
-
const msg = (e && (e.stack || e.message)) || String(e);
|
|
1884
|
-
tee('stderr', cmdCtx.stderr)('shellExecuteTracked error: ' + msg + '\n');
|
|
1885
|
-
exitCode = 1;
|
|
1886
|
-
}
|
|
1887
|
-
finally {
|
|
1888
|
-
// When a long-running script handed off to a live server (the registry
|
|
1889
|
-
// command adopted this pid and returned 0), the process stays running;
|
|
1890
|
-
// emitting an immediate exit would print a false `[shell exited]` and
|
|
1891
|
-
// flip the process to terminated in /api/processes. Mirror the npm-bin
|
|
1892
|
-
// resolver's `handedOffToLongRunningFacet` contract.
|
|
1893
|
-
const handedOffToLongRunningFacet = opts.longRunning === true && exitCode === 0;
|
|
1894
|
-
if (!handedOffToLongRunningFacet) {
|
|
1895
|
-
try {
|
|
1896
|
-
self.processes.exit(pid, exitCode);
|
|
1897
|
-
}
|
|
1898
|
-
catch { }
|
|
1899
|
-
try {
|
|
1900
|
-
if (!self.processes.getExit(pid)) {
|
|
1901
|
-
self.processes.markExit(pid, exitCode);
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
catch { }
|
|
1905
|
-
// Structured exit for the tabs UI. Always fires (the UI doesn't
|
|
1906
|
-
// know which tabs are open, and client-side dedupe is trivial).
|
|
1907
|
-
// Include the command so the UI can backfill a tab for pids it
|
|
1908
|
-
// never saw a spawn event for (e.g. evals routed past onSpawn).
|
|
1909
|
-
notifyTerminalEvent(self.terminal, { type: 'exit', pid, code: exitCode, command: cmd });
|
|
1910
|
-
// Keep shell execution diagnostics on the same session helper used by
|
|
1911
|
-
// the rest of the process subsystem.
|
|
1912
|
-
try {
|
|
1913
|
-
self._emitShellExecDone(pid, cmd, exitCode, Date.now() - startedAt);
|
|
1914
|
-
}
|
|
1915
|
-
catch { }
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
return exitCode;
|
|
1919
|
-
};
|
|
1920
|
-
const runtimeCommandHint = createRuntimeCommandHintResolver(self.env);
|
|
1921
|
-
installNpmBinFallbackResolver(registry, {
|
|
1922
|
-
vfs: kernelFs,
|
|
1923
|
-
getCwd: () => self.shell?.cwd || '/home/user',
|
|
1924
|
-
processes: self.processes,
|
|
1925
|
-
getFacetManager: () => {
|
|
1926
|
-
self.ensureFacetManager();
|
|
1927
|
-
return self.facetManager;
|
|
1928
|
-
},
|
|
1929
|
-
terminal: self.terminal,
|
|
1930
|
-
notifyTerminalEvent: (event) => notifyTerminalEvent(self.terminal, event),
|
|
1931
|
-
runtimeCommandHint,
|
|
1932
|
-
emitShellExecDone: (pid, command, exitCode, durationMs) => {
|
|
1933
|
-
try {
|
|
1934
|
-
self._emitShellExecDone(pid, command, exitCode, durationMs);
|
|
1935
|
-
}
|
|
1936
|
-
catch { }
|
|
1937
|
-
},
|
|
1938
|
-
});
|
|
1939
|
-
// Register core npm with enhanced `npm run <script>` support
|
|
1940
|
-
const coreNpmCmd = createNpmCommand(registry, shellExecute, kernel);
|
|
1941
|
-
registry.register('npm', async (ctx) => {
|
|
1942
|
-
const args = ctx.args || [];
|
|
1943
|
-
const sub = args[0];
|
|
1944
|
-
const cwdKey = normalizeVfsPath(ctx.cwd || '/home/user');
|
|
1945
|
-
// npm run <script> / npm test / npm start — parse package.json and execute
|
|
1946
|
-
if (sub === 'run' || sub === 'run-script' || sub === 'test' || sub === 'start') {
|
|
1947
|
-
const scriptName = sub === 'test' ? 'test' : sub === 'start' ? 'start' : args[1];
|
|
1948
|
-
if (!scriptName) {
|
|
1949
|
-
// npm run (no script) — list available scripts
|
|
1950
|
-
const pkgPath = cwdKey + '/package.json';
|
|
1951
|
-
try {
|
|
1952
|
-
const pkg = JSON.parse(kernelFs.readFileString(pkgPath));
|
|
1953
|
-
if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
|
|
1954
|
-
ctx.stdout.write('Lifecycle scripts:\n');
|
|
1955
|
-
for (const [name, cmd] of Object.entries(pkg.scripts)) {
|
|
1956
|
-
ctx.stdout.write(` ${name}\n ${cmd}\n`);
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
else {
|
|
1960
|
-
ctx.stdout.write('No scripts found in package.json\n');
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
catch {
|
|
1964
|
-
ctx.stderr.write('npm ERR! no package.json found\n');
|
|
1965
|
-
return 1;
|
|
1966
|
-
}
|
|
1967
|
-
return 0;
|
|
1968
|
-
}
|
|
1969
|
-
const pkgPath = cwdKey + '/package.json';
|
|
1970
|
-
try {
|
|
1971
|
-
const pkg = JSON.parse(kernelFs.readFileString(pkgPath));
|
|
1972
|
-
const script = pkg.scripts?.[scriptName];
|
|
1973
|
-
if (!script) {
|
|
1974
|
-
ctx.stderr.write(`npm ERR! Missing script: "${scriptName}"\n`);
|
|
1975
|
-
if (pkg.scripts) {
|
|
1976
|
-
ctx.stderr.write('npm ERR! Available scripts:\n');
|
|
1977
|
-
for (const name of Object.keys(pkg.scripts))
|
|
1978
|
-
ctx.stderr.write(` - ${name}\n`);
|
|
1979
|
-
}
|
|
1980
|
-
return 1;
|
|
1981
|
-
}
|
|
1982
|
-
// ── node_modules preflight ────────────────────────────────────
|
|
1983
|
-
// If the script invokes a known bundler/framework CLI (vite, next,
|
|
1984
|
-
// webpack, tsc, ...) and node_modules is missing, HARD-FAIL before
|
|
1985
|
-
// running it — the tool would crash with a cryptic "command not
|
|
1986
|
-
// found" / "cannot find package" error that's less helpful.
|
|
1987
|
-
// For custom/unknown scripts (e.g. `echo hi`), emit a warning but
|
|
1988
|
-
// continue — the user's intent might not need deps at all.
|
|
1989
|
-
// Bypass with --force / --no-install-check in the script args, or
|
|
1990
|
-
// by setting NIMBUS_SKIP_INSTALL_CHECK=1 in env.
|
|
1991
|
-
const scriptArgs = args.slice(sub === 'run' || sub === 'run-script' ? 2 : 1);
|
|
1992
|
-
const bypassRunCheck = scriptArgs.includes('--force') ||
|
|
1993
|
-
scriptArgs.includes('--no-install-check') ||
|
|
1994
|
-
ctx.env?.NIMBUS_SKIP_INSTALL_CHECK === '1';
|
|
1995
|
-
if (!bypassRunCheck) {
|
|
1996
|
-
const guard = checkNodeModulesGuard(kernelFs, cwdKey);
|
|
1997
|
-
if (guard.missing) {
|
|
1998
|
-
const bundler = detectBundlerBin(script);
|
|
1999
|
-
if (bundler) {
|
|
2000
|
-
// Hard fail: script needs a bundler binary that lives in node_modules/.bin.
|
|
2001
|
-
ctx.stderr.write('\x1b[31m\u2718\x1b[0m \x1b[1mnode_modules/ not found\x1b[0m — ' +
|
|
2002
|
-
`script "${scriptName}" runs \x1b[36m${bundler}\x1b[0m which needs installed dependencies ` +
|
|
2003
|
-
`(${guard.depCount} declared).\n` +
|
|
2004
|
-
' Run \x1b[36mnpm install\x1b[0m first,\n' +
|
|
2005
|
-
' or re-run with \x1b[36mnpm run ' + scriptName + ' -- --force\x1b[0m to skip this check.\n');
|
|
2006
|
-
return 1;
|
|
2007
|
-
}
|
|
2008
|
-
// Soft warning: script might not need deps; let it try.
|
|
2009
|
-
ctx.stderr.write('\x1b[33m\u26A0\x1b[0m node_modules/ not found (' + guard.depCount + ' deps declared) — ' +
|
|
2010
|
-
'proceeding anyway. Run \x1b[36mnpm install\x1b[0m if the script fails.\n\n');
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
ctx.stdout.write(`\n> ${pkg.name || 'project'}@${pkg.version || '1.0.0'} ${scriptName}\n`);
|
|
2014
|
-
ctx.stdout.write(`> ${script}\n\n`);
|
|
2015
|
-
// ── Next.js loud-block (was W11 per-framework substrate) ──────
|
|
2016
|
-
//
|
|
2017
|
-
// runtime primitive support (P12): the previous wave shipped this as a
|
|
2018
|
-
// 45-line standalone module at src/frameworks/next.ts that
|
|
2019
|
-
// exported nothing but the strings used here. Per the
|
|
2020
|
-
// "no per-framework substrate" rule, the message is inlined
|
|
2021
|
-
// (the only thing the module did was hold these strings) and
|
|
2022
|
-
// the file is deleted.
|
|
2023
|
-
//
|
|
2024
|
-
// Next.js dev/start still needs a custom http.Server +
|
|
2025
|
-
// child_process.fork with v8-IPC + webpack/Turbopack, none of
|
|
2026
|
-
// which Nimbus ships. We surface a deterministic message
|
|
2027
|
-
// rather than letting the script hang or emit a confusing
|
|
2028
|
-
// crash. The block remains a one-off symbol-detection
|
|
2029
|
-
// guard, NOT a generic per-framework code path. Any future
|
|
2030
|
-
// similar guard belongs alongside this one — not in its own
|
|
2031
|
-
// src/frameworks/<name>.ts file.
|
|
2032
|
-
if ((scriptName === 'dev' || scriptName === 'start') &&
|
|
2033
|
-
(pkg.dependencies?.next || pkg.devDependencies?.next) &&
|
|
2034
|
-
!(scriptArgs.includes('--force') || scriptArgs.includes('--allow-next'))) {
|
|
2035
|
-
const NEXT_BLOCK_MESSAGE = '\x1b[31m\u2718\x1b[0m \x1b[1mNext.js dev server is not supported in Nimbus.\x1b[0m\n' +
|
|
2036
|
-
' Specific blockers:\n' +
|
|
2037
|
-
" 1. \x1b[2mchild_process.fork\x1b[0m IPC uses v8-serializer (Nimbus ships JSON projection).\n" +
|
|
2038
|
-
' 2. webpack / Turbopack bundlers are not integrated with the pre-bundle pipeline.\n' +
|
|
2039
|
-
' 3. Custom \x1b[2mhttp.Server\x1b[0m semantics (keep-alive, raw sockets) are facet-incompatible.\n' +
|
|
2040
|
-
'\n' +
|
|
2041
|
-
' Workaround: deploy with \x1b[36mnext build\x1b[0m + a hosted runtime,\n' +
|
|
2042
|
-
' or pass \x1b[36m--allow-next\x1b[0m to bypass at your own risk.\n';
|
|
2043
|
-
ctx.stderr.write(NEXT_BLOCK_MESSAGE);
|
|
2044
|
-
return 127;
|
|
2045
|
-
}
|
|
2046
|
-
const scriptTrim = script.trim();
|
|
2047
|
-
const longRunning = scriptName === 'dev' || scriptName === 'start' ||
|
|
2048
|
-
scriptName === 'serve' || scriptName === 'watch';
|
|
2049
|
-
return await shellExecuteTracked(scriptTrim, {
|
|
2050
|
-
...ctx,
|
|
2051
|
-
env: { ...ctx.env, npm_lifecycle_event: scriptName, npm_package_name: pkg.name || '' },
|
|
2052
|
-
}, { longRunning });
|
|
2053
|
-
}
|
|
2054
|
-
catch (e) {
|
|
2055
|
-
ctx.stderr.write(`npm ERR! ${e?.message || e}\n`);
|
|
2056
|
-
return 1;
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
// npm ls — list installed packages
|
|
2060
|
-
if (sub === 'ls' || sub === 'list') {
|
|
2061
|
-
const pkgPath = cwdKey + '/package.json';
|
|
2062
|
-
const nmDir = cwdKey + '/node_modules';
|
|
2063
|
-
try {
|
|
2064
|
-
const pkg = JSON.parse(kernelFs.readFileString(pkgPath));
|
|
2065
|
-
ctx.stdout.write(`${pkg.name || 'project'}@${pkg.version || '1.0.0'} ${ctx.cwd}\n`);
|
|
2066
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
2067
|
-
const names = Object.keys(deps);
|
|
2068
|
-
for (let i = 0; i < names.length; i++) {
|
|
2069
|
-
const isLast = i === names.length - 1;
|
|
2070
|
-
const prefix = isLast ? '└── ' : '├── ';
|
|
2071
|
-
const name = names[i];
|
|
2072
|
-
let version = deps[name];
|
|
2073
|
-
// Try to read actual installed version
|
|
2074
|
-
try {
|
|
2075
|
-
const installed = JSON.parse(kernelFs.readFileString(nmDir + '/' + name + '/package.json'));
|
|
2076
|
-
version = installed.version;
|
|
2077
|
-
}
|
|
2078
|
-
catch { }
|
|
2079
|
-
ctx.stdout.write(`${prefix}${name}@${version}\n`);
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
catch {
|
|
2083
|
-
ctx.stderr.write('npm ERR! no package.json found\n');
|
|
2084
|
-
return 1;
|
|
2085
|
-
}
|
|
2086
|
-
return 0;
|
|
2087
|
-
}
|
|
2088
|
-
// npm init / npm init -y
|
|
2089
|
-
if (sub === 'init') {
|
|
2090
|
-
const cwd = cwdKey;
|
|
2091
|
-
const pkgPath = cwd + '/package.json';
|
|
2092
|
-
if (kernelFs.exists(pkgPath) && !args.includes('-y') && !args.includes('--yes')) {
|
|
2093
|
-
ctx.stderr.write('package.json already exists. Use -y to overwrite.\n');
|
|
2094
|
-
return 1;
|
|
2095
|
-
}
|
|
2096
|
-
const name = cwd.split('/').pop() || 'project';
|
|
2097
|
-
const pkg = {
|
|
2098
|
-
name, version: '1.0.0', description: '', main: 'index.js',
|
|
2099
|
-
type: 'module',
|
|
2100
|
-
scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview', test: 'echo "no test"' },
|
|
2101
|
-
keywords: [], author: '', license: 'MIT', dependencies: {}, devDependencies: {},
|
|
2102
|
-
};
|
|
2103
|
-
kernelFs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
2104
|
-
ctx.stdout.write('Wrote to ' + pkgPath + '\n');
|
|
2105
|
-
return 0;
|
|
2106
|
-
}
|
|
2107
|
-
// npm uninstall <pkg>
|
|
2108
|
-
if (sub === 'uninstall' || sub === 'un' || sub === 'remove' || sub === 'rm') {
|
|
2109
|
-
const packages = args.slice(1).filter(a => !a.startsWith('-'));
|
|
2110
|
-
if (packages.length === 0) {
|
|
2111
|
-
ctx.stderr.write('Usage: npm uninstall <pkg>\n');
|
|
2112
|
-
return 1;
|
|
2113
|
-
}
|
|
2114
|
-
const nmDir = cwdKey + '/node_modules';
|
|
2115
|
-
for (const pkg of packages) {
|
|
2116
|
-
const pkgDir = nmDir + '/' + pkg;
|
|
2117
|
-
// Recursively delete package directory
|
|
2118
|
-
const deleteRecursive = (dir) => {
|
|
2119
|
-
try {
|
|
2120
|
-
for (const e of kernelFs.readdir(dir)) {
|
|
2121
|
-
const fp = dir + '/' + e.name;
|
|
2122
|
-
if (e.type === 'directory')
|
|
2123
|
-
deleteRecursive(fp);
|
|
2124
|
-
else
|
|
2125
|
-
try {
|
|
2126
|
-
kernelFs.unlink(fp);
|
|
2127
|
-
}
|
|
2128
|
-
catch { }
|
|
2129
|
-
}
|
|
2130
|
-
try {
|
|
2131
|
-
kernelFs.rmdir(dir);
|
|
2132
|
-
}
|
|
2133
|
-
catch { }
|
|
2134
|
-
}
|
|
2135
|
-
catch { }
|
|
2136
|
-
};
|
|
2137
|
-
deleteRecursive(pkgDir);
|
|
2138
|
-
ctx.stdout.write('removed ' + pkg + '\n');
|
|
2139
|
-
}
|
|
2140
|
-
// Update package.json
|
|
2141
|
-
const pkgPath = cwdKey + '/package.json';
|
|
2142
|
-
try {
|
|
2143
|
-
const pkgJson = JSON.parse(kernelFs.readFileString(pkgPath));
|
|
2144
|
-
for (const pkg of packages) {
|
|
2145
|
-
delete pkgJson.dependencies?.[pkg];
|
|
2146
|
-
delete pkgJson.devDependencies?.[pkg];
|
|
2147
|
-
}
|
|
2148
|
-
kernelFs.writeFile(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
|
|
2149
|
-
}
|
|
2150
|
-
catch { }
|
|
2151
|
-
return 0;
|
|
2152
|
-
}
|
|
2153
|
-
// npm install (no args or with packages) — use NpmInstaller v2 (batched writes)
|
|
2154
|
-
if (sub === 'install' || sub === 'i' || sub === 'add') {
|
|
2155
|
-
const installInvocation = parseNpmInstallInvocation(args.slice(1));
|
|
2156
|
-
const explicitPkgs = installInvocation.packages;
|
|
2157
|
-
const globalPrefix = installInvocation.global
|
|
2158
|
-
? resolveNpmPrefix(installInvocation.prefix ?? String(ctx.env?.npm_config_prefix || '/usr/local'), ctx.cwd || '/home/user')
|
|
2159
|
-
: null;
|
|
2160
|
-
self.ensureSqliteFs();
|
|
2161
|
-
if (globalPrefix)
|
|
2162
|
-
self.ensureGlobalPrefixDirs(globalPrefix);
|
|
2163
|
-
const installCwd = globalPrefix ? `${globalPrefix}/lib` : cwdKey;
|
|
2164
|
-
// Ensure package.json exists for bare `npm install`
|
|
2165
|
-
if (!globalPrefix && explicitPkgs.length === 0) {
|
|
2166
|
-
const pkgJsonPath = installCwd + '/package.json';
|
|
2167
|
-
if (!kernelFs.exists(pkgJsonPath)) {
|
|
2168
|
-
ctx.stderr.write('npm ERR! no package.json found\n');
|
|
2169
|
-
return 1;
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
if (globalPrefix && explicitPkgs.length === 0) {
|
|
2173
|
-
ctx.stderr.write('npm ERR! missing package name for global install\n');
|
|
2174
|
-
return 1;
|
|
2175
|
-
}
|
|
2176
|
-
const pkgLabel = explicitPkgs.length > 0
|
|
2177
|
-
? `${explicitPkgs.length} packages`
|
|
2178
|
-
: 'dependencies from package.json';
|
|
2179
|
-
ctx.stdout.write(`\x1b[36mInstalling ${pkgLabel} (npm v2 — batched writes)...\x1b[0m\n`);
|
|
2180
|
-
await self.ensureNpmInstaller((msg) => {
|
|
2181
|
-
ctx.stdout.write('[npm] ' + msg + '\n');
|
|
2182
|
-
});
|
|
2183
|
-
// `--loglevel` selects npm's own log protocol on stderr, where npm
|
|
2184
|
-
// writes it. Tooling that drives npm parses those lines rather than
|
|
2185
|
-
// our prose — pi's installer advances its progress label off them.
|
|
2186
|
-
const npmLogLevel = installInvocation.loglevel;
|
|
2187
|
-
const npmLog = npmLogLevel === null ? undefined : ((level, line) => {
|
|
2188
|
-
if (npmLogEnabled(npmLogLevel, level))
|
|
2189
|
-
ctx.stderr.write(line + '\n');
|
|
2190
|
-
});
|
|
2191
|
-
try {
|
|
2192
|
-
const result = await self.npmInstaller.install(installCwd, {
|
|
2193
|
-
packages: explicitPkgs.length > 0 ? explicitPkgs : undefined,
|
|
2194
|
-
production: installInvocation.production,
|
|
2195
|
-
pid: ctx.pid,
|
|
2196
|
-
npmLog,
|
|
2197
|
-
});
|
|
2198
|
-
if (result.failed?.length > 0) {
|
|
2199
|
-
ctx.stderr.write('\x1b[31mFailed: ' + result.failed.join(', ') + '\x1b[0m\n');
|
|
2200
|
-
}
|
|
2201
|
-
// [HONEST INSTALL MESSAGE P0a] Yellow + "(N failed, see above)"
|
|
2202
|
-
// when partial. Green only when failed.length === 0.
|
|
2203
|
-
const partial = (result.failed?.length || 0) > 0;
|
|
2204
|
-
const color = partial ? '\x1b[33m' : '\x1b[32m';
|
|
2205
|
-
const suffix = partial ? ` (${result.failed.length} failed, see above)` : '';
|
|
2206
|
-
ctx.stdout.write(`\n${color}added ${result.installed?.length || 0} packages (${result.totalFiles || 0} files) in ${((result.elapsed || 0) / 1000).toFixed(1)}s${suffix}\x1b[0m\n`);
|
|
2207
|
-
if (result.cachedHits > 0) {
|
|
2208
|
-
ctx.stdout.write(`\x1b[2m (${result.cachedHits} from cache)\x1b[0m\n`);
|
|
2209
|
-
}
|
|
2210
|
-
if (globalPrefix) {
|
|
2211
|
-
// Materialize on-PATH bin shims even for partial installs. The
|
|
2212
|
-
// only writer of shims into ${globalPrefix}/bin used to be gated
|
|
2213
|
-
// behind zero failures across the whole dependency tree — but a
|
|
2214
|
-
// global install of a 100+-dep package almost always has at least
|
|
2215
|
-
// one transitive failure, so /usr/local/bin was ~never created
|
|
2216
|
-
// and the installed package's bin was unreachable. materialize →
|
|
2217
|
-
// validateEntry → resolveExistingTarget already skips bins whose
|
|
2218
|
-
// target file didn't land, so a partial install safely exposes
|
|
2219
|
-
// exactly the bins that actually installed.
|
|
2220
|
-
const linked = materializeNpmBinShims(kernelFs, `${installCwd}/node_modules`, `${globalPrefix}/bin`);
|
|
2221
|
-
if (linked > 0) {
|
|
2222
|
-
ctx.stdout.write(`\x1b[2m linked ${linked} bin${linked === 1 ? '' : 's'} into /${globalPrefix}/bin\x1b[0m\n`);
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
return result.failed?.length > 0 ? 1 : 0;
|
|
2226
|
-
}
|
|
2227
|
-
catch (e) {
|
|
2228
|
-
ctx.stderr.write(`\x1b[31mnpm install failed: ${e?.message}\x1b[0m\n`);
|
|
2229
|
-
return 1;
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
// ── npm create <pkg> / npm init <pkg> → npx create-<pkg> ─────────
|
|
2233
|
-
//
|
|
2234
|
-
// Per the npm spec, `npm create X args...` and `npm init X args...`
|
|
2235
|
-
// (when X is supplied) are sugar for invoking the `create-X`
|
|
2236
|
-
// initializer package via npx. Specifically:
|
|
2237
|
-
//
|
|
2238
|
-
// npm create foo args... → npx create-foo args...
|
|
2239
|
-
// npm create foo@1.2 args... → npx create-foo@1.2 args...
|
|
2240
|
-
// npm create @scope/foo args... → npx @scope/create-foo args...
|
|
2241
|
-
// npm create @scope args... → npx @scope/create args...
|
|
2242
|
-
//
|
|
2243
|
-
// `npm init` (no args) is a different beast — it scaffolds a
|
|
2244
|
-
// package.json interactively. The `sub === 'init'` branch above
|
|
2245
|
-
// handles the no-arg case; here we only intercept the
|
|
2246
|
-
// initializer-package case (1+ args after `init`).
|
|
2247
|
-
//
|
|
2248
|
-
// Without this routing, `npm create vite@latest mvp -- --template
|
|
2249
|
-
// react-ts` hits the base npm dispatch which only knows
|
|
2250
|
-
// {init, install/i/add, uninstall/remove/rm/un, list/ls,
|
|
2251
|
-
// run/run-script, start, test, info/view/show, search, version}
|
|
2252
|
-
// — and emits "npm: unknown command 'create'". Every modern
|
|
2253
|
-
// framework's `create-*` flow (create-vite, create-next-app
|
|
2254
|
-
// routed via "npm create", create-cloudflare, create-astro, etc.)
|
|
2255
|
-
// depends on this.
|
|
2256
|
-
//
|
|
2257
|
-
// This is a primitive: one fix, every framework wins.
|
|
2258
|
-
if (sub === 'create' || (sub === 'init' && args.length >= 2 && !args[1].startsWith('-'))) {
|
|
2259
|
-
const arg1 = args[1];
|
|
2260
|
-
if (!arg1) {
|
|
2261
|
-
ctx.stderr.write('npm create: missing package name\n');
|
|
2262
|
-
ctx.stderr.write('Usage: npm create <pkg> [args...]\n');
|
|
2263
|
-
return 1;
|
|
2264
|
-
}
|
|
2265
|
-
// Parse pkg + version. Scope-aware:
|
|
2266
|
-
// @scope → @scope/create
|
|
2267
|
-
// @scope/foo → @scope/create-foo
|
|
2268
|
-
// foo → create-foo
|
|
2269
|
-
// foo@1.2.3 → create-foo@1.2.3
|
|
2270
|
-
// foo@latest → create-foo@latest
|
|
2271
|
-
function rewriteToCreatePkg(spec) {
|
|
2272
|
-
// Strip an optional version range and re-append after the rewrite.
|
|
2273
|
-
const atIdx = spec.lastIndexOf('@');
|
|
2274
|
-
const hasVersion = atIdx > 0; // a leading @ is the scope; not a version
|
|
2275
|
-
const bare = hasVersion ? spec.slice(0, atIdx) : spec;
|
|
2276
|
-
const version = hasVersion ? spec.slice(atIdx) : '';
|
|
2277
|
-
let pkg;
|
|
2278
|
-
if (bare.startsWith('@')) {
|
|
2279
|
-
const slash = bare.indexOf('/');
|
|
2280
|
-
if (slash < 0) {
|
|
2281
|
-
// @scope → @scope/create
|
|
2282
|
-
pkg = bare + '/create';
|
|
2283
|
-
}
|
|
2284
|
-
else {
|
|
2285
|
-
// @scope/foo → @scope/create-foo
|
|
2286
|
-
const scope = bare.slice(0, slash);
|
|
2287
|
-
const name = bare.slice(slash + 1);
|
|
2288
|
-
pkg = scope + '/create-' + name;
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
else {
|
|
2292
|
-
pkg = 'create-' + bare;
|
|
2293
|
-
}
|
|
2294
|
-
return pkg + version;
|
|
2295
|
-
}
|
|
2296
|
-
const createPkg = rewriteToCreatePkg(arg1);
|
|
2297
|
-
const passThrough = args.slice(2);
|
|
2298
|
-
// `npm create` accepts an optional `--` separator to push the
|
|
2299
|
-
// remaining args to the create script; npx doesn't need a
|
|
2300
|
-
// separator (positional args after the package name go to the
|
|
2301
|
-
// package). We strip a single literal `--` token if present so
|
|
2302
|
-
// `npm create vite@latest mvp -- --template react-ts` becomes
|
|
2303
|
-
// `npx --yes create-vite@latest mvp --template react-ts`.
|
|
2304
|
-
const stripped = passThrough.filter((a, i, arr) => !(a === '--' && i < arr.length - 1) && !(a === '--' && arr.indexOf('--') === i));
|
|
2305
|
-
// Inform the user what we're routing to — matches npm's own
|
|
2306
|
-
// visible "npx" line so the create flow is honest.
|
|
2307
|
-
ctx.stdout.write(`> npx --yes ${createPkg}${stripped.length ? ' ' + stripped.join(' ') : ''}\n`);
|
|
2308
|
-
// Dispatch through the npx registry entry. `--yes` skips the
|
|
2309
|
-
// "Ok to proceed? (y)" prompt.
|
|
2310
|
-
const npxHandler = await registry.resolve('npx');
|
|
2311
|
-
if (!npxHandler) {
|
|
2312
|
-
ctx.stderr.write('npm create: npx command unavailable\n');
|
|
2313
|
-
return 1;
|
|
2314
|
-
}
|
|
2315
|
-
return await npxHandler({
|
|
2316
|
-
...ctx,
|
|
2317
|
-
args: ['--yes', createPkg, ...stripped],
|
|
2318
|
-
});
|
|
2319
|
-
}
|
|
2320
|
-
// Fall through to core npm for other subcommands
|
|
2321
|
-
return coreNpmCmd(ctx);
|
|
2322
|
-
});
|
|
2323
|
-
// npx: check registered commands first, then resolve/install through
|
|
2324
|
-
// Nimbus's NpmInstaller and execute the package bin via the Node runtime.
|
|
2325
|
-
registry.register('npx', async (ctx) => {
|
|
2326
|
-
const npxArgs = ctx.args || [];
|
|
2327
|
-
const { describeNpxSelfInvocation, formatNpxHelp, getNpxCommandArgs, getNpxCommandWord, resolveNpxBinary, } = await import('../npm/npx-install.js');
|
|
2328
|
-
const selfInvocation = describeNpxSelfInvocation(npxArgs);
|
|
2329
|
-
if (selfInvocation === 'missing') {
|
|
2330
|
-
ctx.stderr.write('Usage: npx <command> [args...]\n');
|
|
2331
|
-
return 1;
|
|
2332
|
-
}
|
|
2333
|
-
if (selfInvocation === 'version') {
|
|
2334
|
-
ctx.stdout.write(NPM_VERSION + '\n');
|
|
2335
|
-
return 0;
|
|
2336
|
-
}
|
|
2337
|
-
if (selfInvocation === 'help') {
|
|
2338
|
-
ctx.stdout.write(formatNpxHelp());
|
|
2339
|
-
return 0;
|
|
2340
|
-
}
|
|
2341
|
-
const cmd = getNpxCommandWord(npxArgs);
|
|
2342
|
-
// Check if it's a built-in command (vite, esbuild, etc.)
|
|
2343
|
-
const resolved = cmd ? await registry.resolve(cmd) : null;
|
|
2344
|
-
if (resolved) {
|
|
2345
|
-
return await resolved({ ...ctx, args: getNpxCommandArgs(npxArgs) });
|
|
2346
|
-
}
|
|
2347
|
-
// Nimbus-native npx install + run path. Routes package installation
|
|
2348
|
-
// through NpmInstaller's full-packument resolver.
|
|
2349
|
-
await self.ensureNpmInstaller((msg) => ctx.stdout.write('[npm] ' + msg + '\n'));
|
|
2350
|
-
self.ensureSqliteFs();
|
|
2351
|
-
const installer = self.npmInstaller;
|
|
2352
|
-
const resolveResult = await resolveNpxBinary(installer, self.sqliteFs.as(requireVfsCred('cred' in ctx ? ctx.cred : undefined, 'npx')), ctx.cwd || '/home/user', npxArgs, (msg) => ctx.stdout.write(msg + '\n'), ctx.pid);
|
|
2353
|
-
if (resolveResult.ok && resolveResult.binPath) {
|
|
2354
|
-
const nodeCmd = await registry.resolve('node');
|
|
2355
|
-
if (nodeCmd) {
|
|
2356
|
-
return await nodeCmd({
|
|
2357
|
-
...ctx,
|
|
2358
|
-
args: [resolveResult.binPath, ...(resolveResult.binArgs || [])],
|
|
2359
|
-
__nimbusBundleProfile: resolveResult.bundleProfile,
|
|
2360
|
-
});
|
|
2361
|
-
}
|
|
2362
|
-
ctx.stderr.write('npx: node runtime is not registered\n');
|
|
2363
|
-
return 1;
|
|
2364
|
-
}
|
|
2365
|
-
ctx.stderr.write((resolveResult.error || 'npx: could not resolve binary') + '\n');
|
|
2366
|
-
return 1;
|
|
2367
|
-
});
|
|
2368
|
-
// ── Register process commands (enhanced with facet process tracking) ──
|
|
2369
|
-
registry.register('ps', async (ctx) => {
|
|
2370
|
-
// Pids are generation-strided (see PID_GEN_STRIDE) so they can be 7+
|
|
2371
|
-
// digits; size the column to the widest pid in this listing.
|
|
2372
|
-
const procs = self.processes.getAll();
|
|
2373
|
-
const pidWidth = Math.max(3, ...procs.map((p) => String(p.pid).length));
|
|
2374
|
-
ctx.stdout.write(` ${'PID'.padStart(pidWidth)} STATUS COMMAND\n`);
|
|
2375
|
-
for (const proc of procs) {
|
|
2376
|
-
// Prefer log-store exit info over ProcessTable's: the store has
|
|
2377
|
-
// the authoritative code and survives reap. For `running`, rely
|
|
2378
|
-
// on ProcessTable (store has no "running" concept).
|
|
2379
|
-
let status;
|
|
2380
|
-
if (proc.state === 'running') {
|
|
2381
|
-
status = '\x1b[32mrunning\x1b[0m';
|
|
2382
|
-
}
|
|
2383
|
-
else if (proc.state === 'killed') {
|
|
2384
|
-
status = `\x1b[33mkilled(${proc.exitCode ?? 137})\x1b[0m`;
|
|
2385
|
-
}
|
|
2386
|
-
else {
|
|
2387
|
-
// 'exited' — distinguish clean vs crashed.
|
|
2388
|
-
const code = proc.exitCode ?? 0;
|
|
2389
|
-
status = code === 0
|
|
2390
|
-
? `\x1b[2mexited(0)\x1b[0m`
|
|
2391
|
-
: `\x1b[31mcrashed(${code})\x1b[0m`;
|
|
2392
|
-
}
|
|
2393
|
-
ctx.stdout.write(` ${String(proc.pid).padStart(pidWidth)} ${status.padEnd(26)} ${proc.command}\n`);
|
|
2394
|
-
}
|
|
2395
|
-
// Show vite dev server
|
|
2396
|
-
if (self.viteDevServer?.isRunning) {
|
|
2397
|
-
ctx.stdout.write(` \x1b[33m${'---'.padStart(pidWidth)}\x1b[0m \x1b[32mrunning\x1b[0m vite dev server (${self.viteBasePath}/)\n`);
|
|
2398
|
-
}
|
|
2399
|
-
if (self.processes.getAll().length === 0 && !self.viteDevServer?.isRunning) {
|
|
2400
|
-
ctx.stdout.write(' (no processes)\n');
|
|
2401
|
-
}
|
|
2402
|
-
return 0;
|
|
2403
|
-
});
|
|
2404
|
-
// ── `logs <pid>` — tail per-process ring buffer ──
|
|
2405
|
-
// Flags:
|
|
2406
|
-
// -f / --follow stream new chunks until the process exits
|
|
2407
|
-
// -n / --lines N number of lines from the tail (default 200)
|
|
2408
|
-
// --bytes N max bytes from the tail (overrides --lines)
|
|
2409
|
-
// --plain strip ANSI escapes on output (keeps buffer raw)
|
|
2410
|
-
registry.register('logs', async (ctx) => {
|
|
2411
|
-
const args = ctx.args || [];
|
|
2412
|
-
const follow = args.includes('-f') || args.includes('--follow');
|
|
2413
|
-
const plain = args.includes('--plain');
|
|
2414
|
-
let lines = 200;
|
|
2415
|
-
let bytes;
|
|
2416
|
-
for (let i = 0; i < args.length; i++) {
|
|
2417
|
-
const a = args[i];
|
|
2418
|
-
if ((a === '-n' || a === '--lines') && args[i + 1]) {
|
|
2419
|
-
const n = parseInt(args[i + 1], 10);
|
|
2420
|
-
if (!isNaN(n) && n > 0)
|
|
2421
|
-
lines = n;
|
|
2422
|
-
i++;
|
|
2423
|
-
}
|
|
2424
|
-
else if (a === '--bytes' && args[i + 1]) {
|
|
2425
|
-
const n = parseInt(args[i + 1], 10);
|
|
2426
|
-
if (!isNaN(n) && n > 0)
|
|
2427
|
-
bytes = n;
|
|
2428
|
-
i++;
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
const pidArg = args.find(a => /^\d+$/.test(a));
|
|
2432
|
-
if (!pidArg) {
|
|
2433
|
-
ctx.stderr.write('usage: logs [-f] [-n LINES | --bytes N] [--plain] <pid>\n');
|
|
2434
|
-
return 1;
|
|
2435
|
-
}
|
|
2436
|
-
const pid = parseInt(pidArg, 10);
|
|
2437
|
-
if (!self.processes.hasLogs(pid)) {
|
|
2438
|
-
ctx.stderr.write(`no logs for pid ${pid}\n`);
|
|
2439
|
-
return 1;
|
|
2440
|
-
}
|
|
2441
|
-
// Paint a single chunk for live-stream (follow-mode) rendering.
|
|
2442
|
-
// `--plain` strips ANSI per chunk — safe for live output because
|
|
2443
|
-
// individual streamed chunks from the RPC layer never split an
|
|
2444
|
-
// escape sequence (the RPC boundary always delivers a complete
|
|
2445
|
-
// write call). Backfill is different (see below).
|
|
2446
|
-
const renderChunk = (c) => {
|
|
2447
|
-
let data = c.data;
|
|
2448
|
-
if (plain)
|
|
2449
|
-
data = stripAnsi(data);
|
|
2450
|
-
if (c.stream === 'stderr' && !plain) {
|
|
2451
|
-
return `\x1b[31m${data}\x1b[0m`;
|
|
2452
|
-
}
|
|
2453
|
-
return data;
|
|
2454
|
-
};
|
|
2455
|
-
// Backfill. Concatenate same-stream consecutive chunks BEFORE
|
|
2456
|
-
// stripping so that any ANSI escape split across chunk boundaries
|
|
2457
|
-
// (by the 4 KB splitter inside ProcessLogStore) gets rejoined and
|
|
2458
|
-
// stripped cleanly instead of leaking `1m` / `[31m` fragments.
|
|
2459
|
-
const tailOpts = bytes !== undefined ? { bytes } : { lines };
|
|
2460
|
-
const chunks = self.processes.tailLogs(pid, tailOpts);
|
|
2461
|
-
let group = [];
|
|
2462
|
-
const flushGroup = () => {
|
|
2463
|
-
if (group.length === 0)
|
|
2464
|
-
return;
|
|
2465
|
-
const stream = group[0].stream;
|
|
2466
|
-
let data = group.map(c => c.data).join('');
|
|
2467
|
-
if (plain)
|
|
2468
|
-
data = stripAnsi(data);
|
|
2469
|
-
if (stream === 'stderr' && !plain) {
|
|
2470
|
-
ctx.stdout.write(`\x1b[31m${data}\x1b[0m`);
|
|
2471
|
-
}
|
|
2472
|
-
else {
|
|
2473
|
-
ctx.stdout.write(data);
|
|
2474
|
-
}
|
|
2475
|
-
group = [];
|
|
2476
|
-
};
|
|
2477
|
-
for (const c of chunks) {
|
|
2478
|
-
if (group.length > 0 && group[group.length - 1].stream !== c.stream) {
|
|
2479
|
-
flushGroup();
|
|
2480
|
-
}
|
|
2481
|
-
group.push(c);
|
|
2482
|
-
}
|
|
2483
|
-
flushGroup();
|
|
2484
|
-
if (!follow) {
|
|
2485
|
-
// Footer only when process has exited already.
|
|
2486
|
-
const exit = self.processes.getExit(pid);
|
|
2487
|
-
if (exit) {
|
|
2488
|
-
ctx.stdout.write(`\r\n\x1b[2m[process exited with code ${exit.code}${exit.reason ? ` (${exit.reason})` : ''}]\x1b[0m\r\n`);
|
|
2489
|
-
}
|
|
2490
|
-
return 0;
|
|
2491
|
-
}
|
|
2492
|
-
// Follow mode: subscribe to live appends, poll for exit.
|
|
2493
|
-
const entry = self.processes.get(pid);
|
|
2494
|
-
const alreadyExited = !entry || entry.state !== 'running' || self.processes.getExit(pid);
|
|
2495
|
-
if (alreadyExited) {
|
|
2496
|
-
const exit = self.processes.getExit(pid);
|
|
2497
|
-
if (exit) {
|
|
2498
|
-
ctx.stdout.write(`\r\n\x1b[2m[process exited with code ${exit.code}${exit.reason ? ` (${exit.reason})` : ''}]\x1b[0m\r\n`);
|
|
2499
|
-
}
|
|
2500
|
-
return 0;
|
|
2501
|
-
}
|
|
2502
|
-
return await new Promise((resolve) => {
|
|
2503
|
-
let done = false;
|
|
2504
|
-
const finish = (code) => {
|
|
2505
|
-
if (done)
|
|
2506
|
-
return;
|
|
2507
|
-
done = true;
|
|
2508
|
-
unsub();
|
|
2509
|
-
unsubExit();
|
|
2510
|
-
resolve(code);
|
|
2511
|
-
};
|
|
2512
|
-
const unsub = self.processes.subscribeLogs(pid, (c) => {
|
|
2513
|
-
ctx.stdout.write(renderChunk(c));
|
|
2514
|
-
});
|
|
2515
|
-
const unsubExit = self.processes.subscribeExit(pid, (exit) => {
|
|
2516
|
-
ctx.stdout.write(`\r\n\x1b[2m[process exited with code ${exit.code}${exit.reason ? ` (${exit.reason})` : ''}]\x1b[0m\r\n`);
|
|
2517
|
-
finish(0);
|
|
2518
|
-
});
|
|
2519
|
-
// TOCTOU: the process may have exited between our `alreadyExited`
|
|
2520
|
-
// check above and these subscribe calls. Re-check now that the
|
|
2521
|
-
// exit subscriber is wired — if exit already set, the subscribe
|
|
2522
|
-
// callback never fires, so synthesize the footer ourselves.
|
|
2523
|
-
const exitNow = self.processes.getExit(pid);
|
|
2524
|
-
if (exitNow) {
|
|
2525
|
-
ctx.stdout.write(`\r\n\x1b[2m[process exited with code ${exitNow.code}${exitNow.reason ? ` (${exitNow.reason})` : ''}]\x1b[0m\r\n`);
|
|
2526
|
-
finish(0);
|
|
2527
|
-
return;
|
|
2528
|
-
}
|
|
2529
|
-
// If ctx exposes an AbortSignal (Ctrl+C wired by the shell),
|
|
2530
|
-
// honor it. Otherwise, follow-mode ends only on process exit.
|
|
2531
|
-
if (ctx.signal && typeof ctx.signal.addEventListener === 'function') {
|
|
2532
|
-
ctx.signal.addEventListener('abort', () => finish(130));
|
|
2533
|
-
}
|
|
2534
|
-
});
|
|
2535
|
-
});
|
|
2536
|
-
registry.register('jobs', async (ctx) => {
|
|
2537
|
-
const running = self.processes.getRunning();
|
|
2538
|
-
if (running.length === 0 && !self.viteDevServer?.isRunning) {
|
|
2539
|
-
ctx.stdout.write('No background jobs.\n');
|
|
2540
|
-
return 0;
|
|
2541
|
-
}
|
|
2542
|
-
for (let i = 0; i < running.length; i++) {
|
|
2543
|
-
ctx.stdout.write(`[${i + 1}] Running ${running[i].command} (pid ${running[i].pid})\n`);
|
|
2544
|
-
}
|
|
2545
|
-
if (self.viteDevServer?.isRunning) {
|
|
2546
|
-
ctx.stdout.write(`[${running.length + 1}] Running vite dev server\n`);
|
|
2547
|
-
}
|
|
2548
|
-
return 0;
|
|
2549
|
-
});
|
|
2550
|
-
const shellKillCommand = createKillCommand(processRegistry);
|
|
2551
|
-
registry.register('kill', async (ctx) => {
|
|
2552
|
-
const pidArg = ctx.args[0];
|
|
2553
|
-
if (!pidArg) {
|
|
2554
|
-
ctx.stderr.write('Usage: kill <pid>\n');
|
|
2555
|
-
return 1;
|
|
2556
|
-
}
|
|
2557
|
-
if (pidArg.startsWith('-') || pidArg.startsWith('%')) {
|
|
2558
|
-
return shellKillCommand(ctx);
|
|
2559
|
-
}
|
|
2560
|
-
const pid = parseInt(pidArg);
|
|
2561
|
-
if (isNaN(pid)) {
|
|
2562
|
-
ctx.stderr.write('kill: invalid pid\n');
|
|
2563
|
-
return 1;
|
|
2564
|
-
}
|
|
2565
|
-
if (processRegistry.get(pid)) {
|
|
2566
|
-
return shellKillCommand(ctx);
|
|
2567
|
-
}
|
|
2568
|
-
// runtime primitive support (P11): if the target is the vite shim PID
|
|
2569
|
-
// (registered by P5's long-running spawn), tear down the
|
|
2570
|
-
// in-process viteDevServer / cirrusReal too — facetManager.kill
|
|
2571
|
-
// only handles real Worker-Loader facets, and would leave the
|
|
2572
|
-
// shim running with its port registered against a dead PID.
|
|
2573
|
-
if (self._viteShimPid === pid) {
|
|
2574
|
-
try {
|
|
2575
|
-
if (self.cirrusReal?.isRunning) {
|
|
2576
|
-
self.cirrusReal.stop(self.ctx);
|
|
2577
|
-
self.cirrusReal = null;
|
|
2578
|
-
}
|
|
2579
|
-
if (self.viteDevServer?.isRunning) {
|
|
2580
|
-
self.viteDevServer.stop();
|
|
2581
|
-
self.viteDevServer = null;
|
|
2582
|
-
try {
|
|
2583
|
-
await self.ctx.storage.delete(VITE_CONFIG_KEY);
|
|
2584
|
-
}
|
|
2585
|
-
catch { }
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
catch (e) {
|
|
2589
|
-
ctx.stderr.write('kill: while stopping vite shim: ' + (e?.message || e) + '\n');
|
|
2590
|
-
}
|
|
2591
|
-
try {
|
|
2592
|
-
self.portRegistry.unregisterByPid(pid);
|
|
2593
|
-
}
|
|
2594
|
-
catch { }
|
|
2595
|
-
try {
|
|
2596
|
-
self.processes.kill(pid);
|
|
2597
|
-
}
|
|
2598
|
-
catch { }
|
|
2599
|
-
notifyTerminalEvent(self.terminal, {
|
|
2600
|
-
type: 'exit', pid, code: 137, command: 'vite',
|
|
2601
|
-
});
|
|
2602
|
-
self._viteShimPid = null;
|
|
2603
|
-
self._viteShimPort = null;
|
|
2604
|
-
ctx.stdout.write(`Process ${pid} killed.\n`);
|
|
2605
|
-
return 0;
|
|
2606
|
-
}
|
|
2607
|
-
if (self.facetManager?.kill(pid)) {
|
|
2608
|
-
ctx.stdout.write(`Process ${pid} killed.\n`);
|
|
2609
|
-
return 0;
|
|
2610
|
-
}
|
|
2611
|
-
ctx.stderr.write(`kill: no such process: ${pid}\n`);
|
|
2612
|
-
return 1;
|
|
2613
|
-
});
|
|
2614
|
-
registry.register('top', createTopCommand(processRegistry));
|
|
2615
|
-
registry.register('watch', createWatchCommand(registry));
|
|
2616
|
-
registry.register('help', createHelpCommand(registry));
|
|
2617
|
-
// ── Rehydrate globally-installed npm packages ──
|
|
2618
|
-
try {
|
|
2619
|
-
rehydrateGlobalPackages(self.kernel.vfs, registry);
|
|
2620
|
-
}
|
|
2621
|
-
catch { }
|
|
352
|
+
await registerHostedCommands(self, workspace);
|
|
2622
353
|
// ── Phase O: one-shot online output [B'.1] ─────────────────────────
|
|
2623
354
|
//
|
|
2624
355
|
// Only emit cold-start UI (MOTD, starter-app hint, framework-detect)
|
|
@@ -2647,17 +378,17 @@ export async function initSession(self, ws) {
|
|
|
2647
378
|
catch { }
|
|
2648
379
|
// ── Starter-app hint (only if seed sentinel still exists) ──
|
|
2649
380
|
// We check the live VFS, not a static file, so that if the user
|
|
2650
|
-
// deletes ~/.nimbus-seeded (or
|
|
2651
|
-
// next login.
|
|
381
|
+
// deletes ~/.nimbus-seeded (or the project dir) the hint stops
|
|
382
|
+
// appearing on next login.
|
|
2652
383
|
try {
|
|
2653
384
|
if (hasSeededProject(self.sqliteFs) && kernelFs.exists(SEED_PROJECT_DIR)) {
|
|
2654
|
-
self.terminal.write(
|
|
2655
|
-
|
|
385
|
+
self.terminal.write(`\x1b[2mStarter app ready at \x1b[36m${SEED_PROJECT_TILDE}\x1b[0m\x1b[2m — try:\x1b[0m\r\n` +
|
|
386
|
+
` \x1b[36mcd ${SEED_PROJECT_NAME} && npm install && npm run dev\x1b[0m\r\n\r\n`);
|
|
2656
387
|
}
|
|
2657
388
|
}
|
|
2658
389
|
catch { }
|
|
2659
390
|
// ── W11: framework detection MOTD line ──
|
|
2660
|
-
// If
|
|
391
|
+
// If the seeded project dir has a recognizable framework, print one informational line.
|
|
2661
392
|
// Purely advisory — does not change boot behaviour. Fire-and-forget
|
|
2662
393
|
// because initSession is sync; any failure is silently swallowed.
|
|
2663
394
|
void (async () => {
|
|
@@ -2738,26 +469,18 @@ export async function initSession(self, ws) {
|
|
|
2738
469
|
// ── Start shell ──
|
|
2739
470
|
//
|
|
2740
471
|
// Now, and not inside the workspace, because the login files are the
|
|
2741
|
-
// user's and may name any of the commands registered above.
|
|
2742
|
-
// `shell.start()`
|
|
2743
|
-
//
|
|
2744
|
-
//
|
|
2745
|
-
|
|
2746
|
-
|
|
472
|
+
// user's and may name any of the commands registered above. On a
|
|
473
|
+
// reconnect it is not awaited: `shell.start()` registers the input
|
|
474
|
+
// handler synchronously and the rc files apply as they finish, which is
|
|
475
|
+
// what the terminal has always done. On a wake the caller has the
|
|
476
|
+
// peer's frame in hand and delivers it the moment this returns, so the
|
|
477
|
+
// prompt has to be on the terminal first — otherwise the line runs
|
|
478
|
+
// alongside the rc files and its prompt lands before the shell's own.
|
|
479
|
+
// Either way a user's broken rc file must not take the socket down.
|
|
480
|
+
const started = workspace.start().catch((e) => {
|
|
481
|
+
console.warn('[nimbus] shell start failed:', errorText(e));
|
|
2747
482
|
});
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
HeredocHandler.install(shell, terminal);
|
|
2752
|
-
}
|
|
2753
|
-
function createHeadlessTerminal() {
|
|
2754
|
-
return {
|
|
2755
|
-
cols: 80,
|
|
2756
|
-
rows: 24,
|
|
2757
|
-
write() { },
|
|
2758
|
-
writeln() { },
|
|
2759
|
-
onData() { },
|
|
2760
|
-
focus() { },
|
|
2761
|
-
clear() { },
|
|
2762
|
-
};
|
|
483
|
+
if (options.resume === 'wake')
|
|
484
|
+
await started;
|
|
485
|
+
ws?.send(JSON.stringify({ type: 'ready' }));
|
|
2763
486
|
}
|