sunpeak 0.10.7 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +117 -20
  2. package/bin/commands/build.mjs +1 -11
  3. package/bin/commands/deploy.mjs +7 -1
  4. package/bin/commands/dev.mjs +175 -12
  5. package/bin/commands/new.mjs +211 -0
  6. package/bin/commands/pull.mjs +1 -1
  7. package/bin/commands/push.mjs +36 -13
  8. package/bin/lib/patterns.mjs +25 -0
  9. package/bin/sunpeak.js +15 -232
  10. package/dist/chatgpt/index.cjs +1 -1
  11. package/dist/chatgpt/index.js +1 -1
  12. package/dist/index.cjs +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/mcp/entry.cjs +1 -1
  15. package/dist/mcp/entry.js +1 -1
  16. package/dist/mcp/favicon.d.ts +2 -0
  17. package/dist/mcp/index.cjs +3 -1
  18. package/dist/mcp/index.cjs.map +1 -1
  19. package/dist/mcp/index.d.ts +1 -0
  20. package/dist/mcp/index.js +3 -1
  21. package/dist/{server-BLKltt88.js → server-BI9Y531R.js} +31 -3
  22. package/dist/{server-BLKltt88.js.map → server-BI9Y531R.js.map} +1 -1
  23. package/dist/{server-D_oRdZjX.cjs → server-CcLDAGBE.cjs} +31 -3
  24. package/dist/{server-D_oRdZjX.cjs.map → server-CcLDAGBE.cjs.map} +1 -1
  25. package/dist/{simulator-url-B6DZi3vV.cjs → simulator-url-CYMOGoB1.cjs} +5 -5
  26. package/dist/{simulator-url-B6DZi3vV.cjs.map → simulator-url-CYMOGoB1.cjs.map} +1 -1
  27. package/dist/{simulator-url-izFV6mji.js → simulator-url-DG79-dU3.js} +4 -4
  28. package/dist/{simulator-url-izFV6mji.js.map → simulator-url-DG79-dU3.js.map} +1 -1
  29. package/package.json +1 -1
  30. package/template/.sunpeak/dev.tsx +4 -4
  31. package/template/README.md +25 -27
  32. package/template/_gitignore +4 -0
  33. package/template/dist/albums/albums.json +1 -1
  34. package/template/dist/carousel/carousel.json +1 -1
  35. package/template/dist/map/map.json +1 -1
  36. package/template/dist/review/review.json +1 -1
  37. package/template/index.html +0 -1
  38. package/template/node_modules/.vite/deps/_metadata.json +22 -22
  39. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  40. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Avatar.js +96 -0
  41. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Avatar.js.map +7 -0
  42. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Button.js +625 -0
  43. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Button.js.map +7 -0
  44. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Checkbox.js +33 -0
  45. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Checkbox.js.map +7 -0
  46. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Icon.js +1498 -0
  47. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Icon.js.map +7 -0
  48. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Input.js +13 -0
  49. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Input.js.map +7 -0
  50. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_SegmentedControl.js +103 -0
  51. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_SegmentedControl.js.map +7 -0
  52. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Select.js +3680 -0
  53. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Select.js.map +7 -0
  54. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Textarea.js +95 -0
  55. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_components_Textarea.js.map +7 -0
  56. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_theme.js +45 -0
  57. package/template/node_modules/.vite-mcp/deps_temp_992accd8/@openai_apps-sdk-ui_theme.js.map +7 -0
  58. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-2UDYPUBJ.js +15201 -0
  59. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-2UDYPUBJ.js.map +7 -0
  60. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-6QVG4F2X.js +93 -0
  61. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-6QVG4F2X.js.map +7 -0
  62. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-BUOVMFCD.js +1004 -0
  63. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-BUOVMFCD.js.map +7 -0
  64. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-CNYJBM5F.js +21 -0
  65. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-CNYJBM5F.js.map +7 -0
  66. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-EGRHWZRV.js +1 -0
  67. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-EGRHWZRV.js.map +7 -0
  68. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-ILHRZGIS.js +46 -0
  69. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-ILHRZGIS.js.map +7 -0
  70. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-JAGHY6H6.js +231 -0
  71. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-JAGHY6H6.js.map +7 -0
  72. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-JGVISENQ.js +292 -0
  73. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-JGVISENQ.js.map +7 -0
  74. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-P5LK4A7U.js +112 -0
  75. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-P5LK4A7U.js.map +7 -0
  76. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-QPJAV452.js +13 -0
  77. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-QPJAV452.js.map +7 -0
  78. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-RYYR2YMB.js +111 -0
  79. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-RYYR2YMB.js.map +7 -0
  80. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-UM3ZGDFR.js +4480 -0
  81. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-UM3ZGDFR.js.map +7 -0
  82. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-XZTIOEPG.js +280 -0
  83. package/template/node_modules/.vite-mcp/deps_temp_992accd8/chunk-XZTIOEPG.js.map +7 -0
  84. package/template/node_modules/.vite-mcp/deps_temp_992accd8/clsx.js +10 -0
  85. package/template/node_modules/.vite-mcp/deps_temp_992accd8/clsx.js.map +7 -0
  86. package/template/node_modules/.vite-mcp/deps_temp_992accd8/embla-carousel-react.js +1712 -0
  87. package/template/node_modules/.vite-mcp/deps_temp_992accd8/embla-carousel-react.js.map +7 -0
  88. package/template/node_modules/.vite-mcp/deps_temp_992accd8/embla-carousel-wheel-gestures.js +589 -0
  89. package/template/node_modules/.vite-mcp/deps_temp_992accd8/embla-carousel-wheel-gestures.js.map +7 -0
  90. package/template/node_modules/.vite-mcp/deps_temp_992accd8/mapbox-gl.js +32835 -0
  91. package/template/node_modules/.vite-mcp/deps_temp_992accd8/mapbox-gl.js.map +7 -0
  92. package/template/node_modules/.vite-mcp/deps_temp_992accd8/package.json +3 -0
  93. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react-dom.js +7 -0
  94. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react-dom.js.map +7 -0
  95. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react-dom_client.js +20217 -0
  96. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react-dom_client.js.map +7 -0
  97. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react.js +6 -0
  98. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react.js.map +7 -0
  99. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react_jsx-dev-runtime.js +278 -0
  100. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react_jsx-dev-runtime.js.map +7 -0
  101. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react_jsx-runtime.js +7 -0
  102. package/template/node_modules/.vite-mcp/deps_temp_992accd8/react_jsx-runtime.js.map +7 -0
  103. package/template/node_modules/.vite-mcp/deps_temp_992accd8/tailwind-merge.js +3095 -0
  104. package/template/node_modules/.vite-mcp/deps_temp_992accd8/tailwind-merge.js.map +7 -0
  105. package/template/package.json +0 -1
  106. package/template/vitest.config.ts +1 -1
  107. package/bin/commands/mcp.mjs +0 -244
  108. package/template/public/favicon.ico +0 -0
  109. package/template/src/resources/albums/albums-show-simulation.json +0 -131
  110. package/template/src/resources/carousel/carousel-show-simulation.json +0 -68
  111. package/template/src/resources/map/map-show-simulation.json +0 -123
  112. package/template/src/resources/review/review-diff-simulation.json +0 -80
  113. package/template/src/resources/review/review-post-simulation.json +0 -56
  114. package/template/src/resources/review/review-purchase-simulation.json +0 -88
  115. /package/template/{src/test → tests}/setup.ts +0 -0
  116. /package/template/{dist → tests/simulations}/albums/albums-show-simulation.json +0 -0
  117. /package/template/{dist → tests/simulations}/carousel/carousel-show-simulation.json +0 -0
  118. /package/template/{dist → tests/simulations}/map/map-show-simulation.json +0 -0
  119. /package/template/{dist → tests/simulations}/review/review-diff-simulation.json +0 -0
  120. /package/template/{dist → tests/simulations}/review/review-post-simulation.json +0 -0
  121. /package/template/{dist → tests/simulations}/review/review-purchase-simulation.json +0 -0
package/README.md CHANGED
@@ -17,13 +17,14 @@
17
17
 
18
18
  The ChatGPT App framework.
19
19
 
20
- Quickstart, build, test, and ship your ChatGPT App locally!
20
+ Quickstart, build, test, and ship your ChatGPT Applocally!
21
21
 
22
22
  [Demo (Hosted)](https://sunpeak.ai/#simulator) ~
23
23
  [Demo (Video)](https://d10djik02wlf6x.cloudfront.net/sunpeak-demo-prod.mp4) ~
24
24
  [Discord (NEW)](https://discord.gg/FB2QNXqRnw) ~
25
25
  [Documentation](https://docs.sunpeak.ai/) ~
26
- [GitHub](https://github.com/Sunpeak-AI/sunpeak)
26
+ [GitHub](https://github.com/Sunpeak-AI/sunpeak) ~
27
+ [Resource Repository](https://app.sunpeak.ai/)
27
28
 
28
29
  <div align="center">
29
30
  <a href="https://docs.sunpeak.ai/library/chatgpt-simulator">
@@ -35,8 +36,6 @@ Quickstart, build, test, and ship your ChatGPT App locally!
35
36
 
36
37
  ## Quickstart
37
38
 
38
- ### New Projects
39
-
40
39
  Requirements: Node (20+), pnpm (10+)
41
40
 
42
41
  ```bash
@@ -48,29 +47,69 @@ To add sunpeak to an existing project, refer to the [documentation](https://docs
48
47
 
49
48
  ## Overview
50
49
 
51
- sunpeak is an npm package consisting of:
50
+ sunpeak is an npm package that helps you build ChatGPT Apps (MCP Resources) while keeping your MCP server client-agnostic. sunpeak consists of:
51
+
52
+ ### The `sunpeak` library (`./src`)
53
+
54
+ 1. Runtime APIs: Strongly typed APIs for interacting with the ChatGPT runtime, **architected to support future platforms** (Gemini, Claude, etc.).
55
+ 2. ChatGPT simulator: React component replicating ChatGPT's runtime to **test Apps locally and automatically**.
56
+ 3. MCP server: Serve Resources with mock data to the real ChatGPT with HMR (**no more cache issues or 5-click manual refreshes**).
57
+
58
+ ### The `sunpeak` framework (`./template`)
59
+
60
+ Next.js for ChatGPT Apps. Using a Review page as an example, sunpeak projects look like:
61
+
62
+ ```bash
63
+ my-app/
64
+ ├── src/resources/
65
+ │ └── review/
66
+ │ ├── review-resource.tsx # Review UI component.
67
+ │ └── review-resource.json # Review UI MCP metadata.
68
+ ├── tests/simulations/
69
+ │ └── review/
70
+ │ ├── review-{scenario1}-simulation.json # Mock state for testing.
71
+ │ └── review-{scenario2}-simulation.json # Mock state for testing.
72
+ └── package.json
73
+ ```
74
+
75
+ 1. Project scaffold: Complete development setup with the sunpeak library.
76
+ 2. UI components: Production-ready components following ChatGPT design guidelines and using OpenAI apps-sdk-ui React components.
77
+ 3. Convention over configuration:
78
+ 1. Create UIs by creating a `-resource.tsx` component file ([example](#resource-component)) and `-resource.json` MCP metadata file ([example](#resource-mcp-metadata)).
79
+ 2. Create test state (simulations) for local dev, ChatGPT dev, automated testing, and demos by creating a `-simulation.json` file. ([example](#simulation))
52
80
 
53
- 1. **The `sunpeak` library** (`./src`). An npm package for running & testing ChatGPT Apps. This standalone library contains:
54
- 1. Runtime APIs - Strongly typed, multi-platform APIs for interacting with the ChatGPT runtime, architected to support future platforms (Gemini, Claude).
55
- 2. ChatGPT simulator - React component replicating ChatGPT's runtime.
56
- 3. MCP server - Mock data MCP server for testing local UIs (resources) in the real ChatGPT.
57
- 2. **The `sunpeak` framework** (`./template`). Next.js for ChatGPT Apps. This templated npm package includes:
58
- 1. Project scaffold - Complete development setup with build, test, and mcp tooling, including the sunpeak library.
59
- 2. UI components - Production-ready components following ChatGPT design guidelines and using OpenAI apps-sdk-ui React components.
60
- 3. Convention over configuration - Create UIs (resources) by simply creating a `src/resources/NAME/NAME-resource.tsx` React file and `src/resources/NAME/NAME-resource.json` metadata file.
61
- 3. **The `sunpeak` CLI** (`./bin`). Commands for managing ChatGPT Apps. Includes a client for the [sunpeak Resource Repository](https://app.sunpeak.ai/) (ECR for ChatGPT Apps). The repository helps you & your CI/CD decouple your App from your client-agnostic MCP server:
62
- 1. Tag your app builds with version numbers and environment names (like `v1.0.0` and `prod`)
63
- 2. `push` built Apps to a central location
64
- 3. `pull` built Apps to be run in different environments
81
+ ### The `sunpeak` CLI (`./bin`)
65
82
 
66
- Note that each `sunpeak` component can be used in isolation if preferred, though the most seamless experience combines all 3.
83
+ Commands for managing ChatGPT Apps. Includes a client for the [sunpeak Resource Repository](https://app.sunpeak.ai/). The repository helps you & your CI/CD decouple your App from your client-agnostic MCP server while also providing a hosted runtime to collaborate, demo, and share your ChatGPT Apps.
67
84
 
68
- ## Example Component
85
+ Think Docker Hub for ChatGPT Apps:
86
+
87
+ <div align="center">
88
+ <a href="https://docs.sunpeak.ai/library/chatgpt-simulator">
89
+ <picture>
90
+ <img alt="ChatGPT Resource Repository" src="https://d10djik02wlf6x.cloudfront.net/blog/storybook-for-chatgpt-apps.png">
91
+ </picture>
92
+ </a>
93
+ </div>
94
+
95
+ 1. Tag your app builds with version numbers and environment names (like `v1.0.0` and `prod`)
96
+ 2. `push` built Apps to a central location
97
+ 3. `pull` built Apps to be run in different environments, like your production MCP server.
98
+ 4. Share your fully-functional demo Apps with teammates, prospects, and strangers!
99
+
100
+ ## Examples
101
+
102
+ Example sunpeak resource & simulation files for an MCP Resource called "Review".
103
+
104
+ ### Resource Component
105
+
106
+ React component defining a UI (MCP Resource) in your ChatGPT App.
69
107
 
70
108
  ```tsx
109
+ // src/resources/review-resource.tsx
71
110
  import { Card } from './components';
72
111
 
73
- export function MCPResource() {
112
+ export function ReviewResource() {
74
113
  return (
75
114
  <Card
76
115
  image="https://images.unsplash.com/photo-1520950237264-dfe336995c34"
@@ -86,6 +125,64 @@ export function MCPResource() {
86
125
  }
87
126
  ```
88
127
 
128
+ ### Resource MCP Metadata
129
+
130
+ MCP metadata for your UI. Version your resource metadata alongside the resource itself.
131
+
132
+ This is just an official [MCP Resource object](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource).
133
+
134
+ ```json
135
+ {
136
+ // src/resources/review-resource.json
137
+ "name": "review",
138
+ "title": "Review",
139
+ "description": "Visualize and review a proposed set of changes or actions",
140
+ "mimeType": "text/html+skybridge",
141
+ "_meta": {
142
+ "openai/widgetDomain": "https://sunpeak.ai",
143
+ "openai/widgetCSP": {
144
+ "resource_domains": ["https://cdn.openai.com"]
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### Simulation
151
+
152
+ `sunpeak` object. Testing a ChatGPT App require setting a lot of state: state in your backend (accessed via MCP tool), the stored widget runtime, and ChatGPT itself.
153
+
154
+ Simulation files let you define key App states for development, automated testing, and demo purposes.
155
+
156
+ Simulation files contain an [official MCP Tool object](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool) and an [official MCP CallToolResult object](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#structured-content). ChatGPT state (like light/dark mode) is not set on the simulation, but rather on the sunpeak `ChatGPTSimulator` itself via UI, props, or URL params.
157
+
158
+ ```json
159
+ {
160
+ // tests/simulations/review-diff-simulation.json
161
+ "userMessage": "Refactor the authentication module", // Simulator styling.
162
+ "tool": {
163
+ // Official MCP Tool object.
164
+ "name": "diff-review",
165
+ "description": "Show a review dialog for a proposed code diff",
166
+ "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
167
+ "title": "Diff Review",
168
+ "annotations": { "readOnlyHint": false },
169
+ "_meta": {
170
+ "openai/toolInvocation/invoking": "Preparing changes",
171
+ "openai/toolInvocation/invoked": "Changes ready for review",
172
+ "openai/widgetAccessible": true,
173
+ "openai/resultCanProduceWidget": true
174
+ }
175
+ },
176
+ "callToolResult": {
177
+ // Official MCP CallToolResult object.
178
+ "structuredContent": {
179
+ // ...
180
+ },
181
+ "_meta": {}
182
+ }
183
+ }
184
+ ```
185
+
89
186
  ## Resources
90
187
 
91
188
  - [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
@@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react';
4
4
  import tailwindcss from '@tailwindcss/vite';
5
5
  import { existsSync, rmSync, readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from 'fs';
6
6
  import path from 'path';
7
- import { toPascalCase, isSimulationFile } from '../lib/patterns.mjs';
7
+ import { toPascalCase } from '../lib/patterns.mjs';
8
8
 
9
9
  /**
10
10
  * Build all resources for a Sunpeak project
@@ -241,16 +241,6 @@ export async function build(projectRoot = process.cwd()) {
241
241
  process.exit(1);
242
242
  }
243
243
 
244
- // Copy affiliated simulation files (matching {resource}-*-simulation.json pattern)
245
- const simulationFiles = readdirSync(resourceDir)
246
- .filter(file => isSimulationFile(file, kebabName));
247
-
248
- for (const simFile of simulationFiles) {
249
- const srcSim = path.join(resourceDir, simFile);
250
- const destSim = path.join(distOutDir, simFile);
251
- copyFileSync(srcSim, destSim);
252
- console.log(`✓ Copied ${kebabName}/${simFile}`);
253
- }
254
244
  }
255
245
 
256
246
  // Clean up temp and build directories
@@ -28,6 +28,7 @@ Usage:
28
28
  Options:
29
29
  -r, --repository <owner/repo> Repository name (defaults to git remote origin)
30
30
  -t, --tag <name> Additional tag(s) to assign (always includes "prod")
31
+ --no-simulations Skip pushing simulations, only push resources
31
32
  -h, --help Show this help message
32
33
 
33
34
  Arguments:
@@ -40,6 +41,7 @@ Examples:
40
41
  sunpeak deploy dist/carousel Deploy a single resource
41
42
  sunpeak deploy -r myorg/my-app Deploy to "myorg/my-app" repository
42
43
  sunpeak deploy -t v1.0 Deploy with "prod" and "v1.0" tags
44
+ sunpeak deploy --no-simulations Deploy without simulations
43
45
 
44
46
  This command is equivalent to: sunpeak push --tag prod
45
47
  `);
@@ -75,7 +77,7 @@ This command is equivalent to: sunpeak push --tag prod
75
77
  /**
76
78
  * Parse command line arguments
77
79
  */
78
- function parseArgs(args) {
80
+ export function parseArgs(args) {
79
81
  const options = { tags: [] };
80
82
  let i = 0;
81
83
 
@@ -86,6 +88,8 @@ function parseArgs(args) {
86
88
  options.repository = args[++i];
87
89
  } else if (arg === '--tag' || arg === '-t') {
88
90
  options.tags.push(args[++i]);
91
+ } else if (arg === '--no-simulations') {
92
+ options.noSimulations = true;
89
93
  } else if (arg === '--help' || arg === '-h') {
90
94
  console.log(`
91
95
  sunpeak deploy - Deploy resources to production (push with "prod" tag)
@@ -96,6 +100,7 @@ Usage:
96
100
  Options:
97
101
  -r, --repository <owner/repo> Repository name (defaults to git remote origin)
98
102
  -t, --tag <name> Additional tag(s) to assign (always includes "prod")
103
+ --no-simulations Skip pushing simulations, only push resources
99
104
  -h, --help Show this help message
100
105
 
101
106
  Arguments:
@@ -108,6 +113,7 @@ Examples:
108
113
  sunpeak deploy dist/carousel Deploy a single resource
109
114
  sunpeak deploy -r myorg/my-app Deploy to "myorg/my-app" repository
110
115
  sunpeak deploy -t v1.0 Deploy with "prod" and "v1.0" tags
116
+ sunpeak deploy --no-simulations Deploy without simulations
111
117
 
112
118
  This command is equivalent to: sunpeak push --tag prod
113
119
  `);
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from 'fs';
3
- import { join, resolve, basename, dirname } from 'path';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ const { existsSync, readFileSync } = fs;
5
+ const { join, resolve, basename, dirname } = path;
4
6
  import { createRequire } from 'module';
5
7
  import { pathToFileURL } from 'url';
6
8
 
@@ -81,16 +83,51 @@ export async function dev(projectRoot = process.cwd(), args = []) {
81
83
  port = parseInt(args[portArgIndex + 1]);
82
84
  }
83
85
 
86
+ // Parse --no-begging flag
87
+ const noBegging = args.includes('--no-begging');
88
+
84
89
  console.log(`Starting Vite dev server on port ${port}...`);
85
90
 
86
91
  // Check if we're in the sunpeak workspace (directory is named "template")
87
92
  const isTemplate = basename(projectRoot) === 'template';
88
93
  const parentSrc = resolve(projectRoot, '../src');
89
94
 
95
+ // Import sunpeak modules (MCP server and discovery utilities)
96
+ let sunpeakMcp, sunpeak;
97
+ if (isTemplate) {
98
+ // In workspace dev mode, import from local dist folder
99
+ sunpeakMcp = await import(pathToFileURL(resolve(projectRoot, '../dist/mcp/index.js')).href);
100
+ sunpeak = await import(pathToFileURL(resolve(projectRoot, '../dist/index.js')).href);
101
+ } else {
102
+ // Import from installed sunpeak package
103
+ const sunpeakBase = require.resolve('sunpeak').replace(/dist\/index\.(c)?js$/, '');
104
+ sunpeakMcp = await import(pathToFileURL(join(sunpeakBase, 'dist/mcp/index.js')).href);
105
+ sunpeak = await import(pathToFileURL(join(sunpeakBase, 'dist/index.js')).href);
106
+ }
107
+ const { FAVICON_BUFFER: faviconBuffer, runMCPServer } = sunpeakMcp;
108
+ const { findResourceDirs, findSimulationFiles } = sunpeak;
109
+
110
+ // Vite plugin to serve the sunpeak favicon
111
+ const sunpeakFaviconPlugin = () => ({
112
+ name: 'sunpeak-favicon',
113
+ configureServer(server) {
114
+ server.middlewares.use((req, res, next) => {
115
+ if (req.url === '/favicon.ico') {
116
+ res.setHeader('Content-Type', 'image/png');
117
+ res.setHeader('Content-Length', faviconBuffer.length);
118
+ res.setHeader('Cache-Control', 'public, max-age=86400');
119
+ res.end(faviconBuffer);
120
+ return;
121
+ }
122
+ next();
123
+ });
124
+ },
125
+ });
126
+
90
127
  // Create and start Vite dev server programmatically
91
128
  const server = await createServer({
92
129
  root: projectRoot,
93
- plugins: [react(), tailwindcss()],
130
+ plugins: [react(), tailwindcss(), sunpeakFaviconPlugin()],
94
131
  resolve: {
95
132
  alias: {
96
133
  // In workspace dev mode, use local sunpeak source
@@ -109,16 +146,142 @@ export async function dev(projectRoot = process.cwd(), args = []) {
109
146
  server.printUrls();
110
147
  server.bindCLIShortcuts({ print: true });
111
148
 
112
- // Handle signals
113
- process.on('SIGINT', async () => {
114
- await server.close();
115
- process.exit(0);
116
- });
149
+ // Print star-begging message unless --no-begging is set
150
+ if (!noBegging) {
151
+ // #FFB800 in 24-bit ANSI color
152
+ console.log('\n\n\x1b[38;2;255;184;0m\u2b50\ufe0f \u2192 \u2764\ufe0f https://github.com/Sunpeak-AI/sunpeak\x1b[0m\n');
153
+ }
117
154
 
118
- process.on('SIGTERM', async () => {
119
- await server.close();
120
- process.exit(0);
121
- });
155
+ // Discover simulations using sunpeak's discovery utilities
156
+ const resourcesDir = join(projectRoot, 'src/resources');
157
+ const simulationsDir = join(projectRoot, 'tests/simulations');
158
+ const resourceDirs = findResourceDirs(resourcesDir, (key) => `${key}-resource.json`, fs);
159
+
160
+ const simulations = [];
161
+ for (const { key: resourceKey, dir: resourceDir, resourcePath } of resourceDirs) {
162
+ const resource = JSON.parse(readFileSync(resourcePath, 'utf-8'));
163
+ const resourceSimDir = join(simulationsDir, resourceKey);
164
+ const simulationFiles = findSimulationFiles(resourceSimDir, resourceKey, fs);
165
+
166
+ for (const { filename, path: simPath } of simulationFiles) {
167
+ const simulationKey = filename.replace(/-simulation\.json$/, '');
168
+ const simulation = JSON.parse(readFileSync(simPath, 'utf-8'));
169
+
170
+ simulations.push({
171
+ ...simulation,
172
+ name: simulationKey,
173
+ distPath: join(projectRoot, `dist/${resourceKey}/${resourceKey}.js`),
174
+ srcPath: `/src/resources/${resourceKey}/${resourceKey}-resource.tsx`,
175
+ resource,
176
+ });
177
+ }
178
+ }
179
+
180
+ // Start MCP server with its own Vite instance
181
+ if (simulations.length > 0) {
182
+ console.log(`\nStarting MCP server with ${simulations.length} simulation(s)...`);
183
+
184
+ // Virtual entry module plugin for MCP
185
+ const sunpeakEntryPlugin = () => ({
186
+ name: 'sunpeak-entry',
187
+ resolveId(id) {
188
+ if (id.startsWith('virtual:sunpeak-entry')) {
189
+ return id;
190
+ }
191
+ },
192
+ load(id) {
193
+ if (id.startsWith('virtual:sunpeak-entry')) {
194
+ const url = new URL(id.replace('virtual:sunpeak-entry', 'http://x'));
195
+ const srcPath = url.searchParams.get('src');
196
+ const componentName = url.searchParams.get('component');
197
+
198
+ if (!srcPath || !componentName) {
199
+ return 'console.error("Missing src or component param");';
200
+ }
201
+
202
+ return `
203
+ import { createElement } from 'react';
204
+ import { createRoot } from 'react-dom/client';
205
+ import * as ResourceModule from '${srcPath}';
206
+
207
+ const Component = ResourceModule.default || ResourceModule['${componentName}'];
208
+ if (!Component) {
209
+ document.getElementById('root').innerHTML = '<pre style="color:red;padding:16px">Component not found: ${componentName}\\nExports: ' + Object.keys(ResourceModule).join(', ') + '</pre>';
210
+ } else {
211
+ createRoot(document.getElementById('root')).render(createElement(Component));
212
+ }
213
+ `;
214
+ }
215
+ },
216
+ });
217
+
218
+ // Create Vite dev server in middleware mode for MCP
219
+ // Use separate cache directory to avoid conflicts with main dev server
220
+ const mcpViteServer = await createServer({
221
+ root: projectRoot,
222
+ cacheDir: 'node_modules/.vite-mcp',
223
+ plugins: [react(), tailwindcss(), sunpeakEntryPlugin()],
224
+ resolve: {
225
+ alias: {
226
+ ...(isTemplate && {
227
+ sunpeak: parentSrc,
228
+ }),
229
+ },
230
+ },
231
+ server: {
232
+ middlewareMode: true,
233
+ allowedHosts: true,
234
+ watch: {
235
+ // Only watch files that affect the UI bundle (not JSON, tests, etc.)
236
+ // MCP resources reload on next tool call, not on file change
237
+ ignored: (filePath) => {
238
+ if (!filePath.includes('.')) return false; // Watch directories
239
+ if (/\.(tsx?|css)$/.test(filePath)) {
240
+ return /\.(test|spec)\.tsx?$/.test(filePath); // Ignore tests
241
+ }
242
+ return true; // Ignore everything else
243
+ },
244
+ },
245
+ },
246
+ optimizeDeps: {
247
+ include: ['react', 'react-dom/client'],
248
+ },
249
+ appType: 'custom',
250
+ });
251
+
252
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
253
+ runMCPServer({
254
+ name: pkg.name || 'Sunpeak',
255
+ version: pkg.version || '0.1.0',
256
+ simulations,
257
+ port: 6766,
258
+ viteServer: mcpViteServer,
259
+ });
260
+
261
+ // Handle signals - close both servers
262
+ process.on('SIGINT', async () => {
263
+ await mcpViteServer.close();
264
+ await server.close();
265
+ process.exit(0);
266
+ });
267
+
268
+ process.on('SIGTERM', async () => {
269
+ await mcpViteServer.close();
270
+ await server.close();
271
+ process.exit(0);
272
+ });
273
+ } else {
274
+ // No simulations - just handle signals for the dev server
275
+ process.on('SIGINT', async () => {
276
+ await server.close();
277
+ process.exit(0);
278
+ });
279
+
280
+ process.on('SIGTERM', async () => {
281
+ await server.close();
282
+ process.exit(0);
283
+ });
284
+ }
122
285
  }
123
286
 
124
287
  // Allow running directly
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, renameSync } from 'fs';
3
+ import { join, dirname, basename } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { createInterface } from 'readline';
6
+ import { discoverResources } from '../lib/patterns.mjs';
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+
10
+ /**
11
+ * Default prompt implementation using readline
12
+ * @param {string} question
13
+ * @returns {Promise<string>}
14
+ */
15
+ function defaultPrompt(question) {
16
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
17
+ return new Promise((resolve) => {
18
+ rl.question(question, (answer) => {
19
+ rl.close();
20
+ resolve(answer.trim());
21
+ });
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Default dependencies (real implementations)
27
+ */
28
+ export const defaultDeps = {
29
+ discoverResources,
30
+ existsSync,
31
+ mkdirSync,
32
+ cpSync,
33
+ readFileSync,
34
+ writeFileSync,
35
+ renameSync,
36
+ prompt: defaultPrompt,
37
+ console,
38
+ process,
39
+ cwd: () => process.cwd(),
40
+ templateDir: join(__dirname, '..', '..', 'template'),
41
+ rootPkgPath: join(__dirname, '..', '..', 'package.json'),
42
+ };
43
+
44
+ /**
45
+ * Parse and validate resources input
46
+ * @param {string} input - Comma or space separated resource names
47
+ * @param {string[]} validResources - List of valid resource names
48
+ * @param {Object} deps - Dependencies for testing
49
+ * @returns {string[]} - Validated and deduplicated resource names
50
+ */
51
+ export function parseResourcesInput(input, validResources, deps = defaultDeps) {
52
+ const d = { ...defaultDeps, ...deps };
53
+
54
+ // If no input, return all resources
55
+ if (!input || input.trim() === '') {
56
+ return validResources;
57
+ }
58
+
59
+ // Split by comma or space and trim
60
+ const tokens = input
61
+ .toLowerCase()
62
+ .split(/[,\s]+/)
63
+ .map((s) => s.trim())
64
+ .filter((s) => s.length > 0);
65
+
66
+ // Validate tokens
67
+ const invalid = tokens.filter((t) => !validResources.includes(t));
68
+ if (invalid.length > 0) {
69
+ d.console.error(`Error: Invalid resource(s): ${invalid.join(', ')}`);
70
+ d.console.error(`Valid resources are: ${validResources.join(', ')}`);
71
+ d.process.exit(1);
72
+ }
73
+
74
+ // Remove duplicates
75
+ return [...new Set(tokens)];
76
+ }
77
+
78
+ /**
79
+ * Create a new Sunpeak project
80
+ * @param {string} projectName - Name of the project directory
81
+ * @param {string} resourcesArg - Optional comma/space separated resources to include
82
+ * @param {Object} deps - Dependencies for testing
83
+ */
84
+ export async function init(projectName, resourcesArg, deps = defaultDeps) {
85
+ const d = { ...defaultDeps, ...deps };
86
+
87
+ // Discover available resources from template
88
+ const availableResources = d.discoverResources();
89
+ if (availableResources.length === 0) {
90
+ d.console.error('Error: No resources found in template/src/resources/');
91
+ d.process.exit(1);
92
+ }
93
+
94
+ if (!projectName) {
95
+ projectName = await d.prompt('☀️ 🏔️ Project name [my-app]: ');
96
+ if (!projectName) {
97
+ projectName = 'my-app';
98
+ }
99
+ }
100
+
101
+ if (projectName === 'template') {
102
+ d.console.error('Error: "template" is a reserved name. Please choose another name.');
103
+ d.process.exit(1);
104
+ }
105
+
106
+ // Use resources from args or ask for them
107
+ let resourcesInput;
108
+ if (resourcesArg) {
109
+ resourcesInput = resourcesArg;
110
+ d.console.log(`☀️ 🏔️ Resources: ${resourcesArg}`);
111
+ } else {
112
+ resourcesInput = await d.prompt(
113
+ `☀️ 🏔️ Resources (UIs) to include [${availableResources.join(', ')}]: `
114
+ );
115
+ }
116
+ const selectedResources = parseResourcesInput(resourcesInput, availableResources, d);
117
+
118
+ const targetDir = join(d.cwd(), projectName);
119
+
120
+ if (d.existsSync(targetDir)) {
121
+ d.console.error(`Error: Directory "${projectName}" already exists`);
122
+ d.process.exit(1);
123
+ }
124
+
125
+ d.console.log(`☀️ 🏔️ Creating ${projectName}...`);
126
+
127
+ d.mkdirSync(targetDir, { recursive: true });
128
+
129
+ // Filter resource directories based on selection
130
+ const excludedResources = availableResources.filter((r) => !selectedResources.includes(r));
131
+
132
+ d.cpSync(d.templateDir, targetDir, {
133
+ recursive: true,
134
+ filter: (src) => {
135
+ const name = basename(src);
136
+
137
+ // Skip node_modules and lock file
138
+ if (name === 'node_modules' || name === 'pnpm-lock.yaml') {
139
+ return false;
140
+ }
141
+
142
+ for (const resource of excludedResources) {
143
+ // Skip entire resource directory: src/resources/{resource}/
144
+ if (src.includes('/resources/') && name === resource) {
145
+ return false;
146
+ }
147
+ // Skip simulation directories for excluded resources: tests/simulations/{resource}/
148
+ if (src.includes('/tests/simulations/') && name === resource) {
149
+ return false;
150
+ }
151
+ // Skip e2e test files for excluded resources
152
+ if (src.includes('/tests/e2e/') && name === `${resource}.spec.ts`) {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ return true;
158
+ },
159
+ });
160
+
161
+ // Rename underscore-prefixed files to dotfiles
162
+ const dotfiles = ['_gitignore', '_prettierignore', '_prettierrc'];
163
+ for (const file of dotfiles) {
164
+ const srcPath = join(targetDir, file);
165
+ const destPath = join(targetDir, file.replace(/^_/, '.'));
166
+ if (d.existsSync(srcPath)) {
167
+ d.renameSync(srcPath, destPath);
168
+ }
169
+ }
170
+
171
+ // Read sunpeak version from root package.json
172
+ const rootPkg = JSON.parse(d.readFileSync(d.rootPkgPath, 'utf-8'));
173
+ const sunpeakVersion = `^${rootPkg.version}`;
174
+
175
+ // Update project package.json
176
+ const pkgPath = join(targetDir, 'package.json');
177
+ const pkg = JSON.parse(d.readFileSync(pkgPath, 'utf-8'));
178
+ pkg.name = projectName;
179
+
180
+ // Replace workspace:* with actual version
181
+ if (pkg.dependencies?.sunpeak === 'workspace:*') {
182
+ pkg.dependencies.sunpeak = sunpeakVersion;
183
+ }
184
+
185
+ d.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
186
+
187
+ d.console.log(`
188
+ Done! To get started:
189
+
190
+ cd ${projectName}
191
+ pnpm install
192
+ sunpeak dev
193
+
194
+ That's it! Your project commands:
195
+
196
+ sunpeak dev # Start dev server + MCP endpoint
197
+ sunpeak build # Build for production
198
+ pnpm test # Run tests
199
+
200
+ See README.md for more details.
201
+ `);
202
+ }
203
+
204
+ // Allow running directly
205
+ if (import.meta.url === `file://${process.argv[1]}`) {
206
+ const [projectName, resources] = process.argv.slice(2);
207
+ init(projectName, resources).catch((error) => {
208
+ console.error('Error:', error.message);
209
+ process.exit(1);
210
+ });
211
+ }
@@ -216,7 +216,7 @@ Examples:
216
216
  /**
217
217
  * Parse command line arguments
218
218
  */
219
- function parseArgs(args) {
219
+ export function parseArgs(args) {
220
220
  const options = {};
221
221
  let i = 0;
222
222