sunpeak 0.16.17 → 0.16.18
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/README.md +1 -1
- package/bin/commands/build.mjs +1 -1
- package/bin/commands/dev.mjs +137 -19
- package/bin/commands/new.mjs +21 -2
- package/bin/commands/start.mjs +1 -1
- package/dist/chatgpt/chatgpt-conversation.d.ts +3 -1
- package/dist/chatgpt/globals.css +37 -8
- package/dist/chatgpt/index.cjs +3 -5
- package/dist/chatgpt/index.cjs.map +1 -1
- package/dist/chatgpt/index.d.ts +0 -1
- package/dist/chatgpt/index.js +3 -5
- package/dist/chatgpt/index.js.map +1 -1
- package/dist/claude/claude-conversation.d.ts +3 -1
- package/dist/claude/index.cjs +2 -2
- package/dist/claude/index.d.ts +1 -1
- package/dist/claude/index.js +2 -2
- package/dist/{discovery-DvIQWTez.js → discovery-BVqD-JsT.js} +4 -2
- package/dist/{discovery-DvIQWTez.js.map → discovery-BVqD-JsT.js.map} +1 -1
- package/dist/{discovery-SviNiBkF.cjs → discovery-D1gpaVz4.cjs} +4 -2
- package/dist/{discovery-SviNiBkF.cjs.map → discovery-D1gpaVz4.cjs.map} +1 -1
- package/dist/hooks/index.d.ts +10 -1
- package/dist/hooks/safe-area.d.ts +6 -2
- package/dist/hooks/use-device-capabilities.d.ts +3 -0
- package/dist/hooks/use-platform.d.ts +3 -0
- package/dist/hooks/use-styles.d.ts +2 -0
- package/dist/hooks/use-time-zone.d.ts +1 -0
- package/dist/hooks/use-tool-info.d.ts +3 -0
- package/dist/hooks/use-user-agent.d.ts +1 -0
- package/dist/hooks/use-viewport.d.ts +3 -0
- package/dist/index-BITfgMxk.js +29 -0
- package/dist/index-BITfgMxk.js.map +1 -0
- package/dist/{index-DHcaJ5PU.cjs → index-CJ3nfg7Q.cjs} +4 -4
- package/dist/index-CJ3nfg7Q.cjs.map +1 -0
- package/dist/{index-CsYoMHyn.js → index-C_uYg-eE.js} +4 -4
- package/dist/index-C_uYg-eE.js.map +1 -0
- package/dist/index-DxCrGGzy.cjs +28 -0
- package/dist/index-DxCrGGzy.cjs.map +1 -0
- package/dist/index.cjs +43 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +43 -6
- 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/mcp/index.cjs +24 -8
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +24 -8
- package/dist/mcp/index.js.map +1 -1
- package/dist/simulator/hosts.d.ts +2 -0
- package/dist/simulator/index.cjs +3 -3
- package/dist/simulator/index.js +3 -3
- package/dist/simulator/simple-sidebar.d.ts +18 -4
- package/dist/simulator/simulator-url.d.ts +8 -0
- package/dist/simulator/simulator.d.ts +13 -1
- package/dist/simulator/use-simulator-state.d.ts +10 -6
- package/dist/{simulator-Da9iAupa.js → simulator-DfTsXTK4.js} +956 -490
- package/dist/simulator-DfTsXTK4.js.map +1 -0
- package/dist/{simulator-BEFsuj9Z.cjs → simulator-mbqealxY.cjs} +955 -489
- package/dist/simulator-mbqealxY.cjs.map +1 -0
- package/dist/{simulator-url-rgg_KYOg.cjs → simulator-url-DcSYRl-P.cjs} +7 -1
- package/dist/simulator-url-DcSYRl-P.cjs.map +1 -0
- package/dist/{simulator-url-CuLqtnSS.js → simulator-url-j_XV3EoP.js} +7 -1
- package/dist/simulator-url-j_XV3EoP.js.map +1 -0
- package/dist/style.css +37 -8
- package/package.json +2 -2
- package/template/.sunpeak/dev.tsx +28 -2
- package/template/playwright.config.ts +6 -3
- package/template/src/resources/albums/albums.test.tsx +1 -0
- package/template/src/resources/albums/albums.tsx +5 -2
- package/template/src/resources/albums/components/albums.test.tsx +22 -18
- package/template/src/resources/albums/components/albums.tsx +63 -7
- package/template/src/resources/albums/components/fullscreen-viewer.test.tsx +3 -25
- package/template/src/resources/albums/components/fullscreen-viewer.tsx +2 -3
- package/template/src/resources/carousel/carousel.test.tsx +12 -16
- package/template/src/resources/carousel/carousel.tsx +47 -5
- package/template/src/resources/map/components/map.tsx +65 -9
- package/template/src/resources/map/map.test.tsx +0 -1
- package/template/src/resources/review/review.test.tsx +25 -27
- package/template/src/resources/review/review.tsx +85 -63
- package/template/src/tools/review-diff.test.ts +73 -0
- package/template/src/tools/review-diff.ts +29 -2
- package/template/src/tools/review-post.test.ts +100 -0
- package/template/src/tools/review-post.ts +30 -2
- package/template/src/tools/review-purchase.test.ts +111 -0
- package/template/src/tools/review-purchase.ts +35 -2
- package/template/src/tools/review.test.ts +40 -0
- package/template/src/tools/review.ts +4 -1
- package/template/src/tools/show-albums.test.ts +42 -0
- package/template/src/tools/show-albums.ts +22 -2
- package/template/src/tools/show-carousel.test.ts +45 -0
- package/template/src/tools/show-carousel.ts +19 -2
- package/template/src/tools/show-map.test.ts +74 -0
- package/template/src/tools/show-map.ts +21 -2
- package/template/tests/e2e/albums.spec.ts +75 -0
- package/template/tests/e2e/carousel.spec.ts +65 -0
- package/template/tests/e2e/global-setup.ts +25 -0
- package/template/tests/e2e/map.spec.ts +60 -0
- package/template/tests/e2e/review.spec.ts +72 -11
- package/dist/chatgpt/chatgpt-simulator.d.ts +0 -10
- package/dist/index-BFD3bAHd.cjs +0 -547
- package/dist/index-BFD3bAHd.cjs.map +0 -1
- package/dist/index-CsYoMHyn.js.map +0 -1
- package/dist/index-DHcaJ5PU.cjs.map +0 -1
- package/dist/index-wUvmyoCx.js +0 -532
- package/dist/index-wUvmyoCx.js.map +0 -1
- package/dist/simulator-BEFsuj9Z.cjs.map +0 -1
- package/dist/simulator-Da9iAupa.js.map +0 -1
- package/dist/simulator-url-CuLqtnSS.js.map +0 -1
- package/dist/simulator-url-rgg_KYOg.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
Local-first MCP Apps framework.
|
|
20
20
|
|
|
21
|
-
Quickstart, build, test, and ship your Claude or ChatGPT App!
|
|
21
|
+
Quickstart, build, test, and ship your Claude Connector or ChatGPT App!
|
|
22
22
|
|
|
23
23
|
[Demo (Hosted)](https://sunpeak.ai/simulator) ~
|
|
24
24
|
[Demo (Video)](https://cdn.sunpeak.ai/sunpeak-demo-prod.mp4) ~
|
package/bin/commands/build.mjs
CHANGED
|
@@ -355,7 +355,7 @@ ${jsContents}
|
|
|
355
355
|
|
|
356
356
|
// Find tool files
|
|
357
357
|
const toolFiles = existsSync(toolsDir)
|
|
358
|
-
? readdirSync(toolsDir).filter(f => f.endsWith('.ts'))
|
|
358
|
+
? readdirSync(toolsDir).filter(f => f.endsWith('.ts') && !f.endsWith('.test.ts'))
|
|
359
359
|
: [];
|
|
360
360
|
|
|
361
361
|
const hasServerEntry = existsSync(serverEntryPath);
|
package/bin/commands/dev.mjs
CHANGED
|
@@ -62,7 +62,7 @@ async function importFromProject(require, moduleName) {
|
|
|
62
62
|
*
|
|
63
63
|
* When a file changes during a build, the current build is killed and restarted.
|
|
64
64
|
*/
|
|
65
|
-
function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
65
|
+
function startBuildWatcher(projectRoot, resourcesDir, mcpHandle, { skipInitialBuild = false } = {}) {
|
|
66
66
|
let activeChild = null;
|
|
67
67
|
const sunpeakBin = join(dirname(new URL(import.meta.url).pathname), '..', 'sunpeak.js');
|
|
68
68
|
|
|
@@ -94,8 +94,10 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
94
94
|
});
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
// Initial build
|
|
98
|
-
|
|
97
|
+
// Initial build (skip when --prod-resources already ran a synchronous build)
|
|
98
|
+
if (!skipInitialBuild) {
|
|
99
|
+
runBuild();
|
|
100
|
+
}
|
|
99
101
|
|
|
100
102
|
// Watch src/resources/ for changes using fs.watch (recursive supported on macOS/Windows)
|
|
101
103
|
let debounceTimer = null;
|
|
@@ -150,6 +152,13 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
150
152
|
// Parse --no-begging flag
|
|
151
153
|
const noBegging = args.includes('--no-begging');
|
|
152
154
|
|
|
155
|
+
// Parse --prod-tools and --prod-resources flags
|
|
156
|
+
const isProdTools = args.includes('--prod-tools');
|
|
157
|
+
const isProdResources = args.includes('--prod-resources');
|
|
158
|
+
|
|
159
|
+
if (isProdTools) console.log('Prod Tools enabled by default (toggle in simulator sidebar)');
|
|
160
|
+
if (isProdResources) console.log('Prod Resources: resources will use production-built HTML from dist/');
|
|
161
|
+
|
|
153
162
|
console.log(`Starting Vite dev server on port ${port}...`);
|
|
154
163
|
|
|
155
164
|
// Check if we're in the sunpeak workspace (directory is named "template")
|
|
@@ -197,10 +206,96 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
197
206
|
},
|
|
198
207
|
});
|
|
199
208
|
|
|
209
|
+
// Vite plugin that proxies callServerTool to real tool handlers
|
|
210
|
+
const sunpeakCallToolPlugin = () => ({
|
|
211
|
+
name: 'sunpeak-call-tool',
|
|
212
|
+
configureServer(server) {
|
|
213
|
+
server.middlewares.use('/__sunpeak/call-tool', async (req, res) => {
|
|
214
|
+
if (req.method !== 'POST') {
|
|
215
|
+
res.statusCode = 405;
|
|
216
|
+
res.end('Method not allowed');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const chunks = [];
|
|
220
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
221
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
222
|
+
const { name, arguments: args } = body;
|
|
223
|
+
const toolEntry = toolPathMap.get(name);
|
|
224
|
+
if (!toolEntry) {
|
|
225
|
+
res.setHeader('Content-Type', 'application/json');
|
|
226
|
+
res.end(JSON.stringify({
|
|
227
|
+
content: [{ type: 'text', text: `[Prod Tools] Tool "${name}" not found` }],
|
|
228
|
+
isError: true,
|
|
229
|
+
}));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
// Re-load the handler module on each call so edits take effect without restart
|
|
234
|
+
const mod = await toolLoaderServer.ssrLoadModule(`./${toolEntry.relativePath}`);
|
|
235
|
+
const handler = mod.default;
|
|
236
|
+
if (typeof handler !== 'function') {
|
|
237
|
+
res.setHeader('Content-Type', 'application/json');
|
|
238
|
+
res.end(JSON.stringify({
|
|
239
|
+
content: [{ type: 'text', text: `[Prod Tools] Tool "${name}" has no default export handler` }],
|
|
240
|
+
isError: true,
|
|
241
|
+
}));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
let result = await handler(args ?? {}, {});
|
|
245
|
+
if (typeof result === 'string') {
|
|
246
|
+
result = { content: [{ type: 'text', text: result }] };
|
|
247
|
+
}
|
|
248
|
+
res.setHeader('Content-Type', 'application/json');
|
|
249
|
+
res.end(JSON.stringify(result));
|
|
250
|
+
} catch (err) {
|
|
251
|
+
res.setHeader('Content-Type', 'application/json');
|
|
252
|
+
res.end(JSON.stringify({
|
|
253
|
+
content: [{ type: 'text', text: `[Prod Tools] Tool error: ${err.message}` }],
|
|
254
|
+
isError: true,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Vite plugin that serves production-built HTML from dist/
|
|
262
|
+
const sunpeakDistPlugin = () => ({
|
|
263
|
+
name: 'sunpeak-dist',
|
|
264
|
+
configureServer(server) {
|
|
265
|
+
server.middlewares.use('/dist', (req, res, next) => {
|
|
266
|
+
const filePath = join(projectRoot, 'dist', req.url);
|
|
267
|
+
if (filePath.endsWith('.html')) {
|
|
268
|
+
if (existsSync(filePath)) {
|
|
269
|
+
res.setHeader('Content-Type', 'text/html');
|
|
270
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
271
|
+
res.end(readFileSync(filePath));
|
|
272
|
+
} else {
|
|
273
|
+
// Return 404 instead of falling through to Vite's SPA fallback,
|
|
274
|
+
// which would serve the simulator's own index.html.
|
|
275
|
+
res.statusCode = 404;
|
|
276
|
+
res.end();
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
next();
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
200
285
|
// Create and start Vite dev server programmatically
|
|
201
286
|
const server = await createServer({
|
|
202
287
|
root: projectRoot,
|
|
203
|
-
plugins: [
|
|
288
|
+
plugins: [
|
|
289
|
+
react(),
|
|
290
|
+
tailwindcss(),
|
|
291
|
+
sunpeakFaviconPlugin(),
|
|
292
|
+
sunpeakCallToolPlugin(),
|
|
293
|
+
sunpeakDistPlugin(),
|
|
294
|
+
],
|
|
295
|
+
define: {
|
|
296
|
+
'__SUNPEAK_PROD_TOOLS__': JSON.stringify(isProdTools),
|
|
297
|
+
'__SUNPEAK_PROD_RESOURCES__': JSON.stringify(isProdResources),
|
|
298
|
+
},
|
|
204
299
|
resolve: {
|
|
205
300
|
alias: {
|
|
206
301
|
'@': path.resolve(projectRoot, 'src'),
|
|
@@ -216,6 +311,23 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
216
311
|
},
|
|
217
312
|
});
|
|
218
313
|
|
|
314
|
+
// --prod-resources: Run initial production build so dist/ is ready before server starts
|
|
315
|
+
if (isProdResources) {
|
|
316
|
+
console.log('Building production resources...');
|
|
317
|
+
const sunpeakBin = join(dirname(new URL(import.meta.url).pathname), '..', 'sunpeak.js');
|
|
318
|
+
const { execSync } = await import('child_process');
|
|
319
|
+
try {
|
|
320
|
+
execSync(`${process.execPath} ${sunpeakBin} build`, {
|
|
321
|
+
cwd: projectRoot,
|
|
322
|
+
stdio: 'inherit',
|
|
323
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
324
|
+
});
|
|
325
|
+
} catch {
|
|
326
|
+
console.error('Build failed. Run `sunpeak build` manually to debug.');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
219
331
|
await server.listen();
|
|
220
332
|
server.printUrls();
|
|
221
333
|
server.bindCLIShortcuts({ print: true });
|
|
@@ -270,19 +382,21 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
270
382
|
logLevel: 'silent',
|
|
271
383
|
});
|
|
272
384
|
|
|
273
|
-
//
|
|
385
|
+
// Build path map for prod-tools handler reloading (re-imports on each call for HMR).
|
|
386
|
+
// Also do an initial load to validate handlers and populate toolHandlerMap for the MCP server.
|
|
387
|
+
const toolPathMap = new Map();
|
|
274
388
|
const toolHandlerMap = new Map();
|
|
275
389
|
for (const [toolName, { tool, path: toolPath }] of toolMap) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.warn(`Warning: Could not load handler for backend-only tool "${toolName}": ${err.message}`);
|
|
390
|
+
void tool; // Used for metadata; handler loaded unconditionally
|
|
391
|
+
const relativePath = path.relative(projectRoot, toolPath);
|
|
392
|
+
toolPathMap.set(toolName, { relativePath });
|
|
393
|
+
try {
|
|
394
|
+
const mod = await toolLoaderServer.ssrLoadModule(`./${relativePath}`);
|
|
395
|
+
if (typeof mod.default === 'function') {
|
|
396
|
+
toolHandlerMap.set(toolName, { handler: mod.default });
|
|
285
397
|
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
console.warn(`Warning: Could not load handler for tool "${toolName}": ${err.message}`);
|
|
286
400
|
}
|
|
287
401
|
}
|
|
288
402
|
|
|
@@ -328,8 +442,10 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
328
442
|
srcPath,
|
|
329
443
|
resource: resourceMap.get(resourceKey),
|
|
330
444
|
} : {}),
|
|
331
|
-
// Attach real handler
|
|
332
|
-
|
|
445
|
+
// Attach real handler for tools consumed by the MCP server.
|
|
446
|
+
// Backend-only tools (no resource) always need handlers for callServerTool.
|
|
447
|
+
// UI tools only get handlers in --prod-tools mode (otherwise simulation mock data is used).
|
|
448
|
+
...((toolHandlerMap.has(toolName) && (!resourceKey || isProdTools)) ? {
|
|
333
449
|
handler: toolHandlerMap.get(toolName).handler,
|
|
334
450
|
} : {}),
|
|
335
451
|
});
|
|
@@ -422,7 +538,7 @@ if (import.meta.hot) {
|
|
|
422
538
|
},
|
|
423
539
|
server: {
|
|
424
540
|
middlewareMode: true,
|
|
425
|
-
hmr: { port: 24679 },
|
|
541
|
+
hmr: { port: Number(process.env.SUNPEAK_HMR_PORT || 24679) },
|
|
426
542
|
allowedHosts: true,
|
|
427
543
|
watch: {
|
|
428
544
|
// Only watch files that affect the UI bundle (not JSON, tests, etc.)
|
|
@@ -449,7 +565,9 @@ if (import.meta.hot) {
|
|
|
449
565
|
version: pkg.version || '0.1.0',
|
|
450
566
|
simulations,
|
|
451
567
|
port: 8000,
|
|
452
|
-
viteServer
|
|
568
|
+
// In --prod-resources mode, don't pass viteServer so the MCP server serves pre-built HTML.
|
|
569
|
+
// Otherwise, pass it so ChatGPT gets Vite HMR.
|
|
570
|
+
viteServer: isProdResources ? undefined : mcpViteServer,
|
|
453
571
|
});
|
|
454
572
|
|
|
455
573
|
// Build production bundles and watch for changes.
|
|
@@ -457,7 +575,7 @@ if (import.meta.hot) {
|
|
|
457
575
|
// reach the local Vite dev server. The watcher rebuilds on source file changes
|
|
458
576
|
// so the prod output stays fresh without manual `sunpeak build`.
|
|
459
577
|
// On successful builds, mcpHandle.invalidateResources() notifies tunnel sessions.
|
|
460
|
-
startBuildWatcher(projectRoot, resourcesDir, mcpHandle);
|
|
578
|
+
startBuildWatcher(projectRoot, resourcesDir, mcpHandle, { skipInitialBuild: isProdResources });
|
|
461
579
|
|
|
462
580
|
// Handle signals - close all servers
|
|
463
581
|
process.on('SIGINT', async () => {
|
package/bin/commands/new.mjs
CHANGED
|
@@ -73,6 +73,7 @@ export const defaultDeps = {
|
|
|
73
73
|
execAsync,
|
|
74
74
|
promptName: defaultPromptName,
|
|
75
75
|
selectResources: defaultSelectResources,
|
|
76
|
+
confirm: clack.confirm,
|
|
76
77
|
intro: clack.intro,
|
|
77
78
|
outro: clack.outro,
|
|
78
79
|
spinner: clack.spinner,
|
|
@@ -191,9 +192,9 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
191
192
|
return false;
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
|
-
// Skip tool files for excluded resources: src/tools/*.ts
|
|
195
|
+
// Skip tool files (and their tests) for excluded resources: src/tools/*.ts
|
|
195
196
|
if (src.includes('/src/tools/') && name.endsWith('.ts')) {
|
|
196
|
-
const baseName = name.replace(/\.ts$/, '');
|
|
197
|
+
const baseName = name.replace(/\.(test\.)?ts$/, '');
|
|
197
198
|
if (baseName === resource || baseName.startsWith(resource + '-') || baseName.endsWith('-' + resource)) {
|
|
198
199
|
return false;
|
|
199
200
|
}
|
|
@@ -266,6 +267,24 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
266
267
|
s.stop(`Install failed. You can try running "${pm} install" manually.`);
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
// Offer to install the sunpeak skill (only in interactive mode)
|
|
271
|
+
if (resourcesArg === undefined) {
|
|
272
|
+
const installSkill = await d.confirm({
|
|
273
|
+
message: 'Install the sunpeak skill? (helps your coding agent build your app)',
|
|
274
|
+
initialValue: true,
|
|
275
|
+
});
|
|
276
|
+
if (!clack.isCancel(installSkill) && installSkill) {
|
|
277
|
+
try {
|
|
278
|
+
d.execSync('npx skills add Sunpeak-AI/sunpeak@create-sunpeak-app', {
|
|
279
|
+
cwd: targetDir,
|
|
280
|
+
stdio: 'inherit',
|
|
281
|
+
});
|
|
282
|
+
} catch {
|
|
283
|
+
d.console.log('Skill install skipped. You can install later with: npx skills add Sunpeak-AI/sunpeak@create-sunpeak-app');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
269
288
|
const runCmd = pm === 'npm' ? 'npm run' : pm;
|
|
270
289
|
|
|
271
290
|
d.outro(`Done! To get started:
|
package/bin/commands/start.mjs
CHANGED
|
@@ -114,7 +114,7 @@ export async function start(projectRoot = process.cwd(), args = []) {
|
|
|
114
114
|
const tools = [];
|
|
115
115
|
|
|
116
116
|
if (existsSync(toolsDir)) {
|
|
117
|
-
const toolFiles = readdirSync(toolsDir).filter(f => f.endsWith('.js'));
|
|
117
|
+
const toolFiles = readdirSync(toolsDir).filter(f => f.endsWith('.js') && !f.endsWith('.test.js'));
|
|
118
118
|
|
|
119
119
|
for (const file of toolFiles) {
|
|
120
120
|
const toolName = file.replace(/\.js$/, '');
|
|
@@ -17,6 +17,8 @@ interface ConversationProps {
|
|
|
17
17
|
* border from flashing at a stale height before the iframe resizes.
|
|
18
18
|
*/
|
|
19
19
|
isTransitioning?: boolean;
|
|
20
|
+
/** Optional action element rendered in the conversation header (e.g., Run button) */
|
|
21
|
+
headerAction?: React.ReactNode;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Conversation layout that renders children (iframe) at a stable tree position.
|
|
@@ -31,5 +33,5 @@ interface ConversationProps {
|
|
|
31
33
|
* - **fullscreen**: content wrapper becomes `position: fixed` covering the viewport;
|
|
32
34
|
* fullscreen chrome (header/footer) rendered as a separate fixed overlay
|
|
33
35
|
*/
|
|
34
|
-
export declare function Conversation({ children, screenWidth, displayMode, platform, onRequestDisplayMode, appName, appIcon, userMessage, isTransitioning, }: ConversationProps): import("react/jsx-runtime").JSX.Element;
|
|
36
|
+
export declare function Conversation({ children, screenWidth, displayMode, platform, onRequestDisplayMode, appName, appIcon, userMessage, isTransitioning, headerAction, }: ConversationProps): import("react/jsx-runtime").JSX.Element;
|
|
35
37
|
export {};
|
package/dist/chatgpt/globals.css
CHANGED
|
@@ -618,6 +618,10 @@
|
|
|
618
618
|
left: 340px;
|
|
619
619
|
}
|
|
620
620
|
|
|
621
|
+
.-z-10 {
|
|
622
|
+
z-index: calc(10 * -1);
|
|
623
|
+
}
|
|
624
|
+
|
|
621
625
|
.z-10 {
|
|
622
626
|
z-index: 10;
|
|
623
627
|
}
|
|
@@ -642,6 +646,18 @@
|
|
|
642
646
|
z-index: 51;
|
|
643
647
|
}
|
|
644
648
|
|
|
649
|
+
.z-\[200\] {
|
|
650
|
+
z-index: 200;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.col-span-3 {
|
|
654
|
+
grid-column: span 3 / span 3;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.col-span-4 {
|
|
658
|
+
grid-column: span 4 / span 4;
|
|
659
|
+
}
|
|
660
|
+
|
|
645
661
|
.container {
|
|
646
662
|
width: 100%;
|
|
647
663
|
}
|
|
@@ -1034,10 +1050,18 @@
|
|
|
1034
1050
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1035
1051
|
}
|
|
1036
1052
|
|
|
1053
|
+
.grid-cols-7 {
|
|
1054
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1037
1057
|
.grid-cols-\[1fr_auto_1fr\] {
|
|
1038
1058
|
grid-template-columns: 1fr auto 1fr;
|
|
1039
1059
|
}
|
|
1040
1060
|
|
|
1061
|
+
.grid-cols-\[2fr_1fr\] {
|
|
1062
|
+
grid-template-columns: 2fr 1fr;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1041
1065
|
.flex-col {
|
|
1042
1066
|
flex-direction: column;
|
|
1043
1067
|
}
|
|
@@ -1460,6 +1484,10 @@
|
|
|
1460
1484
|
padding: calc(var(--spacing) * 5);
|
|
1461
1485
|
}
|
|
1462
1486
|
|
|
1487
|
+
.p-8 {
|
|
1488
|
+
padding: calc(var(--spacing) * 8);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1463
1491
|
.p-\[1px\] {
|
|
1464
1492
|
padding: 1px;
|
|
1465
1493
|
}
|
|
@@ -1520,10 +1548,6 @@
|
|
|
1520
1548
|
padding-block: calc(var(--spacing) * 4);
|
|
1521
1549
|
}
|
|
1522
1550
|
|
|
1523
|
-
.py-8 {
|
|
1524
|
-
padding-block: calc(var(--spacing) * 8);
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
1551
|
.pt-0 {
|
|
1528
1552
|
padding-top: calc(var(--spacing) * 0);
|
|
1529
1553
|
}
|
|
@@ -1552,10 +1576,6 @@
|
|
|
1552
1576
|
padding-bottom: calc(var(--spacing) * 10);
|
|
1553
1577
|
}
|
|
1554
1578
|
|
|
1555
|
-
.pl-4 {
|
|
1556
|
-
padding-left: calc(var(--spacing) * 4);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
1579
|
.text-center {
|
|
1560
1580
|
text-align: center;
|
|
1561
1581
|
}
|
|
@@ -1710,6 +1730,10 @@
|
|
|
1710
1730
|
text-transform: uppercase;
|
|
1711
1731
|
}
|
|
1712
1732
|
|
|
1733
|
+
.no-underline {
|
|
1734
|
+
text-decoration-line: none;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1713
1737
|
.antialiased {
|
|
1714
1738
|
-webkit-font-smoothing: antialiased;
|
|
1715
1739
|
-moz-osx-font-smoothing: grayscale;
|
|
@@ -1774,6 +1798,11 @@
|
|
|
1774
1798
|
outline-width: 1px;
|
|
1775
1799
|
}
|
|
1776
1800
|
|
|
1801
|
+
.blur {
|
|
1802
|
+
--tw-blur: blur(8px);
|
|
1803
|
+
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1777
1806
|
.filter {
|
|
1778
1807
|
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
|
|
1779
1808
|
}
|
package/dist/chatgpt/index.cjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const simulator = require("../simulator-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const discovery = require("../discovery-SviNiBkF.cjs");
|
|
3
|
+
const simulator = require("../simulator-mbqealxY.cjs");
|
|
4
|
+
const simulatorUrl = require("../simulator-url-DcSYRl-P.cjs");
|
|
5
|
+
const discovery = require("../discovery-D1gpaVz4.cjs");
|
|
7
6
|
exports.IframeResource = simulator.IframeResource;
|
|
8
7
|
exports.McpAppHost = simulator.McpAppHost;
|
|
9
8
|
exports.SCREEN_WIDTHS = simulator.SCREEN_WIDTHS;
|
|
@@ -12,7 +11,6 @@ exports.ThemeProvider = simulator.ThemeProvider;
|
|
|
12
11
|
exports.extractResourceCSP = simulator.extractResourceCSP;
|
|
13
12
|
exports.resolveServerToolResult = simulator.resolveServerToolResult;
|
|
14
13
|
exports.useThemeContext = simulator.useThemeContext;
|
|
15
|
-
exports.ChatGPTSimulator = chatgpt_index.ChatGPTSimulator;
|
|
16
14
|
exports.createSimulatorUrl = simulatorUrl.createSimulatorUrl;
|
|
17
15
|
exports.buildDevSimulations = discovery.buildDevSimulations;
|
|
18
16
|
exports.buildResourceMap = discovery.buildResourceMap;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/chatgpt/index.d.ts
CHANGED
package/dist/chatgpt/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { I, M, a, S, T, j, m, n } from "../simulator-
|
|
2
|
-
import {
|
|
3
|
-
import { c } from "../
|
|
4
|
-
import { b, a as a2, c as c2, d, e, f, g, h, i, t } from "../discovery-DvIQWTez.js";
|
|
1
|
+
import { I, M, a, S, T, j, m, n } from "../simulator-DfTsXTK4.js";
|
|
2
|
+
import { c } from "../simulator-url-j_XV3EoP.js";
|
|
3
|
+
import { b, a as a2, c as c2, d, e, f, g, h, i, t } from "../discovery-BVqD-JsT.js";
|
|
5
4
|
export {
|
|
6
|
-
C as ChatGPTSimulator,
|
|
7
5
|
I as IframeResource,
|
|
8
6
|
M as McpAppHost,
|
|
9
7
|
a as SCREEN_WIDTHS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
|
|
@@ -12,6 +12,8 @@ interface ClaudeConversationProps {
|
|
|
12
12
|
appIcon?: string;
|
|
13
13
|
userMessage?: string;
|
|
14
14
|
isTransitioning?: boolean;
|
|
15
|
+
/** Optional action element rendered in the conversation header (e.g., Run button) */
|
|
16
|
+
headerAction?: React.ReactNode;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Claude conversation shell — mimics Claude's chat UI chrome.
|
|
@@ -19,5 +21,5 @@ interface ClaudeConversationProps {
|
|
|
19
21
|
* All three display modes (inline, pip, fullscreen) share the same React tree
|
|
20
22
|
* shape so that the iframe never unmounts when switching modes.
|
|
21
23
|
*/
|
|
22
|
-
export declare function ClaudeConversation({ children, screenWidth, displayMode, platform, onRequestDisplayMode, appName, appIcon, userMessage, isTransitioning, }: ClaudeConversationProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export declare function ClaudeConversation({ children, screenWidth, displayMode, platform, onRequestDisplayMode, appName, appIcon, userMessage, isTransitioning, headerAction, }: ClaudeConversationProps): import("react/jsx-runtime").JSX.Element;
|
|
23
25
|
export {};
|
package/dist/claude/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const simulator = require("../simulator-
|
|
4
|
-
exports.
|
|
3
|
+
const simulator = require("../simulator-mbqealxY.cjs");
|
|
4
|
+
exports.Simulator = simulator.Simulator;
|
|
5
5
|
//# sourceMappingURL=index.cjs.map
|
package/dist/claude/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Simulator
|
|
1
|
+
export { Simulator } from '../simulator/simulator';
|
package/dist/claude/index.js
CHANGED
|
@@ -190,7 +190,9 @@ function findToolFiles(toolsDir, fs) {
|
|
|
190
190
|
return [];
|
|
191
191
|
}
|
|
192
192
|
const entries = fs.readdirSync(toolsDir, { withFileTypes: true });
|
|
193
|
-
return entries.filter(
|
|
193
|
+
return entries.filter(
|
|
194
|
+
(entry) => !entry.isDirectory() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")
|
|
195
|
+
).map((entry) => ({
|
|
194
196
|
name: entry.name.replace(/\.ts$/, ""),
|
|
195
197
|
path: `${toolsDir}/${entry.name}`
|
|
196
198
|
}));
|
|
@@ -219,4 +221,4 @@ export {
|
|
|
219
221
|
findToolFiles as k,
|
|
220
222
|
toPascalCase as t
|
|
221
223
|
};
|
|
222
|
-
//# sourceMappingURL=discovery-
|
|
224
|
+
//# sourceMappingURL=discovery-BVqD-JsT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery-DvIQWTez.js","sources":["../src/lib/discovery.ts"],"sourcesContent":["/**\n * Discovery utilities for auto-discovering resources and simulations\n *\n * These helpers process the results of import.meta.glob() calls to extract\n * keys, build component maps, and connect simulations to resources.\n *\n * The glob calls themselves must remain in the template (Vite compile-time),\n * but all the processing logic lives here for easy updates across templates.\n *\n * Node.js utilities (findResourceDirs, findToolFiles, etc.) can be used\n * by CLI commands for build-time and runtime discovery.\n */\n\nimport type { Simulation } from '../types/simulation.js';\n\n/**\n * Convert a kebab-case string to PascalCase\n * @example toPascalCase('review') // 'Review'\n * @example toPascalCase('album-art') // 'AlbumArt'\n */\nexport function toPascalCase(str: string): string {\n return str\n .split('-')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n}\n\n/**\n * Extract the resource key from a resource file path.\n * Matches {name}.tsx (e.g., './albums/albums.tsx' → 'albums')\n */\nexport function extractResourceKey(path: string): string | undefined {\n const match = path.match(/([^/]+)\\.tsx$/);\n return match ? match[1] : undefined;\n}\n\n/**\n * Extract the simulation key from a simulation file path.\n * Matches any *.json file (e.g., './show-albums.json' → 'show-albums')\n */\nexport function extractSimulationKey(path: string): string | undefined {\n const match = path.match(/([^/]+)\\.json$/);\n return match ? match[1] : undefined;\n}\n\n/**\n * Find the best matching resource key for a simulation key.\n * Matches the longest resource name that is a prefix of the simulation key.\n * @example findResourceKey('review-diff', ['review', 'carousel']) // 'review'\n * @example findResourceKey('albums', ['albums', 'review']) // 'albums'\n */\nexport function findResourceKey(simulationKey: string, resourceKeys: string[]): string | undefined {\n // Sort by length descending to find longest match first\n const sorted = [...resourceKeys].sort((a, b) => b.length - a.length);\n for (const resourceKey of sorted) {\n if (simulationKey === resourceKey || simulationKey.startsWith(resourceKey + '-')) {\n return resourceKey;\n }\n }\n return undefined;\n}\n\n/**\n * Get the expected component export name for a resource\n * @example getComponentName('review') // 'ReviewResource'\n * @example getComponentName('album-art') // 'AlbumArtResource'\n */\nexport function getComponentName(resourceKey: string): string {\n return `${toPascalCase(resourceKey)}Resource`;\n}\n\n// --- Glob processing helpers ---\n\ntype GlobModules = Record<string, unknown>;\n\n/**\n * Process resource component modules from import.meta.glob() result.\n * Extracts components and exports them with PascalCase names.\n *\n * @example\n * const modules = import.meta.glob('./*\\/*.tsx', { eager: true });\n * export default createResourceExports(modules);\n */\nexport function createResourceExports(modules: GlobModules): Record<string, React.ComponentType> {\n const resources: Record<string, React.ComponentType> = {};\n\n for (const [path, module] of Object.entries(modules)) {\n const key = extractResourceKey(path);\n if (!key) continue;\n\n const exportName = getComponentName(key);\n const mod = module as Record<string, unknown>;\n\n // Try default export first, then named export matching the expected name\n const component = mod.default ?? mod[exportName];\n\n // Accept functions (regular components) or objects (forwardRef/memo components)\n if (component && (typeof component === 'function' || typeof component === 'object')) {\n resources[exportName] = component as React.ComponentType;\n }\n }\n\n return resources;\n}\n\n/**\n * Build a resource metadata map from import.meta.glob() result.\n * Used for connecting simulations to their resource definitions.\n *\n * @example\n * const modules = import.meta.glob('../src/resources/*\\/*.tsx', { eager: true });\n * const resourcesMap = buildResourceMap(modules);\n */\nexport function buildResourceMap<T>(modules: GlobModules): Map<string, T> {\n const map = new Map<string, T>();\n\n for (const [path, module] of Object.entries(modules)) {\n const key = extractResourceKey(path);\n if (key) {\n map.set(key, (module as { resource: T }).resource);\n }\n }\n\n return map;\n}\n\n/**\n * Options for building simulations from discovered modules\n */\nexport interface BuildSimulationsOptions<TResource, TSimulation> {\n /** Glob result of simulation JSON files */\n simulationModules: GlobModules;\n /** Map of resource key -> resource metadata */\n resourcesMap: Map<string, TResource>;\n /** Map of component name -> React component */\n resourceComponents: Record<string, React.ComponentType>;\n /** Create the final simulation object */\n createSimulation: (\n simulationKey: string,\n simulationData: unknown,\n resource: TResource,\n resourceComponent: React.ComponentType\n ) => TSimulation;\n /** Optional warning handler for missing resources */\n onMissingResource?: (simulationKey: string, expectedPrefix: string) => void;\n}\n\n/**\n * Build simulations by connecting simulation data with resources and components.\n * This is the main orchestration function for dev server bootstrap.\n */\nexport function buildSimulations<TResource, TSimulation>(\n options: BuildSimulationsOptions<TResource, TSimulation>\n): Record<string, TSimulation> {\n const {\n simulationModules,\n resourcesMap,\n resourceComponents,\n createSimulation,\n onMissingResource = (key, prefix) =>\n console.warn(\n `No matching resource found for simulation \"${key}\". ` +\n `Expected a resource file like src/resources/${prefix}/${prefix}.tsx`\n ),\n } = options;\n\n const resourceKeys = Array.from(resourcesMap.keys());\n const simulations: Record<string, TSimulation> = {};\n\n for (const [path, module] of Object.entries(simulationModules)) {\n const simulationKey = extractSimulationKey(path);\n if (!simulationKey) continue;\n\n const simulationData = (module as { default: unknown }).default;\n\n // Find matching resource\n const resourceKey = findResourceKey(simulationKey, resourceKeys);\n if (!resourceKey) {\n onMissingResource(simulationKey, simulationKey.split('-')[0]);\n continue;\n }\n\n const resource = resourcesMap.get(resourceKey)!;\n\n // Get component\n const componentName = getComponentName(resourceKey);\n const resourceComponent = resourceComponents[componentName];\n\n if (!resourceComponent) {\n console.warn(\n `Resource component \"${componentName}\" not found for resource \"${resourceKey}\". ` +\n `Make sure src/resources/${resourceKey}/${resourceKey}.tsx exists with a default export.`\n );\n continue;\n }\n\n simulations[simulationKey] = createSimulation(\n simulationKey,\n simulationData,\n resource,\n resourceComponent\n );\n }\n\n return simulations;\n}\n\n// --- Dev server helpers ---\n\n/**\n * Resource metadata from resource .tsx files\n */\nexport interface ResourceMetadata {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * Options for building dev simulations\n */\nexport interface BuildDevSimulationsOptions {\n /** Glob result of simulation JSON files */\n simulationModules: GlobModules;\n /** Resource components map from src/resources/index.ts */\n resourceComponents: Record<string, React.ComponentType>;\n /** Glob result of tool files: import.meta.glob('src/tools/*.ts', { eager: true }) */\n toolModules: GlobModules;\n /** Glob result of resource .tsx files from src/resources/ */\n resourceModules: GlobModules;\n}\n\n/**\n * Tool metadata extracted from a tool module's `tool` export\n */\ninterface ToolModuleInfo {\n tool: Record<string, unknown>;\n /** Resource name string from tool.resource (undefined for tools without UI) */\n resourceName?: string;\n}\n\n/**\n * Build simulations for the dev server from glob results.\n * Simulation JSON has `\"tool\": \"tool-name\"` string referencing a tool file.\n * Tool files have `resource: 'name'` linking to a resource discovered from resourceModules.\n */\nexport function buildDevSimulations(\n options: BuildDevSimulationsOptions\n): Record<string, Simulation> {\n const { simulationModules, resourceComponents, toolModules, resourceModules } = options;\n\n // Build resource metadata map from resource modules (keyed by resource name)\n const resourceMetaByName = new Map<string, ResourceMetadata>();\n const resourceKeyByName = new Map<string, string>();\n for (const [path, module] of Object.entries(resourceModules)) {\n const key = extractResourceKey(path);\n if (!key) continue;\n const mod = module as { resource?: ResourceMetadata };\n if (mod.resource) {\n // Use explicit name if provided, otherwise derive from directory key\n const name = mod.resource.name ?? key;\n resourceMetaByName.set(name, { ...mod.resource, name });\n resourceKeyByName.set(name, key);\n }\n }\n\n // Build tool map from tool modules\n const toolsMap = new Map<string, ToolModuleInfo>();\n if (toolModules) {\n for (const [path, module] of Object.entries(toolModules)) {\n const nameMatch = path.match(/([^/]+)\\.ts$/);\n if (!nameMatch) continue;\n const mod = module as { tool?: Record<string, unknown> };\n if (mod.tool) {\n const resourceName = mod.tool.resource as string | undefined;\n toolsMap.set(nameMatch[1], { tool: mod.tool, resourceName });\n }\n }\n }\n\n const simulations: Record<string, Simulation> = {};\n\n for (const [path, module] of Object.entries(simulationModules)) {\n const simKey = extractSimulationKey(path);\n if (!simKey) continue;\n\n const simulationData = (module as { default: Record<string, unknown> }).default;\n\n const toolName =\n typeof simulationData.tool === 'string' ? (simulationData.tool as string) : simKey;\n const toolInfo = toolsMap.get(toolName);\n if (!toolInfo) {\n console.warn(\n `Tool \"${toolName}\" not found for simulation \"${simKey}\". ` +\n `Make sure src/tools/${toolName}.ts exists.`\n );\n continue;\n }\n\n // Look up resource metadata by name (if tool has a UI)\n const resourceMeta = toolInfo.resourceName\n ? resourceMetaByName.get(toolInfo.resourceName)\n : undefined;\n const resourceKey = toolInfo.resourceName\n ? resourceKeyByName.get(toolInfo.resourceName)\n : undefined;\n\n if (toolInfo.resourceName && (!resourceMeta || !resourceKey)) {\n console.warn(\n `Resource \"${toolInfo.resourceName}\" not found for tool \"${toolName}\". ` +\n `Make sure a resource with name \"${toolInfo.resourceName}\" exists in src/resources/.`\n );\n continue;\n }\n\n // Build resource block only for UI tools\n let resourceBlock: Pick<Simulation, 'resource' | 'resourceUrl'> = {};\n if (resourceKey && resourceMeta) {\n const componentName = getComponentName(resourceKey);\n const resourceComponent = resourceComponents[componentName];\n\n if (!resourceComponent) {\n console.warn(`Resource component \"${componentName}\" not found for tool \"${toolName}\".`);\n continue;\n }\n\n resourceBlock = {\n resource: {\n uri: `ui://${resourceKey}`,\n name: resourceKey,\n ...(resourceMeta.title != null ? { title: resourceMeta.title as string } : {}),\n ...(resourceMeta.description != null\n ? { description: resourceMeta.description as string }\n : {}),\n ...(resourceMeta.mimeType != null ? { mimeType: resourceMeta.mimeType as string } : {}),\n ...(resourceMeta._meta != null\n ? { _meta: resourceMeta._meta as Record<string, unknown> }\n : {}),\n },\n resourceUrl: `/.sunpeak/resource-loader.html?component=${componentName}`,\n };\n }\n\n simulations[simKey] = {\n name: simKey,\n userMessage: simulationData.userMessage as string | undefined,\n tool: {\n name: toolName,\n description: (toolInfo.tool.description as string) ?? '',\n inputSchema: { type: 'object' as const },\n ...(toolInfo.tool.title != null ? { title: toolInfo.tool.title as string } : {}),\n ...(toolInfo.tool.annotations != null\n ? { annotations: toolInfo.tool.annotations as Record<string, unknown> }\n : {}),\n ...(toolInfo.tool._meta != null\n ? { _meta: toolInfo.tool._meta as Record<string, unknown> }\n : {}),\n },\n ...resourceBlock,\n toolInput: simulationData.toolInput as Record<string, unknown> | undefined,\n toolResult: simulationData.toolResult as Simulation['toolResult'],\n serverTools: simulationData.serverTools as Simulation['serverTools'],\n };\n }\n\n return simulations;\n}\n\n// --- Node.js utilities for CLI commands ---\n// These utilities use standard Node.js APIs and can be imported by build/push/mcp commands.\n\n/**\n * Information about a discovered resource directory\n */\nexport interface ResourceDirInfo {\n /** Resource key (directory name), e.g., 'albums', 'carousel' */\n key: string;\n /** Full path to the resource directory */\n dir: string;\n /** Full path to the main resource file (tsx or json depending on context) */\n resourcePath: string;\n}\n\n/**\n * File system operations interface for dependency injection in tests\n */\nexport interface FsOps {\n readdirSync: (\n path: string,\n options: { withFileTypes: true }\n ) => Array<{ name: string; isDirectory: () => boolean }>;\n existsSync: (path: string) => boolean;\n}\n\n/**\n * Find all resource directories in a base directory.\n * Each valid resource directory contains a file matching the expected pattern.\n *\n * @param baseDir - Base directory to scan (e.g., 'src/resources' or 'dist')\n * @param filePattern - Function to generate expected filename from resource key\n * @param fs - File system operations (for testing)\n *\n * @example\n * // Find source resources (tsx files)\n * const resources = findResourceDirs('src/resources', key => `${key}.tsx`);\n *\n * @example\n * // Find built resources (js files)\n * const resources = findResourceDirs('dist', key => `${key}.js`);\n */\nexport function findResourceDirs(\n baseDir: string,\n filePattern: (key: string) => string,\n fs: FsOps\n): ResourceDirInfo[] {\n if (!fs.existsSync(baseDir)) {\n return [];\n }\n\n const entries = fs.readdirSync(baseDir, { withFileTypes: true });\n\n return entries\n .filter((entry) => entry.isDirectory())\n .map((entry) => {\n const key = entry.name;\n const dir = `${baseDir}/${key}`;\n const resourcePath = `${dir}/${filePattern(key)}`;\n\n if (!fs.existsSync(resourcePath)) {\n return null;\n }\n\n return { key, dir, resourcePath };\n })\n .filter((info): info is ResourceDirInfo => info !== null);\n}\n\n// --- Tool files + flat simulations discovery ---\n\n/**\n * Information about a discovered tool file\n */\nexport interface ToolFileInfo {\n /** Tool name derived from filename (e.g., 'show-albums') */\n name: string;\n /** Full path to the tool file */\n path: string;\n}\n\n/**\n * Find all tool files in a tools directory.\n * Matches *.ts files directly in the directory (not recursive).\n *\n * @example\n * findToolFiles('src/tools', fs)\n * // [{ name: 'show-albums', path: 'src/tools/show-albums.ts' }]\n */\nexport function findToolFiles(\n toolsDir: string,\n fs: Pick<FsOps, 'readdirSync' | 'existsSync'>\n): ToolFileInfo[] {\n if (!fs.existsSync(toolsDir)) {\n return [];\n }\n\n const entries = fs.readdirSync(toolsDir, { withFileTypes: true });\n\n return entries\n .filter((entry) => !entry.isDirectory() && entry.name.endsWith('.ts'))\n .map((entry) => ({\n name: entry.name.replace(/\\.ts$/, ''),\n path: `${toolsDir}/${entry.name}`,\n }));\n}\n\n/**\n * Information about a discovered simulation file (flat convention)\n */\nexport interface SimulationFileInfo {\n /** Filename without extension (e.g., 'show-albums') */\n name: string;\n /** Full path to the simulation file */\n path: string;\n}\n\n/**\n * Find all simulation JSON files in a flat simulations directory.\n * Matches any *.json file directly in the directory.\n *\n * @example\n * findSimulationFilesFlat('tests/simulations', fs)\n * // [{ name: 'show-albums', path: 'tests/simulations/show-albums.json' }]\n */\nexport function findSimulationFilesFlat(\n simulationsDir: string,\n fs: Pick<FsOps, 'readdirSync' | 'existsSync'>\n): SimulationFileInfo[] {\n if (!fs.existsSync(simulationsDir)) {\n return [];\n }\n\n const entries = fs.readdirSync(simulationsDir, { withFileTypes: true });\n\n return entries\n .filter((entry) => !entry.isDirectory() && entry.name.endsWith('.json'))\n .map((entry) => ({\n name: entry.name.replace(/\\.json$/, ''),\n path: `${simulationsDir}/${entry.name}`,\n }));\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"discovery-BVqD-JsT.js","sources":["../src/lib/discovery.ts"],"sourcesContent":["/**\n * Discovery utilities for auto-discovering resources and simulations\n *\n * These helpers process the results of import.meta.glob() calls to extract\n * keys, build component maps, and connect simulations to resources.\n *\n * The glob calls themselves must remain in the template (Vite compile-time),\n * but all the processing logic lives here for easy updates across templates.\n *\n * Node.js utilities (findResourceDirs, findToolFiles, etc.) can be used\n * by CLI commands for build-time and runtime discovery.\n */\n\nimport type { Simulation } from '../types/simulation.js';\n\n/**\n * Convert a kebab-case string to PascalCase\n * @example toPascalCase('review') // 'Review'\n * @example toPascalCase('album-art') // 'AlbumArt'\n */\nexport function toPascalCase(str: string): string {\n return str\n .split('-')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n}\n\n/**\n * Extract the resource key from a resource file path.\n * Matches {name}.tsx (e.g., './albums/albums.tsx' → 'albums')\n */\nexport function extractResourceKey(path: string): string | undefined {\n const match = path.match(/([^/]+)\\.tsx$/);\n return match ? match[1] : undefined;\n}\n\n/**\n * Extract the simulation key from a simulation file path.\n * Matches any *.json file (e.g., './show-albums.json' → 'show-albums')\n */\nexport function extractSimulationKey(path: string): string | undefined {\n const match = path.match(/([^/]+)\\.json$/);\n return match ? match[1] : undefined;\n}\n\n/**\n * Find the best matching resource key for a simulation key.\n * Matches the longest resource name that is a prefix of the simulation key.\n * @example findResourceKey('review-diff', ['review', 'carousel']) // 'review'\n * @example findResourceKey('albums', ['albums', 'review']) // 'albums'\n */\nexport function findResourceKey(simulationKey: string, resourceKeys: string[]): string | undefined {\n // Sort by length descending to find longest match first\n const sorted = [...resourceKeys].sort((a, b) => b.length - a.length);\n for (const resourceKey of sorted) {\n if (simulationKey === resourceKey || simulationKey.startsWith(resourceKey + '-')) {\n return resourceKey;\n }\n }\n return undefined;\n}\n\n/**\n * Get the expected component export name for a resource\n * @example getComponentName('review') // 'ReviewResource'\n * @example getComponentName('album-art') // 'AlbumArtResource'\n */\nexport function getComponentName(resourceKey: string): string {\n return `${toPascalCase(resourceKey)}Resource`;\n}\n\n// --- Glob processing helpers ---\n\ntype GlobModules = Record<string, unknown>;\n\n/**\n * Process resource component modules from import.meta.glob() result.\n * Extracts components and exports them with PascalCase names.\n *\n * @example\n * const modules = import.meta.glob('./*\\/*.tsx', { eager: true });\n * export default createResourceExports(modules);\n */\nexport function createResourceExports(modules: GlobModules): Record<string, React.ComponentType> {\n const resources: Record<string, React.ComponentType> = {};\n\n for (const [path, module] of Object.entries(modules)) {\n const key = extractResourceKey(path);\n if (!key) continue;\n\n const exportName = getComponentName(key);\n const mod = module as Record<string, unknown>;\n\n // Try default export first, then named export matching the expected name\n const component = mod.default ?? mod[exportName];\n\n // Accept functions (regular components) or objects (forwardRef/memo components)\n if (component && (typeof component === 'function' || typeof component === 'object')) {\n resources[exportName] = component as React.ComponentType;\n }\n }\n\n return resources;\n}\n\n/**\n * Build a resource metadata map from import.meta.glob() result.\n * Used for connecting simulations to their resource definitions.\n *\n * @example\n * const modules = import.meta.glob('../src/resources/*\\/*.tsx', { eager: true });\n * const resourcesMap = buildResourceMap(modules);\n */\nexport function buildResourceMap<T>(modules: GlobModules): Map<string, T> {\n const map = new Map<string, T>();\n\n for (const [path, module] of Object.entries(modules)) {\n const key = extractResourceKey(path);\n if (key) {\n map.set(key, (module as { resource: T }).resource);\n }\n }\n\n return map;\n}\n\n/**\n * Options for building simulations from discovered modules\n */\nexport interface BuildSimulationsOptions<TResource, TSimulation> {\n /** Glob result of simulation JSON files */\n simulationModules: GlobModules;\n /** Map of resource key -> resource metadata */\n resourcesMap: Map<string, TResource>;\n /** Map of component name -> React component */\n resourceComponents: Record<string, React.ComponentType>;\n /** Create the final simulation object */\n createSimulation: (\n simulationKey: string,\n simulationData: unknown,\n resource: TResource,\n resourceComponent: React.ComponentType\n ) => TSimulation;\n /** Optional warning handler for missing resources */\n onMissingResource?: (simulationKey: string, expectedPrefix: string) => void;\n}\n\n/**\n * Build simulations by connecting simulation data with resources and components.\n * This is the main orchestration function for dev server bootstrap.\n */\nexport function buildSimulations<TResource, TSimulation>(\n options: BuildSimulationsOptions<TResource, TSimulation>\n): Record<string, TSimulation> {\n const {\n simulationModules,\n resourcesMap,\n resourceComponents,\n createSimulation,\n onMissingResource = (key, prefix) =>\n console.warn(\n `No matching resource found for simulation \"${key}\". ` +\n `Expected a resource file like src/resources/${prefix}/${prefix}.tsx`\n ),\n } = options;\n\n const resourceKeys = Array.from(resourcesMap.keys());\n const simulations: Record<string, TSimulation> = {};\n\n for (const [path, module] of Object.entries(simulationModules)) {\n const simulationKey = extractSimulationKey(path);\n if (!simulationKey) continue;\n\n const simulationData = (module as { default: unknown }).default;\n\n // Find matching resource\n const resourceKey = findResourceKey(simulationKey, resourceKeys);\n if (!resourceKey) {\n onMissingResource(simulationKey, simulationKey.split('-')[0]);\n continue;\n }\n\n const resource = resourcesMap.get(resourceKey)!;\n\n // Get component\n const componentName = getComponentName(resourceKey);\n const resourceComponent = resourceComponents[componentName];\n\n if (!resourceComponent) {\n console.warn(\n `Resource component \"${componentName}\" not found for resource \"${resourceKey}\". ` +\n `Make sure src/resources/${resourceKey}/${resourceKey}.tsx exists with a default export.`\n );\n continue;\n }\n\n simulations[simulationKey] = createSimulation(\n simulationKey,\n simulationData,\n resource,\n resourceComponent\n );\n }\n\n return simulations;\n}\n\n// --- Dev server helpers ---\n\n/**\n * Resource metadata from resource .tsx files\n */\nexport interface ResourceMetadata {\n name: string;\n [key: string]: unknown;\n}\n\n/**\n * Options for building dev simulations\n */\nexport interface BuildDevSimulationsOptions {\n /** Glob result of simulation JSON files */\n simulationModules: GlobModules;\n /** Resource components map from src/resources/index.ts */\n resourceComponents: Record<string, React.ComponentType>;\n /** Glob result of tool files: import.meta.glob('src/tools/*.ts', { eager: true }) */\n toolModules: GlobModules;\n /** Glob result of resource .tsx files from src/resources/ */\n resourceModules: GlobModules;\n}\n\n/**\n * Tool metadata extracted from a tool module's `tool` export\n */\ninterface ToolModuleInfo {\n tool: Record<string, unknown>;\n /** Resource name string from tool.resource (undefined for tools without UI) */\n resourceName?: string;\n}\n\n/**\n * Build simulations for the dev server from glob results.\n * Simulation JSON has `\"tool\": \"tool-name\"` string referencing a tool file.\n * Tool files have `resource: 'name'` linking to a resource discovered from resourceModules.\n */\nexport function buildDevSimulations(\n options: BuildDevSimulationsOptions\n): Record<string, Simulation> {\n const { simulationModules, resourceComponents, toolModules, resourceModules } = options;\n\n // Build resource metadata map from resource modules (keyed by resource name)\n const resourceMetaByName = new Map<string, ResourceMetadata>();\n const resourceKeyByName = new Map<string, string>();\n for (const [path, module] of Object.entries(resourceModules)) {\n const key = extractResourceKey(path);\n if (!key) continue;\n const mod = module as { resource?: ResourceMetadata };\n if (mod.resource) {\n // Use explicit name if provided, otherwise derive from directory key\n const name = mod.resource.name ?? key;\n resourceMetaByName.set(name, { ...mod.resource, name });\n resourceKeyByName.set(name, key);\n }\n }\n\n // Build tool map from tool modules\n const toolsMap = new Map<string, ToolModuleInfo>();\n if (toolModules) {\n for (const [path, module] of Object.entries(toolModules)) {\n const nameMatch = path.match(/([^/]+)\\.ts$/);\n if (!nameMatch) continue;\n const mod = module as { tool?: Record<string, unknown> };\n if (mod.tool) {\n const resourceName = mod.tool.resource as string | undefined;\n toolsMap.set(nameMatch[1], { tool: mod.tool, resourceName });\n }\n }\n }\n\n const simulations: Record<string, Simulation> = {};\n\n for (const [path, module] of Object.entries(simulationModules)) {\n const simKey = extractSimulationKey(path);\n if (!simKey) continue;\n\n const simulationData = (module as { default: Record<string, unknown> }).default;\n\n const toolName =\n typeof simulationData.tool === 'string' ? (simulationData.tool as string) : simKey;\n const toolInfo = toolsMap.get(toolName);\n if (!toolInfo) {\n console.warn(\n `Tool \"${toolName}\" not found for simulation \"${simKey}\". ` +\n `Make sure src/tools/${toolName}.ts exists.`\n );\n continue;\n }\n\n // Look up resource metadata by name (if tool has a UI)\n const resourceMeta = toolInfo.resourceName\n ? resourceMetaByName.get(toolInfo.resourceName)\n : undefined;\n const resourceKey = toolInfo.resourceName\n ? resourceKeyByName.get(toolInfo.resourceName)\n : undefined;\n\n if (toolInfo.resourceName && (!resourceMeta || !resourceKey)) {\n console.warn(\n `Resource \"${toolInfo.resourceName}\" not found for tool \"${toolName}\". ` +\n `Make sure a resource with name \"${toolInfo.resourceName}\" exists in src/resources/.`\n );\n continue;\n }\n\n // Build resource block only for UI tools\n let resourceBlock: Pick<Simulation, 'resource' | 'resourceUrl'> = {};\n if (resourceKey && resourceMeta) {\n const componentName = getComponentName(resourceKey);\n const resourceComponent = resourceComponents[componentName];\n\n if (!resourceComponent) {\n console.warn(`Resource component \"${componentName}\" not found for tool \"${toolName}\".`);\n continue;\n }\n\n resourceBlock = {\n resource: {\n uri: `ui://${resourceKey}`,\n name: resourceKey,\n ...(resourceMeta.title != null ? { title: resourceMeta.title as string } : {}),\n ...(resourceMeta.description != null\n ? { description: resourceMeta.description as string }\n : {}),\n ...(resourceMeta.mimeType != null ? { mimeType: resourceMeta.mimeType as string } : {}),\n ...(resourceMeta._meta != null\n ? { _meta: resourceMeta._meta as Record<string, unknown> }\n : {}),\n },\n resourceUrl: `/.sunpeak/resource-loader.html?component=${componentName}`,\n };\n }\n\n simulations[simKey] = {\n name: simKey,\n userMessage: simulationData.userMessage as string | undefined,\n tool: {\n name: toolName,\n description: (toolInfo.tool.description as string) ?? '',\n inputSchema: { type: 'object' as const },\n ...(toolInfo.tool.title != null ? { title: toolInfo.tool.title as string } : {}),\n ...(toolInfo.tool.annotations != null\n ? { annotations: toolInfo.tool.annotations as Record<string, unknown> }\n : {}),\n ...(toolInfo.tool._meta != null\n ? { _meta: toolInfo.tool._meta as Record<string, unknown> }\n : {}),\n },\n ...resourceBlock,\n toolInput: simulationData.toolInput as Record<string, unknown> | undefined,\n toolResult: simulationData.toolResult as Simulation['toolResult'],\n serverTools: simulationData.serverTools as Simulation['serverTools'],\n };\n }\n\n return simulations;\n}\n\n// --- Node.js utilities for CLI commands ---\n// These utilities use standard Node.js APIs and can be imported by build/push/mcp commands.\n\n/**\n * Information about a discovered resource directory\n */\nexport interface ResourceDirInfo {\n /** Resource key (directory name), e.g., 'albums', 'carousel' */\n key: string;\n /** Full path to the resource directory */\n dir: string;\n /** Full path to the main resource file (tsx or json depending on context) */\n resourcePath: string;\n}\n\n/**\n * File system operations interface for dependency injection in tests\n */\nexport interface FsOps {\n readdirSync: (\n path: string,\n options: { withFileTypes: true }\n ) => Array<{ name: string; isDirectory: () => boolean }>;\n existsSync: (path: string) => boolean;\n}\n\n/**\n * Find all resource directories in a base directory.\n * Each valid resource directory contains a file matching the expected pattern.\n *\n * @param baseDir - Base directory to scan (e.g., 'src/resources' or 'dist')\n * @param filePattern - Function to generate expected filename from resource key\n * @param fs - File system operations (for testing)\n *\n * @example\n * // Find source resources (tsx files)\n * const resources = findResourceDirs('src/resources', key => `${key}.tsx`);\n *\n * @example\n * // Find built resources (js files)\n * const resources = findResourceDirs('dist', key => `${key}.js`);\n */\nexport function findResourceDirs(\n baseDir: string,\n filePattern: (key: string) => string,\n fs: FsOps\n): ResourceDirInfo[] {\n if (!fs.existsSync(baseDir)) {\n return [];\n }\n\n const entries = fs.readdirSync(baseDir, { withFileTypes: true });\n\n return entries\n .filter((entry) => entry.isDirectory())\n .map((entry) => {\n const key = entry.name;\n const dir = `${baseDir}/${key}`;\n const resourcePath = `${dir}/${filePattern(key)}`;\n\n if (!fs.existsSync(resourcePath)) {\n return null;\n }\n\n return { key, dir, resourcePath };\n })\n .filter((info): info is ResourceDirInfo => info !== null);\n}\n\n// --- Tool files + flat simulations discovery ---\n\n/**\n * Information about a discovered tool file\n */\nexport interface ToolFileInfo {\n /** Tool name derived from filename (e.g., 'show-albums') */\n name: string;\n /** Full path to the tool file */\n path: string;\n}\n\n/**\n * Find all tool files in a tools directory.\n * Matches *.ts files directly in the directory (not recursive).\n *\n * @example\n * findToolFiles('src/tools', fs)\n * // [{ name: 'show-albums', path: 'src/tools/show-albums.ts' }]\n */\nexport function findToolFiles(\n toolsDir: string,\n fs: Pick<FsOps, 'readdirSync' | 'existsSync'>\n): ToolFileInfo[] {\n if (!fs.existsSync(toolsDir)) {\n return [];\n }\n\n const entries = fs.readdirSync(toolsDir, { withFileTypes: true });\n\n return entries\n .filter(\n (entry) =>\n !entry.isDirectory() && entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')\n )\n .map((entry) => ({\n name: entry.name.replace(/\\.ts$/, ''),\n path: `${toolsDir}/${entry.name}`,\n }));\n}\n\n/**\n * Information about a discovered simulation file (flat convention)\n */\nexport interface SimulationFileInfo {\n /** Filename without extension (e.g., 'show-albums') */\n name: string;\n /** Full path to the simulation file */\n path: string;\n}\n\n/**\n * Find all simulation JSON files in a flat simulations directory.\n * Matches any *.json file directly in the directory.\n *\n * @example\n * findSimulationFilesFlat('tests/simulations', fs)\n * // [{ name: 'show-albums', path: 'tests/simulations/show-albums.json' }]\n */\nexport function findSimulationFilesFlat(\n simulationsDir: string,\n fs: Pick<FsOps, 'readdirSync' | 'existsSync'>\n): SimulationFileInfo[] {\n if (!fs.existsSync(simulationsDir)) {\n return [];\n }\n\n const entries = fs.readdirSync(simulationsDir, { withFileTypes: true });\n\n return entries\n .filter((entry) => !entry.isDirectory() && entry.name.endsWith('.json'))\n .map((entry) => ({\n name: entry.name.replace(/\\.json$/, ''),\n path: `${simulationsDir}/${entry.name}`,\n }));\n}\n"],"names":[],"mappings":"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"}
|