sunpeak 0.4.4 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/chatgpt/chatgpt-simulator-types.d.ts +1 -1
  2. package/dist/chatgpt/chatgpt-simulator.d.ts +4 -15
  3. package/dist/chatgpt/index.d.ts +1 -1
  4. package/dist/chatgpt/mock-openai.d.ts +4 -16
  5. package/dist/index.cjs +69 -40
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.js +69 -40
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp/index.cjs +76 -112
  10. package/dist/mcp/index.cjs.map +1 -1
  11. package/dist/mcp/index.d.ts +2 -1
  12. package/dist/mcp/index.js +76 -112
  13. package/dist/mcp/index.js.map +1 -1
  14. package/dist/mcp/server.d.ts +2 -4
  15. package/dist/mcp/types.d.ts +16 -62
  16. package/dist/providers/index.d.ts +1 -3
  17. package/dist/providers/openai/index.d.ts +7 -0
  18. package/dist/{chatgpt/openai-provider.d.ts → providers/openai/provider.d.ts} +1 -1
  19. package/dist/{chatgpt/openai-types.d.ts → providers/openai/types.d.ts} +3 -32
  20. package/dist/providers/types.d.ts +4 -5
  21. package/dist/runtime/index.d.ts +7 -0
  22. package/dist/runtime/provider-detection.d.ts +17 -0
  23. package/dist/types/index.d.ts +2 -1
  24. package/dist/types/runtime.d.ts +34 -0
  25. package/dist/types/simulation.d.ts +47 -0
  26. package/package.json +2 -2
  27. package/template/README.md +6 -7
  28. package/template/dev/main.tsx +38 -3
  29. package/template/mcp/server.ts +9 -6
  30. package/template/nodemon.json +7 -0
  31. package/template/package.json +4 -2
  32. package/template/scripts/build-all.mjs +108 -0
  33. package/template/scripts/validate.mjs +16 -7
  34. package/template/src/components/index.ts +1 -1
  35. package/template/src/components/resources/AlbumsResource.tsx +13 -0
  36. package/template/src/components/{apps/PlacesApp.tsx → resources/CarouselResource.tsx} +11 -10
  37. package/template/src/components/{apps/App.tsx → resources/CounterResource.tsx} +5 -14
  38. package/template/src/components/resources/index.ts +3 -0
  39. package/template/src/index-resource.tsx +11 -0
  40. package/template/src/simulations/albums-simulation.ts +35 -9
  41. package/template/src/simulations/carousel-simulation.ts +35 -9
  42. package/template/src/simulations/counter-simulation.ts +41 -0
  43. package/template/src/simulations/index.ts +10 -10
  44. package/template/vite.config.build.ts +21 -15
  45. package/dist/chatgpt/mcp-provider.d.ts +0 -25
  46. package/dist/style.css +0 -4420
  47. package/template/src/components/apps/AlbumsApp.tsx +0 -13
  48. package/template/src/components/apps/active-app.ts +0 -11
  49. package/template/src/components/apps/index.ts +0 -3
  50. package/template/src/index.chatgpt.tsx +0 -9
  51. package/template/src/index.ts +0 -2
  52. package/template/src/simulations/app-configs.ts +0 -30
  53. package/template/src/simulations/app-simulation.ts +0 -15
  54. package/template/src/simulations/simulations.ts +0 -74
@@ -1,74 +1,28 @@
1
1
  import { Resource, Tool } from '@modelcontextprotocol/sdk/types.js';
2
2
  /**
3
- * Supported MCP provider platforms.
3
+ * MCP CallTool response data (subset used in simulations)
4
4
  */
5
- export declare enum MCPProvider {
6
- ChatGPT = "chatgpt"
5
+ export interface SimulationCallToolResult {
6
+ structuredContent?: Record<string, unknown> | null;
7
+ _meta?: Record<string, unknown>;
8
+ }
9
+ /**
10
+ * Simulation configuration for MCP server.
11
+ * Must include distPath for the built widget file.
12
+ */
13
+ export interface SimulationWithDist {
14
+ distPath: string;
15
+ tool: Tool;
16
+ resource: Resource;
17
+ toolCall?: SimulationCallToolResult;
7
18
  }
8
19
  /**
9
20
  * Configuration for the MCP server.
21
+ * Takes an array of simulations with distPath for each built widget.
10
22
  */
11
23
  export interface MCPServerConfig {
12
24
  name?: string;
13
25
  version?: string;
14
26
  port?: number;
15
- distPath: string;
16
- toolName?: string;
17
- toolDescription?: string;
18
- dummyData?: Record<string, unknown>;
19
- provider?: MCPProvider;
20
- }
21
- /**
22
- * Provider-specific metadata for widget descriptors.
23
- */
24
- export interface WidgetDescriptorMeta {
25
- [key: string]: unknown;
26
- }
27
- /**
28
- * Provider-specific metadata for tool invocations.
29
- */
30
- export interface WidgetInvocationMeta {
31
- [key: string]: unknown;
32
- }
33
- /**
34
- * Interface for platform-specific MCP provider implementations.
35
- */
36
- export interface MCPProviderImplementation {
37
- /**
38
- * Get metadata for widget descriptors (tools and resources).
39
- */
40
- getWidgetDescriptorMeta(): WidgetDescriptorMeta;
41
- /**
42
- * Get metadata for tool invocation responses.
43
- */
44
- getWidgetInvocationMeta(): WidgetInvocationMeta;
45
- /**
46
- * Read and wrap the widget content for the platform.
47
- * @param distPath - Path to the built widget JS file.
48
- * @returns The wrapped HTML content ready for the platform.
49
- */
50
- readWidgetContent(distPath: string): string;
51
- /**
52
- * Get the MIME type for widget resources.
53
- */
54
- getWidgetMimeType(): string;
55
- /**
56
- * Get the resource URI for the widget.
57
- */
58
- getWidgetResourceUri(): string;
59
- /**
60
- * Create the tool definition for the provider.
61
- */
62
- createTool(config: {
63
- name: string;
64
- description: string;
65
- inputSchema: Tool["inputSchema"];
66
- }): Tool;
67
- /**
68
- * Create the resource definition for the provider.
69
- */
70
- createResource(config: {
71
- name: string;
72
- description: string;
73
- }): Resource;
27
+ simulations: SimulationWithDist[];
74
28
  }
@@ -1,9 +1,7 @@
1
1
  import { WidgetProvider, WidgetGlobals, WidgetAPI } from './types';
2
2
  export type { WidgetGlobals, WidgetAPI, WidgetProvider } from './types';
3
- export { isOpenAiAvailable, getOpenAiProvider } from '../chatgpt/openai-provider';
4
3
  /**
5
- * Detect and return the appropriate provider for the current environment.
6
- * This function caches the result, so detection only happens once.
4
+ * Get the detected provider for the current environment.
7
5
  *
8
6
  * @returns The detected provider, or null if no provider is available.
9
7
  */
@@ -0,0 +1,7 @@
1
+ /**
2
+ * OpenAI/ChatGPT provider implementation.
3
+ * This module provides the OpenAI-specific implementation of the WidgetProvider interface.
4
+ */
5
+ export { isOpenAiAvailable, getOpenAiProvider } from './provider';
6
+ export type { OpenAiGlobals, OpenAiAPI, CallTool, RequestDisplayMode, RequestModal, NotifyIntrinsicHeight, SetGlobalsEvent, } from './types';
7
+ export { SET_GLOBALS_EVENT_TYPE } from './types';
@@ -1,4 +1,4 @@
1
- import { WidgetProvider, WidgetGlobals, WidgetAPI } from '../providers/types';
1
+ import { WidgetProvider, WidgetGlobals, WidgetAPI } from '../types';
2
2
  /**
3
3
  * Check if the OpenAI provider is available.
4
4
  */
@@ -1,38 +1,9 @@
1
- export type UnknownObject = Record<string, unknown>;
2
- export type Theme = 'light' | 'dark';
3
- export type SafeAreaInsets = {
4
- top: number;
5
- bottom: number;
6
- left: number;
7
- right: number;
8
- };
9
- export type SafeArea = {
10
- insets: SafeAreaInsets;
11
- };
12
- export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
13
- export type UserAgent = {
14
- device: {
15
- type: DeviceType;
16
- };
17
- capabilities: {
18
- hover: boolean;
19
- touch: boolean;
20
- };
21
- };
22
- export type DisplayMode = 'pip' | 'inline' | 'fullscreen';
1
+ import { UnknownObject, Theme, UserAgent, DisplayMode, ViewMode, View, SafeArea, CallToolResponse } from '../../types/runtime';
23
2
  export type RequestDisplayMode = (args: {
24
3
  mode: DisplayMode;
25
4
  }) => Promise<{
26
5
  mode: DisplayMode;
27
6
  }>;
28
- export type ViewMode = 'modal' | 'default';
29
- export type View = {
30
- mode: ViewMode;
31
- params?: UnknownObject;
32
- };
33
- export type CallToolResponse = {
34
- result: string;
35
- };
36
7
  export type CallTool = (name: string, args: Record<string, unknown>) => Promise<CallToolResponse>;
37
8
  export type OpenAiGlobals<ToolInput = UnknownObject, ToolOutput = UnknownObject, ToolResponseMetadata = UnknownObject, WidgetState = UnknownObject> = {
38
9
  theme: Theme;
@@ -46,14 +17,13 @@ export type OpenAiGlobals<ToolInput = UnknownObject, ToolOutput = UnknownObject,
46
17
  toolOutput: ToolOutput | null;
47
18
  toolResponseMetadata: ToolResponseMetadata | null;
48
19
  widgetState: WidgetState | null;
49
- setWidgetState: (state: WidgetState) => Promise<void>;
50
20
  };
51
21
  export type RequestModal = (args: {
52
22
  mode: ViewMode;
53
23
  params?: UnknownObject;
54
24
  }) => Promise<void>;
55
25
  export type NotifyIntrinsicHeight = (height: number) => void;
56
- export type OpenAiAPI = {
26
+ export type OpenAiAPI<WidgetState = UnknownObject> = {
57
27
  callTool: CallTool;
58
28
  sendFollowUpMessage: (args: {
59
29
  prompt: string;
@@ -64,6 +34,7 @@ export type OpenAiAPI = {
64
34
  requestDisplayMode: RequestDisplayMode;
65
35
  requestModal: RequestModal;
66
36
  notifyIntrinsicHeight: NotifyIntrinsicHeight;
37
+ setWidgetState: (state: WidgetState) => Promise<void>;
67
38
  };
68
39
  export declare const SET_GLOBALS_EVENT_TYPE = "openai:set_globals";
69
40
  export declare class SetGlobalsEvent extends CustomEvent<{
@@ -1,12 +1,12 @@
1
- import { UnknownObject, Theme, UserAgent, DisplayMode, SafeArea, View, ViewMode, CallToolResponse } from '../chatgpt/openai-types';
1
+ import { UnknownObject, Theme, UserAgent, DisplayMode, SafeArea, View, ViewMode, CallToolResponse } from '../types/runtime';
2
2
  /**
3
3
  * Provider-agnostic types for widget runtime environments.
4
4
  * These types abstract away the specific host (OpenAI/ChatGPT, etc.)
5
5
  */
6
- export type { UnknownObject, Theme, SafeAreaInsets, SafeArea, DeviceType, UserAgent, DisplayMode, ViewMode, View, CallToolResponse, } from '../chatgpt/openai-types';
6
+ export type { UnknownObject, Theme, SafeAreaInsets, SafeArea, DeviceType, UserAgent, DisplayMode, ViewMode, View, CallToolResponse, } from '../types/runtime';
7
7
  /**
8
8
  * Global state available from the widget runtime environment.
9
- * This is a provider-agnostic alias for OpenAiGlobals.
9
+ * Provider-agnostic interface that abstracts widget globals.
10
10
  */
11
11
  export type WidgetGlobals<ToolInput = UnknownObject, ToolOutput = UnknownObject, ToolResponseMetadata = UnknownObject, WidgetState = UnknownObject> = {
12
12
  theme: Theme;
@@ -20,11 +20,10 @@ export type WidgetGlobals<ToolInput = UnknownObject, ToolOutput = UnknownObject,
20
20
  toolOutput: ToolOutput | null;
21
21
  toolResponseMetadata: ToolResponseMetadata | null;
22
22
  widgetState: WidgetState | null;
23
- setWidgetState: (state: WidgetState) => Promise<void>;
24
23
  };
25
24
  /**
26
25
  * API methods available from the widget runtime environment.
27
- * This is a provider-agnostic alias for OpenAiAPI.
26
+ * Provider-agnostic interface that abstracts widget API methods.
28
27
  */
29
28
  export type WidgetAPI = {
30
29
  callTool?: (name: string, args: Record<string, unknown>) => Promise<CallToolResponse>;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runtime utilities for widget environments.
3
+ *
4
+ * This module contains runtime-specific logic like provider detection
5
+ * that needs to know about concrete provider implementations.
6
+ */
7
+ export { detectProvider, isProviderAvailable, resetProviderCache, } from './provider-detection';
@@ -0,0 +1,17 @@
1
+ import { WidgetProvider } from '../providers/types';
2
+ /**
3
+ * Detect and return the appropriate provider for the current environment.
4
+ * This function caches the result, so detection only happens once.
5
+ *
6
+ * @returns The detected provider, or null if no provider is available.
7
+ */
8
+ export declare function detectProvider(): WidgetProvider | null;
9
+ /**
10
+ * Check if any provider is available.
11
+ */
12
+ export declare function isProviderAvailable(): boolean;
13
+ /**
14
+ * Reset the provider detection cache.
15
+ * Useful for testing or when the environment changes.
16
+ */
17
+ export declare function resetProviderCache(): void;
@@ -1,2 +1,3 @@
1
- export * from '../chatgpt/openai-types';
1
+ export * from './runtime';
2
+ export * from './simulation';
2
3
  export * from '../chatgpt/chatgpt-simulator-types';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generic runtime types for widget environments.
3
+ * These types are provider-agnostic and can be used across different widget platforms.
4
+ */
5
+ export type UnknownObject = Record<string, unknown>;
6
+ export type Theme = 'light' | 'dark';
7
+ export type SafeAreaInsets = {
8
+ top: number;
9
+ bottom: number;
10
+ left: number;
11
+ right: number;
12
+ };
13
+ export type SafeArea = {
14
+ insets: SafeAreaInsets;
15
+ };
16
+ export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
17
+ export type UserAgent = {
18
+ device: {
19
+ type: DeviceType;
20
+ };
21
+ capabilities: {
22
+ hover: boolean;
23
+ touch: boolean;
24
+ };
25
+ };
26
+ export type DisplayMode = 'pip' | 'inline' | 'fullscreen';
27
+ export type ViewMode = 'modal' | 'default';
28
+ export type View = {
29
+ mode: ViewMode;
30
+ params?: UnknownObject;
31
+ };
32
+ export type CallToolResponse = {
33
+ result: string;
34
+ };
@@ -0,0 +1,47 @@
1
+ import { Tool, Resource } from '@modelcontextprotocol/sdk/types.js';
2
+ import { Theme, DisplayMode, UserAgent, SafeArea, View } from './index';
3
+ /**
4
+ * Core simulation types for development and testing.
5
+ * These types define how simulations are configured and used in both
6
+ * the dev simulator and MCP server contexts.
7
+ */
8
+ import type * as React from 'react';
9
+ /**
10
+ * Simulation globals that configure the simulator environment.
11
+ * These values are passed to the mock runtime to set initial values for development/testing.
12
+ * All fields are optional as simulations can use defaults.
13
+ * Globals initialized based on tool responses are instead set in SimulationCallToolResult:
14
+ * (structuredContent > toolOutput, _meta > toolResponseMetadata)
15
+ */
16
+ export interface SimulationGlobals {
17
+ theme?: Theme;
18
+ userAgent?: UserAgent;
19
+ locale?: string;
20
+ maxHeight?: number;
21
+ displayMode?: DisplayMode;
22
+ safeArea?: SafeArea;
23
+ view?: View | null;
24
+ toolInput?: Record<string, unknown>;
25
+ widgetState?: Record<string, unknown> | null;
26
+ }
27
+ /**
28
+ * MCP CallTool response data (subset used in simulations).
29
+ * Note: toolOutput (structuredContent) and toolResponseMetadata (_meta)
30
+ * are set here for use by the MCP server as well, not in SimulationGlobals.
31
+ */
32
+ export interface SimulationCallToolResult {
33
+ structuredContent?: Record<string, unknown> | null;
34
+ _meta?: Record<string, unknown>;
35
+ }
36
+ /**
37
+ * A simulation packages a component with its example data and metadata.
38
+ * Each simulation represents a complete tool experience in the simulator.
39
+ */
40
+ export interface Simulation {
41
+ resourceComponent: React.ComponentType;
42
+ userMessage?: string;
43
+ simulationGlobals?: SimulationGlobals;
44
+ tool: Tool;
45
+ resource: Resource;
46
+ toolCall?: SimulationCallToolResult;
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunpeak",
3
- "version": "0.4.4",
3
+ "version": "0.5.3",
4
4
  "description": "The MCP App SDK. Quickstart, build, & test your ChatGPT App locally!",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -55,12 +55,12 @@
55
55
  "author": "Sunpeak AI",
56
56
  "license": "MIT",
57
57
  "peerDependencies": {
58
+ "@openai/apps-sdk-ui": "^0.2.0",
58
59
  "react": "^18.0.0 || ^19.0.0",
59
60
  "react-dom": "^18.0.0 || ^19.0.0"
60
61
  },
61
62
  "dependencies": {
62
63
  "@modelcontextprotocol/sdk": "^0.5.0",
63
- "@openai/apps-sdk-ui": "^0.2.0",
64
64
  "clsx": "^2.1.1",
65
65
  "tailwind-merge": "^3.4.0",
66
66
  "zod": "^3.23.8"
@@ -10,7 +10,7 @@ For an initial overview of your new app and the sunpeak API, refer to the [docum
10
10
  pnpm dev
11
11
  ```
12
12
 
13
- Edit [./src/components/apps/App.tsx](./src/components/apps/App.tsx) to build your app UI.
13
+ Edit the resource files in [./src/components/resources/](./src/components/resources/) to build your resource UI.
14
14
 
15
15
  ## Development
16
16
 
@@ -40,19 +40,18 @@ pnpm dev
40
40
  Test your app directly in ChatGPT using the built-in MCP server:
41
41
 
42
42
  ```bash
43
- # 1. Build your app. You must rebuild your app for changes to take effect.
44
- pnpm build
45
-
46
- # 2. Start the MCP server.
43
+ # Start the MCP server (rebuilds and restarts on file changes).
47
44
  pnpm mcp
48
45
 
49
- # 3. In another terminal, run a tunnel. For example:
46
+ # In another terminal, run a tunnel. For example:
50
47
  ngrok http 6766
51
48
  ```
52
49
 
53
50
  You can then connect to the tunnel forwarding URL at the `/mcp` path from ChatGPT **in developer mode** to see your UI in action: `User > Settings > Apps & Connectors > Create`
54
51
 
55
- Once your app is connected, send `show app` to ChatGPT. Many changes require you to Refresh your app on the same settings modal.
52
+ Once your app is connected, send the name of a tool, like `show counter`, to ChatGPT.
53
+
54
+ When you make changes to the UI, refresh your app in ChatGPT after the MCP server has finished rebuilding your app: `User > Settings > Apps & Connectors > My App > Refresh`
56
55
 
57
56
  ## Build & Deploy
58
57
 
@@ -2,11 +2,46 @@ import '../src/styles/globals.css';
2
2
 
3
3
  import { StrictMode } from 'react';
4
4
  import { createRoot } from 'react-dom/client';
5
- import { ChatGPTSimulator } from 'sunpeak';
6
- import { simulations } from '../src/simulations';
5
+ import { ChatGPTSimulator, type Simulation } from 'sunpeak';
6
+ import { SIMULATIONS } from '../src/simulations';
7
+ import * as Resources from '../src/components/resources';
8
+
9
+ /**
10
+ * Extract the resource component name from a URI
11
+ * Example: 'ui://CounterResource.tsx' -> 'CounterResource'
12
+ */
13
+ function getResourceComponentFromURI(uri: string): React.ComponentType {
14
+ // Extract component name from URI pattern: ui://ComponentName.tsx
15
+ const match = uri.match(/^ui:\/\/(.+)\.tsx$/);
16
+ if (!match) {
17
+ throw new Error(`Invalid resource URI format: ${uri}. Expected format: ui://ComponentName.tsx`);
18
+ }
19
+
20
+ const componentName = match[1];
21
+ const component = Resources[componentName as keyof typeof Resources];
22
+
23
+ if (!component) {
24
+ throw new Error(
25
+ `Resource component "${componentName}" not found. ` +
26
+ `Make sure it's exported from src/components/resources/index.ts`
27
+ );
28
+ }
29
+
30
+ return component as React.ComponentType;
31
+ }
32
+
33
+ // Package the resource component with the simulation
34
+ const simulations: Simulation[] = Object.values(SIMULATIONS).map((simulation) => ({
35
+ ...simulation,
36
+ resourceComponent: getResourceComponentFromURI(simulation.resource.uri),
37
+ }));
7
38
 
8
39
  createRoot(document.getElementById('root')!).render(
9
40
  <StrictMode>
10
- <ChatGPTSimulator simulations={simulations} />
41
+ <ChatGPTSimulator
42
+ simulations={simulations}
43
+ appName="Sunpeak App"
44
+ appIcon="🌄"
45
+ />
11
46
  </StrictMode>
12
47
  );
@@ -1,16 +1,19 @@
1
1
  import { runMCPServer } from 'sunpeak/mcp';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { activeConfig } from '../src/simulations/app-configs.js';
4
+ import { SIMULATIONS } from '../src/simulations';
5
5
 
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
 
8
+ // Add distPath to each simulation for the MCP server
9
+ const simulations = Object.entries(SIMULATIONS).map(([simulationKey, simulation]) => ({
10
+ ...simulation,
11
+ distPath: path.resolve(__dirname, `../dist/chatgpt/${simulationKey}.js`),
12
+ }));
13
+
8
14
  runMCPServer({
9
- name: activeConfig.appName,
15
+ name: 'Sunpeak App',
10
16
  version: '0.1.0',
11
- distPath: path.resolve(__dirname, '../dist/chatgpt/index.js'),
12
- toolName: activeConfig.toolName,
13
- toolDescription: activeConfig.toolDescription,
14
- dummyData: activeConfig.toolOutput ?? {},
17
+ simulations,
15
18
  port: 6766,
16
19
  });
@@ -0,0 +1,7 @@
1
+ {
2
+ "watch": ["src"],
3
+ "ext": "ts,tsx,js,jsx,css",
4
+ "ignore": ["dist", "node_modules", ".tmp"],
5
+ "exec": "pnpm build && pnpm mcp:serve",
6
+ "delay": 500
7
+ }
@@ -4,9 +4,10 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build": "vite build --config vite.config.build.ts",
7
+ "build": "node scripts/build-all.mjs",
8
8
  "dev": "vite --port ${PORT:-6767}",
9
- "mcp": "tsx mcp/server.ts",
9
+ "mcp": "nodemon",
10
+ "mcp:serve": "tsx mcp/server.ts",
10
11
  "lint": "eslint . --ext .ts,.tsx --fix",
11
12
  "typecheck": "tsc --noEmit",
12
13
  "test": "vitest run",
@@ -36,6 +37,7 @@
36
37
  "eslint-plugin-react": "^7.37.5",
37
38
  "eslint-plugin-react-hooks": "^7.0.1",
38
39
  "jsdom": "^27.2.0",
40
+ "nodemon": "^3.1.11",
39
41
  "postcss": "^8.4.49",
40
42
  "prettier": "^3.6.2",
41
43
  "react": "^18.3.1",
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'child_process';
3
+ import { existsSync, rmSync, readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const distDir = path.join(__dirname, '../dist/chatgpt');
9
+ const buildDir = path.join(__dirname, '../dist/build-output');
10
+ const tempDir = path.join(__dirname, '../.tmp');
11
+ const resourcesDir = path.join(__dirname, '../src/components/resources');
12
+ const templateFile = path.join(__dirname, '../src/index-resource.tsx');
13
+
14
+ // Clean dist and temp directories
15
+ if (existsSync(distDir)) {
16
+ rmSync(distDir, { recursive: true });
17
+ }
18
+ if (existsSync(tempDir)) {
19
+ rmSync(tempDir, { recursive: true });
20
+ }
21
+ mkdirSync(distDir, { recursive: true });
22
+ mkdirSync(tempDir, { recursive: true });
23
+
24
+ // Auto-discover all resources
25
+ const resourceFiles = readdirSync(resourcesDir)
26
+ .filter(file => file.endsWith('Resource.tsx'))
27
+ .map(file => {
28
+ const resourceName = file.replace('Resource.tsx', '');
29
+ return {
30
+ componentName: `${resourceName}Resource`,
31
+ entry: `.tmp/index-${resourceName.toLowerCase()}.tsx`,
32
+ output: `${resourceName.toLowerCase()}.js`,
33
+ buildOutDir: path.join(buildDir, resourceName.toLowerCase()),
34
+ };
35
+ });
36
+
37
+ console.log('Building all tools...\n');
38
+
39
+ // Read the template
40
+ const template = readFileSync(templateFile, 'utf-8');
41
+
42
+ // Build all resources (but don't copy yet)
43
+ resourceFiles.forEach(({ componentName, entry, output, buildOutDir }, index) => {
44
+ console.log(`[${index + 1}/${resourceFiles.length}] Building ${output}...`);
45
+
46
+ try {
47
+ // Create build directory if it doesn't exist
48
+ if (!existsSync(buildOutDir)) {
49
+ mkdirSync(buildOutDir, { recursive: true });
50
+ }
51
+
52
+ // Create entry file from template in temp directory
53
+ const entryContent = template
54
+ .replace('// RESOURCE_IMPORT', `import { ${componentName} } from '../src/components/resources/${componentName}';`)
55
+ .replace('// RESOURCE_MOUNT', `createRoot(root).render(<${componentName} />);`);
56
+
57
+ const entryPath = path.join(__dirname, '..', entry);
58
+ writeFileSync(entryPath, entryContent);
59
+
60
+ // Build with vite to build directory
61
+ execSync(
62
+ `vite build --config vite.config.build.ts`,
63
+ {
64
+ stdio: 'inherit',
65
+ env: {
66
+ ...process.env,
67
+ ENTRY_FILE: entry,
68
+ OUTPUT_FILE: output,
69
+ OUT_DIR: buildOutDir,
70
+ },
71
+ }
72
+ );
73
+ } catch (error) {
74
+ console.error(`Failed to build ${output}`);
75
+ process.exit(1);
76
+ }
77
+ });
78
+
79
+ // Now copy all files from build-output to dist/chatgpt
80
+ console.log('\nCopying built files to dist/chatgpt...');
81
+ resourceFiles.forEach(({ output, buildOutDir }) => {
82
+ const builtFile = path.join(buildOutDir, output);
83
+ const destFile = path.join(distDir, output);
84
+
85
+ if (existsSync(builtFile)) {
86
+ copyFileSync(builtFile, destFile);
87
+ console.log(`✓ Copied ${output}`);
88
+ } else {
89
+ console.error(`Built file not found: ${builtFile}`);
90
+ if (existsSync(buildOutDir)) {
91
+ console.log(` Files in ${buildOutDir}:`, readdirSync(buildOutDir));
92
+ } else {
93
+ console.log(` Build directory doesn't exist: ${buildOutDir}`);
94
+ }
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ // Clean up temp and build directories
100
+ if (existsSync(tempDir)) {
101
+ rmSync(tempDir, { recursive: true });
102
+ }
103
+ if (existsSync(buildDir)) {
104
+ rmSync(buildDir, { recursive: true });
105
+ }
106
+
107
+ console.log('\n✓ All tools built successfully!');
108
+ console.log(`\nBuilt files:`, readdirSync(distDir));
@@ -100,17 +100,26 @@ try {
100
100
  throw new Error('pnpm build failed');
101
101
  }
102
102
  const chatgptDir = join(PROJECT_ROOT, 'dist', 'chatgpt');
103
- const builtFile = join(chatgptDir, 'index.js');
104
- if (!existsSync(builtFile)) {
105
- printError('Missing expected file: ./dist/chatgpt/index.js');
106
- process.exit(1);
103
+ const expectedFiles = ['counter.js', 'albums.js', 'carousel.js'];
104
+
105
+ // Check all expected files exist
106
+ for (const file of expectedFiles) {
107
+ const filePath = join(chatgptDir, file);
108
+ if (!existsSync(filePath)) {
109
+ printError(`Missing expected file: ./dist/chatgpt/${file}`);
110
+ process.exit(1);
111
+ }
107
112
  }
113
+
114
+ // Verify only expected files are present
108
115
  const files = readdirSync(chatgptDir);
109
- if (files.length !== 1 || files[0] !== 'index.js') {
110
- printError(`Unexpected files in ./dist/chatgpt/: ${files.join(', ')}`);
111
- printError('Expected only: index.js');
116
+ const unexpectedFiles = files.filter(f => !expectedFiles.includes(f));
117
+ if (unexpectedFiles.length > 0) {
118
+ printError(`Unexpected files in ./dist/chatgpt/: ${unexpectedFiles.join(', ')}`);
119
+ printError(`Expected only: ${expectedFiles.join(', ')}`);
112
120
  process.exit(1);
113
121
  }
122
+
114
123
  console.log()
115
124
  printSuccess('pnpm build');
116
125
 
@@ -1,4 +1,4 @@
1
1
  export * from './card';
2
2
  export * from './carousel';
3
3
  export * from './album';
4
- export * from './apps';
4
+ export * from './resources';
@@ -0,0 +1,13 @@
1
+ import * as React from "react"
2
+ import { Albums } from "../album/albums"
3
+
4
+ /**
5
+ * Production-ready Albums Resource
6
+ *
7
+ * This resource displays photo albums in a carousel layout with fullscreen viewing capability.
8
+ * Can be dropped into any production environment without changes.
9
+ */
10
+ export const AlbumsResource = React.forwardRef<HTMLDivElement>((_props, ref) => {
11
+ return <Albums ref={ref} />
12
+ })
13
+ AlbumsResource.displayName = "AlbumsResource"