sunpeak 0.9.12 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/commands/build.mjs +56 -30
- package/bin/commands/deploy.mjs +17 -17
- package/bin/commands/push.mjs +115 -64
- package/bin/lib/patterns.mjs +40 -0
- package/bin/sunpeak.js +50 -106
- package/dist/index.cjs +149 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +165 -27
- package/dist/index.js.map +1 -1
- package/dist/lib/discovery.d.ts +76 -13
- package/dist/mcp/entry.cjs +24 -27
- package/dist/mcp/entry.cjs.map +1 -1
- package/dist/mcp/entry.js +25 -28
- package/dist/mcp/entry.js.map +1 -1
- package/package.json +1 -1
- package/template/.sunpeak/dev.tsx +5 -5
- package/template/README.md +54 -50
- package/template/dist/{albums.json → albums/albums.json} +1 -1
- package/template/dist/{carousel.json → carousel/carousel.json} +1 -1
- package/template/dist/{map.json → map/map.json} +1 -1
- package/template/dist/{review.json → review/review.json} +1 -1
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Button.js +3 -3
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +11 -11
- package/template/node_modules/.vite/deps/_metadata.json +26 -26
- package/template/node_modules/.vite/deps/{chunk-N6DVYEXK.js → chunk-LVZJNEGE.js} +7 -7
- package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/template/src/resources/{albums-resource.test.tsx → albums/albums-resource.test.tsx} +1 -1
- package/template/src/resources/{albums-resource.tsx → albums/albums-resource.tsx} +1 -1
- package/template/src/resources/albums/albums-show-simulation.json +131 -0
- package/template/src/{components/album → resources/albums/components}/album-card.tsx +1 -1
- package/template/src/{components/album → resources/albums/components}/album-carousel.tsx +1 -1
- package/template/src/{components/album → resources/albums/components}/film-strip.tsx +1 -1
- package/template/src/{components/album → resources/albums/components}/fullscreen-viewer.tsx +1 -1
- package/template/src/resources/{carousel-resource.test.tsx → carousel/carousel-resource.test.tsx} +1 -1
- package/template/src/resources/{carousel-resource.tsx → carousel/carousel-resource.tsx} +1 -1
- package/template/src/resources/carousel/carousel-show-simulation.json +68 -0
- package/template/src/{components/carousel → resources/carousel/components}/card.tsx +1 -1
- package/template/src/{components/carousel → resources/carousel/components}/carousel.tsx +1 -1
- package/template/src/resources/index.ts +5 -5
- package/template/src/{components/map → resources/map/components}/map-view.tsx +1 -1
- package/template/src/{components/map → resources/map/components}/map.tsx +1 -1
- package/template/src/{components/map → resources/map/components}/place-card.tsx +1 -1
- package/template/src/{components/map → resources/map/components}/place-carousel.tsx +1 -1
- package/template/src/{components/map → resources/map/components}/place-inspector.tsx +1 -1
- package/template/src/{components/map → resources/map/components}/place-list.tsx +1 -1
- package/template/src/resources/{map-resource.test.tsx → map/map-resource.test.tsx} +1 -1
- package/template/src/resources/{map-resource.tsx → map/map-resource.tsx} +1 -1
- package/template/src/resources/map/map-show-simulation.json +123 -0
- package/template/src/resources/review/review-diff-simulation.json +80 -0
- package/template/src/resources/review/review-post-simulation.json +56 -0
- package/template/src/resources/review/review-purchase-simulation.json +88 -0
- package/dist/discovery-a4WId9PC.cjs +0 -125
- package/dist/discovery-a4WId9PC.cjs.map +0 -1
- package/dist/discovery-ft3cd2dW.js +0 -126
- package/dist/discovery-ft3cd2dW.js.map +0 -1
- package/template/src/components/index.ts +0 -3
- package/template/src/simulations/index.ts +0 -16
- /package/template/{src/simulations → dist/albums}/albums-show-simulation.json +0 -0
- /package/template/dist/{albums.js → albums/albums.js} +0 -0
- /package/template/{src/simulations → dist/carousel}/carousel-show-simulation.json +0 -0
- /package/template/dist/{carousel.js → carousel/carousel.js} +0 -0
- /package/template/{src/simulations → dist/map}/map-show-simulation.json +0 -0
- /package/template/dist/{map.js → map/map.js} +0 -0
- /package/template/{src/simulations → dist/review}/review-diff-simulation.json +0 -0
- /package/template/{src/simulations → dist/review}/review-post-simulation.json +0 -0
- /package/template/{src/simulations → dist/review}/review-purchase-simulation.json +0 -0
- /package/template/dist/{review.js → review/review.js} +0 -0
- /package/template/node_modules/.vite/deps/{chunk-N6DVYEXK.js.map → chunk-LVZJNEGE.js.map} +0 -0
- /package/template/src/resources/{albums-resource.json → albums/albums-resource.json} +0 -0
- /package/template/src/{components/album → resources/albums/components}/album-card.test.tsx +0 -0
- /package/template/src/{components/album → resources/albums/components}/album-carousel.test.tsx +0 -0
- /package/template/src/{components/album → resources/albums/components}/albums.test.tsx +0 -0
- /package/template/src/{components/album → resources/albums/components}/albums.tsx +0 -0
- /package/template/src/{components/album → resources/albums/components}/film-strip.test.tsx +0 -0
- /package/template/src/{components/album → resources/albums/components}/fullscreen-viewer.test.tsx +0 -0
- /package/template/src/{components/album → resources/albums/components}/index.ts +0 -0
- /package/template/src/resources/{carousel-resource.json → carousel/carousel-resource.json} +0 -0
- /package/template/src/{components/carousel → resources/carousel/components}/card.test.tsx +0 -0
- /package/template/src/{components/carousel → resources/carousel/components}/carousel.test.tsx +0 -0
- /package/template/src/{components/carousel → resources/carousel/components}/index.ts +0 -0
- /package/template/src/{components/map → resources/map/components}/index.ts +0 -0
- /package/template/src/{components/map → resources/map/components}/map-view.test.tsx +0 -0
- /package/template/src/{components/map → resources/map/components}/place-card.test.tsx +0 -0
- /package/template/src/{components/map → resources/map/components}/place-carousel.test.tsx +0 -0
- /package/template/src/{components/map → resources/map/components}/place-inspector.test.tsx +0 -0
- /package/template/src/{components/map → resources/map/components}/place-list.test.tsx +0 -0
- /package/template/src/{components/map → resources/map/components}/types.ts +0 -0
- /package/template/src/resources/{map-resource.json → map/map-resource.json} +0 -0
- /package/template/src/resources/{review-resource.json → review/review-resource.json} +0 -0
- /package/template/src/resources/{review-resource.test.tsx → review/review-resource.test.tsx} +0 -0
- /package/template/src/resources/{review-resource.tsx → review/review-resource.tsx} +0 -0
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ sunpeak is an npm package consisting of:
|
|
|
57
57
|
2. **The `sunpeak` framework** (`./template`). Next.js for ChatGPT Apps. This templated npm package includes:
|
|
58
58
|
1. Project scaffold - Complete development setup with build, test, and mcp tooling, including the sunpeak library.
|
|
59
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-resource.tsx` React file and `src/resources/NAME-resource.json` metadata file.
|
|
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
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
62
|
1. Tag your app builds with version numbers and environment names (like `v1.0.0` and `prod`)
|
|
63
63
|
2. `push` built Apps to a central location
|
|
@@ -68,7 +68,7 @@ Note that each `sunpeak` component can be used in isolation if preferred, though
|
|
|
68
68
|
## Example Component
|
|
69
69
|
|
|
70
70
|
```tsx
|
|
71
|
-
import { Card } from '
|
|
71
|
+
import { Card } from './components';
|
|
72
72
|
|
|
73
73
|
export function MCPResource() {
|
|
74
74
|
return (
|
package/bin/commands/build.mjs
CHANGED
|
@@ -4,6 +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
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Build all resources for a Sunpeak project
|
|
@@ -83,31 +84,38 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
83
84
|
mkdirSync(distDir, { recursive: true });
|
|
84
85
|
mkdirSync(tempDir, { recursive: true });
|
|
85
86
|
|
|
86
|
-
// Auto-discover all resources
|
|
87
|
-
const resourceFiles = readdirSync(resourcesDir)
|
|
88
|
-
.filter(
|
|
89
|
-
.map(
|
|
90
|
-
|
|
91
|
-
const
|
|
87
|
+
// Auto-discover all resources (each resource is a subdirectory)
|
|
88
|
+
const resourceFiles = readdirSync(resourcesDir, { withFileTypes: true })
|
|
89
|
+
.filter(entry => entry.isDirectory())
|
|
90
|
+
.map(entry => {
|
|
91
|
+
const kebabName = entry.name;
|
|
92
|
+
const resourceFile = `${kebabName}-resource.tsx`;
|
|
93
|
+
const resourcePath = path.join(resourcesDir, kebabName, resourceFile);
|
|
94
|
+
|
|
95
|
+
// Skip directories without a resource file
|
|
96
|
+
if (!existsSync(resourcePath)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
92
99
|
|
|
93
100
|
// Convert kebab-case to PascalCase: 'review' -> 'Review', 'my-widget' -> 'MyWidget'
|
|
94
|
-
const pascalName = kebabName
|
|
95
|
-
.split('-')
|
|
96
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
97
|
-
.join('');
|
|
101
|
+
const pascalName = toPascalCase(kebabName);
|
|
98
102
|
|
|
99
103
|
return {
|
|
100
104
|
componentName: `${pascalName}Resource`,
|
|
101
|
-
componentFile:
|
|
105
|
+
componentFile: `${kebabName}-resource`,
|
|
106
|
+
kebabName,
|
|
107
|
+
resourceDir: path.join(resourcesDir, kebabName),
|
|
102
108
|
entry: `.tmp/index-${kebabName}.tsx`,
|
|
103
109
|
output: `${kebabName}.js`,
|
|
104
110
|
buildOutDir: path.join(buildDir, kebabName),
|
|
111
|
+
distOutDir: path.join(distDir, kebabName), // Final output: dist/{resource}/
|
|
105
112
|
};
|
|
106
|
-
})
|
|
113
|
+
})
|
|
114
|
+
.filter(Boolean);
|
|
107
115
|
|
|
108
116
|
if (resourceFiles.length === 0) {
|
|
109
|
-
console.error('Error: No resource
|
|
110
|
-
console.error('
|
|
117
|
+
console.error('Error: No resource directories found in src/resources/');
|
|
118
|
+
console.error('Each resource should be a directory like: src/resources/review/review-resource.tsx');
|
|
111
119
|
process.exit(1);
|
|
112
120
|
}
|
|
113
121
|
|
|
@@ -133,7 +141,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
133
141
|
|
|
134
142
|
// Build all resources (but don't copy yet)
|
|
135
143
|
for (let i = 0; i < resourceFiles.length; i++) {
|
|
136
|
-
const { componentName, componentFile, entry, output, buildOutDir } = resourceFiles[i];
|
|
144
|
+
const { componentName, componentFile, kebabName, entry, output, buildOutDir } = resourceFiles[i];
|
|
137
145
|
console.log(`[${i + 1}/${resourceFiles.length}] Building ${output}...`);
|
|
138
146
|
|
|
139
147
|
try {
|
|
@@ -144,7 +152,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
144
152
|
|
|
145
153
|
// Create entry file from template in temp directory
|
|
146
154
|
const entryContent = template
|
|
147
|
-
.replace('// RESOURCE_IMPORT', `import { ${componentName} } from '../src/resources/${componentFile}';`)
|
|
155
|
+
.replace('// RESOURCE_IMPORT', `import { ${componentName} } from '../src/resources/${kebabName}/${componentFile}';`)
|
|
148
156
|
.replace('// RESOURCE_MOUNT', `createRoot(root).render(<${componentName} />);`);
|
|
149
157
|
|
|
150
158
|
const entryPath = path.join(projectRoot, entry);
|
|
@@ -194,15 +202,23 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
194
202
|
}
|
|
195
203
|
}
|
|
196
204
|
|
|
197
|
-
// Now copy all files from build-output to dist/
|
|
205
|
+
// Now copy all files from build-output to dist/{resource}/
|
|
198
206
|
console.log('\nCopying built files to dist/...');
|
|
199
|
-
|
|
207
|
+
const timestamp = Date.now().toString(36);
|
|
208
|
+
|
|
209
|
+
for (const { output, buildOutDir, distOutDir, kebabName, componentFile, resourceDir } of resourceFiles) {
|
|
210
|
+
// Create resource-specific output directory
|
|
211
|
+
if (!existsSync(distOutDir)) {
|
|
212
|
+
mkdirSync(distOutDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Copy built JS file
|
|
200
216
|
const builtFile = path.join(buildOutDir, output);
|
|
201
|
-
const destFile = path.join(
|
|
217
|
+
const destFile = path.join(distOutDir, output);
|
|
202
218
|
|
|
203
219
|
if (existsSync(builtFile)) {
|
|
204
220
|
copyFileSync(builtFile, destFile);
|
|
205
|
-
console.log(`✓ Copied ${output}`);
|
|
221
|
+
console.log(`✓ Copied ${kebabName}/${output}`);
|
|
206
222
|
} else {
|
|
207
223
|
console.error(`Built file not found: ${builtFile}`);
|
|
208
224
|
if (existsSync(buildOutDir)) {
|
|
@@ -212,22 +228,28 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
212
228
|
}
|
|
213
229
|
process.exit(1);
|
|
214
230
|
}
|
|
215
|
-
}
|
|
216
231
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (const { componentFile } of resourceFiles) {
|
|
221
|
-
const kebabName = componentFile.replace('-resource', '');
|
|
222
|
-
const srcJson = path.join(resourcesDir, `${componentFile}.json`);
|
|
223
|
-
const destJson = path.join(distDir, `${kebabName}.json`);
|
|
232
|
+
// Copy and process resource metadata JSON file
|
|
233
|
+
const srcJson = path.join(resourceDir, `${componentFile}.json`);
|
|
234
|
+
const destJson = path.join(distOutDir, `${kebabName}.json`);
|
|
224
235
|
|
|
225
236
|
if (existsSync(srcJson)) {
|
|
226
237
|
const meta = JSON.parse(readFileSync(srcJson, 'utf-8'));
|
|
227
238
|
// Generate URI using resource name and build timestamp
|
|
228
239
|
meta.uri = `ui://${meta.name}-${timestamp}`;
|
|
229
240
|
writeFileSync(destJson, JSON.stringify(meta, null, 2));
|
|
230
|
-
console.log(`✓ Generated ${kebabName}.json (uri: ${meta.uri})`);
|
|
241
|
+
console.log(`✓ Generated ${kebabName}/${kebabName}.json (uri: ${meta.uri})`);
|
|
242
|
+
}
|
|
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}`);
|
|
231
253
|
}
|
|
232
254
|
}
|
|
233
255
|
|
|
@@ -240,7 +262,11 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
240
262
|
}
|
|
241
263
|
|
|
242
264
|
console.log('\n✓ All resources built successfully!');
|
|
243
|
-
console.log(
|
|
265
|
+
console.log('\nBuilt resources:');
|
|
266
|
+
for (const { kebabName, distOutDir } of resourceFiles) {
|
|
267
|
+
const files = readdirSync(distOutDir);
|
|
268
|
+
console.log(` ${kebabName}/: ${files.join(', ')}`);
|
|
269
|
+
}
|
|
244
270
|
}
|
|
245
271
|
|
|
246
272
|
// Allow running directly
|
package/bin/commands/deploy.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export async function deploy(projectRoot = process.cwd(), options = {}, deps = d
|
|
|
23
23
|
sunpeak deploy - Push resources to production (push with "prod" tag)
|
|
24
24
|
|
|
25
25
|
Usage:
|
|
26
|
-
sunpeak deploy [
|
|
26
|
+
sunpeak deploy [directory] [options]
|
|
27
27
|
|
|
28
28
|
Options:
|
|
29
29
|
-r, --repository <owner/repo> Repository name (defaults to git remote origin)
|
|
@@ -31,15 +31,15 @@ Options:
|
|
|
31
31
|
-h, --help Show this help message
|
|
32
32
|
|
|
33
33
|
Arguments:
|
|
34
|
-
|
|
34
|
+
directory Optional resource directory to deploy (e.g., dist/carousel)
|
|
35
35
|
If not provided, looks for resources in current
|
|
36
36
|
directory first, then falls back to dist/
|
|
37
37
|
|
|
38
38
|
Examples:
|
|
39
|
-
sunpeak deploy
|
|
40
|
-
sunpeak deploy dist/carousel
|
|
41
|
-
sunpeak deploy -r myorg/my-app
|
|
42
|
-
sunpeak deploy -t v1.0
|
|
39
|
+
sunpeak deploy Deploy all resources with "prod" tag
|
|
40
|
+
sunpeak deploy dist/carousel Deploy a single resource
|
|
41
|
+
sunpeak deploy -r myorg/my-app Deploy to "myorg/my-app" repository
|
|
42
|
+
sunpeak deploy -t v1.0 Deploy with "prod" and "v1.0" tags
|
|
43
43
|
|
|
44
44
|
This command is equivalent to: sunpeak push --tag prod
|
|
45
45
|
`);
|
|
@@ -56,13 +56,13 @@ This command is equivalent to: sunpeak push --tag prod
|
|
|
56
56
|
d.console.log('Deploying to production...');
|
|
57
57
|
d.console.log();
|
|
58
58
|
|
|
59
|
-
// If no specific
|
|
60
|
-
if (!deployOptions.
|
|
59
|
+
// If no specific directory provided, check current directory first, then dist/
|
|
60
|
+
if (!deployOptions.dir) {
|
|
61
61
|
const cwdResources = findResources(projectRoot, d);
|
|
62
62
|
if (cwdResources.length > 0) {
|
|
63
63
|
// Found resources in current directory, push each one
|
|
64
64
|
for (const resource of cwdResources) {
|
|
65
|
-
await push(projectRoot, { ...deployOptions,
|
|
65
|
+
await push(projectRoot, { ...deployOptions, dir: resource.dir }, d);
|
|
66
66
|
}
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
@@ -91,7 +91,7 @@ function parseArgs(args) {
|
|
|
91
91
|
sunpeak deploy - Deploy resources to production (push with "prod" tag)
|
|
92
92
|
|
|
93
93
|
Usage:
|
|
94
|
-
sunpeak deploy [
|
|
94
|
+
sunpeak deploy [directory] [options]
|
|
95
95
|
|
|
96
96
|
Options:
|
|
97
97
|
-r, --repository <owner/repo> Repository name (defaults to git remote origin)
|
|
@@ -99,22 +99,22 @@ Options:
|
|
|
99
99
|
-h, --help Show this help message
|
|
100
100
|
|
|
101
101
|
Arguments:
|
|
102
|
-
|
|
102
|
+
directory Optional resource directory to deploy (e.g., dist/carousel)
|
|
103
103
|
If not provided, looks for resources in current
|
|
104
104
|
directory first, then falls back to dist/
|
|
105
105
|
|
|
106
106
|
Examples:
|
|
107
|
-
sunpeak deploy
|
|
108
|
-
sunpeak deploy dist/carousel
|
|
109
|
-
sunpeak deploy -r myorg/my-app
|
|
110
|
-
sunpeak deploy -t v1.0
|
|
107
|
+
sunpeak deploy Deploy all resources with "prod" tag
|
|
108
|
+
sunpeak deploy dist/carousel Deploy a single resource
|
|
109
|
+
sunpeak deploy -r myorg/my-app Deploy to "myorg/my-app" repository
|
|
110
|
+
sunpeak deploy -t v1.0 Deploy with "prod" and "v1.0" tags
|
|
111
111
|
|
|
112
112
|
This command is equivalent to: sunpeak push --tag prod
|
|
113
113
|
`);
|
|
114
114
|
process.exit(0);
|
|
115
115
|
} else if (!arg.startsWith('-')) {
|
|
116
|
-
// Positional argument - treat as
|
|
117
|
-
options.
|
|
116
|
+
// Positional argument - treat as directory path
|
|
117
|
+
options.dir = arg;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
i++;
|
package/bin/commands/push.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
3
|
-
import { join,
|
|
3
|
+
import { join, basename } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
|
+
import { isSimulationFile, extractSimulationName } from '../lib/patterns.mjs';
|
|
6
7
|
|
|
7
8
|
const SUNPEAK_API_URL = process.env.SUNPEAK_API_URL || 'https://app.sunpeak.ai';
|
|
8
9
|
const CREDENTIALS_DIR = join(homedir(), '.sunpeak');
|
|
@@ -59,9 +60,41 @@ export const defaultDeps = {
|
|
|
59
60
|
apiUrl: SUNPEAK_API_URL,
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Find simulation files for a resource in its directory
|
|
65
|
+
* Expects files matching: {resourceName}-*-simulation.json
|
|
66
|
+
* Returns array of parsed simulation objects
|
|
67
|
+
*/
|
|
68
|
+
function findSimulations(resourceDir, resourceName, deps = defaultDeps) {
|
|
69
|
+
const d = { ...defaultDeps, ...deps };
|
|
70
|
+
|
|
71
|
+
if (!d.existsSync(resourceDir)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entries = d.readdirSync(resourceDir);
|
|
76
|
+
const simulations = [];
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (isSimulationFile(entry, resourceName)) {
|
|
80
|
+
const simPath = join(resourceDir, entry);
|
|
81
|
+
try {
|
|
82
|
+
const simData = JSON.parse(d.readFileSync(simPath, 'utf-8'));
|
|
83
|
+
const simName = extractSimulationName(entry, resourceName);
|
|
84
|
+
simulations.push({ ...simData, name: simName });
|
|
85
|
+
} catch {
|
|
86
|
+
d.console.warn(`Warning: Could not parse simulation file ${entry}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return simulations;
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
/**
|
|
63
95
|
* Find all resources in a directory
|
|
64
|
-
*
|
|
96
|
+
* Expects folder structure: dist/{resource}/{resource}.js
|
|
97
|
+
* Returns array of { name, jsPath, metaPath, meta, simulations }
|
|
65
98
|
*/
|
|
66
99
|
export function findResources(distDir, deps = defaultDeps) {
|
|
67
100
|
const d = { ...defaultDeps, ...deps };
|
|
@@ -70,62 +103,73 @@ export function findResources(distDir, deps = defaultDeps) {
|
|
|
70
103
|
return [];
|
|
71
104
|
}
|
|
72
105
|
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
d.console.warn(`Warning: Could not parse ${name}.json`);
|
|
106
|
+
const entries = d.readdirSync(distDir, { withFileTypes: true });
|
|
107
|
+
const resources = [];
|
|
108
|
+
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
const resourceName = entry.name;
|
|
112
|
+
const resourceDir = join(distDir, resourceName);
|
|
113
|
+
const jsPath = join(resourceDir, `${resourceName}.js`);
|
|
114
|
+
const metaPath = join(resourceDir, `${resourceName}.json`);
|
|
115
|
+
|
|
116
|
+
if (d.existsSync(jsPath) && d.existsSync(metaPath)) {
|
|
117
|
+
let meta = null;
|
|
118
|
+
try {
|
|
119
|
+
meta = JSON.parse(d.readFileSync(metaPath, 'utf-8'));
|
|
120
|
+
} catch {
|
|
121
|
+
d.console.warn(`Warning: Could not parse ${resourceName}.json`);
|
|
122
|
+
}
|
|
123
|
+
const simulations = findSimulations(resourceDir, resourceName, d);
|
|
124
|
+
resources.push({ name: resourceName, dir: resourceDir, jsPath, metaPath, meta, simulations });
|
|
93
125
|
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
94
128
|
|
|
95
|
-
|
|
96
|
-
});
|
|
129
|
+
return resources;
|
|
97
130
|
}
|
|
98
131
|
|
|
99
132
|
/**
|
|
100
|
-
* Build a resource from a
|
|
101
|
-
*
|
|
133
|
+
* Build a resource from a resource directory path
|
|
134
|
+
* Expects structure: dir/{name}.js, dir/{name}.json
|
|
135
|
+
* Returns { name, jsPath, metaPath, meta, simulations }
|
|
102
136
|
*/
|
|
103
|
-
function
|
|
137
|
+
function buildResourceFromDir(resourceDir, deps = defaultDeps) {
|
|
104
138
|
const d = { ...defaultDeps, ...deps };
|
|
105
139
|
|
|
106
|
-
if
|
|
107
|
-
|
|
140
|
+
// Remove trailing slash if present
|
|
141
|
+
const dir = resourceDir.replace(/\/$/, '');
|
|
142
|
+
|
|
143
|
+
if (!d.existsSync(dir)) {
|
|
144
|
+
d.console.error(`Error: Directory not found: ${dir}`);
|
|
108
145
|
d.process.exit(1);
|
|
109
146
|
}
|
|
110
147
|
|
|
111
|
-
// Extract name from
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
// Look for .json in the same directory
|
|
116
|
-
const dir = dirname(jsPath);
|
|
148
|
+
// Extract resource name from directory name
|
|
149
|
+
const name = basename(dir);
|
|
150
|
+
const jsPath = join(dir, `${name}.js`);
|
|
117
151
|
const metaPath = join(dir, `${name}.json`);
|
|
118
152
|
|
|
153
|
+
if (!d.existsSync(jsPath)) {
|
|
154
|
+
d.console.error(`Error: Resource JS file not found: ${jsPath}`);
|
|
155
|
+
d.process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!d.existsSync(metaPath)) {
|
|
159
|
+
d.console.error(`Error: Resource metadata file not found: ${metaPath}`);
|
|
160
|
+
d.process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
let meta = null;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
d.console.warn(`Warning: Could not parse ${name}.json`);
|
|
125
|
-
}
|
|
164
|
+
try {
|
|
165
|
+
meta = JSON.parse(d.readFileSync(metaPath, 'utf-8'));
|
|
166
|
+
} catch {
|
|
167
|
+
d.console.warn(`Warning: Could not parse ${name}.json`);
|
|
126
168
|
}
|
|
127
169
|
|
|
128
|
-
|
|
170
|
+
const simulations = findSimulations(dir, name, d);
|
|
171
|
+
|
|
172
|
+
return { name, jsPath, metaPath, meta, simulations };
|
|
129
173
|
}
|
|
130
174
|
|
|
131
175
|
/**
|
|
@@ -189,6 +233,11 @@ async function pushResource(resource, repository, tags, accessToken, deps = defa
|
|
|
189
233
|
});
|
|
190
234
|
}
|
|
191
235
|
|
|
236
|
+
// Add simulations if present
|
|
237
|
+
if (resource.simulations && resource.simulations.length > 0) {
|
|
238
|
+
formData.append('simulations', JSON.stringify(resource.simulations));
|
|
239
|
+
}
|
|
240
|
+
|
|
192
241
|
const response = await d.fetch(`${d.apiUrl}/api/v1/resources`, {
|
|
193
242
|
method: 'POST',
|
|
194
243
|
headers: {
|
|
@@ -214,7 +263,7 @@ async function pushResource(resource, repository, tags, accessToken, deps = defa
|
|
|
214
263
|
* @param {string} projectRoot - Project root directory
|
|
215
264
|
* @param {Object} options - Command options
|
|
216
265
|
* @param {string} options.repository - Repository name (optional, defaults to git repo name)
|
|
217
|
-
* @param {string} options.
|
|
266
|
+
* @param {string} options.dir - Path to a specific resource directory (optional)
|
|
218
267
|
* @param {string[]} options.tags - Tags to assign to the pushed resources (optional)
|
|
219
268
|
* @param {Object} deps - Dependencies (for testing). Uses defaultDeps if not provided.
|
|
220
269
|
*/
|
|
@@ -227,7 +276,7 @@ export async function push(projectRoot = process.cwd(), options = {}, deps = def
|
|
|
227
276
|
sunpeak push - Push resources to the Sunpeak repository
|
|
228
277
|
|
|
229
278
|
Usage:
|
|
230
|
-
sunpeak push [
|
|
279
|
+
sunpeak push [directory] [options]
|
|
231
280
|
|
|
232
281
|
Options:
|
|
233
282
|
-r, --repository <owner/repo> Repository name (defaults to git remote origin)
|
|
@@ -235,15 +284,15 @@ Options:
|
|
|
235
284
|
-h, --help Show this help message
|
|
236
285
|
|
|
237
286
|
Arguments:
|
|
238
|
-
|
|
287
|
+
directory Optional resource directory to push (e.g., dist/carousel)
|
|
239
288
|
If not provided, pushes all resources from dist/
|
|
240
289
|
|
|
241
290
|
Examples:
|
|
242
|
-
sunpeak push
|
|
243
|
-
sunpeak push dist/carousel
|
|
244
|
-
sunpeak push -r myorg/my-app
|
|
245
|
-
sunpeak push -t v1.0.0
|
|
246
|
-
sunpeak push -t v1.0.0 -t prod
|
|
291
|
+
sunpeak push Push all resources from dist/
|
|
292
|
+
sunpeak push dist/carousel Push a single resource
|
|
293
|
+
sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
|
|
294
|
+
sunpeak push -t v1.0.0 Push with a version tag
|
|
295
|
+
sunpeak push -t v1.0.0 -t prod Push with multiple tags
|
|
247
296
|
`);
|
|
248
297
|
return;
|
|
249
298
|
}
|
|
@@ -264,11 +313,11 @@ Examples:
|
|
|
264
313
|
d.process.exit(1);
|
|
265
314
|
}
|
|
266
315
|
|
|
267
|
-
// Find resources - either a specific
|
|
316
|
+
// Find resources - either a specific directory or all from dist directory
|
|
268
317
|
let resources;
|
|
269
|
-
if (options.
|
|
270
|
-
// Push a single specific resource
|
|
271
|
-
resources = [
|
|
318
|
+
if (options.dir) {
|
|
319
|
+
// Push a single specific resource from directory
|
|
320
|
+
resources = [buildResourceFromDir(options.dir, d)];
|
|
272
321
|
} else {
|
|
273
322
|
// Default: find all resources in dist directory
|
|
274
323
|
const distDir = join(projectRoot, 'dist');
|
|
@@ -297,7 +346,9 @@ Examples:
|
|
|
297
346
|
for (const resource of resources) {
|
|
298
347
|
try {
|
|
299
348
|
const result = await pushResource(resource, repository, options.tags, credentials.access_token, d);
|
|
300
|
-
|
|
349
|
+
const simCount = resource.simulations?.length || 0;
|
|
350
|
+
const simInfo = simCount > 0 ? `, ${simCount} simulation(s)` : '';
|
|
351
|
+
d.console.log(`✓ Pushed ${resource.name} (id: ${result.id}${simInfo})`);
|
|
301
352
|
if (result.tags?.length > 0) {
|
|
302
353
|
d.console.log(` Tags: ${result.tags.join(', ')}`);
|
|
303
354
|
}
|
|
@@ -335,7 +386,7 @@ function parseArgs(args) {
|
|
|
335
386
|
sunpeak push - Push resources to the Sunpeak repository
|
|
336
387
|
|
|
337
388
|
Usage:
|
|
338
|
-
sunpeak push [
|
|
389
|
+
sunpeak push [directory] [options]
|
|
339
390
|
|
|
340
391
|
Options:
|
|
341
392
|
-r, --repository <owner/repo> Repository name (defaults to git remote origin)
|
|
@@ -343,20 +394,20 @@ Options:
|
|
|
343
394
|
-h, --help Show this help message
|
|
344
395
|
|
|
345
396
|
Arguments:
|
|
346
|
-
|
|
397
|
+
directory Optional resource directory to push (e.g., dist/carousel)
|
|
347
398
|
If not provided, pushes all resources from dist/
|
|
348
399
|
|
|
349
400
|
Examples:
|
|
350
|
-
sunpeak push
|
|
351
|
-
sunpeak push dist/carousel
|
|
352
|
-
sunpeak push -r myorg/my-app
|
|
353
|
-
sunpeak push -t v1.0.0
|
|
354
|
-
sunpeak push -t v1.0.0 -t prod
|
|
401
|
+
sunpeak push Push all resources from dist/
|
|
402
|
+
sunpeak push dist/carousel Push a single resource
|
|
403
|
+
sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
|
|
404
|
+
sunpeak push -t v1.0.0 Push with a version tag
|
|
405
|
+
sunpeak push -t v1.0.0 -t prod Push with multiple tags
|
|
355
406
|
`);
|
|
356
407
|
process.exit(0);
|
|
357
408
|
} else if (!arg.startsWith('-')) {
|
|
358
|
-
// Positional argument - treat as
|
|
359
|
-
options.
|
|
409
|
+
// Positional argument - treat as directory path
|
|
410
|
+
options.dir = arg;
|
|
360
411
|
}
|
|
361
412
|
|
|
362
413
|
i++;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared patterns and utilities for CLI commands.
|
|
3
|
+
* These mirror the patterns in src/lib/discovery.ts for consistency.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert a kebab-case string to PascalCase
|
|
8
|
+
* @param {string} str
|
|
9
|
+
* @returns {string}
|
|
10
|
+
* @example toPascalCase('review') // 'Review'
|
|
11
|
+
* @example toPascalCase('album-art') // 'AlbumArt'
|
|
12
|
+
*/
|
|
13
|
+
export function toPascalCase(str) {
|
|
14
|
+
return str
|
|
15
|
+
.split('-')
|
|
16
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
17
|
+
.join('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a filename is a simulation file for a given resource.
|
|
22
|
+
* @param {string} filename
|
|
23
|
+
* @param {string} resourceKey
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
* @example isSimulationFile('albums-show-simulation.json', 'albums') // true
|
|
26
|
+
*/
|
|
27
|
+
export function isSimulationFile(filename, resourceKey) {
|
|
28
|
+
return filename.startsWith(`${resourceKey}-`) && filename.endsWith('-simulation.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract the simulation name from a simulation filename.
|
|
33
|
+
* @param {string} filename
|
|
34
|
+
* @param {string} resourceKey
|
|
35
|
+
* @returns {string}
|
|
36
|
+
* @example extractSimulationName('albums-show-simulation.json', 'albums') // 'show'
|
|
37
|
+
*/
|
|
38
|
+
export function extractSimulationName(filename, resourceKey) {
|
|
39
|
+
return filename.replace(`${resourceKey}-`, '').replace('-simulation.json', '');
|
|
40
|
+
}
|