sunpeak 0.16.29 → 0.17.1

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 (140) hide show
  1. package/bin/commands/dev.mjs +169 -342
  2. package/bin/commands/inspect.mjs +763 -0
  3. package/bin/commands/new.mjs +2 -2
  4. package/bin/lib/inspect/inspect-config.d.mts +20 -0
  5. package/bin/lib/inspect/inspect-config.mjs +76 -0
  6. package/bin/lib/live/global-setup.mjs +6 -1
  7. package/bin/sunpeak.js +11 -1
  8. package/dist/chatgpt/globals.css +8 -0
  9. package/dist/chatgpt/index.cjs +3 -11
  10. package/dist/chatgpt/index.cjs.map +1 -1
  11. package/dist/chatgpt/index.d.ts +2 -2
  12. package/dist/chatgpt/index.js +4 -8
  13. package/dist/chatgpt/index.js.map +1 -1
  14. package/dist/claude/index.cjs +1 -1
  15. package/dist/claude/index.js +1 -1
  16. package/dist/discovery-Cgoegt62.js +114 -0
  17. package/dist/discovery-Cgoegt62.js.map +1 -0
  18. package/dist/discovery-Clu4uHp1.cjs +161 -0
  19. package/dist/discovery-Clu4uHp1.cjs.map +1 -0
  20. package/dist/index.cjs +1 -4
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.js +2 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/discovery-cli.cjs +1 -1
  25. package/dist/lib/discovery-cli.js +1 -1
  26. package/dist/lib/discovery.d.ts +7 -67
  27. package/dist/lib/index.d.ts +0 -1
  28. package/dist/mcp/index.cjs +34 -23
  29. package/dist/mcp/index.cjs.map +1 -1
  30. package/dist/mcp/index.js +34 -23
  31. package/dist/mcp/index.js.map +1 -1
  32. package/dist/mcp/types.d.ts +5 -0
  33. package/dist/simulator/index.cjs +5 -11
  34. package/dist/simulator/index.cjs.map +1 -1
  35. package/dist/simulator/index.d.ts +4 -2
  36. package/dist/simulator/index.js +5 -8
  37. package/dist/simulator/index.js.map +1 -1
  38. package/dist/simulator/simple-sidebar.d.ts +7 -4
  39. package/dist/simulator/simulator-url.d.ts +8 -0
  40. package/dist/simulator/simulator.d.ts +15 -2
  41. package/dist/simulator/use-mcp-connection.d.ts +19 -0
  42. package/dist/{simulator-DIVvI69i.cjs → simulator-CH9hs0N6.cjs} +129 -21
  43. package/dist/simulator-CH9hs0N6.cjs.map +1 -0
  44. package/dist/{simulator-C7mkK7Sz.js → simulator-Dl8B-Ljb.js} +124 -22
  45. package/dist/simulator-Dl8B-Ljb.js.map +1 -0
  46. package/dist/{simulator-url-BDGD4vZD.cjs → simulator-url-CozKF1jf.cjs} +3 -1
  47. package/dist/simulator-url-CozKF1jf.cjs.map +1 -0
  48. package/dist/{simulator-url-Bkxj43yT.js → simulator-url-KoS_ToP6.js} +3 -1
  49. package/dist/simulator-url-KoS_ToP6.js.map +1 -0
  50. package/dist/style.css +8 -0
  51. package/package.json +9 -1
  52. package/template/dist/albums/albums.html +105 -0
  53. package/template/dist/albums/albums.json +16 -0
  54. package/template/dist/carousel/carousel.html +105 -0
  55. package/template/dist/carousel/carousel.json +16 -0
  56. package/template/dist/map/map.html +3060 -0
  57. package/template/dist/map/map.json +22 -0
  58. package/template/dist/review/review.html +105 -0
  59. package/template/dist/review/review.json +16 -0
  60. package/template/dist/server.js +15 -0
  61. package/template/dist/tools/review-diff.js +50 -0
  62. package/template/dist/tools/review-post.js +50 -0
  63. package/template/dist/tools/review-purchase.js +61 -0
  64. package/template/dist/tools/review.js +31 -0
  65. package/template/dist/tools/show-albums.js +56 -0
  66. package/template/dist/tools/show-carousel.js +41 -0
  67. package/template/dist/tools/show-map.js +47 -0
  68. package/template/node_modules/.vite/deps/_metadata.json +8 -0
  69. package/template/node_modules/.vite/deps/package.json +3 -0
  70. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +500 -0
  71. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -0
  72. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +563 -0
  73. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -0
  74. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +575 -0
  75. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -0
  76. package/template/node_modules/.vite-mcp/deps/@testing-library_react.js +11363 -0
  77. package/template/node_modules/.vite-mcp/deps/@testing-library_react.js.map +1 -0
  78. package/template/node_modules/.vite-mcp/deps/_metadata.json +130 -0
  79. package/template/node_modules/.vite-mcp/deps/chunk-BoAXSpZd.js +33 -0
  80. package/template/node_modules/.vite-mcp/deps/client-CU1wWud4.js +14385 -0
  81. package/template/node_modules/.vite-mcp/deps/client-CU1wWud4.js.map +1 -0
  82. package/template/node_modules/.vite-mcp/deps/clsx.js +18 -0
  83. package/template/node_modules/.vite-mcp/deps/clsx.js.map +1 -0
  84. package/template/node_modules/.vite-mcp/deps/dist-uWX8WbjY.js +505 -0
  85. package/template/node_modules/.vite-mcp/deps/dist-uWX8WbjY.js.map +1 -0
  86. package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js +1461 -0
  87. package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js.map +1 -0
  88. package/template/node_modules/.vite-mcp/deps/embla-carousel-wheel-gestures.js +536 -0
  89. package/template/node_modules/.vite-mcp/deps/embla-carousel-wheel-gestures.js.map +1 -0
  90. package/template/node_modules/.vite-mcp/deps/magic-string.es-Cklsmr-5.js +1013 -0
  91. package/template/node_modules/.vite-mcp/deps/magic-string.es-Cklsmr-5.js.map +1 -0
  92. package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +46311 -0
  93. package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -0
  94. package/template/node_modules/.vite-mcp/deps/package.json +3 -0
  95. package/template/node_modules/.vite-mcp/deps/protocol-CTflwIfG.js +2090 -0
  96. package/template/node_modules/.vite-mcp/deps/protocol-CTflwIfG.js.map +1 -0
  97. package/template/node_modules/.vite-mcp/deps/react-dom.js +186 -0
  98. package/template/node_modules/.vite-mcp/deps/react-dom.js.map +1 -0
  99. package/template/node_modules/.vite-mcp/deps/react-dom_client.js +2 -0
  100. package/template/node_modules/.vite-mcp/deps/react.js +769 -0
  101. package/template/node_modules/.vite-mcp/deps/react.js.map +1 -0
  102. package/template/node_modules/.vite-mcp/deps/react_jsx-dev-runtime.js +205 -0
  103. package/template/node_modules/.vite-mcp/deps/react_jsx-dev-runtime.js.map +1 -0
  104. package/template/node_modules/.vite-mcp/deps/react_jsx-runtime.js +209 -0
  105. package/template/node_modules/.vite-mcp/deps/react_jsx-runtime.js.map +1 -0
  106. package/template/node_modules/.vite-mcp/deps/schemas-NsgmY9QV.js +12157 -0
  107. package/template/node_modules/.vite-mcp/deps/schemas-NsgmY9QV.js.map +1 -0
  108. package/template/node_modules/.vite-mcp/deps/tailwind-merge.js +2025 -0
  109. package/template/node_modules/.vite-mcp/deps/tailwind-merge.js.map +1 -0
  110. package/template/node_modules/.vite-mcp/deps/vitest.js +14021 -0
  111. package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -0
  112. package/template/node_modules/.vite-mcp/deps/zod.js +624 -0
  113. package/template/node_modules/.vite-mcp/deps/zod.js.map +1 -0
  114. package/template/src/tools/review-diff.test.ts +5 -1
  115. package/template/src/tools/review-diff.ts +1 -1
  116. package/template/src/tools/review-post.test.ts +5 -1
  117. package/template/src/tools/review-post.ts +1 -1
  118. package/template/src/tools/review-purchase.test.ts +5 -1
  119. package/template/src/tools/review-purchase.ts +1 -1
  120. package/template/src/tools/review.test.ts +5 -1
  121. package/template/src/tools/review.ts +1 -1
  122. package/template/src/tools/show-albums.test.ts +5 -1
  123. package/template/src/tools/show-albums.ts +1 -1
  124. package/template/src/tools/show-carousel.test.ts +5 -1
  125. package/template/src/tools/show-carousel.ts +1 -1
  126. package/template/src/tools/show-map.test.ts +5 -1
  127. package/template/src/tools/show-map.ts +1 -1
  128. package/dist/discovery-BxKCIgG5.cjs +0 -332
  129. package/dist/discovery-BxKCIgG5.cjs.map +0 -1
  130. package/dist/discovery-Du4LHrih.js +0 -261
  131. package/dist/discovery-Du4LHrih.js.map +0 -1
  132. package/dist/simulator-C7mkK7Sz.js.map +0 -1
  133. package/dist/simulator-DIVvI69i.cjs.map +0 -1
  134. package/dist/simulator-url-BDGD4vZD.cjs.map +0 -1
  135. package/dist/simulator-url-Bkxj43yT.js.map +0 -1
  136. package/template/.sunpeak/dev.tsx +0 -79
  137. package/template/.sunpeak/resource-loader.html +0 -20
  138. package/template/.sunpeak/resource-loader.tsx +0 -57
  139. package/template/index.html +0 -14
  140. package/template/src/resources/index.ts +0 -17
@@ -8,6 +8,7 @@ import { pathToFileURL } from 'url';
8
8
  import { spawn } from 'child_process';
9
9
  import { getPort } from '../lib/get-port.mjs';
10
10
  import { startSandboxServer } from '../lib/sandbox-server.mjs';
11
+ import { inspectServer } from './inspect.mjs';
11
12
 
12
13
  /**
13
14
  * Import a module from the project's node_modules using ESM resolution
@@ -123,8 +124,11 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle, { skipInitialBu
123
124
  }
124
125
 
125
126
  /**
126
- * Start the Vite development server
127
- * Runs in the context of a user's project directory
127
+ * Start the Vite development server.
128
+ *
129
+ * Starts the MCP server (with Vite HMR for resources) and then launches the
130
+ * inspector UI pointed at it. The inspector handles the simulator UI, tool call
131
+ * proxying, and resource loading — all through the MCP protocol.
128
132
  */
129
133
  export async function dev(projectRoot = process.cwd(), args = []) {
130
134
  // Check for package.json
@@ -161,7 +165,7 @@ export async function dev(projectRoot = process.cwd(), args = []) {
161
165
  if (isProdTools) console.log('Prod Tools enabled by default (toggle in simulator sidebar)');
162
166
  if (isProdResources) console.log('Prod Resources: resources will use production-built HTML from dist/');
163
167
 
164
- console.log(`Starting Vite dev server on port ${port}...`);
168
+ console.log(`Starting dev server on port ${port}...`);
165
169
 
166
170
  // Check if we're in the sunpeak workspace (directory is named "template")
167
171
  const isTemplate = basename(projectRoot) === 'template';
@@ -188,205 +192,10 @@ export async function dev(projectRoot = process.cwd(), args = []) {
188
192
  sunpeakMcp = await import(pathToFileURL(join(sunpeakBase, 'dist/mcp/index.js')).href);
189
193
  sunpeakDiscovery = await import(pathToFileURL(join(sunpeakBase, 'dist/lib/discovery-cli.js')).href);
190
194
  }
191
- const { FAVICON_BUFFER: faviconBuffer, FAVICON_DATA_URI: faviconDataUri, runMCPServer } = sunpeakMcp;
195
+ const { runMCPServer } = sunpeakMcp;
192
196
  const { findResourceDirs, findSimulationFilesFlat, findToolFiles, extractResourceExport, extractToolExport } = sunpeakDiscovery;
193
197
 
194
- // Vite plugin to serve the sunpeak favicon
195
- const sunpeakFaviconPlugin = () => ({
196
- name: 'sunpeak-favicon',
197
- configureServer(server) {
198
- server.middlewares.use((req, res, next) => {
199
- if (req.url === '/favicon.ico') {
200
- res.setHeader('Content-Type', 'image/png');
201
- res.setHeader('Content-Length', faviconBuffer.length);
202
- res.setHeader('Cache-Control', 'public, max-age=86400');
203
- res.end(faviconBuffer);
204
- return;
205
- }
206
- next();
207
- });
208
- },
209
- });
210
-
211
- // Vite plugin that proxies callServerTool to real tool handlers
212
- const sunpeakCallToolPlugin = () => ({
213
- name: 'sunpeak-call-tool',
214
- configureServer(server) {
215
- server.middlewares.use('/__sunpeak/call-tool', async (req, res) => {
216
- if (req.method !== 'POST') {
217
- res.statusCode = 405;
218
- res.end('Method not allowed');
219
- return;
220
- }
221
- const chunks = [];
222
- for await (const chunk of req) chunks.push(chunk);
223
- const body = JSON.parse(Buffer.concat(chunks).toString());
224
- const { name, arguments: args } = body;
225
- const toolEntry = toolPathMap.get(name);
226
- if (!toolEntry) {
227
- res.setHeader('Content-Type', 'application/json');
228
- res.end(JSON.stringify({
229
- content: [{ type: 'text', text: `[Prod Tools] Tool "${name}" not found` }],
230
- isError: true,
231
- }));
232
- return;
233
- }
234
- try {
235
- // Re-load the handler module on each call so edits take effect without restart
236
- const mod = await toolLoaderServer.ssrLoadModule(`./${toolEntry.relativePath}`);
237
- const handler = mod.default;
238
- if (typeof handler !== 'function') {
239
- res.setHeader('Content-Type', 'application/json');
240
- res.end(JSON.stringify({
241
- content: [{ type: 'text', text: `[Prod Tools] Tool "${name}" has no default export handler` }],
242
- isError: true,
243
- }));
244
- return;
245
- }
246
- let result = await handler(args ?? {}, {});
247
- if (typeof result === 'string') {
248
- result = { content: [{ type: 'text', text: result }] };
249
- }
250
- res.setHeader('Content-Type', 'application/json');
251
- res.end(JSON.stringify(result));
252
- } catch (err) {
253
- res.setHeader('Content-Type', 'application/json');
254
- res.end(JSON.stringify({
255
- content: [{ type: 'text', text: `[Prod Tools] Tool error: ${err.message}` }],
256
- isError: true,
257
- }));
258
- }
259
- });
260
- },
261
- });
262
-
263
- // Vite plugin that serves production-built HTML from dist/
264
- const sunpeakDistPlugin = () => ({
265
- name: 'sunpeak-dist',
266
- configureServer(server) {
267
- server.middlewares.use('/dist', (req, res, next) => {
268
- const filePath = join(projectRoot, 'dist', req.url);
269
- if (filePath.endsWith('.html')) {
270
- if (existsSync(filePath)) {
271
- res.setHeader('Content-Type', 'text/html');
272
- res.setHeader('Cache-Control', 'no-cache');
273
- res.end(readFileSync(filePath));
274
- } else {
275
- // Return 404 instead of falling through to Vite's SPA fallback,
276
- // which would serve the simulator's own index.html.
277
- res.statusCode = 404;
278
- res.end();
279
- }
280
- return;
281
- }
282
- next();
283
- });
284
- },
285
- });
286
-
287
- // Start the separate-origin sandbox server for cross-origin iframe isolation.
288
- // This matches how production hosts (ChatGPT, Claude) run app iframes on a
289
- // separate sandbox origin (e.g., web-sandbox.oaiusercontent.com).
290
- const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT || 24680);
291
- const sandbox = await startSandboxServer({ preferredPort: sandboxPort });
292
-
293
- // Load server config from src/server.ts (if present) for simulator display.
294
- // Uses a temporary SSR server so the values are available as Vite defines
295
- // before the main simulator UI server starts.
296
- // The fallback chain matches the MCP server: serverInfo.name → pkg.name → 'sunpeak-app'.
297
198
  const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
298
- let serverDisplayName = pkg.name ?? null;
299
- let serverDisplayIcon = undefined;
300
- const serverEntryPath = join(projectRoot, 'src/server.ts');
301
- if (existsSync(serverEntryPath)) {
302
- const configLoader = await createServer({
303
- root: projectRoot,
304
- server: { middlewareMode: true, hmr: false },
305
- resolve: { alias: { '@': path.resolve(projectRoot, 'src'), ...(isTemplate && { sunpeak: parentSrc }) } },
306
- appType: 'custom',
307
- logLevel: 'silent',
308
- });
309
- try {
310
- const serverMod = await configLoader.ssrLoadModule('./src/server.ts');
311
- if (serverMod.server && typeof serverMod.server === 'object') {
312
- if (serverMod.server.name) serverDisplayName = serverMod.server.name;
313
- // Extract a display icon from the icons array (first non-dark icon, or first icon)
314
- const icons = serverMod.server.icons;
315
- if (Array.isArray(icons) && icons.length > 0) {
316
- const lightIcon = icons.find(i => !i.theme || i.theme === 'light') ?? icons[0];
317
- serverDisplayIcon = lightIcon?.src;
318
- }
319
- }
320
- } catch (err) {
321
- // Non-fatal — simulator will use defaults
322
- } finally {
323
- await configLoader.close();
324
- }
325
- }
326
-
327
- // Create and start Vite dev server programmatically
328
- const server = await createServer({
329
- root: projectRoot,
330
- optimizeDeps: {
331
- // The simulator UI entry (.sunpeak/dev.tsx) imports sunpeak/simulator
332
- // which pulls in React and the simulator components. Pre-include the
333
- // dev.tsx entry so its transitive deps are discovered at startup.
334
- entries: ['.sunpeak/dev.tsx'],
335
- },
336
- plugins: [
337
- react(),
338
- tailwindcss(),
339
- sunpeakFaviconPlugin(),
340
- sunpeakCallToolPlugin(),
341
- sunpeakDistPlugin(),
342
- // Inject paint fence responder into all HTML pages served by Vite.
343
- // When resources are loaded in the cross-origin sandbox proxy's inner
344
- // iframe, the proxy can't inject scripts (cross-origin). This plugin
345
- // ensures the fence responder is always present so display mode
346
- // transitions resolve deterministically.
347
- {
348
- name: 'sunpeak-fence-responder',
349
- transformIndexHtml(html) {
350
- const fenceScript = `<script>window.addEventListener("message",function(e){if(e.data&&e.data.method==="sunpeak/fence"){var fid=e.data.params&&e.data.params.fenceId;requestAnimationFrame(function(){e.source.postMessage({jsonrpc:"2.0",method:"sunpeak/fence-ack",params:{fenceId:fid}},"*");});}});</script>`;
351
- return html.replace('</head>', fenceScript + '</head>');
352
- },
353
- },
354
- // Health endpoint for Playwright webServer readiness check
355
- {
356
- name: 'sunpeak-health',
357
- configureServer(server) {
358
- server.middlewares.use('/health', (_req, res) => {
359
- res.writeHead(200, { 'Content-Type': 'application/json' });
360
- res.end(JSON.stringify({ status: 'ok' }));
361
- });
362
- },
363
- },
364
- ],
365
- define: {
366
- '__SUNPEAK_PROD_TOOLS__': JSON.stringify(isProdTools),
367
- '__SUNPEAK_PROD_RESOURCES__': JSON.stringify(isProdResources),
368
- '__SUNPEAK_SANDBOX_URL__': JSON.stringify(sandbox.url),
369
- '__SUNPEAK_APP_NAME__': JSON.stringify(serverDisplayName ?? null),
370
- '__SUNPEAK_APP_ICON__': JSON.stringify(serverDisplayIcon ?? null),
371
- '__SUNPEAK_DEFAULT_ICON__': JSON.stringify(faviconDataUri),
372
- },
373
- resolve: {
374
- alias: {
375
- '@': path.resolve(projectRoot, 'src'),
376
- // In workspace dev mode, use local sunpeak source
377
- ...(isTemplate && {
378
- sunpeak: parentSrc,
379
- }),
380
- },
381
- },
382
- server: {
383
- port,
384
- // Don't auto-open browser when started by Playwright or CI
385
- open: !process.env.CI && !process.env.SUNPEAK_LIVE_TEST,
386
- // Allow tunnel hosts (ngrok, cloudflared, etc.) to reach the dev server
387
- allowedHosts: 'all',
388
- },
389
- });
390
199
 
391
200
  // --prod-resources: Run initial production build so dist/ is ready before server starts
392
201
  if (isProdResources) {
@@ -405,16 +214,6 @@ export async function dev(projectRoot = process.cwd(), args = []) {
405
214
  }
406
215
  }
407
216
 
408
- await server.listen();
409
- server.printUrls();
410
- server.bindCLIShortcuts({ print: true });
411
-
412
- // Print star-begging message unless --no-begging is set
413
- if (!noBegging) {
414
- // #FFB800 in 24-bit ANSI color
415
- console.log('\n\n\x1b[38;2;255;184;0m\u2b50\ufe0f \u2192 \u2764\ufe0f https://github.com/Sunpeak-AI/sunpeak\x1b[0m\n');
416
- }
417
-
418
217
  // Discover simulations using sunpeak's discovery utilities
419
218
  const resourcesDir = join(projectRoot, 'src/resources');
420
219
  const simulationsDir = join(projectRoot, 'tests/simulations');
@@ -461,12 +260,10 @@ export async function dev(projectRoot = process.cwd(), args = []) {
461
260
 
462
261
  // Build path map for prod-tools handler reloading (re-imports on each call for HMR).
463
262
  // Also do an initial load to validate handlers and populate toolHandlerMap for the MCP server.
464
- const toolPathMap = new Map();
465
263
  const toolHandlerMap = new Map();
466
264
  for (const [toolName, { tool, path: toolPath }] of toolMap) {
467
265
  void tool; // Used for metadata; handler loaded unconditionally
468
266
  const relativePath = path.relative(projectRoot, toolPath);
469
- toolPathMap.set(toolName, { relativePath });
470
267
  try {
471
268
  const mod = await toolLoaderServer.ssrLoadModule(`./${relativePath}`);
472
269
  if (typeof mod.default === 'function') {
@@ -523,10 +320,10 @@ export async function dev(projectRoot = process.cwd(), args = []) {
523
320
  ...(toolHandlerMap.has(toolName) && toolHandlerMap.get(toolName).outputSchema ? {
524
321
  outputSchema: toolHandlerMap.get(toolName).outputSchema,
525
322
  } : {}),
526
- // Attach real handler for tools consumed by the MCP server.
527
- // Backend-only tools (no resource) always need handlers for callServerTool.
528
- // UI tools only get handlers in --prod-tools mode (otherwise simulation mock data is used).
529
- ...((toolHandlerMap.has(toolName) && (!resourceKey || isProdTools)) ? {
323
+ // Attach real handler so Prod Tools mode works at runtime.
324
+ // The --prod-tools flag only sets the default checkbox state; the handler
325
+ // must always be available for when the user toggles it in the sidebar.
326
+ ...(toolHandlerMap.has(toolName) ? {
530
327
  handler: toolHandlerMap.get(toolName).handler,
531
328
  } : {}),
532
329
  });
@@ -552,32 +349,43 @@ export async function dev(projectRoot = process.cwd(), args = []) {
552
349
  }
553
350
 
554
351
  // Start MCP server with its own Vite instance for HMR
555
- if (simulations.length > 0) {
556
- // Find available ports for the MCP server and HMR WebSocket
557
- const mcpPort = await getPort(8000);
558
- const hmrPort = await getPort(Number(process.env.SUNPEAK_HMR_PORT || 24679));
559
-
560
- console.log(`\nStarting MCP server with ${simulations.length} simulation(s) (Vite HMR)...`);
561
-
562
- // Virtual entry module plugin for MCP
563
- const sunpeakEntryPlugin = () => ({
564
- name: 'sunpeak-entry',
565
- resolveId(id) {
566
- if (id.startsWith('virtual:sunpeak-entry')) {
567
- return id;
352
+ if (simulations.length === 0) {
353
+ console.warn('No simulations found. Create simulation files in tests/simulations/.');
354
+ // Close loader servers since there's nothing to serve
355
+ await toolLoaderServer.close();
356
+ if (loaderServer) await loaderServer.close();
357
+ return;
358
+ }
359
+
360
+ // Start the separate-origin sandbox server for cross-origin iframe isolation.
361
+ const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT || 24680);
362
+ const sandbox = await startSandboxServer({ preferredPort: sandboxPort });
363
+
364
+ // Find available ports for the MCP server and HMR WebSocket
365
+ const mcpPort = await getPort(8000);
366
+ const hmrPort = await getPort(Number(process.env.SUNPEAK_HMR_PORT || 24679));
367
+
368
+ console.log(`\nStarting MCP server with ${simulations.length} simulation(s) (Vite HMR)...`);
369
+
370
+ // Virtual entry module plugin for MCP (serves resource HTML with HMR)
371
+ const sunpeakEntryPlugin = () => ({
372
+ name: 'sunpeak-entry',
373
+ resolveId(id) {
374
+ if (id.startsWith('virtual:sunpeak-entry')) {
375
+ return id;
376
+ }
377
+ },
378
+ load(id) {
379
+ if (id.startsWith('virtual:sunpeak-entry')) {
380
+ const url = new URL(id.replace('virtual:sunpeak-entry', 'http://x'));
381
+ const srcPath = url.searchParams.get('src');
382
+ const componentName = url.searchParams.get('component');
383
+
384
+ if (!srcPath || !componentName) {
385
+ return 'console.error("Missing src or component param");';
568
386
  }
569
- },
570
- load(id) {
571
- if (id.startsWith('virtual:sunpeak-entry')) {
572
- const url = new URL(id.replace('virtual:sunpeak-entry', 'http://x'));
573
- const srcPath = url.searchParams.get('src');
574
- const componentName = url.searchParams.get('component');
575
-
576
- if (!srcPath || !componentName) {
577
- return 'console.error("Missing src or component param");';
578
- }
579
387
 
580
- return `
388
+ return `
581
389
  import { createElement } from 'react';
582
390
  import { createRoot } from 'react-dom/client';
583
391
  import { AppProvider } from 'sunpeak';
@@ -604,123 +412,142 @@ if (import.meta.hot) {
604
412
  import.meta.hot.accept();
605
413
  }
606
414
  `;
607
- }
608
- },
609
- });
415
+ }
416
+ },
417
+ });
610
418
 
611
- // Create Vite dev server in middleware mode for MCP
612
- // Use separate cache directory to avoid conflicts with main dev server
613
- const mcpViteServer = await createServer({
614
- root: projectRoot,
615
- cacheDir: 'node_modules/.vite-mcp',
616
- plugins: [react(), tailwindcss(), sunpeakEntryPlugin()],
617
- resolve: {
618
- alias: {
619
- '@': path.resolve(projectRoot, 'src'),
620
- ...(isTemplate && {
621
- sunpeak: parentSrc,
622
- }),
623
- },
419
+ // Create Vite dev server in middleware mode for MCP
420
+ // Use separate cache directory to avoid conflicts with main dev server
421
+ const mcpViteServer = await createServer({
422
+ root: projectRoot,
423
+ cacheDir: 'node_modules/.vite-mcp',
424
+ plugins: [react(), tailwindcss(), sunpeakEntryPlugin()],
425
+ resolve: {
426
+ alias: {
427
+ '@': path.resolve(projectRoot, 'src'),
428
+ ...(isTemplate && {
429
+ sunpeak: parentSrc,
430
+ }),
624
431
  },
625
- server: {
626
- middlewareMode: true,
627
- hmr: { port: hmrPort },
628
- allowedHosts: true,
629
- watch: {
630
- // Only watch files that affect the UI bundle (not JSON, tests, etc.)
631
- // MCP resources reload on next tool call, not on file change
632
- ignored: (filePath) => {
633
- if (!filePath.includes('.')) return false; // Watch directories
634
- if (/\.(tsx?|css)$/.test(filePath)) {
635
- return /\.(test|spec)\.tsx?$/.test(filePath); // Ignore tests
636
- }
637
- return true; // Ignore everything else
638
- },
432
+ },
433
+ server: {
434
+ middlewareMode: true,
435
+ hmr: { port: hmrPort },
436
+ allowedHosts: true,
437
+ watch: {
438
+ // Only watch files that affect the UI bundle (not JSON, tests, etc.)
439
+ // MCP resources reload on next tool call, not on file change
440
+ ignored: (filePath) => {
441
+ if (!filePath.includes('.')) return false; // Watch directories
442
+ if (/\.(tsx?|css)$/.test(filePath)) {
443
+ return /\.(test|spec)\.tsx?$/.test(filePath); // Ignore tests
444
+ }
445
+ return true; // Ignore everything else
639
446
  },
640
447
  },
641
- optimizeDeps: {
642
- // Pre-scan resource source files so ALL their dependencies are
643
- // discovered and pre-bundled at startup. Without this, the first
644
- // resource load discovers new deps (e.g., mapbox-gl, embla-carousel),
645
- // triggers re-optimization, and reloads all connections killing
646
- // any active ChatGPT/Claude iframe connections with ECONNRESET.
647
- entries: [
648
- 'src/resources/**/*.{ts,tsx}',
649
- 'src/tools/**/*.ts',
650
- ],
651
- include: ['react', 'react-dom/client'],
652
- },
653
- appType: 'custom',
654
- });
448
+ },
449
+ optimizeDeps: {
450
+ // Pre-scan resource source files so ALL their dependencies are
451
+ // discovered and pre-bundled at startup. Without this, the first
452
+ // resource load discovers new deps (e.g., mapbox-gl, embla-carousel),
453
+ // triggers re-optimization, and reloads all connections killing
454
+ // any active ChatGPT/Claude iframe connections with ECONNRESET.
455
+ entries: [
456
+ 'src/resources/**/*.{ts,tsx}',
457
+ 'src/tools/**/*.ts',
458
+ ],
459
+ include: ['react', 'react-dom/client'],
460
+ },
461
+ appType: 'custom',
462
+ });
655
463
 
656
- // Load server config from src/server.ts (if present) for server identity
657
- let serverInfo = undefined;
658
- if (existsSync(serverEntryPath)) {
659
- try {
660
- const serverMod = await toolLoaderServer.ssrLoadModule('./src/server.ts');
661
- if (serverMod.server && typeof serverMod.server === 'object') {
662
- serverInfo = serverMod.server;
464
+ // Load server config from src/server.ts (if present) for server identity
465
+ const serverEntryPath = join(projectRoot, 'src/server.ts');
466
+ let serverInfo = undefined;
467
+ let serverDisplayName = pkg.name ?? null;
468
+ let serverDisplayIcon = undefined;
469
+ if (existsSync(serverEntryPath)) {
470
+ try {
471
+ const serverMod = await toolLoaderServer.ssrLoadModule('./src/server.ts');
472
+ if (serverMod.server && typeof serverMod.server === 'object') {
473
+ serverInfo = serverMod.server;
474
+ if (serverMod.server.name) serverDisplayName = serverMod.server.name;
475
+ // Extract a display icon from the icons array (first non-dark icon, or first icon)
476
+ const icons = serverMod.server.icons;
477
+ if (Array.isArray(icons) && icons.length > 0) {
478
+ const lightIcon = icons.find(i => !i.theme || i.theme === 'light') ?? icons[0];
479
+ serverDisplayIcon = lightIcon?.src;
663
480
  }
664
- } catch (err) {
665
- console.warn(`Warning: Could not load server config: ${err.message}`);
666
481
  }
482
+ } catch (err) {
483
+ console.warn(`Warning: Could not load server config: ${err.message}`);
667
484
  }
485
+ }
668
486
 
669
- const mcpHandle = runMCPServer({
670
- name: serverInfo?.name ?? pkg.name ?? 'Sunpeak',
671
- version: serverInfo?.version ?? pkg.version ?? '0.1.0',
672
- serverInfo,
673
- simulations,
674
- port: mcpPort,
675
- hmrPort,
676
- // In --prod-resources mode, don't pass viteServer so the MCP server serves pre-built HTML.
677
- // Otherwise, pass it so ChatGPT gets Vite HMR.
678
- viteServer: isProdResources ? undefined : mcpViteServer,
679
- });
680
-
681
- // Build production bundles and watch for changes.
682
- // Tunnel clients (e.g. Claude via ngrok) get the pre-built HTML since they can't
683
- // reach the local Vite dev server. The watcher rebuilds on source file changes
684
- // so the prod output stays fresh without manual `sunpeak build`.
685
- // On successful builds, mcpHandle.invalidateResources() notifies tunnel sessions.
686
- startBuildWatcher(projectRoot, resourcesDir, mcpHandle, { skipInitialBuild: isProdResources });
687
-
688
- // Handle signals - close all servers
689
- process.on('SIGINT', async () => {
690
- await mcpViteServer.close();
691
- await toolLoaderServer.close();
692
- if (loaderServer) await loaderServer.close();
693
- await sandbox.close();
694
- await server.close();
695
- process.exit(0);
696
- });
487
+ const mcpHandle = runMCPServer({
488
+ name: serverInfo?.name ?? pkg.name ?? 'Sunpeak',
489
+ version: serverInfo?.version ?? pkg.version ?? '0.1.0',
490
+ serverInfo,
491
+ simulations,
492
+ port: mcpPort,
493
+ hmrPort,
494
+ // In --prod-resources mode, don't pass viteServer so the MCP server serves pre-built HTML.
495
+ // Otherwise, pass it so ChatGPT gets Vite HMR.
496
+ viteServer: isProdResources ? undefined : mcpViteServer,
497
+ });
697
498
 
698
- process.on('SIGTERM', async () => {
499
+ // Wait for the MCP server to be listening before starting the inspector
500
+ await mcpHandle.ready;
501
+
502
+ // Build production bundles and watch for changes.
503
+ // Tunnel clients (e.g. Claude via ngrok) get the pre-built HTML since they can't
504
+ // reach the local Vite dev server. The watcher rebuilds on source file changes
505
+ // so the prod output stays fresh without manual `sunpeak build`.
506
+ // On successful builds, mcpHandle.invalidateResources() notifies tunnel sessions.
507
+ startBuildWatcher(projectRoot, resourcesDir, mcpHandle, { skipInitialBuild: isProdResources });
508
+
509
+ // Launch the inspector UI pointed at the local MCP server.
510
+ // This serves the simulator UI via Vite, connecting to our MCP server as a client.
511
+ // In framework mode, the simulator shows prod-tools/prod-resources toggles instead
512
+ // of the server URL input.
513
+ const mcpUrl = `http://localhost:${mcpPort}/mcp`;
514
+ await inspectServer({
515
+ server: mcpUrl,
516
+ simulationsDir,
517
+ port,
518
+ name: serverDisplayName,
519
+ sandboxUrl: sandbox.url,
520
+ frameworkMode: true,
521
+ defaultProdTools: isProdTools,
522
+ defaultProdResources: isProdResources,
523
+ projectRoot,
524
+ noBegging,
525
+ open: !process.env.CI && !process.env.SUNPEAK_LIVE_TEST,
526
+ // Direct tool handler call for Prod Tools Run button.
527
+ // Re-imports via Vite SSR on each call so handlers pick up HMR changes.
528
+ callToolDirect: async (name, args) => {
529
+ for (const [toolName, { path: toolPath }] of toolMap) {
530
+ if (toolName !== name) continue;
531
+ const relativePath = path.relative(projectRoot, toolPath);
532
+ const mod = await toolLoaderServer.ssrLoadModule(`./${relativePath}`);
533
+ if (typeof mod.default !== 'function') {
534
+ throw new Error(`Tool "${name}" has no default export handler`);
535
+ }
536
+ const result = await mod.default(args, {});
537
+ if (typeof result === 'string') {
538
+ return { content: [{ type: 'text', text: result }] };
539
+ }
540
+ return result;
541
+ }
542
+ throw new Error(`Tool "${name}" not found`);
543
+ },
544
+ onCleanup: async () => {
699
545
  await mcpViteServer.close();
700
546
  await toolLoaderServer.close();
701
547
  if (loaderServer) await loaderServer.close();
702
548
  await sandbox.close();
703
- await server.close();
704
- process.exit(0);
705
- });
706
- } else {
707
- // No simulations - just handle signals for the dev server
708
- process.on('SIGINT', async () => {
709
- await toolLoaderServer.close();
710
- if (loaderServer) await loaderServer.close();
711
- await sandbox.close();
712
- await server.close();
713
- process.exit(0);
714
- });
715
-
716
- process.on('SIGTERM', async () => {
717
- await toolLoaderServer.close();
718
- if (loaderServer) await loaderServer.close();
719
- await sandbox.close();
720
- await server.close();
721
- process.exit(0);
722
- });
723
- }
549
+ },
550
+ });
724
551
  }
725
552
 
726
553
  // Allow running directly