sunpeak 0.6.6 → 0.7.9

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 (46) hide show
  1. package/README.md +3 -3
  2. package/bin/commands/build.mjs +22 -5
  3. package/bin/commands/deploy.mjs +108 -0
  4. package/bin/commands/login.mjs +217 -0
  5. package/bin/commands/logout.mjs +87 -0
  6. package/bin/commands/pull.mjs +254 -0
  7. package/bin/commands/push.mjs +347 -0
  8. package/bin/sunpeak.js +85 -2
  9. package/dist/mcp/entry.cjs +2 -2
  10. package/dist/mcp/entry.cjs.map +1 -1
  11. package/dist/mcp/entry.js +2 -2
  12. package/dist/mcp/entry.js.map +1 -1
  13. package/dist/mcp/index.cjs +1 -1
  14. package/dist/mcp/index.js +1 -1
  15. package/dist/{server-CQGbJWbk.cjs → server-BOYwNazb.cjs} +25 -26
  16. package/dist/{server-CQGbJWbk.cjs.map → server-BOYwNazb.cjs.map} +1 -1
  17. package/dist/{server-DGCvp1RA.js → server-C6vMGV6H.js} +25 -26
  18. package/dist/{server-DGCvp1RA.js.map → server-C6vMGV6H.js.map} +1 -1
  19. package/package.json +1 -1
  20. package/template/.sunpeak/dev.tsx +8 -10
  21. package/template/README.md +4 -4
  22. package/template/dist/albums.json +15 -0
  23. package/template/dist/carousel.json +15 -0
  24. package/template/dist/counter.json +10 -0
  25. package/template/dist/map.json +19 -0
  26. package/template/index.html +1 -1
  27. package/template/node_modules/.vite/deps/_metadata.json +19 -19
  28. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  29. package/template/src/components/map/map-view.test.tsx +146 -0
  30. package/template/src/components/map/place-card.test.tsx +76 -0
  31. package/template/src/components/map/place-carousel.test.tsx +84 -0
  32. package/template/src/components/map/place-inspector.test.tsx +91 -0
  33. package/template/src/components/map/place-list.test.tsx +97 -0
  34. package/template/src/resources/albums-resource.json +12 -0
  35. package/template/src/resources/carousel-resource.json +12 -0
  36. package/template/src/resources/counter-resource.json +9 -0
  37. package/template/src/resources/map-resource.json +13 -0
  38. package/template/src/simulations/albums-simulation.ts +3 -15
  39. package/template/src/simulations/carousel-simulation.ts +3 -15
  40. package/template/src/simulations/counter-simulation.ts +3 -15
  41. package/template/src/simulations/map-simulation.ts +5 -28
  42. package/template/src/simulations/widget-config.ts +0 -42
  43. /package/template/dist/{chatgpt/albums.js → albums.js} +0 -0
  44. /package/template/dist/{chatgpt/carousel.js → carousel.js} +0 -0
  45. /package/template/dist/{chatgpt/counter.js → counter.js} +0 -0
  46. /package/template/dist/{chatgpt/map.js → map.js} +0 -0
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const SUNPEAK_API_URL = process.env.SUNPEAK_API_URL || 'https://app.sunpeak.ai';
7
+ const CREDENTIALS_DIR = join(homedir(), '.sunpeak');
8
+ const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');
9
+
10
+ /**
11
+ * Load credentials from disk
12
+ */
13
+ function loadCredentials() {
14
+ if (!existsSync(CREDENTIALS_FILE)) {
15
+ return null;
16
+ }
17
+ try {
18
+ return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Lookup resources by tag and repository
26
+ * @returns {Promise<Array>} Array of matching resources
27
+ */
28
+ async function lookupResources(tag, repository, accessToken, name = null) {
29
+ const params = new URLSearchParams({ tag, repository });
30
+ if (name) {
31
+ params.set('name', name);
32
+ }
33
+ const response = await fetch(`${SUNPEAK_API_URL}/api/v1/resources/lookup?${params}`, {
34
+ headers: {
35
+ Authorization: `Bearer ${accessToken}`,
36
+ },
37
+ });
38
+
39
+ if (!response.ok) {
40
+ const data = await response.json().catch(() => ({}));
41
+ throw new Error(data.message || data.error || `HTTP ${response.status}`);
42
+ }
43
+
44
+ const data = await response.json();
45
+ return data.resources;
46
+ }
47
+
48
+ /**
49
+ * Download the JS file for a resource
50
+ */
51
+ async function downloadJsFile(resource) {
52
+ if (!resource.js_file?.url) {
53
+ throw new Error('Resource has no JS file attached');
54
+ }
55
+
56
+ // The URL is a pre-signed S3 URL, no additional auth needed
57
+ const response = await fetch(resource.js_file.url);
58
+
59
+ if (!response.ok) {
60
+ throw new Error(`Failed to download JS file: HTTP ${response.status}`);
61
+ }
62
+
63
+ return response.text();
64
+ }
65
+
66
+ /**
67
+ * Main pull command
68
+ * @param {string} projectRoot - Project root directory
69
+ * @param {Object} options - Command options
70
+ * @param {string} options.repository - Repository name in owner/repo format (required)
71
+ * @param {string} options.tag - Tag name to pull (required)
72
+ * @param {string} options.name - Resource name to filter by (optional)
73
+ * @param {string} options.output - Output directory (optional, defaults to current directory)
74
+ */
75
+ export async function pull(projectRoot = process.cwd(), options = {}) {
76
+ // Handle help flag
77
+ if (options.help) {
78
+ console.log(`
79
+ sunpeak pull - Pull resources from the Sunpeak repository
80
+
81
+ Usage:
82
+ sunpeak pull -r <owner/repo> -t <tag> [options]
83
+
84
+ Options:
85
+ -r, --repository <owner/repo> Repository name (required)
86
+ -t, --tag <name> Tag name to pull (required)
87
+ -n, --name <name> Resource name to filter by (optional)
88
+ -o, --output <path> Output directory (defaults to current directory)
89
+ -h, --help Show this help message
90
+
91
+ Examples:
92
+ sunpeak pull -r myorg/my-app -t prod Pull all resources tagged "prod"
93
+ sunpeak pull -r myorg/my-app -t prod -n counter Pull only the "counter" resource
94
+ sunpeak pull -r myorg/my-app -t v1.0.0 Pull a specific version
95
+ `);
96
+ return;
97
+ }
98
+
99
+ // Check credentials
100
+ const credentials = loadCredentials();
101
+ if (!credentials?.access_token) {
102
+ console.error('Error: Not logged in. Run "sunpeak login" first.');
103
+ process.exit(1);
104
+ }
105
+
106
+ // Require repository
107
+ if (!options.repository) {
108
+ console.error('Error: Repository is required. Use --repository or -r to specify a repository.');
109
+ console.error('Example: sunpeak pull -r myorg/my-app -t prod');
110
+ process.exit(1);
111
+ }
112
+
113
+ // Require tag
114
+ if (!options.tag) {
115
+ console.error('Error: Tag is required. Use --tag or -t to specify a tag.');
116
+ console.error('Example: sunpeak pull -r myorg/my-app -t prod');
117
+ process.exit(1);
118
+ }
119
+
120
+ const repository = options.repository;
121
+
122
+ const nameFilter = options.name ? ` with name "${options.name}"` : '';
123
+ console.log(`Pulling resources from repository "${repository}" with tag "${options.tag}"${nameFilter}...`);
124
+ console.log();
125
+
126
+ try {
127
+ // Lookup resources
128
+ const resources = await lookupResources(options.tag, repository, credentials.access_token, options.name);
129
+
130
+ if (!resources || resources.length === 0) {
131
+ console.error('Error: No resources found matching the criteria.');
132
+ process.exit(1);
133
+ }
134
+
135
+ console.log(`Found ${resources.length} resource(s):\n`);
136
+
137
+ // Determine output directory
138
+ const outputDir = options.output || projectRoot;
139
+
140
+ // Create output directory if it doesn't exist
141
+ if (!existsSync(outputDir)) {
142
+ mkdirSync(outputDir, { recursive: true });
143
+ }
144
+
145
+ // Process each resource
146
+ for (const resource of resources) {
147
+ console.log(`Resource: ${resource.name}`);
148
+ console.log(` Title: ${resource.title}`);
149
+ console.log(` URI: ${resource.uri}`);
150
+ console.log(` Tags: ${resource.tags?.join(', ') || 'none'}`);
151
+ console.log(` Created: ${resource.created_at}`);
152
+
153
+ if (!resource.js_file) {
154
+ console.log(` ⚠ Skipping: No JS file attached.\n`);
155
+ continue;
156
+ }
157
+
158
+ // Download the JS file
159
+ console.log(` Downloading JS file...`);
160
+ const jsContent = await downloadJsFile(resource);
161
+
162
+ const outputFile = join(outputDir, `${resource.name}.js`);
163
+ const metaFile = join(outputDir, `${resource.name}.json`);
164
+
165
+ // Write the JS file
166
+ writeFileSync(outputFile, jsContent);
167
+ console.log(` ✓ Saved ${resource.name}.js`);
168
+
169
+ // Write metadata JSON
170
+ const meta = {
171
+ uri: resource.uri,
172
+ name: resource.name,
173
+ title: resource.title,
174
+ description: resource.description,
175
+ mimeType: resource.mime_type,
176
+ _meta: {
177
+ 'openai/widgetDomain': resource.widget_domain,
178
+ 'openai/widgetCSP': {
179
+ connect_domains: resource.widget_csp_connect_domains || [],
180
+ resource_domains: resource.widget_csp_resource_domains || [],
181
+ },
182
+ },
183
+ };
184
+ writeFileSync(metaFile, JSON.stringify(meta, null, 2));
185
+ console.log(` ✓ Saved ${resource.name}.json\n`);
186
+ }
187
+
188
+ console.log(`✓ Successfully pulled ${resources.length} resource(s) to ${outputDir}`);
189
+ } catch (error) {
190
+ console.error(`Error: ${error.message}`);
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Parse command line arguments
197
+ */
198
+ function parseArgs(args) {
199
+ const options = {};
200
+ let i = 0;
201
+
202
+ while (i < args.length) {
203
+ const arg = args[i];
204
+
205
+ if (arg === '--repository' || arg === '-r') {
206
+ options.repository = args[++i];
207
+ } else if (arg === '--tag' || arg === '-t') {
208
+ options.tag = args[++i];
209
+ } else if (arg === '--name' || arg === '-n') {
210
+ options.name = args[++i];
211
+ } else if (arg === '--output' || arg === '-o') {
212
+ options.output = args[++i];
213
+ } else if (arg === '--help' || arg === '-h') {
214
+ console.log(`
215
+ sunpeak pull - Pull resources from the Sunpeak repository
216
+
217
+ Usage:
218
+ sunpeak pull -r <owner/repo> -t <tag> [options]
219
+
220
+ Options:
221
+ -r, --repository <owner/repo> Repository name (required)
222
+ -t, --tag <name> Tag name to pull (required)
223
+ -n, --name <name> Resource name to filter by (optional)
224
+ -o, --output <path> Output directory (defaults to current directory)
225
+ -h, --help Show this help message
226
+
227
+ Examples:
228
+ sunpeak pull -r myorg/my-app -t prod Pull all resources tagged "prod"
229
+ sunpeak pull -r myorg/my-app -t prod -n counter Pull only the "counter" resource
230
+ sunpeak pull -r myorg/my-app -t v1.0.0 Pull a specific version
231
+ `);
232
+ process.exit(0);
233
+ } else if (!arg.startsWith('-')) {
234
+ // Positional argument - treat as tag
235
+ if (!options.tag) {
236
+ options.tag = arg;
237
+ }
238
+ }
239
+
240
+ i++;
241
+ }
242
+
243
+ return options;
244
+ }
245
+
246
+ // Allow running directly
247
+ if (import.meta.url === `file://${process.argv[1]}`) {
248
+ const args = process.argv.slice(2);
249
+ const options = parseArgs(args);
250
+ pull(process.cwd(), options).catch((error) => {
251
+ console.error('Error:', error.message);
252
+ process.exit(1);
253
+ });
254
+ }
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, readdirSync } from 'fs';
3
+ import { join, dirname, basename } from 'path';
4
+ import { homedir } from 'os';
5
+ import { execSync } from 'child_process';
6
+
7
+ const SUNPEAK_API_URL = process.env.SUNPEAK_API_URL || 'https://app.sunpeak.ai';
8
+ const CREDENTIALS_DIR = join(homedir(), '.sunpeak');
9
+ const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');
10
+
11
+ /**
12
+ * Load credentials from disk
13
+ */
14
+ function loadCredentials() {
15
+ if (!existsSync(CREDENTIALS_FILE)) {
16
+ return null;
17
+ }
18
+ try {
19
+ return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Get the current git repository name in owner/repo format
27
+ */
28
+ function getGitRepoName() {
29
+ try {
30
+ // Try to get the remote URL first
31
+ const remoteUrl = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf-8' }).trim();
32
+ if (remoteUrl) {
33
+ // Extract owner/repo from URL
34
+ // Handles: https://github.com/owner/repo.git, git@github.com:owner/repo.git
35
+ const match = remoteUrl.match(/[/:]([^/:]+\/[^/]+?)(?:\.git)?$/);
36
+ if (match) {
37
+ return match[1];
38
+ }
39
+ }
40
+ } catch {
41
+ // No remote
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Find all resources in the dist folder
49
+ * Returns array of { name, jsPath, metaPath, meta }
50
+ */
51
+ function findResources(distDir) {
52
+ if (!existsSync(distDir)) {
53
+ return [];
54
+ }
55
+
56
+ const files = readdirSync(distDir);
57
+ const jsFiles = files.filter((f) => f.endsWith('.js') && !f.endsWith('.meta.js'));
58
+
59
+ return jsFiles.map((jsFile) => {
60
+ const name = jsFile.replace('.js', '');
61
+ const jsPath = join(distDir, jsFile);
62
+ const metaPath = join(distDir, `${name}.json`);
63
+
64
+ let meta = null;
65
+ if (existsSync(metaPath)) {
66
+ try {
67
+ meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
68
+ } catch {
69
+ console.warn(`Warning: Could not parse ${name}.json`);
70
+ }
71
+ }
72
+
73
+ return { name, jsPath, metaPath, meta };
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Build a resource from a specific JS file path
79
+ * Returns { name, jsPath, metaPath, meta }
80
+ */
81
+ function buildResourceFromFile(jsPath) {
82
+ if (!existsSync(jsPath)) {
83
+ console.error(`Error: File not found: ${jsPath}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ // Extract name from filename (remove .js extension)
88
+ const fileName = basename(jsPath);
89
+ const name = fileName.replace('.js', '');
90
+
91
+ // Look for .json in the same directory
92
+ const dir = dirname(jsPath);
93
+ const metaPath = join(dir, `${name}.json`);
94
+
95
+ let meta = null;
96
+ if (existsSync(metaPath)) {
97
+ try {
98
+ meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
99
+ } catch {
100
+ console.warn(`Warning: Could not parse ${name}.json`);
101
+ }
102
+ }
103
+
104
+ return { name, jsPath, metaPath, meta };
105
+ }
106
+
107
+ /**
108
+ * Push a single resource to the API
109
+ */
110
+ async function pushResource(resource, repository, tags, accessToken) {
111
+ if (!resource.meta?.uri) {
112
+ throw new Error('Resource is missing URI. Run "sunpeak build" to generate URIs.');
113
+ }
114
+
115
+ const jsContent = readFileSync(resource.jsPath);
116
+ const jsBlob = new Blob([jsContent], { type: 'application/javascript' });
117
+
118
+ // Build form data
119
+ const formData = new FormData();
120
+ formData.append('repository', repository);
121
+ formData.append('js_file', jsBlob, `${resource.name}.js`);
122
+
123
+ // Add metadata fields
124
+ if (resource.meta) {
125
+ formData.append('name', resource.meta.name || resource.name);
126
+ formData.append('title', resource.meta.title || resource.name);
127
+ if (resource.meta.description) {
128
+ formData.append('description', resource.meta.description);
129
+ }
130
+ formData.append('mime_type', resource.meta.mimeType || 'text/html+skybridge');
131
+ formData.append('uri', resource.meta.uri);
132
+
133
+ // Handle OpenAI widget metadata
134
+ if (resource.meta._meta) {
135
+ if (resource.meta._meta['openai/widgetDomain']) {
136
+ formData.append('widget_domain', resource.meta._meta['openai/widgetDomain']);
137
+ }
138
+ if (resource.meta._meta['openai/widgetCSP']) {
139
+ const csp = resource.meta._meta['openai/widgetCSP'];
140
+ if (csp.connect_domains) {
141
+ csp.connect_domains.forEach((domain) => {
142
+ formData.append('widget_csp_connect_domains[]', domain);
143
+ });
144
+ }
145
+ if (csp.resource_domains) {
146
+ csp.resource_domains.forEach((domain) => {
147
+ formData.append('widget_csp_resource_domains[]', domain);
148
+ });
149
+ }
150
+ }
151
+ }
152
+ } else {
153
+ // Fallback metadata
154
+ formData.append('name', resource.name);
155
+ formData.append('title', resource.name);
156
+ formData.append('mime_type', 'text/html+skybridge');
157
+ }
158
+
159
+ // Add tags if provided
160
+ if (tags && tags.length > 0) {
161
+ tags.forEach((tag) => {
162
+ formData.append('tags[]', tag);
163
+ });
164
+ }
165
+
166
+ const response = await fetch(`${SUNPEAK_API_URL}/api/v1/resources`, {
167
+ method: 'POST',
168
+ headers: {
169
+ Authorization: `Bearer ${accessToken}`,
170
+ },
171
+ body: formData,
172
+ });
173
+
174
+ if (!response.ok) {
175
+ const data = await response.json().catch(() => ({}));
176
+ let errorMessage = data.message || data.error || `HTTP ${response.status}`;
177
+ if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
178
+ errorMessage += ': ' + data.errors.join(', ');
179
+ }
180
+ throw new Error(errorMessage);
181
+ }
182
+
183
+ return response.json();
184
+ }
185
+
186
+ /**
187
+ * Main push command
188
+ * @param {string} projectRoot - Project root directory
189
+ * @param {Object} options - Command options
190
+ * @param {string} options.repository - Repository name (optional, defaults to git repo name)
191
+ * @param {string} options.file - Path to a specific resource JS file (optional)
192
+ * @param {string[]} options.tags - Tags to assign to the pushed resources (optional)
193
+ */
194
+ export async function push(projectRoot = process.cwd(), options = {}) {
195
+ // Handle help flag
196
+ if (options.help) {
197
+ console.log(`
198
+ sunpeak push - Push resources to the Sunpeak repository
199
+
200
+ Usage:
201
+ sunpeak push [file] [options]
202
+
203
+ Options:
204
+ -r, --repository <owner/repo> Repository name (defaults to git remote origin)
205
+ -t, --tag <name> Tag to assign (can be specified multiple times)
206
+ -h, --help Show this help message
207
+
208
+ Arguments:
209
+ file Optional JS file to push (e.g., dist/carousel.js)
210
+ If not provided, pushes all resources from dist/
211
+
212
+ Examples:
213
+ sunpeak push Push all resources from dist/
214
+ sunpeak push dist/carousel.js Push a single resource
215
+ sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
216
+ sunpeak push -t v1.0.0 Push with a version tag
217
+ sunpeak push -t v1.0.0 -t latest Push with multiple tags
218
+ `);
219
+ return;
220
+ }
221
+
222
+ // Check credentials
223
+ const credentials = loadCredentials();
224
+ if (!credentials?.access_token) {
225
+ console.error('Error: Not logged in. Run "sunpeak login" first.');
226
+ process.exit(1);
227
+ }
228
+
229
+ // Determine repository name (owner/repo format)
230
+ const repository = options.repository || getGitRepoName();
231
+ if (!repository) {
232
+ console.error('Error: Could not determine repository name.');
233
+ console.error('Please provide a repository name: sunpeak push --repository <owner/repo>');
234
+ console.error('Or run this command from within a git repository with a remote origin.');
235
+ process.exit(1);
236
+ }
237
+
238
+ // Find resources - either a specific file or all from dist directory
239
+ let resources;
240
+ if (options.file) {
241
+ // Push a single specific resource
242
+ resources = [buildResourceFromFile(options.file)];
243
+ } else {
244
+ // Default: find all resources in dist directory
245
+ const distDir = join(projectRoot, 'dist');
246
+ if (!existsSync(distDir)) {
247
+ console.error(`Error: dist/ directory not found`);
248
+ console.error('Run "sunpeak build" first to build your resources.');
249
+ process.exit(1);
250
+ }
251
+
252
+ resources = findResources(distDir);
253
+ if (resources.length === 0) {
254
+ console.error(`Error: No resources found in dist/`);
255
+ console.error('Run "sunpeak build" first to build your resources.');
256
+ process.exit(1);
257
+ }
258
+ }
259
+
260
+ console.log(`Pushing ${resources.length} resource(s) to repository "${repository}"...`);
261
+ if (options.tags && options.tags.length > 0) {
262
+ console.log(`Tags: ${options.tags.join(', ')}`);
263
+ }
264
+ console.log();
265
+
266
+ // Push each resource
267
+ let successCount = 0;
268
+ for (const resource of resources) {
269
+ try {
270
+ const result = await pushResource(resource, repository, options.tags, credentials.access_token);
271
+ console.log(`✓ Pushed ${resource.name} (id: ${result.id})`);
272
+ if (result.tags?.length > 0) {
273
+ console.log(` Tags: ${result.tags.join(', ')}`);
274
+ }
275
+ successCount++;
276
+ } catch (error) {
277
+ console.error(`✗ Failed to push ${resource.name}: ${error.message}`);
278
+ }
279
+ }
280
+
281
+ console.log();
282
+ if (successCount === resources.length) {
283
+ console.log(`✓ Successfully pushed ${successCount} resource(s).`);
284
+ } else {
285
+ console.log(`Pushed ${successCount}/${resources.length} resource(s).`);
286
+ process.exit(1);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Parse command line arguments
292
+ */
293
+ function parseArgs(args) {
294
+ const options = { tags: [] };
295
+ let i = 0;
296
+
297
+ while (i < args.length) {
298
+ const arg = args[i];
299
+
300
+ if (arg === '--repository' || arg === '-r') {
301
+ options.repository = args[++i];
302
+ } else if (arg === '--tag' || arg === '-t') {
303
+ options.tags.push(args[++i]);
304
+ } else if (arg === '--help' || arg === '-h') {
305
+ console.log(`
306
+ sunpeak push - Push resources to the Sunpeak repository
307
+
308
+ Usage:
309
+ sunpeak push [file] [options]
310
+
311
+ Options:
312
+ -r, --repository <owner/repo> Repository name (defaults to git remote origin)
313
+ -t, --tag <name> Tag to assign (can be specified multiple times)
314
+ -h, --help Show this help message
315
+
316
+ Arguments:
317
+ file Optional JS file to push (e.g., dist/carousel.js)
318
+ If not provided, pushes all resources from dist/
319
+
320
+ Examples:
321
+ sunpeak push Push all resources from dist/
322
+ sunpeak push dist/carousel.js Push a single resource
323
+ sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
324
+ sunpeak push -t v1.0.0 Push with a version tag
325
+ sunpeak push -t v1.0.0 -t latest Push with multiple tags
326
+ `);
327
+ process.exit(0);
328
+ } else if (!arg.startsWith('-')) {
329
+ // Positional argument - treat as file path
330
+ options.file = arg;
331
+ }
332
+
333
+ i++;
334
+ }
335
+
336
+ return options;
337
+ }
338
+
339
+ // Allow running directly
340
+ if (import.meta.url === `file://${process.argv[1]}`) {
341
+ const args = process.argv.slice(2);
342
+ const options = parseArgs(args);
343
+ push(process.cwd(), options).catch((error) => {
344
+ console.error('Error:', error.message);
345
+ process.exit(1);
346
+ });
347
+ }
package/bin/sunpeak.js CHANGED
@@ -232,10 +232,53 @@ See README.md for more details.
232
232
 
233
233
  const [, , command, ...args] = process.argv;
234
234
 
235
+ /**
236
+ * Parse arguments for resource commands (push, pull, deploy)
237
+ */
238
+ function parseResourceArgs(args) {
239
+ const options = { tags: [] };
240
+ let i = 0;
241
+
242
+ while (i < args.length) {
243
+ const arg = args[i];
244
+
245
+ if (arg === '--repository' || arg === '-r') {
246
+ options.repository = args[++i];
247
+ } else if (arg === '--tag' || arg === '-t') {
248
+ options.tags.push(args[++i]);
249
+ } else if (arg === '--name' || arg === '-n') {
250
+ options.name = args[++i];
251
+ } else if (arg === '--output' || arg === '-o') {
252
+ options.output = args[++i];
253
+ } else if (arg === '--help' || arg === '-h') {
254
+ options.help = true;
255
+ } else if (!arg.startsWith('-')) {
256
+ // Positional argument - treat as file path
257
+ options.file = arg;
258
+ }
259
+
260
+ i++;
261
+ }
262
+
263
+ // Set singular tag for commands that expect it (e.g., pull)
264
+ options.tag = options.tags[0];
265
+
266
+ return options;
267
+ }
268
+
235
269
  // Main CLI handler
236
270
  (async () => {
237
271
  // Commands that don't require a package.json
238
- const standaloneCommands = ['new', 'help', undefined];
272
+ const standaloneCommands = [
273
+ 'new',
274
+ 'login',
275
+ 'logout',
276
+ 'push',
277
+ 'pull',
278
+ 'deploy',
279
+ 'help',
280
+ undefined,
281
+ ];
239
282
 
240
283
  if (command && !standaloneCommands.includes(command)) {
241
284
  checkPackageJson();
@@ -267,10 +310,45 @@ const [, , command, ...args] = process.argv;
267
310
  }
268
311
  break;
269
312
 
313
+ case 'login':
314
+ {
315
+ const { login } = await import(join(COMMANDS_DIR, 'login.mjs'));
316
+ await login();
317
+ }
318
+ break;
319
+
320
+ case 'logout':
321
+ {
322
+ const { logout } = await import(join(COMMANDS_DIR, 'logout.mjs'));
323
+ await logout();
324
+ }
325
+ break;
326
+
327
+ case 'push':
328
+ {
329
+ const { push } = await import(join(COMMANDS_DIR, 'push.mjs'));
330
+ await push(process.cwd(), parseResourceArgs(args));
331
+ }
332
+ break;
333
+
334
+ case 'pull':
335
+ {
336
+ const { pull } = await import(join(COMMANDS_DIR, 'pull.mjs'));
337
+ await pull(process.cwd(), parseResourceArgs(args));
338
+ }
339
+ break;
340
+
341
+ case 'deploy':
342
+ {
343
+ const { deploy } = await import(join(COMMANDS_DIR, 'deploy.mjs'));
344
+ await deploy(process.cwd(), parseResourceArgs(args));
345
+ }
346
+ break;
347
+
270
348
  case 'help':
271
349
  case undefined:
272
350
  console.log(`
273
- ☀️ 🏔️ sunpeak - The MCP App framework
351
+ ☀️ 🏔️ sunpeak - The ChatGPT App framework
274
352
 
275
353
  Usage:
276
354
  npx sunpeak new [name] [resources] Create a new project (no install needed)
@@ -290,6 +368,11 @@ Direct CLI commands (when sunpeak is installed):
290
368
  sunpeak dev Start dev server
291
369
  sunpeak build Build resources
292
370
  sunpeak mcp Start MCP server
371
+ sunpeak login Log in to Sunpeak
372
+ sunpeak logout Log out of Sunpeak
373
+ sunpeak push Push resources to repository
374
+ sunpeak pull Pull resources from repository
375
+ sunpeak deploy Push resources with "prod" tag
293
376
 
294
377
  For more information, visit: https://sunpeak.ai/
295
378
  `);