sunpeak 0.7.11 → 0.8.4
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 +2 -1
- package/bin/commands/deploy.mjs +18 -8
- package/bin/commands/dev.mjs +60 -4
- package/bin/commands/login.mjs +73 -55
- package/bin/commands/logout.mjs +26 -12
- package/bin/commands/mcp.mjs +1 -1
- package/bin/commands/pull.mjs +60 -39
- package/bin/commands/push.mjs +73 -49
- package/bin/commands/upgrade.mjs +203 -0
- package/bin/sunpeak.js +68 -35
- package/dist/chatgpt/chatgpt-simulator.d.ts +2 -1
- package/dist/index.cjs +13 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -14
- package/dist/index.js.map +1 -1
- package/dist/mcp/entry.cjs +41 -9
- package/dist/mcp/entry.cjs.map +1 -1
- package/dist/mcp/entry.js +42 -10
- 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-CziiHU7V.cjs → server-B9YgCQdS.cjs} +3 -2
- package/dist/{server-CziiHU7V.cjs.map → server-B9YgCQdS.cjs.map} +1 -1
- package/dist/{server-D8kyzuiq.js → server-DVmTC-SF.js} +3 -2
- package/dist/{server-D8kyzuiq.js.map → server-DVmTC-SF.js.map} +1 -1
- package/dist/style.css +62 -0
- package/dist/types/simulation.d.ts +1 -1
- package/package.json +1 -1
- package/template/.sunpeak/dev.tsx +78 -15
- package/template/.sunpeak/vite-env.d.ts +1 -0
- package/template/README.md +35 -20
- package/template/dist/albums.js +1 -1
- package/template/dist/albums.json +3 -2
- package/template/dist/carousel.js +1 -1
- package/template/dist/carousel.json +3 -2
- package/template/dist/confirmation.js +49 -0
- package/template/dist/confirmation.json +16 -0
- package/template/dist/counter.js +1 -1
- package/template/dist/counter.json +7 -2
- package/template/dist/map.js +1 -1
- package/template/dist/map.json +6 -3
- package/template/node_modules/.vite/deps/_metadata.json +19 -19
- package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/template/src/components/map/map-view.test.tsx +1 -1
- package/template/src/components/map/map-view.tsx +1 -1
- package/template/src/components/map/map.tsx +1 -1
- package/template/src/components/map/place-card.test.tsx +1 -1
- package/template/src/components/map/place-card.tsx +1 -1
- package/template/src/components/map/place-carousel.test.tsx +1 -1
- package/template/src/components/map/place-carousel.tsx +1 -1
- package/template/src/components/map/place-inspector.test.tsx +1 -1
- package/template/src/components/map/place-inspector.tsx +1 -1
- package/template/src/components/map/place-list.test.tsx +1 -1
- package/template/src/components/map/place-list.tsx +1 -1
- package/template/src/components/map/types.ts +18 -0
- package/template/src/resources/albums-resource.json +1 -1
- package/template/src/resources/carousel-resource.json +1 -1
- package/template/src/resources/confirmation-resource.json +12 -0
- package/template/src/resources/confirmation-resource.tsx +479 -0
- package/template/src/resources/counter-resource.json +4 -1
- package/template/src/resources/index.ts +39 -4
- package/template/src/resources/map-resource.json +7 -2
- package/template/src/simulations/albums-show-simulation.json +131 -0
- package/template/src/simulations/carousel-show-simulation.json +68 -0
- package/template/src/simulations/confirmation-diff-simulation.json +80 -0
- package/template/src/simulations/confirmation-post-simulation.json +56 -0
- package/template/src/simulations/confirmation-purchase-simulation.json +88 -0
- package/template/src/simulations/counter-show-simulation.json +20 -0
- package/template/src/simulations/index.ts +17 -12
- package/template/src/simulations/map-show-simulation.json +123 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.json +1 -1
- package/template/src/simulations/albums-simulation.ts +0 -147
- package/template/src/simulations/carousel-simulation.ts +0 -84
- package/template/src/simulations/counter-simulation.ts +0 -34
- package/template/src/simulations/map-simulation.ts +0 -154
package/README.md
CHANGED
|
@@ -52,10 +52,11 @@ sunpeak is an npm package consisting of:
|
|
|
52
52
|
1. **The `sunpeak` library** (`./src`). An npm package for running & testing ChatGPT Apps. This standalone library contains:
|
|
53
53
|
1. Runtime APIs - Strongly typed, multi-platform APIs for interacting with the ChatGPT runtime, architected to support future platforms (Gemini, Claude).
|
|
54
54
|
2. ChatGPT simulator - React component replicating ChatGPT's runtime.
|
|
55
|
-
3. MCP server - Mock data MCP server for testing local
|
|
55
|
+
3. MCP server - Mock data MCP server for testing local UIs (resources) in the real ChatGPT.
|
|
56
56
|
2. **The `sunpeak` framework** (`./template`). Next.js for ChatGPT Apps. This templated npm package includes:
|
|
57
57
|
1. Project scaffold - Complete development setup with build, test, and mcp tooling, including the sunpeak library.
|
|
58
58
|
2. UI components - Production-ready components following ChatGPT design guidelines and using OpenAI apps-sdk-ui React components.
|
|
59
|
+
3. Convention over configuration - Create UIs (resources) by simply creating a `resources/NAME-resource.tsx` React file and `resources/NAME-resource.json` metadata file.
|
|
59
60
|
3. **The `sunpeak` CLI** (`./bin`). Commands for managing ChatGPT Apps. Includes a client for the [sunpeak Resource Repository](https://app.sunpeak.ai/) (ECR for ChatGPT Apps). The repository helps you & your CI/CD decouple your App from your client-agnostic MCP server:
|
|
60
61
|
1. Tag your app builds with version numbers and environment names (like `v1.0.0` and `prod`)
|
|
61
62
|
2. `push` built Apps to a central location
|
package/bin/commands/deploy.mjs
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { push, findResources } from './push.mjs';
|
|
2
|
+
import { push, findResources, defaultDeps as pushDefaultDeps } from './push.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default dependencies (real implementations)
|
|
6
|
+
*/
|
|
7
|
+
export const defaultDeps = {
|
|
8
|
+
...pushDefaultDeps,
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Deploy command - same as push but with tag="prod"
|
|
6
13
|
* @param {string} projectRoot - Project root directory
|
|
7
14
|
* @param {Object} options - Command options (same as push, but tag defaults to "prod")
|
|
15
|
+
* @param {Object} deps - Dependencies (for testing). Uses defaultDeps if not provided.
|
|
8
16
|
*/
|
|
9
|
-
export async function deploy(projectRoot = process.cwd(), options = {}) {
|
|
17
|
+
export async function deploy(projectRoot = process.cwd(), options = {}, deps = defaultDeps) {
|
|
18
|
+
const d = { ...defaultDeps, ...deps };
|
|
19
|
+
|
|
10
20
|
// Handle help flag
|
|
11
21
|
if (options.help) {
|
|
12
|
-
console.log(`
|
|
22
|
+
d.console.log(`
|
|
13
23
|
sunpeak deploy - Push resources to production (push with "prod" tag)
|
|
14
24
|
|
|
15
25
|
Usage:
|
|
@@ -43,23 +53,23 @@ This command is equivalent to: sunpeak push --tag prod
|
|
|
43
53
|
tags: ['prod', ...additionalTags],
|
|
44
54
|
};
|
|
45
55
|
|
|
46
|
-
console.log('Deploying to production...');
|
|
47
|
-
console.log();
|
|
56
|
+
d.console.log('Deploying to production...');
|
|
57
|
+
d.console.log();
|
|
48
58
|
|
|
49
59
|
// If no specific file provided, check current directory first, then dist/
|
|
50
60
|
if (!deployOptions.file) {
|
|
51
|
-
const cwdResources = findResources(projectRoot);
|
|
61
|
+
const cwdResources = findResources(projectRoot, d);
|
|
52
62
|
if (cwdResources.length > 0) {
|
|
53
63
|
// Found resources in current directory, push each one
|
|
54
64
|
for (const resource of cwdResources) {
|
|
55
|
-
await push(projectRoot, { ...deployOptions, file: resource.jsPath });
|
|
65
|
+
await push(projectRoot, { ...deployOptions, file: resource.jsPath }, d);
|
|
56
66
|
}
|
|
57
67
|
return;
|
|
58
68
|
}
|
|
59
69
|
// Fall back to dist/ directory (handled by push)
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
await push(projectRoot, deployOptions);
|
|
72
|
+
await push(projectRoot, deployOptions, d);
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/**
|
package/bin/commands/dev.mjs
CHANGED
|
@@ -1,9 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createServer } from 'vite';
|
|
3
|
-
import react from '@vitejs/plugin-react';
|
|
4
|
-
import tailwindcss from '@tailwindcss/vite';
|
|
5
2
|
import { existsSync } from 'fs';
|
|
6
|
-
import { join, resolve, basename } from 'path';
|
|
3
|
+
import { join, resolve, basename, dirname } from 'path';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Import a module from the project's node_modules using ESM resolution
|
|
9
|
+
*/
|
|
10
|
+
async function importFromProject(require, moduleName) {
|
|
11
|
+
// Resolve the module's main entry to find its location
|
|
12
|
+
const resolvedPath = require.resolve(moduleName);
|
|
13
|
+
|
|
14
|
+
// Walk up to find package.json
|
|
15
|
+
const { readFileSync } = await import('fs');
|
|
16
|
+
let pkgDir = dirname(resolvedPath);
|
|
17
|
+
let pkg;
|
|
18
|
+
while (pkgDir !== dirname(pkgDir)) {
|
|
19
|
+
try {
|
|
20
|
+
const pkgJsonPath = join(pkgDir, 'package.json');
|
|
21
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
22
|
+
if (pkg.name === moduleName || moduleName.startsWith(pkg.name + '/')) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// No package.json at this level, keep looking
|
|
27
|
+
}
|
|
28
|
+
pkgDir = dirname(pkgDir);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!pkg) {
|
|
32
|
+
// Fallback to CJS resolution if we can't find package.json
|
|
33
|
+
return import(resolvedPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Determine ESM entry: exports.import > exports.default > module > main
|
|
37
|
+
let entry = pkg.main || 'index.js';
|
|
38
|
+
if (pkg.exports) {
|
|
39
|
+
const exp = pkg.exports['.'] || pkg.exports;
|
|
40
|
+
if (typeof exp === 'string') {
|
|
41
|
+
entry = exp;
|
|
42
|
+
} else if (exp.import) {
|
|
43
|
+
entry = typeof exp.import === 'string' ? exp.import : exp.import.default;
|
|
44
|
+
} else if (exp.default) {
|
|
45
|
+
entry = exp.default;
|
|
46
|
+
}
|
|
47
|
+
} else if (pkg.module) {
|
|
48
|
+
entry = pkg.module;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entryPath = join(pkgDir, entry);
|
|
52
|
+
return import(pathToFileURL(entryPath).href);
|
|
53
|
+
}
|
|
7
54
|
|
|
8
55
|
/**
|
|
9
56
|
* Start the Vite development server
|
|
@@ -18,6 +65,15 @@ export async function dev(projectRoot = process.cwd(), args = []) {
|
|
|
18
65
|
process.exit(1);
|
|
19
66
|
}
|
|
20
67
|
|
|
68
|
+
// Import vite and plugins from the project's node_modules (ESM)
|
|
69
|
+
const require = createRequire(join(projectRoot, 'package.json'));
|
|
70
|
+
const vite = await importFromProject(require, 'vite');
|
|
71
|
+
const createServer = vite.createServer;
|
|
72
|
+
const reactPlugin = await importFromProject(require, '@vitejs/plugin-react');
|
|
73
|
+
const react = reactPlugin.default;
|
|
74
|
+
const tailwindPlugin = await importFromProject(require, '@tailwindcss/vite');
|
|
75
|
+
const tailwindcss = tailwindPlugin.default;
|
|
76
|
+
|
|
21
77
|
// Parse port from args or use default
|
|
22
78
|
let port = parseInt(process.env.PORT || '6767');
|
|
23
79
|
const portArgIndex = args.findIndex(arg => arg === '--port' || arg === '-p');
|
package/bin/commands/login.mjs
CHANGED
|
@@ -15,7 +15,7 @@ const MAX_POLL_DURATION_MS = 2 * 60 * 1000; // 2 minutes max.
|
|
|
15
15
|
/**
|
|
16
16
|
* Load existing credentials if present
|
|
17
17
|
*/
|
|
18
|
-
function
|
|
18
|
+
function loadCredentialsImpl() {
|
|
19
19
|
if (!existsSync(CREDENTIALS_FILE)) {
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
@@ -29,18 +29,62 @@ function loadCredentials() {
|
|
|
29
29
|
/**
|
|
30
30
|
* Save credentials to disk
|
|
31
31
|
*/
|
|
32
|
-
function
|
|
32
|
+
function saveCredentialsImpl(credentials) {
|
|
33
33
|
if (!existsSync(CREDENTIALS_DIR)) {
|
|
34
34
|
mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
35
35
|
}
|
|
36
36
|
writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Open a URL in the default browser
|
|
41
|
+
* Returns true if successful, false otherwise
|
|
42
|
+
*/
|
|
43
|
+
function openBrowserImpl(url) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const os = platform();
|
|
46
|
+
let command;
|
|
47
|
+
|
|
48
|
+
// Platform-specific commands to open URLs
|
|
49
|
+
if (os === 'darwin') {
|
|
50
|
+
command = `open "${url}"`;
|
|
51
|
+
} else if (os === 'win32') {
|
|
52
|
+
command = `start "" "${url}"`;
|
|
53
|
+
} else {
|
|
54
|
+
// Linux and other Unix-like systems
|
|
55
|
+
command = `xdg-open "${url}"`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exec(command, (error) => {
|
|
59
|
+
resolve(!error);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sleep(ms) {
|
|
65
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Default dependencies (real implementations)
|
|
70
|
+
*/
|
|
71
|
+
export const defaultDeps = {
|
|
72
|
+
fetch: globalThis.fetch,
|
|
73
|
+
loadCredentials: loadCredentialsImpl,
|
|
74
|
+
saveCredentials: saveCredentialsImpl,
|
|
75
|
+
openBrowser: openBrowserImpl,
|
|
76
|
+
console,
|
|
77
|
+
sleep,
|
|
78
|
+
apiUrl: SUNPEAK_API_URL,
|
|
79
|
+
pollIntervalMs: POLL_INTERVAL_MS,
|
|
80
|
+
maxPollDurationMs: MAX_POLL_DURATION_MS,
|
|
81
|
+
};
|
|
82
|
+
|
|
39
83
|
/**
|
|
40
84
|
* Request a device code from the authorization server
|
|
41
85
|
*/
|
|
42
|
-
async function requestDeviceCode() {
|
|
43
|
-
const response = await fetch(`${
|
|
86
|
+
async function requestDeviceCode(deps) {
|
|
87
|
+
const response = await deps.fetch(`${deps.apiUrl}/oauth/device_authorization`, {
|
|
44
88
|
method: 'POST',
|
|
45
89
|
headers: { 'Content-Type': 'application/json' },
|
|
46
90
|
});
|
|
@@ -56,13 +100,13 @@ async function requestDeviceCode() {
|
|
|
56
100
|
/**
|
|
57
101
|
* Poll for the access token
|
|
58
102
|
*/
|
|
59
|
-
async function pollForToken(deviceCode) {
|
|
103
|
+
async function pollForToken(deviceCode, deps) {
|
|
60
104
|
const startTime = Date.now();
|
|
61
105
|
|
|
62
|
-
while (Date.now() - startTime <
|
|
106
|
+
while (Date.now() - startTime < deps.maxPollDurationMs) {
|
|
63
107
|
let response;
|
|
64
108
|
try {
|
|
65
|
-
response = await fetch(`${
|
|
109
|
+
response = await deps.fetch(`${deps.apiUrl}/oauth/token`, {
|
|
66
110
|
method: 'POST',
|
|
67
111
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
68
112
|
body: new URLSearchParams({
|
|
@@ -72,7 +116,7 @@ async function pollForToken(deviceCode) {
|
|
|
72
116
|
});
|
|
73
117
|
} catch (err) {
|
|
74
118
|
// Network error - wait and retry
|
|
75
|
-
await sleep(
|
|
119
|
+
await deps.sleep(deps.pollIntervalMs);
|
|
76
120
|
continue;
|
|
77
121
|
}
|
|
78
122
|
|
|
@@ -95,13 +139,13 @@ async function pollForToken(deviceCode) {
|
|
|
95
139
|
// Handle standard OAuth 2.0 device flow errors (expected during polling)
|
|
96
140
|
if (data.error === 'authorization_pending') {
|
|
97
141
|
// User hasn't authorized yet, keep polling
|
|
98
|
-
await sleep(
|
|
142
|
+
await deps.sleep(deps.pollIntervalMs);
|
|
99
143
|
continue;
|
|
100
144
|
}
|
|
101
145
|
|
|
102
146
|
if (data.error === 'slow_down') {
|
|
103
147
|
// Server asking us to slow down, increase interval
|
|
104
|
-
await sleep(
|
|
148
|
+
await deps.sleep(deps.pollIntervalMs * 2);
|
|
105
149
|
continue;
|
|
106
150
|
}
|
|
107
151
|
|
|
@@ -126,50 +170,24 @@ async function pollForToken(deviceCode) {
|
|
|
126
170
|
throw new Error('Authorization timed out. Please try again.');
|
|
127
171
|
}
|
|
128
172
|
|
|
129
|
-
function sleep(ms) {
|
|
130
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Open a URL in the default browser
|
|
135
|
-
* Returns true if successful, false otherwise
|
|
136
|
-
*/
|
|
137
|
-
function openBrowser(url) {
|
|
138
|
-
return new Promise((resolve) => {
|
|
139
|
-
const os = platform();
|
|
140
|
-
let command;
|
|
141
|
-
|
|
142
|
-
// Platform-specific commands to open URLs
|
|
143
|
-
if (os === 'darwin') {
|
|
144
|
-
command = `open "${url}"`;
|
|
145
|
-
} else if (os === 'win32') {
|
|
146
|
-
command = `start "" "${url}"`;
|
|
147
|
-
} else {
|
|
148
|
-
// Linux and other Unix-like systems
|
|
149
|
-
command = `xdg-open "${url}"`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
exec(command, (error) => {
|
|
153
|
-
resolve(!error);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
173
|
/**
|
|
159
174
|
* Main login command
|
|
175
|
+
* @param {Object} deps - Dependencies (for testing). Uses defaultDeps if not provided.
|
|
160
176
|
*/
|
|
161
|
-
export async function login() {
|
|
177
|
+
export async function login(deps = defaultDeps) {
|
|
178
|
+
const d = { ...defaultDeps, ...deps };
|
|
179
|
+
|
|
162
180
|
// Check if already logged in
|
|
163
|
-
const existing = loadCredentials();
|
|
181
|
+
const existing = d.loadCredentials();
|
|
164
182
|
if (existing?.access_token) {
|
|
165
|
-
console.log('Already logged in. Run "sunpeak logout" first to switch accounts.');
|
|
183
|
+
d.console.log('Already logged in. Run "sunpeak logout" first to switch accounts.');
|
|
166
184
|
return;
|
|
167
185
|
}
|
|
168
186
|
|
|
169
|
-
console.log('Starting device authorization flow...\n');
|
|
187
|
+
d.console.log('Starting device authorization flow...\n');
|
|
170
188
|
|
|
171
189
|
// Step 1: Request device code
|
|
172
|
-
const deviceAuth = await requestDeviceCode();
|
|
190
|
+
const deviceAuth = await requestDeviceCode(d);
|
|
173
191
|
|
|
174
192
|
// Step 2: Open browser and display instructions
|
|
175
193
|
// Prefer verification_uri_complete which has the code pre-filled
|
|
@@ -177,21 +195,21 @@ export async function login() {
|
|
|
177
195
|
deviceAuth.verification_uri_complete ||
|
|
178
196
|
`${deviceAuth.verification_uri}?user_code=${encodeURIComponent(deviceAuth.user_code)}`;
|
|
179
197
|
|
|
180
|
-
const browserOpened = await openBrowser(authUrl);
|
|
198
|
+
const browserOpened = await d.openBrowser(authUrl);
|
|
181
199
|
|
|
182
200
|
if (browserOpened) {
|
|
183
|
-
console.log('Opening browser for authentication...\n');
|
|
184
|
-
console.log(`If the browser didn't open, visit: ${deviceAuth.verification_uri}`);
|
|
185
|
-
console.log(`And enter code: ${deviceAuth.user_code}`);
|
|
201
|
+
d.console.log('Opening browser for authentication...\n');
|
|
202
|
+
d.console.log(`If the browser didn't open, visit: ${deviceAuth.verification_uri}`);
|
|
203
|
+
d.console.log(`And enter code: ${deviceAuth.user_code}`);
|
|
186
204
|
} else {
|
|
187
|
-
console.log('To complete login, please:');
|
|
188
|
-
console.log(` 1. Visit: ${deviceAuth.verification_uri}`);
|
|
189
|
-
console.log(` 2. Enter code: ${deviceAuth.user_code}`);
|
|
205
|
+
d.console.log('To complete login, please:');
|
|
206
|
+
d.console.log(` 1. Visit: ${deviceAuth.verification_uri}`);
|
|
207
|
+
d.console.log(` 2. Enter code: ${deviceAuth.user_code}`);
|
|
190
208
|
}
|
|
191
|
-
console.log('\nWaiting for authorization...');
|
|
209
|
+
d.console.log('\nWaiting for authorization...');
|
|
192
210
|
|
|
193
211
|
// Step 3: Poll for token
|
|
194
|
-
const tokenResponse = await pollForToken(deviceAuth.device_code);
|
|
212
|
+
const tokenResponse = await pollForToken(deviceAuth.device_code, d);
|
|
195
213
|
|
|
196
214
|
// Step 4: Save credentials
|
|
197
215
|
const credentials = {
|
|
@@ -203,9 +221,9 @@ export async function login() {
|
|
|
203
221
|
created_at: Date.now(),
|
|
204
222
|
};
|
|
205
223
|
|
|
206
|
-
saveCredentials(credentials);
|
|
224
|
+
d.saveCredentials(credentials);
|
|
207
225
|
|
|
208
|
-
console.log('\n✓ Successfully logged in to Sunpeak!');
|
|
226
|
+
d.console.log('\n✓ Successfully logged in to Sunpeak!');
|
|
209
227
|
}
|
|
210
228
|
|
|
211
229
|
// Allow running directly
|
package/bin/commands/logout.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');
|
|
|
10
10
|
/**
|
|
11
11
|
* Load existing credentials if present
|
|
12
12
|
*/
|
|
13
|
-
function
|
|
13
|
+
function loadCredentialsImpl() {
|
|
14
14
|
if (!existsSync(CREDENTIALS_FILE)) {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
@@ -24,18 +24,29 @@ function loadCredentials() {
|
|
|
24
24
|
/**
|
|
25
25
|
* Delete credentials file
|
|
26
26
|
*/
|
|
27
|
-
function
|
|
27
|
+
function deleteCredentialsImpl() {
|
|
28
28
|
if (existsSync(CREDENTIALS_FILE)) {
|
|
29
29
|
unlinkSync(CREDENTIALS_FILE);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Default dependencies (real implementations)
|
|
35
|
+
*/
|
|
36
|
+
export const defaultDeps = {
|
|
37
|
+
fetch: globalThis.fetch,
|
|
38
|
+
loadCredentials: loadCredentialsImpl,
|
|
39
|
+
deleteCredentials: deleteCredentialsImpl,
|
|
40
|
+
console,
|
|
41
|
+
apiUrl: SUNPEAK_API_URL,
|
|
42
|
+
};
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Revoke the access token on the server
|
|
35
46
|
*/
|
|
36
|
-
async function revokeToken(accessToken) {
|
|
47
|
+
async function revokeToken(accessToken, deps) {
|
|
37
48
|
try {
|
|
38
|
-
const response = await fetch(`${
|
|
49
|
+
const response = await deps.fetch(`${deps.apiUrl}/oauth/revoke`, {
|
|
39
50
|
method: 'POST',
|
|
40
51
|
headers: {
|
|
41
52
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -54,27 +65,30 @@ async function revokeToken(accessToken) {
|
|
|
54
65
|
|
|
55
66
|
/**
|
|
56
67
|
* Main logout command
|
|
68
|
+
* @param {Object} deps - Dependencies (for testing). Uses defaultDeps if not provided.
|
|
57
69
|
*/
|
|
58
|
-
export async function logout() {
|
|
59
|
-
const
|
|
70
|
+
export async function logout(deps = defaultDeps) {
|
|
71
|
+
const d = { ...defaultDeps, ...deps };
|
|
72
|
+
|
|
73
|
+
const credentials = d.loadCredentials();
|
|
60
74
|
|
|
61
75
|
if (!credentials?.access_token) {
|
|
62
|
-
console.log('Not logged in.');
|
|
76
|
+
d.console.log('Not logged in.');
|
|
63
77
|
return;
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
console.log('Logging out...');
|
|
80
|
+
d.console.log('Logging out...');
|
|
67
81
|
|
|
68
82
|
// Revoke token on server
|
|
69
|
-
const revoked = await revokeToken(credentials.access_token);
|
|
83
|
+
const revoked = await revokeToken(credentials.access_token, d);
|
|
70
84
|
|
|
71
85
|
// Always delete local credentials regardless of revocation result
|
|
72
|
-
deleteCredentials();
|
|
86
|
+
d.deleteCredentials();
|
|
73
87
|
|
|
74
88
|
if (revoked) {
|
|
75
|
-
console.log('✓ Successfully logged out of Sunpeak.');
|
|
89
|
+
d.console.log('✓ Successfully logged out of Sunpeak.');
|
|
76
90
|
} else {
|
|
77
|
-
console.log('✓ Logged out locally. (Server token revocation may have failed)');
|
|
91
|
+
d.console.log('✓ Logged out locally. (Server token revocation may have failed)');
|
|
78
92
|
}
|
|
79
93
|
}
|
|
80
94
|
|
package/bin/commands/mcp.mjs
CHANGED
|
@@ -29,7 +29,7 @@ export async function mcp(projectRoot = process.cwd(), args = []) {
|
|
|
29
29
|
// Add inline nodemon configuration
|
|
30
30
|
nodemonArgs.push(
|
|
31
31
|
'--watch', 'src',
|
|
32
|
-
'--ext', 'ts,tsx,js,jsx,css',
|
|
32
|
+
'--ext', 'ts,tsx,js,jsx,css,json',
|
|
33
33
|
'--ignore', 'dist',
|
|
34
34
|
'--ignore', 'node_modules',
|
|
35
35
|
'--ignore', '.tmp',
|
package/bin/commands/pull.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');
|
|
|
10
10
|
/**
|
|
11
11
|
* Load credentials from disk
|
|
12
12
|
*/
|
|
13
|
-
function
|
|
13
|
+
function loadCredentialsImpl() {
|
|
14
14
|
if (!existsSync(CREDENTIALS_FILE)) {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
@@ -21,16 +21,32 @@ function loadCredentials() {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Default dependencies (real implementations)
|
|
26
|
+
*/
|
|
27
|
+
export const defaultDeps = {
|
|
28
|
+
fetch: globalThis.fetch,
|
|
29
|
+
loadCredentials: loadCredentialsImpl,
|
|
30
|
+
existsSync,
|
|
31
|
+
mkdirSync,
|
|
32
|
+
writeFileSync,
|
|
33
|
+
console,
|
|
34
|
+
process,
|
|
35
|
+
apiUrl: SUNPEAK_API_URL,
|
|
36
|
+
};
|
|
37
|
+
|
|
24
38
|
/**
|
|
25
39
|
* Lookup resources by tag and repository
|
|
26
40
|
* @returns {Promise<Array>} Array of matching resources
|
|
27
41
|
*/
|
|
28
|
-
async function lookupResources(tag, repository, accessToken, name = null) {
|
|
42
|
+
async function lookupResources(tag, repository, accessToken, name = null, deps = defaultDeps) {
|
|
43
|
+
const d = { ...defaultDeps, ...deps };
|
|
44
|
+
|
|
29
45
|
const params = new URLSearchParams({ tag, repository });
|
|
30
46
|
if (name) {
|
|
31
47
|
params.set('name', name);
|
|
32
48
|
}
|
|
33
|
-
const response = await fetch(`${
|
|
49
|
+
const response = await d.fetch(`${d.apiUrl}/api/v1/resources/lookup?${params}`, {
|
|
34
50
|
headers: {
|
|
35
51
|
Authorization: `Bearer ${accessToken}`,
|
|
36
52
|
},
|
|
@@ -48,13 +64,15 @@ async function lookupResources(tag, repository, accessToken, name = null) {
|
|
|
48
64
|
/**
|
|
49
65
|
* Download the JS file for a resource
|
|
50
66
|
*/
|
|
51
|
-
async function downloadJsFile(resource) {
|
|
67
|
+
async function downloadJsFile(resource, deps = defaultDeps) {
|
|
68
|
+
const d = { ...defaultDeps, ...deps };
|
|
69
|
+
|
|
52
70
|
if (!resource.js_file?.url) {
|
|
53
71
|
throw new Error('Resource has no JS file attached');
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
// The URL is a pre-signed S3 URL, no additional auth needed
|
|
57
|
-
const response = await fetch(resource.js_file.url);
|
|
75
|
+
const response = await d.fetch(resource.js_file.url);
|
|
58
76
|
|
|
59
77
|
if (!response.ok) {
|
|
60
78
|
throw new Error(`Failed to download JS file: HTTP ${response.status}`);
|
|
@@ -71,11 +89,14 @@ async function downloadJsFile(resource) {
|
|
|
71
89
|
* @param {string} options.tag - Tag name to pull (required)
|
|
72
90
|
* @param {string} options.name - Resource name to filter by (optional)
|
|
73
91
|
* @param {string} options.output - Output directory (optional, defaults to current directory)
|
|
92
|
+
* @param {Object} deps - Dependencies (for testing). Uses defaultDeps if not provided.
|
|
74
93
|
*/
|
|
75
|
-
export async function pull(projectRoot = process.cwd(), options = {}) {
|
|
94
|
+
export async function pull(projectRoot = process.cwd(), options = {}, deps = defaultDeps) {
|
|
95
|
+
const d = { ...defaultDeps, ...deps };
|
|
96
|
+
|
|
76
97
|
// Handle help flag
|
|
77
98
|
if (options.help) {
|
|
78
|
-
console.log(`
|
|
99
|
+
d.console.log(`
|
|
79
100
|
sunpeak pull - Pull resources from the Sunpeak repository
|
|
80
101
|
|
|
81
102
|
Usage:
|
|
@@ -97,74 +118,74 @@ Examples:
|
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
// Check credentials
|
|
100
|
-
const credentials = loadCredentials();
|
|
121
|
+
const credentials = d.loadCredentials();
|
|
101
122
|
if (!credentials?.access_token) {
|
|
102
|
-
console.error('Error: Not logged in. Run "sunpeak login" first.');
|
|
103
|
-
process.exit(1);
|
|
123
|
+
d.console.error('Error: Not logged in. Run "sunpeak login" first.');
|
|
124
|
+
d.process.exit(1);
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
// Require repository
|
|
107
128
|
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);
|
|
129
|
+
d.console.error('Error: Repository is required. Use --repository or -r to specify a repository.');
|
|
130
|
+
d.console.error('Example: sunpeak pull -r myorg/my-app -t prod');
|
|
131
|
+
d.process.exit(1);
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
// Require tag
|
|
114
135
|
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);
|
|
136
|
+
d.console.error('Error: Tag is required. Use --tag or -t to specify a tag.');
|
|
137
|
+
d.console.error('Example: sunpeak pull -r myorg/my-app -t prod');
|
|
138
|
+
d.process.exit(1);
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
const repository = options.repository;
|
|
121
142
|
|
|
122
143
|
const nameFilter = options.name ? ` with name "${options.name}"` : '';
|
|
123
|
-
console.log(`Pulling resources from repository "${repository}" with tag "${options.tag}"${nameFilter}...`);
|
|
124
|
-
console.log();
|
|
144
|
+
d.console.log(`Pulling resources from repository "${repository}" with tag "${options.tag}"${nameFilter}...`);
|
|
145
|
+
d.console.log();
|
|
125
146
|
|
|
126
147
|
try {
|
|
127
148
|
// Lookup resources
|
|
128
|
-
const resources = await lookupResources(options.tag, repository, credentials.access_token, options.name);
|
|
149
|
+
const resources = await lookupResources(options.tag, repository, credentials.access_token, options.name, d);
|
|
129
150
|
|
|
130
151
|
if (!resources || resources.length === 0) {
|
|
131
|
-
console.error('Error: No resources found matching the criteria.');
|
|
132
|
-
process.exit(1);
|
|
152
|
+
d.console.error('Error: No resources found matching the criteria.');
|
|
153
|
+
d.process.exit(1);
|
|
133
154
|
}
|
|
134
155
|
|
|
135
|
-
console.log(`Found ${resources.length} resource(s):\n`);
|
|
156
|
+
d.console.log(`Found ${resources.length} resource(s):\n`);
|
|
136
157
|
|
|
137
158
|
// Determine output directory
|
|
138
159
|
const outputDir = options.output || projectRoot;
|
|
139
160
|
|
|
140
161
|
// Create output directory if it doesn't exist
|
|
141
|
-
if (!existsSync(outputDir)) {
|
|
142
|
-
mkdirSync(outputDir, { recursive: true });
|
|
162
|
+
if (!d.existsSync(outputDir)) {
|
|
163
|
+
d.mkdirSync(outputDir, { recursive: true });
|
|
143
164
|
}
|
|
144
165
|
|
|
145
166
|
// Process each resource
|
|
146
167
|
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}`);
|
|
168
|
+
d.console.log(`Resource: ${resource.name}`);
|
|
169
|
+
d.console.log(` Title: ${resource.title}`);
|
|
170
|
+
d.console.log(` URI: ${resource.uri}`);
|
|
171
|
+
d.console.log(` Tags: ${resource.tags?.join(', ') || 'none'}`);
|
|
172
|
+
d.console.log(` Created: ${resource.created_at}`);
|
|
152
173
|
|
|
153
174
|
if (!resource.js_file) {
|
|
154
|
-
console.log(` ⚠ Skipping: No JS file attached.\n`);
|
|
175
|
+
d.console.log(` ⚠ Skipping: No JS file attached.\n`);
|
|
155
176
|
continue;
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
// Download the JS file
|
|
159
|
-
console.log(` Downloading JS file...`);
|
|
160
|
-
const jsContent = await downloadJsFile(resource);
|
|
180
|
+
d.console.log(` Downloading JS file...`);
|
|
181
|
+
const jsContent = await downloadJsFile(resource, d);
|
|
161
182
|
|
|
162
183
|
const outputFile = join(outputDir, `${resource.name}.js`);
|
|
163
184
|
const metaFile = join(outputDir, `${resource.name}.json`);
|
|
164
185
|
|
|
165
186
|
// Write the JS file
|
|
166
|
-
writeFileSync(outputFile, jsContent);
|
|
167
|
-
console.log(` ✓ Saved ${resource.name}.js`);
|
|
187
|
+
d.writeFileSync(outputFile, jsContent);
|
|
188
|
+
d.console.log(` ✓ Saved ${resource.name}.js`);
|
|
168
189
|
|
|
169
190
|
// Write metadata JSON
|
|
170
191
|
const meta = {
|
|
@@ -181,14 +202,14 @@ Examples:
|
|
|
181
202
|
},
|
|
182
203
|
},
|
|
183
204
|
};
|
|
184
|
-
writeFileSync(metaFile, JSON.stringify(meta, null, 2));
|
|
185
|
-
console.log(` ✓ Saved ${resource.name}.json\n`);
|
|
205
|
+
d.writeFileSync(metaFile, JSON.stringify(meta, null, 2));
|
|
206
|
+
d.console.log(` ✓ Saved ${resource.name}.json\n`);
|
|
186
207
|
}
|
|
187
208
|
|
|
188
|
-
console.log(`✓ Successfully pulled ${resources.length} resource(s) to ${outputDir}`);
|
|
209
|
+
d.console.log(`✓ Successfully pulled ${resources.length} resource(s) to ${outputDir}`);
|
|
189
210
|
} catch (error) {
|
|
190
|
-
console.error(`Error: ${error.message}`);
|
|
191
|
-
process.exit(1);
|
|
211
|
+
d.console.error(`Error: ${error.message}`);
|
|
212
|
+
d.process.exit(1);
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
|