sunpeak 0.5.17 → 0.5.19

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  <div align="center">
2
2
  <a href="https://sunpeak.ai">
3
3
  <picture>
4
- <img alt="Sunpeak logo" src="https://sunpeak.ai/images/sunpeak_github.svg">
4
+ <img alt="Sunpeak logo" src="https://d10djik02wlf6x.cloudfront.net/sunpeak-github.svg">
5
5
  </picture>
6
6
  </a>
7
7
  </div>
@@ -24,7 +24,7 @@ Quickstart, build, and test your ChatGPT App locally with OpenAI apps-sdk-ui Rea
24
24
  <div align="center">
25
25
  <a href="https://docs.sunpeak.ai/library/chatgpt-simulator">
26
26
  <picture>
27
- <img alt="ChatGPT Simulator" src="https://sunpeak.ai/images/chatgpt-simulator.png">
27
+ <img alt="ChatGPT Simulator" src="https://d10djik02wlf6x.cloudfront.net/chatgpt-simulator.png">
28
28
  </picture>
29
29
  </a>
30
30
  </div>
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]: ');
@@ -82,7 +106,8 @@ async function init(projectName) {
82
106
  Done! To get started:
83
107
 
84
108
  cd ${projectName}
85
- pnpm install && pnpm dev
109
+ pnpm install
110
+ sunpeak dev
86
111
 
87
112
  See README.md for more details.
88
113
  `);
@@ -90,13 +115,100 @@ See README.md for more details.
90
115
 
91
116
  const [, , command, ...args] = process.argv;
92
117
 
93
- if (command === 'new') {
94
- init(args[0]);
95
- } else {
96
- console.log(`
97
- sunpeak - The MCP App SDK
118
+ // Main CLI handler
119
+ (async () => {
120
+ // Commands that don't require a package.json
121
+ const standaloneCommands = ['new', 'help', undefined];
122
+
123
+ if (command && !standaloneCommands.includes(command)) {
124
+ checkPackageJson();
125
+ }
126
+
127
+ switch (command) {
128
+ case 'new':
129
+ await init(args[0]);
130
+ break;
131
+
132
+ case 'dev':
133
+ runCommand('pnpm', ['dev', ...args]);
134
+ break;
135
+
136
+ case 'build':
137
+ {
138
+ const { build } = await import(join(CLI_DIR, 'build.mjs'));
139
+ await build(process.cwd());
140
+ }
141
+ break;
142
+
143
+ case 'mcp':
144
+ case 'mcp:serve':
145
+ if (command === 'mcp:serve' || args[0] === 'serve' || args[0] === ':serve') {
146
+ runCommand('pnpm', ['mcp:serve', ...(command === 'mcp:serve' ? args : args.slice(1))]);
147
+ } else {
148
+ runCommand('pnpm', ['mcp', ...args]);
149
+ }
150
+ break;
151
+
152
+ case 'lint':
153
+ runCommand('pnpm', ['lint', ...args]);
154
+ break;
155
+
156
+ case 'typecheck':
157
+ runCommand('pnpm', ['typecheck', ...args]);
158
+ break;
159
+
160
+ case 'test':
161
+ runCommand('pnpm', ['test', ...args]);
162
+ break;
163
+
164
+ case 'format':
165
+ runCommand('pnpm', ['format', ...args]);
166
+ break;
167
+
168
+ case 'validate':
169
+ {
170
+ const { validate } = await import(join(CLI_DIR, 'validate.mjs'));
171
+ await validate(process.cwd());
172
+ }
173
+ break;
174
+
175
+ case 'help':
176
+ case undefined:
177
+ console.log(`
178
+ ☀️ 🏔️ sunpeak - The MCP App SDK
179
+
180
+ Usage:
181
+ sunpeak <command> [options]
98
182
 
99
183
  Commands:
100
- new [name] Create a new project from template
184
+ new [name] Create a new project from template
185
+ dev Start the development server
186
+ build Build all resources for production
187
+ mcp Run the MCP server with nodemon
188
+ mcp:serve Run the MCP server directly
189
+ lint Run ESLint to check code quality
190
+ typecheck Run TypeScript type checking
191
+ test Run tests with Vitest
192
+ format Format code with Prettier
193
+ validate Run full validation suite
194
+ help Show this help message
195
+
196
+ Examples:
197
+ sunpeak new my-app
198
+ sunpeak dev
199
+ sunpeak build
200
+ sunpeak mcp
201
+
202
+ For more information, visit: https://sunpeak.ai/
101
203
  `);
102
- }
204
+ break;
205
+
206
+ default:
207
+ console.error(`Unknown command: ${command}`);
208
+ console.error('Run "sunpeak help" to see available commands.');
209
+ process.exit(1);
210
+ }
211
+ })().catch((error) => {
212
+ console.error('Error:', error.message);
213
+ process.exit(1);
214
+ });
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
+ }
@@ -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.17",
3
+ "version": "0.5.19",
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
  ],
@@ -7,12 +7,44 @@ For an initial overview of your new app and the sunpeak API, refer to the [docum
7
7
  ## Quickstart
8
8
 
9
9
  ```bash
10
- pnpm dev
10
+ pnpm install
11
+ sunpeak dev
11
12
  ```
12
13
 
13
14
  Edit the resource files in [./src/components/resources/](./src/components/resources/) to build your resource UI.
14
15
 
15
- ## Development
16
+ ## CLI Commands
17
+
18
+ The `sunpeak` CLI provides convenient commands for common tasks:
19
+
20
+ ```bash
21
+ sunpeak dev # Start development server (vite)
22
+ sunpeak build # Build all resources for production
23
+ sunpeak mcp # Run MCP server with auto-reload
24
+ sunpeak validate # Run full validation suite
25
+ sunpeak test # Run tests
26
+ sunpeak typecheck # Run TypeScript checks
27
+ sunpeak lint # Run linting
28
+ sunpeak format # Format code
29
+ ```
30
+
31
+ ### Customization
32
+
33
+ **You can customize:**
34
+
35
+ - Package.json scripts (format, lint, typecheck, test are optional)
36
+ - Tooling configuration (ESLint, Prettier, TypeScript, Vite dev server)
37
+ - Component structure within `src/components/`
38
+ - Package manager (pnpm, npm, or yarn auto-detected)
39
+
40
+ **Do not customize (required by `sunpeak build`):**
41
+
42
+ - `src/components/resources/` - Resource files must be here
43
+ - `src/index-resource.tsx` - Build template (must have `// RESOURCE_IMPORT` and `// RESOURCE_MOUNT` comments)
44
+ - `vite.config.build.ts` - Build configuration
45
+ - Resource file naming: `*-resource.tsx` (e.g., `counter-resource.tsx`)
46
+
47
+ If you need to customize these paths, create a custom build script in package.json instead of using `sunpeak build`.
16
48
 
17
49
  ## Testing
18
50
 
@@ -21,7 +53,7 @@ Edit the resource files in [./src/components/resources/](./src/components/resour
21
53
  Run all the checks with the following:
22
54
 
23
55
  ```bash
24
- pnpm validate
56
+ sunpeak validate
25
57
  ```
26
58
 
27
59
  This will:
@@ -33,7 +65,7 @@ This will:
33
65
  For manual QA of the UI, run:
34
66
 
35
67
  ```bash
36
- pnpm dev
68
+ sunpeak dev
37
69
  ```
38
70
 
39
71
  ### Testing in ChatGPT
@@ -42,7 +74,7 @@ Test your app directly in ChatGPT using the built-in MCP server:
42
74
 
43
75
  ```bash
44
76
  # Start the MCP server (rebuilds and restarts on file changes).
45
- pnpm mcp
77
+ sunpeak mcp
46
78
 
47
79
  # In another terminal, run a tunnel. For example:
48
80
  ngrok http 6766
@@ -59,13 +91,17 @@ When you make changes to the UI, refresh your app in ChatGPT after the MCP serve
59
91
  Build your app for production:
60
92
 
61
93
  ```bash
62
- pnpm build
94
+ sunpeak build
63
95
  ```
64
96
 
65
- This creates optimized builds in the `dist/` directory:
97
+ This creates optimized builds in `dist/chatgpt/`:
98
+
99
+ - `dist/chatgpt/counter.js`
100
+ - `dist/chatgpt/albums.js`
101
+ - `dist/chatgpt/carousel.js`
102
+ - _(One .js file per resource in src/components/resources/)_
66
103
 
67
- - `dist/chatgpt/index.js` - ChatGPT iframe component
68
- - Host this file somewhere and reference it as a resource in your production MCP server.
104
+ Each file is a self-contained bundle with CSS inlined. Host these files and reference them as resources in your production MCP server.
69
105
 
70
106
  ## Resources
71
107
 
@@ -2,9 +2,9 @@ import {
2
2
  Button,
3
3
  ButtonLink,
4
4
  CopyButton
5
- } from "./chunk-PRLY65A2.js";
6
- import "./chunk-XB525PXG.js";
5
+ } from "./chunk-675LFNY2.js";
7
6
  import "./chunk-QPJAV452.js";
7
+ import "./chunk-XB525PXG.js";
8
8
  import "./chunk-YOJ6QPGS.js";
9
9
  import "./chunk-BAG6OO6S.js";
10
10
  import "./chunk-EGRHWZRV.js";
@@ -5,11 +5,11 @@ import {
5
5
  handlePressableMouseEnter,
6
6
  waitForAnimationFrame
7
7
  } from "./chunk-BAG6OO6S.js";
8
+ import "./chunk-EGRHWZRV.js";
8
9
  import {
9
10
  dist_exports4 as dist_exports
10
11
  } from "./chunk-SGWD4VEU.js";
11
12
  import "./chunk-KFGKZMLK.js";
12
- import "./chunk-EGRHWZRV.js";
13
13
  import {
14
14
  clsx_default
15
15
  } from "./chunk-CNYJBM5F.js";
@@ -1,8 +1,14 @@
1
+ import {
2
+ Input
3
+ } from "./chunk-CQ3GYAYB.js";
1
4
  import {
2
5
  Button,
3
6
  LoadingIndicator,
4
7
  TransitionGroup
5
- } from "./chunk-PRLY65A2.js";
8
+ } from "./chunk-675LFNY2.js";
9
+ import {
10
+ o
11
+ } from "./chunk-QPJAV452.js";
6
12
  import {
7
13
  Check_default,
8
14
  ChevronDownVector_default,
@@ -11,12 +17,6 @@ import {
11
17
  Search_default,
12
18
  X_default
13
19
  } from "./chunk-XB525PXG.js";
14
- import {
15
- Input
16
- } from "./chunk-CQ3GYAYB.js";
17
- import {
18
- o
19
- } from "./chunk-QPJAV452.js";
20
20
  import {
21
21
  useTimeout
22
22
  } from "./chunk-YOJ6QPGS.js";
@@ -26,13 +26,13 @@ import {
26
26
  toCssVariables,
27
27
  waitForAnimationFrame
28
28
  } from "./chunk-BAG6OO6S.js";
29
+ import "./chunk-EGRHWZRV.js";
29
30
  import {
30
31
  dist_exports,
31
32
  dist_exports3 as dist_exports2,
32
33
  dist_exports5 as dist_exports3
33
34
  } from "./chunk-SGWD4VEU.js";
34
35
  import "./chunk-KFGKZMLK.js";
35
- import "./chunk-EGRHWZRV.js";
36
36
  import {
37
37
  clsx_default
38
38
  } from "./chunk-CNYJBM5F.js";
@@ -7,134 +7,134 @@
7
7
  "react": {
8
8
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/index.js",
9
9
  "file": "react.js",
10
- "fileHash": "9f9292ff",
10
+ "fileHash": "abb9cab2",
11
11
  "needsInterop": true
12
12
  },
13
13
  "react-dom": {
14
14
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.0_react@19.2.0/node_modules/react-dom/index.js",
15
15
  "file": "react-dom.js",
16
- "fileHash": "4943b018",
16
+ "fileHash": "8575f6ec",
17
17
  "needsInterop": true
18
18
  },
19
19
  "react/jsx-dev-runtime": {
20
20
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/jsx-dev-runtime.js",
21
21
  "file": "react_jsx-dev-runtime.js",
22
- "fileHash": "13cb4e82",
22
+ "fileHash": "9472cef2",
23
23
  "needsInterop": true
24
24
  },
25
25
  "react/jsx-runtime": {
26
26
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/jsx-runtime.js",
27
27
  "file": "react_jsx-runtime.js",
28
- "fileHash": "c1b7a323",
28
+ "fileHash": "08af77d4",
29
29
  "needsInterop": true
30
30
  },
31
31
  "@openai/apps-sdk-ui/components/Button": {
32
32
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Button/index.js",
33
33
  "file": "@openai_apps-sdk-ui_components_Button.js",
34
- "fileHash": "d11f10e3",
34
+ "fileHash": "9937baa5",
35
35
  "needsInterop": false
36
36
  },
37
37
  "@openai/apps-sdk-ui/components/Checkbox": {
38
38
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Checkbox/index.js",
39
39
  "file": "@openai_apps-sdk-ui_components_Checkbox.js",
40
- "fileHash": "48b405d7",
40
+ "fileHash": "4cc1f6d9",
41
41
  "needsInterop": false
42
42
  },
43
43
  "@openai/apps-sdk-ui/components/Icon": {
44
44
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Icon/index.js",
45
45
  "file": "@openai_apps-sdk-ui_components_Icon.js",
46
- "fileHash": "82b7cd9b",
46
+ "fileHash": "7a6d731b",
47
47
  "needsInterop": false
48
48
  },
49
49
  "@openai/apps-sdk-ui/components/Input": {
50
50
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Input/index.js",
51
51
  "file": "@openai_apps-sdk-ui_components_Input.js",
52
- "fileHash": "25d68597",
52
+ "fileHash": "35b40b58",
53
53
  "needsInterop": false
54
54
  },
55
55
  "@openai/apps-sdk-ui/components/SegmentedControl": {
56
56
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/SegmentedControl/index.js",
57
57
  "file": "@openai_apps-sdk-ui_components_SegmentedControl.js",
58
- "fileHash": "6f65ea54",
58
+ "fileHash": "62dffaa3",
59
59
  "needsInterop": false
60
60
  },
61
61
  "@openai/apps-sdk-ui/components/Select": {
62
62
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Select/index.js",
63
63
  "file": "@openai_apps-sdk-ui_components_Select.js",
64
- "fileHash": "cd7121dd",
64
+ "fileHash": "28afd0cc",
65
65
  "needsInterop": false
66
66
  },
67
67
  "@openai/apps-sdk-ui/components/Textarea": {
68
68
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Textarea/index.js",
69
69
  "file": "@openai_apps-sdk-ui_components_Textarea.js",
70
- "fileHash": "37a29f03",
70
+ "fileHash": "cf25f4ed",
71
71
  "needsInterop": false
72
72
  },
73
73
  "@openai/apps-sdk-ui/theme": {
74
74
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/lib/theme.js",
75
75
  "file": "@openai_apps-sdk-ui_theme.js",
76
- "fileHash": "cec2c357",
76
+ "fileHash": "bd38d415",
77
77
  "needsInterop": false
78
78
  },
79
79
  "clsx": {
80
80
  "src": "../../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs",
81
81
  "file": "clsx.js",
82
- "fileHash": "7a00c4b1",
82
+ "fileHash": "e128f89b",
83
83
  "needsInterop": false
84
84
  },
85
85
  "embla-carousel-react": {
86
86
  "src": "../../../../node_modules/.pnpm/embla-carousel-react@8.6.0_react@19.2.0/node_modules/embla-carousel-react/esm/embla-carousel-react.esm.js",
87
87
  "file": "embla-carousel-react.js",
88
- "fileHash": "7977568f",
88
+ "fileHash": "9e2b9226",
89
89
  "needsInterop": false
90
90
  },
91
91
  "embla-carousel-wheel-gestures": {
92
92
  "src": "../../../../node_modules/.pnpm/embla-carousel-wheel-gestures@8.1.0_embla-carousel@8.6.0/node_modules/embla-carousel-wheel-gestures/dist/embla-carousel-wheel-gestures.esm.js",
93
93
  "file": "embla-carousel-wheel-gestures.js",
94
- "fileHash": "c5f8d825",
94
+ "fileHash": "6cc0c995",
95
95
  "needsInterop": false
96
96
  },
97
97
  "react-dom/client": {
98
98
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.0_react@19.2.0/node_modules/react-dom/client.js",
99
99
  "file": "react-dom_client.js",
100
- "fileHash": "8b09036e",
100
+ "fileHash": "783cb9f4",
101
101
  "needsInterop": true
102
102
  },
103
103
  "tailwind-merge": {
104
104
  "src": "../../../../node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.mjs",
105
105
  "file": "tailwind-merge.js",
106
- "fileHash": "5d548d9e",
106
+ "fileHash": "e1ba7ad3",
107
107
  "needsInterop": false
108
108
  }
109
109
  },
110
110
  "chunks": {
111
- "chunk-PRLY65A2": {
112
- "file": "chunk-PRLY65A2.js"
113
- },
114
- "chunk-XB525PXG": {
115
- "file": "chunk-XB525PXG.js"
116
- },
117
111
  "chunk-CQ3GYAYB": {
118
112
  "file": "chunk-CQ3GYAYB.js"
119
113
  },
114
+ "chunk-675LFNY2": {
115
+ "file": "chunk-675LFNY2.js"
116
+ },
120
117
  "chunk-QPJAV452": {
121
118
  "file": "chunk-QPJAV452.js"
122
119
  },
120
+ "chunk-XB525PXG": {
121
+ "file": "chunk-XB525PXG.js"
122
+ },
123
123
  "chunk-YOJ6QPGS": {
124
124
  "file": "chunk-YOJ6QPGS.js"
125
125
  },
126
126
  "chunk-BAG6OO6S": {
127
127
  "file": "chunk-BAG6OO6S.js"
128
128
  },
129
+ "chunk-EGRHWZRV": {
130
+ "file": "chunk-EGRHWZRV.js"
131
+ },
129
132
  "chunk-SGWD4VEU": {
130
133
  "file": "chunk-SGWD4VEU.js"
131
134
  },
132
135
  "chunk-KFGKZMLK": {
133
136
  "file": "chunk-KFGKZMLK.js"
134
137
  },
135
- "chunk-EGRHWZRV": {
136
- "file": "chunk-EGRHWZRV.js"
137
- },
138
138
  "chunk-CNYJBM5F": {
139
139
  "file": "chunk-CNYJBM5F.js"
140
140
  },
@@ -1,10 +1,10 @@
1
+ import {
2
+ o
3
+ } from "./chunk-QPJAV452.js";
1
4
  import {
2
5
  Check_default,
3
6
  Copy_default
4
7
  } from "./chunk-XB525PXG.js";
5
- import {
6
- o
7
- } from "./chunk-QPJAV452.js";
8
8
  import {
9
9
  useTimeout
10
10
  } from "./chunk-YOJ6QPGS.js";
@@ -625,4 +625,4 @@ export {
625
625
  ButtonLink,
626
626
  CopyButton
627
627
  };
628
- //# sourceMappingURL=chunk-PRLY65A2.js.map
628
+ //# sourceMappingURL=chunk-675LFNY2.js.map
@@ -1 +1 @@
1
- {"version":"4.0.13","results":[[":src/components/album/fullscreen-viewer.test.tsx",{"duration":225.97365200000013,"failed":false}],[":src/components/resources/carousel-resource.test.tsx",{"duration":254.122474,"failed":false}],[":src/components/album/albums.test.tsx",{"duration":316.821197,"failed":false}],[":src/components/resources/counter-resource.test.tsx",{"duration":330.27491199999986,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":86.61474699999962,"failed":false}],[":src/components/resources/albums-resource.test.tsx",{"duration":280.079291,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":461.38244800000007,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":341.3352309999998,"failed":false}],[":src/components/card/card.test.tsx",{"duration":54.68800999999985,"failed":false}]]}
1
+ {"version":"4.0.13","results":[[":src/components/resources/carousel-resource.test.tsx",{"duration":272.0362339999999,"failed":false}],[":src/components/album/albums.test.tsx",{"duration":303.67703000000006,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":280.3775370000003,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":87.16681699999981,"failed":false}],[":src/components/resources/counter-resource.test.tsx",{"duration":299.4044319999998,"failed":false}],[":src/components/resources/albums-resource.test.tsx",{"duration":289.73864500000013,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":432.83026500000005,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":344.5009920000002,"failed":false}],[":src/components/card/card.test.tsx",{"duration":54.60665700000004,"failed":false}]]}
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build": "node scripts/build-all.mjs",
7
+ "build": "sunpeak build",
8
8
  "dev": "vite --port ${PORT:-6767}",
9
9
  "format": "prettier --write --list-different \"**/*.{ts,tsx,js,jsx,json,md}\"",
10
10
  "mcp": "nodemon",
@@ -12,7 +12,7 @@
12
12
  "lint": "eslint . --ext .ts,.tsx --fix",
13
13
  "typecheck": "tsc --noEmit",
14
14
  "test": "vitest run",
15
- "validate": "node scripts/validate.mjs"
15
+ "validate": "sunpeak validate"
16
16
  },
17
17
  "dependencies": {
18
18
  "@openai/apps-sdk-ui": "^0.2.0",
@@ -1,117 +0,0 @@
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
- import { fileURLToPath } from 'url';
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const distDir = path.join(__dirname, '../dist/chatgpt');
9
- const buildDir = path.join(__dirname, '../dist/build-output');
10
- const tempDir = path.join(__dirname, '../.tmp');
11
- const resourcesDir = path.join(__dirname, '../src/components/resources');
12
- const templateFile = path.join(__dirname, '../src/index-resource.tsx');
13
-
14
- // Clean dist and temp directories
15
- if (existsSync(distDir)) {
16
- rmSync(distDir, { recursive: true });
17
- }
18
- if (existsSync(tempDir)) {
19
- rmSync(tempDir, { recursive: true });
20
- }
21
- mkdirSync(distDir, { recursive: true });
22
- mkdirSync(tempDir, { recursive: true });
23
-
24
- // Auto-discover all resources
25
- const resourceFiles = readdirSync(resourcesDir)
26
- .filter(file => file.endsWith('-resource.tsx'))
27
- .map(file => {
28
- // Extract kebab-case name: 'counter-resource.tsx' -> 'counter'
29
- const kebabName = file.replace('-resource.tsx', '');
30
-
31
- // Convert kebab-case to PascalCase: 'counter' -> 'Counter', 'my-widget' -> 'MyWidget'
32
- const pascalName = kebabName
33
- .split('-')
34
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
35
- .join('');
36
-
37
- return {
38
- componentName: `${pascalName}Resource`,
39
- componentFile: file.replace('.tsx', ''),
40
- entry: `.tmp/index-${kebabName}.tsx`,
41
- output: `${kebabName}.js`,
42
- buildOutDir: path.join(buildDir, kebabName),
43
- };
44
- });
45
-
46
- console.log('Building all resources...\n');
47
-
48
- // Read the template
49
- const template = readFileSync(templateFile, 'utf-8');
50
-
51
- // Build all resources (but don't copy yet)
52
- resourceFiles.forEach(({ componentName, componentFile, entry, output, buildOutDir }, index) => {
53
- console.log(`[${index + 1}/${resourceFiles.length}] Building ${output}...`);
54
-
55
- try {
56
- // Create build directory if it doesn't exist
57
- if (!existsSync(buildOutDir)) {
58
- mkdirSync(buildOutDir, { recursive: true });
59
- }
60
-
61
- // Create entry file from template in temp directory
62
- const entryContent = template
63
- .replace('// RESOURCE_IMPORT', `import { ${componentName} } from '../src/components/resources/${componentFile}';`)
64
- .replace('// RESOURCE_MOUNT', `createRoot(root).render(<${componentName} />);`);
65
-
66
- const entryPath = path.join(__dirname, '..', entry);
67
- writeFileSync(entryPath, entryContent);
68
-
69
- // Build with vite to build directory
70
- execSync(
71
- `vite build --config vite.config.build.ts`,
72
- {
73
- stdio: 'inherit',
74
- env: {
75
- ...process.env,
76
- ENTRY_FILE: entry,
77
- OUTPUT_FILE: output,
78
- OUT_DIR: buildOutDir,
79
- },
80
- }
81
- );
82
- } catch (error) {
83
- console.error(`Failed to build ${output}`);
84
- process.exit(1);
85
- }
86
- });
87
-
88
- // Now copy all files from build-output to dist/chatgpt
89
- console.log('\nCopying built files to dist/chatgpt...');
90
- resourceFiles.forEach(({ output, buildOutDir }) => {
91
- const builtFile = path.join(buildOutDir, output);
92
- const destFile = path.join(distDir, output);
93
-
94
- if (existsSync(builtFile)) {
95
- copyFileSync(builtFile, destFile);
96
- console.log(`✓ Copied ${output}`);
97
- } else {
98
- console.error(`Built file not found: ${builtFile}`);
99
- if (existsSync(buildOutDir)) {
100
- console.log(` Files in ${buildOutDir}:`, readdirSync(buildOutDir));
101
- } else {
102
- console.log(` Build directory doesn't exist: ${buildOutDir}`);
103
- }
104
- process.exit(1);
105
- }
106
- });
107
-
108
- // Clean up temp and build directories
109
- if (existsSync(tempDir)) {
110
- rmSync(tempDir, { recursive: true });
111
- }
112
- if (existsSync(buildDir)) {
113
- rmSync(buildDir, { recursive: true });
114
- }
115
-
116
- console.log('\n✓ All resources built successfully!');
117
- console.log(`\nBuilt files:`, readdirSync(distDir));
@@ -1,186 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Local testing script for Sunpeak project.
5
- */
6
-
7
- import { execSync, spawn } from 'child_process';
8
- import { existsSync, readdirSync } from 'fs';
9
- import { fileURLToPath } from 'url';
10
- import { dirname, join } from 'path';
11
- import http from 'http';
12
-
13
- // Color codes for output
14
- const colors = {
15
- red: '\x1b[0;31m',
16
- green: '\x1b[0;32m',
17
- blue: '\x1b[0;34m',
18
- yellow: '\x1b[1;33m',
19
- reset: '\x1b[0m',
20
- };
21
-
22
- // Get project root
23
- const __filename = fileURLToPath(import.meta.url);
24
- const __dirname = dirname(__filename);
25
- const PROJECT_ROOT = join(__dirname, '..');
26
-
27
- function printSuccess(text) {
28
- console.log(`${colors.green}✓ ${text}${colors.reset}`);
29
- }
30
-
31
- function printError(text) {
32
- console.log(`${colors.red}✗ ${text}${colors.reset}`);
33
- }
34
-
35
- function runCommand(command, cwd) {
36
- try {
37
- execSync(command, {
38
- cwd,
39
- stdio: 'inherit',
40
- env: { ...process.env, FORCE_COLOR: '1' },
41
- });
42
- return true;
43
- } catch (error) {
44
- return false;
45
- }
46
- }
47
-
48
- function waitForServer(port, timeout = 10000) {
49
- return new Promise((resolve, reject) => {
50
- const startTime = Date.now();
51
- const checkServer = () => {
52
- const req = http.get(`http://localhost:${port}`, () => {
53
- resolve();
54
- });
55
- req.on('error', () => {
56
- if (Date.now() - startTime > timeout) {
57
- reject(new Error(`Server did not start within ${timeout}ms`));
58
- } else {
59
- setTimeout(checkServer, 500);
60
- }
61
- });
62
- req.end();
63
- };
64
- checkServer();
65
- });
66
- }
67
-
68
- // Main testing flow
69
- console.log(`${colors.yellow}Starting local testing for Sunpeak project...${colors.reset}`);
70
- console.log(`Project root: ${PROJECT_ROOT}\n`);
71
-
72
- try {
73
- console.log('Running: pnpm install');
74
- if (!runCommand('pnpm install', PROJECT_ROOT)) {
75
- throw new Error('pnpm install failed');
76
- }
77
- console.log()
78
- printSuccess('pnpm install');
79
-
80
- console.log('\nRunning: pnpm format');
81
- if (!runCommand('pnpm format', PROJECT_ROOT)) {
82
- throw new Error('pnpm format failed');
83
- }
84
- printSuccess('pnpm format');
85
-
86
- console.log('\nRunning: pnpm lint');
87
- if (!runCommand('pnpm lint', PROJECT_ROOT)) {
88
- throw new Error('pnpm lint failed');
89
- }
90
- printSuccess('pnpm lint');
91
-
92
- console.log('\nRunning: pnpm typecheck');
93
- if (!runCommand('pnpm typecheck', PROJECT_ROOT)) {
94
- throw new Error('pnpm typecheck failed');
95
- }
96
- printSuccess('pnpm typecheck');
97
-
98
- console.log('\nRunning: pnpm test');
99
- if (!runCommand('pnpm test', PROJECT_ROOT)) {
100
- throw new Error('pnpm test failed');
101
- }
102
- printSuccess('pnpm test');
103
-
104
- console.log('\nRunning: pnpm build');
105
- if (!runCommand('pnpm build', PROJECT_ROOT)) {
106
- throw new Error('pnpm build failed');
107
- }
108
- const chatgptDir = join(PROJECT_ROOT, 'dist', 'chatgpt');
109
- const expectedFiles = ['counter.js', 'albums.js', 'carousel.js'];
110
-
111
- // Check all expected files exist
112
- for (const file of expectedFiles) {
113
- const filePath = join(chatgptDir, file);
114
- if (!existsSync(filePath)) {
115
- printError(`Missing expected file: ./dist/chatgpt/${file}`);
116
- process.exit(1);
117
- }
118
- }
119
-
120
- // Verify only expected files are present
121
- const files = readdirSync(chatgptDir);
122
- const unexpectedFiles = files.filter(f => !expectedFiles.includes(f));
123
- if (unexpectedFiles.length > 0) {
124
- printError(`Unexpected files in ./dist/chatgpt/: ${unexpectedFiles.join(', ')}`);
125
- printError(`Expected only: ${expectedFiles.join(', ')}`);
126
- process.exit(1);
127
- }
128
-
129
- console.log()
130
- printSuccess('pnpm build');
131
-
132
- // MCP Server Check
133
- console.log('\nRunning: pnpm mcp:serve');
134
- const mcpProcess = spawn('pnpm', ['mcp:serve'], {
135
- cwd: PROJECT_ROOT,
136
- stdio: ['ignore', 'pipe', 'pipe'],
137
- env: { ...process.env, FORCE_COLOR: '1' },
138
- });
139
-
140
- const mcpErrors = [];
141
-
142
- mcpProcess.stderr.on('data', (data) => {
143
- const message = data.toString();
144
- if (message.includes('error') || message.includes('Error')) {
145
- mcpErrors.push(message.trim());
146
- }
147
- });
148
-
149
- // Store process for cleanup
150
- process.on('exit', () => {
151
- if (mcpProcess && !mcpProcess.killed) {
152
- mcpProcess.kill();
153
- }
154
- });
155
-
156
- try {
157
- console.log('\nWaiting for MCP server to start on port 6766...');
158
- await waitForServer(6766, 10000);
159
-
160
- // Give it a moment to ensure no immediate errors
161
- await new Promise(resolve => setTimeout(resolve, 1000));
162
-
163
- if (mcpErrors.length > 0) {
164
- printError('MCP server started but reported errors:');
165
- mcpErrors.forEach(err => console.log(` ${err}`));
166
- throw new Error('MCP server has errors');
167
- }
168
-
169
- } catch (error) {
170
- printError(`MCP server failed to start: ${error.message}`);
171
- throw error;
172
- } finally {
173
- console.log('Stopping MCP server...');
174
- mcpProcess.kill();
175
- // Give it a moment to shut down
176
- await new Promise(resolve => setTimeout(resolve, 1000));
177
- }
178
- console.log()
179
- printSuccess('pnpm mcp\n');
180
-
181
- printSuccess('All systems GO!\n\n');
182
- process.exit(0);
183
- } catch (error) {
184
- console.error(`\n${colors.red}Error: ${error.message}${colors.reset}\n`);
185
- process.exit(1);
186
- }