sunpeak 0.20.1 → 0.20.5

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.
Files changed (59) hide show
  1. package/README.md +59 -89
  2. package/bin/commands/inspect.mjs +142 -13
  3. package/bin/commands/new.mjs +33 -9
  4. package/bin/commands/test-init.mjs +113 -100
  5. package/bin/commands/test.mjs +7 -2
  6. package/bin/lib/eval/eval-runner.mjs +7 -1
  7. package/bin/lib/inspect/inspect-config.mjs +1 -1
  8. package/bin/lib/live/live-config.d.mts +10 -0
  9. package/bin/lib/live/live-config.mjs +34 -2
  10. package/bin/lib/test/base-config.mjs +3 -1
  11. package/bin/lib/test/test-config.mjs +1 -1
  12. package/bin/sunpeak.js +16 -15
  13. package/dist/chatgpt/index.cjs +1 -1
  14. package/dist/chatgpt/index.js +1 -1
  15. package/dist/claude/index.cjs +1 -1
  16. package/dist/claude/index.js +1 -1
  17. package/dist/host/chatgpt/index.cjs +1 -1
  18. package/dist/host/chatgpt/index.js +1 -1
  19. package/dist/index.cjs +3 -3
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +3 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/inspector/index.cjs +1 -1
  24. package/dist/inspector/index.js +1 -1
  25. package/dist/{inspector-BBDa5yCm.js → inspector-60Na_Zc4.js} +2 -2
  26. package/dist/inspector-60Na_Zc4.js.map +1 -0
  27. package/dist/{inspector-DAA1Wiyh.cjs → inspector-D0qOqYX2.cjs} +2 -2
  28. package/dist/{inspector-BBDa5yCm.js.map → inspector-D0qOqYX2.cjs.map} +1 -1
  29. package/dist/mcp/index.cjs +1 -1
  30. package/dist/mcp/index.cjs.map +1 -1
  31. package/dist/mcp/index.js +1 -1
  32. package/dist/mcp/index.js.map +1 -1
  33. package/dist/{use-app-DPkj5Jp_.cjs → use-app-B33mckz4.cjs} +7 -3
  34. package/dist/use-app-B33mckz4.cjs.map +1 -0
  35. package/dist/{use-app-Cr0auUa1.js → use-app-kv5GQr0G.js} +7 -3
  36. package/dist/use-app-kv5GQr0G.js.map +1 -0
  37. package/package.json +3 -3
  38. package/template/README.md +21 -23
  39. package/template/dist/albums/albums.html +1 -1
  40. package/template/dist/albums/albums.json +1 -1
  41. package/template/dist/carousel/carousel.html +1 -1
  42. package/template/dist/carousel/carousel.json +1 -1
  43. package/template/dist/map/map.html +1 -1
  44. package/template/dist/map/map.json +1 -1
  45. package/template/dist/review/review.html +1 -1
  46. package/template/dist/review/review.json +1 -1
  47. package/template/node_modules/.vite/deps/_metadata.json +3 -3
  48. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js +6 -2
  49. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps.js.map +1 -1
  50. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js +1 -1
  51. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_app-bridge.js.map +1 -1
  52. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js +6 -2
  53. package/template/node_modules/.vite-mcp/deps/@modelcontextprotocol_ext-apps_react.js.map +1 -1
  54. package/template/node_modules/.vite-mcp/deps/_metadata.json +22 -22
  55. package/template/package.json +2 -1
  56. package/template/tests/e2e/visual.spec.ts +2 -2
  57. package/dist/inspector-DAA1Wiyh.cjs.map +0 -1
  58. package/dist/use-app-Cr0auUa1.js.map +0 -1
  59. package/dist/use-app-DPkj5Jp_.cjs.map +0 -1
@@ -40,6 +40,7 @@ export const defaultDeps = {
40
40
  mkdirSync,
41
41
  execSync,
42
42
  cwd: () => process.cwd(),
43
+ isTTY: () => !!process.stdin.isTTY,
43
44
  intro: p.intro,
44
45
  outro: p.outro,
45
46
  confirm: p.confirm,
@@ -80,6 +81,7 @@ export async function testInit(args = [], deps = defaultDeps) {
80
81
  : undefined;
81
82
 
82
83
  const projectType = detectProjectType(d);
84
+ const interactive = d.isTTY();
83
85
 
84
86
  if (projectType === 'sunpeak') {
85
87
  await initSunpeakProject(d);
@@ -89,74 +91,78 @@ export async function testInit(args = [], deps = defaultDeps) {
89
91
  await initExternalProject(cliServer, d);
90
92
  }
91
93
 
92
- // Offer to configure eval providers
93
- const providers = await d.selectProviders();
94
- if (!d.isCancel(providers) && providers.length > 0) {
95
- const pm = d.detectPackageManager();
96
- const pkgsToInstall = ['ai', ...providers.map((p) => p.pkg)];
97
- const installCmd = `${pm} add -D ${pkgsToInstall.join(' ')}`;
98
- try {
99
- d.execSync(installCmd, { cwd: d.cwd(), stdio: 'inherit' });
100
- } catch {
101
- d.log.info(`Provider install failed. Install manually: ${installCmd}`);
102
- }
94
+ // Offer to configure eval providers (skip without a TTY — prompts can't work)
95
+ if (interactive) {
96
+ const providers = await d.selectProviders();
97
+ if (!d.isCancel(providers) && providers.length > 0) {
98
+ const pm = d.detectPackageManager();
99
+ const pkgsToInstall = ['ai', ...providers.map((p) => p.pkg)];
100
+ const installCmd = `${pm} add -D ${pkgsToInstall.join(' ')}`;
101
+ try {
102
+ d.execSync(installCmd, { cwd: d.cwd(), stdio: 'inherit' });
103
+ } catch {
104
+ d.log.info(`Provider install failed. Install manually: ${installCmd}`);
105
+ }
103
106
 
104
- // Uncomment selected models in eval.config.ts
105
- const evalDir = d.existsSync(join(d.cwd(), 'tests', 'evals'))
106
- ? join(d.cwd(), 'tests', 'evals')
107
- : d.existsSync(join(d.cwd(), 'tests', 'sunpeak', 'evals'))
108
- ? join(d.cwd(), 'tests', 'sunpeak', 'evals')
109
- : null;
110
- if (evalDir) {
111
- const configPath = join(evalDir, 'eval.config.ts');
112
- if (d.existsSync(configPath)) {
113
- let config = d.readFileSync(configPath, 'utf-8');
114
- for (const prov of providers) {
115
- for (const model of prov.models) {
116
- config = config.replace(
117
- new RegExp(`^(\\s*)// ('${model.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}',?.*)$`, 'm'),
118
- '$1$2'
119
- );
107
+ // Uncomment selected models in eval.config.ts
108
+ const evalDir = d.existsSync(join(d.cwd(), 'tests', 'evals'))
109
+ ? join(d.cwd(), 'tests', 'evals')
110
+ : d.existsSync(join(d.cwd(), 'tests', 'sunpeak', 'evals'))
111
+ ? join(d.cwd(), 'tests', 'sunpeak', 'evals')
112
+ : null;
113
+ if (evalDir) {
114
+ const configPath = join(evalDir, 'eval.config.ts');
115
+ if (d.existsSync(configPath)) {
116
+ let config = d.readFileSync(configPath, 'utf-8');
117
+ for (const prov of providers) {
118
+ for (const model of prov.models) {
119
+ config = config.replace(
120
+ new RegExp(`^(\\s*)// ('${model.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}',?.*)$`, 'm'),
121
+ '$1$2'
122
+ );
123
+ }
120
124
  }
125
+ d.writeFileSync(configPath, config);
121
126
  }
122
- d.writeFileSync(configPath, config);
123
- }
124
127
 
125
- // Prompt for API keys and write .env
126
- const envLines = [];
127
- const seen = new Set();
128
- for (const prov of providers) {
129
- if (seen.has(prov.envVar)) continue;
130
- seen.add(prov.envVar);
131
- const key = await d.password({
132
- message: `${prov.envVar} (enter to skip)`,
133
- mask: '*',
134
- });
135
- if (!d.isCancel(key) && key) {
136
- envLines.push(`${prov.envVar}=${key}`);
128
+ // Prompt for API keys and write .env
129
+ const envLines = [];
130
+ const seen = new Set();
131
+ for (const prov of providers) {
132
+ if (seen.has(prov.envVar)) continue;
133
+ seen.add(prov.envVar);
134
+ const key = await d.password({
135
+ message: `${prov.envVar} (enter to skip)`,
136
+ mask: '*',
137
+ });
138
+ if (!d.isCancel(key) && key) {
139
+ envLines.push(`${prov.envVar}=${key}`);
140
+ }
141
+ }
142
+ if (envLines.length > 0 && evalDir) {
143
+ const relEnvPath = evalDir.startsWith(d.cwd()) ? evalDir.slice(d.cwd().length + 1) : evalDir;
144
+ d.writeFileSync(join(evalDir, '.env'), envLines.join('\n') + '\n');
145
+ d.log.info(`API keys saved to ${relEnvPath}/.env (gitignored)`);
137
146
  }
138
- }
139
- if (envLines.length > 0 && evalDir) {
140
- const relEnvPath = evalDir.startsWith(d.cwd()) ? evalDir.slice(d.cwd().length + 1) : evalDir;
141
- d.writeFileSync(join(evalDir, '.env'), envLines.join('\n') + '\n');
142
- d.log.info(`API keys saved to ${relEnvPath}/.env (gitignored)`);
143
147
  }
144
148
  }
145
- }
146
149
 
147
- // Offer to install the testing skill
148
- const installSkill = await d.confirm({
149
- message: 'Install the test-mcp-server skill? (helps your coding agent write tests)',
150
- initialValue: true,
151
- });
152
- if (!d.isCancel(installSkill) && installSkill) {
153
- try {
154
- d.execSync('pnpm dlx skills add Sunpeak-AI/sunpeak@test-mcp-server', {
155
- cwd: d.cwd(),
156
- stdio: 'inherit',
157
- });
158
- } catch {
159
- d.log.info('Skill install skipped. Install later: pnpm dlx skills add Sunpeak-AI/sunpeak@test-mcp-server');
150
+ // Offer to install the testing skill
151
+ const installSkill = await d.confirm({
152
+ message: 'Install the test-mcp-server skill? (helps your coding agent write tests)',
153
+ initialValue: true,
154
+ });
155
+ if (!d.isCancel(installSkill) && installSkill) {
156
+ const pm = d.detectPackageManager();
157
+ const dlx = pm === 'yarn' ? 'yarn dlx' : pm === 'npm' ? 'npx' : 'pnpm dlx';
158
+ try {
159
+ d.execSync(`${dlx} skills add Sunpeak-AI/sunpeak@test-mcp-server`, {
160
+ cwd: d.cwd(),
161
+ stdio: 'inherit',
162
+ });
163
+ } catch {
164
+ d.log.info(`Skill install skipped. Install later: ${dlx} skills add Sunpeak-AI/sunpeak@test-mcp-server`);
165
+ }
160
166
  }
161
167
  }
162
168
 
@@ -191,6 +197,11 @@ async function getServerConfig(cliServer, d) {
191
197
  return { type: 'command', value: cliServer };
192
198
  }
193
199
 
200
+ // Without a TTY, interactive prompts can't work — default to "configure later".
201
+ if (!d.isTTY()) {
202
+ return { type: 'later' };
203
+ }
204
+
194
205
  const serverType = await d.select({
195
206
  message: 'How does your MCP server start?',
196
207
  options: [
@@ -348,7 +359,7 @@ function scaffoldEvals(evalsDir, { server, isSunpeak, d: deps } = {}) {
348
359
  * 2. Install the AI SDK and provider packages (e.g. pnpm add ai @ai-sdk/openai)
349
360
  * 3. Copy .env.example to .env and add your API keys
350
361
  * 4. Replace this file with evals for your own tools
351
- * 5. Run: sunpeak test --eval
362
+ * 5. Run: npx sunpeak test --eval
352
363
  *
353
364
  * Each case sends a prompt to every configured model and checks
354
365
  * that the model calls the expected tool with the expected arguments.
@@ -394,10 +405,10 @@ function scaffoldVisualTest(filePath, d) {
394
405
  /**
395
406
  * Visual regression tests — compare screenshots against saved baselines.
396
407
  *
397
- * Screenshots only run with: sunpeak test --visual
398
- * Update baselines with: sunpeak test --visual --update
408
+ * Screenshots only run with: npx sunpeak test --visual
409
+ * Update baselines with: npx sunpeak test --visual --update
399
410
  *
400
- * During normal \`sunpeak test\` runs, screenshot() calls are silently
411
+ * During normal \`npx sunpeak test\` runs, screenshot() calls are silently
401
412
  * skipped so these tests still pass without baselines.
402
413
  *
403
414
  * Uncomment the tests below and replace 'your-tool' with your tool name.
@@ -437,9 +448,9 @@ function scaffoldVisualTest(filePath, d) {
437
448
  /**
438
449
  * Scaffold live test boilerplate (test against real ChatGPT/Claude).
439
450
  * @param {string} liveDir - Directory to create live test files in
440
- * @param {{ isSunpeak?: boolean, d: object }} options
451
+ * @param {{ isSunpeak?: boolean, server?: object, d: object }} options
441
452
  */
442
- function scaffoldLiveTests(liveDir, { isSunpeak, d } = {}) {
453
+ function scaffoldLiveTests(liveDir, { isSunpeak, server, d } = {}) {
443
454
  if (d.existsSync(join(liveDir, 'playwright.config.ts'))) {
444
455
  d.log.info('Live test config already exists. Skipping live test scaffold.');
445
456
  return;
@@ -456,12 +467,27 @@ function scaffoldLiveTests(liveDir, { isSunpeak, d } = {}) {
456
467
  * Prerequisites:
457
468
  * 1. Your MCP server must be accessible via a public URL (e.g., ngrok tunnel)
458
469
  * 2. The server must be registered as an MCP action in the host
459
- * 3. Run: sunpeak test --live
470
+ * 3. Run: npx sunpeak test --live
460
471
  *
461
472
  * On first run, a browser window opens for you to log in to the host.
462
- * The session is saved for subsequent runs (typically lasts a few hours).`;
473
+ * The session is saved for subsequent runs (typically lasts a few hours).
474
+ */`;
475
+
476
+ // Build the server option for non-sunpeak projects
477
+ let serverOption = '';
478
+ if (!isSunpeak && server?.type === 'url') {
479
+ serverOption = `\n server: { url: '${server.value}' },`;
480
+ } else if (!isSunpeak && server?.type === 'command') {
481
+ const parts = server.value.split(/\s+/);
482
+ const cmd = parts[0];
483
+ const args = parts.slice(1);
484
+ serverOption = args.length > 0
485
+ ? `\n server: { command: '${cmd}', args: [${args.map(a => `'${a}'`).join(', ')}] },`
486
+ : `\n server: { command: '${cmd}' },`;
487
+ }
463
488
 
464
- const liveConfigExport = `export default defineLiveConfig({
489
+ const configContent = `${liveConfigPreamble}
490
+ export default defineLiveConfig({${serverOption}
465
491
  // hosts: ['chatgpt'], // Which hosts to test against
466
492
  // colorScheme: 'light', // Default color scheme
467
493
  // viewport: { width: 1280, height: 720 },
@@ -469,19 +495,6 @@ function scaffoldLiveTests(liveDir, { isSunpeak, d } = {}) {
469
495
  });
470
496
  `;
471
497
 
472
- const configContent = isSunpeak
473
- ? `${liveConfigPreamble}
474
- */
475
- ${liveConfigExport}`
476
- : `${liveConfigPreamble}
477
- *
478
- * NOTE: defineLiveConfig() starts a local sunpeak dev server as its backend.
479
- * If your MCP server is not a sunpeak project, you may need to customize the
480
- * webServer option in the Playwright config below to start your own server,
481
- * or remove webServer entirely if your server is already running.
482
- */
483
- ${liveConfigExport}`;
484
-
485
498
  d.writeFileSync(join(liveDir, 'playwright.config.ts'), configContent);
486
499
 
487
500
  // Live test example
@@ -497,9 +510,9 @@ ${liveConfigExport}`;
497
510
  * - live.setColorScheme('dark', app) — switch theme while app is visible
498
511
  * - live.page — the underlying Playwright page
499
512
  *
500
- * Run with: sunpeak test --live
513
+ * Run with: npx sunpeak test --live
501
514
  *
502
- * These tests are excluded from normal \`sunpeak test\` runs because
515
+ * These tests are excluded from normal \`npx sunpeak test\` runs because
503
516
  * they require host accounts and cost API credits.
504
517
  */
505
518
 
@@ -542,7 +555,7 @@ function scaffoldUnitTest(filePath, d) {
542
555
  * Import your tool handler directly and test its input/output
543
556
  * without starting the MCP server or inspector.
544
557
  *
545
- * Run with: sunpeak test --unit
558
+ * Run with: npx sunpeak test --unit
546
559
  *
547
560
  * To set up vitest, add it to your devDependencies:
548
561
  * npm install -D vitest
@@ -663,7 +676,7 @@ test('server exposes tools', async ({ mcp }) => {
663
676
  scaffoldVisualTest(join(testDir, 'visual.test.ts'), d);
664
677
 
665
678
  // 3. Live tests
666
- scaffoldLiveTests(join(testDir, 'live'), { isSunpeak: false, d });
679
+ scaffoldLiveTests(join(testDir, 'live'), { isSunpeak: false, server, d });
667
680
 
668
681
  // 4. Eval boilerplate
669
682
  scaffoldEvals(join(testDir, 'evals'), { server, d });
@@ -690,10 +703,10 @@ test('server exposes tools', async ({ mcp }) => {
690
703
  }
691
704
 
692
705
  d.log.step('Ready! Run tests with:');
693
- d.log.message(' sunpeak test # E2E tests');
694
- d.log.message(' sunpeak test --visual # Visual regression (generates baselines on first run)');
695
- d.log.message(' sunpeak test --live # Live tests against real hosts (requires login)');
696
- d.log.message(' sunpeak test --eval # Multi-model evals (configure models in evals/eval.config.ts)');
706
+ d.log.message(' npx sunpeak test # E2E tests');
707
+ d.log.message(' npx sunpeak test --visual # Visual regression (generates baselines on first run)');
708
+ d.log.message(' npx sunpeak test --live # Live tests against real hosts (requires login)');
709
+ d.log.message(' npx sunpeak test --eval # Multi-model evals (configure models in evals/eval.config.ts)');
697
710
  }
698
711
 
699
712
  async function initJsProject(cliServer, d) {
@@ -757,7 +770,7 @@ test('server exposes tools', async ({ mcp }) => {
757
770
  scaffoldVisualTest(join(e2eDir, 'visual.test.ts'), d);
758
771
 
759
772
  // 3. Live tests
760
- scaffoldLiveTests(join(cwd, 'tests', 'live'), { isSunpeak: false, d });
773
+ scaffoldLiveTests(join(cwd, 'tests', 'live'), { isSunpeak: false, server, d });
761
774
 
762
775
  // 4. Eval boilerplate
763
776
  scaffoldEvals(join(cwd, 'tests', 'evals'), { server, d });
@@ -773,11 +786,11 @@ test('server exposes tools', async ({ mcp }) => {
773
786
  d.log.message(` ${pkgMgr} add -D sunpeak @playwright/test vitest`);
774
787
  d.log.message(` ${pkgMgr} exec playwright install chromium`);
775
788
  d.log.message('');
776
- d.log.message(' sunpeak test # E2E tests');
777
- d.log.message(' sunpeak test --unit # Unit tests (vitest)');
778
- d.log.message(' sunpeak test --visual # Visual regression');
779
- d.log.message(' sunpeak test --live # Live tests against real hosts');
780
- d.log.message(' sunpeak test --eval # Multi-model evals');
789
+ d.log.message(' npx sunpeak test # E2E tests');
790
+ d.log.message(' npx sunpeak test --unit # Unit tests (vitest)');
791
+ d.log.message(' npx sunpeak test --visual # Visual regression');
792
+ d.log.message(' npx sunpeak test --live # Live tests against real hosts');
793
+ d.log.message(' npx sunpeak test --eval # Multi-model evals');
781
794
  }
782
795
 
783
796
  async function initSunpeakProject(d) {
@@ -824,10 +837,10 @@ export default defineConfig();
824
837
  scaffoldUnitTest(join(cwd, 'tests', 'unit', 'example.test.ts'), d);
825
838
 
826
839
  d.log.step('Scaffolded test types:');
827
- d.log.message(' tests/e2e/visual.test.ts — Visual regression (sunpeak test --visual)');
828
- d.log.message(' tests/live/ — Live host tests (sunpeak test --live)');
829
- d.log.message(' tests/evals/ — Multi-model evals (sunpeak test --eval)');
830
- d.log.message(' tests/unit/example.test.ts — Unit tests (sunpeak test --unit)');
840
+ d.log.message(' tests/e2e/visual.test.ts — Visual regression (npx sunpeak test --visual)');
841
+ d.log.message(' tests/live/ — Live host tests (npx sunpeak test --live)');
842
+ d.log.message(' tests/evals/ — Multi-model evals (npx sunpeak test --eval)');
843
+ d.log.message(' tests/unit/example.test.ts — Unit tests (npx sunpeak test --unit)');
831
844
  d.log.message('');
832
845
  d.log.message(' Migrate existing e2e tests:');
833
846
  d.log.message(' Replace: import { test, expect } from "@playwright/test"');
@@ -85,12 +85,15 @@ export async function runTest(args) {
85
85
  configCandidates: [
86
86
  'tests/live/playwright.config.ts',
87
87
  'tests/live/playwright.config.js',
88
- // Fallback for non-JS projects: tests/sunpeak/ self-contained directory
88
+ // Non-JS projects: tests/sunpeak/ self-contained directory (run from project root)
89
89
  'tests/sunpeak/live/playwright.config.ts',
90
90
  'tests/sunpeak/live/playwright.config.js',
91
+ // Non-JS projects: run from within tests/sunpeak/ directly
92
+ 'live/playwright.config.ts',
93
+ 'live/playwright.config.js',
91
94
  ],
92
95
  configRequired: true,
93
- configErrorMessage: 'No live test config found at tests/live/playwright.config.ts',
96
+ configErrorMessage: 'No live test config found. Expected at tests/live/playwright.config.ts or live/playwright.config.ts',
94
97
  });
95
98
  results.push({ suite: 'live', code });
96
99
  }
@@ -537,6 +540,8 @@ function findEvalDir() {
537
540
  const candidates = [
538
541
  'tests/evals',
539
542
  'tests/sunpeak/evals',
543
+ // When running from within tests/sunpeak/ directly (non-JS projects)
544
+ 'evals',
540
545
  ];
541
546
 
542
547
  for (const candidate of candidates) {
@@ -77,7 +77,13 @@ export async function createMcpConnection(serverArg) {
77
77
  const { StreamableHTTPClientTransport } = await import(
78
78
  '@modelcontextprotocol/sdk/client/streamableHttp.js'
79
79
  );
80
- const transport = new StreamableHTTPClientTransport(new URL(serverArg));
80
+ // Follow redirects (e.g. /mcp → /mcp/) before creating the transport.
81
+ let finalUrl = serverArg;
82
+ try {
83
+ const resp = await fetch(serverArg, { method: 'HEAD', redirect: 'follow' });
84
+ if (resp.url && resp.url !== serverArg) finalUrl = resp.url;
85
+ } catch { /* use original URL */ }
86
+ const transport = new StreamableHTTPClientTransport(new URL(finalUrl));
81
87
  await client.connect(transport);
82
88
  return { client, transport };
83
89
  } else {
@@ -79,7 +79,7 @@ export function defineInspectConfig(options) {
79
79
  timeout,
80
80
  webServer: {
81
81
  command,
82
- healthUrl: `http://localhost:${port}/health`,
82
+ healthUrl: `http://127.0.0.1:${port}/health`,
83
83
  },
84
84
  });
85
85
  }
@@ -28,6 +28,16 @@ export interface LiveConfigOptions {
28
28
 
29
29
  /** Additional Playwright `use` options, merged with defaults. */
30
30
  use?: Record<string, unknown>;
31
+
32
+ /** External MCP server config. Omit for sunpeak framework projects. */
33
+ server?: {
34
+ /** Server URL (e.g., 'http://localhost:8000/mcp') */
35
+ url?: string;
36
+ /** Server start command */
37
+ command?: string;
38
+ /** Command arguments */
39
+ args?: string[];
40
+ };
31
41
  }
32
42
 
33
43
  export interface HostConfigOptions {
@@ -11,6 +11,7 @@ import { join, dirname } from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { ANTI_BOT_ARGS, CHROME_USER_AGENT } from './utils.mjs';
13
13
  import { getPortSync } from '../get-port.mjs';
14
+ import { resolveSunpeakBin } from '../resolve-bin.mjs';
14
15
 
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
17
  const GLOBAL_SETUP_PATH = join(__dirname, 'global-setup.mjs');
@@ -34,6 +35,10 @@ const GLOBAL_SETUP_PATH = join(__dirname, 'global-setup.mjs');
34
35
  * @param {string[]} [options.permissions] - Browser permissions to grant (e.g., ['geolocation'])
35
36
  * @param {boolean} [options.devOverlay=true] - Show the dev overlay (resource timestamp + tool timing) in resources
36
37
  * @param {Object} [options.use] - Additional Playwright `use` options (merged with defaults)
38
+ * @param {Object} [options.server] - External MCP server config (omit for sunpeak projects)
39
+ * @param {string} [options.server.url] - Server URL (e.g., 'http://localhost:8000/mcp')
40
+ * @param {string} [options.server.command] - Server start command
41
+ * @param {string[]} [options.server.args] - Command arguments
37
42
  */
38
43
  export function createLiveConfig(hostOptions, options = {}) {
39
44
  const { hostId, authFileName } = hostOptions;
@@ -49,6 +54,7 @@ export function createLiveConfig(hostOptions, options = {}) {
49
54
  geolocation,
50
55
  permissions,
51
56
  use: userUse,
57
+ server,
52
58
  } = options;
53
59
 
54
60
  const resolvedAuthDir = authDir || join(testDir, '.auth');
@@ -91,10 +97,36 @@ export function createLiveConfig(hostOptions, options = {}) {
91
97
  },
92
98
  ],
93
99
  webServer: {
94
- command: `SUNPEAK_LIVE_TEST=1 SUNPEAK_SANDBOX_PORT=${getPortSync(24680)}${devOverlay ? '' : ' SUNPEAK_DEV_OVERLAY=false'} pnpm dev -- --prod-resources --port ${vitePort}`,
95
- url: `http://localhost:${vitePort}/health`,
100
+ command: buildLiveWebServerCommand({ server, vitePort, devOverlay }),
101
+ url: `http://127.0.0.1:${vitePort}/health`,
96
102
  reuseExistingServer: !process.env.CI,
97
103
  timeout: 60_000,
98
104
  },
99
105
  };
100
106
  }
107
+
108
+ /**
109
+ * Build the webServer command for live tests.
110
+ * Uses `sunpeak inspect` for external servers, `pnpm dev` for sunpeak projects.
111
+ */
112
+ function buildLiveWebServerCommand({ server, vitePort, devOverlay }) {
113
+ const sandboxPort = getPortSync(24680);
114
+ const envPrefix = `SUNPEAK_LIVE_TEST=1 SUNPEAK_SANDBOX_PORT=${sandboxPort}${devOverlay ? '' : ' SUNPEAK_DEV_OVERLAY=false'}`;
115
+
116
+ if (server) {
117
+ // External MCP server — launch sunpeak inspect
118
+ const bin = resolveSunpeakBin();
119
+ if (server.url) {
120
+ return `${envPrefix} ${bin} inspect --server ${server.url} --port ${vitePort}`;
121
+ }
122
+ if (server.command) {
123
+ const cmd = server.args
124
+ ? `${server.command} ${server.args.join(' ')}`
125
+ : server.command;
126
+ return `${envPrefix} ${bin} inspect --server "${cmd}" --port ${vitePort}`;
127
+ }
128
+ }
129
+
130
+ // sunpeak framework project — use pnpm dev
131
+ return `${envPrefix} pnpm dev -- --prod-resources --port ${vitePort}`;
132
+ }
@@ -44,7 +44,9 @@ export function createBaseConfig({ hosts, testDir, webServer, port, use, globalS
44
44
  ? { expect: { toHaveScreenshot: toHaveScreenshotDefaults } }
45
45
  : {}),
46
46
  use: {
47
- baseURL: `http://localhost:${port}`,
47
+ // Use 127.0.0.1 instead of localhost to avoid IPv4/IPv6 resolution
48
+ // ambiguity that causes ECONNREFUSED flakes on macOS.
49
+ baseURL: `http://127.0.0.1:${port}`,
48
50
  trace: 'on-first-retry',
49
51
  ...use,
50
52
  },
@@ -76,7 +76,7 @@ export function defineConfig(options = {}) {
76
76
  timeout,
77
77
  webServer: {
78
78
  command,
79
- healthUrl: `http://localhost:${port}/health`,
79
+ healthUrl: `http://127.0.0.1:${port}/health`,
80
80
  },
81
81
  });
82
82
  }
package/bin/sunpeak.js CHANGED
@@ -102,22 +102,11 @@ function getVersion() {
102
102
  {
103
103
  const resources = discoverResources();
104
104
  console.log(`
105
- ☀️ 🏔️ sunpeak - Inspector, testing framework, and app framework for MCP Apps
105
+ ☀️ 🏔️ sunpeak - App framework, testing framework, and inspector for MCP Apps
106
106
 
107
- Install:
108
- pnpm add -g sunpeak
107
+ Usage: npx sunpeak <command>
109
108
 
110
- Testing (works with any MCP server):
111
- sunpeak inspect Inspect any MCP server in the inspector
112
- --server, -s <url|cmd> MCP server URL or stdio command (required)
113
- --simulations <dir> Simulation JSON directory
114
- sunpeak test Run e2e tests against the inspector
115
- init Scaffold test infrastructure into a project
116
- --unit Run unit tests (vitest)
117
- --live Run live tests against real hosts
118
- --eval Run evals against LLM models
119
-
120
- App framework (for sunpeak projects):
109
+ App framework:
121
110
  sunpeak new [name] [resources] Create a new project
122
111
  sunpeak dev Start dev server + inspector + MCP endpoint
123
112
  --no-begging Suppress GitHub star message
@@ -125,8 +114,20 @@ App framework (for sunpeak projects):
125
114
  sunpeak start Start production MCP server
126
115
  --port, -p Server port (default: 8000, or PORT env)
127
116
  sunpeak upgrade Upgrade sunpeak to latest version
128
- sunpeak --version Show version number
129
117
 
118
+ Testing (works with any MCP server):
119
+ sunpeak test Run e2e tests against the inspector
120
+ init Scaffold test infrastructure into a project
121
+ --unit Run unit tests (vitest)
122
+ --live Run live tests against real hosts
123
+ --eval Run evals against LLM models
124
+
125
+ Inspector (works with any MCP server):
126
+ sunpeak inspect Inspect any MCP server in the inspector
127
+ --server, -s <url|cmd> MCP server URL or stdio command (required)
128
+ --simulations <dir> Simulation JSON directory
129
+
130
+ sunpeak --version Show version number
130
131
  Resources: ${resources.join(', ')} (comma/space separated)
131
132
  Example: sunpeak new sunpeak-app "${resources.slice(0, 2).join(',')}"
132
133
  `);
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../chunk-9hOWP6kD.cjs");
3
- const require_inspector = require("../inspector-DAA1Wiyh.cjs");
3
+ const require_inspector = require("../inspector-D0qOqYX2.cjs");
4
4
  const require_inspector_url = require("../inspector-url-C3LTKgXt.cjs");
5
5
  const require_discovery = require("../discovery-Clu4uHp1.cjs");
6
6
  //#region src/chatgpt/index.ts
@@ -1,5 +1,5 @@
1
1
  import { r as __exportAll } from "../chunk-D6g4UhsZ.js";
2
- import { _ as McpAppHost, d as ThemeProvider, f as useThemeContext, g as extractResourceCSP, h as IframeResource, n as resolveServerToolResult, t as Inspector, v as SCREEN_WIDTHS } from "../inspector-BBDa5yCm.js";
2
+ import { _ as McpAppHost, d as ThemeProvider, f as useThemeContext, g as extractResourceCSP, h as IframeResource, n as resolveServerToolResult, t as Inspector, v as SCREEN_WIDTHS } from "../inspector-60Na_Zc4.js";
3
3
  import { t as createInspectorUrl } from "../inspector-url-CyQcuBI9.js";
4
4
  import { c as toPascalCase, i as findResourceKey, n as extractSimulationKey, r as findResourceDirs, s as getComponentName, t as extractResourceKey } from "../discovery-Cgoegt62.js";
5
5
  //#region src/chatgpt/index.ts
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../chunk-9hOWP6kD.cjs");
3
- const require_inspector = require("../inspector-DAA1Wiyh.cjs");
3
+ const require_inspector = require("../inspector-D0qOqYX2.cjs");
4
4
  exports.Inspector = require_inspector.Inspector;
@@ -1,2 +1,2 @@
1
- import { t as Inspector } from "../inspector-BBDa5yCm.js";
1
+ import { t as Inspector } from "../inspector-60Na_Zc4.js";
2
2
  export { Inspector };
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-9hOWP6kD.cjs");
3
- const require_use_app = require("../../use-app-DPkj5Jp_.cjs");
3
+ const require_use_app = require("../../use-app-B33mckz4.cjs");
4
4
  let react = require("react");
5
5
  //#region src/host/chatgpt/openai-types.ts
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { t as useApp } from "../../use-app-Cr0auUa1.js";
1
+ import { t as useApp } from "../../use-app-kv5GQr0G.js";
2
2
  import { useCallback } from "react";
3
3
  //#region src/host/chatgpt/openai-types.ts
4
4
  /**
package/dist/index.cjs CHANGED
@@ -1,15 +1,15 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-9hOWP6kD.cjs");
3
3
  const require_protocol = require("./protocol-C8pFDmcy.cjs");
4
- const require_use_app = require("./use-app-DPkj5Jp_.cjs");
5
- const require_inspector = require("./inspector-DAA1Wiyh.cjs");
4
+ const require_use_app = require("./use-app-B33mckz4.cjs");
5
+ const require_inspector = require("./inspector-D0qOqYX2.cjs");
6
6
  const require_host_index = require("./host/index.cjs");
7
7
  const require_inspector_index = require("./inspector/index.cjs");
8
8
  const require_chatgpt_index = require("./chatgpt/index.cjs");
9
9
  let react = require("react");
10
10
  react = require_chunk.__toESM(react, 1);
11
11
  let react_jsx_runtime = require("react/jsx-runtime");
12
- //#region ../../node_modules/.pnpm/@modelcontextprotocol+ext-apps@1.5.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__react-_a8a8e071c354e7dd6f62871eadf46f99/node_modules/@modelcontextprotocol/ext-apps/dist/src/react/index.js
12
+ //#region ../../node_modules/.pnpm/@modelcontextprotocol+ext-apps@1.6.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__react-_1fd7d0151a0915598274278e5ddb69e9/node_modules/@modelcontextprotocol/ext-apps/dist/src/react/index.js
13
13
  var m = require_protocol.union([require_protocol.literal("light"), require_protocol.literal("dark")]).describe("Color theme preference for the host environment."), N$1 = require_protocol.union([
14
14
  require_protocol.literal("inline"),
15
15
  require_protocol.literal("fullscreen"),