sunpeak 0.16.5 → 0.16.7
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 +0 -2
- package/bin/commands/build.mjs +19 -16
- package/bin/commands/dev.mjs +7 -6
- package/bin/commands/new.mjs +69 -40
- package/package.json +3 -2
- package/template/src/resources/review/review.tsx +4 -4
package/README.md
CHANGED
|
@@ -43,8 +43,6 @@ pnpm add -g sunpeak
|
|
|
43
43
|
sunpeak new
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
To add `sunpeak` to an existing project, refer to the [documentation](https://sunpeak.ai/docs/add-to-existing-project).
|
|
47
|
-
|
|
48
46
|
## Overview
|
|
49
47
|
|
|
50
48
|
`sunpeak` is an npm package that helps you build MCP Apps (interactive UI resources) while keeping your MCP server client-agnostic. Built on the [MCP Apps SDK](https://github.com/modelcontextprotocol/ext-apps) (`@modelcontextprotocol/ext-apps`). `sunpeak` consists of:
|
package/bin/commands/build.mjs
CHANGED
|
@@ -49,7 +49,8 @@ function resolveEsmEntry(require, packageName) {
|
|
|
49
49
|
* Build all resources for a Sunpeak project
|
|
50
50
|
* Runs in the context of a user's project directory
|
|
51
51
|
*/
|
|
52
|
-
export async function build(projectRoot = process.cwd()) {
|
|
52
|
+
export async function build(projectRoot = process.cwd(), { quiet = false } = {}) {
|
|
53
|
+
const log = quiet ? () => {} : console.log.bind(console);
|
|
53
54
|
|
|
54
55
|
// Check for package.json first
|
|
55
56
|
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
@@ -191,7 +192,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
191
192
|
process.exit(1);
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
log('Building all resources...\n');
|
|
195
196
|
|
|
196
197
|
// Read and validate the template
|
|
197
198
|
const template = readFileSync(templateFile, 'utf-8');
|
|
@@ -214,7 +215,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
214
215
|
// Build all resources (but don't copy yet)
|
|
215
216
|
for (let i = 0; i < resourceFiles.length; i++) {
|
|
216
217
|
const { componentName, componentFile, kebabName, entry, jsOutput, buildOutDir } = resourceFiles[i];
|
|
217
|
-
|
|
218
|
+
log(`[${i + 1}/${resourceFiles.length}] Building ${kebabName}...`);
|
|
218
219
|
|
|
219
220
|
try {
|
|
220
221
|
// Create build directory if it doesn't exist
|
|
@@ -234,6 +235,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
234
235
|
await viteBuild({
|
|
235
236
|
mode: 'production',
|
|
236
237
|
root: projectRoot,
|
|
238
|
+
...(quiet && { logLevel: 'silent' }),
|
|
237
239
|
plugins: [react(), tailwindcss(), inlineCssPlugin(buildOutDir)],
|
|
238
240
|
define: {
|
|
239
241
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
@@ -277,7 +279,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
// Now copy all files from build-output to dist/{resource}/
|
|
280
|
-
|
|
282
|
+
log('\nCopying built files to dist/...');
|
|
281
283
|
const timestamp = Date.now().toString(36);
|
|
282
284
|
|
|
283
285
|
for (const { jsOutput, htmlOutput, buildOutDir, distOutDir, kebabName, componentFile, resourceDir } of resourceFiles) {
|
|
@@ -296,7 +298,7 @@ export async function build(projectRoot = process.cwd()) {
|
|
|
296
298
|
// Generate URI using resource name and build timestamp
|
|
297
299
|
meta.uri = `ui://${meta.name}-${timestamp}`;
|
|
298
300
|
writeFileSync(destJson, JSON.stringify(meta, null, 2));
|
|
299
|
-
|
|
301
|
+
log(`✓ Generated ${kebabName}/${kebabName}.json (uri: ${meta.uri})`);
|
|
300
302
|
|
|
301
303
|
// Read built JS file and wrap in HTML shell
|
|
302
304
|
const builtJsFile = path.join(buildOutDir, jsOutput);
|
|
@@ -318,13 +320,13 @@ ${jsContents}
|
|
|
318
320
|
</body>
|
|
319
321
|
</html>`;
|
|
320
322
|
writeFileSync(destHtmlFile, html);
|
|
321
|
-
|
|
323
|
+
log(`✓ Built ${kebabName}/${htmlOutput}`);
|
|
322
324
|
} else {
|
|
323
325
|
console.error(`Built file not found: ${builtJsFile}`);
|
|
324
326
|
if (existsSync(buildOutDir)) {
|
|
325
|
-
|
|
327
|
+
log(` Files in ${buildOutDir}:`, readdirSync(buildOutDir));
|
|
326
328
|
} else {
|
|
327
|
-
|
|
329
|
+
log(` Build directory doesn't exist: ${buildOutDir}`);
|
|
328
330
|
}
|
|
329
331
|
process.exit(1);
|
|
330
332
|
}
|
|
@@ -339,10 +341,10 @@ ${jsContents}
|
|
|
339
341
|
rmSync(buildDir, { recursive: true });
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
log('\n✓ All resources built successfully!');
|
|
345
|
+
log('\nBuilt resources:');
|
|
344
346
|
for (const { kebabName } of resourceFiles) {
|
|
345
|
-
|
|
347
|
+
log(` ${kebabName}`);
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
// ========================================================================
|
|
@@ -360,7 +362,7 @@ ${jsContents}
|
|
|
360
362
|
const hasServerEntry = existsSync(serverEntryPath);
|
|
361
363
|
|
|
362
364
|
if (toolFiles.length > 0 || hasServerEntry) {
|
|
363
|
-
|
|
365
|
+
log('\nCompiling server-side code...');
|
|
364
366
|
|
|
365
367
|
let esbuild;
|
|
366
368
|
try {
|
|
@@ -407,7 +409,7 @@ ${jsContents}
|
|
|
407
409
|
loader: { '.tsx': 'tsx', '.ts': 'ts' },
|
|
408
410
|
logLevel: 'warning',
|
|
409
411
|
});
|
|
410
|
-
|
|
412
|
+
log(`✓ Compiled tools/${toolName}.js`);
|
|
411
413
|
} catch (err) {
|
|
412
414
|
console.error(`Failed to compile tool ${toolName}:`, err.message);
|
|
413
415
|
process.exit(1);
|
|
@@ -439,7 +441,7 @@ ${jsContents}
|
|
|
439
441
|
loader: { '.tsx': 'tsx', '.ts': 'ts' },
|
|
440
442
|
logLevel: 'warning',
|
|
441
443
|
});
|
|
442
|
-
|
|
444
|
+
log(`✓ Compiled server.js`);
|
|
443
445
|
} catch (err) {
|
|
444
446
|
console.error(`Failed to compile server entry:`, err.message);
|
|
445
447
|
process.exit(1);
|
|
@@ -448,12 +450,13 @@ ${jsContents}
|
|
|
448
450
|
}
|
|
449
451
|
}
|
|
450
452
|
|
|
451
|
-
|
|
453
|
+
log('\n✓ Build complete!');
|
|
452
454
|
}
|
|
453
455
|
|
|
454
456
|
// Allow running directly
|
|
455
457
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
456
|
-
|
|
458
|
+
const quiet = process.argv.includes('--quiet');
|
|
459
|
+
build(process.cwd(), { quiet }).catch(error => {
|
|
457
460
|
console.error(error);
|
|
458
461
|
process.exit(1);
|
|
459
462
|
});
|
package/bin/commands/dev.mjs
CHANGED
|
@@ -66,17 +66,17 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
66
66
|
let activeChild = null;
|
|
67
67
|
const sunpeakBin = join(dirname(new URL(import.meta.url).pathname), '..', 'sunpeak.js');
|
|
68
68
|
|
|
69
|
-
const runBuild = (
|
|
69
|
+
const runBuild = () => {
|
|
70
70
|
// Kill any in-progress build and start fresh
|
|
71
71
|
if (activeChild) {
|
|
72
72
|
activeChild.kill('SIGTERM');
|
|
73
73
|
activeChild = null;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
console.log(`[build]
|
|
77
|
-
const child = spawn(process.execPath, [sunpeakBin, 'build'], {
|
|
76
|
+
console.log(`[build] Building resources for the MCP server for non-ChatGPT hosts...`);
|
|
77
|
+
const child = spawn(process.execPath, [sunpeakBin, 'build', '--quiet'], {
|
|
78
78
|
cwd: projectRoot,
|
|
79
|
-
stdio: ['ignore', '
|
|
79
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
80
80
|
env: { ...process.env, NODE_ENV: 'production' },
|
|
81
81
|
});
|
|
82
82
|
activeChild = child;
|
|
@@ -85,6 +85,7 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
85
85
|
if (child !== activeChild) return; // Superseded by a newer build
|
|
86
86
|
activeChild = null;
|
|
87
87
|
if (code === 0) {
|
|
88
|
+
console.log(`[build] Built resources for the MCP server for non-ChatGPT hosts.`);
|
|
88
89
|
// Notify non-local sessions (Claude, etc.) that resources changed
|
|
89
90
|
mcpHandle?.invalidateResources();
|
|
90
91
|
} else if (code !== null) {
|
|
@@ -94,7 +95,7 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
94
95
|
};
|
|
95
96
|
|
|
96
97
|
// Initial build
|
|
97
|
-
runBuild(
|
|
98
|
+
runBuild();
|
|
98
99
|
|
|
99
100
|
// Watch src/resources/ for changes using fs.watch (recursive supported on macOS/Windows)
|
|
100
101
|
let debounceTimer = null;
|
|
@@ -108,7 +109,7 @@ function startBuildWatcher(projectRoot, resourcesDir, mcpHandle) {
|
|
|
108
109
|
|
|
109
110
|
clearTimeout(debounceTimer);
|
|
110
111
|
debounceTimer = setTimeout(() => {
|
|
111
|
-
runBuild(
|
|
112
|
+
runBuild();
|
|
112
113
|
}, 500);
|
|
113
114
|
});
|
|
114
115
|
console.log('[build] Watching src/resources/ for changes...');
|
package/bin/commands/new.mjs
CHANGED
|
@@ -2,26 +2,59 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
3
3
|
import { join, dirname, basename } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { execSync, exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
import * as clack from '@clack/prompts';
|
|
7
10
|
import { discoverResources } from '../lib/patterns.mjs';
|
|
8
11
|
import { detectPackageManager } from '../utils.mjs';
|
|
9
12
|
|
|
10
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
|
-
* Default prompt
|
|
14
|
-
* @param {string} question
|
|
16
|
+
* Default prompt for project name using clack text input.
|
|
15
17
|
* @returns {Promise<string>}
|
|
16
18
|
*/
|
|
17
|
-
function
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
async function defaultPromptName() {
|
|
20
|
+
const value = await clack.text({
|
|
21
|
+
message: 'Project name',
|
|
22
|
+
placeholder: 'my-app',
|
|
23
|
+
defaultValue: 'my-app',
|
|
24
|
+
validate: (v) => {
|
|
25
|
+
if (v === 'template') return '"template" is a reserved name';
|
|
26
|
+
},
|
|
24
27
|
});
|
|
28
|
+
if (clack.isCancel(value)) {
|
|
29
|
+
clack.cancel('Cancelled.');
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default resource selection using clack multiselect.
|
|
37
|
+
* @param {string[]} availableResources
|
|
38
|
+
* @returns {Promise<string[]>}
|
|
39
|
+
*/
|
|
40
|
+
async function defaultSelectResources(availableResources) {
|
|
41
|
+
const selected = await clack.multiselect({
|
|
42
|
+
message: 'Resources (UIs) to include (space to toggle)',
|
|
43
|
+
options: (() => {
|
|
44
|
+
const maxLen = Math.max(...availableResources.map((r) => r.length));
|
|
45
|
+
return availableResources.map((r) => ({
|
|
46
|
+
value: r,
|
|
47
|
+
label: `${r.padEnd(maxLen)} (https://sunpeak.ai/docs/api-reference/resources/${r})`,
|
|
48
|
+
}));
|
|
49
|
+
})(),
|
|
50
|
+
initialValues: availableResources,
|
|
51
|
+
required: true,
|
|
52
|
+
});
|
|
53
|
+
if (clack.isCancel(selected)) {
|
|
54
|
+
clack.cancel('Cancelled.');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
return selected;
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
/**
|
|
@@ -37,7 +70,12 @@ export const defaultDeps = {
|
|
|
37
70
|
writeFileSync,
|
|
38
71
|
renameSync,
|
|
39
72
|
execSync,
|
|
40
|
-
|
|
73
|
+
execAsync,
|
|
74
|
+
promptName: defaultPromptName,
|
|
75
|
+
selectResources: defaultSelectResources,
|
|
76
|
+
intro: clack.intro,
|
|
77
|
+
outro: clack.outro,
|
|
78
|
+
spinner: clack.spinner,
|
|
41
79
|
console,
|
|
42
80
|
process,
|
|
43
81
|
cwd: () => process.cwd(),
|
|
@@ -88,6 +126,8 @@ export function parseResourcesInput(input, validResources, deps = defaultDeps) {
|
|
|
88
126
|
export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
89
127
|
const d = { ...defaultDeps, ...deps };
|
|
90
128
|
|
|
129
|
+
d.intro('☀️ sunpeak');
|
|
130
|
+
|
|
91
131
|
// Discover available resources from template
|
|
92
132
|
const availableResources = d.discoverResources();
|
|
93
133
|
if (availableResources.length === 0) {
|
|
@@ -96,10 +136,7 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
96
136
|
}
|
|
97
137
|
|
|
98
138
|
if (!projectName) {
|
|
99
|
-
projectName = await d.
|
|
100
|
-
if (!projectName) {
|
|
101
|
-
projectName = 'my-app';
|
|
102
|
-
}
|
|
139
|
+
projectName = await d.promptName();
|
|
103
140
|
}
|
|
104
141
|
|
|
105
142
|
if (projectName === 'template') {
|
|
@@ -107,17 +144,13 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
107
144
|
d.process.exit(1);
|
|
108
145
|
}
|
|
109
146
|
|
|
110
|
-
// Use resources from args or
|
|
111
|
-
let
|
|
112
|
-
if (resourcesArg) {
|
|
113
|
-
|
|
114
|
-
d.console.log(`☀️ 🏔️ Resources: ${resourcesArg}`);
|
|
147
|
+
// Use resources from args or interactively select them
|
|
148
|
+
let selectedResources;
|
|
149
|
+
if (resourcesArg !== undefined) {
|
|
150
|
+
selectedResources = parseResourcesInput(resourcesArg, availableResources, d);
|
|
115
151
|
} else {
|
|
116
|
-
|
|
117
|
-
`☀️ 🏔️ Resources (UIs) to include [${availableResources.join(', ')}]: `
|
|
118
|
-
);
|
|
152
|
+
selectedResources = await d.selectResources(availableResources);
|
|
119
153
|
}
|
|
120
|
-
const selectedResources = parseResourcesInput(resourcesInput, availableResources, d);
|
|
121
154
|
|
|
122
155
|
const targetDir = join(d.cwd(), projectName);
|
|
123
156
|
|
|
@@ -126,13 +159,11 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
126
159
|
d.process.exit(1);
|
|
127
160
|
}
|
|
128
161
|
|
|
129
|
-
d.console.log(`☀️ 🏔️ Creating ${projectName}...`);
|
|
130
|
-
|
|
131
|
-
d.mkdirSync(targetDir, { recursive: true });
|
|
132
|
-
|
|
133
162
|
// Filter resource directories based on selection
|
|
134
163
|
const excludedResources = availableResources.filter((r) => !selectedResources.includes(r));
|
|
135
164
|
|
|
165
|
+
d.mkdirSync(targetDir, { recursive: true });
|
|
166
|
+
|
|
136
167
|
d.cpSync(d.templateDir, targetDir, {
|
|
137
168
|
recursive: true,
|
|
138
169
|
filter: (src) => {
|
|
@@ -223,32 +254,30 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
|
|
|
223
254
|
|
|
224
255
|
d.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
225
256
|
|
|
226
|
-
//
|
|
257
|
+
// Install dependencies with spinner
|
|
227
258
|
const pm = d.detectPackageManager();
|
|
228
|
-
d.
|
|
259
|
+
const s = d.spinner();
|
|
260
|
+
s.start(`Installing dependencies with ${pm}...`);
|
|
229
261
|
|
|
230
262
|
try {
|
|
231
|
-
d.
|
|
263
|
+
await d.execAsync(`${pm} install`, { cwd: targetDir });
|
|
264
|
+
s.stop(`Installed dependencies with ${pm}`);
|
|
232
265
|
} catch {
|
|
233
|
-
|
|
266
|
+
s.stop(`Install failed. You can try running "${pm} install" manually.`);
|
|
234
267
|
}
|
|
235
268
|
|
|
236
269
|
const runCmd = pm === 'npm' ? 'npm run' : pm;
|
|
237
270
|
|
|
238
|
-
d.
|
|
239
|
-
Done! To get started:
|
|
271
|
+
d.outro(`Done! To get started:
|
|
240
272
|
|
|
241
273
|
cd ${projectName}
|
|
242
274
|
sunpeak dev
|
|
243
275
|
|
|
244
|
-
|
|
276
|
+
Your project commands:
|
|
245
277
|
|
|
246
278
|
sunpeak dev # Start dev server + MCP endpoint
|
|
247
279
|
sunpeak build # Build for production
|
|
248
|
-
${runCmd} test # Run tests
|
|
249
|
-
|
|
250
|
-
See README.md for more details.
|
|
251
|
-
`);
|
|
280
|
+
${runCmd} test # Run tests`);
|
|
252
281
|
}
|
|
253
282
|
|
|
254
283
|
// Allow running directly
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sunpeak",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.7",
|
|
4
4
|
"description": "Local-first MCP Apps framework. Quickstart, build, test, and ship your Claude or ChatGPT App!",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -93,11 +93,12 @@
|
|
|
93
93
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
94
94
|
},
|
|
95
95
|
"dependencies": {
|
|
96
|
+
"@clack/prompts": "^1.1.0",
|
|
96
97
|
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
|
97
98
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
98
99
|
"clsx": "^2.1.1",
|
|
99
|
-
"tailwind-merge": "^3.4.0",
|
|
100
100
|
"esbuild": "^0.27.0",
|
|
101
|
+
"tailwind-merge": "^3.4.0",
|
|
101
102
|
"zod": "^3.25.76"
|
|
102
103
|
},
|
|
103
104
|
"devDependencies": {
|
|
@@ -354,7 +354,7 @@ function AlertBanner({ alert }: { alert: Alert }) {
|
|
|
354
354
|
const config = alertTypeConfig[alert.type];
|
|
355
355
|
return (
|
|
356
356
|
<div
|
|
357
|
-
className="flex items-
|
|
357
|
+
className="flex items-center gap-2 p-3 rounded-lg"
|
|
358
358
|
style={{
|
|
359
359
|
backgroundColor: config.bg,
|
|
360
360
|
borderWidth: 1,
|
|
@@ -362,12 +362,12 @@ function AlertBanner({ alert }: { alert: Alert }) {
|
|
|
362
362
|
borderColor: config.border,
|
|
363
363
|
}}
|
|
364
364
|
>
|
|
365
|
-
<span className="flex-shrink-0" style={{ color: config.text }}>
|
|
365
|
+
<span className="flex-shrink-0 text-base leading-none" style={{ color: config.text }}>
|
|
366
366
|
{config.icon}
|
|
367
367
|
</span>
|
|
368
|
-
<
|
|
368
|
+
<span className="text-sm" style={{ color: config.text }}>
|
|
369
369
|
{alert.message}
|
|
370
|
-
</
|
|
370
|
+
</span>
|
|
371
371
|
</div>
|
|
372
372
|
);
|
|
373
373
|
}
|