sunpeak 0.16.28 → 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.
- package/bin/commands/dev.mjs +169 -342
- package/bin/commands/inspect.mjs +763 -0
- package/bin/commands/new.mjs +2 -2
- package/bin/lib/inspect/inspect-config.d.mts +20 -0
- package/bin/lib/inspect/inspect-config.mjs +76 -0
- package/bin/lib/live/global-setup.mjs +6 -1
- package/bin/sunpeak.js +11 -1
- package/dist/chatgpt/globals.css +35 -18
- package/dist/chatgpt/index.cjs +3 -11
- package/dist/chatgpt/index.cjs.map +1 -1
- package/dist/chatgpt/index.d.ts +2 -2
- package/dist/chatgpt/index.js +4 -8
- package/dist/chatgpt/index.js.map +1 -1
- package/dist/claude/index.cjs +1 -1
- package/dist/claude/index.js +1 -1
- package/dist/discovery-Cgoegt62.js +114 -0
- package/dist/discovery-Cgoegt62.js.map +1 -0
- package/dist/discovery-Clu4uHp1.cjs +161 -0
- package/dist/discovery-Clu4uHp1.cjs.map +1 -0
- package/dist/index.cjs +1 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/discovery-cli.cjs +1 -1
- package/dist/lib/discovery-cli.js +1 -1
- package/dist/lib/discovery.d.ts +7 -67
- package/dist/lib/index.d.ts +0 -1
- package/dist/mcp/index.cjs +34 -23
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +34 -23
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/types.d.ts +5 -0
- package/dist/simulator/index.cjs +5 -11
- package/dist/simulator/index.cjs.map +1 -1
- package/dist/simulator/index.d.ts +4 -2
- package/dist/simulator/index.js +5 -8
- package/dist/simulator/index.js.map +1 -1
- package/dist/simulator/simple-sidebar.d.ts +7 -4
- package/dist/simulator/simulator-url.d.ts +8 -0
- package/dist/simulator/simulator.d.ts +15 -2
- package/dist/simulator/use-mcp-connection.d.ts +19 -0
- package/dist/{simulator-BYIH-xqQ.cjs → simulator-CH9hs0N6.cjs} +159 -52
- package/dist/simulator-CH9hs0N6.cjs.map +1 -0
- package/dist/{simulator-CmgNnWBO.js → simulator-Dl8B-Ljb.js} +154 -53
- package/dist/simulator-Dl8B-Ljb.js.map +1 -0
- package/dist/{simulator-url-BDGD4vZD.cjs → simulator-url-CozKF1jf.cjs} +3 -1
- package/dist/simulator-url-CozKF1jf.cjs.map +1 -0
- package/dist/{simulator-url-Bkxj43yT.js → simulator-url-KoS_ToP6.js} +3 -1
- package/dist/simulator-url-KoS_ToP6.js.map +1 -0
- package/dist/style.css +35 -18
- package/package.json +9 -1
- package/template/dist/albums/albums.html +105 -0
- package/template/dist/albums/albums.json +16 -0
- package/template/dist/carousel/carousel.html +105 -0
- package/template/dist/carousel/carousel.json +16 -0
- package/template/dist/map/map.html +3060 -0
- package/template/dist/map/map.json +22 -0
- package/template/dist/review/review.html +105 -0
- package/template/dist/review/review.json +16 -0
- package/template/dist/server.js +15 -0
- package/template/dist/tools/review-diff.js +50 -0
- package/template/dist/tools/review-post.js +50 -0
- package/template/dist/tools/review-purchase.js +61 -0
- package/template/dist/tools/review.js +31 -0
- package/template/dist/tools/show-albums.js +56 -0
- package/template/dist/tools/show-carousel.js +41 -0
- package/template/dist/tools/show-map.js +47 -0
- package/template/node_modules/.vite/deps/_metadata.json +8 -0
- package/template/node_modules/.vite/deps/package.json +3 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +500 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +563 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +575 -0
- package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/@testing-library_react.js +11363 -0
- package/template/node_modules/.vite-mcp/deps/@testing-library_react.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/_metadata.json +130 -0
- package/template/node_modules/.vite-mcp/deps/chunk-BoAXSpZd.js +33 -0
- package/template/node_modules/.vite-mcp/deps/client-CU1wWud4.js +14385 -0
- package/template/node_modules/.vite-mcp/deps/client-CU1wWud4.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/clsx.js +18 -0
- package/template/node_modules/.vite-mcp/deps/clsx.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/dist-uWX8WbjY.js +505 -0
- package/template/node_modules/.vite-mcp/deps/dist-uWX8WbjY.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js +1461 -0
- package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/embla-carousel-wheel-gestures.js +536 -0
- package/template/node_modules/.vite-mcp/deps/embla-carousel-wheel-gestures.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/magic-string.es-Cklsmr-5.js +1013 -0
- package/template/node_modules/.vite-mcp/deps/magic-string.es-Cklsmr-5.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +46311 -0
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/package.json +3 -0
- package/template/node_modules/.vite-mcp/deps/protocol-CTflwIfG.js +2090 -0
- package/template/node_modules/.vite-mcp/deps/protocol-CTflwIfG.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/react-dom.js +186 -0
- package/template/node_modules/.vite-mcp/deps/react-dom.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/react-dom_client.js +2 -0
- package/template/node_modules/.vite-mcp/deps/react.js +769 -0
- package/template/node_modules/.vite-mcp/deps/react.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/react_jsx-dev-runtime.js +205 -0
- package/template/node_modules/.vite-mcp/deps/react_jsx-dev-runtime.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/react_jsx-runtime.js +209 -0
- package/template/node_modules/.vite-mcp/deps/react_jsx-runtime.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/schemas-NsgmY9QV.js +12157 -0
- package/template/node_modules/.vite-mcp/deps/schemas-NsgmY9QV.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/tailwind-merge.js +2025 -0
- package/template/node_modules/.vite-mcp/deps/tailwind-merge.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/vitest.js +14021 -0
- package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -0
- package/template/node_modules/.vite-mcp/deps/zod.js +624 -0
- package/template/node_modules/.vite-mcp/deps/zod.js.map +1 -0
- package/template/src/tools/review-diff.test.ts +5 -1
- package/template/src/tools/review-diff.ts +1 -1
- package/template/src/tools/review-post.test.ts +5 -1
- package/template/src/tools/review-post.ts +1 -1
- package/template/src/tools/review-purchase.test.ts +5 -1
- package/template/src/tools/review-purchase.ts +1 -1
- package/template/src/tools/review.test.ts +5 -1
- package/template/src/tools/review.ts +1 -1
- package/template/src/tools/show-albums.test.ts +5 -1
- package/template/src/tools/show-albums.ts +1 -1
- package/template/src/tools/show-carousel.test.ts +5 -1
- package/template/src/tools/show-carousel.ts +1 -1
- package/template/src/tools/show-map.test.ts +5 -1
- package/template/src/tools/show-map.ts +1 -1
- package/template/tests/e2e/map.spec.ts +4 -2
- package/dist/discovery-BxKCIgG5.cjs +0 -332
- package/dist/discovery-BxKCIgG5.cjs.map +0 -1
- package/dist/discovery-Du4LHrih.js +0 -261
- package/dist/discovery-Du4LHrih.js.map +0 -1
- package/dist/simulator-BYIH-xqQ.cjs.map +0 -1
- package/dist/simulator-CmgNnWBO.js.map +0 -1
- package/dist/simulator-url-BDGD4vZD.cjs.map +0 -1
- package/dist/simulator-url-Bkxj43yT.js.map +0 -1
- package/template/.sunpeak/dev.tsx +0 -79
- package/template/.sunpeak/resource-loader.html +0 -20
- package/template/.sunpeak/resource-loader.tsx +0 -57
- package/template/index.html +0 -14
- package/template/src/resources/index.ts +0 -17
package/bin/commands/dev.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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 {
|
|
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
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
...(
|
|
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
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
|
|
704
|
-
|
|
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
|