@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
@@ -0,0 +1,609 @@
1
+ /**
2
+ * session/vite-command.ts — the `vite` builtin command.
3
+ *
4
+ * Extracted from init.ts so the handler can be driven through the
5
+ * command registry in unit tests — the module's only session coupling
6
+ * is `self`, the same InitHost view initSession passes to every other
7
+ * registration.
8
+ *
9
+ * Subcommands: `vite` (dev server), `vite build`, `vite preview`,
10
+ * `vite stop`. Build honours build.outDir with Vite parity: output
11
+ * writes to the resolved path, only an outDir strictly inside the
12
+ * project root is emptied first, and the bundle is validated before
13
+ * old output is cleared.
14
+ */
15
+ import { normalizeVfsPath, parentVfsPath, resolveVfsPath, stripLeadingSlashes } from '@nimbus-sh/core/vfs/path.js';
16
+ import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
17
+ import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
18
+ import { parseViteConfigSource, viteBuildBlockingPlugins, unhandledVitePlugins } from '@nimbus-sh/core/runtime/vite-config-parser.js';
19
+ import { findHtmlScriptEntrypoint, rewriteViteBuildHtml } from '../runtime/html-entrypoint.js';
20
+ import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
21
+ import { ViteDevServer } from '../facets/vite-dev-server.js';
22
+ import { shouldUseRealVite } from '../facets/cirrus-real.js';
23
+ import { makeLongRunningPortStub, resolveLongRunningPort, expandArgvShellDefaults, } from '@nimbus-sh/core/runtime/long-running-handle.js';
24
+ import { checkNodeModulesGuard, withLoudTimeout, VITE_BUILD_TIMEOUT_MS, } from './helpers.js';
25
+ import { startRealVite } from './start-real-vite.js';
26
+ import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
27
+ import { VITE_CONFIG_KEY } from './keys.js';
28
+ import { registerServingPort } from './serving-port.js';
29
+ export function createViteCommand(self) {
30
+ return async (ctx) => {
31
+ const args = ctx.args || [];
32
+ const cwd = normalizeVfsPath(ctx.cwd || '/home/user');
33
+ if (args.includes('--help') || args.includes('-h')) {
34
+ ctx.stdout.write('Usage: vite [command] [options]\n\n');
35
+ ctx.stdout.write('Commands:\n');
36
+ ctx.stdout.write(' (default) Start dev server\n');
37
+ ctx.stdout.write(' build Build for production\n');
38
+ ctx.stdout.write(' preview Serve the built dist/\n');
39
+ ctx.stdout.write(' stop Stop dev server\n\n');
40
+ ctx.stdout.write('Options:\n');
41
+ ctx.stdout.write(' --root <dir> Project root\n');
42
+ ctx.stdout.write(' --port <n> Server port\n');
43
+ return 0;
44
+ }
45
+ self.ensureSqliteFs();
46
+ const kernelFs = self.sqliteFs.as(CRED_KERNEL);
47
+ const viteConfig = {};
48
+ for (const cfgName of ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']) {
49
+ const cfgPath = cwd + '/' + cfgName;
50
+ if (kernelFs.exists(cfgPath)) {
51
+ try {
52
+ let cfgCode = kernelFs.readFileString(cfgPath);
53
+ // Transform TS to JS
54
+ if (cfgName.endsWith('.ts')) {
55
+ if (!self.esbuildService)
56
+ self.esbuildService = new EsbuildService(kernelFs);
57
+ const t = await self.esbuildService.transform(cfgCode, { loader: 'ts', format: 'esm' });
58
+ cfgCode = t.code;
59
+ }
60
+ Object.assign(viteConfig, parseViteConfigSource(cfgCode));
61
+ }
62
+ catch (e) {
63
+ ctx.stderr.write(`Warning: could not parse ${cfgName}: ${e?.message}\n`);
64
+ }
65
+ break;
66
+ }
67
+ }
68
+ // ── vite build ──
69
+ if (args[0] === 'build') {
70
+ // Capability gate: the built-in builder is esbuild underneath and
71
+ // never evaluates vite.config `plugins`. Only framework plugins
72
+ // (SvelteKit, Vue, Solid) mean the project is not a plain-Vite
73
+ // app — they would die deep in esbuild on an entry point that
74
+ // does not exist or framework syntax the JS loader cannot parse,
75
+ // so say what is actually wrong. Every other plugin is a
76
+ // warning, not a refusal.
77
+ const blockingPlugins = viteBuildBlockingPlugins(viteConfig);
78
+ if (blockingPlugins.length) {
79
+ ctx.stderr.write('\x1b[31m✘\x1b[0m vite build: this project needs Vite plugins the built-in build server cannot run' +
80
+ ' (' + blockingPlugins.join(', ') + ').\n' +
81
+ ' The built-in Vite server supports plain Vite projects (React, JSX/TS, CSS, and asset imports);\n' +
82
+ ' framework projects like SvelteKit, Vue, Solid, or Astro require a real Vite —\n' +
83
+ ' Nimbus does not run one for `vite build` yet.\n');
84
+ return 1;
85
+ }
86
+ const buildSkippedPlugins = unhandledVitePlugins(viteConfig);
87
+ if (buildSkippedPlugins.length) {
88
+ ctx.stderr.write('\x1b[33m!\x1b[0m vite build: skipping plugins the built-in build cannot run' +
89
+ ' (' + buildSkippedPlugins.join(', ') + '); output is the plain-Vite bundle.\n');
90
+ }
91
+ if (!self.esbuildService)
92
+ self.esbuildService = new EsbuildService(kernelFs);
93
+ const htmlPath = cwd + '/index.html';
94
+ let entryPoint = cwd + '/src/main.tsx';
95
+ let origHtml = '';
96
+ try {
97
+ origHtml = kernelFs.readFileString(htmlPath);
98
+ const htmlEntrypoint = await findHtmlScriptEntrypoint(origHtml);
99
+ if (htmlEntrypoint)
100
+ entryPoint = cwd + '/' + stripLeadingSlashes(htmlEntrypoint);
101
+ }
102
+ catch {
103
+ ctx.stderr.write('Warning: no index.html\n');
104
+ }
105
+ if (!kernelFs.exists(entryPoint)) {
106
+ const alts = [cwd + '/src/main.tsx', cwd + '/src/main.ts', cwd + '/src/index.tsx', cwd + '/src/index.ts'];
107
+ entryPoint = alts.find(p => kernelFs.exists(p)) || entryPoint;
108
+ }
109
+ if (!kernelFs.exists(entryPoint)) {
110
+ ctx.stderr.write('\x1b[31m✘\x1b[0m vite build: no entry point — index.html declares no <script src> and none of\n' +
111
+ ' src/main.{tsx,ts} or src/index.{tsx,ts} exists. The built-in build server handles plain\n' +
112
+ ' Vite apps; projects with other layouts need a real Vite.\n');
113
+ return 1;
114
+ }
115
+ ctx.stdout.write('Building for production...\n');
116
+ ctx.stdout.write(' Entry: ' + entryPoint + '\n');
117
+ const t0 = Date.now();
118
+ try {
119
+ // Vite parity: build writes to the resolved outDir wherever it
120
+ // lands inside the VFS — the common monorepo layout builds the
121
+ // frontend into ../server/public. Only the empty step is gated:
122
+ // an outDir not strictly inside the project root is never
123
+ // emptied (outDir == root is not inside), and Vite's warning is
124
+ // printed verbatim.
125
+ const outDir = viteConfig.outDir || 'dist';
126
+ const resolvedOutDir = resolveVfsPath(outDir, cwd);
127
+ const insideRoot = resolvedOutDir.length > cwd.length && resolvedOutDir.startsWith(cwd + '/');
128
+ const distDir = resolvedOutDir;
129
+ if (!insideRoot) {
130
+ ctx.stderr.write(`\x1b[33m(!)\x1b[0m outDir ${resolvedOutDir} is not inside project root and will not be emptied.\n`);
131
+ }
132
+ const publicDir = cwd + '/public';
133
+ const hasPublic = kernelFs.exists(publicDir) && kernelFs.isDirectory(publicDir);
134
+ // Detect which packages are installed vs need CDN
135
+ const nmDir = cwd + '/node_modules';
136
+ const externals = [];
137
+ const cdnPackages = [];
138
+ for (const pkg of ['react', 'react-dom', 'react/jsx-runtime', 'react-dom/client']) {
139
+ const pkgBase = pkg.split('/')[0];
140
+ if (!kernelFs.exists(nmDir + '/' + pkgBase)) {
141
+ externals.push(pkg);
142
+ if (!cdnPackages.includes(pkgBase))
143
+ cdnPackages.push(pkgBase);
144
+ }
145
+ }
146
+ if (viteConfig.alias)
147
+ externals.push(...Object.keys(viteConfig.alias));
148
+ // vite-equivalent output layout: dist/assets/<name>-<hash>.<ext>
149
+ // for both the entry and every emitted asset — entryNames +
150
+ // assetNames is what makes esbuild's `file` loader land there.
151
+ // Bounded (G5): an esbuild stall must surface as a loud timeout
152
+ // naming the entry, never a silent hang. The existing catch below
153
+ // renders the timeout as `Build error: …`.
154
+ ctx.stdout.write(' Bundling ' + entryPoint + ' …\n');
155
+ const result = await withLoudTimeout(self.esbuildService.build([entryPoint], {
156
+ bundle: true, format: 'esm', target: 'es2020', platform: 'browser',
157
+ minify: true, outdir: distDir,
158
+ entryNames: 'assets/[name]-[hash]',
159
+ chunkNames: 'assets/[name]-[hash]',
160
+ assetNames: 'assets/[name]-[hash]',
161
+ external: externals.length > 0 ? externals : undefined,
162
+ viteAssets: true,
163
+ vitePublicDir: hasPublic ? publicDir : undefined,
164
+ }), VITE_BUILD_TIMEOUT_MS, `vite build of ${entryPoint}`);
165
+ if (result.errors?.length) {
166
+ for (const e of result.errors)
167
+ ctx.stderr.write(' error: ' + e.text + '\n');
168
+ return 1;
169
+ }
170
+ // The entry output must exist before old output is cleared —
171
+ // a failed build must never wipe the previously-good dist/.
172
+ const entryOutputRel = Object.entries(result.metafile?.outputs || {})
173
+ .find(([, o]) => o.entryPoint)?.[0]?.replace(/^\/+/, '');
174
+ const entryJs = (entryOutputRel
175
+ ? result.outputFiles.find((f) => f.path.replace(/^\/+/, '') === entryOutputRel)
176
+ : undefined) ?? result.outputFiles.find((f) => f.path.endsWith('.js'));
177
+ if (!entryJs) {
178
+ ctx.stderr.write('Build error: bundler produced no JS output\n');
179
+ return 1;
180
+ }
181
+ const jsFilename = entryJs.path.slice(entryJs.path.lastIndexOf('/') + 1);
182
+ // Vite's emptyOutDir: stale hashed outputs must not accumulate.
183
+ // Only an outDir strictly inside the project root is emptied —
184
+ // the warning above covered the rest.
185
+ if (insideRoot && kernelFs.exists(distDir)) {
186
+ kernelFs.removeRecursive(distDir);
187
+ }
188
+ kernelFs.mkdir(distDir, { recursive: true });
189
+ // CSS bundled through the entry imports (and its url() assets)
190
+ // arrives as the entry's cssBundle sidecar — esbuild already
191
+ // rewrote every url() to the hashed emitted path.
192
+ const cssBundlePath = entryOutputRel
193
+ ? result.metafile?.outputs?.[entryOutputRel]?.cssBundle
194
+ : undefined;
195
+ let cssFilename = cssBundlePath
196
+ ? cssBundlePath.slice(cssBundlePath.lastIndexOf('/') + 1)
197
+ : result.outputFiles
198
+ .find((f) => f.path.endsWith('.css'))
199
+ ?.path.split('/').pop();
200
+ // Fallback for HTML-linked stylesheets the bundle never saw
201
+ // (`<link href="src/site.css">` in index.html): concatenate what
202
+ // src/ declares, like the pre-asset-pipeline path did.
203
+ let fallbackCss = '';
204
+ if (!cssFilename) {
205
+ const collectCss = (dir) => {
206
+ try {
207
+ for (const e of kernelFs.readdir(dir)) {
208
+ const fp = dir + '/' + e.name;
209
+ if (e.type === 'directory')
210
+ collectCss(fp);
211
+ else if (e.name.endsWith('.css')) {
212
+ try {
213
+ fallbackCss += kernelFs.readFileString(fp) + '\n';
214
+ }
215
+ catch { }
216
+ }
217
+ }
218
+ }
219
+ catch { }
220
+ };
221
+ collectCss(cwd + '/src');
222
+ if (fallbackCss.trim()) {
223
+ const cssHashNum = fallbackCss.split('').reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0);
224
+ cssFilename = 'index-' + (cssHashNum >>> 0).toString(36).padStart(6, '0') + '.css';
225
+ }
226
+ }
227
+ // Write every emitted output (bytes, not text — `file` assets are
228
+ // binary), printing vite-style size lines.
229
+ for (const f of result.outputFiles) {
230
+ const outPath = normalizeVfsPath(f.path);
231
+ const parent = parentVfsPath(outPath);
232
+ if (parent && !kernelFs.exists(parent))
233
+ kernelFs.mkdir(parent, { recursive: true });
234
+ kernelFs.writeFile(outPath, f.bytes);
235
+ const rel = outPath.slice(cwd.length + 1);
236
+ ctx.stdout.write(' \x1b[2m' + rel + '\x1b[0m ' + (f.bytes.length / 1024).toFixed(2) + ' kB\n');
237
+ }
238
+ if (fallbackCss.trim() && cssFilename) {
239
+ const cssPath = distDir + '/assets/' + cssFilename;
240
+ kernelFs.mkdir(distDir + '/assets', { recursive: true });
241
+ kernelFs.writeFile(cssPath, fallbackCss);
242
+ ctx.stdout.write(' \x1b[2m' + outDir + '/assets/' + cssFilename + '\x1b[0m ' + (fallbackCss.length / 1024).toFixed(2) + ' kB\n');
243
+ }
244
+ // public/ copies verbatim to dist/ (same `vite build` semantics —
245
+ // the favicon the template references lives there).
246
+ if (hasPublic) {
247
+ const copyTree = (src, dst) => {
248
+ for (const e of kernelFs.readdir(src)) {
249
+ const s = src + '/' + e.name;
250
+ const d = dst + '/' + e.name;
251
+ if (e.type === 'directory') {
252
+ copyTree(s, d);
253
+ }
254
+ else {
255
+ const parent = parentVfsPath(d);
256
+ if (parent && !kernelFs.exists(parent))
257
+ kernelFs.mkdir(parent, { recursive: true });
258
+ kernelFs.writeFile(d, kernelFs.readFile(s));
259
+ }
260
+ }
261
+ };
262
+ copyTree(publicDir, distDir);
263
+ }
264
+ // Generate dist/index.html
265
+ if (origHtml) {
266
+ const distHtml = await rewriteViteBuildHtml(origHtml, {
267
+ jsFilename,
268
+ cssFilename,
269
+ removeImportMap: cdnPackages.length === 0,
270
+ injectCss: true,
271
+ });
272
+ kernelFs.writeFile(distDir + '/index.html', distHtml);
273
+ ctx.stdout.write(' \x1b[2m' + outDir + '/index.html\x1b[0m ' + (distHtml.length / 1024).toFixed(2) + ' kB\n');
274
+ if (cdnPackages.length > 0) {
275
+ ctx.stdout.write(' \x1b[33mNote: ' + cdnPackages.join(', ') + ' loaded from CDN (not bundled)\x1b[0m\n');
276
+ }
277
+ }
278
+ ctx.stdout.write('\n\x1b[32m✓ built in ' + ((Date.now() - t0) / 1000).toFixed(2) + 's\x1b[0m\n');
279
+ return 0;
280
+ }
281
+ catch (e) {
282
+ ctx.stderr.write('Build error: ' + errorText(e) + '\n');
283
+ return 1;
284
+ }
285
+ }
286
+ // ── vite preview ──
287
+ if (args[0] === 'preview') {
288
+ ctx.stdout.write('Serving dist/ — open ' + self.viteBasePath + '/\n');
289
+ // Vite parity: preview serves the resolved outDir wherever it
290
+ // landed — build wrote there, so preview must read there.
291
+ const distRoot = resolveVfsPath(viteConfig.outDir || 'dist', cwd);
292
+ if (!kernelFs.exists(distRoot)) {
293
+ ctx.stderr.write('dist/ not found. Run vite build first.\n');
294
+ return 1;
295
+ }
296
+ // Start vite on the dist directory
297
+ if (!self.esbuildService)
298
+ self.esbuildService = new EsbuildService(kernelFs);
299
+ if (self.viteDevServer?.isRunning)
300
+ self.viteDevServer.stop();
301
+ const previewBasePath = self.viteBasePath;
302
+ // process metadata support: same long-running treatment as the
303
+ // dev path, just on the dist/ directory.
304
+ const previewPort = viteConfig.port || 4173; // vite preview default
305
+ const previewProcEntry = self.processes.spawn('vite preview (' + distRoot + ')', ['vite', ...args], distRoot, { longRunning: true });
306
+ self.viteDevServer = new ViteDevServer({
307
+ vfs: self.sqliteFs, esbuild: self.esbuildService, root: distRoot,
308
+ onHmrMessage: () => { },
309
+ sql: self.ctx.storage.sql,
310
+ basePath: previewBasePath,
311
+ env: self.env,
312
+ ctx: self.ctx,
313
+ bundlePool: self.ensureBundlePool(),
314
+ port: previewPort,
315
+ pid: previewProcEntry.pid,
316
+ processes: self.processes,
317
+ });
318
+ self.viteDevServer.start();
319
+ try {
320
+ const previewStub = makeLongRunningPortStub(self.viteDevServer);
321
+ self.portRegistry.bindFacetStub(previewProcEntry.pid, previewStub);
322
+ await registerServingPort(self, previewProcEntry.pid, previewPort);
323
+ self._viteShimPid = previewProcEntry.pid;
324
+ self._viteShimPort = previewPort;
325
+ }
326
+ catch { }
327
+ try {
328
+ await self.ctx.storage.put(VITE_CONFIG_KEY, {
329
+ root: distRoot, basePath: previewBasePath, port: previewPort,
330
+ identity: { cwd: previewProcEntry.cwd, argv: previewProcEntry.argv },
331
+ });
332
+ }
333
+ catch { }
334
+ ctx.stdout.write('Serving at ' + previewBasePath + '/ \x1b[2m(pid=' + previewProcEntry.pid + ', port=' + previewPort + ')\x1b[0m\n');
335
+ return 0;
336
+ }
337
+ // ── vite stop ──
338
+ if (args[0] === 'stop') {
339
+ let stopped = false;
340
+ if (self.cirrusReal?.isRunning) {
341
+ self.cirrusReal.stop(self.ctx);
342
+ self.cirrusReal = null;
343
+ stopped = true;
344
+ }
345
+ if (self.viteDevServer?.isRunning) {
346
+ self.viteDevServer.stop();
347
+ self.viteDevServer = null;
348
+ try {
349
+ await self.ctx.storage.delete(VITE_CONFIG_KEY);
350
+ }
351
+ catch { }
352
+ stopped = true;
353
+ }
354
+ // Primitive #3 teardown — symmetric with the start path. Always
355
+ // safe to call: unregisterByPid is idempotent, exit() guards
356
+ // against re-marking already-terminal entries.
357
+ if (self._viteShimPid != null) {
358
+ try {
359
+ self.portRegistry.unregisterByPid(self._viteShimPid);
360
+ }
361
+ catch { }
362
+ try {
363
+ self.processes.exit(self._viteShimPid, 0);
364
+ }
365
+ catch { }
366
+ notifyTerminalEvent(self.terminal, {
367
+ type: 'exit', pid: self._viteShimPid, code: 0, command: 'vite',
368
+ });
369
+ self._viteShimPid = null;
370
+ self._viteShimPort = null;
371
+ }
372
+ if (stopped) {
373
+ ctx.stdout.write('\x1b[33mDev server stopped.\x1b[0m\n');
374
+ }
375
+ else {
376
+ ctx.stdout.write('No dev server running.\n');
377
+ }
378
+ return 0;
379
+ }
380
+ // ── vite (default: dev server) ──
381
+ let vfsRoot = cwd;
382
+ for (let i = 0; i < args.length; i++) {
383
+ if (args[i] === '--root' && args[i + 1])
384
+ vfsRoot = resolveVfsPath(args[i + 1], cwd);
385
+ }
386
+ if (viteConfig.root && viteConfig.root !== '.') {
387
+ vfsRoot = resolveVfsPath(viteConfig.root, cwd);
388
+ }
389
+ vfsRoot = normalizeVfsPath(vfsRoot);
390
+ // Argv expansion: package.json scripts commonly write
391
+ // `--port ${PORT:-3000}`. Resolve it once and feed both Vite
392
+ // backends so `/api/stats`, `/preview/`, and port tabs agree.
393
+ const expandedArgs = expandArgvShellDefaults(args, ctx.env || {});
394
+ const vitePortDefault = 5173;
395
+ const resolvedPort = resolveLongRunningPort({
396
+ argv: expandedArgs,
397
+ env: ctx.env,
398
+ configPort: viteConfig.port,
399
+ fallback: vitePortDefault,
400
+ });
401
+ // ── Preflight: node_modules guard ────────────────────────────────────
402
+ // Direct `vite` invocation requires installed deps. Bail loudly BEFORE
403
+ // spawning a dev server that would just serve broken modules and
404
+ // confuse the user. --force / --no-install-check bypasses the check.
405
+ const bypassInstallCheck = expandedArgs.includes('--force') || expandedArgs.includes('--no-install-check');
406
+ if (!bypassInstallCheck) {
407
+ const guard = checkNodeModulesGuard(kernelFs, vfsRoot);
408
+ if (guard.missing) {
409
+ ctx.stderr.write('\x1b[31m\u2718\x1b[0m \x1b[1mnode_modules/ not found\x1b[0m' +
410
+ (guard.depCount > 0 ? ` (${guard.depCount} dependencies declared)` : '') + '\n' +
411
+ ' Run \x1b[36mnpm install\x1b[0m in ' + vfsRoot + ' first,\n' +
412
+ ' or re-run with \x1b[36m--force\x1b[0m to skip this check.\n');
413
+ return 1;
414
+ }
415
+ }
416
+ if (self.viteDevServer?.isRunning)
417
+ self.viteDevServer.stop();
418
+ // ── Real-vite mode (Phase 0 spike, opt-in) ─────────────────────────
419
+ // NIMBUS_REAL_VITE=1 or `nimbusDevServer: 'real'` in vite.config.ts
420
+ // routes the session through a dynamic-worker facet running the
421
+ // real `vite` npm package. The in-process Cirrus shim is bypassed.
422
+ //
423
+ // This is EXPERIMENTAL and gated behind an explicit opt-in. Any
424
+ // error here falls back to Cirrus by the user re-running without
425
+ // the env flag — we do not silently fall back (fidelity over
426
+ // magic).
427
+ const sessionEnv = (ctx && ctx.env) || {};
428
+ const useReal = shouldUseRealVite({ env: sessionEnv, viteConfig });
429
+ // Long-running handoff (bin-spawn contract): when invoked from a
430
+ // wrapper that already allocated a pid (`npm run dev` via
431
+ // shellExecuteTracked, the npm-bin resolver, the SDK's startProcess),
432
+ // ADOPT that pid instead of spawning a second one. One pid, one start
433
+ // banner, no false exit — and one identity: the process table's
434
+ // cwd+argv for that pid is what the app verbs derive the dev server's
435
+ // owner from, so it is persisted with the config and given back to
436
+ // the entry a hibernation restore allocates.
437
+ const binSpawn = ctx.__nimbusBinSpawn;
438
+ const adoptedEntry = binSpawn?.skipSpawn && binSpawn.callerPid != null
439
+ ? self.processes.get(binSpawn.callerPid)
440
+ : undefined;
441
+ const handedOff = adoptedEntry != null;
442
+ const identity = adoptedEntry
443
+ ? { cwd: adoptedEntry.cwd, argv: adoptedEntry.argv }
444
+ : { cwd: vfsRoot, argv: expandedArgs };
445
+ if (useReal) {
446
+ const vitePort = resolvedPort;
447
+ const previewBasePath = self.viteBasePath;
448
+ // One boot path, shared with hibernation-restore (start-real-vite.ts):
449
+ // pre-bundles the user's vite.config, boots the facet, registers the
450
+ // port, and persists the vite-config so a woken session rebuilds the
451
+ // same real-vite server. Only the banner below is command-specific.
452
+ const { cirrusReal, userConfigBundle, cfgPath } = await startRealVite(self, {
453
+ root: vfsRoot,
454
+ port: vitePort,
455
+ basePath: previewBasePath,
456
+ configDir: cwd,
457
+ identity,
458
+ signal: ctx.signal,
459
+ onConfigError: (msg) => {
460
+ ctx.stderr.write('\x1b[33m!\x1b[0m vite.config bundling failed: ' + msg + '\n');
461
+ ctx.stderr.write(' Real-vite will run with default config.\n');
462
+ },
463
+ });
464
+ {
465
+ // ── Boot banner (§4.3 of PHASE2-REAL-VITE-PLAN.md) ──────
466
+ const snap = cirrusReal.stats.snapshot;
467
+ ctx.stdout.write('\n\x1b[1;36m Nimbus: real-vite mode\x1b[0m \x1b[2m(experimental, Phase 1-4)\x1b[0m\n\n');
468
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Preview: \x1b[36m' + previewBasePath + '/\x1b[0m\n');
469
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Vite: ' + cirrusReal.stats.viteVersion + ' (bundled)\n');
470
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Root: ' + vfsRoot + '\n');
471
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Port: ' + vitePort + ' \x1b[2m(virtual routing key)\x1b[0m\n');
472
+ if (snap) {
473
+ const kb = (snap.totalBytes / 1024).toFixed(1);
474
+ const pkgJson = snap.packageJsonCount;
475
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Snapshot: ' + snap.fileCount + ' files / ' +
476
+ kb + ' KB ' +
477
+ (pkgJson ? '\x1b[2m(incl. ' + pkgJson + ' package.json, rest lazy)\x1b[0m' : '') + '\n');
478
+ }
479
+ if (userConfigBundle) {
480
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Config: ' + cfgPath + ' \x1b[2m(' +
481
+ (userConfigBundle.length / 1024).toFixed(0) + ' KB bundled)\x1b[0m\n');
482
+ }
483
+ ctx.stdout.write('\n \x1b[2mWorks:\x1b[0m @vitejs/plugin-react, JSX/TSX transforms, SPA fallback, HMR.\n');
484
+ ctx.stdout.write(' \x1b[2mPartial:\x1b[0m other plugins (Babel-family generally OK; SWC/Rolldown blocked).\n');
485
+ ctx.stdout.write(' \x1b[2mBlocked:\x1b[0m vite build (rolldown needs node:wasi). Use cirrus for build.\n');
486
+ 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');
487
+ return 0;
488
+ }
489
+ }
490
+ // The built-in dev server evaluates no vite.config `plugins`, but it
491
+ // never refuses on them: plain-Vite apps routinely declare plugins
492
+ // the shim covers (react, cloudflare, tailwind) or doesn't need
493
+ // (watch/reload hooks). One warning line for the plugins it can't
494
+ // run — frameworks land there too: their pages won't render, but the
495
+ // server itself still serves, which is the pre-diagnostic behavior.
496
+ const devSkipped = [
497
+ ...viteBuildBlockingPlugins(viteConfig),
498
+ ...unhandledVitePlugins(viteConfig),
499
+ ];
500
+ if (devSkipped.length) {
501
+ ctx.stderr.write('\x1b[33m!\x1b[0m vite: plugins are not evaluated by the built-in dev server' +
502
+ ' (' + devSkipped.join(', ') + '); serving the plain-Vite app.\n');
503
+ }
504
+ if (!self.esbuildService)
505
+ self.esbuildService = new EsbuildService(kernelFs);
506
+ const previewBasePath = self.viteBasePath;
507
+ const viteDefine = viteConfig.define;
508
+ // Vite dev servers are represented as long-running process-table
509
+ // entries and port-registry handlers, so `ps`, logs, preview routing,
510
+ // `vite stop`, and `kill <pid>` share the same lifecycle primitives.
511
+ // Allocate PID FIRST so we can plumb it into ViteDevServer's
512
+ // process-log wiring at construction time. The PID stays valid
513
+ // for the life of this dev-server instance; subsequent log lines
514
+ // emitted by ViteDevServer flow into the pid's stderr ring,
515
+ // visible in the Process tab.
516
+ //
517
+ // The adopted wrapper pid stays `running` in /api/processes with this
518
+ // port; a fresh spawn carries the identity inputs computed above.
519
+ const viteProcEntry = adoptedEntry ?? self.processes.spawn('vite (' + vfsRoot + ')', identity.argv, identity.cwd, { longRunning: true });
520
+ if (handedOff)
521
+ self.processes.setLongRunning(viteProcEntry.pid);
522
+ self.viteDevServer = new ViteDevServer({
523
+ vfs: self.sqliteFs,
524
+ esbuild: self.esbuildService,
525
+ root: vfsRoot,
526
+ port: resolvedPort,
527
+ aliases: viteConfig.alias,
528
+ define: viteDefine,
529
+ onHmrMessage: (msg) => {
530
+ if (self.terminal)
531
+ try {
532
+ self.terminal.ws.send(JSON.stringify({ type: 'hmr', data: msg }));
533
+ }
534
+ catch { }
535
+ },
536
+ sql: self.ctx.storage.sql,
537
+ injectBasename: viteConfig.injectBasename,
538
+ basePath: previewBasePath,
539
+ env: self.env,
540
+ ctx: self.ctx,
541
+ // The shared bundle pool puts cold /@modules/ misses on the facet
542
+ // path. This site never passed it before, so `vite` served every
543
+ // cold miss through in-supervisor esbuild-wasm.
544
+ bundlePool: self.ensureBundlePool(),
545
+ // Process diagnostics support: wire dev-server diagnostics into the
546
+ // supervisor's per-PID log store so the Process tab is not silent
547
+ // after the banner.
548
+ pid: viteProcEntry.pid,
549
+ processes: self.processes,
550
+ });
551
+ self.viteDevServer.start();
552
+ try {
553
+ await self.ctx.storage.put(VITE_CONFIG_KEY, {
554
+ root: vfsRoot, aliases: viteConfig.alias, define: viteDefine,
555
+ injectBasename: viteConfig.injectBasename, basePath: previewBasePath,
556
+ port: resolvedPort,
557
+ identity: { cwd: viteProcEntry.cwd, argv: viteProcEntry.argv },
558
+ });
559
+ }
560
+ catch { }
561
+ // Register the port and build the long-running stub. The stub
562
+ // forwards into the in-process viteDevServer through the generic
563
+ // long-running adapter — same hook every future long-running
564
+ // facet uses (Express, Bun.serve, http.createServer().listen()).
565
+ const viteStub = makeLongRunningPortStub(self.viteDevServer);
566
+ self.portRegistry.bindFacetStub(viteProcEntry.pid, viteStub);
567
+ await registerServingPort(self, viteProcEntry.pid, resolvedPort);
568
+ // Track the wiring so `vite stop` and crash-handlers can tear it
569
+ // down without searching the registry.
570
+ self._viteShimPid = viteProcEntry.pid;
571
+ self._viteShimPort = resolvedPort;
572
+ // Spawn / long-running event for the Process tab UI. Mirrors the
573
+ // shellExecuteTracked banner so the user sees the same shape no
574
+ // matter how vite was invoked. Suppressed on handoff — the wrapper
575
+ // already emitted the single start banner and spawn event for this pid.
576
+ if (!handedOff) {
577
+ if (self.terminal) {
578
+ self.terminal.write(`\x1b[2m[shell started (long-running): pid=${viteProcEntry.pid} cmd="vite ${expandedArgs.join(' ')}"]\x1b[0m\r\n`);
579
+ }
580
+ notifyTerminalEvent(self.terminal, {
581
+ type: 'spawn',
582
+ pid: viteProcEntry.pid,
583
+ command: 'vite ' + expandedArgs.join(' '),
584
+ longRunning: true,
585
+ attachedTty: false,
586
+ });
587
+ }
588
+ // Banner — reports the resolved port and PID so the user can
589
+ // verify the multi-target routing.
590
+ ctx.stdout.write('\n\x1b[1;36m Nimbus Vite Dev Server\x1b[0m\n\n');
591
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Preview: \x1b[36m' + previewBasePath + '/\x1b[0m');
592
+ if (resolvedPort !== vitePortDefault) {
593
+ ctx.stdout.write(' \x1b[2m(also: ' + previewBasePath + '/?port=' + resolvedPort + ')\x1b[0m');
594
+ }
595
+ ctx.stdout.write('\n');
596
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Root: ' + vfsRoot + '\n');
597
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Port: ' + resolvedPort + ' \x1b[2m(pid=' + viteProcEntry.pid + ')\x1b[0m\n');
598
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Transforms: .ts .tsx .jsx (React JSX automatic)\n');
599
+ if (viteConfig.alias)
600
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Aliases: ' + Object.keys(viteConfig.alias).join(', ') + '\n');
601
+ if (viteDefine)
602
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Define: ' + Object.keys(viteDefine).join(', ') + '\n');
603
+ const twCfg = [vfsRoot + '/tailwind.config.js', vfsRoot + '/tailwind.config.ts'].find(p => kernelFs.exists(p));
604
+ if (twCfg)
605
+ ctx.stdout.write(' \x1b[32m\u279C\x1b[0m Tailwind: edge-vendored Play CDN \x1b[2m(detected)\x1b[0m\n');
606
+ ctx.stdout.write('\n \x1b[2mRun \x1b[0mvite stop\x1b[2m, or \x1b[0mkill ' + viteProcEntry.pid + '\x1b[2m, to stop.\x1b[0m\n\n');
607
+ return 0;
608
+ };
609
+ }