sunpeak 0.11.1 → 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.
- package/README.md +84 -12
- package/bin/commands/build.mjs +1 -11
- package/bin/commands/deploy.mjs +7 -1
- package/bin/commands/dev.mjs +3 -1
- package/bin/commands/new.mjs +211 -0
- package/bin/commands/pull.mjs +1 -1
- package/bin/commands/push.mjs +36 -13
- package/bin/lib/patterns.mjs +25 -0
- package/bin/sunpeak.js +13 -222
- package/package.json +1 -1
- package/template/.sunpeak/dev.tsx +4 -4
- package/template/README.md +21 -22
- package/template/dist/albums/albums.json +1 -1
- package/template/dist/carousel/carousel.json +1 -1
- package/template/dist/map/map.json +1 -1
- package/template/dist/review/review.json +1 -1
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Button.js +7 -7
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_SegmentedControl.js +1 -1
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +7 -7
- package/template/node_modules/.vite/deps/_metadata.json +30 -30
- package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/template/vitest.config.ts +1 -1
- package/template/src/resources/albums/albums-show-simulation.json +0 -131
- package/template/src/resources/carousel/carousel-show-simulation.json +0 -68
- package/template/src/resources/map/map-show-simulation.json +0 -123
- package/template/src/resources/review/review-diff-simulation.json +0 -80
- package/template/src/resources/review/review-post-simulation.json +0 -56
- package/template/src/resources/review/review-purchase-simulation.json +0 -88
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Avatar.js +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Avatar.js.map +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Button.js +10 -10
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Button.js.map +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Checkbox.js +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Checkbox.js.map +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Icon.js +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Icon.js.map +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Input.js +1 -1
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Input.js.map +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_SegmentedControl.js +4 -4
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_SegmentedControl.js.map +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Select.js +9 -9
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Select.js.map +0 -0
- package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Textarea.js +1 -1
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_components_Textarea.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_theme.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/@openai_apps-sdk-ui_theme.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-2UDYPUBJ.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-2UDYPUBJ.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-6QVG4F2X.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-6QVG4F2X.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-BUOVMFCD.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-BUOVMFCD.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-CNYJBM5F.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-CNYJBM5F.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-EGRHWZRV.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-EGRHWZRV.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-ILHRZGIS.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-ILHRZGIS.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-JAGHY6H6.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-JAGHY6H6.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-JGVISENQ.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-JGVISENQ.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-P5LK4A7U.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-P5LK4A7U.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-QPJAV452.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-QPJAV452.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-RYYR2YMB.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-RYYR2YMB.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-UM3ZGDFR.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-UM3ZGDFR.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-XZTIOEPG.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/chunk-XZTIOEPG.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/clsx.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/clsx.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/embla-carousel-react.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/embla-carousel-react.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/embla-carousel-wheel-gestures.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/embla-carousel-wheel-gestures.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/mapbox-gl.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/mapbox-gl.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/package.json +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react-dom.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react-dom.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react-dom_client.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react-dom_client.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react_jsx-dev-runtime.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react_jsx-dev-runtime.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react_jsx-runtime.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/react_jsx-runtime.js.map +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/tailwind-merge.js +0 -0
- /package/template/node_modules/.vite-mcp/{deps_temp_c8c8077a → deps_temp_992accd8}/tailwind-merge.js.map +0 -0
- /package/template/{src/test → tests}/setup.ts +0 -0
- /package/template/{dist → tests/simulations}/albums/albums-show-simulation.json +0 -0
- /package/template/{dist → tests/simulations}/carousel/carousel-show-simulation.json +0 -0
- /package/template/{dist → tests/simulations}/map/map-show-simulation.json +0 -0
- /package/template/{dist → tests/simulations}/review/review-diff-simulation.json +0 -0
- /package/template/{dist → tests/simulations}/review/review-post-simulation.json +0 -0
- /package/template/{dist → tests/simulations}/review/review-purchase-simulation.json +0 -0
package/README.md
CHANGED
|
@@ -57,26 +57,32 @@ sunpeak is an npm package that helps you build ChatGPT Apps (MCP Resources) whil
|
|
|
57
57
|
|
|
58
58
|
### The `sunpeak` framework (`./template`)
|
|
59
59
|
|
|
60
|
-
Next.js for ChatGPT Apps.
|
|
60
|
+
Next.js for ChatGPT Apps. Using a Review page as an example, sunpeak projects look like:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
my-app/
|
|
64
|
-
├── src/
|
|
65
|
-
│ └──
|
|
66
|
-
│
|
|
67
|
-
│
|
|
68
|
-
|
|
69
|
-
│
|
|
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.
|
|
70
72
|
└── package.json
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
1. Project scaffold: Complete development setup with the sunpeak library.
|
|
74
76
|
2. UI components: Production-ready components following ChatGPT design guidelines and using OpenAI apps-sdk-ui React components.
|
|
75
|
-
3. Convention over configuration:
|
|
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))
|
|
76
80
|
|
|
77
81
|
### The `sunpeak` CLI (`./bin`)
|
|
78
82
|
|
|
79
|
-
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
|
|
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.
|
|
84
|
+
|
|
85
|
+
Think Docker Hub for ChatGPT Apps:
|
|
80
86
|
|
|
81
87
|
<div align="center">
|
|
82
88
|
<a href="https://docs.sunpeak.ai/library/chatgpt-simulator">
|
|
@@ -88,14 +94,22 @@ Commands for managing ChatGPT Apps. Includes a client for the [sunpeak Resource
|
|
|
88
94
|
|
|
89
95
|
1. Tag your app builds with version numbers and environment names (like `v1.0.0` and `prod`)
|
|
90
96
|
2. `push` built Apps to a central location
|
|
91
|
-
3. `pull` built Apps to be run in different environments
|
|
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".
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
### Resource Component
|
|
105
|
+
|
|
106
|
+
React component defining a UI (MCP Resource) in your ChatGPT App.
|
|
94
107
|
|
|
95
108
|
```tsx
|
|
109
|
+
// src/resources/review-resource.tsx
|
|
96
110
|
import { Card } from './components';
|
|
97
111
|
|
|
98
|
-
export function
|
|
112
|
+
export function ReviewResource() {
|
|
99
113
|
return (
|
|
100
114
|
<Card
|
|
101
115
|
image="https://images.unsplash.com/photo-1520950237264-dfe336995c34"
|
|
@@ -111,6 +125,64 @@ export function MCPResource() {
|
|
|
111
125
|
}
|
|
112
126
|
```
|
|
113
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
|
+
|
|
114
186
|
## Resources
|
|
115
187
|
|
|
116
188
|
- [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
|
package/bin/commands/build.mjs
CHANGED
|
@@ -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
|
|
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
|
package/bin/commands/deploy.mjs
CHANGED
|
@@ -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
|
`);
|
package/bin/commands/dev.mjs
CHANGED
|
@@ -154,12 +154,14 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
154
154
|
|
|
155
155
|
// Discover simulations using sunpeak's discovery utilities
|
|
156
156
|
const resourcesDir = join(projectRoot, 'src/resources');
|
|
157
|
+
const simulationsDir = join(projectRoot, 'tests/simulations');
|
|
157
158
|
const resourceDirs = findResourceDirs(resourcesDir, (key) => `${key}-resource.json`, fs);
|
|
158
159
|
|
|
159
160
|
const simulations = [];
|
|
160
161
|
for (const { key: resourceKey, dir: resourceDir, resourcePath } of resourceDirs) {
|
|
161
162
|
const resource = JSON.parse(readFileSync(resourcePath, 'utf-8'));
|
|
162
|
-
const
|
|
163
|
+
const resourceSimDir = join(simulationsDir, resourceKey);
|
|
164
|
+
const simulationFiles = findSimulationFiles(resourceSimDir, resourceKey, fs);
|
|
163
165
|
|
|
164
166
|
for (const { filename, path: simPath } of simulationFiles) {
|
|
165
167
|
const simulationKey = filename.replace(/-simulation\.json$/, '');
|
|
@@ -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
|
+
}
|
package/bin/commands/pull.mjs
CHANGED
package/bin/commands/push.mjs
CHANGED
|
@@ -61,23 +61,24 @@ export const defaultDeps = {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Find simulation files for a resource in
|
|
65
|
-
* Expects files
|
|
64
|
+
* Find simulation files for a resource in the simulations directory
|
|
65
|
+
* Expects files in: tests/simulations/{resourceName}/{resourceName}-*-simulation.json
|
|
66
66
|
* Returns array of parsed simulation objects
|
|
67
67
|
*/
|
|
68
|
-
function findSimulations(
|
|
68
|
+
function findSimulations(simulationsDir, resourceName, deps = defaultDeps) {
|
|
69
69
|
const d = { ...defaultDeps, ...deps };
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
const resourceSimDir = join(simulationsDir, resourceName);
|
|
72
|
+
if (!d.existsSync(resourceSimDir)) {
|
|
72
73
|
return [];
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
const entries = d.readdirSync(
|
|
76
|
+
const entries = d.readdirSync(resourceSimDir);
|
|
76
77
|
const simulations = [];
|
|
77
78
|
|
|
78
79
|
for (const entry of entries) {
|
|
79
80
|
if (isSimulationFile(entry, resourceName)) {
|
|
80
|
-
const simPath = join(
|
|
81
|
+
const simPath = join(resourceSimDir, entry);
|
|
81
82
|
try {
|
|
82
83
|
const simData = JSON.parse(d.readFileSync(simPath, 'utf-8'));
|
|
83
84
|
const simName = extractSimulationName(entry, resourceName);
|
|
@@ -94,9 +95,10 @@ function findSimulations(resourceDir, resourceName, deps = defaultDeps) {
|
|
|
94
95
|
/**
|
|
95
96
|
* Find all resources in a directory
|
|
96
97
|
* Expects folder structure: dist/{resource}/{resource}.js
|
|
98
|
+
* Simulations are loaded from tests/simulations/{resource}/
|
|
97
99
|
* Returns array of { name, jsPath, metaPath, meta, simulations }
|
|
98
100
|
*/
|
|
99
|
-
export function findResources(distDir, deps = defaultDeps) {
|
|
101
|
+
export function findResources(distDir, simulationsDir, deps = defaultDeps) {
|
|
100
102
|
const d = { ...defaultDeps, ...deps };
|
|
101
103
|
|
|
102
104
|
if (!d.existsSync(distDir)) {
|
|
@@ -120,7 +122,7 @@ export function findResources(distDir, deps = defaultDeps) {
|
|
|
120
122
|
} catch {
|
|
121
123
|
d.console.warn(`Warning: Could not parse ${resourceName}.json`);
|
|
122
124
|
}
|
|
123
|
-
const simulations = findSimulations(
|
|
125
|
+
const simulations = findSimulations(simulationsDir, resourceName, d);
|
|
124
126
|
resources.push({ name: resourceName, dir: resourceDir, jsPath, metaPath, meta, simulations });
|
|
125
127
|
}
|
|
126
128
|
}
|
|
@@ -132,9 +134,10 @@ export function findResources(distDir, deps = defaultDeps) {
|
|
|
132
134
|
/**
|
|
133
135
|
* Build a resource from a resource directory path
|
|
134
136
|
* Expects structure: dir/{name}.js, dir/{name}.json
|
|
137
|
+
* Simulations are loaded from tests/simulations/{name}/
|
|
135
138
|
* Returns { name, jsPath, metaPath, meta, simulations }
|
|
136
139
|
*/
|
|
137
|
-
function buildResourceFromDir(resourceDir, deps = defaultDeps) {
|
|
140
|
+
function buildResourceFromDir(resourceDir, simulationsDir, deps = defaultDeps) {
|
|
138
141
|
const d = { ...defaultDeps, ...deps };
|
|
139
142
|
|
|
140
143
|
// Remove trailing slash if present
|
|
@@ -167,7 +170,7 @@ function buildResourceFromDir(resourceDir, deps = defaultDeps) {
|
|
|
167
170
|
d.console.warn(`Warning: Could not parse ${name}.json`);
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
const simulations = findSimulations(
|
|
173
|
+
const simulations = findSimulations(simulationsDir, name, d);
|
|
171
174
|
|
|
172
175
|
return { name, jsPath, metaPath, meta, simulations };
|
|
173
176
|
}
|
|
@@ -281,6 +284,7 @@ Usage:
|
|
|
281
284
|
Options:
|
|
282
285
|
-r, --repository <owner/repo> Repository name (defaults to git remote origin)
|
|
283
286
|
-t, --tag <name> Tag to assign (can be specified multiple times)
|
|
287
|
+
--no-simulations Skip pushing simulations, only push resources
|
|
284
288
|
-h, --help Show this help message
|
|
285
289
|
|
|
286
290
|
Arguments:
|
|
@@ -293,6 +297,7 @@ Examples:
|
|
|
293
297
|
sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
|
|
294
298
|
sunpeak push -t v1.0.0 Push with a version tag
|
|
295
299
|
sunpeak push -t v1.0.0 -t prod Push with multiple tags
|
|
300
|
+
sunpeak push --no-simulations Push resources without simulations
|
|
296
301
|
`);
|
|
297
302
|
return;
|
|
298
303
|
}
|
|
@@ -314,10 +319,11 @@ Examples:
|
|
|
314
319
|
}
|
|
315
320
|
|
|
316
321
|
// Find resources - either a specific directory or all from dist directory
|
|
322
|
+
const simulationsDir = join(projectRoot, 'tests/simulations');
|
|
317
323
|
let resources;
|
|
318
324
|
if (options.dir) {
|
|
319
325
|
// Push a single specific resource from directory
|
|
320
|
-
resources = [buildResourceFromDir(options.dir, d)];
|
|
326
|
+
resources = [buildResourceFromDir(options.dir, simulationsDir, d)];
|
|
321
327
|
} else {
|
|
322
328
|
// Default: find all resources in dist directory
|
|
323
329
|
const distDir = join(projectRoot, 'dist');
|
|
@@ -327,7 +333,7 @@ Examples:
|
|
|
327
333
|
d.process.exit(1);
|
|
328
334
|
}
|
|
329
335
|
|
|
330
|
-
resources = findResources(distDir, d);
|
|
336
|
+
resources = findResources(distDir, simulationsDir, d);
|
|
331
337
|
if (resources.length === 0) {
|
|
332
338
|
d.console.error(`Error: No resources found in dist/`);
|
|
333
339
|
d.console.error('Run "sunpeak build" first to build your resources.');
|
|
@@ -335,10 +341,20 @@ Examples:
|
|
|
335
341
|
}
|
|
336
342
|
}
|
|
337
343
|
|
|
344
|
+
// Clear simulations if --no-simulations flag is set
|
|
345
|
+
if (options.noSimulations) {
|
|
346
|
+
for (const resource of resources) {
|
|
347
|
+
resource.simulations = [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
338
351
|
d.console.log(`Pushing ${resources.length} resource(s) to repository "${repository}"...`);
|
|
339
352
|
if (options.tags && options.tags.length > 0) {
|
|
340
353
|
d.console.log(`Tags: ${options.tags.join(', ')}`);
|
|
341
354
|
}
|
|
355
|
+
if (options.noSimulations) {
|
|
356
|
+
d.console.log('Simulations: skipped');
|
|
357
|
+
}
|
|
342
358
|
d.console.log();
|
|
343
359
|
|
|
344
360
|
// Push each resource
|
|
@@ -354,6 +370,9 @@ Examples:
|
|
|
354
370
|
successCount++;
|
|
355
371
|
} catch (error) {
|
|
356
372
|
d.console.error(`✗ Failed to push ${resource.name}: ${error.message}`);
|
|
373
|
+
if (error.message.includes('Uri must be unique')) {
|
|
374
|
+
d.console.error(' You are trying to push a build that has already been pushed.');
|
|
375
|
+
}
|
|
357
376
|
}
|
|
358
377
|
}
|
|
359
378
|
|
|
@@ -369,7 +388,7 @@ Examples:
|
|
|
369
388
|
/**
|
|
370
389
|
* Parse command line arguments
|
|
371
390
|
*/
|
|
372
|
-
function parseArgs(args) {
|
|
391
|
+
export function parseArgs(args) {
|
|
373
392
|
const options = { tags: [] };
|
|
374
393
|
let i = 0;
|
|
375
394
|
|
|
@@ -380,6 +399,8 @@ function parseArgs(args) {
|
|
|
380
399
|
options.repository = args[++i];
|
|
381
400
|
} else if (arg === '--tag' || arg === '-t') {
|
|
382
401
|
options.tags.push(args[++i]);
|
|
402
|
+
} else if (arg === '--no-simulations') {
|
|
403
|
+
options.noSimulations = true;
|
|
383
404
|
} else if (arg === '--help' || arg === '-h') {
|
|
384
405
|
console.log(`
|
|
385
406
|
sunpeak push - Push resources to the Sunpeak repository
|
|
@@ -390,6 +411,7 @@ Usage:
|
|
|
390
411
|
Options:
|
|
391
412
|
-r, --repository <owner/repo> Repository name (defaults to git remote origin)
|
|
392
413
|
-t, --tag <name> Tag to assign (can be specified multiple times)
|
|
414
|
+
--no-simulations Skip pushing simulations, only push resources
|
|
393
415
|
-h, --help Show this help message
|
|
394
416
|
|
|
395
417
|
Arguments:
|
|
@@ -402,6 +424,7 @@ Examples:
|
|
|
402
424
|
sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
|
|
403
425
|
sunpeak push -t v1.0.0 Push with a version tag
|
|
404
426
|
sunpeak push -t v1.0.0 -t prod Push with multiple tags
|
|
427
|
+
sunpeak push --no-simulations Push resources without simulations
|
|
405
428
|
`);
|
|
406
429
|
process.exit(0);
|
|
407
430
|
} else if (!arg.startsWith('-')) {
|
package/bin/lib/patterns.mjs
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
* These mirror the patterns in src/lib/discovery.ts for consistency.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { existsSync, readdirSync } from 'fs';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Auto-discover available resources from template/src/resources directories.
|
|
14
|
+
* Each subdirectory containing a {name}-resource.tsx file is a valid resource.
|
|
15
|
+
* @returns {string[]} Array of resource names
|
|
16
|
+
*/
|
|
17
|
+
export function discoverResources() {
|
|
18
|
+
const resourcesDir = join(__dirname, '..', '..', 'template', 'src', 'resources');
|
|
19
|
+
if (!existsSync(resourcesDir)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return readdirSync(resourcesDir, { withFileTypes: true })
|
|
23
|
+
.filter((entry) => entry.isDirectory())
|
|
24
|
+
.filter((entry) => {
|
|
25
|
+
const resourceFile = join(resourcesDir, entry.name, `${entry.name}-resource.tsx`);
|
|
26
|
+
return existsSync(resourceFile);
|
|
27
|
+
})
|
|
28
|
+
.map((entry) => entry.name);
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
/**
|
|
7
32
|
* Convert a kebab-case string to PascalCase
|
|
8
33
|
* @param {string} str
|