sunpeak 0.6.7 → 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.
- package/README.md +3 -3
- package/bin/commands/build.mjs +22 -5
- package/bin/commands/deploy.mjs +108 -0
- package/bin/commands/login.mjs +217 -0
- package/bin/commands/logout.mjs +87 -0
- package/bin/commands/pull.mjs +254 -0
- package/bin/commands/push.mjs +347 -0
- package/bin/sunpeak.js +85 -2
- package/dist/mcp/entry.cjs +2 -2
- package/dist/mcp/entry.cjs.map +1 -1
- package/dist/mcp/entry.js +2 -2
- package/dist/mcp/entry.js.map +1 -1
- package/dist/mcp/index.cjs +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/{server-CQGbJWbk.cjs → server-BOYwNazb.cjs} +25 -26
- package/dist/{server-CQGbJWbk.cjs.map → server-BOYwNazb.cjs.map} +1 -1
- package/dist/{server-DGCvp1RA.js → server-C6vMGV6H.js} +25 -26
- package/dist/{server-DGCvp1RA.js.map → server-C6vMGV6H.js.map} +1 -1
- package/package.json +1 -1
- package/template/.sunpeak/dev.tsx +8 -10
- package/template/README.md +4 -4
- package/template/dist/albums.json +15 -0
- package/template/dist/carousel.json +15 -0
- package/template/dist/counter.json +10 -0
- package/template/dist/map.json +19 -0
- package/template/index.html +1 -1
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Button.js +3 -3
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_SegmentedControl.js +1 -1
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +16 -16
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Textarea.js +3 -3
- package/template/node_modules/.vite/deps/_metadata.json +32 -32
- package/template/node_modules/.vite/deps/{chunk-DQAZDQU3.js → chunk-LR7NKCX5.js} +8 -8
- package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/template/src/resources/albums-resource.json +12 -0
- package/template/src/resources/carousel-resource.json +12 -0
- package/template/src/resources/counter-resource.json +9 -0
- package/template/src/resources/map-resource.json +13 -0
- package/template/src/simulations/albums-simulation.ts +3 -15
- package/template/src/simulations/carousel-simulation.ts +3 -15
- package/template/src/simulations/counter-simulation.ts +3 -15
- package/template/src/simulations/map-simulation.ts +5 -28
- package/template/src/simulations/widget-config.ts +0 -42
- /package/template/dist/{chatgpt/albums.js → albums.js} +0 -0
- /package/template/dist/{chatgpt/carousel.js → carousel.js} +0 -0
- /package/template/dist/{chatgpt/counter.js → counter.js} +0 -0
- /package/template/dist/{chatgpt/map.js → map.js} +0 -0
- /package/template/node_modules/.vite/deps/{chunk-DQAZDQU3.js.map → chunk-LR7NKCX5.js.map} +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 = [
|
|
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
|
|
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
|
`);
|