sunpeak 0.7.10 → 0.8.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 (114) hide show
  1. package/README.md +9 -11
  2. package/bin/commands/deploy.mjs +18 -9
  3. package/bin/commands/login.mjs +73 -55
  4. package/bin/commands/logout.mjs +26 -12
  5. package/bin/commands/pull.mjs +60 -39
  6. package/bin/commands/push.mjs +73 -49
  7. package/bin/commands/upgrade.mjs +203 -0
  8. package/bin/sunpeak.js +62 -31
  9. package/dist/chatgpt/chatgpt-simulator.d.ts +2 -1
  10. package/dist/index.cjs +15 -12
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +15 -12
  13. package/dist/index.js.map +1 -1
  14. package/dist/mcp/entry.cjs +41 -9
  15. package/dist/mcp/entry.cjs.map +1 -1
  16. package/dist/mcp/entry.js +42 -10
  17. package/dist/mcp/entry.js.map +1 -1
  18. package/dist/mcp/index.cjs +1 -1
  19. package/dist/mcp/index.js +1 -1
  20. package/dist/server-B9YgCQdS.cjs +5067 -0
  21. package/dist/server-B9YgCQdS.cjs.map +1 -0
  22. package/dist/server-DVmTC-SF.js +5068 -0
  23. package/dist/server-DVmTC-SF.js.map +1 -0
  24. package/dist/style.css +1 -1
  25. package/dist/types/simulation.d.ts +1 -1
  26. package/package.json +17 -17
  27. package/template/.sunpeak/dev.tsx +78 -15
  28. package/template/.sunpeak/vite-env.d.ts +1 -0
  29. package/template/README.md +6 -6
  30. package/template/dist/albums.js +4 -4
  31. package/template/dist/albums.json +1 -1
  32. package/template/dist/carousel.js +8 -8
  33. package/template/dist/carousel.json +1 -1
  34. package/template/dist/counter.js +4 -4
  35. package/template/dist/counter.json +1 -1
  36. package/template/dist/map.js +4 -4
  37. package/template/dist/map.json +1 -1
  38. package/template/node_modules/.bin/tsx +2 -2
  39. package/template/node_modules/.bin/vite +2 -2
  40. package/template/node_modules/.bin/vitest +2 -2
  41. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Avatar.js +7 -7
  42. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Avatar.js.map +1 -1
  43. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Button.js +6 -6
  44. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Checkbox.js +6 -6
  45. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Checkbox.js.map +1 -1
  46. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Icon.js +3 -3
  47. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Input.js +3 -3
  48. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_SegmentedControl.js +9 -9
  49. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_SegmentedControl.js.map +1 -1
  50. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +33 -33
  51. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js.map +1 -1
  52. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Textarea.js +7 -7
  53. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Textarea.js.map +1 -1
  54. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_theme.js +2 -2
  55. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_theme.js.map +1 -1
  56. package/template/node_modules/.vite/deps/_metadata.json +60 -60
  57. package/template/node_modules/.vite/deps/{chunk-CQ3GYAYB.js → chunk-2DZGWGIP.js} +5 -5
  58. package/template/node_modules/.vite/deps/{chunk-CQ3GYAYB.js.map → chunk-2DZGWGIP.js.map} +1 -1
  59. package/template/node_modules/.vite/deps/{chunk-4TLBUCVB.js → chunk-BUOVMFCD.js} +6 -6
  60. package/template/node_modules/.vite/deps/{chunk-4TLBUCVB.js.map → chunk-BUOVMFCD.js.map} +2 -2
  61. package/template/node_modules/.vite/deps/{chunk-BAG6OO6S.js → chunk-DYQDWJMS.js} +5 -5
  62. package/template/node_modules/.vite/deps/{chunk-BAG6OO6S.js.map → chunk-DYQDWJMS.js.map} +1 -1
  63. package/template/node_modules/.vite/deps/{chunk-YOJ6QPGS.js → chunk-JAGHY6H6.js} +3 -3
  64. package/template/node_modules/.vite/deps/{chunk-YOJ6QPGS.js.map → chunk-JAGHY6H6.js.map} +1 -1
  65. package/template/node_modules/.vite/deps/{chunk-PTVT3RFX.js → chunk-JGVISENQ.js} +6 -6
  66. package/template/node_modules/.vite/deps/{chunk-PTVT3RFX.js.map → chunk-JGVISENQ.js.map} +1 -1
  67. package/template/node_modules/.vite/deps/{chunk-LR7NKCX5.js → chunk-SPYXUHEY.js} +44 -44
  68. package/template/node_modules/.vite/deps/{chunk-LR7NKCX5.js.map → chunk-SPYXUHEY.js.map} +1 -1
  69. package/template/node_modules/.vite/deps/{chunk-SGWD4VEU.js → chunk-TSEQUROC.js} +113 -107
  70. package/template/node_modules/.vite/deps/chunk-TSEQUROC.js.map +7 -0
  71. package/template/node_modules/.vite/deps/{chunk-XB525PXG.js → chunk-UM3ZGDFR.js} +747 -747
  72. package/template/node_modules/.vite/deps/{chunk-XB525PXG.js.map → chunk-UM3ZGDFR.js.map} +1 -1
  73. package/template/node_modules/.vite/deps/{chunk-KFGKZMLK.js → chunk-XZTIOEPG.js} +7 -7
  74. package/template/node_modules/.vite/deps/{chunk-KFGKZMLK.js.map → chunk-XZTIOEPG.js.map} +2 -2
  75. package/template/node_modules/.vite/deps/embla-carousel-react.js +3 -3
  76. package/template/node_modules/.vite/deps/embla-carousel-react.js.map +1 -1
  77. package/template/node_modules/.vite/deps/react-dom.js +2 -2
  78. package/template/node_modules/.vite/deps/react-dom_client.js +11 -11
  79. package/template/node_modules/.vite/deps/react-dom_client.js.map +2 -2
  80. package/template/node_modules/.vite/deps/react.js +1 -1
  81. package/template/node_modules/.vite/deps/react_jsx-dev-runtime.js +5 -5
  82. package/template/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +1 -1
  83. package/template/node_modules/.vite/deps/react_jsx-runtime.js +2 -2
  84. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  85. package/template/package.json +11 -11
  86. package/template/src/components/map/map-view.test.tsx +1 -1
  87. package/template/src/components/map/map-view.tsx +1 -1
  88. package/template/src/components/map/map.tsx +1 -1
  89. package/template/src/components/map/place-card.test.tsx +1 -1
  90. package/template/src/components/map/place-card.tsx +1 -1
  91. package/template/src/components/map/place-carousel.test.tsx +1 -1
  92. package/template/src/components/map/place-carousel.tsx +1 -1
  93. package/template/src/components/map/place-inspector.test.tsx +1 -1
  94. package/template/src/components/map/place-inspector.tsx +1 -1
  95. package/template/src/components/map/place-list.test.tsx +1 -1
  96. package/template/src/components/map/place-list.tsx +1 -1
  97. package/template/src/components/map/types.ts +18 -0
  98. package/template/src/resources/index.ts +39 -4
  99. package/template/src/simulations/albums-show-simulation.json +131 -0
  100. package/template/src/simulations/carousel-show-simulation.json +68 -0
  101. package/template/src/simulations/counter-show-simulation.json +20 -0
  102. package/template/src/simulations/index.ts +17 -12
  103. package/template/src/simulations/map-show-simulation.json +123 -0
  104. package/template/src/vite-env.d.ts +1 -0
  105. package/template/tsconfig.json +1 -1
  106. package/dist/server-BOYwNazb.cjs +0 -930
  107. package/dist/server-BOYwNazb.cjs.map +0 -1
  108. package/dist/server-C6vMGV6H.js +0 -931
  109. package/dist/server-C6vMGV6H.js.map +0 -1
  110. package/template/node_modules/.vite/deps/chunk-SGWD4VEU.js.map +0 -7
  111. package/template/src/simulations/albums-simulation.ts +0 -147
  112. package/template/src/simulations/carousel-simulation.ts +0 -84
  113. package/template/src/simulations/counter-simulation.ts +0 -34
  114. 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
@@ -57,10 +57,10 @@ 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
+ counter: { component: null, resourceClass: 'CounterResource' },
63
+ map: { component: 'map', resourceClass: 'MapResource' },
64
64
  };
65
65
 
66
66
  // Update components/index.ts
@@ -73,31 +73,47 @@ function updateIndexFiles(targetDir, selectedResources) {
73
73
  .join('\n');
74
74
  writeFileSync(componentsIndexPath, componentExports + '\n');
75
75
 
76
- // Update resources/index.ts
76
+ // Update resources/index.ts - must have default export for dev.tsx
77
77
  const resourcesIndexPath = join(targetDir, 'src', 'resources', 'index.ts');
78
- const resourceExports = selectedResources
79
- .map((r) => `export { ${resourceMap[r].resourceClass} } from './${r}-resource';`)
78
+ const resourceImports = selectedResources
79
+ .map((r) => `import { ${resourceMap[r].resourceClass} } from './${r}-resource';`)
80
80
  .join('\n');
81
- writeFileSync(resourcesIndexPath, resourceExports + '\n');
81
+ const resourceExportsObject = selectedResources
82
+ .map((r) => ` ${resourceMap[r].resourceClass},`)
83
+ .join('\n');
84
+ const resourcesContent = `${resourceImports}
85
+
86
+ export default {
87
+ ${resourceExportsObject}
88
+ };
89
+ `;
90
+ writeFileSync(resourcesIndexPath, resourcesContent);
82
91
 
83
- // Update simulations/index.ts
92
+ // Update simulations/index.ts - uses auto-discovery for JSON simulation files
84
93
  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
94
  const simulationsContent = `/**
90
95
  * Server-safe simulation configurations
91
96
  *
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.
97
+ * Auto-discovers all *-simulation.json files in this directory.
98
+ * File naming: {resource}-{tool}-simulation.json (e.g., albums-show-simulation.json)
99
+ *
100
+ * This file can be safely imported in Node.js contexts (like MCP servers)
101
+ * without causing issues with CSS imports or React components.
94
102
  */
95
103
 
96
- ${simulationImports}
97
-
98
- export const SIMULATIONS = {
99
- ${simulationExports}
100
- } as const;
104
+ // Auto-discover all simulation JSON files
105
+ const simulationModules = import.meta.glob('./*-simulation.json', { eager: true });
106
+
107
+ // Build SIMULATIONS object from discovered files
108
+ // Key is the full name without -simulation.json suffix (e.g., 'albums-show')
109
+ export const SIMULATIONS = Object.fromEntries(
110
+ Object.entries(simulationModules).map(([path, module]) => {
111
+ // Extract simulation key from path: './albums-show-simulation.json' -> 'albums-show'
112
+ const match = path.match(/\\.\\/(.+)-simulation\\.json$/);
113
+ const key = match?.[1] ?? path;
114
+ return [key, (module as { default: unknown }).default];
115
+ })
116
+ ) as Record<string, unknown>;
101
117
  `;
102
118
  writeFileSync(simulationsIndexPath, simulationsContent);
103
119
  }
@@ -163,12 +179,17 @@ async function init(projectName, resourcesArg) {
163
179
  const excludedResources = VALID_RESOURCES.filter((r) => !selectedResources.includes(r));
164
180
 
165
181
  for (const resource of excludedResources) {
166
- // Skip resource files
167
- if (name === `${resource}-resource.tsx` || name === `${resource}-resource.test.tsx`) {
182
+ // Skip resource files (tsx, test, and json metadata)
183
+ if (
184
+ name === `${resource}-resource.tsx` ||
185
+ name === `${resource}-resource.test.tsx` ||
186
+ name === `${resource}-resource.json`
187
+ ) {
168
188
  return false;
169
189
  }
170
- // Skip simulation files
171
- if (name === `${resource}-simulation.ts`) {
190
+ // Skip simulation JSON files that start with the resource name
191
+ // e.g., albums-show-simulation.json, albums-edit-simulation.json
192
+ if (name.startsWith(`${resource}-`) && name.endsWith('-simulation.json')) {
172
193
  return false;
173
194
  }
174
195
  // Skip component directories (map resource name to component dir name)
@@ -217,13 +238,13 @@ Done! To get started:
217
238
 
218
239
  cd ${projectName}
219
240
  pnpm install
220
- pnpm dev
241
+ sunpeak dev
221
242
 
222
243
  That's it! Your project commands:
223
244
 
224
- pnpm dev # Start development server
225
- pnpm build # Build for production
226
- pnpm mcp # Start MCP server
245
+ sunpeak dev # Start development server
246
+ sunpeak build # Build for production
247
+ sunpeak mcp # Start MCP server
227
248
  pnpm test # Run tests
228
249
 
229
250
  See README.md for more details.
@@ -291,6 +312,7 @@ function parseResourceArgs(args) {
291
312
  'push',
292
313
  'pull',
293
314
  'deploy',
315
+ 'upgrade',
294
316
  'help',
295
317
  undefined,
296
318
  ];
@@ -360,6 +382,17 @@ function parseResourceArgs(args) {
360
382
  }
361
383
  break;
362
384
 
385
+ case 'upgrade':
386
+ {
387
+ const { upgrade } = await import(join(COMMANDS_DIR, 'upgrade.mjs'));
388
+ const options = {
389
+ check: args.includes('--check') || args.includes('-c'),
390
+ help: args.includes('--help') || args.includes('-h'),
391
+ };
392
+ await upgrade(options);
393
+ }
394
+ break;
395
+
363
396
  case 'help':
364
397
  case undefined:
365
398
  console.log(`
@@ -373,9 +406,6 @@ Usage:
373
406
  Example: npx sunpeak new my-app "albums,carousel"
374
407
 
375
408
  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
409
  pnpm test Run tests
380
410
 
381
411
  Direct CLI commands (when sunpeak is installed):
@@ -388,6 +418,7 @@ Direct CLI commands (when sunpeak is installed):
388
418
  sunpeak push Push resources to repository
389
419
  sunpeak pull Pull resources from repository
390
420
  sunpeak deploy Push resources with "prod" tag
421
+ sunpeak upgrade Upgrade sunpeak to latest version
391
422
  sunpeak --version Show version number
392
423
 
393
424
  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
  }