sunpeak 0.16.17 → 0.16.21
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 +2 -2
- 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/{platform → host}/chatgpt/index.cjs +1 -1
- package/dist/host/chatgpt/index.cjs.map +1 -0
- package/dist/{platform → host}/chatgpt/index.d.ts +2 -2
- package/dist/{platform → host}/chatgpt/index.js +1 -1
- package/dist/host/chatgpt/index.js.map +1 -0
- package/dist/{platform → host}/chatgpt/use-create-file.d.ts +2 -2
- package/dist/{platform → host}/chatgpt/use-file-download.d.ts +2 -2
- package/dist/{platform → host}/chatgpt/use-open-modal.d.ts +2 -2
- package/dist/{platform → host}/chatgpt/use-request-checkout.d.ts +2 -2
- package/dist/{platform → host}/index.cjs +5 -3
- package/dist/host/index.cjs.map +1 -0
- package/dist/{platform → host}/index.d.ts +15 -11
- package/dist/{platform → host}/index.js +5 -3
- package/dist/host/index.js.map +1 -0
- package/dist/{index-CsYoMHyn.js → index-B4aC3vjH.js} +4 -4
- package/dist/index-B4aC3vjH.js.map +1 -0
- package/dist/{index-DHcaJ5PU.cjs → index-CKabCJyV.cjs} +4 -4
- package/dist/index-CKabCJyV.cjs.map +1 -0
- package/dist/index-CX6Z4bED.js +29 -0
- package/dist/index-CX6Z4bED.js.map +1 -0
- package/dist/index-bKBBCBK6.cjs +28 -0
- package/dist/index-bKBBCBK6.cjs.map +1 -0
- package/dist/index.cjs +233 -6297
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +228 -6292
- 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 +680 -6766
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +682 -6768
- package/dist/mcp/index.js.map +1 -1
- package/dist/{protocol-CfvM5B6z.cjs → protocol-DkDHRwOW.cjs} +50 -5
- package/dist/{protocol-CfvM5B6z.cjs.map → protocol-DkDHRwOW.cjs.map} +1 -1
- package/dist/{protocol-CF-P_kw5.js → protocol-uge7qFev.js} +102 -57
- package/dist/{protocol-CF-P_kw5.js.map → protocol-uge7qFev.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-D8t-r7HH.js +3222 -0
- package/dist/simulator-D8t-r7HH.js.map +1 -0
- package/dist/simulator-FFNttkqL.cjs +3237 -0
- package/dist/simulator-FFNttkqL.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/dist/use-app-C9gpzIQO.js +349 -0
- package/dist/use-app-C9gpzIQO.js.map +1 -0
- package/dist/use-app-D09O2swh.cjs +348 -0
- package/dist/use-app-D09O2swh.cjs.map +1 -0
- package/package.json +26 -14
- package/template/.sunpeak/dev.tsx +28 -2
- package/template/node_modules/.bin/vite +2 -2
- package/template/node_modules/.bin/vitest +2 -2
- package/template/package.json +5 -5
- 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/platform/chatgpt/index.cjs.map +0 -1
- package/dist/platform/chatgpt/index.js.map +0 -1
- package/dist/platform/index.cjs.map +0 -1
- package/dist/platform/index.js.map +0 -1
- package/dist/simulator-BEFsuj9Z.cjs +0 -8872
- package/dist/simulator-BEFsuj9Z.cjs.map +0 -1
- package/dist/simulator-Da9iAupa.js +0 -8857
- 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/dist/use-app-CaTJmpgj.cjs +0 -6449
- package/dist/use-app-CaTJmpgj.cjs.map +0 -1
- package/dist/use-app-DTTzqi-0.js +0 -6450
- package/dist/use-app-DTTzqi-0.js.map +0 -1
- /package/dist/{platform → host}/chatgpt/openai-types.d.ts +0 -0
|
@@ -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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"}
|
|
@@ -191,7 +191,9 @@ function findToolFiles(toolsDir, fs) {
|
|
|
191
191
|
return [];
|
|
192
192
|
}
|
|
193
193
|
const entries = fs.readdirSync(toolsDir, { withFileTypes: true });
|
|
194
|
-
return entries.filter(
|
|
194
|
+
return entries.filter(
|
|
195
|
+
(entry) => !entry.isDirectory() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")
|
|
196
|
+
).map((entry) => ({
|
|
195
197
|
name: entry.name.replace(/\.ts$/, ""),
|
|
196
198
|
path: `${toolsDir}/${entry.name}`
|
|
197
199
|
}));
|
|
@@ -218,4 +220,4 @@ exports.findSimulationFilesFlat = findSimulationFilesFlat;
|
|
|
218
220
|
exports.findToolFiles = findToolFiles;
|
|
219
221
|
exports.getComponentName = getComponentName;
|
|
220
222
|
exports.toPascalCase = toPascalCase;
|
|
221
|
-
//# sourceMappingURL=discovery-
|
|
223
|
+
//# sourceMappingURL=discovery-D1gpaVz4.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery-SviNiBkF.cjs","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":["module"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"discovery-D1gpaVz4.cjs","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":["module"],"mappings":"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"}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -7,10 +7,19 @@ export { useHostStyleVariables, useHostFonts, useHostStyles, } from '@modelconte
|
|
|
7
7
|
export { useHostContext } from './use-host-context';
|
|
8
8
|
export { useToolData } from './use-tool-data';
|
|
9
9
|
export type { ToolData } from './use-tool-data';
|
|
10
|
-
export {
|
|
10
|
+
export { useDeviceCapabilities } from './use-device-capabilities';
|
|
11
|
+
export type { DeviceCapabilities } from './use-device-capabilities';
|
|
11
12
|
export { useDisplayMode } from './use-display-mode';
|
|
12
13
|
export { useLocale } from './use-locale';
|
|
14
|
+
export { usePlatform } from './use-platform';
|
|
15
|
+
export type { HostPlatform } from './use-platform';
|
|
13
16
|
export { useSafeArea } from './use-safe-area';
|
|
17
|
+
export { useStyles } from './use-styles';
|
|
18
|
+
export { useTheme } from './use-theme';
|
|
19
|
+
export { useTimeZone } from './use-time-zone';
|
|
20
|
+
export { useToolInfo } from './use-tool-info';
|
|
21
|
+
export type { ToolInfo } from './use-tool-info';
|
|
22
|
+
export { useUserAgent } from './use-user-agent';
|
|
14
23
|
export { useViewport } from './use-viewport';
|
|
15
24
|
export type { Viewport } from './use-viewport';
|
|
16
25
|
export { useIsMobile } from './use-mobile';
|
|
@@ -3,10 +3,14 @@ export interface SafeAreaProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
3
3
|
children: ReactNode;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Wrapper component that applies safe-area padding and viewport
|
|
6
|
+
* Wrapper component that applies safe-area padding and viewport constraints.
|
|
7
7
|
*
|
|
8
8
|
* Replaces the common boilerplate of calling `useSafeArea()` + `useViewport()`
|
|
9
|
-
* and manually applying padding/maxHeight styles on a container div.
|
|
9
|
+
* and manually applying padding/maxHeight/maxWidth styles on a container div.
|
|
10
|
+
*
|
|
11
|
+
* In fullscreen mode, SafeArea fills the iframe viewport (`100dvh`) so that
|
|
12
|
+
* flex column layouts (e.g. sticky header / scrollable content / sticky footer)
|
|
13
|
+
* work correctly without each resource having to handle display mode sizing.
|
|
10
14
|
*
|
|
11
15
|
* @example
|
|
12
16
|
* ```tsx
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useTimeZone(): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useUserAgent(): string | undefined;
|
|
@@ -2,6 +2,9 @@ import { McpUiHostContext } from '@modelcontextprotocol/ext-apps';
|
|
|
2
2
|
type ContainerDimensions = NonNullable<McpUiHostContext['containerDimensions']>;
|
|
3
3
|
export type Viewport = ContainerDimensions & {
|
|
4
4
|
maxHeight?: number;
|
|
5
|
+
maxWidth?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
width?: number;
|
|
5
8
|
};
|
|
6
9
|
export declare function useViewport(): Viewport | null;
|
|
7
10
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const React = require("react");
|
|
4
|
-
const useApp = require("../../use-app-
|
|
4
|
+
const useApp = require("../../use-app-D09O2swh.cjs");
|
|
5
5
|
function getOpenAIRuntime() {
|
|
6
6
|
if (typeof window !== "undefined" && "openai" in window) {
|
|
7
7
|
return window.openai;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../../src/host/chatgpt/openai-types.ts","../../../src/host/chatgpt/use-create-file.ts","../../../src/host/chatgpt/use-file-download.ts","../../../src/host/chatgpt/use-open-modal.ts","../../../src/host/chatgpt/use-request-checkout.ts"],"sourcesContent":["/**\n * TypeScript declarations for the `window.openai` ChatGPT runtime.\n *\n * These APIs are available inside ChatGPT-hosted iframes and provide\n * platform-specific capabilities beyond the MCP Apps standard.\n *\n * @see https://developers.openai.com/apps-sdk/mcp-apps-in-chatgpt\n */\n\nexport interface OpenAIUploadFileResult {\n fileId: string;\n}\n\nexport interface OpenAIFileDownloadUrlResult {\n downloadUrl: string;\n}\n\nexport interface OpenAIRequestModalParams {\n /** URL of an alternate UI template to load in the modal. Omit to reuse the current UI. */\n template?: string;\n /** Arbitrary params forwarded to the modal content. */\n params?: Record<string, unknown>;\n}\n\nexport interface OpenAICheckoutPaymentProvider {\n provider: string;\n merchant_id: string;\n supported_payment_methods: string[];\n}\n\nexport interface OpenAICheckoutTotal {\n type: string;\n display_text: string;\n amount: number;\n}\n\nexport interface OpenAICheckoutLink {\n type: 'terms_of_use' | 'privacy_policy' | string;\n url: string;\n}\n\nexport interface OpenAICheckoutSession {\n id: string;\n payment_provider: OpenAICheckoutPaymentProvider;\n status: 'ready_for_payment';\n currency: string;\n totals: OpenAICheckoutTotal[];\n links: OpenAICheckoutLink[];\n payment_mode: 'live' | 'test';\n}\n\nexport interface OpenAICheckoutOrder {\n id: string;\n checkout_session_id: string;\n status: 'completed' | string;\n permalink_url?: string;\n}\n\n/**\n * The `window.openai` runtime object injected by ChatGPT into hosted iframes.\n */\nexport interface OpenAIRuntime {\n // --- File APIs ---\n uploadFile?(file: File): Promise<OpenAIUploadFileResult>;\n getFileDownloadUrl?(params: { fileId: string }): Promise<OpenAIFileDownloadUrlResult>;\n\n // --- Modal API ---\n requestModal?(params: OpenAIRequestModalParams): Promise<void>;\n\n // --- Checkout API ---\n requestCheckout?(session: OpenAICheckoutSession): Promise<OpenAICheckoutOrder>;\n\n // --- Display ---\n requestClose?(): void;\n requestDisplayMode?(params: { mode: 'inline' | 'PiP' | 'fullscreen' }): Promise<void>;\n\n // --- Messaging ---\n sendFollowUpMessage?(params: { prompt: string }): void;\n\n // --- Other ---\n openExternal?(params: { href: string }): void;\n}\n\n/**\n * Get the `window.openai` runtime if available.\n * Returns `undefined` outside of ChatGPT or in SSR.\n */\nexport function getOpenAIRuntime(): OpenAIRuntime | undefined {\n if (typeof window !== 'undefined' && 'openai' in window) {\n return (window as unknown as { openai: OpenAIRuntime }).openai;\n }\n return undefined;\n}\n","import { useCallback } from 'react';\nimport { useApp } from '../../hooks/use-app';\nimport { getOpenAIRuntime, type OpenAIUploadFileResult } from './openai-types';\n\nexport type { OpenAIUploadFileResult as CreateFileResult };\n\n/**\n * Upload a file from the app UI into the ChatGPT conversation.\n *\n * Wraps `window.openai.uploadFile` which is only available inside ChatGPT.\n * Supported formats: `image/png`, `image/jpeg`, `image/webp`.\n *\n * Import from `sunpeak/host/chatgpt`:\n *\n * @example\n * ```tsx\n * import { useUploadFile } from 'sunpeak/host/chatgpt';\n *\n * function ImageUploader() {\n * const uploadFile = useUploadFile();\n *\n * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n * const file = e.currentTarget.files?.[0];\n * if (!file) return;\n * const { fileId } = await uploadFile(file);\n * console.log('Uploaded:', fileId);\n * };\n *\n * return <input type=\"file\" accept=\"image/*\" onChange={handleChange} />;\n * }\n * ```\n */\nexport function useUploadFile(): (file: File) => Promise<OpenAIUploadFileResult> {\n const app = useApp();\n return useCallback(\n async (file: File) => {\n if (!app) {\n throw new Error('[useUploadFile] App not connected');\n }\n const runtime = getOpenAIRuntime();\n if (!runtime?.uploadFile) {\n throw new Error('[useUploadFile] window.openai.uploadFile not available');\n }\n return runtime.uploadFile(file);\n },\n [app]\n );\n}\n","import { useCallback } from 'react';\nimport { useApp } from '../../hooks/use-app';\nimport { getOpenAIRuntime, type OpenAIFileDownloadUrlResult } from './openai-types';\n\nexport type { OpenAIFileDownloadUrlResult as FileDownloadUrlResult };\n\n/**\n * Get a temporary download URL for a file by its ID.\n *\n * @deprecated Use {@link useDownloadFile} from `sunpeak` instead — it works\n * across all hosts via the MCP Apps SDK `app.downloadFile()` method.\n *\n * Wraps `window.openai.getFileDownloadUrl` which is only available inside\n * ChatGPT. Use this to retrieve URLs for files uploaded via {@link useUploadFile}\n * or file IDs received in tool parameters.\n *\n * Import from `sunpeak/host/chatgpt`:\n *\n * @example\n * ```tsx\n * import { useGetFileDownloadUrl } from 'sunpeak/host/chatgpt';\n *\n * function FilePreview({ fileId }: { fileId: string }) {\n * const getFileDownloadUrl = useGetFileDownloadUrl();\n * const [src, setSrc] = useState<string>();\n *\n * useEffect(() => {\n * getFileDownloadUrl({ fileId }).then(({ downloadUrl }) => setSrc(downloadUrl));\n * }, [fileId, getFileDownloadUrl]);\n *\n * return src ? <img src={src} /> : <p>Loading...</p>;\n * }\n * ```\n */\nexport function useGetFileDownloadUrl(): (params: {\n fileId: string;\n}) => Promise<OpenAIFileDownloadUrlResult> {\n const app = useApp();\n return useCallback(\n async (params: { fileId: string }) => {\n if (!app) {\n throw new Error('[useGetFileDownloadUrl] App not connected');\n }\n const runtime = getOpenAIRuntime();\n if (!runtime?.getFileDownloadUrl) {\n throw new Error('[useGetFileDownloadUrl] window.openai.getFileDownloadUrl not available');\n }\n return runtime.getFileDownloadUrl(params);\n },\n [app]\n );\n}\n","import { useCallback } from 'react';\nimport { useApp } from '../../hooks/use-app';\nimport { getOpenAIRuntime, type OpenAIRequestModalParams } from './openai-types';\n\nexport type { OpenAIRequestModalParams as OpenModalParams };\n\n/**\n * Open a host-controlled modal in ChatGPT.\n *\n * Wraps `window.openai.requestModal` which is only available inside ChatGPT.\n * Pass a `template` URL to load alternate UI content in the modal, or omit\n * it to reuse the current UI.\n *\n * Import from `sunpeak/host/chatgpt`:\n *\n * @example\n * ```tsx\n * import { useRequestModal } from 'sunpeak/host/chatgpt';\n *\n * function CheckoutButton() {\n * const requestModal = useRequestModal();\n *\n * return (\n * <button onClick={() => requestModal({ template: 'ui://widget/checkout.html' })}>\n * Checkout\n * </button>\n * );\n * }\n * ```\n */\nexport function useRequestModal(): (params: OpenAIRequestModalParams) => Promise<void> {\n const app = useApp();\n return useCallback(\n async (params: OpenAIRequestModalParams) => {\n if (!app) {\n console.warn('[useRequestModal] App not connected');\n return;\n }\n const runtime = getOpenAIRuntime();\n if (!runtime?.requestModal) {\n throw new Error('[useRequestModal] window.openai.requestModal not available');\n }\n return runtime.requestModal(params);\n },\n [app]\n );\n}\n","import { useCallback } from 'react';\nimport { useApp } from '../../hooks/use-app';\nimport {\n getOpenAIRuntime,\n type OpenAICheckoutSession,\n type OpenAICheckoutOrder,\n} from './openai-types';\n\nexport type { OpenAICheckoutSession as CheckoutSession };\nexport type { OpenAICheckoutOrder as CheckoutOrder };\n\n/**\n * Trigger the ChatGPT instant checkout flow.\n *\n * Wraps `window.openai.requestCheckout` which is only available inside\n * ChatGPT. Opens the host checkout UI, handles payment display, and\n * resolves with the finalized order. Rejects on error or user cancel.\n *\n * Import from `sunpeak/host/chatgpt`:\n *\n * @example\n * ```tsx\n * import { useRequestCheckout } from 'sunpeak/host/chatgpt';\n *\n * function BuyButton() {\n * const requestCheckout = useRequestCheckout();\n *\n * const handleBuy = async () => {\n * try {\n * const order = await requestCheckout({\n * id: 'session-1',\n * payment_provider: {\n * provider: 'stripe',\n * merchant_id: 'acct_xxx',\n * supported_payment_methods: ['card', 'apple_pay'],\n * },\n * status: 'ready_for_payment',\n * currency: 'USD',\n * totals: [{ type: 'total', display_text: 'Total', amount: 999 }],\n * links: [],\n * payment_mode: 'live',\n * });\n * console.log('Order completed:', order.id);\n * } catch {\n * console.log('Checkout cancelled or failed');\n * }\n * };\n *\n * return <button onClick={handleBuy}>Buy Now</button>;\n * }\n * ```\n */\nexport function useRequestCheckout(): (\n session: OpenAICheckoutSession\n) => Promise<OpenAICheckoutOrder> {\n const app = useApp();\n return useCallback(\n async (session: OpenAICheckoutSession) => {\n if (!app) {\n throw new Error('[useRequestCheckout] App not connected');\n }\n const runtime = getOpenAIRuntime();\n if (!runtime?.requestCheckout) {\n throw new Error('[useRequestCheckout] window.openai.requestCheckout not available');\n }\n return runtime.requestCheckout(session);\n },\n [app]\n );\n}\n"],"names":["useApp","useCallback"],"mappings":";;;;AAuFO,SAAS,mBAA8C;AAC5D,MAAI,OAAO,WAAW,eAAe,YAAY,QAAQ;AACvD,WAAQ,OAAgD;AAAA,EAC1D;AACA,SAAO;AACT;AC5DO,SAAS,gBAAiE;AAC/E,QAAM,MAAMA,OAAAA,OAAA;AACZ,SAAOC,MAAAA;AAAAA,IACL,OAAO,SAAe;AACpB,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,mCAAmC;AAAA,MACrD;AACA,YAAM,UAAU,iBAAA;AAChB,UAAI,CAAC,SAAS,YAAY;AACxB,cAAM,IAAI,MAAM,wDAAwD;AAAA,MAC1E;AACA,aAAO,QAAQ,WAAW,IAAI;AAAA,IAChC;AAAA,IACA,CAAC,GAAG;AAAA,EAAA;AAER;ACbO,SAAS,wBAE2B;AACzC,QAAM,MAAMD,OAAAA,OAAA;AACZ,SAAOC,MAAAA;AAAAA,IACL,OAAO,WAA+B;AACpC,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,2CAA2C;AAAA,MAC7D;AACA,YAAM,UAAU,iBAAA;AAChB,UAAI,CAAC,SAAS,oBAAoB;AAChC,cAAM,IAAI,MAAM,wEAAwE;AAAA,MAC1F;AACA,aAAO,QAAQ,mBAAmB,MAAM;AAAA,IAC1C;AAAA,IACA,CAAC,GAAG;AAAA,EAAA;AAER;ACrBO,SAAS,kBAAuE;AACrF,QAAM,MAAMD,OAAAA,OAAA;AACZ,SAAOC,MAAAA;AAAAA,IACL,OAAO,WAAqC;AAC1C,UAAI,CAAC,KAAK;AACR,gBAAQ,KAAK,qCAAqC;AAClD;AAAA,MACF;AACA,YAAM,UAAU,iBAAA;AAChB,UAAI,CAAC,SAAS,cAAc;AAC1B,cAAM,IAAI,MAAM,4DAA4D;AAAA,MAC9E;AACA,aAAO,QAAQ,aAAa,MAAM;AAAA,IACpC;AAAA,IACA,CAAC,GAAG;AAAA,EAAA;AAER;ACMO,SAAS,qBAEkB;AAChC,QAAM,MAAMD,OAAAA,OAAA;AACZ,SAAOC,MAAAA;AAAAA,IACL,OAAO,YAAmC;AACxC,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,wCAAwC;AAAA,MAC1D;AACA,YAAM,UAAU,iBAAA;AAChB,UAAI,CAAC,SAAS,iBAAiB;AAC7B,cAAM,IAAI,MAAM,kEAAkE;AAAA,MACpF;AACA,aAAO,QAAQ,gBAAgB,OAAO;AAAA,IACxC;AAAA,IACA,CAAC,GAAG;AAAA,EAAA;AAER;;;;;;"}
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```tsx
|
|
13
|
-
* import { useUploadFile, useRequestModal } from 'sunpeak/
|
|
13
|
+
* import { useUploadFile, useRequestModal } from 'sunpeak/host/chatgpt';
|
|
14
14
|
* ```
|
|
15
15
|
*
|
|
16
16
|
* @see https://developers.openai.com/apps-sdk/mcp-apps-in-chatgpt
|
|
17
|
-
* @module sunpeak/
|
|
17
|
+
* @module sunpeak/host/chatgpt
|
|
18
18
|
*/
|
|
19
19
|
export { getOpenAIRuntime } from './openai-types';
|
|
20
20
|
export type { OpenAIRuntime } from './openai-types';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
import { u as useApp } from "../../use-app-
|
|
2
|
+
import { u as useApp } from "../../use-app-C9gpzIQO.js";
|
|
3
3
|
function getOpenAIRuntime() {
|
|
4
4
|
if (typeof window !== "undefined" && "openai" in window) {
|
|
5
5
|
return window.openai;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/host/chatgpt/openai-types.ts","../../../src/host/chatgpt/use-create-file.ts","../../../src/host/chatgpt/use-file-download.ts","../../../src/host/chatgpt/use-open-modal.ts","../../../src/host/chatgpt/use-request-checkout.ts"],"sourcesContent":["/**\n * TypeScript declarations for the `window.openai` ChatGPT runtime.\n *\n * These APIs are available inside ChatGPT-hosted iframes and provide\n * platform-specific capabilities beyond the MCP Apps standard.\n *\n * @see https://developers.openai.com/apps-sdk/mcp-apps-in-chatgpt\n */\n\nexport interface OpenAIUploadFileResult {\n fileId: string;\n}\n\nexport interface OpenAIFileDownloadUrlResult {\n downloadUrl: string;\n}\n\nexport interface OpenAIRequestModalParams {\n /** URL of an alternate UI template to load in the modal. Omit to reuse the current UI. */\n template?: string;\n /** Arbitrary params forwarded to the modal content. */\n params?: Record<string, unknown>;\n}\n\nexport interface OpenAICheckoutPaymentProvider {\n provider: string;\n merchant_id: string;\n supported_payment_methods: string[];\n}\n\nexport interface OpenAICheckoutTotal {\n type: string;\n display_text: string;\n amount: number;\n}\n\nexport interface OpenAICheckoutLink {\n type: 'terms_of_use' | 'privacy_policy' | string;\n url: string;\n}\n\nexport interface OpenAICheckoutSession {\n id: string;\n payment_provider: OpenAICheckoutPaymentProvider;\n status: 'ready_for_payment';\n currency: string;\n totals: OpenAICheckoutTotal[];\n links: OpenAICheckoutLink[];\n payment_mode: 'live' | 'test';\n}\n\nexport interface OpenAICheckoutOrder {\n id: string;\n checkout_session_id: string;\n status: 'completed' | string;\n permalink_url?: string;\n}\n\n/**\n * The `window.openai` runtime object injected by ChatGPT into hosted iframes.\n */\nexport interface OpenAIRuntime {\n // --- File APIs ---\n uploadFile?(file: File): Promise<OpenAIUploadFileResult>;\n getFileDownloadUrl?(params: { fileId: string }): Promise<OpenAIFileDownloadUrlResult>;\n\n // --- Modal API ---\n requestModal?(params: OpenAIRequestModalParams): Promise<void>;\n\n // --- Checkout API ---\n requestCheckout?(session: OpenAICheckoutSession): Promise<OpenAICheckoutOrder>;\n\n // --- Display ---\n requestClose?(): void;\n requestDisplayMode?(params: { mode: 'inline' | 'PiP' | 'fullscreen' }): Promise<void>;\n\n // --- Messaging ---\n sendFollowUpMessage?(params: { prompt: string }): void;\n\n // --- Other ---\n openExternal?(params: { href: string }): void;\n}\n\n/**\n * Get the `window.openai` runtime if available.\n * Returns `undefined` outside of ChatGPT or in SSR.\n */\nexport function getOpenAIRuntime(): OpenAIRuntime | undefined {\n if (typeof window !== 'undefined' && 'openai' in window) {\n return (window as unknown as { openai: OpenAIRuntime }).openai;\n }\n return undefined;\n}\n","import { useCallback } from 'react';\nimport { useApp } from '../../hooks/use-app';\nimport { getOpenAIRuntime, type OpenAIUploadFileResult } from './openai-types';\n\nexport type { OpenAIUploadFileResult as CreateFileResult };\n\n/**\n * Upload a file from the app UI into the ChatGPT conversation.\n *\n * Wraps `window.openai.uploadFile` which is only available inside ChatGPT.\n * Supported formats: `image/png`, `image/jpeg`, `image/webp`.\n *\n * Import from `sunpeak/host/chatgpt`:\n *\n * @example\n * ```tsx\n * import { useUploadFile } from 'sunpeak/host/chatgpt';\n *\n * function ImageUploader() {\n * const uploadFile = useUploadFile();\n *\n * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n * const file = e.currentTarget.files?.[0];\n * if (!file) return;\n * const { fileId } = await uploadFile(file);\n * console.log('Uploaded:', fileId);\n * };\n *\n * return <input type=\"file\" accept=\"image/*\" onChange={handleChange} />;\n * }\n * ```\n */\nexport function useUploadFile(): (file: File) => Promise<OpenAIUploadFileResult> {\n const app = useApp();\n return useCallback(\n async (file: File) => {\n if (!app) {\n throw new Error('[useUploadFile] App not connected');\n }\n const runtime = getOpenAIRuntime();\n if (!runtime?.uploadFile) {\n throw new Error('[useUploadFile] window.openai.uploadFile not available');\n }\n return runtime.uploadFile(file);\n },\n [app]\n );\n}\n","import { useCallback } from 'react';\nimport { useApp } from '../../hooks/use-app';\nimport { getOpenAIRuntime, type OpenAIFileDownloadUrlResult } from './openai-types';\n\nexport type { OpenAIFileDownloadUrlResult as FileDownloadUrlResult };\n\n/**\n * Get a temporary download URL for a file by its ID.\n *\n * @deprecated Use {@link useDownloadFile} from `sunpeak` instead — it works\n * across all hosts via the MCP Apps SDK `app.downloadFile()` method.\n *\n * Wraps `window.openai.getFileDownloadUrl` which is only available inside\n * ChatGPT. Use this to retrieve URLs for files uploaded via {@link useUploadFile}\n * or file IDs received in tool parameters.\n *\n * Import from `sunpeak/host/chatgpt`:\n *\n * @example\n * ```tsx\n * import { useGetFileDownloadUrl } from 'sunpeak/host/chatgpt';\n *\n * function FilePreview({ fileId }: { fileId: string }) {\n * const getFileDownloadUrl = useGetFileDownloadUrl();\n * const [src, setSrc] = useState<string>();\n *\n * useEffect(() => {\n * getFileDownloadUrl({ fileId }).then(({ downloadUrl }) => setSrc(downloadUrl));\n * }, [fileId, getFileDownloadUrl]);\n *\n * return src ? <img src={src} /> : <p>Loading...</p>;\n * }\n * ```\n */\nexport function useGetFileDownloadUrl(): (params: {\n fileId: string;\n}) => Promise<OpenAIFileDownloadUrlResult> {\n const app = useApp();\n return useCallback(\n async (params: { fileId: string }) => {\n if (!app) {\n throw new Error('[useGetFileDownloadUrl] App not connected');\n }\n const runtime = getOpenAIRuntime();\n if (!runtime?.getFileDownloadUrl) {\n throw new Error('[useGetFileDownloadUrl] window.openai.getFileDownloadUrl not available');\n }\n return runtime.getFileDownloadUrl(params);\n },\n [app]\n );\n}\n","import { useCallback } from 'react';\nimport { useApp } from '../../hooks/use-app';\nimport { getOpenAIRuntime, type OpenAIRequestModalParams } from './openai-types';\n\nexport type { OpenAIRequestModalParams as OpenModalParams };\n\n/**\n * Open a host-controlled modal in ChatGPT.\n *\n * Wraps `window.openai.requestModal` which is only available inside ChatGPT.\n * Pass a `template` URL to load alternate UI content in the modal, or omit\n * it to reuse the current UI.\n *\n * Import from `sunpeak/host/chatgpt`:\n *\n * @example\n * ```tsx\n * import { useRequestModal } from 'sunpeak/host/chatgpt';\n *\n * function CheckoutButton() {\n * const requestModal = useRequestModal();\n *\n * return (\n * <button onClick={() => requestModal({ template: 'ui://widget/checkout.html' })}>\n * Checkout\n * </button>\n * );\n * }\n * ```\n */\nexport function useRequestModal(): (params: OpenAIRequestModalParams) => Promise<void> {\n const app = useApp();\n return useCallback(\n async (params: OpenAIRequestModalParams) => {\n if (!app) {\n console.warn('[useRequestModal] App not connected');\n return;\n }\n const runtime = getOpenAIRuntime();\n if (!runtime?.requestModal) {\n throw new Error('[useRequestModal] window.openai.requestModal not available');\n }\n return runtime.requestModal(params);\n },\n [app]\n );\n}\n","import { useCallback } from 'react';\nimport { useApp } from '../../hooks/use-app';\nimport {\n getOpenAIRuntime,\n type OpenAICheckoutSession,\n type OpenAICheckoutOrder,\n} from './openai-types';\n\nexport type { OpenAICheckoutSession as CheckoutSession };\nexport type { OpenAICheckoutOrder as CheckoutOrder };\n\n/**\n * Trigger the ChatGPT instant checkout flow.\n *\n * Wraps `window.openai.requestCheckout` which is only available inside\n * ChatGPT. Opens the host checkout UI, handles payment display, and\n * resolves with the finalized order. Rejects on error or user cancel.\n *\n * Import from `sunpeak/host/chatgpt`:\n *\n * @example\n * ```tsx\n * import { useRequestCheckout } from 'sunpeak/host/chatgpt';\n *\n * function BuyButton() {\n * const requestCheckout = useRequestCheckout();\n *\n * const handleBuy = async () => {\n * try {\n * const order = await requestCheckout({\n * id: 'session-1',\n * payment_provider: {\n * provider: 'stripe',\n * merchant_id: 'acct_xxx',\n * supported_payment_methods: ['card', 'apple_pay'],\n * },\n * status: 'ready_for_payment',\n * currency: 'USD',\n * totals: [{ type: 'total', display_text: 'Total', amount: 999 }],\n * links: [],\n * payment_mode: 'live',\n * });\n * console.log('Order completed:', order.id);\n * } catch {\n * console.log('Checkout cancelled or failed');\n * }\n * };\n *\n * return <button onClick={handleBuy}>Buy Now</button>;\n * }\n * ```\n */\nexport function useRequestCheckout(): (\n session: OpenAICheckoutSession\n) => Promise<OpenAICheckoutOrder> {\n const app = useApp();\n return useCallback(\n async (session: OpenAICheckoutSession) => {\n if (!app) {\n throw new Error('[useRequestCheckout] App not connected');\n }\n const runtime = getOpenAIRuntime();\n if (!runtime?.requestCheckout) {\n throw new Error('[useRequestCheckout] window.openai.requestCheckout not available');\n }\n return runtime.requestCheckout(session);\n },\n [app]\n );\n}\n"],"names":[],"mappings":";;AAuFO,SAAS,mBAA8C;AAC5D,MAAI,OAAO,WAAW,eAAe,YAAY,QAAQ;AACvD,WAAQ,OAAgD;AAAA,EAC1D;AACA,SAAO;AACT;AC5DO,SAAS,gBAAiE;AAC/E,QAAM,MAAM,OAAA;AACZ,SAAO;AAAA,IACL,OAAO,SAAe;AACpB,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,mCAAmC;AAAA,MACrD;AACA,YAAM,UAAU,iBAAA;AAChB,UAAI,CAAC,SAAS,YAAY;AACxB,cAAM,IAAI,MAAM,wDAAwD;AAAA,MAC1E;AACA,aAAO,QAAQ,WAAW,IAAI;AAAA,IAChC;AAAA,IACA,CAAC,GAAG;AAAA,EAAA;AAER;ACbO,SAAS,wBAE2B;AACzC,QAAM,MAAM,OAAA;AACZ,SAAO;AAAA,IACL,OAAO,WAA+B;AACpC,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,2CAA2C;AAAA,MAC7D;AACA,YAAM,UAAU,iBAAA;AAChB,UAAI,CAAC,SAAS,oBAAoB;AAChC,cAAM,IAAI,MAAM,wEAAwE;AAAA,MAC1F;AACA,aAAO,QAAQ,mBAAmB,MAAM;AAAA,IAC1C;AAAA,IACA,CAAC,GAAG;AAAA,EAAA;AAER;ACrBO,SAAS,kBAAuE;AACrF,QAAM,MAAM,OAAA;AACZ,SAAO;AAAA,IACL,OAAO,WAAqC;AAC1C,UAAI,CAAC,KAAK;AACR,gBAAQ,KAAK,qCAAqC;AAClD;AAAA,MACF;AACA,YAAM,UAAU,iBAAA;AAChB,UAAI,CAAC,SAAS,cAAc;AAC1B,cAAM,IAAI,MAAM,4DAA4D;AAAA,MAC9E;AACA,aAAO,QAAQ,aAAa,MAAM;AAAA,IACpC;AAAA,IACA,CAAC,GAAG;AAAA,EAAA;AAER;ACMO,SAAS,qBAEkB;AAChC,QAAM,MAAM,OAAA;AACZ,SAAO;AAAA,IACL,OAAO,YAAmC;AACxC,UAAI,CAAC,KAAK;AACR,cAAM,IAAI,MAAM,wCAAwC;AAAA,MAC1D;AACA,YAAM,UAAU,iBAAA;AAChB,UAAI,CAAC,SAAS,iBAAiB;AAC7B,cAAM,IAAI,MAAM,kEAAkE;AAAA,MACpF;AACA,aAAO,QAAQ,gBAAgB,OAAO;AAAA,IACxC;AAAA,IACA,CAAC,GAAG;AAAA,EAAA;AAER;"}
|
|
@@ -6,11 +6,11 @@ export type { OpenAIUploadFileResult as CreateFileResult };
|
|
|
6
6
|
* Wraps `window.openai.uploadFile` which is only available inside ChatGPT.
|
|
7
7
|
* Supported formats: `image/png`, `image/jpeg`, `image/webp`.
|
|
8
8
|
*
|
|
9
|
-
* Import from `sunpeak/
|
|
9
|
+
* Import from `sunpeak/host/chatgpt`:
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```tsx
|
|
13
|
-
* import { useUploadFile } from 'sunpeak/
|
|
13
|
+
* import { useUploadFile } from 'sunpeak/host/chatgpt';
|
|
14
14
|
*
|
|
15
15
|
* function ImageUploader() {
|
|
16
16
|
* const uploadFile = useUploadFile();
|
|
@@ -10,11 +10,11 @@ export type { OpenAIFileDownloadUrlResult as FileDownloadUrlResult };
|
|
|
10
10
|
* ChatGPT. Use this to retrieve URLs for files uploaded via {@link useUploadFile}
|
|
11
11
|
* or file IDs received in tool parameters.
|
|
12
12
|
*
|
|
13
|
-
* Import from `sunpeak/
|
|
13
|
+
* Import from `sunpeak/host/chatgpt`:
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
16
|
* ```tsx
|
|
17
|
-
* import { useGetFileDownloadUrl } from 'sunpeak/
|
|
17
|
+
* import { useGetFileDownloadUrl } from 'sunpeak/host/chatgpt';
|
|
18
18
|
*
|
|
19
19
|
* function FilePreview({ fileId }: { fileId: string }) {
|
|
20
20
|
* const getFileDownloadUrl = useGetFileDownloadUrl();
|
|
@@ -7,11 +7,11 @@ export type { OpenAIRequestModalParams as OpenModalParams };
|
|
|
7
7
|
* Pass a `template` URL to load alternate UI content in the modal, or omit
|
|
8
8
|
* it to reuse the current UI.
|
|
9
9
|
*
|
|
10
|
-
* Import from `sunpeak/
|
|
10
|
+
* Import from `sunpeak/host/chatgpt`:
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```tsx
|
|
14
|
-
* import { useRequestModal } from 'sunpeak/
|
|
14
|
+
* import { useRequestModal } from 'sunpeak/host/chatgpt';
|
|
15
15
|
*
|
|
16
16
|
* function CheckoutButton() {
|
|
17
17
|
* const requestModal = useRequestModal();
|
|
@@ -8,11 +8,11 @@ export type { OpenAICheckoutOrder as CheckoutOrder };
|
|
|
8
8
|
* ChatGPT. Opens the host checkout UI, handles payment display, and
|
|
9
9
|
* resolves with the finalized order. Rejects on error or user cancel.
|
|
10
10
|
*
|
|
11
|
-
* Import from `sunpeak/
|
|
11
|
+
* Import from `sunpeak/host/chatgpt`:
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
14
|
* ```tsx
|
|
15
|
-
* import { useRequestCheckout } from 'sunpeak/
|
|
15
|
+
* import { useRequestCheckout } from 'sunpeak/host/chatgpt';
|
|
16
16
|
*
|
|
17
17
|
* function BuyButton() {
|
|
18
18
|
* const requestCheckout = useRequestCheckout();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
function
|
|
3
|
+
function detectHost() {
|
|
4
4
|
if (typeof window === "undefined") {
|
|
5
5
|
return "unknown";
|
|
6
6
|
}
|
|
@@ -17,11 +17,13 @@ function detectPlatform() {
|
|
|
17
17
|
return "unknown";
|
|
18
18
|
}
|
|
19
19
|
function isChatGPT() {
|
|
20
|
-
return
|
|
20
|
+
return detectHost() === "chatgpt";
|
|
21
21
|
}
|
|
22
22
|
function isClaude() {
|
|
23
|
-
return
|
|
23
|
+
return detectHost() === "claude";
|
|
24
24
|
}
|
|
25
|
+
const detectPlatform = detectHost;
|
|
26
|
+
exports.detectHost = detectHost;
|
|
25
27
|
exports.detectPlatform = detectPlatform;
|
|
26
28
|
exports.isChatGPT = isChatGPT;
|
|
27
29
|
exports.isClaude = isClaude;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/host/index.ts"],"sourcesContent":["/**\n * Host detection utilities for MCP Apps.\n *\n * Use these functions to detect which host is running your app\n * and conditionally use host-specific features.\n *\n * @example\n * ```tsx\n * import { isChatGPT } from 'sunpeak/host';\n *\n * function MyResource() {\n * // Only use ChatGPT-specific features when running on ChatGPT\n * if (isChatGPT()) {\n * // Use updateModelContext, etc.\n * }\n * }\n * ```\n */\n\n/**\n * Supported hosts.\n */\nexport type Host = 'chatgpt' | 'claude' | 'unknown';\n\n/**\n * Detect the current host.\n *\n * Detection is based on:\n * 1. Host runtime objects (window.openai for ChatGPT — works in both\n * real hosts and the simulator when the ChatGPT host shell is active)\n * 2. User agent patterns as fallback\n * 3. Hostname matching as final fallback\n *\n * @returns The detected host\n */\nexport function detectHost(): Host {\n // Check if we're in a browser environment\n if (typeof window === 'undefined') {\n return 'unknown';\n }\n\n // ChatGPT injects window.openai; the simulator does the same when\n // the ChatGPT host shell is selected. This is the most reliable signal.\n if ('openai' in window) {\n return 'chatgpt';\n }\n\n // Check user agent patterns for host detection\n const ua = navigator.userAgent.toLowerCase();\n\n // ChatGPT iOS/Android apps and web\n if (ua.includes('chatgpt') || window.location.hostname.includes('chatgpt.com')) {\n return 'chatgpt';\n }\n\n // Claude apps and web\n if (ua.includes('claude') || window.location.hostname.includes('claude.ai')) {\n return 'claude';\n }\n\n return 'unknown';\n}\n\n/**\n * Check if the app is running in a ChatGPT host.\n *\n * @returns true if running in ChatGPT\n *\n * @example\n * ```tsx\n * import { isChatGPT } from 'sunpeak/host';\n *\n * function MyResource() {\n * if (isChatGPT()) {\n * // Use ChatGPT-specific features\n * }\n * }\n * ```\n */\nexport function isChatGPT(): boolean {\n return detectHost() === 'chatgpt';\n}\n\n/**\n * Check if the app is running in a Claude host.\n *\n * @returns true if running in Claude\n */\nexport function isClaude(): boolean {\n return detectHost() === 'claude';\n}\n\n/** @deprecated Use `Host` instead. */\nexport type Platform = Host;\n\n/** @deprecated Use `detectHost` instead. */\nexport const detectPlatform = detectHost;\n"],"names":[],"mappings":";;AAmCO,SAAS,aAAmB;AAEjC,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAIA,MAAI,YAAY,QAAQ;AACtB,WAAO;AAAA,EACT;AAGA,QAAM,KAAK,UAAU,UAAU,YAAA;AAG/B,MAAI,GAAG,SAAS,SAAS,KAAK,OAAO,SAAS,SAAS,SAAS,aAAa,GAAG;AAC9E,WAAO;AAAA,EACT;AAGA,MAAI,GAAG,SAAS,QAAQ,KAAK,OAAO,SAAS,SAAS,SAAS,WAAW,GAAG;AAC3E,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAkBO,SAAS,YAAqB;AACnC,SAAO,iBAAiB;AAC1B;AAOO,SAAS,WAAoB;AAClC,SAAO,iBAAiB;AAC1B;AAMO,MAAM,iBAAiB;;;;;"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Host detection utilities for MCP Apps.
|
|
3
3
|
*
|
|
4
|
-
* Use these functions to detect which host
|
|
5
|
-
* and conditionally use
|
|
4
|
+
* Use these functions to detect which host is running your app
|
|
5
|
+
* and conditionally use host-specific features.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```tsx
|
|
9
|
-
* import { isChatGPT } from 'sunpeak/
|
|
9
|
+
* import { isChatGPT } from 'sunpeak/host';
|
|
10
10
|
*
|
|
11
11
|
* function MyResource() {
|
|
12
12
|
* // Only use ChatGPT-specific features when running on ChatGPT
|
|
@@ -17,21 +17,21 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
/**
|
|
20
|
-
* Supported
|
|
20
|
+
* Supported hosts.
|
|
21
21
|
*/
|
|
22
|
-
export type
|
|
22
|
+
export type Host = 'chatgpt' | 'claude' | 'unknown';
|
|
23
23
|
/**
|
|
24
|
-
* Detect the current host
|
|
24
|
+
* Detect the current host.
|
|
25
25
|
*
|
|
26
26
|
* Detection is based on:
|
|
27
|
-
* 1.
|
|
27
|
+
* 1. Host runtime objects (window.openai for ChatGPT — works in both
|
|
28
28
|
* real hosts and the simulator when the ChatGPT host shell is active)
|
|
29
29
|
* 2. User agent patterns as fallback
|
|
30
30
|
* 3. Hostname matching as final fallback
|
|
31
31
|
*
|
|
32
|
-
* @returns The detected
|
|
32
|
+
* @returns The detected host
|
|
33
33
|
*/
|
|
34
|
-
export declare function
|
|
34
|
+
export declare function detectHost(): Host;
|
|
35
35
|
/**
|
|
36
36
|
* Check if the app is running in a ChatGPT host.
|
|
37
37
|
*
|
|
@@ -39,7 +39,7 @@ export declare function detectPlatform(): Platform;
|
|
|
39
39
|
*
|
|
40
40
|
* @example
|
|
41
41
|
* ```tsx
|
|
42
|
-
* import { isChatGPT } from 'sunpeak/
|
|
42
|
+
* import { isChatGPT } from 'sunpeak/host';
|
|
43
43
|
*
|
|
44
44
|
* function MyResource() {
|
|
45
45
|
* if (isChatGPT()) {
|
|
@@ -55,3 +55,7 @@ export declare function isChatGPT(): boolean;
|
|
|
55
55
|
* @returns true if running in Claude
|
|
56
56
|
*/
|
|
57
57
|
export declare function isClaude(): boolean;
|
|
58
|
+
/** @deprecated Use `Host` instead. */
|
|
59
|
+
export type Platform = Host;
|
|
60
|
+
/** @deprecated Use `detectHost` instead. */
|
|
61
|
+
export declare const detectPlatform: typeof detectHost;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
1
|
+
function detectHost() {
|
|
2
2
|
if (typeof window === "undefined") {
|
|
3
3
|
return "unknown";
|
|
4
4
|
}
|
|
@@ -15,12 +15,14 @@ function detectPlatform() {
|
|
|
15
15
|
return "unknown";
|
|
16
16
|
}
|
|
17
17
|
function isChatGPT() {
|
|
18
|
-
return
|
|
18
|
+
return detectHost() === "chatgpt";
|
|
19
19
|
}
|
|
20
20
|
function isClaude() {
|
|
21
|
-
return
|
|
21
|
+
return detectHost() === "claude";
|
|
22
22
|
}
|
|
23
|
+
const detectPlatform = detectHost;
|
|
23
24
|
export {
|
|
25
|
+
detectHost,
|
|
24
26
|
detectPlatform,
|
|
25
27
|
isChatGPT,
|
|
26
28
|
isClaude
|