sunpeak 0.5.18 → 0.5.20
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 +5 -1
- package/bin/sunpeak.js +128 -8
- package/cli/build.mjs +199 -0
- package/cli/validate.mjs +245 -0
- package/package.json +2 -1
- package/template/README.md +53 -9
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Button.js +3 -3
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_SegmentedControl.js +1 -1
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +16 -16
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Textarea.js +3 -3
- package/template/node_modules/.vite/deps/_metadata.json +31 -31
- package/template/node_modules/.vite/deps/{chunk-675LFNY2.js → chunk-EVJ3DVH5.js} +8 -8
- package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/template/package.json +2 -2
- package/template/scripts/build-all.mjs +0 -117
- package/template/scripts/validate.mjs +0 -186
- /package/template/node_modules/.vite/deps/{chunk-675LFNY2.js.map → chunk-EVJ3DVH5.js.map} +0 -0
package/README.md
CHANGED
|
@@ -36,7 +36,11 @@ Quickstart, build, and test your ChatGPT App locally with OpenAI apps-sdk-ui Rea
|
|
|
36
36
|
Requirements: Node (20+), pnpm (10+)
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
|
|
39
|
+
# Install the CLI globally
|
|
40
|
+
pnpm add -g sunpeak
|
|
41
|
+
|
|
42
|
+
# Create a new project
|
|
43
|
+
sunpeak new
|
|
40
44
|
```
|
|
41
45
|
|
|
42
46
|
To add sunpeak to an existing project, refer to the [documentation](https://docs.sunpeak.ai/add-to-existing-project).
|
package/bin/sunpeak.js
CHANGED
|
@@ -4,8 +4,10 @@ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, renameSync
|
|
|
4
4
|
import { join, dirname, basename } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { createInterface } from 'readline';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const CLI_DIR = join(__dirname, '..', 'cli');
|
|
9
11
|
|
|
10
12
|
function prompt(question) {
|
|
11
13
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -17,6 +19,28 @@ function prompt(question) {
|
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
function runCommand(command, args = [], options = {}) {
|
|
23
|
+
const child = spawn(command, args, {
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
stdio: 'inherit',
|
|
26
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
27
|
+
...options,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
child.on('exit', (code) => {
|
|
31
|
+
process.exit(code || 0);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkPackageJson() {
|
|
36
|
+
const pkgPath = join(process.cwd(), 'package.json');
|
|
37
|
+
if (!existsSync(pkgPath)) {
|
|
38
|
+
console.error('Error: No package.json found in current directory.');
|
|
39
|
+
console.error('Make sure you are in a Sunpeak project directory.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
async function init(projectName) {
|
|
21
45
|
if (!projectName) {
|
|
22
46
|
projectName = await prompt('☀️ 🏔️ Project name [my-app]: ');
|
|
@@ -81,8 +105,17 @@ async function init(projectName) {
|
|
|
81
105
|
console.log(`
|
|
82
106
|
Done! To get started:
|
|
83
107
|
|
|
108
|
+
# Install the CLI (if not already installed)
|
|
109
|
+
pnpm add -g sunpeak
|
|
110
|
+
|
|
111
|
+
# Navigate to your project and install dependencies
|
|
84
112
|
cd ${projectName}
|
|
85
|
-
pnpm install
|
|
113
|
+
pnpm install
|
|
114
|
+
|
|
115
|
+
# Start development
|
|
116
|
+
sunpeak dev
|
|
117
|
+
|
|
118
|
+
Alternatively, use "pnpm dlx sunpeak dev" if you prefer not to install globally.
|
|
86
119
|
|
|
87
120
|
See README.md for more details.
|
|
88
121
|
`);
|
|
@@ -90,13 +123,100 @@ See README.md for more details.
|
|
|
90
123
|
|
|
91
124
|
const [, , command, ...args] = process.argv;
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
126
|
+
// Main CLI handler
|
|
127
|
+
(async () => {
|
|
128
|
+
// Commands that don't require a package.json
|
|
129
|
+
const standaloneCommands = ['new', 'help', undefined];
|
|
130
|
+
|
|
131
|
+
if (command && !standaloneCommands.includes(command)) {
|
|
132
|
+
checkPackageJson();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
switch (command) {
|
|
136
|
+
case 'new':
|
|
137
|
+
await init(args[0]);
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'dev':
|
|
141
|
+
runCommand('pnpm', ['dev', ...args]);
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'build':
|
|
145
|
+
{
|
|
146
|
+
const { build } = await import(join(CLI_DIR, 'build.mjs'));
|
|
147
|
+
await build(process.cwd());
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case 'mcp':
|
|
152
|
+
case 'mcp:serve':
|
|
153
|
+
if (command === 'mcp:serve' || args[0] === 'serve' || args[0] === ':serve') {
|
|
154
|
+
runCommand('pnpm', ['mcp:serve', ...(command === 'mcp:serve' ? args : args.slice(1))]);
|
|
155
|
+
} else {
|
|
156
|
+
runCommand('pnpm', ['mcp', ...args]);
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case 'lint':
|
|
161
|
+
runCommand('pnpm', ['lint', ...args]);
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case 'typecheck':
|
|
165
|
+
runCommand('pnpm', ['typecheck', ...args]);
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case 'test':
|
|
169
|
+
runCommand('pnpm', ['test', ...args]);
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case 'format':
|
|
173
|
+
runCommand('pnpm', ['format', ...args]);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'validate':
|
|
177
|
+
{
|
|
178
|
+
const { validate } = await import(join(CLI_DIR, 'validate.mjs'));
|
|
179
|
+
await validate(process.cwd());
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case 'help':
|
|
184
|
+
case undefined:
|
|
185
|
+
console.log(`
|
|
186
|
+
☀️ 🏔️ sunpeak - The MCP App SDK
|
|
187
|
+
|
|
188
|
+
Usage:
|
|
189
|
+
sunpeak <command> [options]
|
|
98
190
|
|
|
99
191
|
Commands:
|
|
100
|
-
new [name]
|
|
192
|
+
new [name] Create a new project from template
|
|
193
|
+
dev Start the development server
|
|
194
|
+
build Build all resources for production
|
|
195
|
+
mcp Run the MCP server with nodemon
|
|
196
|
+
mcp:serve Run the MCP server directly
|
|
197
|
+
lint Run ESLint to check code quality
|
|
198
|
+
typecheck Run TypeScript type checking
|
|
199
|
+
test Run tests with Vitest
|
|
200
|
+
format Format code with Prettier
|
|
201
|
+
validate Run full validation suite
|
|
202
|
+
help Show this help message
|
|
203
|
+
|
|
204
|
+
Examples:
|
|
205
|
+
sunpeak new my-app
|
|
206
|
+
sunpeak dev
|
|
207
|
+
sunpeak build
|
|
208
|
+
sunpeak mcp
|
|
209
|
+
|
|
210
|
+
For more information, visit: https://sunpeak.ai/
|
|
101
211
|
`);
|
|
102
|
-
|
|
212
|
+
break;
|
|
213
|
+
|
|
214
|
+
default:
|
|
215
|
+
console.error(`Unknown command: ${command}`);
|
|
216
|
+
console.error('Run "sunpeak help" to see available commands.');
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
})().catch((error) => {
|
|
220
|
+
console.error('Error:', error.message);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
});
|
package/cli/build.mjs
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { existsSync, rmSync, readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect package manager for the project
|
|
8
|
+
*/
|
|
9
|
+
function detectPackageManager(projectRoot) {
|
|
10
|
+
if (existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
11
|
+
if (existsSync(path.join(projectRoot, 'yarn.lock'))) return 'yarn';
|
|
12
|
+
if (existsSync(path.join(projectRoot, 'package-lock.json'))) return 'npm';
|
|
13
|
+
return 'pnpm'; // default
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build all resources for a Sunpeak project
|
|
18
|
+
* Runs in the context of a user's project directory
|
|
19
|
+
*/
|
|
20
|
+
export async function build(projectRoot = process.cwd()) {
|
|
21
|
+
const pm = detectPackageManager(projectRoot);
|
|
22
|
+
|
|
23
|
+
// Check for package.json first
|
|
24
|
+
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
25
|
+
if (!existsSync(pkgJsonPath)) {
|
|
26
|
+
console.error('Error: No package.json found in current directory');
|
|
27
|
+
console.error('Make sure you are in a Sunpeak project directory');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const distDir = path.join(projectRoot, 'dist/chatgpt');
|
|
32
|
+
const buildDir = path.join(projectRoot, 'dist/build-output');
|
|
33
|
+
const tempDir = path.join(projectRoot, '.tmp');
|
|
34
|
+
const resourcesDir = path.join(projectRoot, 'src/components/resources');
|
|
35
|
+
const templateFile = path.join(projectRoot, 'src/index-resource.tsx');
|
|
36
|
+
const viteConfigFile = path.join(projectRoot, 'vite.config.build.ts');
|
|
37
|
+
|
|
38
|
+
// Validate project structure
|
|
39
|
+
if (!existsSync(resourcesDir)) {
|
|
40
|
+
console.error('Error: src/components/resources directory not found');
|
|
41
|
+
console.error('Expected location: ' + resourcesDir);
|
|
42
|
+
console.error('\nThe build command expects the standard Sunpeak project structure.');
|
|
43
|
+
console.error('If you have customized your project structure, you may need to use');
|
|
44
|
+
console.error('a custom build script instead of "sunpeak build".');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!existsSync(templateFile)) {
|
|
49
|
+
console.error('Error: src/index-resource.tsx not found');
|
|
50
|
+
console.error('Expected location: ' + templateFile);
|
|
51
|
+
console.error('\nThis file is the template entry point for building resources.');
|
|
52
|
+
console.error('If you have moved or renamed it, you may need to use a custom build script.');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!existsSync(viteConfigFile)) {
|
|
57
|
+
console.error('Error: vite.config.build.ts not found');
|
|
58
|
+
console.error('Expected location: ' + viteConfigFile);
|
|
59
|
+
console.error('\nThis Vite config is required for building resources.');
|
|
60
|
+
console.error('If you have renamed it, you may need to use a custom build script.');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Clean dist and temp directories
|
|
65
|
+
if (existsSync(distDir)) {
|
|
66
|
+
rmSync(distDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
if (existsSync(tempDir)) {
|
|
69
|
+
rmSync(tempDir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
mkdirSync(distDir, { recursive: true });
|
|
72
|
+
mkdirSync(tempDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
// Auto-discover all resources
|
|
75
|
+
const resourceFiles = readdirSync(resourcesDir)
|
|
76
|
+
.filter(file => file.endsWith('-resource.tsx'))
|
|
77
|
+
.map(file => {
|
|
78
|
+
// Extract kebab-case name: 'counter-resource.tsx' -> 'counter'
|
|
79
|
+
const kebabName = file.replace('-resource.tsx', '');
|
|
80
|
+
|
|
81
|
+
// Convert kebab-case to PascalCase: 'counter' -> 'Counter', 'my-widget' -> 'MyWidget'
|
|
82
|
+
const pascalName = kebabName
|
|
83
|
+
.split('-')
|
|
84
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
85
|
+
.join('');
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
componentName: `${pascalName}Resource`,
|
|
89
|
+
componentFile: file.replace('.tsx', ''),
|
|
90
|
+
entry: `.tmp/index-${kebabName}.tsx`,
|
|
91
|
+
output: `${kebabName}.js`,
|
|
92
|
+
buildOutDir: path.join(buildDir, kebabName),
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (resourceFiles.length === 0) {
|
|
97
|
+
console.error('Error: No resource files found in src/components/resources/');
|
|
98
|
+
console.error('Resource files should be named like: counter-resource.tsx');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log('Building all resources...\n');
|
|
103
|
+
|
|
104
|
+
// Read and validate the template
|
|
105
|
+
const template = readFileSync(templateFile, 'utf-8');
|
|
106
|
+
|
|
107
|
+
// Verify template has required placeholders
|
|
108
|
+
if (!template.includes('// RESOURCE_IMPORT')) {
|
|
109
|
+
console.error('Error: src/index-resource.tsx is missing "// RESOURCE_IMPORT" placeholder');
|
|
110
|
+
console.error('\nThe template file must include this comment where the resource import should go.');
|
|
111
|
+
console.error('If you have customized this file, ensure it has the required placeholders.');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!template.includes('// RESOURCE_MOUNT')) {
|
|
116
|
+
console.error('Error: src/index-resource.tsx is missing "// RESOURCE_MOUNT" placeholder');
|
|
117
|
+
console.error('\nThe template file must include this comment where the resource mount should go.');
|
|
118
|
+
console.error('If you have customized this file, ensure it has the required placeholders.');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Build all resources (but don't copy yet)
|
|
123
|
+
resourceFiles.forEach(({ componentName, componentFile, entry, output, buildOutDir }, index) => {
|
|
124
|
+
console.log(`[${index + 1}/${resourceFiles.length}] Building ${output}...`);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Create build directory if it doesn't exist
|
|
128
|
+
if (!existsSync(buildOutDir)) {
|
|
129
|
+
mkdirSync(buildOutDir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Create entry file from template in temp directory
|
|
133
|
+
const entryContent = template
|
|
134
|
+
.replace('// RESOURCE_IMPORT', `import { ${componentName} } from '../src/components/resources/${componentFile}';`)
|
|
135
|
+
.replace('// RESOURCE_MOUNT', `createRoot(root).render(<${componentName} />);`);
|
|
136
|
+
|
|
137
|
+
const entryPath = path.join(projectRoot, entry);
|
|
138
|
+
writeFileSync(entryPath, entryContent);
|
|
139
|
+
|
|
140
|
+
// Build with vite to build directory
|
|
141
|
+
const viteCommand = pm === 'npm' ? 'npx vite' : `${pm} exec vite`;
|
|
142
|
+
execSync(
|
|
143
|
+
`${viteCommand} build --config vite.config.build.ts`,
|
|
144
|
+
{
|
|
145
|
+
cwd: projectRoot,
|
|
146
|
+
stdio: 'inherit',
|
|
147
|
+
env: {
|
|
148
|
+
...process.env,
|
|
149
|
+
ENTRY_FILE: entry,
|
|
150
|
+
OUTPUT_FILE: output,
|
|
151
|
+
OUT_DIR: buildOutDir,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`Failed to build ${output}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Now copy all files from build-output to dist/chatgpt
|
|
162
|
+
console.log('\nCopying built files to dist/chatgpt...');
|
|
163
|
+
resourceFiles.forEach(({ output, buildOutDir }) => {
|
|
164
|
+
const builtFile = path.join(buildOutDir, output);
|
|
165
|
+
const destFile = path.join(distDir, output);
|
|
166
|
+
|
|
167
|
+
if (existsSync(builtFile)) {
|
|
168
|
+
copyFileSync(builtFile, destFile);
|
|
169
|
+
console.log(`✓ Copied ${output}`);
|
|
170
|
+
} else {
|
|
171
|
+
console.error(`Built file not found: ${builtFile}`);
|
|
172
|
+
if (existsSync(buildOutDir)) {
|
|
173
|
+
console.log(` Files in ${buildOutDir}:`, readdirSync(buildOutDir));
|
|
174
|
+
} else {
|
|
175
|
+
console.log(` Build directory doesn't exist: ${buildOutDir}`);
|
|
176
|
+
}
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Clean up temp and build directories
|
|
182
|
+
if (existsSync(tempDir)) {
|
|
183
|
+
rmSync(tempDir, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
if (existsSync(buildDir)) {
|
|
186
|
+
rmSync(buildDir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('\n✓ All resources built successfully!');
|
|
190
|
+
console.log(`\nBuilt files:`, readdirSync(distDir));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Allow running directly
|
|
194
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
195
|
+
build().catch(error => {
|
|
196
|
+
console.error(error);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|
|
199
|
+
}
|
package/cli/validate.mjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawn } from 'child_process';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import http from 'http';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate a Sunpeak project by running all checks
|
|
9
|
+
* Runs in the context of a user's project directory
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Color codes for output
|
|
13
|
+
const colors = {
|
|
14
|
+
red: '\x1b[0;31m',
|
|
15
|
+
green: '\x1b[0;32m',
|
|
16
|
+
blue: '\x1b[0;34m',
|
|
17
|
+
yellow: '\x1b[1;33m',
|
|
18
|
+
reset: '\x1b[0m',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function printSuccess(text) {
|
|
22
|
+
console.log(`${colors.green}✓ ${text}${colors.reset}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function printError(text) {
|
|
26
|
+
console.log(`${colors.red}✗ ${text}${colors.reset}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printWarning(text) {
|
|
30
|
+
console.log(`${colors.yellow}⚠ ${text}${colors.reset}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function detectPackageManager(projectRoot) {
|
|
34
|
+
if (existsSync(join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
35
|
+
if (existsSync(join(projectRoot, 'yarn.lock'))) return 'yarn';
|
|
36
|
+
if (existsSync(join(projectRoot, 'package-lock.json'))) return 'npm';
|
|
37
|
+
return 'pnpm'; // default
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasScript(projectRoot, scriptName) {
|
|
41
|
+
try {
|
|
42
|
+
const pkgJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8'));
|
|
43
|
+
return !!pkgJson.scripts?.[scriptName];
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runCommand(command, cwd, pm = 'pnpm') {
|
|
50
|
+
try {
|
|
51
|
+
// Replace pnpm with detected package manager
|
|
52
|
+
const actualCommand = command.replace(/^pnpm/, pm);
|
|
53
|
+
execSync(actualCommand, {
|
|
54
|
+
cwd,
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
57
|
+
});
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function waitForServer(port, timeout = 10000) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
const checkServer = () => {
|
|
68
|
+
const req = http.get(`http://localhost:${port}`, () => {
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
req.on('error', () => {
|
|
72
|
+
if (Date.now() - startTime > timeout) {
|
|
73
|
+
reject(new Error(`Server did not start within ${timeout}ms`));
|
|
74
|
+
} else {
|
|
75
|
+
setTimeout(checkServer, 500);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
req.end();
|
|
79
|
+
};
|
|
80
|
+
checkServer();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function validate(projectRoot = process.cwd()) {
|
|
85
|
+
const pm = detectPackageManager(projectRoot);
|
|
86
|
+
|
|
87
|
+
// Check package.json exists
|
|
88
|
+
if (!existsSync(join(projectRoot, 'package.json'))) {
|
|
89
|
+
console.error('Error: No package.json found in current directory');
|
|
90
|
+
console.error('Make sure you are in a Sunpeak project directory');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`${colors.yellow}Starting validation for Sunpeak project...${colors.reset}`);
|
|
95
|
+
console.log(`Project root: ${projectRoot}`);
|
|
96
|
+
console.log(`Package manager: ${pm}\n`);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
console.log(`Running: ${pm} install`);
|
|
100
|
+
if (!runCommand(`${pm} install`, projectRoot, pm)) {
|
|
101
|
+
throw new Error(`${pm} install failed`);
|
|
102
|
+
}
|
|
103
|
+
console.log()
|
|
104
|
+
printSuccess(`${pm} install`);
|
|
105
|
+
|
|
106
|
+
// Format (optional)
|
|
107
|
+
if (hasScript(projectRoot, 'format')) {
|
|
108
|
+
console.log(`\nRunning: ${pm} format`);
|
|
109
|
+
if (!runCommand(`${pm} format`, projectRoot, pm)) {
|
|
110
|
+
throw new Error(`${pm} format failed`);
|
|
111
|
+
}
|
|
112
|
+
printSuccess(`${pm} format`);
|
|
113
|
+
} else {
|
|
114
|
+
printWarning('Skipping format: no "format" script in package.json');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Lint (optional)
|
|
118
|
+
if (hasScript(projectRoot, 'lint')) {
|
|
119
|
+
console.log(`\nRunning: ${pm} lint`);
|
|
120
|
+
if (!runCommand(`${pm} lint`, projectRoot, pm)) {
|
|
121
|
+
throw new Error(`${pm} lint failed`);
|
|
122
|
+
}
|
|
123
|
+
printSuccess(`${pm} lint`);
|
|
124
|
+
} else {
|
|
125
|
+
printWarning('Skipping lint: no "lint" script in package.json');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Typecheck (optional)
|
|
129
|
+
if (hasScript(projectRoot, 'typecheck')) {
|
|
130
|
+
console.log(`\nRunning: ${pm} typecheck`);
|
|
131
|
+
if (!runCommand(`${pm} typecheck`, projectRoot, pm)) {
|
|
132
|
+
throw new Error(`${pm} typecheck failed`);
|
|
133
|
+
}
|
|
134
|
+
printSuccess(`${pm} typecheck`);
|
|
135
|
+
} else {
|
|
136
|
+
printWarning('Skipping typecheck: no "typecheck" script in package.json');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Test (optional)
|
|
140
|
+
if (hasScript(projectRoot, 'test')) {
|
|
141
|
+
console.log(`\nRunning: ${pm} test`);
|
|
142
|
+
if (!runCommand(`${pm} test`, projectRoot, pm)) {
|
|
143
|
+
throw new Error(`${pm} test failed`);
|
|
144
|
+
}
|
|
145
|
+
printSuccess(`${pm} test`);
|
|
146
|
+
} else {
|
|
147
|
+
printWarning('Skipping test: no "test" script in package.json');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`\nRunning: sunpeak build`);
|
|
151
|
+
// Import and run build directly
|
|
152
|
+
const { build } = await import('./build.mjs');
|
|
153
|
+
await build(projectRoot);
|
|
154
|
+
|
|
155
|
+
const chatgptDir = join(projectRoot, 'dist', 'chatgpt');
|
|
156
|
+
|
|
157
|
+
if (!existsSync(chatgptDir)) {
|
|
158
|
+
printError('dist/chatgpt directory not found after build');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const files = readdirSync(chatgptDir);
|
|
163
|
+
if (files.length === 0) {
|
|
164
|
+
printError('No files found in dist/chatgpt/');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Verify all files are .js files
|
|
169
|
+
const nonJsFiles = files.filter(f => !f.endsWith('.js'));
|
|
170
|
+
if (nonJsFiles.length > 0) {
|
|
171
|
+
printError(`Unexpected non-JS files in ./dist/chatgpt/: ${nonJsFiles.join(', ')}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log()
|
|
176
|
+
printSuccess('sunpeak build');
|
|
177
|
+
|
|
178
|
+
// MCP Server Check (optional)
|
|
179
|
+
if (hasScript(projectRoot, 'mcp:serve')) {
|
|
180
|
+
console.log(`\nRunning: ${pm} mcp:serve`);
|
|
181
|
+
const mcpProcess = spawn(pm, ['mcp:serve'], {
|
|
182
|
+
cwd: projectRoot,
|
|
183
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
184
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const mcpErrors = [];
|
|
188
|
+
|
|
189
|
+
mcpProcess.stderr.on('data', (data) => {
|
|
190
|
+
const message = data.toString();
|
|
191
|
+
if (message.includes('error') || message.includes('Error')) {
|
|
192
|
+
mcpErrors.push(message.trim());
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Store process for cleanup
|
|
197
|
+
process.on('exit', () => {
|
|
198
|
+
if (mcpProcess && !mcpProcess.killed) {
|
|
199
|
+
mcpProcess.kill();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
console.log('\nWaiting for MCP server to start on port 6766...');
|
|
205
|
+
await waitForServer(6766, 10000);
|
|
206
|
+
|
|
207
|
+
// Give it a moment to ensure no immediate errors
|
|
208
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
209
|
+
|
|
210
|
+
if (mcpErrors.length > 0) {
|
|
211
|
+
printError('MCP server started but reported errors:');
|
|
212
|
+
mcpErrors.forEach(err => console.log(` ${err}`));
|
|
213
|
+
throw new Error('MCP server has errors');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
printError(`MCP server failed to start: ${error.message}`);
|
|
218
|
+
throw error;
|
|
219
|
+
} finally {
|
|
220
|
+
console.log('Stopping MCP server...');
|
|
221
|
+
mcpProcess.kill();
|
|
222
|
+
// Give it a moment to shut down
|
|
223
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
224
|
+
}
|
|
225
|
+
console.log()
|
|
226
|
+
printSuccess(`${pm} mcp\n`);
|
|
227
|
+
} else {
|
|
228
|
+
printWarning('Skipping MCP server check: no "mcp:serve" script in package.json\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
printSuccess('All systems GO!\n\n');
|
|
232
|
+
process.exit(0);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(`\n${colors.red}Error: ${error.message}${colors.reset}\n`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Allow running directly
|
|
240
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
241
|
+
validate().catch(error => {
|
|
242
|
+
console.error(error);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
245
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sunpeak",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.20",
|
|
4
4
|
"description": "The MCP App SDK. Quickstart, build, & test your ChatGPT App locally!",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
|
35
35
|
"bin",
|
|
36
|
+
"cli",
|
|
36
37
|
"template",
|
|
37
38
|
"README.md"
|
|
38
39
|
],
|