sunpeak 0.9.12 → 0.10.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 (87) hide show
  1. package/bin/commands/build.mjs +56 -30
  2. package/bin/commands/deploy.mjs +17 -17
  3. package/bin/commands/push.mjs +115 -64
  4. package/bin/lib/patterns.mjs +40 -0
  5. package/bin/sunpeak.js +50 -106
  6. package/dist/index.cjs +149 -11
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +165 -27
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/discovery.d.ts +76 -13
  11. package/dist/mcp/entry.cjs +24 -27
  12. package/dist/mcp/entry.cjs.map +1 -1
  13. package/dist/mcp/entry.js +25 -28
  14. package/dist/mcp/entry.js.map +1 -1
  15. package/package.json +1 -1
  16. package/template/.sunpeak/dev.tsx +5 -5
  17. package/template/README.md +54 -50
  18. package/template/dist/{albums.json → albums/albums.json} +1 -1
  19. package/template/dist/{carousel.json → carousel/carousel.json} +1 -1
  20. package/template/dist/{map.json → map/map.json} +1 -1
  21. package/template/dist/{review.json → review/review.json} +1 -1
  22. package/template/node_modules/.vite/deps/_metadata.json +19 -19
  23. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  24. package/template/src/resources/{albums-resource.test.tsx → albums/albums-resource.test.tsx} +1 -1
  25. package/template/src/resources/{albums-resource.tsx → albums/albums-resource.tsx} +1 -1
  26. package/template/src/resources/albums/albums-show-simulation.json +131 -0
  27. package/template/src/{components/album → resources/albums/components}/album-card.tsx +1 -1
  28. package/template/src/{components/album → resources/albums/components}/album-carousel.tsx +1 -1
  29. package/template/src/{components/album → resources/albums/components}/film-strip.tsx +1 -1
  30. package/template/src/{components/album → resources/albums/components}/fullscreen-viewer.tsx +1 -1
  31. package/template/src/resources/{carousel-resource.test.tsx → carousel/carousel-resource.test.tsx} +1 -1
  32. package/template/src/resources/{carousel-resource.tsx → carousel/carousel-resource.tsx} +1 -1
  33. package/template/src/resources/carousel/carousel-show-simulation.json +68 -0
  34. package/template/src/{components/carousel → resources/carousel/components}/card.tsx +1 -1
  35. package/template/src/{components/carousel → resources/carousel/components}/carousel.tsx +1 -1
  36. package/template/src/resources/index.ts +5 -5
  37. package/template/src/{components/map → resources/map/components}/map-view.tsx +1 -1
  38. package/template/src/{components/map → resources/map/components}/map.tsx +1 -1
  39. package/template/src/{components/map → resources/map/components}/place-card.tsx +1 -1
  40. package/template/src/{components/map → resources/map/components}/place-carousel.tsx +1 -1
  41. package/template/src/{components/map → resources/map/components}/place-inspector.tsx +1 -1
  42. package/template/src/{components/map → resources/map/components}/place-list.tsx +1 -1
  43. package/template/src/resources/{map-resource.test.tsx → map/map-resource.test.tsx} +1 -1
  44. package/template/src/resources/{map-resource.tsx → map/map-resource.tsx} +1 -1
  45. package/template/src/resources/map/map-show-simulation.json +123 -0
  46. package/template/src/resources/review/review-diff-simulation.json +80 -0
  47. package/template/src/resources/review/review-post-simulation.json +56 -0
  48. package/template/src/resources/review/review-purchase-simulation.json +88 -0
  49. package/dist/discovery-a4WId9PC.cjs +0 -125
  50. package/dist/discovery-a4WId9PC.cjs.map +0 -1
  51. package/dist/discovery-ft3cd2dW.js +0 -126
  52. package/dist/discovery-ft3cd2dW.js.map +0 -1
  53. package/template/src/components/index.ts +0 -3
  54. package/template/src/simulations/index.ts +0 -16
  55. /package/template/{src/simulations → dist/albums}/albums-show-simulation.json +0 -0
  56. /package/template/dist/{albums.js → albums/albums.js} +0 -0
  57. /package/template/{src/simulations → dist/carousel}/carousel-show-simulation.json +0 -0
  58. /package/template/dist/{carousel.js → carousel/carousel.js} +0 -0
  59. /package/template/{src/simulations → dist/map}/map-show-simulation.json +0 -0
  60. /package/template/dist/{map.js → map/map.js} +0 -0
  61. /package/template/{src/simulations → dist/review}/review-diff-simulation.json +0 -0
  62. /package/template/{src/simulations → dist/review}/review-post-simulation.json +0 -0
  63. /package/template/{src/simulations → dist/review}/review-purchase-simulation.json +0 -0
  64. /package/template/dist/{review.js → review/review.js} +0 -0
  65. /package/template/src/resources/{albums-resource.json → albums/albums-resource.json} +0 -0
  66. /package/template/src/{components/album → resources/albums/components}/album-card.test.tsx +0 -0
  67. /package/template/src/{components/album → resources/albums/components}/album-carousel.test.tsx +0 -0
  68. /package/template/src/{components/album → resources/albums/components}/albums.test.tsx +0 -0
  69. /package/template/src/{components/album → resources/albums/components}/albums.tsx +0 -0
  70. /package/template/src/{components/album → resources/albums/components}/film-strip.test.tsx +0 -0
  71. /package/template/src/{components/album → resources/albums/components}/fullscreen-viewer.test.tsx +0 -0
  72. /package/template/src/{components/album → resources/albums/components}/index.ts +0 -0
  73. /package/template/src/resources/{carousel-resource.json → carousel/carousel-resource.json} +0 -0
  74. /package/template/src/{components/carousel → resources/carousel/components}/card.test.tsx +0 -0
  75. /package/template/src/{components/carousel → resources/carousel/components}/carousel.test.tsx +0 -0
  76. /package/template/src/{components/carousel → resources/carousel/components}/index.ts +0 -0
  77. /package/template/src/{components/map → resources/map/components}/index.ts +0 -0
  78. /package/template/src/{components/map → resources/map/components}/map-view.test.tsx +0 -0
  79. /package/template/src/{components/map → resources/map/components}/place-card.test.tsx +0 -0
  80. /package/template/src/{components/map → resources/map/components}/place-carousel.test.tsx +0 -0
  81. /package/template/src/{components/map → resources/map/components}/place-inspector.test.tsx +0 -0
  82. /package/template/src/{components/map → resources/map/components}/place-list.test.tsx +0 -0
  83. /package/template/src/{components/map → resources/map/components}/types.ts +0 -0
  84. /package/template/src/resources/{map-resource.json → map/map-resource.json} +0 -0
  85. /package/template/src/resources/{review-resource.json → review/review-resource.json} +0 -0
  86. /package/template/src/resources/{review-resource.test.tsx → review/review-resource.test.tsx} +0 -0
  87. /package/template/src/resources/{review-resource.tsx → review/review-resource.tsx} +0 -0
@@ -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(file => file.endsWith('-resource.tsx'))
89
- .map(file => {
90
- // Extract kebab-case name: 'review-resource.tsx' -> 'review'
91
- const kebabName = file.replace('-resource.tsx', '');
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: file.replace('.tsx', ''),
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 files found in src/resources/');
110
- console.error('Resource files should be named like: review-resource.tsx');
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
- for (const { output, buildOutDir } of resourceFiles) {
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(distDir, output);
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
- // Generate resource metadata JSON files with URIs
218
- console.log('\nGenerating resource metadata JSON files...');
219
- const timestamp = Date.now().toString(36);
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(`\nBuilt files:`, readdirSync(distDir));
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
@@ -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 [file] [options]
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
- file Optional JS file to deploy (e.g., dist/carousel.js)
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 Push all resources with "prod" tag
40
- sunpeak deploy dist/carousel.js 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
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 file provided, check current directory first, then dist/
60
- if (!deployOptions.file) {
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, file: resource.jsPath }, d);
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 [file] [options]
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
- file Optional JS file to deploy (e.g., dist/carousel.js)
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 Deploy all resources with "prod" tag
108
- sunpeak deploy dist/carousel.js 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
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 file path
117
- options.file = arg;
116
+ // Positional argument - treat as directory path
117
+ options.dir = arg;
118
118
  }
119
119
 
120
120
  i++;
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, readdirSync } from 'fs';
3
- import { join, dirname, basename } from 'path';
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
- * Returns array of { name, jsPath, metaPath, meta }
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 files = d.readdirSync(distDir);
74
- const jsFiles = files.filter((f) => f.endsWith('.js'));
75
- const jsonFiles = new Set(files.filter((f) => f.endsWith('.json')));
76
-
77
- // Only include .js files that have a matching .json file
78
- return jsFiles
79
- .filter((jsFile) => {
80
- const name = jsFile.replace('.js', '');
81
- return jsonFiles.has(`${name}.json`);
82
- })
83
- .map((jsFile) => {
84
- const name = jsFile.replace('.js', '');
85
- const jsPath = join(distDir, jsFile);
86
- const metaPath = join(distDir, `${name}.json`);
87
-
88
- let meta = null;
89
- try {
90
- meta = JSON.parse(d.readFileSync(metaPath, 'utf-8'));
91
- } catch {
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
- return { name, jsPath, metaPath, meta };
96
- });
129
+ return resources;
97
130
  }
98
131
 
99
132
  /**
100
- * Build a resource from a specific JS file path
101
- * Returns { name, jsPath, metaPath, meta }
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 buildResourceFromFile(jsPath, deps = defaultDeps) {
137
+ function buildResourceFromDir(resourceDir, deps = defaultDeps) {
104
138
  const d = { ...defaultDeps, ...deps };
105
139
 
106
- if (!d.existsSync(jsPath)) {
107
- d.console.error(`Error: File not found: ${jsPath}`);
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 filename (remove .js extension)
112
- const fileName = basename(jsPath);
113
- const name = fileName.replace('.js', '');
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
- if (d.existsSync(metaPath)) {
121
- try {
122
- meta = JSON.parse(d.readFileSync(metaPath, 'utf-8'));
123
- } catch {
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
- return { name, jsPath, metaPath, meta };
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.file - Path to a specific resource JS file (optional)
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 [file] [options]
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
- file Optional JS file to push (e.g., dist/carousel.js)
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 Push all resources from dist/
243
- sunpeak push dist/carousel.js Push a single resource
244
- sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
245
- sunpeak push -t v1.0.0 Push with a version tag
246
- sunpeak push -t v1.0.0 -t prod Push with multiple tags
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 file or all from dist directory
316
+ // Find resources - either a specific directory or all from dist directory
268
317
  let resources;
269
- if (options.file) {
270
- // Push a single specific resource
271
- resources = [buildResourceFromFile(options.file, d)];
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
- d.console.log(`✓ Pushed ${resource.name} (id: ${result.id})`);
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 [file] [options]
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
- file Optional JS file to push (e.g., dist/carousel.js)
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 Push all resources from dist/
351
- sunpeak push dist/carousel.js Push a single resource
352
- sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
353
- sunpeak push -t v1.0.0 Push with a version tag
354
- sunpeak push -t v1.0.0 -t prod Push with multiple tags
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 file path
359
- options.file = arg;
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
+ }