sunpeak 0.20.60 → 0.20.64
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 -0
- package/bin/commands/inspect.mjs +10 -7
- package/bin/commands/new.mjs +11 -8
- package/bin/commands/test-init.mjs +1 -0
- package/bin/commands/test.mjs +8 -4
- package/bin/lib/live/chatgpt-page.mjs +40 -25
- package/bin/lib/live/global-setup.mjs +2 -2
- package/bin/lib/live/host-page.mjs +30 -24
- package/bin/lib/resolve-bin.mjs +18 -0
- package/bin/lib/test/base-config.mjs +3 -1
- package/bin/lib/test/test-config.mjs +11 -2
- package/bin/lib/test/test-fixtures.mjs +12 -3
- package/bin/sunpeak.js +4 -2
- package/dist/chatgpt/index.cjs +1 -1
- package/dist/chatgpt/index.js +1 -1
- package/dist/claude/index.cjs +1 -1
- package/dist/claude/index.js +1 -1
- package/dist/embed.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/inspector/index.cjs +1 -1
- package/dist/inspector/index.js +1 -1
- package/dist/{inspector-WIO0nm2Q.cjs → inspector-Cl5w_7yQ.cjs} +7 -4
- package/dist/inspector-Cl5w_7yQ.cjs.map +1 -0
- package/dist/{inspector-CFSIZYOm.js → inspector-DPJeSQAd.js} +7 -4
- package/dist/inspector-DPJeSQAd.js.map +1 -0
- package/dist/protocol-DVkrOIAq.cjs.map +1 -1
- package/dist/protocol-fGgeFIiL.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +16 -15
- package/template/README.md +12 -4
- package/template/dist/albums/albums.html +4 -4
- package/template/dist/albums/albums.json +1 -1
- package/template/dist/carousel/carousel.html +7 -7
- package/template/dist/carousel/carousel.json +1 -1
- package/template/dist/map/map.html +7 -7
- package/template/dist/map/map.json +1 -1
- package/template/dist/review/review.html +4 -4
- package/template/dist/review/review.json +1 -1
- package/template/node_modules/.bin/playwright +2 -2
- package/template/node_modules/.bin/vite +2 -2
- package/template/node_modules/.bin/vitest +2 -2
- package/template/node_modules/.vite/deps/_metadata.json +4 -4
- package/template/node_modules/.vite-mcp/deps/_metadata.json +21 -21
- package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js +2 -1
- package/template/node_modules/.vite-mcp/deps/embla-carousel-react.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/external-D8EJN_Cc.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +114 -87
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/tailwind-merge.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/vitest.js +40 -24
- package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -1
- package/template/package.json +6 -5
- package/template/tests/e2e/carousel.spec.ts +9 -9
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-chatgpt-darwin.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-claude-darwin.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-chatgpt-darwin.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-claude-darwin.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-chatgpt-darwin.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-claude-darwin.png +0 -0
- package/template/vitest.config.ts +2 -0
- package/dist/inspector-CFSIZYOm.js.map +0 -1
- package/dist/inspector-WIO0nm2Q.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ Server-agnostic MCP testing framework and full-stack MCP App framework.
|
|
|
20
20
|
|
|
21
21
|
MCP Apps are cross-platform, meaning sunpeak is a ChatGPT App framework, Claude Connector framework, and more.
|
|
22
22
|
|
|
23
|
+
ChatGPT apps are now submitted and published as plugins. The app remains an MCP-backed app, so sunpeak's architecture and runtime do not change. The plugin is the package used for local installation, review, and public distribution.
|
|
24
|
+
|
|
23
25
|
```bash
|
|
24
26
|
npx sunpeak new
|
|
25
27
|
```
|
|
@@ -149,4 +151,5 @@ Toggle between hosts, themes, display modes, and device types from the sidebar.
|
|
|
149
151
|
- [MCP Overview](https://sunpeak.ai/docs/mcp-apps/mcp/overview) · [Tools](https://sunpeak.ai/docs/mcp-apps/mcp/tools) · [Resources](https://sunpeak.ai/docs/mcp-apps/mcp/resources)
|
|
150
152
|
- [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps)
|
|
151
153
|
- [ChatGPT Apps SDK Design Guidelines](https://developers.openai.com/apps-sdk/concepts/design-guidelines)
|
|
154
|
+
- [Build ChatGPT Plugins](https://learn.chatgpt.com/docs/build-plugins) · [Submit ChatGPT Plugins](https://learn.chatgpt.com/docs/submit-plugins)
|
|
152
155
|
- [Troubleshooting](https://sunpeak.ai/docs/app-framework/guides/troubleshooting)
|
package/bin/commands/inspect.mjs
CHANGED
|
@@ -1032,6 +1032,12 @@ function appendInspectorRequestToken(resourceUrl, requestToken) {
|
|
|
1032
1032
|
}
|
|
1033
1033
|
}
|
|
1034
1034
|
|
|
1035
|
+
function isInspectorRequestTokenValid(req, requestToken) {
|
|
1036
|
+
if (!requestToken) return true;
|
|
1037
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1038
|
+
return url.searchParams.get('__sunpeak_token') === requestToken;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1035
1041
|
function addInspectorRequestTokenToSimulations(simulations, requestToken) {
|
|
1036
1042
|
if (!requestToken || !simulations || typeof simulations !== 'object') return simulations;
|
|
1037
1043
|
return Object.fromEntries(
|
|
@@ -2073,12 +2079,7 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
2073
2079
|
}
|
|
2074
2080
|
|
|
2075
2081
|
function requireInspectorRequestToken(req, res) {
|
|
2076
|
-
if (
|
|
2077
|
-
const fetchSiteHeader = req.headers['sec-fetch-site'];
|
|
2078
|
-
const fetchSite = Array.isArray(fetchSiteHeader) ? fetchSiteHeader[0] : fetchSiteHeader;
|
|
2079
|
-
if (fetchSite !== 'cross-site') return true;
|
|
2080
|
-
const url = new URL(req.url, 'http://localhost');
|
|
2081
|
-
if (url.searchParams.get('__sunpeak_token') === pluginOpts.requestToken) return true;
|
|
2082
|
+
if (isInspectorRequestTokenValid(req, pluginOpts.requestToken)) return true;
|
|
2082
2083
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
2083
2084
|
res.end('Forbidden: missing or invalid inspector request token');
|
|
2084
2085
|
return false;
|
|
@@ -2847,8 +2848,9 @@ function sunpeakInspectEndpointsPlugin(getClient, setClient, pluginOpts = {}) {
|
|
|
2847
2848
|
const url = new URL(req.url, 'http://localhost');
|
|
2848
2849
|
const provider = url.searchParams.get('provider') || 'openai';
|
|
2849
2850
|
try {
|
|
2851
|
+
const status = await getApiKeyStatus(provider);
|
|
2850
2852
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2851
|
-
res.end(JSON.stringify(
|
|
2853
|
+
res.end(JSON.stringify(status));
|
|
2852
2854
|
} catch (err) {
|
|
2853
2855
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2854
2856
|
res.end(JSON.stringify({ hasKey: false, error: err.message }));
|
|
@@ -3089,6 +3091,7 @@ export const _securityTestExports = {
|
|
|
3089
3091
|
formatSharedAppContextForModel,
|
|
3090
3092
|
addInspectorRequestTokenToSimulations,
|
|
3091
3093
|
appendInspectorRequestToken,
|
|
3094
|
+
isInspectorRequestTokenValid,
|
|
3092
3095
|
normalizeApiKey,
|
|
3093
3096
|
normalizeModelChatMessages,
|
|
3094
3097
|
normalizeModelAppContext,
|
package/bin/commands/new.mjs
CHANGED
|
@@ -87,6 +87,7 @@ export const defaultDeps = {
|
|
|
87
87
|
renameSync,
|
|
88
88
|
execSync,
|
|
89
89
|
execAsync,
|
|
90
|
+
skipInstall: false,
|
|
90
91
|
promptName: defaultPromptName,
|
|
91
92
|
selectResources: defaultSelectResources,
|
|
92
93
|
selectProviders: defaultSelectProviders,
|
|
@@ -331,14 +332,16 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
331
332
|
d.writeFileSync(readmePath, readme);
|
|
332
333
|
}
|
|
333
334
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
335
|
+
if (!d.skipInstall) {
|
|
336
|
+
const s = d.spinner();
|
|
337
|
+
s.start(`Installing dependencies with ${pm}...`);
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
await d.execAsync(`${pm} install`, { cwd: targetDir });
|
|
341
|
+
s.stop(`Installed dependencies with ${pm}`);
|
|
342
|
+
} catch {
|
|
343
|
+
s.stop(`Install failed. You can try running "${pm} install" manually.`);
|
|
344
|
+
}
|
|
342
345
|
}
|
|
343
346
|
|
|
344
347
|
// Offer to configure eval providers (only in interactive mode)
|
package/bin/commands/test.mjs
CHANGED
|
@@ -34,9 +34,7 @@ export async function runTest(args) {
|
|
|
34
34
|
const isEval = args.includes('--eval');
|
|
35
35
|
let isVisual = args.includes('--visual');
|
|
36
36
|
const isUpdate = args.includes('--update');
|
|
37
|
-
const filteredArgs = args
|
|
38
|
-
(a) => !['--unit', '--e2e', '--live', '--eval', '--visual', '--update'].includes(a)
|
|
39
|
-
);
|
|
37
|
+
const filteredArgs = filterRunnerArgs(args);
|
|
40
38
|
|
|
41
39
|
// --update implies --visual (no point updating without enabling visual)
|
|
42
40
|
if (isUpdate) isVisual = true;
|
|
@@ -117,6 +115,12 @@ export async function runTest(args) {
|
|
|
117
115
|
process.exit(failed ? failed.code : 0);
|
|
118
116
|
}
|
|
119
117
|
|
|
118
|
+
export function filterRunnerArgs(args) {
|
|
119
|
+
return args.filter(
|
|
120
|
+
(a) => !['--unit', '--e2e', '--live', '--eval', '--visual', '--update', '--'].includes(a)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
120
124
|
/**
|
|
121
125
|
* Spawn a child process and return its exit code.
|
|
122
126
|
*/
|
|
@@ -151,7 +155,7 @@ function runPlaywright(args, options = {}) {
|
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
const configArgs = config ? ['--config', config] : [];
|
|
154
|
-
const extraArgs = updateSnapshots ? ['--update-snapshots'] : [];
|
|
158
|
+
const extraArgs = updateSnapshots ? ['--update-snapshots=all'] : [];
|
|
155
159
|
|
|
156
160
|
return runChild(
|
|
157
161
|
'pnpm',
|
|
@@ -11,7 +11,7 @@ import { HostPage } from './host-page.mjs';
|
|
|
11
11
|
/**
|
|
12
12
|
* All ChatGPT DOM selectors in one place for easy maintenance.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Updated: 2026-07-10 for the documented ChatGPT Plugins flow.
|
|
15
15
|
*/
|
|
16
16
|
const SELECTORS = {
|
|
17
17
|
// Chat interface
|
|
@@ -23,8 +23,9 @@ const SELECTORS = {
|
|
|
23
23
|
loggedInIndicator: '[data-testid="accounts-profile-button"]',
|
|
24
24
|
loginPage: 'button:has-text("Log in")',
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
|
|
26
|
+
// Plugin directory navigation
|
|
27
|
+
createdByYou:
|
|
28
|
+
'button:has-text("Created by you"), [role="tab"]:has-text("Created by you"), a:has-text("Created by you")',
|
|
28
29
|
refreshButton: 'button:has-text("Refresh")',
|
|
29
30
|
reconnectButton: 'button:has-text("Reconnect")',
|
|
30
31
|
|
|
@@ -40,40 +41,51 @@ const SELECTORS = {
|
|
|
40
41
|
|
|
41
42
|
const URLS = {
|
|
42
43
|
base: 'https://chatgpt.com',
|
|
43
|
-
|
|
44
|
+
plugins: 'https://chatgpt.com/plugins',
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
const PLUGIN_SETUP_HINT =
|
|
48
|
+
'Enable Developer mode from the bottom-left user menu under Settings > Security and login > Developer mode, then add the app from Plugins > +.';
|
|
49
|
+
|
|
46
50
|
export { SELECTORS as CHATGPT_SELECTORS, URLS as CHATGPT_URLS };
|
|
47
51
|
|
|
48
52
|
export class ChatGPTPage extends HostPage {
|
|
49
|
-
get hostId() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
get
|
|
53
|
+
get hostId() {
|
|
54
|
+
return 'chatgpt';
|
|
55
|
+
}
|
|
56
|
+
get hostName() {
|
|
57
|
+
return 'ChatGPT';
|
|
58
|
+
}
|
|
59
|
+
get selectors() {
|
|
60
|
+
return SELECTORS;
|
|
61
|
+
}
|
|
62
|
+
get urls() {
|
|
63
|
+
return URLS;
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
|
-
* Refresh the MCP server connection
|
|
56
|
-
*
|
|
67
|
+
* Refresh the MCP server connection from the ChatGPT Plugins directory.
|
|
68
|
+
* Opens Plugins, finds the developer-mode app, clicks Refresh,
|
|
57
69
|
* and waits for the success/error toast.
|
|
58
70
|
*/
|
|
59
71
|
async refreshMcpServer({ tunnelUrl, appName } = {}) {
|
|
60
|
-
await this.page.goto(URLS.
|
|
72
|
+
await this.page.goto(URLS.plugins, { waitUntil: 'domcontentloaded' });
|
|
61
73
|
await this.page.waitForTimeout(3_000);
|
|
62
74
|
|
|
63
75
|
const found = await this._findAndClickRefresh(appName);
|
|
64
76
|
|
|
65
77
|
if (!found) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
await
|
|
78
|
+
const createdByYou = this.page.locator(SELECTORS.createdByYou).first();
|
|
79
|
+
const hasCreatedByYou = await createdByYou.isVisible().catch(() => false);
|
|
80
|
+
if (hasCreatedByYou) {
|
|
81
|
+
await createdByYou.click();
|
|
70
82
|
await this.page.waitForTimeout(2_000);
|
|
71
83
|
const retryFound = await this._findAndClickRefresh(appName);
|
|
72
84
|
if (!retryFound) {
|
|
73
|
-
await this._screenshotAndThrow('refresh-mcp-
|
|
85
|
+
await this._screenshotAndThrow('refresh-mcp-plugin', tunnelUrl, PLUGIN_SETUP_HINT);
|
|
74
86
|
}
|
|
75
87
|
} else {
|
|
76
|
-
await this._screenshotAndThrow('
|
|
88
|
+
await this._screenshotAndThrow('developer-plugin-not-found', tunnelUrl, PLUGIN_SETUP_HINT);
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
|
|
@@ -82,7 +94,7 @@ export class ChatGPTPage extends HostPage {
|
|
|
82
94
|
if (hasError) {
|
|
83
95
|
throw new Error(
|
|
84
96
|
`MCP server refresh failed in ChatGPT:\n${errorText.trim()}\n\n` +
|
|
85
|
-
|
|
97
|
+
`Make sure your MCP dev server is running (pnpm dev) and your tunnel is active.`
|
|
86
98
|
);
|
|
87
99
|
}
|
|
88
100
|
|
|
@@ -107,13 +119,18 @@ export class ChatGPTPage extends HostPage {
|
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
// Wait for the outer sandbox iframe
|
|
110
|
-
await this.page
|
|
122
|
+
await this.page
|
|
123
|
+
.locator(SELECTORS.mcpAppOuterIframe)
|
|
124
|
+
.first()
|
|
125
|
+
.waitFor({ state: 'attached', timeout: 30_000 });
|
|
111
126
|
|
|
112
127
|
// Wait for the inner frame to appear inside the sandboxed outer iframe.
|
|
113
128
|
// waitForFunction can't cross the sandbox boundary, so use Playwright's frameLocator instead.
|
|
114
129
|
const outerFrame = this.page.frameLocator(SELECTORS.mcpAppOuterIframe).first();
|
|
115
130
|
await outerFrame
|
|
116
|
-
.locator(
|
|
131
|
+
.locator(
|
|
132
|
+
`iframe[name="${SELECTORS.mcpAppInnerFrameName}"], iframe#${SELECTORS.mcpAppInnerFrameName}`
|
|
133
|
+
)
|
|
117
134
|
.waitFor({ state: 'attached', timeout: 15_000 });
|
|
118
135
|
|
|
119
136
|
const appFrame = this.getAppIframe();
|
|
@@ -182,13 +199,11 @@ export class ChatGPTPage extends HostPage {
|
|
|
182
199
|
return false;
|
|
183
200
|
};
|
|
184
201
|
|
|
185
|
-
if (await tryClickRefresh()) return true;
|
|
186
|
-
|
|
187
202
|
if (appName) {
|
|
188
203
|
const strategies = [
|
|
189
204
|
() => this.page.getByText(appName, { exact: true }).first(),
|
|
190
|
-
() => this.page.
|
|
191
|
-
() => this.page.
|
|
205
|
+
() => this.page.getByRole('link', { name: appName, exact: true }).first(),
|
|
206
|
+
() => this.page.getByRole('button', { name: appName, exact: true }).first(),
|
|
192
207
|
];
|
|
193
208
|
|
|
194
209
|
for (const getLocator of strategies) {
|
|
@@ -205,6 +220,6 @@ export class ChatGPTPage extends HostPage {
|
|
|
205
220
|
}
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
return false;
|
|
223
|
+
return appName ? false : tryClickRefresh();
|
|
209
224
|
}
|
|
210
225
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Runs exactly once before all workers. Two responsibilities:
|
|
5
5
|
* 1. Authenticate — launch a browser, verify login, wait for user if needed.
|
|
6
|
-
* 2. Refresh MCP server
|
|
6
|
+
* 2. Refresh MCP server: open the host's app management UI and click Refresh so
|
|
7
7
|
* all workers start with pre-loaded resources.
|
|
8
8
|
*
|
|
9
9
|
* Auth approach:
|
|
@@ -150,7 +150,7 @@ export default async function globalSetup() {
|
|
|
150
150
|
// Refresh MCP server in the SAME browser session.
|
|
151
151
|
// This is critical — Cloudflare's cf_clearance cookie is HttpOnly and
|
|
152
152
|
// won't be in the saved storageState. By refreshing here, the cookie
|
|
153
|
-
// is still valid for navigating to
|
|
153
|
+
// is still valid for navigating to the plugin directory.
|
|
154
154
|
//
|
|
155
155
|
// This MUST succeed — if the MCP server isn't reachable or the refresh
|
|
156
156
|
// fails, tests will fail with confusing iframe/timeout errors.
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
/**
|
|
24
24
|
* @typedef {Object} HostUrls
|
|
25
25
|
* @property {string} base - Host base URL (e.g., 'https://chatgpt.com')
|
|
26
|
-
* @property {string}
|
|
27
|
-
* @property {string} loginTestId - Test ID for login detection (if using getByTestId)
|
|
26
|
+
* @property {string} [plugins] - Host plugin or app management URL
|
|
28
27
|
*/
|
|
29
28
|
|
|
30
29
|
export class HostPage {
|
|
@@ -80,7 +79,8 @@ export class HostPage {
|
|
|
80
79
|
if (warnings.length > 0) {
|
|
81
80
|
console.warn(
|
|
82
81
|
`\n⚠️ ${this.hostName} DOM may have changed — update selectors in ${this.hostId}-page.mjs:\n` +
|
|
83
|
-
|
|
82
|
+
warnings.join('\n') +
|
|
83
|
+
'\n'
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -135,14 +135,14 @@ export class HostPage {
|
|
|
135
135
|
// Not logged in. Wait for the user to authenticate in this browser window.
|
|
136
136
|
console.log(
|
|
137
137
|
`\n` +
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
`╔══════════════════════════════════════════════════════════════╗\n` +
|
|
139
|
+
`║ Not logged into ${this.hostName.padEnd(42)}║\n` +
|
|
140
|
+
`║ ║\n` +
|
|
141
|
+
`║ Please log in at: ${this.urls.base.padEnd(39)}║\n` +
|
|
142
|
+
`║ in the browser window that just opened. ║\n` +
|
|
143
|
+
`║ ║\n` +
|
|
144
|
+
`║ Waiting up to 3 minutes... ║\n` +
|
|
145
|
+
`╚══════════════════════════════════════════════════════════════╝\n`
|
|
146
146
|
);
|
|
147
147
|
|
|
148
148
|
// Poll for login — the user may need to pass Cloudflare + enter credentials
|
|
@@ -160,14 +160,14 @@ export class HostPage {
|
|
|
160
160
|
|
|
161
161
|
throw new Error(
|
|
162
162
|
`Login to ${this.hostName} timed out after 3 minutes.\n` +
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
`Please log in at ${this.urls.base} in the browser window and try again.\n` +
|
|
164
|
+
'If the session expired, delete the .auth/ directory and try again.'
|
|
165
165
|
);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* Refresh the MCP server connection in host
|
|
170
|
-
* Subclasses must implement this
|
|
169
|
+
* Refresh the MCP server connection in the host's app management UI.
|
|
170
|
+
* Subclasses must implement this because each host has a different flow.
|
|
171
171
|
*
|
|
172
172
|
* @param {Object} [options]
|
|
173
173
|
* @param {string} [options.tunnelUrl] - Tunnel URL for error messages
|
|
@@ -228,9 +228,10 @@ export class HostPage {
|
|
|
228
228
|
* Capture a debug screenshot and throw with helpful message.
|
|
229
229
|
* @param {string} context - Context label for the screenshot filename
|
|
230
230
|
* @param {string} [tunnelUrl] - Tunnel URL for the error message
|
|
231
|
+
* @param {string} [setupHint] - Host-specific setup steps
|
|
231
232
|
* @protected
|
|
232
233
|
*/
|
|
233
|
-
async _screenshotAndThrow(context, tunnelUrl) {
|
|
234
|
+
async _screenshotAndThrow(context, tunnelUrl, setupHint) {
|
|
234
235
|
const screenshotPath = `/tmp/sunpeak-live-debug-${this.hostId}-${context}.png`;
|
|
235
236
|
try {
|
|
236
237
|
await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
@@ -241,29 +242,34 @@ export class HostPage {
|
|
|
241
242
|
|
|
242
243
|
try {
|
|
243
244
|
const buttons = await this.page.locator('button').allTextContents();
|
|
244
|
-
console.error('Visible buttons on page:', buttons.filter(t => t.trim()).join(', '));
|
|
245
|
+
console.error('Visible buttons on page:', buttons.filter((t) => t.trim()).join(', '));
|
|
245
246
|
} catch {
|
|
246
247
|
// Best effort
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
throw new Error(
|
|
250
|
-
`Could not find Refresh/Reconnect
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
`Could not find Refresh/Reconnect in ${this.hostName} app management.\n` +
|
|
252
|
+
`Make sure your MCP server is added to ${this.hostName}` +
|
|
253
|
+
(tunnelUrl ? ` with URL: ${tunnelUrl}/mcp` : '') +
|
|
254
|
+
(setupHint ? `\n${setupHint}` : '') +
|
|
255
|
+
`\n\nDebug screenshot: ${screenshotPath}`
|
|
254
256
|
);
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
/**
|
|
258
260
|
* Wait for a toast/alert banner and check for errors.
|
|
259
|
-
* Many hosts show success/error toasts after
|
|
261
|
+
* Many hosts show success/error toasts after app management actions.
|
|
260
262
|
* @param {Object} [options]
|
|
261
263
|
* @param {string} [options.alertSelector='[role="alert"]'] - Selector for toast elements
|
|
262
264
|
* @param {number} [options.timeout=30000] - Max time to wait
|
|
263
265
|
* @param {number} [options.minTextLength=5] - Minimum text length to consider as a real toast
|
|
264
266
|
* @protected
|
|
265
267
|
*/
|
|
266
|
-
async _waitForToast({
|
|
268
|
+
async _waitForToast({
|
|
269
|
+
alertSelector = '[role="alert"]',
|
|
270
|
+
timeout = 30_000,
|
|
271
|
+
minTextLength = 5,
|
|
272
|
+
} = {}) {
|
|
267
273
|
try {
|
|
268
274
|
await this.page.waitForFunction(
|
|
269
275
|
({ selector, minLen }) => {
|
|
@@ -275,7 +281,7 @@ export class HostPage {
|
|
|
275
281
|
return false;
|
|
276
282
|
},
|
|
277
283
|
{ selector: alertSelector, minLen: minTextLength },
|
|
278
|
-
{ timeout }
|
|
284
|
+
{ timeout }
|
|
279
285
|
);
|
|
280
286
|
} catch {
|
|
281
287
|
console.warn('No toast detected — assuming success.');
|
package/bin/lib/resolve-bin.mjs
CHANGED
|
@@ -37,3 +37,21 @@ export function resolveSunpeakBin() {
|
|
|
37
37
|
|
|
38
38
|
return 'sunpeak';
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find the JavaScript entry for the sunpeak CLI when sunpeak is installed
|
|
43
|
+
* locally. Returns null when only a global/bare binary is available.
|
|
44
|
+
*
|
|
45
|
+
* @returns {string | null} Path to bin/sunpeak.js, or null as fallback
|
|
46
|
+
*/
|
|
47
|
+
export function resolveSunpeakJsBin() {
|
|
48
|
+
const cwdJsBin = join(process.cwd(), 'node_modules', 'sunpeak', 'bin', 'sunpeak.js');
|
|
49
|
+
if (existsSync(cwdJsBin)) return cwdJsBin;
|
|
50
|
+
|
|
51
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
const pkgRoot = join(__dirname, '..', '..');
|
|
53
|
+
const packageJsBin = join(pkgRoot, 'bin', 'sunpeak.js');
|
|
54
|
+
if (existsSync(packageJsBin)) return packageJsBin;
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
@@ -20,6 +20,7 @@ export function createBaseConfig({ hosts, testDir, webServer, port, use, globalS
|
|
|
20
20
|
// Separate snapshot path from other visual options passed to expect.toHaveScreenshot
|
|
21
21
|
const { snapshotPathTemplate, ...toHaveScreenshotDefaults } = visual ?? {};
|
|
22
22
|
const workerOverride = parsePositiveInt(process.env.SUNPEAK_TEST_WORKERS);
|
|
23
|
+
const webServerTimeout = parsePositiveInt(process.env.SUNPEAK_WEB_SERVER_TIMEOUT_MS);
|
|
23
24
|
|
|
24
25
|
return {
|
|
25
26
|
...(globalSetup ? { globalSetup } : {}),
|
|
@@ -27,6 +28,7 @@ export function createBaseConfig({ hosts, testDir, webServer, port, use, globalS
|
|
|
27
28
|
fullyParallel: true,
|
|
28
29
|
forbidOnly: !!process.env.CI,
|
|
29
30
|
retries: process.env.CI ? 2 : 1,
|
|
31
|
+
timeout: parsePositiveInt(process.env.SUNPEAK_TEST_TIMEOUT_MS) ?? 60_000,
|
|
30
32
|
// Limit workers to avoid overwhelming the double-iframe sandbox proxy.
|
|
31
33
|
workers: workerOverride ?? (process.env.CI ? 1 : 2),
|
|
32
34
|
reporter: 'list',
|
|
@@ -54,7 +56,7 @@ export function createBaseConfig({ hosts, testDir, webServer, port, use, globalS
|
|
|
54
56
|
command: webServer.command,
|
|
55
57
|
url: webServer.healthUrl,
|
|
56
58
|
reuseExistingServer: !process.env.CI,
|
|
57
|
-
timeout: timeout ?? 60_000,
|
|
59
|
+
timeout: timeout ?? webServerTimeout ?? 60_000,
|
|
58
60
|
},
|
|
59
61
|
};
|
|
60
62
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { existsSync, readFileSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
import { createBaseConfig, resolvePorts } from './base-config.mjs';
|
|
21
|
-
import { resolveSunpeakBin } from '../resolve-bin.mjs';
|
|
21
|
+
import { resolveSunpeakBin, resolveSunpeakJsBin } from '../resolve-bin.mjs';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* @param {Object} [options]
|
|
@@ -60,7 +60,7 @@ export function defineConfig(options = {}) {
|
|
|
60
60
|
command = buildInspectCommand({ server, port, sandboxPort, simulationsDir });
|
|
61
61
|
} else if (isSunpeakProject) {
|
|
62
62
|
// sunpeak framework project mode
|
|
63
|
-
command = `PORT=${port} SUNPEAK_SANDBOX_PORT=${sandboxPort}
|
|
63
|
+
command = `PORT=${port} SUNPEAK_SANDBOX_PORT=${sandboxPort} ${buildSunpeakCommand('dev')}`;
|
|
64
64
|
} else {
|
|
65
65
|
throw new Error(
|
|
66
66
|
'defineConfig: either provide a `server` option or run from a sunpeak project directory.'
|
|
@@ -142,6 +142,15 @@ function buildInspectCommand({ server, port, sandboxPort, simulationsDir }) {
|
|
|
142
142
|
return parts.join(' ');
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
function buildSunpeakCommand(subcommand) {
|
|
146
|
+
const jsBin = resolveSunpeakJsBin();
|
|
147
|
+
if (jsBin) {
|
|
148
|
+
return `${shellQuote(process.execPath)} ${shellQuote(jsBin)} ${subcommand}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return `${shellQuote(resolveSunpeakBin())} ${subcommand}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
145
154
|
function shellQuote(value) {
|
|
146
155
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
147
156
|
}
|
|
@@ -67,6 +67,14 @@ async function fetchJson(page, path) {
|
|
|
67
67
|
return response.json();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
async function getInspectorRequestToken(page) {
|
|
71
|
+
const baseURL = page.context()._options?.baseURL || '';
|
|
72
|
+
const response = await page.request.get(`${baseURL}/@id/__x00__virtual:sunpeak-inspect-entry`);
|
|
73
|
+
if (!response.ok()) return null;
|
|
74
|
+
const source = await response.text();
|
|
75
|
+
return source.match(/[?&]__sunpeak_token=([^"'&\\]+)/)?.[1] ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
/**
|
|
71
79
|
* Read the tool result from the inspector's <script id="__tool-result"> element.
|
|
72
80
|
*/
|
|
@@ -199,9 +207,10 @@ const test = base.extend({
|
|
|
199
207
|
|
|
200
208
|
async readResource(uri) {
|
|
201
209
|
const baseURL = page.context()._options?.baseURL || '';
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
);
|
|
210
|
+
const params = new URLSearchParams({ uri });
|
|
211
|
+
const requestToken = await getInspectorRequestToken(page);
|
|
212
|
+
if (requestToken) params.set('__sunpeak_token', requestToken);
|
|
213
|
+
const response = await page.request.get(`${baseURL}/__sunpeak/read-resource?${params}`);
|
|
205
214
|
if (!response.ok()) {
|
|
206
215
|
throw new Error(
|
|
207
216
|
`readResource(${uri}) returned ${response.status()}: ${await response.text()}`
|
package/bin/sunpeak.js
CHANGED
|
@@ -51,8 +51,10 @@ function getVersion() {
|
|
|
51
51
|
switch (command) {
|
|
52
52
|
case 'new':
|
|
53
53
|
{
|
|
54
|
-
const { init } = await import(commandUrl('new.mjs'));
|
|
55
|
-
|
|
54
|
+
const { defaultDeps, init } = await import(commandUrl('new.mjs'));
|
|
55
|
+
const skipInstall = args.includes('--skip-install');
|
|
56
|
+
const positionalArgs = args.filter((arg) => arg !== '--skip-install');
|
|
57
|
+
await init(positionalArgs[0], positionalArgs[1], { ...defaultDeps, skipInstall });
|
|
56
58
|
}
|
|
57
59
|
break;
|
|
58
60
|
|
package/dist/chatgpt/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("../rolldown-runtime-Bd6kNlEP.cjs");
|
|
3
|
-
const require_inspector = require("../inspector-
|
|
3
|
+
const require_inspector = require("../inspector-Cl5w_7yQ.cjs");
|
|
4
4
|
const require_inspector_url = require("../inspector-url-C0I97f8-.cjs");
|
|
5
5
|
const require_discovery = require("../discovery-31_n0zcu.cjs");
|
|
6
6
|
//#region src/chatgpt/index.ts
|
package/dist/chatgpt/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Ct as __exportAll } from "../protocol-fGgeFIiL.js";
|
|
2
|
-
import { C as McpAppHost, S as extractResourceCSP, f as ThemeProvider, p as useThemeContext, r as resolveServerToolResult, t as Inspector, w as SCREEN_WIDTHS, x as IframeResource } from "../inspector-
|
|
2
|
+
import { C as McpAppHost, S as extractResourceCSP, f as ThemeProvider, p as useThemeContext, r as resolveServerToolResult, t as Inspector, w as SCREEN_WIDTHS, x as IframeResource } from "../inspector-DPJeSQAd.js";
|
|
3
3
|
import { t as createInspectorUrl } from "../inspector-url-vtW8g8yB.js";
|
|
4
4
|
import { c as toPascalCase, i as findResourceKey, n as extractSimulationKey, r as findResourceDirs, s as getComponentName, t as extractResourceKey } from "../discovery-DOVner--.js";
|
|
5
5
|
//#region src/chatgpt/index.ts
|
package/dist/claude/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
require("../rolldown-runtime-Bd6kNlEP.cjs");
|
|
3
|
-
const require_inspector = require("../inspector-
|
|
3
|
+
const require_inspector = require("../inspector-Cl5w_7yQ.cjs");
|
|
4
4
|
exports.Inspector = require_inspector.Inspector;
|
package/dist/claude/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as Inspector } from "../inspector-
|
|
1
|
+
import { t as Inspector } from "../inspector-DPJeSQAd.js";
|
|
2
2
|
export { Inspector };
|