sunpeak 0.13.12 → 0.14.3

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.
@@ -1,465 +0,0 @@
1
- #!/usr/bin/env node
2
- import { existsSync, readFileSync, readdirSync } from 'fs';
3
- import { join, basename } from 'path';
4
- import { homedir } from 'os';
5
- import { execSync } from 'child_process';
6
- import { isSimulationFile, extractSimulationName } from '../lib/patterns.mjs';
7
-
8
- const SUNPEAK_API_URL = process.env.SUNPEAK_API_URL || 'https://app.sunpeak.ai';
9
- const CREDENTIALS_DIR = join(homedir(), '.sunpeak');
10
- const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');
11
-
12
- /**
13
- * Load credentials from disk
14
- */
15
- function loadCredentialsImpl() {
16
- if (!existsSync(CREDENTIALS_FILE)) {
17
- return null;
18
- }
19
- try {
20
- return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'));
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- /**
27
- * Get the current git repository name in owner/repo format
28
- */
29
- function getGitRepoNameImpl() {
30
- try {
31
- // Try to get the remote URL first
32
- const remoteUrl = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf-8' }).trim();
33
- if (remoteUrl) {
34
- // Extract owner/repo from URL
35
- // Handles: https://github.com/owner/repo.git, git@github.com:owner/repo.git
36
- const match = remoteUrl.match(/[/:]([^/:]+\/[^/]+?)(?:\.git)?$/);
37
- if (match) {
38
- return match[1];
39
- }
40
- }
41
- } catch {
42
- // No remote
43
- }
44
-
45
- return null;
46
- }
47
-
48
- /**
49
- * Default dependencies (real implementations)
50
- */
51
- export const defaultDeps = {
52
- fetch: globalThis.fetch,
53
- loadCredentials: loadCredentialsImpl,
54
- getGitRepoName: getGitRepoNameImpl,
55
- existsSync,
56
- readFileSync,
57
- readdirSync,
58
- console,
59
- process,
60
- apiUrl: SUNPEAK_API_URL,
61
- };
62
-
63
- /**
64
- * Find simulation files for a resource in the simulations directory
65
- * Expects files in: tests/simulations/{resourceName}/{resourceName}-*-simulation.json
66
- * Returns array of parsed simulation objects
67
- */
68
- function findSimulations(simulationsDir, resourceName, deps = defaultDeps) {
69
- const d = { ...defaultDeps, ...deps };
70
-
71
- const resourceSimDir = join(simulationsDir, resourceName);
72
- if (!d.existsSync(resourceSimDir)) {
73
- return [];
74
- }
75
-
76
- const entries = d.readdirSync(resourceSimDir);
77
- const simulations = [];
78
-
79
- for (const entry of entries) {
80
- if (isSimulationFile(entry, resourceName)) {
81
- const simPath = join(resourceSimDir, entry);
82
- try {
83
- const simData = JSON.parse(d.readFileSync(simPath, 'utf-8'));
84
- const simName = extractSimulationName(entry, resourceName);
85
- simulations.push({ ...simData, name: simName });
86
- } catch {
87
- d.console.warn(`Warning: Could not parse simulation file ${entry}`);
88
- }
89
- }
90
- }
91
-
92
- return simulations;
93
- }
94
-
95
- /**
96
- * Find all resources in a directory
97
- * Expects folder structure: dist/{resource}/{resource}.html
98
- * Simulations are loaded from tests/simulations/{resource}/
99
- * Returns array of { name, htmlPath, metaPath, meta, simulations }
100
- */
101
- export function findResources(distDir, simulationsDir, deps = defaultDeps) {
102
- const d = { ...defaultDeps, ...deps };
103
-
104
- if (!d.existsSync(distDir)) {
105
- return [];
106
- }
107
-
108
- const entries = d.readdirSync(distDir, { withFileTypes: true });
109
- const resources = [];
110
-
111
- for (const entry of entries) {
112
- if (entry.isDirectory()) {
113
- const resourceName = entry.name;
114
- const resourceDir = join(distDir, resourceName);
115
- const htmlPath = join(resourceDir, `${resourceName}.html`);
116
- const metaPath = join(resourceDir, `${resourceName}.json`);
117
-
118
- if (d.existsSync(htmlPath) && d.existsSync(metaPath)) {
119
- let meta = null;
120
- try {
121
- meta = JSON.parse(d.readFileSync(metaPath, 'utf-8'));
122
- } catch {
123
- d.console.warn(`Warning: Could not parse ${resourceName}.json`);
124
- }
125
- const simulations = findSimulations(simulationsDir, resourceName, d);
126
- resources.push({ name: resourceName, dir: resourceDir, htmlPath, metaPath, meta, simulations });
127
- }
128
- }
129
- }
130
-
131
- return resources;
132
- }
133
-
134
- /**
135
- * Build a resource from a resource directory path
136
- * Expects structure: dir/{name}.html, dir/{name}.json
137
- * Simulations are loaded from tests/simulations/{name}/
138
- * Returns { name, htmlPath, metaPath, meta, simulations }
139
- */
140
- function buildResourceFromDir(resourceDir, simulationsDir, deps = defaultDeps) {
141
- const d = { ...defaultDeps, ...deps };
142
-
143
- // Remove trailing slash if present
144
- const dir = resourceDir.replace(/\/$/, '');
145
-
146
- if (!d.existsSync(dir)) {
147
- d.console.error(`Error: Directory not found: ${dir}`);
148
- d.process.exit(1);
149
- }
150
-
151
- // Extract resource name from directory name
152
- const name = basename(dir);
153
- const htmlPath = join(dir, `${name}.html`);
154
- const metaPath = join(dir, `${name}.json`);
155
-
156
- if (!d.existsSync(htmlPath)) {
157
- d.console.error(`Error: Resource HTML file not found: ${htmlPath}`);
158
- d.process.exit(1);
159
- }
160
-
161
- if (!d.existsSync(metaPath)) {
162
- d.console.error(`Error: Resource metadata file not found: ${metaPath}`);
163
- d.process.exit(1);
164
- }
165
-
166
- let meta = null;
167
- try {
168
- meta = JSON.parse(d.readFileSync(metaPath, 'utf-8'));
169
- } catch {
170
- d.console.warn(`Warning: Could not parse ${name}.json`);
171
- }
172
-
173
- const simulations = findSimulations(simulationsDir, name, d);
174
-
175
- return { name, htmlPath, metaPath, meta, simulations };
176
- }
177
-
178
- /**
179
- * Push a single resource to the API
180
- */
181
- async function pushResource(resource, repository, tags, accessToken, deps = defaultDeps) {
182
- const d = { ...defaultDeps, ...deps };
183
-
184
- if (!resource.meta?.uri) {
185
- throw new Error('Resource is missing URI. Run "sunpeak build" to generate URIs.');
186
- }
187
-
188
- const htmlContent = d.readFileSync(resource.htmlPath);
189
- const htmlBlob = new Blob([htmlContent], { type: 'text/html' });
190
-
191
- // Build form data
192
- const formData = new FormData();
193
- formData.append('repository', repository);
194
- formData.append('html_file', htmlBlob, `${resource.name}.html`);
195
-
196
- // Add metadata fields
197
- if (resource.meta) {
198
- formData.append('name', resource.meta.name || resource.name);
199
- formData.append('title', resource.meta.title || resource.name);
200
- if (resource.meta.description) {
201
- formData.append('description', resource.meta.description);
202
- }
203
- formData.append('mime_type', resource.meta.mimeType || 'text/html+skybridge');
204
- formData.append('uri', resource.meta.uri);
205
-
206
- // Handle UI resource metadata (_meta.ui matches McpUiResourceMeta from ext-apps SDK)
207
- if (resource.meta._meta?.ui) {
208
- const ui = resource.meta._meta.ui;
209
- if (ui.domain) {
210
- formData.append('widget_domain', ui.domain);
211
- }
212
- if (ui.prefersBorder != null) {
213
- formData.append('prefers_border', String(ui.prefersBorder));
214
- }
215
- if (ui.permissions) {
216
- formData.append('permissions', JSON.stringify(ui.permissions));
217
- }
218
- if (ui.csp) {
219
- if (ui.csp.connectDomains) {
220
- ui.csp.connectDomains.forEach((domain) => {
221
- formData.append('widget_csp_connect_domains[]', domain);
222
- });
223
- }
224
- if (ui.csp.resourceDomains) {
225
- ui.csp.resourceDomains.forEach((domain) => {
226
- formData.append('widget_csp_resource_domains[]', domain);
227
- });
228
- }
229
- if (ui.csp.frameDomains) {
230
- ui.csp.frameDomains.forEach((domain) => {
231
- formData.append('widget_csp_frame_domains[]', domain);
232
- });
233
- }
234
- if (ui.csp.baseUriDomains) {
235
- ui.csp.baseUriDomains.forEach((domain) => {
236
- formData.append('widget_csp_base_uri_domains[]', domain);
237
- });
238
- }
239
- }
240
- }
241
- } else {
242
- // Fallback metadata
243
- formData.append('name', resource.name);
244
- formData.append('title', resource.name);
245
- formData.append('mime_type', 'text/html+skybridge');
246
- }
247
-
248
- // Add tags if provided
249
- if (tags && tags.length > 0) {
250
- tags.forEach((tag) => {
251
- formData.append('tags[]', tag);
252
- });
253
- }
254
-
255
- // Add simulations if present
256
- if (resource.simulations && resource.simulations.length > 0) {
257
- formData.append('simulations', JSON.stringify(resource.simulations));
258
- }
259
-
260
- const response = await d.fetch(`${d.apiUrl}/api/v1/resources`, {
261
- method: 'POST',
262
- headers: {
263
- Authorization: `Bearer ${accessToken}`,
264
- },
265
- body: formData,
266
- });
267
-
268
- if (!response.ok) {
269
- const data = await response.json().catch(() => ({}));
270
- let errorMessage = data.message || data.error || `HTTP ${response.status}`;
271
- if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
272
- errorMessage += ': ' + data.errors.join(', ');
273
- }
274
- throw new Error(errorMessage);
275
- }
276
-
277
- return response.json();
278
- }
279
-
280
- /**
281
- * Main push command
282
- * @param {string} projectRoot - Project root directory
283
- * @param {Object} options - Command options
284
- * @param {string} options.repository - Repository name (optional, defaults to git repo name)
285
- * @param {string} options.dir - Path to a specific resource directory (optional)
286
- * @param {string[]} options.tags - Tags to assign to the pushed resources (optional)
287
- * @param {Object} deps - Dependencies (for testing). Uses defaultDeps if not provided.
288
- */
289
- export async function push(projectRoot = process.cwd(), options = {}, deps = defaultDeps) {
290
- const d = { ...defaultDeps, ...deps };
291
-
292
- // Handle help flag
293
- if (options.help) {
294
- d.console.log(`
295
- sunpeak push - Push resources to the Sunpeak repository
296
-
297
- Usage:
298
- sunpeak push [directory] [options]
299
-
300
- Options:
301
- -r, --repository <owner/repo> Repository name (defaults to git remote origin)
302
- -t, --tag <name> Tag to assign (can be specified multiple times)
303
- --no-simulations Skip pushing simulations, only push resources
304
- -h, --help Show this help message
305
-
306
- Arguments:
307
- directory Optional resource directory to push (e.g., dist/carousel)
308
- If not provided, pushes all resources from dist/
309
-
310
- Examples:
311
- sunpeak push Push all resources from dist/
312
- sunpeak push dist/carousel Push a single resource
313
- sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
314
- sunpeak push -t v1.0.0 Push with a version tag
315
- sunpeak push -t v1.0.0 -t prod Push with multiple tags
316
- sunpeak push --no-simulations Push resources without simulations
317
- `);
318
- return;
319
- }
320
-
321
- // Check credentials
322
- const credentials = d.loadCredentials();
323
- if (!credentials?.access_token) {
324
- d.console.error('Error: Not logged in. Run "sunpeak login" first.');
325
- d.process.exit(1);
326
- }
327
-
328
- // Determine repository name (owner/repo format)
329
- const repository = options.repository || d.getGitRepoName();
330
- if (!repository) {
331
- d.console.error('Error: Could not determine repository name.');
332
- d.console.error('Please provide a repository name: sunpeak push --repository <owner/repo>');
333
- d.console.error('Or run this command from within a git repository with a remote origin.');
334
- d.process.exit(1);
335
- }
336
-
337
- // Find resources - either a specific directory or all from dist directory
338
- const simulationsDir = join(projectRoot, 'tests/simulations');
339
- let resources;
340
- if (options.dir) {
341
- // Push a single specific resource from directory
342
- resources = [buildResourceFromDir(options.dir, simulationsDir, d)];
343
- } else {
344
- // Default: find all resources in dist directory
345
- const distDir = join(projectRoot, 'dist');
346
- if (!d.existsSync(distDir)) {
347
- d.console.error(`Error: dist/ directory not found`);
348
- d.console.error('Run "sunpeak build" first to build your resources.');
349
- d.process.exit(1);
350
- }
351
-
352
- resources = findResources(distDir, simulationsDir, d);
353
- if (resources.length === 0) {
354
- d.console.error(`Error: No resources found in dist/`);
355
- d.console.error('Run "sunpeak build" first to build your resources.');
356
- d.process.exit(1);
357
- }
358
- }
359
-
360
- // Clear simulations if --no-simulations flag is set
361
- if (options.noSimulations) {
362
- for (const resource of resources) {
363
- resource.simulations = [];
364
- }
365
- }
366
-
367
- d.console.log(`Pushing ${resources.length} resource(s) to repository "${repository}"...`);
368
- if (options.tags && options.tags.length > 0) {
369
- d.console.log(`Tags: ${options.tags.join(', ')}`);
370
- }
371
- if (options.noSimulations) {
372
- d.console.log('Simulations: skipped');
373
- }
374
- d.console.log();
375
-
376
- // Push each resource
377
- let successCount = 0;
378
- for (const resource of resources) {
379
- try {
380
- const result = await pushResource(resource, repository, options.tags, credentials.access_token, d);
381
- const simCount = resource.simulations?.length || 0;
382
- const simInfo = simCount > 0 ? `, ${simCount} simulation(s)` : '';
383
- const tagInfo = result.tags?.length > 0 ? `, tag(s): ${result.tags.join(', ')}` : '';
384
- d.console.log(`✓ Pushed ${resource.name}${simInfo}${tagInfo}`);
385
- d.console.log(` ${d.apiUrl}/resources/${result.id}`);
386
- successCount++;
387
- } catch (error) {
388
- d.console.error(`✗ Failed to push ${resource.name}: ${error.message}`);
389
- if (error.message.includes('Uri must be unique')) {
390
- d.console.error(' You are trying to push a build that has already been pushed.');
391
- }
392
- }
393
- }
394
-
395
- d.console.log();
396
- if (successCount === resources.length) {
397
- d.console.log(`✓ Successfully pushed ${successCount} resource(s).`);
398
- } else {
399
- d.console.log(`Pushed ${successCount}/${resources.length} resource(s).`);
400
- d.process.exit(1);
401
- }
402
- }
403
-
404
- /**
405
- * Parse command line arguments
406
- */
407
- export function parseArgs(args) {
408
- const options = { tags: [] };
409
- let i = 0;
410
-
411
- while (i < args.length) {
412
- const arg = args[i];
413
-
414
- if (arg === '--repository' || arg === '-r') {
415
- options.repository = args[++i];
416
- } else if (arg === '--tag' || arg === '-t') {
417
- options.tags.push(args[++i]);
418
- } else if (arg === '--no-simulations') {
419
- options.noSimulations = true;
420
- } else if (arg === '--help' || arg === '-h') {
421
- console.log(`
422
- sunpeak push - Push resources to the Sunpeak repository
423
-
424
- Usage:
425
- sunpeak push [directory] [options]
426
-
427
- Options:
428
- -r, --repository <owner/repo> Repository name (defaults to git remote origin)
429
- -t, --tag <name> Tag to assign (can be specified multiple times)
430
- --no-simulations Skip pushing simulations, only push resources
431
- -h, --help Show this help message
432
-
433
- Arguments:
434
- directory Optional resource directory to push (e.g., dist/carousel)
435
- If not provided, pushes all resources from dist/
436
-
437
- Examples:
438
- sunpeak push Push all resources from dist/
439
- sunpeak push dist/carousel Push a single resource
440
- sunpeak push -r myorg/my-app Push to "myorg/my-app" repository
441
- sunpeak push -t v1.0.0 Push with a version tag
442
- sunpeak push -t v1.0.0 -t prod Push with multiple tags
443
- sunpeak push --no-simulations Push resources without simulations
444
- `);
445
- process.exit(0);
446
- } else if (!arg.startsWith('-')) {
447
- // Positional argument - treat as directory path
448
- options.dir = arg;
449
- }
450
-
451
- i++;
452
- }
453
-
454
- return options;
455
- }
456
-
457
- // Allow running directly
458
- if (import.meta.url === `file://${process.argv[1]}`) {
459
- const args = process.argv.slice(2);
460
- const options = parseArgs(args);
461
- push(process.cwd(), options).catch((error) => {
462
- console.error('Error:', error.message);
463
- process.exit(1);
464
- });
465
- }