sunpeak 0.7.11 → 0.8.4

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 (76) hide show
  1. package/README.md +2 -1
  2. package/bin/commands/deploy.mjs +18 -8
  3. package/bin/commands/dev.mjs +60 -4
  4. package/bin/commands/login.mjs +73 -55
  5. package/bin/commands/logout.mjs +26 -12
  6. package/bin/commands/mcp.mjs +1 -1
  7. package/bin/commands/pull.mjs +60 -39
  8. package/bin/commands/push.mjs +73 -49
  9. package/bin/commands/upgrade.mjs +203 -0
  10. package/bin/sunpeak.js +68 -35
  11. package/dist/chatgpt/chatgpt-simulator.d.ts +2 -1
  12. package/dist/index.cjs +13 -14
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.js +13 -14
  15. package/dist/index.js.map +1 -1
  16. package/dist/mcp/entry.cjs +41 -9
  17. package/dist/mcp/entry.cjs.map +1 -1
  18. package/dist/mcp/entry.js +42 -10
  19. package/dist/mcp/entry.js.map +1 -1
  20. package/dist/mcp/index.cjs +1 -1
  21. package/dist/mcp/index.js +1 -1
  22. package/dist/{server-CziiHU7V.cjs → server-B9YgCQdS.cjs} +3 -2
  23. package/dist/{server-CziiHU7V.cjs.map → server-B9YgCQdS.cjs.map} +1 -1
  24. package/dist/{server-D8kyzuiq.js → server-DVmTC-SF.js} +3 -2
  25. package/dist/{server-D8kyzuiq.js.map → server-DVmTC-SF.js.map} +1 -1
  26. package/dist/style.css +62 -0
  27. package/dist/types/simulation.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/template/.sunpeak/dev.tsx +78 -15
  30. package/template/.sunpeak/vite-env.d.ts +1 -0
  31. package/template/README.md +35 -20
  32. package/template/dist/albums.js +1 -1
  33. package/template/dist/albums.json +3 -2
  34. package/template/dist/carousel.js +1 -1
  35. package/template/dist/carousel.json +3 -2
  36. package/template/dist/confirmation.js +49 -0
  37. package/template/dist/confirmation.json +16 -0
  38. package/template/dist/counter.js +1 -1
  39. package/template/dist/counter.json +7 -2
  40. package/template/dist/map.js +1 -1
  41. package/template/dist/map.json +6 -3
  42. package/template/node_modules/.vite/deps/_metadata.json +19 -19
  43. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  44. package/template/src/components/map/map-view.test.tsx +1 -1
  45. package/template/src/components/map/map-view.tsx +1 -1
  46. package/template/src/components/map/map.tsx +1 -1
  47. package/template/src/components/map/place-card.test.tsx +1 -1
  48. package/template/src/components/map/place-card.tsx +1 -1
  49. package/template/src/components/map/place-carousel.test.tsx +1 -1
  50. package/template/src/components/map/place-carousel.tsx +1 -1
  51. package/template/src/components/map/place-inspector.test.tsx +1 -1
  52. package/template/src/components/map/place-inspector.tsx +1 -1
  53. package/template/src/components/map/place-list.test.tsx +1 -1
  54. package/template/src/components/map/place-list.tsx +1 -1
  55. package/template/src/components/map/types.ts +18 -0
  56. package/template/src/resources/albums-resource.json +1 -1
  57. package/template/src/resources/carousel-resource.json +1 -1
  58. package/template/src/resources/confirmation-resource.json +12 -0
  59. package/template/src/resources/confirmation-resource.tsx +479 -0
  60. package/template/src/resources/counter-resource.json +4 -1
  61. package/template/src/resources/index.ts +39 -4
  62. package/template/src/resources/map-resource.json +7 -2
  63. package/template/src/simulations/albums-show-simulation.json +131 -0
  64. package/template/src/simulations/carousel-show-simulation.json +68 -0
  65. package/template/src/simulations/confirmation-diff-simulation.json +80 -0
  66. package/template/src/simulations/confirmation-post-simulation.json +56 -0
  67. package/template/src/simulations/confirmation-purchase-simulation.json +88 -0
  68. package/template/src/simulations/counter-show-simulation.json +20 -0
  69. package/template/src/simulations/index.ts +17 -12
  70. package/template/src/simulations/map-show-simulation.json +123 -0
  71. package/template/src/vite-env.d.ts +1 -0
  72. package/template/tsconfig.json +1 -1
  73. package/template/src/simulations/albums-simulation.ts +0 -147
  74. package/template/src/simulations/carousel-simulation.ts +0 -84
  75. package/template/src/simulations/counter-simulation.ts +0 -34
  76. package/template/src/simulations/map-simulation.ts +0 -154
@@ -11,7 +11,7 @@ const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');
11
11
  /**
12
12
  * Load credentials from disk
13
13
  */
14
- function loadCredentials() {
14
+ function loadCredentialsImpl() {
15
15
  if (!existsSync(CREDENTIALS_FILE)) {
16
16
  return null;
17
17
  }
@@ -25,7 +25,7 @@ function loadCredentials() {
25
25
  /**
26
26
  * Get the current git repository name in owner/repo format
27
27
  */
28
- function getGitRepoName() {
28
+ function getGitRepoNameImpl() {
29
29
  try {
30
30
  // Try to get the remote URL first
31
31
  const remoteUrl = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf-8' }).trim();
@@ -44,16 +44,33 @@ function getGitRepoName() {
44
44
  return null;
45
45
  }
46
46
 
47
+ /**
48
+ * Default dependencies (real implementations)
49
+ */
50
+ export const defaultDeps = {
51
+ fetch: globalThis.fetch,
52
+ loadCredentials: loadCredentialsImpl,
53
+ getGitRepoName: getGitRepoNameImpl,
54
+ existsSync,
55
+ readFileSync,
56
+ readdirSync,
57
+ console,
58
+ process,
59
+ apiUrl: SUNPEAK_API_URL,
60
+ };
61
+
47
62
  /**
48
63
  * Find all resources in a directory
49
64
  * Returns array of { name, jsPath, metaPath, meta }
50
65
  */
51
- export function findResources(distDir) {
52
- if (!existsSync(distDir)) {
66
+ export function findResources(distDir, deps = defaultDeps) {
67
+ const d = { ...defaultDeps, ...deps };
68
+
69
+ if (!d.existsSync(distDir)) {
53
70
  return [];
54
71
  }
55
72
 
56
- const files = readdirSync(distDir);
73
+ const files = d.readdirSync(distDir);
57
74
  const jsFiles = files.filter((f) => f.endsWith('.js'));
58
75
  const jsonFiles = new Set(files.filter((f) => f.endsWith('.json')));
59
76
 
@@ -70,9 +87,9 @@ export function findResources(distDir) {
70
87
 
71
88
  let meta = null;
72
89
  try {
73
- meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
90
+ meta = JSON.parse(d.readFileSync(metaPath, 'utf-8'));
74
91
  } catch {
75
- console.warn(`Warning: Could not parse ${name}.json`);
92
+ d.console.warn(`Warning: Could not parse ${name}.json`);
76
93
  }
77
94
 
78
95
  return { name, jsPath, metaPath, meta };
@@ -83,10 +100,12 @@ export function findResources(distDir) {
83
100
  * Build a resource from a specific JS file path
84
101
  * Returns { name, jsPath, metaPath, meta }
85
102
  */
86
- function buildResourceFromFile(jsPath) {
87
- if (!existsSync(jsPath)) {
88
- console.error(`Error: File not found: ${jsPath}`);
89
- process.exit(1);
103
+ function buildResourceFromFile(jsPath, deps = defaultDeps) {
104
+ const d = { ...defaultDeps, ...deps };
105
+
106
+ if (!d.existsSync(jsPath)) {
107
+ d.console.error(`Error: File not found: ${jsPath}`);
108
+ d.process.exit(1);
90
109
  }
91
110
 
92
111
  // Extract name from filename (remove .js extension)
@@ -98,11 +117,11 @@ function buildResourceFromFile(jsPath) {
98
117
  const metaPath = join(dir, `${name}.json`);
99
118
 
100
119
  let meta = null;
101
- if (existsSync(metaPath)) {
120
+ if (d.existsSync(metaPath)) {
102
121
  try {
103
- meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
122
+ meta = JSON.parse(d.readFileSync(metaPath, 'utf-8'));
104
123
  } catch {
105
- console.warn(`Warning: Could not parse ${name}.json`);
124
+ d.console.warn(`Warning: Could not parse ${name}.json`);
106
125
  }
107
126
  }
108
127
 
@@ -112,12 +131,14 @@ function buildResourceFromFile(jsPath) {
112
131
  /**
113
132
  * Push a single resource to the API
114
133
  */
115
- async function pushResource(resource, repository, tags, accessToken) {
134
+ async function pushResource(resource, repository, tags, accessToken, deps = defaultDeps) {
135
+ const d = { ...defaultDeps, ...deps };
136
+
116
137
  if (!resource.meta?.uri) {
117
138
  throw new Error('Resource is missing URI. Run "sunpeak build" to generate URIs.');
118
139
  }
119
140
 
120
- const jsContent = readFileSync(resource.jsPath);
141
+ const jsContent = d.readFileSync(resource.jsPath);
121
142
  const jsBlob = new Blob([jsContent], { type: 'application/javascript' });
122
143
 
123
144
  // Build form data
@@ -168,7 +189,7 @@ async function pushResource(resource, repository, tags, accessToken) {
168
189
  });
169
190
  }
170
191
 
171
- const response = await fetch(`${SUNPEAK_API_URL}/api/v1/resources`, {
192
+ const response = await d.fetch(`${d.apiUrl}/api/v1/resources`, {
172
193
  method: 'POST',
173
194
  headers: {
174
195
  Authorization: `Bearer ${accessToken}`,
@@ -195,11 +216,14 @@ async function pushResource(resource, repository, tags, accessToken) {
195
216
  * @param {string} options.repository - Repository name (optional, defaults to git repo name)
196
217
  * @param {string} options.file - Path to a specific resource JS file (optional)
197
218
  * @param {string[]} options.tags - Tags to assign to the pushed resources (optional)
219
+ * @param {Object} deps - Dependencies (for testing). Uses defaultDeps if not provided.
198
220
  */
199
- export async function push(projectRoot = process.cwd(), options = {}) {
221
+ export async function push(projectRoot = process.cwd(), options = {}, deps = defaultDeps) {
222
+ const d = { ...defaultDeps, ...deps };
223
+
200
224
  // Handle help flag
201
225
  if (options.help) {
202
- console.log(`
226
+ d.console.log(`
203
227
  sunpeak push - Push resources to the Sunpeak repository
204
228
 
205
229
  Usage:
@@ -219,76 +243,76 @@ Examples:
219
243
  sunpeak push dist/carousel.js Push a single resource
220
244
  sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
221
245
  sunpeak push -t v1.0.0 Push with a version tag
222
- sunpeak push -t v1.0.0 -t latest Push with multiple tags
246
+ sunpeak push -t v1.0.0 -t prod Push with multiple tags
223
247
  `);
224
248
  return;
225
249
  }
226
250
 
227
251
  // Check credentials
228
- const credentials = loadCredentials();
252
+ const credentials = d.loadCredentials();
229
253
  if (!credentials?.access_token) {
230
- console.error('Error: Not logged in. Run "sunpeak login" first.');
231
- process.exit(1);
254
+ d.console.error('Error: Not logged in. Run "sunpeak login" first.');
255
+ d.process.exit(1);
232
256
  }
233
257
 
234
258
  // Determine repository name (owner/repo format)
235
- const repository = options.repository || getGitRepoName();
259
+ const repository = options.repository || d.getGitRepoName();
236
260
  if (!repository) {
237
- console.error('Error: Could not determine repository name.');
238
- console.error('Please provide a repository name: sunpeak push --repository <owner/repo>');
239
- console.error('Or run this command from within a git repository with a remote origin.');
240
- process.exit(1);
261
+ d.console.error('Error: Could not determine repository name.');
262
+ d.console.error('Please provide a repository name: sunpeak push --repository <owner/repo>');
263
+ d.console.error('Or run this command from within a git repository with a remote origin.');
264
+ d.process.exit(1);
241
265
  }
242
266
 
243
267
  // Find resources - either a specific file or all from dist directory
244
268
  let resources;
245
269
  if (options.file) {
246
270
  // Push a single specific resource
247
- resources = [buildResourceFromFile(options.file)];
271
+ resources = [buildResourceFromFile(options.file, d)];
248
272
  } else {
249
273
  // Default: find all resources in dist directory
250
274
  const distDir = join(projectRoot, 'dist');
251
- if (!existsSync(distDir)) {
252
- console.error(`Error: dist/ directory not found`);
253
- console.error('Run "sunpeak build" first to build your resources.');
254
- process.exit(1);
275
+ if (!d.existsSync(distDir)) {
276
+ d.console.error(`Error: dist/ directory not found`);
277
+ d.console.error('Run "sunpeak build" first to build your resources.');
278
+ d.process.exit(1);
255
279
  }
256
280
 
257
- resources = findResources(distDir);
281
+ resources = findResources(distDir, d);
258
282
  if (resources.length === 0) {
259
- console.error(`Error: No resources found in dist/`);
260
- console.error('Run "sunpeak build" first to build your resources.');
261
- process.exit(1);
283
+ d.console.error(`Error: No resources found in dist/`);
284
+ d.console.error('Run "sunpeak build" first to build your resources.');
285
+ d.process.exit(1);
262
286
  }
263
287
  }
264
288
 
265
- console.log(`Pushing ${resources.length} resource(s) to repository "${repository}"...`);
289
+ d.console.log(`Pushing ${resources.length} resource(s) to repository "${repository}"...`);
266
290
  if (options.tags && options.tags.length > 0) {
267
- console.log(`Tags: ${options.tags.join(', ')}`);
291
+ d.console.log(`Tags: ${options.tags.join(', ')}`);
268
292
  }
269
- console.log();
293
+ d.console.log();
270
294
 
271
295
  // Push each resource
272
296
  let successCount = 0;
273
297
  for (const resource of resources) {
274
298
  try {
275
- const result = await pushResource(resource, repository, options.tags, credentials.access_token);
276
- console.log(`✓ Pushed ${resource.name} (id: ${result.id})`);
299
+ const result = await pushResource(resource, repository, options.tags, credentials.access_token, d);
300
+ d.console.log(`✓ Pushed ${resource.name} (id: ${result.id})`);
277
301
  if (result.tags?.length > 0) {
278
- console.log(` Tags: ${result.tags.join(', ')}`);
302
+ d.console.log(` Tags: ${result.tags.join(', ')}`);
279
303
  }
280
304
  successCount++;
281
305
  } catch (error) {
282
- console.error(`✗ Failed to push ${resource.name}: ${error.message}`);
306
+ d.console.error(`✗ Failed to push ${resource.name}: ${error.message}`);
283
307
  }
284
308
  }
285
309
 
286
- console.log();
310
+ d.console.log();
287
311
  if (successCount === resources.length) {
288
- console.log(`✓ Successfully pushed ${successCount} resource(s).`);
312
+ d.console.log(`✓ Successfully pushed ${successCount} resource(s).`);
289
313
  } else {
290
- console.log(`Pushed ${successCount}/${resources.length} resource(s).`);
291
- process.exit(1);
314
+ d.console.log(`Pushed ${successCount}/${resources.length} resource(s).`);
315
+ d.process.exit(1);
292
316
  }
293
317
  }
294
318
 
@@ -327,7 +351,7 @@ Examples:
327
351
  sunpeak push dist/carousel.js Push a single resource
328
352
  sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
329
353
  sunpeak push -t v1.0.0 Push with a version tag
330
- sunpeak push -t v1.0.0 -t latest Push with multiple tags
354
+ sunpeak push -t v1.0.0 -t prod Push with multiple tags
331
355
  `);
332
356
  process.exit(0);
333
357
  } else if (!arg.startsWith('-')) {
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ import { execSync, spawn } from 'child_process';
3
+ import { readFileSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ /**
10
+ * Get the current installed version from package.json
11
+ */
12
+ function getCurrentVersionImpl() {
13
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
14
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
15
+ return pkg.version;
16
+ }
17
+
18
+ /**
19
+ * Fetch the latest version from npm registry
20
+ */
21
+ async function fetchLatestVersionImpl(packageName = 'sunpeak') {
22
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
23
+ if (!response.ok) {
24
+ throw new Error(`Failed to fetch latest version: ${response.status}`);
25
+ }
26
+ const data = await response.json();
27
+ return data.version;
28
+ }
29
+
30
+ /**
31
+ * Compare two semver versions
32
+ * Returns: -1 if a < b, 0 if a === b, 1 if a > b
33
+ */
34
+ function compareVersions(a, b) {
35
+ const partsA = a.split('.').map(Number);
36
+ const partsB = b.split('.').map(Number);
37
+
38
+ for (let i = 0; i < 3; i++) {
39
+ const numA = partsA[i] || 0;
40
+ const numB = partsB[i] || 0;
41
+ if (numA < numB) return -1;
42
+ if (numA > numB) return 1;
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ /**
48
+ * Detect which package manager is being used
49
+ */
50
+ function detectPackageManagerImpl() {
51
+ // Check npm_config_user_agent first (set by npm/pnpm/yarn when running scripts)
52
+ const userAgent = process.env.npm_config_user_agent || '';
53
+ if (userAgent.includes('pnpm')) return 'pnpm';
54
+ if (userAgent.includes('yarn')) return 'yarn';
55
+ if (userAgent.includes('npm')) return 'npm';
56
+
57
+ // Fallback: check if commands exist
58
+ try {
59
+ execSync('pnpm --version', { stdio: 'ignore' });
60
+ return 'pnpm';
61
+ } catch {
62
+ // pnpm not available
63
+ }
64
+
65
+ try {
66
+ execSync('yarn --version', { stdio: 'ignore' });
67
+ return 'yarn';
68
+ } catch {
69
+ // yarn not available
70
+ }
71
+
72
+ return 'npm';
73
+ }
74
+
75
+ /**
76
+ * Run the upgrade command
77
+ */
78
+ function runUpgradeImpl(packageManager, packageName = 'sunpeak') {
79
+ const commands = {
80
+ npm: ['npm', ['install', '-g', packageName]],
81
+ pnpm: ['pnpm', ['add', '-g', packageName]],
82
+ yarn: ['yarn', ['global', 'add', packageName]],
83
+ };
84
+
85
+ const [cmd, args] = commands[packageManager];
86
+
87
+ return new Promise((resolve, reject) => {
88
+ const child = spawn(cmd, args, {
89
+ stdio: 'inherit',
90
+ shell: process.platform === 'win32',
91
+ });
92
+
93
+ child.on('close', (code) => {
94
+ if (code === 0) {
95
+ resolve();
96
+ } else {
97
+ reject(new Error(`Upgrade failed with exit code ${code}`));
98
+ }
99
+ });
100
+
101
+ child.on('error', (err) => {
102
+ reject(err);
103
+ });
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Default dependencies (real implementations)
109
+ */
110
+ export const defaultDeps = {
111
+ getCurrentVersion: getCurrentVersionImpl,
112
+ fetchLatestVersion: fetchLatestVersionImpl,
113
+ detectPackageManager: detectPackageManagerImpl,
114
+ runUpgrade: runUpgradeImpl,
115
+ console,
116
+ process,
117
+ };
118
+
119
+ /**
120
+ * Main upgrade command
121
+ * @param {Object} options - Command options
122
+ * @param {Object} deps - Dependencies (for testing). Uses defaultDeps if not provided.
123
+ */
124
+ export async function upgrade(options = {}, deps = defaultDeps) {
125
+ const d = { ...defaultDeps, ...deps };
126
+
127
+ // Show help if requested
128
+ if (options.help) {
129
+ d.console.log(`
130
+ sunpeak upgrade - Upgrade sunpeak to the latest version
131
+
132
+ Usage:
133
+ sunpeak upgrade [options]
134
+
135
+ Options:
136
+ --check, -c Check for updates without installing
137
+ --help, -h Show this help message
138
+
139
+ Examples:
140
+ sunpeak upgrade # Upgrade to latest version
141
+ sunpeak upgrade --check # Check if updates are available
142
+ `);
143
+ return;
144
+ }
145
+
146
+ const currentVersion = d.getCurrentVersion();
147
+ d.console.log(`Current version: ${currentVersion}`);
148
+ d.console.log('Checking for updates...');
149
+
150
+ let latestVersion;
151
+ try {
152
+ latestVersion = await d.fetchLatestVersion();
153
+ } catch (error) {
154
+ d.console.error(`Error checking for updates: ${error.message}`);
155
+ d.process.exit(1);
156
+ return;
157
+ }
158
+
159
+ const comparison = compareVersions(currentVersion, latestVersion);
160
+
161
+ if (comparison >= 0) {
162
+ d.console.log(`\n✓ You are already on the latest version (${currentVersion})`);
163
+ return;
164
+ }
165
+
166
+ d.console.log(`\nNew version available: ${latestVersion}`);
167
+
168
+ // If --check flag, just report and exit
169
+ if (options.check) {
170
+ d.console.log(`\nRun "sunpeak upgrade" to upgrade.`);
171
+ return;
172
+ }
173
+
174
+ const packageManager = d.detectPackageManager();
175
+ d.console.log(`\nUpgrading using ${packageManager}...`);
176
+
177
+ try {
178
+ await d.runUpgrade(packageManager);
179
+ d.console.log(`\n✓ Successfully upgraded to sunpeak@${latestVersion}`);
180
+ } catch (error) {
181
+ d.console.error(`\nError upgrading: ${error.message}`);
182
+ d.console.log(`\nYou can manually upgrade by running:`);
183
+ d.console.log(` ${packageManager} ${packageManager === 'yarn' ? 'global add' : packageManager === 'pnpm' ? 'add -g' : 'install -g'} sunpeak`);
184
+ d.process.exit(1);
185
+ }
186
+ }
187
+
188
+ // Export for testing
189
+ export { compareVersions };
190
+
191
+ // Allow running directly
192
+ if (import.meta.url === `file://${process.argv[1]}`) {
193
+ const args = process.argv.slice(2);
194
+ const options = {
195
+ check: args.includes('--check') || args.includes('-c'),
196
+ help: args.includes('--help') || args.includes('-h'),
197
+ };
198
+
199
+ upgrade(options).catch((error) => {
200
+ console.error('Error:', error.message);
201
+ process.exit(1);
202
+ });
203
+ }
package/bin/sunpeak.js CHANGED
@@ -28,7 +28,7 @@ function checkPackageJson() {
28
28
  }
29
29
 
30
30
  function parseResourcesInput(input) {
31
- const VALID_RESOURCES = ['albums', 'carousel', 'counter', 'map'];
31
+ const VALID_RESOURCES = ['albums', 'carousel', 'confirmation', 'counter', 'map'];
32
32
 
33
33
  // If no input, return all resources
34
34
  if (!input || input.trim() === '') {
@@ -57,10 +57,11 @@ function parseResourcesInput(input) {
57
57
  function updateIndexFiles(targetDir, selectedResources) {
58
58
  // Map resource names to their component/export names
59
59
  const resourceMap = {
60
- albums: { component: 'album', resourceClass: 'AlbumsResource', simulation: 'albums' },
61
- carousel: { component: 'carousel', resourceClass: 'CarouselResource', simulation: 'carousel' },
62
- counter: { component: null, resourceClass: 'CounterResource', simulation: 'counter' },
63
- map: { component: 'map', resourceClass: 'MapResource', simulation: 'map' },
60
+ albums: { component: 'album', resourceClass: 'AlbumsResource' },
61
+ carousel: { component: 'carousel', resourceClass: 'CarouselResource' },
62
+ confirmation: { component: null, resourceClass: 'ConfirmationResource' },
63
+ counter: { component: null, resourceClass: 'CounterResource' },
64
+ map: { component: 'map', resourceClass: 'MapResource' },
64
65
  };
65
66
 
66
67
  // Update components/index.ts
@@ -73,31 +74,47 @@ function updateIndexFiles(targetDir, selectedResources) {
73
74
  .join('\n');
74
75
  writeFileSync(componentsIndexPath, componentExports + '\n');
75
76
 
76
- // Update resources/index.ts
77
+ // Update resources/index.ts - must have default export for dev.tsx
77
78
  const resourcesIndexPath = join(targetDir, 'src', 'resources', 'index.ts');
78
- const resourceExports = selectedResources
79
- .map((r) => `export { ${resourceMap[r].resourceClass} } from './${r}-resource';`)
79
+ const resourceImports = selectedResources
80
+ .map((r) => `import { ${resourceMap[r].resourceClass} } from './${r}-resource';`)
80
81
  .join('\n');
81
- writeFileSync(resourcesIndexPath, resourceExports + '\n');
82
+ const resourceExportsObject = selectedResources
83
+ .map((r) => ` ${resourceMap[r].resourceClass},`)
84
+ .join('\n');
85
+ const resourcesContent = `${resourceImports}
86
+
87
+ export default {
88
+ ${resourceExportsObject}
89
+ };
90
+ `;
91
+ writeFileSync(resourcesIndexPath, resourcesContent);
82
92
 
83
- // Update simulations/index.ts
93
+ // Update simulations/index.ts - uses auto-discovery for JSON simulation files
84
94
  const simulationsIndexPath = join(targetDir, 'src', 'simulations', 'index.ts');
85
- const simulationImports = selectedResources
86
- .map((r) => `import { ${r}Simulation } from './${r}-simulation.js';`)
87
- .join('\n');
88
- const simulationExports = selectedResources.map((r) => ` ${r}: ${r}Simulation,`).join('\n');
89
95
  const simulationsContent = `/**
90
96
  * Server-safe simulation configurations
91
97
  *
92
- * This file contains only metadata and can be safely imported in Node.js contexts
93
- * (like MCP servers) without causing issues with CSS imports or React components.
98
+ * Auto-discovers all *-simulation.json files in this directory.
99
+ * File naming: {resource}-{tool}-simulation.json (e.g., albums-show-simulation.json)
100
+ *
101
+ * This file can be safely imported in Node.js contexts (like MCP servers)
102
+ * without causing issues with CSS imports or React components.
94
103
  */
95
104
 
96
- ${simulationImports}
97
-
98
- export const SIMULATIONS = {
99
- ${simulationExports}
100
- } as const;
105
+ // Auto-discover all simulation JSON files
106
+ const simulationModules = import.meta.glob('./*-simulation.json', { eager: true });
107
+
108
+ // Build SIMULATIONS object from discovered files
109
+ // Key is the full name without -simulation.json suffix (e.g., 'albums-show')
110
+ export const SIMULATIONS = Object.fromEntries(
111
+ Object.entries(simulationModules).map(([path, module]) => {
112
+ // Extract simulation key from path: './albums-show-simulation.json' -> 'albums-show'
113
+ const match = path.match(/\\.\\/(.+)-simulation\\.json$/);
114
+ const key = match?.[1] ?? path;
115
+ return [key, (module as { default: unknown }).default];
116
+ })
117
+ ) as Record<string, unknown>;
101
118
  `;
102
119
  writeFileSync(simulationsIndexPath, simulationsContent);
103
120
  }
@@ -122,7 +139,7 @@ async function init(projectName, resourcesArg) {
122
139
  console.log(`☀️ 🏔️ Resources: ${resourcesArg}`);
123
140
  } else {
124
141
  resourcesInput = await prompt(
125
- '☀️ 🏔️ Resources (UIs) to include [albums, carousel, counter, map]: '
142
+ '☀️ 🏔️ Resources (UIs) to include [albums, carousel, confirmation, counter, map]: '
126
143
  );
127
144
  }
128
145
  const selectedResources = parseResourcesInput(resourcesInput);
@@ -144,6 +161,7 @@ async function init(projectName, resourcesArg) {
144
161
  const resourceComponentMap = {
145
162
  albums: 'album',
146
163
  carousel: 'carousel',
164
+ confirmation: null, // Confirmation doesn't have a component directory
147
165
  counter: null, // Counter doesn't have a component directory
148
166
  map: 'map',
149
167
  };
@@ -159,16 +177,21 @@ async function init(projectName, resourcesArg) {
159
177
  }
160
178
 
161
179
  // Filter resource files based on selection
162
- const VALID_RESOURCES = ['albums', 'carousel', 'counter', 'map'];
180
+ const VALID_RESOURCES = ['albums', 'carousel', 'confirmation', 'counter', 'map'];
163
181
  const excludedResources = VALID_RESOURCES.filter((r) => !selectedResources.includes(r));
164
182
 
165
183
  for (const resource of excludedResources) {
166
- // Skip resource files
167
- if (name === `${resource}-resource.tsx` || name === `${resource}-resource.test.tsx`) {
184
+ // Skip resource files (tsx, test, and json metadata)
185
+ if (
186
+ name === `${resource}-resource.tsx` ||
187
+ name === `${resource}-resource.test.tsx` ||
188
+ name === `${resource}-resource.json`
189
+ ) {
168
190
  return false;
169
191
  }
170
- // Skip simulation files
171
- if (name === `${resource}-simulation.ts`) {
192
+ // Skip simulation JSON files that start with the resource name
193
+ // e.g., albums-show-simulation.json, albums-edit-simulation.json
194
+ if (name.startsWith(`${resource}-`) && name.endsWith('-simulation.json')) {
172
195
  return false;
173
196
  }
174
197
  // Skip component directories (map resource name to component dir name)
@@ -217,13 +240,13 @@ Done! To get started:
217
240
 
218
241
  cd ${projectName}
219
242
  pnpm install
220
- pnpm dev
243
+ sunpeak dev
221
244
 
222
245
  That's it! Your project commands:
223
246
 
224
- pnpm dev # Start development server
225
- pnpm build # Build for production
226
- pnpm mcp # Start MCP server
247
+ sunpeak dev # Start development server
248
+ sunpeak build # Build for production
249
+ sunpeak mcp # Start MCP server
227
250
  pnpm test # Run tests
228
251
 
229
252
  See README.md for more details.
@@ -291,6 +314,7 @@ function parseResourceArgs(args) {
291
314
  'push',
292
315
  'pull',
293
316
  'deploy',
317
+ 'upgrade',
294
318
  'help',
295
319
  undefined,
296
320
  ];
@@ -360,6 +384,17 @@ function parseResourceArgs(args) {
360
384
  }
361
385
  break;
362
386
 
387
+ case 'upgrade':
388
+ {
389
+ const { upgrade } = await import(join(COMMANDS_DIR, 'upgrade.mjs'));
390
+ const options = {
391
+ check: args.includes('--check') || args.includes('-c'),
392
+ help: args.includes('--help') || args.includes('-h'),
393
+ };
394
+ await upgrade(options);
395
+ }
396
+ break;
397
+
363
398
  case 'help':
364
399
  case undefined:
365
400
  console.log(`
@@ -369,13 +404,10 @@ Usage:
369
404
  npx sunpeak new [name] [resources] Create a new project (no install needed)
370
405
  pnpm dlx sunpeak new Alternative with pnpm
371
406
 
372
- Resources: albums, carousel, counter, map (comma/space separated)
407
+ Resources: albums, carousel, confirmation, counter, map (comma/space separated)
373
408
  Example: npx sunpeak new my-app "albums,carousel"
374
409
 
375
410
  Inside your project, use npm scripts:
376
- pnpm dev Start development server
377
- pnpm build Build for production
378
- pnpm mcp Start MCP server
379
411
  pnpm test Run tests
380
412
 
381
413
  Direct CLI commands (when sunpeak is installed):
@@ -388,6 +420,7 @@ Direct CLI commands (when sunpeak is installed):
388
420
  sunpeak push Push resources to repository
389
421
  sunpeak pull Pull resources from repository
390
422
  sunpeak deploy Push resources with "prod" tag
423
+ sunpeak upgrade Upgrade sunpeak to latest version
391
424
  sunpeak --version Show version number
392
425
 
393
426
  For more information, visit: https://sunpeak.ai/
@@ -1,8 +1,9 @@
1
1
  import { Simulation } from '../types/simulation';
2
2
  import * as React from 'react';
3
+ type SimulationWithResource = Simulation & Required<Pick<Simulation, 'resource'>>;
3
4
  interface ChatGPTSimulatorProps {
4
5
  children?: React.ReactNode;
5
- simulations?: Simulation[];
6
+ simulations?: SimulationWithResource[];
6
7
  appName?: string;
7
8
  appIcon?: string;
8
9
  }