@nimbus-sh/worker 0.6.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 (133) hide show
  1. package/README.md +2 -2
  2. package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
  3. package/dist/cirrus-npm-cjs.generated.js +1 -1
  4. package/dist/facets/cirrus-real.d.ts.map +1 -1
  5. package/dist/facets/cirrus-real.js +6 -5
  6. package/dist/facets/compose.d.ts +148 -0
  7. package/dist/facets/compose.d.ts.map +1 -0
  8. package/dist/facets/compose.js +146 -0
  9. package/dist/facets/durable-images.d.ts +9 -5
  10. package/dist/facets/durable-images.d.ts.map +1 -1
  11. package/dist/facets/durable-images.js +17 -7
  12. package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
  13. package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
  14. package/dist/facets/esbuild-bundle-pool.js +122 -0
  15. package/dist/facets/exec-telemetry.d.ts +8 -0
  16. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  17. package/dist/facets/manager.d.ts +240 -109
  18. package/dist/facets/manager.d.ts.map +1 -1
  19. package/dist/facets/manager.js +661 -337
  20. package/dist/facets/process.d.ts +20 -16
  21. package/dist/facets/process.d.ts.map +1 -1
  22. package/dist/facets/process.js +59 -28
  23. package/dist/facets/real-vite-fs-shim.js +2 -2
  24. package/dist/facets/vite-dev-server.d.ts +25 -47
  25. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  26. package/dist/facets/vite-dev-server.js +185 -219
  27. package/dist/facets/wasm-image-digest.d.ts +25 -0
  28. package/dist/facets/wasm-image-digest.d.ts.map +1 -0
  29. package/dist/facets/wasm-image-digest.js +25 -0
  30. package/dist/facets/wasm-swap-registry.d.ts +51 -92
  31. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  32. package/dist/facets/wasm-swap-registry.js +68 -227
  33. package/dist/git/network-facet.d.ts.map +1 -1
  34. package/dist/git/network-facet.js +6 -5
  35. package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
  36. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  37. package/dist/loaders/child-process/spawn-pool.js +3 -2
  38. package/dist/loaders/generated-workers.d.ts +2 -2
  39. package/dist/loaders/generated-workers.d.ts.map +1 -1
  40. package/dist/loaders/generated-workers.js +3 -3
  41. package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
  42. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  43. package/dist/loaders/npm-resolve-preamble.js +34 -108
  44. package/dist/node-shims-artifact.generated.js +3 -3
  45. package/dist/npm/install-batch-facet.d.ts +3 -2
  46. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  47. package/dist/npm/install-batch-facet.js +45 -15
  48. package/dist/npm/installer.d.ts +22 -17
  49. package/dist/npm/installer.d.ts.map +1 -1
  50. package/dist/npm/installer.js +209 -193
  51. package/dist/npm/pre-bundle-facet.d.ts +1 -1
  52. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  53. package/dist/npm/pre-bundle-facet.js +1 -1
  54. package/dist/npm/resolve-facet.d.ts +6 -0
  55. package/dist/npm/resolve-facet.d.ts.map +1 -1
  56. package/dist/npm/resolve-one-facet.d.ts +21 -20
  57. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  58. package/dist/npm/resolve-one-facet.js +85 -84
  59. package/dist/npm/semver.d.ts +59 -0
  60. package/dist/npm/semver.d.ts.map +1 -0
  61. package/dist/npm/semver.js +226 -0
  62. package/dist/router/remote-api.js +31 -1
  63. package/dist/runtime/html-entrypoint.d.ts +7 -0
  64. package/dist/runtime/html-entrypoint.d.ts.map +1 -1
  65. package/dist/runtime/html-entrypoint.js +18 -2
  66. package/dist/runtime/node-runner.d.ts +3 -0
  67. package/dist/runtime/node-runner.d.ts.map +1 -1
  68. package/dist/runtime/node-runner.js +7 -0
  69. package/dist/runtime/node-shims.d.ts.map +1 -1
  70. package/dist/runtime/node-shims.js +305 -29
  71. package/dist/runtime/opencode-facet-runner.js +2 -2
  72. package/dist/session/helpers.d.ts +38 -0
  73. package/dist/session/helpers.d.ts.map +1 -1
  74. package/dist/session/helpers.js +76 -0
  75. package/dist/session/init-phases.d.ts +1 -1
  76. package/dist/session/init-phases.d.ts.map +1 -1
  77. package/dist/session/init-phases.js +2 -12
  78. package/dist/session/init.d.ts +15 -1
  79. package/dist/session/init.d.ts.map +1 -1
  80. package/dist/session/init.js +82 -603
  81. package/dist/session/nimbus-session.d.ts +56 -22
  82. package/dist/session/nimbus-session.d.ts.map +1 -1
  83. package/dist/session/nimbus-session.js +130 -101
  84. package/dist/session/npm-install-port.d.ts +16 -0
  85. package/dist/session/npm-install-port.d.ts.map +1 -0
  86. package/dist/session/npm-install-port.js +37 -0
  87. package/dist/session/port-capability.d.ts +44 -0
  88. package/dist/session/port-capability.d.ts.map +1 -1
  89. package/dist/session/port-capability.js +110 -0
  90. package/dist/session/programmatic.d.ts +31 -5
  91. package/dist/session/programmatic.d.ts.map +1 -1
  92. package/dist/session/programmatic.js +94 -68
  93. package/dist/session/routes.d.ts +30 -17
  94. package/dist/session/routes.d.ts.map +1 -1
  95. package/dist/session/routes.js +42 -131
  96. package/dist/session/rpc.d.ts +15 -15
  97. package/dist/session/rpc.d.ts.map +1 -1
  98. package/dist/session/rpc.js +97 -47
  99. package/dist/session/serving-port.d.ts +27 -0
  100. package/dist/session/serving-port.d.ts.map +1 -0
  101. package/dist/session/serving-port.js +27 -0
  102. package/dist/session/start-real-vite.d.ts +12 -0
  103. package/dist/session/start-real-vite.d.ts.map +1 -1
  104. package/dist/session/start-real-vite.js +4 -4
  105. package/dist/session/supervisor-op.d.ts +2 -2
  106. package/dist/session/supervisor-op.d.ts.map +1 -1
  107. package/dist/session/supervisor-op.js +13 -7
  108. package/dist/session/supervisor-rpc.d.ts +7 -7
  109. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  110. package/dist/session/supervisor-rpc.js +4 -0
  111. package/dist/session/vite-command.d.ts +18 -0
  112. package/dist/session/vite-command.d.ts.map +1 -0
  113. package/dist/session/vite-command.js +609 -0
  114. package/dist/session/ws.d.ts +61 -0
  115. package/dist/session/ws.d.ts.map +1 -1
  116. package/dist/session/ws.js +122 -12
  117. package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
  118. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  119. package/dist/shell/npm-bin-entrypoints.js +12 -85
  120. package/package.json +24 -4
  121. package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
  122. package/public/index.html +1 -1
  123. package/scripts/bundle-facet-workers.mjs +1 -1
  124. package/scripts/bundle-npm-cjs.mjs +2 -2
  125. package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
  126. package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
  127. package/dist/facets/on-demand-bundle-gate.js +0 -125
  128. package/dist/npm/install-args.d.ts +0 -21
  129. package/dist/npm/install-args.d.ts.map +0 -1
  130. package/dist/npm/install-args.js +0 -70
  131. package/dist/npm/npm-log.d.ts +0 -31
  132. package/dist/npm/npm-log.d.ts.map +0 -1
  133. 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,22 +43,16 @@ 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';
62
57
  import { rpcExposeApp, rpcListApps, rpcRemoveApp, rpcRotateLink } from './programmatic.js';
63
58
  import { listInstalledRuntimes, rehydrateInstalledRuntimes, registerRunnerFactory, } from '@nimbus-sh/core/runtime/installed-runtimes.js';
@@ -66,27 +61,23 @@ import { listInstalledRuntimes, rehydrateInstalledRuntimes, registerRunnerFactor
66
61
  // Keeping them off the top-level import graph shaves their module-eval cost
67
62
  // (zod, embedded socket-kernel/shim sources, wasm loaders) out of the
68
63
  // one-time Worker Startup Time paid on every fresh-isolate cold run.
69
- 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';
70
65
  import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
71
66
  import { generation } from '@nimbus-sh/fabric/generation.js';
72
67
  import { stripAnsi } from '@nimbus-sh/core/runtime/process-logs.js';
73
68
  import { DEFAULT_MOUNT_POINTS, NODE_VERSION, } from '@nimbus-sh/core/constants.js';
74
- 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';
75
70
  import { recordRecoveryEvent } from '@nimbus-sh/platform/oom-discriminator.js';
76
71
  import { sessionAiEnv } from './ai.js';
77
72
  import { routeSessionLoopback } from './loopback.js';
78
73
  import { setPhase } from './init-phases.js';
74
+ import { shellTerminalTee } from './ws.js';
79
75
  import { VITE_CONFIG_KEY } from './keys.js';
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
- }
86
76
  function quoteShellArgument(value) {
87
77
  return `'${value.replace(/'/g, `'\\''`)}'`;
88
78
  }
89
- export async function initSession(self, ws) {
79
+ export async function initSession(self, ws, options = {}) {
80
+ const replayScrollback = options.resume !== 'wake';
90
81
  self.ensureSqliteFs();
91
82
  const kernelFs = self.sqliteFs.as(CRED_KERNEL);
92
83
  self.ensureFacetManager();
@@ -115,17 +106,7 @@ export async function initSession(self, ws) {
115
106
  // facet and the scrollback replay below is wire-phase work.
116
107
  // Phase B will tag in once we start building the kernel.
117
108
  setPhase(self, 'wire', 'init-session');
118
- self.terminal = new WebSocketTerminal(ws, (frame) => {
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
- });
109
+ self.terminal = new WebSocketTerminal(ws, shellTerminalTee(self));
129
110
  // [B'.3] Replay persisted scrollback BEFORE the cold-start UI gate.
130
111
  // On rehydrate (hasPersistedState=true) we emit the prior
131
112
  // session's terminal contents as a single batched write — the
@@ -139,16 +120,18 @@ export async function initSession(self, ws) {
139
120
  // same scrollback both times. The cap eviction keeps total bytes
140
121
  // bounded.
141
122
  if (persisted.hasPersistedState) {
142
- try {
143
- const replay = loadScrollback(self.ctx);
144
- if (replay.length > 0)
145
- self.terminal.write(replay);
146
- }
147
- catch (e) {
123
+ if (replayScrollback) {
148
124
  try {
149
- 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 { }
150
134
  }
151
- catch { }
152
135
  }
153
136
  // The scrollback stops mid-command when the previous instance was
154
137
  // reset under it — the platform kills the isolate without running a
@@ -263,8 +246,8 @@ export async function initSession(self, ws) {
263
246
  stdout: parent.isFdTerminal?.(1) ?? false,
264
247
  stderr: parent.isFdTerminal?.(2) ?? false,
265
248
  },
266
- onStdout: (data) => parent.stdout.write(data),
267
- onStderr: (data) => parent.stderr.write(data),
249
+ onStdout: textSink((data) => parent.stdout.write(data)),
250
+ onStderr: textSink((data) => parent.stderr.write(data)),
268
251
  commandContext: {
269
252
  pid: identity.pid,
270
253
  cred: identity.cred,
@@ -822,8 +805,8 @@ export async function initSession(self, ws) {
822
805
  const shellResult = await shell.execute(command, {
823
806
  cwd: ctx.cwd,
824
807
  env: ctx.env,
825
- onStdout: (d) => ctx.stdout.write(d),
826
- onStderr: (d) => ctx.stderr.write(d),
808
+ onStdout: textSink((d) => ctx.stdout.write(d)),
809
+ onStderr: textSink((d) => ctx.stderr.write(d)),
827
810
  });
828
811
  return shellResult.exitCode;
829
812
  }
@@ -1079,443 +1062,7 @@ export async function initSession(self, ws) {
1079
1062
  }
1080
1063
  });
1081
1064
  // ── 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
- });
1065
+ registry.register('vite', createViteCommand(self));
1519
1066
  // ── nimbus-wrangler / wrangler command: Worker dev server ─────────────
1520
1067
  //
1521
1068
  // `wrangler` is registered as a transparent alias for `nimbus-wrangler`
@@ -1751,8 +1298,8 @@ export async function initSession(self, ws) {
1751
1298
  const result = await shell.execute(cmd, {
1752
1299
  cwd: cmdCtx.cwd,
1753
1300
  env: cmdCtx.env,
1754
- onStdout: (d) => cmdCtx.stdout.write(d),
1755
- onStderr: (d) => cmdCtx.stderr.write(d),
1301
+ onStdout: textSink((d) => cmdCtx.stdout.write(d)),
1302
+ onStderr: textSink((d) => cmdCtx.stderr.write(d)),
1756
1303
  stdin,
1757
1304
  terminalStdin: cmdCtx.terminalStdin,
1758
1305
  // `npm run x` runs x on npm's own fds. Handing the nested execution a
@@ -1830,15 +1377,18 @@ export async function initSession(self, ws) {
1830
1377
  });
1831
1378
  // Wrap the caller-supplied streams so every chunk is both displayed
1832
1379
  // 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 { }
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
+ };
1842
1392
  };
1843
1393
  let exitCode = 1;
1844
1394
  try {
@@ -1881,7 +1431,15 @@ export async function initSession(self, ws) {
1881
1431
  catch (e) {
1882
1432
  // Surface the error in the terminal and the ring buffer.
1883
1433
  const msg = (e && (e.stack || e.message)) || String(e);
1884
- 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 { }
1885
1443
  exitCode = 1;
1886
1444
  }
1887
1445
  finally {
@@ -1936,8 +1494,11 @@ export async function initSession(self, ws) {
1936
1494
  catch { }
1937
1495
  },
1938
1496
  });
1939
- // Register core npm with enhanced `npm run <script>` support
1940
- 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
+ });
1941
1502
  registry.register('npm', async (ctx) => {
1942
1503
  const args = ctx.args || [];
1943
1504
  const sub = args[0];
@@ -2021,26 +1582,17 @@ export async function initSession(self, ws) {
2021
1582
  // (the only thing the module did was hold these strings) and
2022
1583
  // the file is deleted.
2023
1584
  //
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);
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);
2044
1596
  return 127;
2045
1597
  }
2046
1598
  const scriptTrim = script.trim();
@@ -2150,85 +1702,6 @@ export async function initSession(self, ws) {
2150
1702
  catch { }
2151
1703
  return 0;
2152
1704
  }
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
1705
  // ── npm create <pkg> / npm init <pkg> → npx create-<pkg> ─────────
2233
1706
  //
2234
1707
  // Per the npm spec, `npm create X args...` and `npm init X args...`
@@ -2647,17 +2120,17 @@ export async function initSession(self, ws) {
2647
2120
  catch { }
2648
2121
  // ── Starter-app hint (only if seed sentinel still exists) ──
2649
2122
  // We check the live VFS, not a static file, so that if the user
2650
- // deletes ~/.nimbus-seeded (or ~/app) the hint stops appearing on
2651
- // next login.
2123
+ // deletes ~/.nimbus-seeded (or the project dir) the hint stops
2124
+ // appearing on next login.
2652
2125
  try {
2653
2126
  if (hasSeededProject(self.sqliteFs) && kernelFs.exists(SEED_PROJECT_DIR)) {
2654
- self.terminal.write('\x1b[2mStarter app ready at \x1b[36m~/app\x1b[0m\x1b[2m — try:\x1b[0m\r\n' +
2655
- ' \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`);
2656
2129
  }
2657
2130
  }
2658
2131
  catch { }
2659
2132
  // ── W11: framework detection MOTD line ──
2660
- // If ~/app has a recognizable framework, print one informational line.
2133
+ // If the seeded project dir has a recognizable framework, print one informational line.
2661
2134
  // Purely advisory — does not change boot behaviour. Fire-and-forget
2662
2135
  // because initSession is sync; any failure is silently swallowed.
2663
2136
  void (async () => {
@@ -2738,13 +2211,19 @@ export async function initSession(self, ws) {
2738
2211
  // ── Start shell ──
2739
2212
  //
2740
2213
  // Now, and not inside the workspace, because the login files are the
2741
- // user's and may name any of the commands registered above. Not awaited:
2742
- // `shell.start()` runs synchronously and the rc files apply as they
2743
- // finish, which is what the terminal has always done. A user's broken
2744
- // rc file must not take the socket down with it.
2745
- workspace.start().catch((e) => {
2746
- 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));
2747
2224
  });
2225
+ if (options.resume === 'wake')
2226
+ await started;
2748
2227
  ws.send(JSON.stringify({ type: 'ready' }));
2749
2228
  }
2750
2229
  function installShellExecutionFeatures(shell, terminal) {