@nimbus-sh/worker 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +2 -2
  2. package/dist/_shared/preview-host.d.ts +26 -8
  3. package/dist/_shared/preview-host.d.ts.map +1 -1
  4. package/dist/_shared/preview-host.js +51 -11
  5. package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
  6. package/dist/cirrus-npm-cjs.generated.js +1 -1
  7. package/dist/facets/cirrus-real.d.ts.map +1 -1
  8. package/dist/facets/cirrus-real.js +6 -5
  9. package/dist/facets/compose.d.ts +148 -0
  10. package/dist/facets/compose.d.ts.map +1 -0
  11. package/dist/facets/compose.js +146 -0
  12. package/dist/facets/durable-images.d.ts +18 -5
  13. package/dist/facets/durable-images.d.ts.map +1 -1
  14. package/dist/facets/durable-images.js +36 -7
  15. package/dist/facets/durable-slots.d.ts +1 -1
  16. package/dist/facets/durable-slots.d.ts.map +1 -1
  17. package/dist/facets/durable-slots.js +2 -1
  18. package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
  19. package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
  20. package/dist/facets/esbuild-bundle-pool.js +122 -0
  21. package/dist/facets/exec-telemetry.d.ts +8 -0
  22. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  23. package/dist/facets/manager.d.ts +337 -123
  24. package/dist/facets/manager.d.ts.map +1 -1
  25. package/dist/facets/manager.js +1100 -449
  26. package/dist/facets/process.d.ts +20 -16
  27. package/dist/facets/process.d.ts.map +1 -1
  28. package/dist/facets/process.js +59 -28
  29. package/dist/facets/real-vite-fs-shim.js +2 -2
  30. package/dist/facets/resident-identity.d.ts +23 -0
  31. package/dist/facets/resident-identity.d.ts.map +1 -0
  32. package/dist/facets/resident-identity.js +31 -0
  33. package/dist/facets/vite-dev-server.d.ts +25 -47
  34. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  35. package/dist/facets/vite-dev-server.js +185 -219
  36. package/dist/facets/wasm-image-digest.d.ts +25 -0
  37. package/dist/facets/wasm-image-digest.d.ts.map +1 -0
  38. package/dist/facets/wasm-image-digest.js +25 -0
  39. package/dist/facets/wasm-swap-registry.d.ts +51 -92
  40. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  41. package/dist/facets/wasm-swap-registry.js +68 -227
  42. package/dist/git/commands.d.ts +0 -1
  43. package/dist/git/commands.d.ts.map +1 -1
  44. package/dist/git/commands.js +0 -3
  45. package/dist/git/network-facet.d.ts.map +1 -1
  46. package/dist/git/network-facet.js +6 -5
  47. package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
  48. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  49. package/dist/loaders/child-process/spawn-pool.js +3 -2
  50. package/dist/loaders/generated-workers.d.ts +2 -2
  51. package/dist/loaders/generated-workers.d.ts.map +1 -1
  52. package/dist/loaders/generated-workers.js +3 -3
  53. package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
  54. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  55. package/dist/loaders/npm-resolve-preamble.js +34 -108
  56. package/dist/node-shims-artifact.generated.js +3 -3
  57. package/dist/npm/install-batch-facet.d.ts +3 -2
  58. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  59. package/dist/npm/install-batch-facet.js +45 -15
  60. package/dist/npm/installer.d.ts +22 -17
  61. package/dist/npm/installer.d.ts.map +1 -1
  62. package/dist/npm/installer.js +209 -193
  63. package/dist/npm/pre-bundle-facet.d.ts +1 -1
  64. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  65. package/dist/npm/pre-bundle-facet.js +1 -1
  66. package/dist/npm/resolve-facet.d.ts +6 -0
  67. package/dist/npm/resolve-facet.d.ts.map +1 -1
  68. package/dist/npm/resolve-one-facet.d.ts +21 -20
  69. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  70. package/dist/npm/resolve-one-facet.js +85 -84
  71. package/dist/npm/semver.d.ts +59 -0
  72. package/dist/npm/semver.d.ts.map +1 -0
  73. package/dist/npm/semver.js +226 -0
  74. package/dist/router/index.d.ts.map +1 -1
  75. package/dist/router/index.js +47 -10
  76. package/dist/router/public-directory.d.ts +5 -1
  77. package/dist/router/public-directory.d.ts.map +1 -1
  78. package/dist/router/public-directory.js +9 -2
  79. package/dist/router/remote-api.d.ts +18 -0
  80. package/dist/router/remote-api.d.ts.map +1 -1
  81. package/dist/router/remote-api.js +64 -5
  82. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  83. package/dist/runtime/cpython-resident.js +3 -1
  84. package/dist/runtime/html-entrypoint.d.ts +7 -0
  85. package/dist/runtime/html-entrypoint.d.ts.map +1 -1
  86. package/dist/runtime/html-entrypoint.js +18 -2
  87. package/dist/runtime/node-runner.d.ts +3 -0
  88. package/dist/runtime/node-runner.d.ts.map +1 -1
  89. package/dist/runtime/node-runner.js +7 -0
  90. package/dist/runtime/node-shims.d.ts.map +1 -1
  91. package/dist/runtime/node-shims.js +305 -29
  92. package/dist/runtime/opencode-facet-runner.js +2 -2
  93. package/dist/runtime/package-manager.d.ts +44 -1
  94. package/dist/runtime/package-manager.d.ts.map +1 -1
  95. package/dist/runtime/package-manager.js +168 -3
  96. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  97. package/dist/runtime/ruby-resident.js +2 -0
  98. package/dist/session/agent.d.ts.map +1 -1
  99. package/dist/session/agent.js +28 -1
  100. package/dist/session/helpers.d.ts +38 -0
  101. package/dist/session/helpers.d.ts.map +1 -1
  102. package/dist/session/helpers.js +76 -0
  103. package/dist/session/init-phases.d.ts +1 -1
  104. package/dist/session/init-phases.d.ts.map +1 -1
  105. package/dist/session/init-phases.js +2 -12
  106. package/dist/session/init.d.ts +15 -1
  107. package/dist/session/init.d.ts.map +1 -1
  108. package/dist/session/init.js +91 -603
  109. package/dist/session/keys.d.ts +4 -0
  110. package/dist/session/keys.d.ts.map +1 -1
  111. package/dist/session/keys.js +4 -0
  112. package/dist/session/nimbus-session.d.ts +90 -23
  113. package/dist/session/nimbus-session.d.ts.map +1 -1
  114. package/dist/session/nimbus-session.js +200 -110
  115. package/dist/session/npm-install-port.d.ts +16 -0
  116. package/dist/session/npm-install-port.d.ts.map +1 -0
  117. package/dist/session/npm-install-port.js +37 -0
  118. package/dist/session/port-capability.d.ts +90 -0
  119. package/dist/session/port-capability.d.ts.map +1 -1
  120. package/dist/session/port-capability.js +219 -9
  121. package/dist/session/programmatic.d.ts +125 -5
  122. package/dist/session/programmatic.d.ts.map +1 -1
  123. package/dist/session/programmatic.js +439 -89
  124. package/dist/session/routes.d.ts +39 -17
  125. package/dist/session/routes.d.ts.map +1 -1
  126. package/dist/session/routes.js +67 -131
  127. package/dist/session/rpc.d.ts +39 -16
  128. package/dist/session/rpc.d.ts.map +1 -1
  129. package/dist/session/rpc.js +137 -130
  130. package/dist/session/serving-port.d.ts +27 -0
  131. package/dist/session/serving-port.d.ts.map +1 -0
  132. package/dist/session/serving-port.js +27 -0
  133. package/dist/session/start-real-vite.d.ts +12 -0
  134. package/dist/session/start-real-vite.d.ts.map +1 -1
  135. package/dist/session/start-real-vite.js +4 -4
  136. package/dist/session/supervisor-op.d.ts +41 -4
  137. package/dist/session/supervisor-op.d.ts.map +1 -1
  138. package/dist/session/supervisor-op.js +101 -73
  139. package/dist/session/supervisor-rpc.d.ts +7 -7
  140. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  141. package/dist/session/supervisor-rpc.js +6 -3
  142. package/dist/session/vite-command.d.ts +18 -0
  143. package/dist/session/vite-command.d.ts.map +1 -0
  144. package/dist/session/vite-command.js +609 -0
  145. package/dist/session/ws.d.ts +61 -0
  146. package/dist/session/ws.d.ts.map +1 -1
  147. package/dist/session/ws.js +122 -12
  148. package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
  149. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  150. package/dist/shell/npm-bin-entrypoints.js +12 -85
  151. package/package.json +24 -4
  152. package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
  153. package/public/index.html +1 -1
  154. package/scripts/bundle-facet-workers.mjs +1 -1
  155. package/scripts/bundle-npm-cjs.mjs +2 -2
  156. package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
  157. package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
  158. package/dist/facets/on-demand-bundle-gate.js +0 -125
  159. package/dist/npm/install-args.d.ts +0 -21
  160. package/dist/npm/install-args.d.ts.map +0 -1
  161. package/dist/npm/install-args.js +0 -70
  162. package/dist/npm/npm-log.d.ts +0 -31
  163. package/dist/npm/npm-log.d.ts.map +0 -1
  164. package/dist/npm/npm-log.js +0 -51
@@ -35,6 +35,7 @@
35
35
  */
36
36
  import { Shell, createCurlCommand, createNpmCommand, NPM_VERSION, createTopCommand, createWatchCommand, createHelpCommand, rehydrateGlobalPackages, } from '@nimbus-sh/core/substrate/lifo/index.js';
37
37
  import { createKillCommand } from '@nimbus-sh/core/substrate/lifo/commands/system/kill.js';
38
+ import { textSink } from '@nimbus-sh/core/_shared/bytes.js';
38
39
  import { NimbusWorkspace } from '@nimbus-sh/core/workspace';
39
40
  import { CRED_KERNEL, requireVfsCred } from '@nimbus-sh/core/runtime/os-contracts.js';
40
41
  import { WebSocketTerminal } from '../facets/ws-terminal.js';
@@ -42,50 +43,41 @@ import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
42
43
  import { runFresh } from '../runtime/node-runner.js';
43
44
  import { runBunScript, BUN_VERSION } from '../runtime/bun-runner.js';
44
45
  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
46
  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';
47
+ import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
52
48
  import { NimbusWrangler } from '../wrangler/nimbus-wrangler.js';
53
- import { filterWranglerFlags, detectBundlerBin, checkNodeModulesGuard, detectUnsupportedWranglerConfig, } from './helpers.js';
49
+ import { filterWranglerFlags, detectBundlerBin, checkNodeModulesGuard, detectUnsupportedWranglerConfig, refusedNextSubcommand, NEXT_REFUSAL_MESSAGE, } from './helpers.js';
50
+ import { createViteCommand } from './vite-command.js';
54
51
  import { HeredocHandler, LineEditorExtender } from '@nimbus-sh/core/shell/features.js';
55
52
  import { registerShellEntrypointCommands } from '@nimbus-sh/core/shell/shell-entrypoints.js';
56
53
  import { makeChshCommand } from '@nimbus-sh/core/substrate/lifo/shell/default-shell.js';
57
54
  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';
55
+ import { createNpmInstallPort } from './npm-install-port.js';
61
56
  import { makeNimbusVerbHandler, createRuntimeCommandHintResolver, } from '../runtime/package-manager.js';
57
+ import { rpcExposeApp, rpcListApps, rpcRemoveApp, rpcRotateLink } from './programmatic.js';
62
58
  import { listInstalledRuntimes, rehydrateInstalledRuntimes, registerRunnerFactory, } from '@nimbus-sh/core/runtime/installed-runtimes.js';
63
59
  // Runtime factories (clang/python/ruby/bash/wasm) are imported lazily at
64
60
  // first-use inside their registered handlers — see the registrations below.
65
61
  // Keeping them off the top-level import graph shaves their module-eval cost
66
62
  // (zod, embedded socket-kernel/shim sources, wasm loaders) out of the
67
63
  // one-time Worker Startup Time paid on every fresh-isolate cold run.
68
- import { hasSeededProject, SEED_PROJECT_DIR } from '@nimbus-sh/core/vfs/seed-project.js';
64
+ import { hasSeededProject, SEED_PROJECT_DIR, SEED_PROJECT_NAME, SEED_PROJECT_TILDE } from '@nimbus-sh/core/vfs/seed-project.js';
69
65
  import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
70
66
  import { generation } from '@nimbus-sh/fabric/generation.js';
71
67
  import { stripAnsi } from '@nimbus-sh/core/runtime/process-logs.js';
72
68
  import { DEFAULT_MOUNT_POINTS, NODE_VERSION, } from '@nimbus-sh/core/constants.js';
73
- import { ensureSessionStateSchema, loadShellState, stampHydratedAt, countSessionStateKeys, loadKernelMounts, persistKernelMounts, appendScrollback, loadScrollback, } from './state-store.js';
69
+ import { ensureSessionStateSchema, loadShellState, stampHydratedAt, countSessionStateKeys, loadKernelMounts, persistKernelMounts, loadScrollback, } from './state-store.js';
74
70
  import { recordRecoveryEvent } from '@nimbus-sh/platform/oom-discriminator.js';
75
71
  import { sessionAiEnv } from './ai.js';
76
72
  import { routeSessionLoopback } from './loopback.js';
77
73
  import { setPhase } from './init-phases.js';
74
+ import { shellTerminalTee } from './ws.js';
78
75
  import { VITE_CONFIG_KEY } from './keys.js';
79
- import { clearPortCapability } from './port-capability.js';
80
- function resolveNpmPrefix(prefix, cwd) {
81
- return prefix.startsWith('/')
82
- ? normalizeVfsPath(prefix)
83
- : resolveVfsPath(prefix, cwd || '/home/user');
84
- }
85
76
  function quoteShellArgument(value) {
86
77
  return `'${value.replace(/'/g, `'\\''`)}'`;
87
78
  }
88
- export async function initSession(self, ws) {
79
+ export async function initSession(self, ws, options = {}) {
80
+ const replayScrollback = options.resume !== 'wake';
89
81
  self.ensureSqliteFs();
90
82
  const kernelFs = self.sqliteFs.as(CRED_KERNEL);
91
83
  self.ensureFacetManager();
@@ -114,17 +106,7 @@ export async function initSession(self, ws) {
114
106
  // facet and the scrollback replay below is wire-phase work.
115
107
  // Phase B will tag in once we start building the kernel.
116
108
  setPhase(self, 'wire', 'init-session');
117
- self.terminal = new WebSocketTerminal(ws, (frame) => {
118
- try {
119
- appendScrollback(self.ctx, frame, Date.now());
120
- }
121
- catch (e) {
122
- try {
123
- console.warn('[B\'.3] appendScrollback failed:', e?.message || e);
124
- }
125
- catch { }
126
- }
127
- });
109
+ self.terminal = new WebSocketTerminal(ws, shellTerminalTee(self));
128
110
  // [B'.3] Replay persisted scrollback BEFORE the cold-start UI gate.
129
111
  // On rehydrate (hasPersistedState=true) we emit the prior
130
112
  // session's terminal contents as a single batched write — the
@@ -138,16 +120,18 @@ export async function initSession(self, ws) {
138
120
  // same scrollback both times. The cap eviction keeps total bytes
139
121
  // bounded.
140
122
  if (persisted.hasPersistedState) {
141
- try {
142
- const replay = loadScrollback(self.ctx);
143
- if (replay.length > 0)
144
- self.terminal.write(replay);
145
- }
146
- catch (e) {
123
+ if (replayScrollback) {
147
124
  try {
148
- console.warn('[B\'.3] scrollback replay failed:', e?.message || e);
125
+ const replay = loadScrollback(self.ctx);
126
+ if (replay.length > 0)
127
+ self.terminal.write(replay);
128
+ }
129
+ catch (e) {
130
+ try {
131
+ console.warn('[B\'.3] scrollback replay failed:', e?.message || e);
132
+ }
133
+ catch { }
149
134
  }
150
- catch { }
151
135
  }
152
136
  // The scrollback stops mid-command when the previous instance was
153
137
  // reset under it — the platform kills the isolate without running a
@@ -262,8 +246,8 @@ export async function initSession(self, ws) {
262
246
  stdout: parent.isFdTerminal?.(1) ?? false,
263
247
  stderr: parent.isFdTerminal?.(2) ?? false,
264
248
  },
265
- onStdout: (data) => parent.stdout.write(data),
266
- onStderr: (data) => parent.stderr.write(data),
249
+ onStdout: textSink((data) => parent.stdout.write(data)),
250
+ onStderr: textSink((data) => parent.stderr.write(data)),
267
251
  commandContext: {
268
252
  pid: identity.pid,
269
253
  cred: identity.cred,
@@ -589,6 +573,14 @@ export async function initSession(self, ws) {
589
573
  vfs: sqliteFs,
590
574
  registry: pkgRegistry,
591
575
  getHome: nimbusGetHome,
576
+ // The application verbs are the session's own (programmatic.ts):
577
+ // the shell, the SDK and the Agent all reach the same policy.
578
+ apps: {
579
+ expose: (target, options) => rpcExposeApp(self, target, options),
580
+ list: () => rpcListApps(self),
581
+ rotateLink: (target) => rpcRotateLink(self, target),
582
+ remove: (target) => rpcRemoveApp(self, target),
583
+ },
592
584
  warmRuntime: async (target, ctx) => {
593
585
  // The runtime name, which is what `nimbus install python` installs
594
586
  // — not the name the user typed. Left as 'python' through the
@@ -813,8 +805,8 @@ export async function initSession(self, ws) {
813
805
  const shellResult = await shell.execute(command, {
814
806
  cwd: ctx.cwd,
815
807
  env: ctx.env,
816
- onStdout: (d) => ctx.stdout.write(d),
817
- onStderr: (d) => ctx.stderr.write(d),
808
+ onStdout: textSink((d) => ctx.stdout.write(d)),
809
+ onStderr: textSink((d) => ctx.stderr.write(d)),
818
810
  });
819
811
  return shellResult.exitCode;
820
812
  }
@@ -1070,443 +1062,7 @@ export async function initSession(self, ws) {
1070
1062
  }
1071
1063
  });
1072
1064
  // ── vite command: start/stop the dev server ──────────────────────────
1073
- registry.register('vite', async (ctx) => {
1074
- const args = ctx.args || [];
1075
- const cwd = normalizeVfsPath(ctx.cwd || '/home/user');
1076
- if (args.includes('--help') || args.includes('-h')) {
1077
- ctx.stdout.write('Usage: vite [command] [options]\n\n');
1078
- ctx.stdout.write('Commands:\n');
1079
- ctx.stdout.write(' (default) Start dev server\n');
1080
- ctx.stdout.write(' build Build for production\n');
1081
- ctx.stdout.write(' preview Serve the built dist/\n');
1082
- ctx.stdout.write(' stop Stop dev server\n\n');
1083
- ctx.stdout.write('Options:\n');
1084
- ctx.stdout.write(' --root <dir> Project root\n');
1085
- ctx.stdout.write(' --port <n> Server port\n');
1086
- return 0;
1087
- }
1088
- self.ensureSqliteFs();
1089
- const viteConfig = {};
1090
- for (const cfgName of ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']) {
1091
- const cfgPath = cwd + '/' + cfgName;
1092
- if (kernelFs.exists(cfgPath)) {
1093
- try {
1094
- let cfgCode = kernelFs.readFileString(cfgPath);
1095
- // Transform TS to JS
1096
- if (cfgName.endsWith('.ts')) {
1097
- if (!self.esbuildService)
1098
- self.esbuildService = new EsbuildService(kernelFs);
1099
- const t = await self.esbuildService.transform(cfgCode, { loader: 'ts', format: 'esm' });
1100
- cfgCode = t.code;
1101
- }
1102
- Object.assign(viteConfig, parseViteConfigSource(cfgCode));
1103
- }
1104
- catch (e) {
1105
- ctx.stderr.write(`Warning: could not parse ${cfgName}: ${e?.message}\n`);
1106
- }
1107
- break;
1108
- }
1109
- }
1110
- // ── vite build ──
1111
- if (args[0] === 'build') {
1112
- if (!self.esbuildService)
1113
- self.esbuildService = new EsbuildService(kernelFs);
1114
- const htmlPath = cwd + '/index.html';
1115
- let entryPoint = cwd + '/src/main.tsx';
1116
- let origHtml = '';
1117
- try {
1118
- origHtml = kernelFs.readFileString(htmlPath);
1119
- const htmlEntrypoint = await findHtmlScriptEntrypoint(origHtml);
1120
- if (htmlEntrypoint)
1121
- entryPoint = cwd + '/' + stripLeadingSlashes(htmlEntrypoint);
1122
- }
1123
- catch {
1124
- ctx.stderr.write('Warning: no index.html\n');
1125
- }
1126
- if (!kernelFs.exists(entryPoint)) {
1127
- const alts = [cwd + '/src/main.tsx', cwd + '/src/main.ts', cwd + '/src/index.tsx', cwd + '/src/index.ts'];
1128
- entryPoint = alts.find(p => kernelFs.exists(p)) || entryPoint;
1129
- }
1130
- ctx.stdout.write('Building for production...\n');
1131
- ctx.stdout.write(' Entry: ' + entryPoint + '\n');
1132
- const t0 = Date.now();
1133
- try {
1134
- const outDir = viteConfig.outDir || 'dist';
1135
- const distDir = cwd + '/' + outDir;
1136
- // Detect which packages are installed vs need CDN
1137
- const nmDir = cwd + '/node_modules';
1138
- const externals = [];
1139
- const cdnPackages = [];
1140
- for (const pkg of ['react', 'react-dom', 'react/jsx-runtime', 'react-dom/client']) {
1141
- const pkgBase = pkg.split('/')[0];
1142
- if (!kernelFs.exists(nmDir + '/' + pkgBase)) {
1143
- externals.push(pkg);
1144
- if (!cdnPackages.includes(pkgBase))
1145
- cdnPackages.push(pkgBase);
1146
- }
1147
- }
1148
- if (viteConfig.alias)
1149
- externals.push(...Object.keys(viteConfig.alias));
1150
- // Bundle JS
1151
- const result = await self.esbuildService.build([entryPoint], {
1152
- bundle: true, format: 'esm', target: 'es2020', platform: 'browser',
1153
- minify: true, outdir: '/' + distDir + '/assets',
1154
- external: externals.length > 0 ? externals : undefined,
1155
- });
1156
- if (result.errors?.length) {
1157
- for (const e of result.errors)
1158
- ctx.stderr.write(' error: ' + e.text + '\n');
1159
- return 1;
1160
- }
1161
- // Generate content hash for filenames
1162
- let jsContent = '';
1163
- for (const f of result.outputFiles || []) {
1164
- jsContent = f.contents;
1165
- }
1166
- const hashNum = jsContent.split('').reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0);
1167
- const hash = (hashNum >>> 0).toString(36).padStart(6, '0');
1168
- // Write JS with hashed filename
1169
- const jsFilename = 'index-' + hash + '.js';
1170
- const jsPath = distDir + '/assets/' + jsFilename;
1171
- kernelFs.mkdir(distDir + '/assets', { recursive: true });
1172
- kernelFs.writeFile(jsPath, jsContent);
1173
- ctx.stdout.write(' \x1b[2m' + outDir + '/assets/' + jsFilename + '\x1b[0m ' + (jsContent.length / 1024).toFixed(2) + ' kB\n');
1174
- // Collect all CSS files from src/
1175
- let allCss = '';
1176
- const collectCss = (dir) => {
1177
- try {
1178
- for (const e of kernelFs.readdir(dir)) {
1179
- const fp = dir + '/' + e.name;
1180
- if (e.type === 'directory')
1181
- collectCss(fp);
1182
- else if (e.name.endsWith('.css')) {
1183
- try {
1184
- allCss += kernelFs.readFileString(fp) + '\n';
1185
- }
1186
- catch { }
1187
- }
1188
- }
1189
- }
1190
- catch { }
1191
- };
1192
- collectCss(cwd + '/src');
1193
- const cssFilename = 'index-' + hash + '.css';
1194
- if (allCss.trim()) {
1195
- kernelFs.writeFile(distDir + '/assets/' + cssFilename, allCss);
1196
- ctx.stdout.write(' \x1b[2m' + outDir + '/assets/' + cssFilename + '\x1b[0m ' + (allCss.length / 1024).toFixed(2) + ' kB\n');
1197
- }
1198
- // Generate dist/index.html
1199
- if (origHtml) {
1200
- const distHtml = await rewriteViteBuildHtml(origHtml, {
1201
- jsFilename,
1202
- cssFilename: allCss.trim() ? cssFilename : undefined,
1203
- removeImportMap: cdnPackages.length === 0,
1204
- });
1205
- kernelFs.writeFile(distDir + '/index.html', distHtml);
1206
- ctx.stdout.write(' \x1b[2m' + outDir + '/index.html\x1b[0m ' + (distHtml.length / 1024).toFixed(2) + ' kB\n');
1207
- if (cdnPackages.length > 0) {
1208
- ctx.stdout.write(' \x1b[33mNote: ' + cdnPackages.join(', ') + ' loaded from CDN (not bundled)\x1b[0m\n');
1209
- }
1210
- }
1211
- ctx.stdout.write('\n\x1b[32m\u2713 built in ' + ((Date.now() - t0) / 1000).toFixed(2) + 's\x1b[0m\n');
1212
- return 0;
1213
- }
1214
- catch (e) {
1215
- ctx.stderr.write('Build error: ' + (e?.message || e) + '\n');
1216
- return 1;
1217
- }
1218
- }
1219
- // ── vite preview ──
1220
- if (args[0] === 'preview') {
1221
- ctx.stdout.write('Serving dist/ — open ' + self.viteBasePath + '/\n');
1222
- const distRoot = cwd + '/' + (viteConfig.outDir || 'dist');
1223
- if (!kernelFs.exists(distRoot)) {
1224
- ctx.stderr.write('dist/ not found. Run vite build first.\n');
1225
- return 1;
1226
- }
1227
- // Start vite on the dist directory
1228
- if (!self.esbuildService)
1229
- self.esbuildService = new EsbuildService(kernelFs);
1230
- if (self.viteDevServer?.isRunning)
1231
- self.viteDevServer.stop();
1232
- const previewBasePath = self.viteBasePath;
1233
- // process metadata support: same long-running treatment as the
1234
- // dev path, just on the dist/ directory.
1235
- const previewPort = viteConfig.port || 4173; // vite preview default
1236
- const previewProcEntry = self.processes.spawn('vite preview (' + distRoot + ')', [], distRoot, { longRunning: true });
1237
- self.viteDevServer = new ViteDevServer({
1238
- vfs: self.sqliteFs, esbuild: self.esbuildService, root: distRoot,
1239
- onHmrMessage: () => { },
1240
- sql: self.ctx.storage.sql,
1241
- basePath: previewBasePath,
1242
- env: self.env,
1243
- ctx: self.ctx,
1244
- port: previewPort,
1245
- pid: previewProcEntry.pid,
1246
- processes: self.processes,
1247
- });
1248
- self.viteDevServer.start();
1249
- try {
1250
- const previewStub = makeLongRunningPortStub(self.viteDevServer);
1251
- self.portRegistry.bindFacetStub(previewProcEntry.pid, previewStub);
1252
- await clearPortCapability(self, previewPort);
1253
- self.portRegistry.register(previewPort, previewProcEntry.pid);
1254
- self._viteShimPid = previewProcEntry.pid;
1255
- self._viteShimPort = previewPort;
1256
- }
1257
- catch { }
1258
- try {
1259
- await self.ctx.storage.put(VITE_CONFIG_KEY, { root: distRoot, basePath: previewBasePath, port: previewPort });
1260
- }
1261
- catch { }
1262
- ctx.stdout.write('Serving at ' + previewBasePath + '/ \x1b[2m(pid=' + previewProcEntry.pid + ', port=' + previewPort + ')\x1b[0m\n');
1263
- return 0;
1264
- }
1265
- // ── vite stop ──
1266
- if (args[0] === 'stop') {
1267
- let stopped = false;
1268
- if (self.cirrusReal?.isRunning) {
1269
- self.cirrusReal.stop(self.ctx);
1270
- self.cirrusReal = null;
1271
- stopped = true;
1272
- }
1273
- if (self.viteDevServer?.isRunning) {
1274
- self.viteDevServer.stop();
1275
- self.viteDevServer = null;
1276
- try {
1277
- await self.ctx.storage.delete(VITE_CONFIG_KEY);
1278
- }
1279
- catch { }
1280
- stopped = true;
1281
- }
1282
- // Primitive #3 teardown — symmetric with the start path. Always
1283
- // safe to call: unregisterByPid is idempotent, exit() guards
1284
- // against re-marking already-terminal entries.
1285
- if (self._viteShimPid != null) {
1286
- try {
1287
- self.portRegistry.unregisterByPid(self._viteShimPid);
1288
- }
1289
- catch { }
1290
- try {
1291
- self.processes.exit(self._viteShimPid, 0);
1292
- }
1293
- catch { }
1294
- notifyTerminalEvent(self.terminal, {
1295
- type: 'exit', pid: self._viteShimPid, code: 0, command: 'vite',
1296
- });
1297
- self._viteShimPid = null;
1298
- self._viteShimPort = null;
1299
- }
1300
- if (stopped) {
1301
- ctx.stdout.write('\x1b[33mDev server stopped.\x1b[0m\n');
1302
- }
1303
- else {
1304
- ctx.stdout.write('No dev server running.\n');
1305
- }
1306
- return 0;
1307
- }
1308
- // ── vite (default: dev server) ──
1309
- let vfsRoot = cwd;
1310
- for (let i = 0; i < args.length; i++) {
1311
- if (args[i] === '--root' && args[i + 1])
1312
- vfsRoot = resolveVfsPath(args[i + 1], cwd);
1313
- }
1314
- if (viteConfig.root && viteConfig.root !== '.') {
1315
- vfsRoot = resolveVfsPath(viteConfig.root, cwd);
1316
- }
1317
- vfsRoot = normalizeVfsPath(vfsRoot);
1318
- // Argv expansion: package.json scripts commonly write
1319
- // `--port ${PORT:-3000}`. Resolve it once and feed both Vite
1320
- // backends so `/api/stats`, `/preview/`, and port tabs agree.
1321
- const expandedArgs = expandArgvShellDefaults(args, ctx.env || {});
1322
- const vitePortDefault = 5173;
1323
- const resolvedPort = resolveLongRunningPort({
1324
- argv: expandedArgs,
1325
- env: ctx.env,
1326
- configPort: viteConfig.port,
1327
- fallback: vitePortDefault,
1328
- });
1329
- // ── Preflight: node_modules guard ────────────────────────────────────
1330
- // Direct `vite` invocation requires installed deps. Bail loudly BEFORE
1331
- // spawning a dev server that would just serve broken modules and
1332
- // confuse the user. --force / --no-install-check bypasses the check.
1333
- const bypassInstallCheck = expandedArgs.includes('--force') || expandedArgs.includes('--no-install-check');
1334
- if (!bypassInstallCheck) {
1335
- const guard = checkNodeModulesGuard(kernelFs, vfsRoot);
1336
- if (guard.missing) {
1337
- ctx.stderr.write('\x1b[31m\u2718\x1b[0m \x1b[1mnode_modules/ not found\x1b[0m' +
1338
- (guard.depCount > 0 ? ` (${guard.depCount} dependencies declared)` : '') + '\n' +
1339
- ' Run \x1b[36mnpm install\x1b[0m in ' + vfsRoot + ' first,\n' +
1340
- ' or re-run with \x1b[36m--force\x1b[0m to skip this check.\n');
1341
- return 1;
1342
- }
1343
- }
1344
- if (self.viteDevServer?.isRunning)
1345
- self.viteDevServer.stop();
1346
- // ── Real-vite mode (Phase 0 spike, opt-in) ─────────────────────────
1347
- // NIMBUS_REAL_VITE=1 or `nimbusDevServer: 'real'` in vite.config.ts
1348
- // routes the session through a dynamic-worker facet running the
1349
- // real `vite` npm package. The in-process Cirrus shim is bypassed.
1350
- //
1351
- // This is EXPERIMENTAL and gated behind an explicit opt-in. Any
1352
- // error here falls back to Cirrus by the user re-running without
1353
- // the env flag — we do not silently fall back (fidelity over
1354
- // magic).
1355
- const sessionEnv = (ctx && ctx.env) || {};
1356
- const useReal = shouldUseRealVite({ env: sessionEnv, viteConfig });
1357
- if (useReal) {
1358
- const vitePort = resolvedPort;
1359
- const previewBasePath = self.viteBasePath;
1360
- // One boot path, shared with hibernation-restore (start-real-vite.ts):
1361
- // pre-bundles the user's vite.config, boots the facet, registers the
1362
- // port, and persists the vite-config so a woken session rebuilds the
1363
- // same real-vite server. Only the banner below is command-specific.
1364
- const { cirrusReal, userConfigBundle, cfgPath } = await startRealVite(self, {
1365
- root: vfsRoot,
1366
- port: vitePort,
1367
- basePath: previewBasePath,
1368
- configDir: cwd,
1369
- signal: ctx.signal,
1370
- onConfigError: (msg) => {
1371
- ctx.stderr.write('\x1b[33m!\x1b[0m vite.config bundling failed: ' + msg + '\n');
1372
- ctx.stderr.write(' Real-vite will run with default config.\n');
1373
- },
1374
- });
1375
- {
1376
- // ── Boot banner (§4.3 of PHASE2-REAL-VITE-PLAN.md) ──────
1377
- const snap = cirrusReal.stats.snapshot;
1378
- ctx.stdout.write('\n\x1b[1;36m Nimbus: real-vite mode\x1b[0m \x1b[2m(experimental, Phase 1-4)\x1b[0m\n\n');
1379
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Preview: \x1b[36m' + previewBasePath + '/\x1b[0m\n');
1380
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Vite: ' + cirrusReal.stats.viteVersion + ' (bundled)\n');
1381
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Root: ' + vfsRoot + '\n');
1382
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Port: ' + vitePort + ' \x1b[2m(virtual routing key)\x1b[0m\n');
1383
- if (snap) {
1384
- const kb = (snap.totalBytes / 1024).toFixed(1);
1385
- const pkgJson = snap.packageJsonCount;
1386
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Snapshot: ' + snap.fileCount + ' files / ' +
1387
- kb + ' KB ' +
1388
- (pkgJson ? '\x1b[2m(incl. ' + pkgJson + ' package.json, rest lazy)\x1b[0m' : '') + '\n');
1389
- }
1390
- if (userConfigBundle) {
1391
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Config: ' + cfgPath + ' \x1b[2m(' +
1392
- (userConfigBundle.length / 1024).toFixed(0) + ' KB bundled)\x1b[0m\n');
1393
- }
1394
- ctx.stdout.write('\n \x1b[2mWorks:\x1b[0m @vitejs/plugin-react, JSX/TSX transforms, SPA fallback, HMR.\n');
1395
- ctx.stdout.write(' \x1b[2mPartial:\x1b[0m other plugins (Babel-family generally OK; SWC/Rolldown blocked).\n');
1396
- ctx.stdout.write(' \x1b[2mBlocked:\x1b[0m vite build (rolldown needs node:wasi). Use cirrus for build.\n');
1397
- 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');
1398
- return 0;
1399
- }
1400
- }
1401
- if (!self.esbuildService)
1402
- self.esbuildService = new EsbuildService(kernelFs);
1403
- const previewBasePath = self.viteBasePath;
1404
- const viteDefine = viteConfig.define;
1405
- // Vite dev servers are represented as long-running process-table
1406
- // entries and port-registry handlers, so `ps`, logs, preview routing,
1407
- // `vite stop`, and `kill <pid>` share the same lifecycle primitives.
1408
- // Allocate PID FIRST so we can plumb it into ViteDevServer's
1409
- // process-log wiring at construction time. The PID stays valid
1410
- // for the life of this dev-server instance; subsequent log lines
1411
- // emitted by ViteDevServer flow into the pid's stderr ring,
1412
- // visible in the Process tab.
1413
- //
1414
- // Long-running handoff (bin-spawn contract): when invoked from a
1415
- // wrapper that already allocated a pid (`npm run dev` via
1416
- // shellExecuteTracked, or the npm-bin resolver), ADOPT that pid instead
1417
- // of spawning a second one. One pid, one start banner, no false exit —
1418
- // the wrapper stays `running` in /api/processes with this port.
1419
- const binSpawn = ctx.__nimbusBinSpawn;
1420
- const adoptedEntry = binSpawn?.skipSpawn && binSpawn.callerPid != null
1421
- ? self.processes.get(binSpawn.callerPid)
1422
- : undefined;
1423
- const handedOff = adoptedEntry != null;
1424
- const viteProcEntry = adoptedEntry ?? self.processes.spawn('vite (' + vfsRoot + ')', expandedArgs, vfsRoot, { longRunning: true });
1425
- if (handedOff)
1426
- self.processes.setLongRunning(viteProcEntry.pid);
1427
- self.viteDevServer = new ViteDevServer({
1428
- vfs: self.sqliteFs,
1429
- esbuild: self.esbuildService,
1430
- root: vfsRoot,
1431
- port: resolvedPort,
1432
- aliases: viteConfig.alias,
1433
- define: viteDefine,
1434
- onHmrMessage: (msg) => {
1435
- if (self.terminal)
1436
- try {
1437
- self.terminal.ws.send(JSON.stringify({ type: 'hmr', data: msg }));
1438
- }
1439
- catch { }
1440
- },
1441
- sql: self.ctx.storage.sql,
1442
- injectBasename: viteConfig.injectBasename,
1443
- basePath: previewBasePath,
1444
- env: self.env,
1445
- ctx: self.ctx,
1446
- // process diagnostics support: wire dev-server diagnostics into the
1447
- // supervisor's per-PID log store so the Process tab is no
1448
- // longer silent after the banner.
1449
- pid: viteProcEntry.pid,
1450
- processes: self.processes,
1451
- });
1452
- self.viteDevServer.start();
1453
- try {
1454
- await self.ctx.storage.put(VITE_CONFIG_KEY, {
1455
- root: vfsRoot, aliases: viteConfig.alias, define: viteDefine,
1456
- injectBasename: viteConfig.injectBasename, basePath: previewBasePath,
1457
- port: resolvedPort,
1458
- });
1459
- }
1460
- catch { }
1461
- // Register the port and build the long-running stub. The stub
1462
- // forwards into the in-process viteDevServer through the generic
1463
- // long-running adapter — same hook every future long-running
1464
- // facet uses (Express, Bun.serve, http.createServer().listen()).
1465
- const viteStub = makeLongRunningPortStub(self.viteDevServer);
1466
- self.portRegistry.bindFacetStub(viteProcEntry.pid, viteStub);
1467
- await clearPortCapability(self, resolvedPort);
1468
- self.portRegistry.register(resolvedPort, viteProcEntry.pid);
1469
- // Track the wiring so `vite stop` and crash-handlers can tear it
1470
- // down without searching the registry.
1471
- self._viteShimPid = viteProcEntry.pid;
1472
- self._viteShimPort = resolvedPort;
1473
- // Spawn / long-running event for the Process tab UI. Mirrors the
1474
- // shellExecuteTracked banner so the user sees the same shape no
1475
- // matter how vite was invoked. Suppressed on handoff — the wrapper
1476
- // already emitted the single start banner and spawn event for this pid.
1477
- if (!handedOff) {
1478
- if (self.terminal) {
1479
- self.terminal.write(`\x1b[2m[shell started (long-running): pid=${viteProcEntry.pid} cmd="vite ${expandedArgs.join(' ')}"]\x1b[0m\r\n`);
1480
- }
1481
- notifyTerminalEvent(self.terminal, {
1482
- type: 'spawn',
1483
- pid: viteProcEntry.pid,
1484
- command: 'vite ' + expandedArgs.join(' '),
1485
- longRunning: true,
1486
- attachedTty: false,
1487
- });
1488
- }
1489
- // Banner — reports the resolved port and PID so the user can
1490
- // verify the multi-target routing.
1491
- ctx.stdout.write('\n\x1b[1;36m Nimbus Vite Dev Server\x1b[0m\n\n');
1492
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Preview: \x1b[36m' + previewBasePath + '/\x1b[0m');
1493
- if (resolvedPort !== vitePortDefault) {
1494
- ctx.stdout.write(' \x1b[2m(also: ' + previewBasePath + '/?port=' + resolvedPort + ')\x1b[0m');
1495
- }
1496
- ctx.stdout.write('\n');
1497
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Root: ' + vfsRoot + '\n');
1498
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Port: ' + resolvedPort + ' \x1b[2m(pid=' + viteProcEntry.pid + ')\x1b[0m\n');
1499
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Transforms: .ts .tsx .jsx (React JSX automatic)\n');
1500
- if (viteConfig.alias)
1501
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Aliases: ' + Object.keys(viteConfig.alias).join(', ') + '\n');
1502
- if (viteDefine)
1503
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Define: ' + Object.keys(viteDefine).join(', ') + '\n');
1504
- const twCfg = [vfsRoot + '/tailwind.config.js', vfsRoot + '/tailwind.config.ts'].find(p => kernelFs.exists(p));
1505
- if (twCfg)
1506
- ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Tailwind: edge-vendored Play CDN \x1b[2m(detected)\x1b[0m\n');
1507
- ctx.stdout.write('\n \x1b[2mRun \x1b[0mvite stop\x1b[2m, or \x1b[0mkill ' + viteProcEntry.pid + '\x1b[2m, to stop.\x1b[0m\n\n');
1508
- return 0;
1509
- });
1065
+ registry.register('vite', createViteCommand(self));
1510
1066
  // ── nimbus-wrangler / wrangler command: Worker dev server ─────────────
1511
1067
  //
1512
1068
  // `wrangler` is registered as a transparent alias for `nimbus-wrangler`
@@ -1742,8 +1298,8 @@ export async function initSession(self, ws) {
1742
1298
  const result = await shell.execute(cmd, {
1743
1299
  cwd: cmdCtx.cwd,
1744
1300
  env: cmdCtx.env,
1745
- onStdout: (d) => cmdCtx.stdout.write(d),
1746
- onStderr: (d) => cmdCtx.stderr.write(d),
1301
+ onStdout: textSink((d) => cmdCtx.stdout.write(d)),
1302
+ onStderr: textSink((d) => cmdCtx.stderr.write(d)),
1747
1303
  stdin,
1748
1304
  terminalStdin: cmdCtx.terminalStdin,
1749
1305
  // `npm run x` runs x on npm's own fds. Handing the nested execution a
@@ -1821,15 +1377,18 @@ export async function initSession(self, ws) {
1821
1377
  });
1822
1378
  // Wrap the caller-supplied streams so every chunk is both displayed
1823
1379
  // AND captured in the ring buffer keyed by this PID.
1824
- const tee = (stream, target) => (d) => {
1825
- try {
1826
- self.processes.appendOutput(pid, stream, String(d));
1827
- }
1828
- catch { }
1829
- try {
1830
- target.write(d);
1831
- }
1832
- catch { }
1380
+ const tee = (stream, target) => {
1381
+ const toTarget = textSink((text) => target.write(text));
1382
+ return (d) => {
1383
+ try {
1384
+ self.processes.appendOutputBytes(pid, stream, d);
1385
+ }
1386
+ catch { }
1387
+ try {
1388
+ toTarget(d);
1389
+ }
1390
+ catch { }
1391
+ };
1833
1392
  };
1834
1393
  let exitCode = 1;
1835
1394
  try {
@@ -1872,7 +1431,15 @@ export async function initSession(self, ws) {
1872
1431
  catch (e) {
1873
1432
  // Surface the error in the terminal and the ring buffer.
1874
1433
  const msg = (e && (e.stack || e.message)) || String(e);
1875
- tee('stderr', cmdCtx.stderr)('shellExecuteTracked error: ' + msg + '\n');
1434
+ const line = 'shellExecuteTracked error: ' + msg + '\n';
1435
+ try {
1436
+ self.processes.appendOutput(pid, 'stderr', line);
1437
+ }
1438
+ catch { }
1439
+ try {
1440
+ cmdCtx.stderr.write(line);
1441
+ }
1442
+ catch { }
1876
1443
  exitCode = 1;
1877
1444
  }
1878
1445
  finally {
@@ -1927,8 +1494,11 @@ export async function initSession(self, ws) {
1927
1494
  catch { }
1928
1495
  },
1929
1496
  });
1930
- // Register core npm with enhanced `npm run <script>` support
1931
- const coreNpmCmd = createNpmCommand(registry, shellExecute, kernel);
1497
+ // Register core npm — the install work itself goes through the port,
1498
+ // which owns the NpmInstaller, prefix dirs and bin materialisation.
1499
+ const coreNpmCmd = createNpmCommand(registry, shellExecute, kernel, {
1500
+ installer: createNpmInstallPort(self),
1501
+ });
1932
1502
  registry.register('npm', async (ctx) => {
1933
1503
  const args = ctx.args || [];
1934
1504
  const sub = args[0];
@@ -2012,26 +1582,17 @@ export async function initSession(self, ws) {
2012
1582
  // (the only thing the module did was hold these strings) and
2013
1583
  // the file is deleted.
2014
1584
  //
2015
- // Next.js dev/start still needs a custom http.Server +
2016
- // child_process.fork with v8-IPC + webpack/Turbopack, none of
2017
- // which Nimbus ships. We surface a deterministic message
2018
- // rather than letting the script hang or emit a confusing
2019
- // crash. The block remains a one-off symbol-detection
2020
- // guard, NOT a generic per-framework code path. Any future
2021
- // similar guard belongs alongside this one — not in its own
2022
- // src/frameworks/<name>.ts file.
2023
- if ((scriptName === 'dev' || scriptName === 'start') &&
2024
- (pkg.dependencies?.next || pkg.devDependencies?.next) &&
2025
- !(scriptArgs.includes('--force') || scriptArgs.includes('--allow-next'))) {
2026
- const NEXT_BLOCK_MESSAGE = '\x1b[31m\u2718\x1b[0m \x1b[1mNext.js dev server is not supported in Nimbus.\x1b[0m\n' +
2027
- ' Specific blockers:\n' +
2028
- " 1. \x1b[2mchild_process.fork\x1b[0m IPC uses v8-serializer (Nimbus ships JSON projection).\n" +
2029
- ' 2. webpack / Turbopack bundlers are not integrated with the pre-bundle pipeline.\n' +
2030
- ' 3. Custom \x1b[2mhttp.Server\x1b[0m semantics (keep-alive, raw sockets) are facet-incompatible.\n' +
2031
- '\n' +
2032
- ' Workaround: deploy with \x1b[36mnext build\x1b[0m + a hosted runtime,\n' +
2033
- ' or pass \x1b[36m--allow-next\x1b[0m to bypass at your own risk.\n';
2034
- ctx.stderr.write(NEXT_BLOCK_MESSAGE);
1585
+ // Next.js dev/start/build/export all need the same pipeline —
1586
+ // custom http.Server + child_process.fork with v8-IPC +
1587
+ // webpack/Turbopack — none of which Nimbus ships. We surface a
1588
+ // deterministic refusal rather than letting the script hang or
1589
+ // (build) reset the isolate during spawn. The block remains a
1590
+ // one-off symbol-detection guard, NOT a generic per-framework
1591
+ // code path. Any future similar guard belongs alongside this
1592
+ // one — not in its own src/frameworks/<name>.ts file.
1593
+ const refused = refusedNextSubcommand(scriptName, script, pkg, scriptArgs);
1594
+ if (refused) {
1595
+ ctx.stderr.write(NEXT_REFUSAL_MESSAGE);
2035
1596
  return 127;
2036
1597
  }
2037
1598
  const scriptTrim = script.trim();
@@ -2141,85 +1702,6 @@ export async function initSession(self, ws) {
2141
1702
  catch { }
2142
1703
  return 0;
2143
1704
  }
2144
- // npm install (no args or with packages) — use NpmInstaller v2 (batched writes)
2145
- if (sub === 'install' || sub === 'i' || sub === 'add') {
2146
- const installInvocation = parseNpmInstallInvocation(args.slice(1));
2147
- const explicitPkgs = installInvocation.packages;
2148
- const globalPrefix = installInvocation.global
2149
- ? resolveNpmPrefix(installInvocation.prefix ?? String(ctx.env?.npm_config_prefix || '/usr/local'), ctx.cwd || '/home/user')
2150
- : null;
2151
- self.ensureSqliteFs();
2152
- if (globalPrefix)
2153
- self.ensureGlobalPrefixDirs(globalPrefix);
2154
- const installCwd = globalPrefix ? `${globalPrefix}/lib` : cwdKey;
2155
- // Ensure package.json exists for bare `npm install`
2156
- if (!globalPrefix && explicitPkgs.length === 0) {
2157
- const pkgJsonPath = installCwd + '/package.json';
2158
- if (!kernelFs.exists(pkgJsonPath)) {
2159
- ctx.stderr.write('npm ERR! no package.json found\n');
2160
- return 1;
2161
- }
2162
- }
2163
- if (globalPrefix && explicitPkgs.length === 0) {
2164
- ctx.stderr.write('npm ERR! missing package name for global install\n');
2165
- return 1;
2166
- }
2167
- const pkgLabel = explicitPkgs.length > 0
2168
- ? `${explicitPkgs.length} packages`
2169
- : 'dependencies from package.json';
2170
- ctx.stdout.write(`\x1b[36mInstalling ${pkgLabel} (npm v2 — batched writes)...\x1b[0m\n`);
2171
- await self.ensureNpmInstaller((msg) => {
2172
- ctx.stdout.write('[npm] ' + msg + '\n');
2173
- });
2174
- // `--loglevel` selects npm's own log protocol on stderr, where npm
2175
- // writes it. Tooling that drives npm parses those lines rather than
2176
- // our prose — pi's installer advances its progress label off them.
2177
- const npmLogLevel = installInvocation.loglevel;
2178
- const npmLog = npmLogLevel === null ? undefined : ((level, line) => {
2179
- if (npmLogEnabled(npmLogLevel, level))
2180
- ctx.stderr.write(line + '\n');
2181
- });
2182
- try {
2183
- const result = await self.npmInstaller.install(installCwd, {
2184
- packages: explicitPkgs.length > 0 ? explicitPkgs : undefined,
2185
- production: installInvocation.production,
2186
- pid: ctx.pid,
2187
- npmLog,
2188
- });
2189
- if (result.failed?.length > 0) {
2190
- ctx.stderr.write('\x1b[31mFailed: ' + result.failed.join(', ') + '\x1b[0m\n');
2191
- }
2192
- // [HONEST INSTALL MESSAGE P0a] Yellow + "(N failed, see above)"
2193
- // when partial. Green only when failed.length === 0.
2194
- const partial = (result.failed?.length || 0) > 0;
2195
- const color = partial ? '\x1b[33m' : '\x1b[32m';
2196
- const suffix = partial ? ` (${result.failed.length} failed, see above)` : '';
2197
- 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`);
2198
- if (result.cachedHits > 0) {
2199
- ctx.stdout.write(`\x1b[2m (${result.cachedHits} from cache)\x1b[0m\n`);
2200
- }
2201
- if (globalPrefix) {
2202
- // Materialize on-PATH bin shims even for partial installs. The
2203
- // only writer of shims into ${globalPrefix}/bin used to be gated
2204
- // behind zero failures across the whole dependency tree — but a
2205
- // global install of a 100+-dep package almost always has at least
2206
- // one transitive failure, so /usr/local/bin was ~never created
2207
- // and the installed package's bin was unreachable. materialize →
2208
- // validateEntry → resolveExistingTarget already skips bins whose
2209
- // target file didn't land, so a partial install safely exposes
2210
- // exactly the bins that actually installed.
2211
- const linked = materializeNpmBinShims(kernelFs, `${installCwd}/node_modules`, `${globalPrefix}/bin`);
2212
- if (linked > 0) {
2213
- ctx.stdout.write(`\x1b[2m linked ${linked} bin${linked === 1 ? '' : 's'} into /${globalPrefix}/bin\x1b[0m\n`);
2214
- }
2215
- }
2216
- return result.failed?.length > 0 ? 1 : 0;
2217
- }
2218
- catch (e) {
2219
- ctx.stderr.write(`\x1b[31mnpm install failed: ${e?.message}\x1b[0m\n`);
2220
- return 1;
2221
- }
2222
- }
2223
1705
  // ── npm create <pkg> / npm init <pkg> → npx create-<pkg> ─────────
2224
1706
  //
2225
1707
  // Per the npm spec, `npm create X args...` and `npm init X args...`
@@ -2638,17 +2120,17 @@ export async function initSession(self, ws) {
2638
2120
  catch { }
2639
2121
  // ── Starter-app hint (only if seed sentinel still exists) ──
2640
2122
  // We check the live VFS, not a static file, so that if the user
2641
- // deletes ~/.nimbus-seeded (or ~/app) the hint stops appearing on
2642
- // next login.
2123
+ // deletes ~/.nimbus-seeded (or the project dir) the hint stops
2124
+ // appearing on next login.
2643
2125
  try {
2644
2126
  if (hasSeededProject(self.sqliteFs) && kernelFs.exists(SEED_PROJECT_DIR)) {
2645
- self.terminal.write('\x1b[2mStarter app ready at \x1b[36m~/app\x1b[0m\x1b[2m — try:\x1b[0m\r\n' +
2646
- ' \x1b[36mcd app && npm install && npm run dev\x1b[0m\r\n\r\n');
2127
+ self.terminal.write(`\x1b[2mStarter app ready at \x1b[36m${SEED_PROJECT_TILDE}\x1b[0m\x1b[2m — try:\x1b[0m\r\n` +
2128
+ ` \x1b[36mcd ${SEED_PROJECT_NAME} && npm install && npm run dev\x1b[0m\r\n\r\n`);
2647
2129
  }
2648
2130
  }
2649
2131
  catch { }
2650
2132
  // ── W11: framework detection MOTD line ──
2651
- // If ~/app has a recognizable framework, print one informational line.
2133
+ // If the seeded project dir has a recognizable framework, print one informational line.
2652
2134
  // Purely advisory — does not change boot behaviour. Fire-and-forget
2653
2135
  // because initSession is sync; any failure is silently swallowed.
2654
2136
  void (async () => {
@@ -2729,13 +2211,19 @@ export async function initSession(self, ws) {
2729
2211
  // ── Start shell ──
2730
2212
  //
2731
2213
  // Now, and not inside the workspace, because the login files are the
2732
- // user's and may name any of the commands registered above. Not awaited:
2733
- // `shell.start()` runs synchronously and the rc files apply as they
2734
- // finish, which is what the terminal has always done. A user's broken
2735
- // rc file must not take the socket down with it.
2736
- workspace.start().catch((e) => {
2737
- console.warn('[nimbus] shell start failed:', e?.message || e);
2214
+ // user's and may name any of the commands registered above. On a
2215
+ // reconnect it is not awaited: `shell.start()` registers the input
2216
+ // handler synchronously and the rc files apply as they finish, which is
2217
+ // what the terminal has always done. On a wake the caller has the
2218
+ // peer's frame in hand and delivers it the moment this returns, so the
2219
+ // prompt has to be on the terminal first — otherwise the line runs
2220
+ // alongside the rc files and its prompt lands before the shell's own.
2221
+ // Either way a user's broken rc file must not take the socket down.
2222
+ const started = workspace.start().catch((e) => {
2223
+ console.warn('[nimbus] shell start failed:', errorText(e));
2738
2224
  });
2225
+ if (options.resume === 'wake')
2226
+ await started;
2739
2227
  ws.send(JSON.stringify({ type: 'ready' }));
2740
2228
  }
2741
2229
  function installShellExecutionFeatures(shell, terminal) {