sunpeak 0.6.7 → 0.7.10
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 +125 -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 +352 -0
- package/bin/sunpeak.js +101 -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,352 @@
|
|
|
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 a directory
|
|
49
|
+
* Returns array of { name, jsPath, metaPath, meta }
|
|
50
|
+
*/
|
|
51
|
+
export 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'));
|
|
58
|
+
const jsonFiles = new Set(files.filter((f) => f.endsWith('.json')));
|
|
59
|
+
|
|
60
|
+
// Only include .js files that have a matching .json file
|
|
61
|
+
return jsFiles
|
|
62
|
+
.filter((jsFile) => {
|
|
63
|
+
const name = jsFile.replace('.js', '');
|
|
64
|
+
return jsonFiles.has(`${name}.json`);
|
|
65
|
+
})
|
|
66
|
+
.map((jsFile) => {
|
|
67
|
+
const name = jsFile.replace('.js', '');
|
|
68
|
+
const jsPath = join(distDir, jsFile);
|
|
69
|
+
const metaPath = join(distDir, `${name}.json`);
|
|
70
|
+
|
|
71
|
+
let meta = null;
|
|
72
|
+
try {
|
|
73
|
+
meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
74
|
+
} catch {
|
|
75
|
+
console.warn(`Warning: Could not parse ${name}.json`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { name, jsPath, metaPath, meta };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build a resource from a specific JS file path
|
|
84
|
+
* Returns { name, jsPath, metaPath, meta }
|
|
85
|
+
*/
|
|
86
|
+
function buildResourceFromFile(jsPath) {
|
|
87
|
+
if (!existsSync(jsPath)) {
|
|
88
|
+
console.error(`Error: File not found: ${jsPath}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract name from filename (remove .js extension)
|
|
93
|
+
const fileName = basename(jsPath);
|
|
94
|
+
const name = fileName.replace('.js', '');
|
|
95
|
+
|
|
96
|
+
// Look for .json in the same directory
|
|
97
|
+
const dir = dirname(jsPath);
|
|
98
|
+
const metaPath = join(dir, `${name}.json`);
|
|
99
|
+
|
|
100
|
+
let meta = null;
|
|
101
|
+
if (existsSync(metaPath)) {
|
|
102
|
+
try {
|
|
103
|
+
meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
104
|
+
} catch {
|
|
105
|
+
console.warn(`Warning: Could not parse ${name}.json`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { name, jsPath, metaPath, meta };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Push a single resource to the API
|
|
114
|
+
*/
|
|
115
|
+
async function pushResource(resource, repository, tags, accessToken) {
|
|
116
|
+
if (!resource.meta?.uri) {
|
|
117
|
+
throw new Error('Resource is missing URI. Run "sunpeak build" to generate URIs.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const jsContent = readFileSync(resource.jsPath);
|
|
121
|
+
const jsBlob = new Blob([jsContent], { type: 'application/javascript' });
|
|
122
|
+
|
|
123
|
+
// Build form data
|
|
124
|
+
const formData = new FormData();
|
|
125
|
+
formData.append('repository', repository);
|
|
126
|
+
formData.append('js_file', jsBlob, `${resource.name}.js`);
|
|
127
|
+
|
|
128
|
+
// Add metadata fields
|
|
129
|
+
if (resource.meta) {
|
|
130
|
+
formData.append('name', resource.meta.name || resource.name);
|
|
131
|
+
formData.append('title', resource.meta.title || resource.name);
|
|
132
|
+
if (resource.meta.description) {
|
|
133
|
+
formData.append('description', resource.meta.description);
|
|
134
|
+
}
|
|
135
|
+
formData.append('mime_type', resource.meta.mimeType || 'text/html+skybridge');
|
|
136
|
+
formData.append('uri', resource.meta.uri);
|
|
137
|
+
|
|
138
|
+
// Handle OpenAI widget metadata
|
|
139
|
+
if (resource.meta._meta) {
|
|
140
|
+
if (resource.meta._meta['openai/widgetDomain']) {
|
|
141
|
+
formData.append('widget_domain', resource.meta._meta['openai/widgetDomain']);
|
|
142
|
+
}
|
|
143
|
+
if (resource.meta._meta['openai/widgetCSP']) {
|
|
144
|
+
const csp = resource.meta._meta['openai/widgetCSP'];
|
|
145
|
+
if (csp.connect_domains) {
|
|
146
|
+
csp.connect_domains.forEach((domain) => {
|
|
147
|
+
formData.append('widget_csp_connect_domains[]', domain);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (csp.resource_domains) {
|
|
151
|
+
csp.resource_domains.forEach((domain) => {
|
|
152
|
+
formData.append('widget_csp_resource_domains[]', domain);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
// Fallback metadata
|
|
159
|
+
formData.append('name', resource.name);
|
|
160
|
+
formData.append('title', resource.name);
|
|
161
|
+
formData.append('mime_type', 'text/html+skybridge');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add tags if provided
|
|
165
|
+
if (tags && tags.length > 0) {
|
|
166
|
+
tags.forEach((tag) => {
|
|
167
|
+
formData.append('tags[]', tag);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const response = await fetch(`${SUNPEAK_API_URL}/api/v1/resources`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${accessToken}`,
|
|
175
|
+
},
|
|
176
|
+
body: formData,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const data = await response.json().catch(() => ({}));
|
|
181
|
+
let errorMessage = data.message || data.error || `HTTP ${response.status}`;
|
|
182
|
+
if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
|
|
183
|
+
errorMessage += ': ' + data.errors.join(', ');
|
|
184
|
+
}
|
|
185
|
+
throw new Error(errorMessage);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return response.json();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Main push command
|
|
193
|
+
* @param {string} projectRoot - Project root directory
|
|
194
|
+
* @param {Object} options - Command options
|
|
195
|
+
* @param {string} options.repository - Repository name (optional, defaults to git repo name)
|
|
196
|
+
* @param {string} options.file - Path to a specific resource JS file (optional)
|
|
197
|
+
* @param {string[]} options.tags - Tags to assign to the pushed resources (optional)
|
|
198
|
+
*/
|
|
199
|
+
export async function push(projectRoot = process.cwd(), options = {}) {
|
|
200
|
+
// Handle help flag
|
|
201
|
+
if (options.help) {
|
|
202
|
+
console.log(`
|
|
203
|
+
sunpeak push - Push resources to the Sunpeak repository
|
|
204
|
+
|
|
205
|
+
Usage:
|
|
206
|
+
sunpeak push [file] [options]
|
|
207
|
+
|
|
208
|
+
Options:
|
|
209
|
+
-r, --repository <owner/repo> Repository name (defaults to git remote origin)
|
|
210
|
+
-t, --tag <name> Tag to assign (can be specified multiple times)
|
|
211
|
+
-h, --help Show this help message
|
|
212
|
+
|
|
213
|
+
Arguments:
|
|
214
|
+
file Optional JS file to push (e.g., dist/carousel.js)
|
|
215
|
+
If not provided, pushes all resources from dist/
|
|
216
|
+
|
|
217
|
+
Examples:
|
|
218
|
+
sunpeak push Push all resources from dist/
|
|
219
|
+
sunpeak push dist/carousel.js Push a single resource
|
|
220
|
+
sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
|
|
221
|
+
sunpeak push -t v1.0.0 Push with a version tag
|
|
222
|
+
sunpeak push -t v1.0.0 -t latest Push with multiple tags
|
|
223
|
+
`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check credentials
|
|
228
|
+
const credentials = loadCredentials();
|
|
229
|
+
if (!credentials?.access_token) {
|
|
230
|
+
console.error('Error: Not logged in. Run "sunpeak login" first.');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Determine repository name (owner/repo format)
|
|
235
|
+
const repository = options.repository || getGitRepoName();
|
|
236
|
+
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);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Find resources - either a specific file or all from dist directory
|
|
244
|
+
let resources;
|
|
245
|
+
if (options.file) {
|
|
246
|
+
// Push a single specific resource
|
|
247
|
+
resources = [buildResourceFromFile(options.file)];
|
|
248
|
+
} else {
|
|
249
|
+
// Default: find all resources in dist directory
|
|
250
|
+
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);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
resources = findResources(distDir);
|
|
258
|
+
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);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(`Pushing ${resources.length} resource(s) to repository "${repository}"...`);
|
|
266
|
+
if (options.tags && options.tags.length > 0) {
|
|
267
|
+
console.log(`Tags: ${options.tags.join(', ')}`);
|
|
268
|
+
}
|
|
269
|
+
console.log();
|
|
270
|
+
|
|
271
|
+
// Push each resource
|
|
272
|
+
let successCount = 0;
|
|
273
|
+
for (const resource of resources) {
|
|
274
|
+
try {
|
|
275
|
+
const result = await pushResource(resource, repository, options.tags, credentials.access_token);
|
|
276
|
+
console.log(`✓ Pushed ${resource.name} (id: ${result.id})`);
|
|
277
|
+
if (result.tags?.length > 0) {
|
|
278
|
+
console.log(` Tags: ${result.tags.join(', ')}`);
|
|
279
|
+
}
|
|
280
|
+
successCount++;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(`✗ Failed to push ${resource.name}: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log();
|
|
287
|
+
if (successCount === resources.length) {
|
|
288
|
+
console.log(`✓ Successfully pushed ${successCount} resource(s).`);
|
|
289
|
+
} else {
|
|
290
|
+
console.log(`Pushed ${successCount}/${resources.length} resource(s).`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parse command line arguments
|
|
297
|
+
*/
|
|
298
|
+
function parseArgs(args) {
|
|
299
|
+
const options = { tags: [] };
|
|
300
|
+
let i = 0;
|
|
301
|
+
|
|
302
|
+
while (i < args.length) {
|
|
303
|
+
const arg = args[i];
|
|
304
|
+
|
|
305
|
+
if (arg === '--repository' || arg === '-r') {
|
|
306
|
+
options.repository = args[++i];
|
|
307
|
+
} else if (arg === '--tag' || arg === '-t') {
|
|
308
|
+
options.tags.push(args[++i]);
|
|
309
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
310
|
+
console.log(`
|
|
311
|
+
sunpeak push - Push resources to the Sunpeak repository
|
|
312
|
+
|
|
313
|
+
Usage:
|
|
314
|
+
sunpeak push [file] [options]
|
|
315
|
+
|
|
316
|
+
Options:
|
|
317
|
+
-r, --repository <owner/repo> Repository name (defaults to git remote origin)
|
|
318
|
+
-t, --tag <name> Tag to assign (can be specified multiple times)
|
|
319
|
+
-h, --help Show this help message
|
|
320
|
+
|
|
321
|
+
Arguments:
|
|
322
|
+
file Optional JS file to push (e.g., dist/carousel.js)
|
|
323
|
+
If not provided, pushes all resources from dist/
|
|
324
|
+
|
|
325
|
+
Examples:
|
|
326
|
+
sunpeak push Push all resources from dist/
|
|
327
|
+
sunpeak push dist/carousel.js Push a single resource
|
|
328
|
+
sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
|
|
329
|
+
sunpeak push -t v1.0.0 Push with a version tag
|
|
330
|
+
sunpeak push -t v1.0.0 -t latest Push with multiple tags
|
|
331
|
+
`);
|
|
332
|
+
process.exit(0);
|
|
333
|
+
} else if (!arg.startsWith('-')) {
|
|
334
|
+
// Positional argument - treat as file path
|
|
335
|
+
options.file = arg;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
i++;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return options;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Allow running directly
|
|
345
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
346
|
+
const args = process.argv.slice(2);
|
|
347
|
+
const options = parseArgs(args);
|
|
348
|
+
push(process.cwd(), options).catch((error) => {
|
|
349
|
+
console.error('Error:', error.message);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
});
|
|
352
|
+
}
|