create-brightsy-component-lib 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1105 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ /**
4
+ * create-brightsy-component-lib
5
+ *
6
+ * CLI tool to scaffold and deploy Brightsy component library projects.
7
+ *
8
+ * Usage:
9
+ * npx create-brightsy-component-lib my-components
10
+ * npx create-brightsy-component-lib my-components --template app
11
+ * npx create-brightsy-component-lib login
12
+ * npx create-brightsy-component-lib deploy
13
+ */
14
+ import { program } from 'commander';
15
+ import prompts from 'prompts';
16
+ import chalk from 'chalk';
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import { fileURLToPath } from 'url';
20
+ import { execSync } from 'child_process';
21
+ import { createServer } from 'http';
22
+ import { randomBytes, createHash } from 'crypto';
23
+ import { URL } from 'url';
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+ // Config file location
27
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.brightsy');
28
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
29
+ // Load saved config
30
+ function loadConfig() {
31
+ try {
32
+ if (fs.existsSync(CONFIG_FILE)) {
33
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
34
+ }
35
+ }
36
+ catch (e) {
37
+ // Ignore errors
38
+ }
39
+ return {};
40
+ }
41
+ // Save config
42
+ function saveConfig(config) {
43
+ if (!fs.existsSync(CONFIG_DIR)) {
44
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
45
+ }
46
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
47
+ // Set restrictive permissions
48
+ fs.chmodSync(CONFIG_FILE, 0o600);
49
+ }
50
+ // Generate PKCE code verifier and challenge
51
+ function generatePKCE() {
52
+ const verifier = randomBytes(32).toString('base64url');
53
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
54
+ return { verifier, challenge };
55
+ }
56
+ // OAuth login flow
57
+ async function login(options) {
58
+ console.log(BANNER);
59
+ console.log(chalk.cyan('Authenticating with Brightsy...\n'));
60
+ const endpoint = options.endpoint || 'https://brightsy.ai';
61
+ const clientId = 'brightsy-cli';
62
+ const redirectPort = 8976;
63
+ const redirectUri = `http://localhost:${redirectPort}/callback`;
64
+ const { verifier, challenge } = generatePKCE();
65
+ const state = randomBytes(16).toString('hex');
66
+ // Build authorization URL
67
+ const authUrl = new URL(`${endpoint}/oauth/authorize`);
68
+ authUrl.searchParams.set('client_id', clientId);
69
+ authUrl.searchParams.set('redirect_uri', redirectUri);
70
+ authUrl.searchParams.set('response_type', 'code');
71
+ authUrl.searchParams.set('scope', 'openid profile files libraries');
72
+ authUrl.searchParams.set('code_challenge', challenge);
73
+ authUrl.searchParams.set('code_challenge_method', 'S256');
74
+ authUrl.searchParams.set('state', state);
75
+ console.log(chalk.cyan('Opening browser for authentication...'));
76
+ console.log(chalk.dim(`If the browser doesn't open, visit:\n${authUrl.toString()}\n`));
77
+ // Open browser
78
+ const openCommand = process.platform === 'darwin' ? 'open' :
79
+ process.platform === 'win32' ? 'start' : 'xdg-open';
80
+ try {
81
+ execSync(`${openCommand} "${authUrl.toString()}"`, { stdio: 'ignore' });
82
+ }
83
+ catch (e) {
84
+ // Browser open failed, user will need to copy URL manually
85
+ }
86
+ // Start local server to receive callback
87
+ return new Promise((resolve, reject) => {
88
+ const server = createServer(async (req, res) => {
89
+ const url = new URL(req.url || '', `http://localhost:${redirectPort}`);
90
+ if (url.pathname === '/callback') {
91
+ const code = url.searchParams.get('code');
92
+ const returnedState = url.searchParams.get('state');
93
+ const error = url.searchParams.get('error');
94
+ if (error) {
95
+ res.writeHead(400, { 'Content-Type': 'text/html' });
96
+ res.end(`<html><body><h1>Authentication Failed</h1><p>${error}</p><script>window.close()</script></body></html>`);
97
+ server.close();
98
+ reject(new Error(error));
99
+ return;
100
+ }
101
+ if (returnedState !== state) {
102
+ res.writeHead(400, { 'Content-Type': 'text/html' });
103
+ res.end(`<html><body><h1>Authentication Failed</h1><p>State mismatch</p><script>window.close()</script></body></html>`);
104
+ server.close();
105
+ reject(new Error('State mismatch - possible CSRF attack'));
106
+ return;
107
+ }
108
+ if (!code) {
109
+ res.writeHead(400, { 'Content-Type': 'text/html' });
110
+ res.end(`<html><body><h1>Authentication Failed</h1><p>No code received</p><script>window.close()</script></body></html>`);
111
+ server.close();
112
+ reject(new Error('No authorization code received'));
113
+ return;
114
+ }
115
+ try {
116
+ // Exchange code for tokens
117
+ const tokenResponse = await fetch(`${endpoint}/oauth/token`, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({
121
+ grant_type: 'authorization_code',
122
+ client_id: clientId,
123
+ code,
124
+ redirect_uri: redirectUri,
125
+ code_verifier: verifier,
126
+ })
127
+ });
128
+ if (!tokenResponse.ok) {
129
+ const error = await tokenResponse.json().catch(() => ({}));
130
+ throw new Error(error.error_description || error.error || 'Token exchange failed');
131
+ }
132
+ const tokens = await tokenResponse.json();
133
+ // Get user info to find account
134
+ const userResponse = await fetch(`${endpoint}/oauth/userinfo`, {
135
+ headers: { 'Authorization': `Bearer ${tokens.access_token}` }
136
+ });
137
+ let accountId = '';
138
+ let accountSlug = '';
139
+ if (userResponse.ok) {
140
+ const userInfo = await userResponse.json();
141
+ // If user has accounts, let them select one
142
+ if (userInfo.accounts && userInfo.accounts.length > 0) {
143
+ if (userInfo.accounts.length === 1) {
144
+ accountId = userInfo.accounts[0].id;
145
+ accountSlug = userInfo.accounts[0].slug;
146
+ }
147
+ else {
148
+ // Multiple accounts - will prompt after
149
+ console.log(chalk.yellow('\nMultiple accounts found. Please select one:'));
150
+ const { selectedAccount } = await prompts({
151
+ type: 'select',
152
+ name: 'selectedAccount',
153
+ message: 'Select account:',
154
+ choices: userInfo.accounts.map((acc) => ({
155
+ title: acc.name || acc.slug,
156
+ value: acc,
157
+ }))
158
+ });
159
+ accountId = selectedAccount.id;
160
+ accountSlug = selectedAccount.slug;
161
+ }
162
+ }
163
+ }
164
+ // Save config
165
+ const config = {
166
+ access_token: tokens.access_token,
167
+ refresh_token: tokens.refresh_token,
168
+ account_id: accountId,
169
+ account_slug: accountSlug,
170
+ endpoint,
171
+ expires_at: Date.now() + (tokens.expires_in * 1000),
172
+ };
173
+ saveConfig(config);
174
+ res.writeHead(200, { 'Content-Type': 'text/html' });
175
+ res.end(`
176
+ <html>
177
+ <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
178
+ <div style="text-align: center;">
179
+ <h1 style="color: #3b82f6;">Authentication Successful!</h1>
180
+ <p>You can close this window and return to the terminal.</p>
181
+ </div>
182
+ <script>setTimeout(() => window.close(), 2000)</script>
183
+ </body>
184
+ </html>
185
+ `);
186
+ server.close();
187
+ console.log(chalk.green('\n✓ Authentication successful!'));
188
+ console.log(` Account: ${chalk.bold(accountSlug || accountId)}`);
189
+ console.log(` Config saved to: ${chalk.dim(CONFIG_FILE)}`);
190
+ console.log();
191
+ resolve();
192
+ }
193
+ catch (error) {
194
+ res.writeHead(500, { 'Content-Type': 'text/html' });
195
+ res.end(`<html><body><h1>Authentication Failed</h1><p>${error.message}</p></body></html>`);
196
+ server.close();
197
+ reject(error);
198
+ }
199
+ }
200
+ else {
201
+ res.writeHead(404);
202
+ res.end('Not found');
203
+ }
204
+ });
205
+ server.listen(redirectPort, () => {
206
+ console.log(chalk.dim(`Waiting for authentication callback on port ${redirectPort}...`));
207
+ });
208
+ // Timeout after 5 minutes
209
+ setTimeout(() => {
210
+ server.close();
211
+ reject(new Error('Authentication timed out'));
212
+ }, 5 * 60 * 1000);
213
+ });
214
+ }
215
+ // Logout
216
+ function logout() {
217
+ console.log(BANNER);
218
+ if (fs.existsSync(CONFIG_FILE)) {
219
+ fs.unlinkSync(CONFIG_FILE);
220
+ console.log(chalk.green('✓ Logged out successfully'));
221
+ }
222
+ else {
223
+ console.log(chalk.yellow('Not logged in'));
224
+ }
225
+ }
226
+ // Check if logged in and token is valid
227
+ async function ensureLoggedIn(options) {
228
+ const config = loadConfig();
229
+ if (!config.access_token) {
230
+ console.log(chalk.yellow('Not logged in. Starting authentication...\n'));
231
+ await login(options);
232
+ return loadConfig();
233
+ }
234
+ // Check if token is expired
235
+ if (config.expires_at && Date.now() > config.expires_at) {
236
+ if (config.refresh_token) {
237
+ console.log(chalk.dim('Token expired, refreshing...'));
238
+ try {
239
+ const endpoint = config.endpoint || options.endpoint || 'https://brightsy.ai';
240
+ const response = await fetch(`${endpoint}/oauth/token`, {
241
+ method: 'POST',
242
+ headers: { 'Content-Type': 'application/json' },
243
+ body: JSON.stringify({
244
+ grant_type: 'refresh_token',
245
+ client_id: 'brightsy-cli',
246
+ refresh_token: config.refresh_token,
247
+ })
248
+ });
249
+ if (response.ok) {
250
+ const tokens = await response.json();
251
+ config.access_token = tokens.access_token;
252
+ if (tokens.refresh_token) {
253
+ config.refresh_token = tokens.refresh_token;
254
+ }
255
+ config.expires_at = Date.now() + (tokens.expires_in * 1000);
256
+ saveConfig(config);
257
+ console.log(chalk.green('✓ Token refreshed'));
258
+ }
259
+ else {
260
+ throw new Error('Refresh failed');
261
+ }
262
+ }
263
+ catch (e) {
264
+ console.log(chalk.yellow('Session expired. Please log in again.\n'));
265
+ await login(options);
266
+ return loadConfig();
267
+ }
268
+ }
269
+ else {
270
+ console.log(chalk.yellow('Session expired. Please log in again.\n'));
271
+ await login(options);
272
+ return loadConfig();
273
+ }
274
+ }
275
+ return config;
276
+ }
277
+ // ASCII art banner
278
+ const BANNER = `
279
+ ${chalk.blue('╔═══════════════════════════════════════════════╗')}
280
+ ${chalk.blue('║')} ${chalk.bold.cyan('Brightsy Component Library Generator')} ${chalk.blue('║')}
281
+ ${chalk.blue('╚═══════════════════════════════════════════════╝')}
282
+ `;
283
+ // Template files for "components" template
284
+ const COMPONENTS_TEMPLATE = {
285
+ 'package.json': (config) => JSON.stringify({
286
+ name: config.projectName,
287
+ version: '1.0.0',
288
+ description: config.description,
289
+ type: 'module',
290
+ main: './dist/index.js',
291
+ module: './dist/index.js',
292
+ types: './dist/index.d.ts',
293
+ exports: {
294
+ '.': {
295
+ types: './dist/index.d.ts',
296
+ import: './dist/index.js'
297
+ }
298
+ },
299
+ scripts: {
300
+ dev: 'vite --port 5180',
301
+ build: 'vite build',
302
+ preview: 'vite preview',
303
+ typecheck: 'tsc --noEmit'
304
+ },
305
+ dependencies: {
306
+ react: '^18.2.0',
307
+ 'react-dom': '^18.2.0'
308
+ },
309
+ devDependencies: {
310
+ '@brightsy/component-dev-kit': '^0.1.0',
311
+ '@types/react': '^18.2.0',
312
+ '@types/react-dom': '^18.2.0',
313
+ '@vitejs/plugin-react': '^4.3.4',
314
+ typescript: '^5.6.3',
315
+ vite: '^6.0.1'
316
+ },
317
+ peerDependencies: {
318
+ '@brightsy/page-builder': '*',
319
+ '@brightsy/client': '*'
320
+ },
321
+ author: config.author,
322
+ license: 'MIT'
323
+ }, null, 2),
324
+ 'tsconfig.json': () => JSON.stringify({
325
+ compilerOptions: {
326
+ target: 'ES2020',
327
+ useDefineForClassFields: true,
328
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
329
+ module: 'ESNext',
330
+ skipLibCheck: true,
331
+ moduleResolution: 'bundler',
332
+ allowImportingTsExtensions: true,
333
+ resolveJsonModule: true,
334
+ isolatedModules: true,
335
+ noEmit: true,
336
+ jsx: 'react-jsx',
337
+ strict: true,
338
+ noUnusedLocals: true,
339
+ noUnusedParameters: true,
340
+ noFallthroughCasesInSwitch: true,
341
+ esModuleInterop: true
342
+ },
343
+ include: ['src'],
344
+ references: [{ path: './tsconfig.node.json' }]
345
+ }, null, 2),
346
+ 'tsconfig.node.json': () => JSON.stringify({
347
+ compilerOptions: {
348
+ composite: true,
349
+ skipLibCheck: true,
350
+ module: 'ESNext',
351
+ moduleResolution: 'bundler',
352
+ allowSyntheticDefaultImports: true,
353
+ strict: true
354
+ },
355
+ include: ['vite.config.ts']
356
+ }, null, 2),
357
+ 'vite.config.ts': (config) => `import { defineConfig } from 'vite';
358
+ import react from '@vitejs/plugin-react';
359
+ import { brightsyComponentLibrary } from '@brightsy/component-dev-kit/vite';
360
+ import path from 'path';
361
+
362
+ export default defineConfig({
363
+ plugins: [
364
+ react({
365
+ jsxRuntime: 'classic',
366
+ jsxImportSource: undefined,
367
+ }),
368
+ ...brightsyComponentLibrary({
369
+ name: '${config.projectName}',
370
+ }),
371
+ ],
372
+ build: {
373
+ outDir: 'dist',
374
+ emptyOutDir: true,
375
+ cssCodeSplit: false,
376
+ lib: {
377
+ entry: path.resolve(__dirname, 'src/index.tsx'),
378
+ formats: ['es'],
379
+ fileName: () => 'index.js',
380
+ },
381
+ rollupOptions: {
382
+ external: ['react', 'react-dom'],
383
+ output: {
384
+ globals: {
385
+ react: 'React',
386
+ 'react-dom': 'ReactDOM',
387
+ },
388
+ },
389
+ },
390
+ sourcemap: true,
391
+ },
392
+ server: {
393
+ port: 5180,
394
+ cors: true,
395
+ },
396
+ });
397
+ `,
398
+ '.gitignore': () => `node_modules
399
+ dist
400
+ .DS_Store
401
+ *.local
402
+ `,
403
+ 'src/index.tsx': (config) => `/**
404
+ * ${config.projectName}
405
+ *
406
+ * ${config.description}
407
+ */
408
+
409
+ import { createComponentExports } from '@brightsy/component-dev-kit';
410
+ import { HelloWorld, helloWorldConfig, helloWorldLLMInstruction } from './components/HelloWorld';
411
+
412
+ // Create the library export
413
+ const library = createComponentExports(
414
+ // Components
415
+ {
416
+ HelloWorld,
417
+ },
418
+ // Configs
419
+ {
420
+ HelloWorld: helloWorldConfig,
421
+ },
422
+ // LLM Instructions
423
+ {
424
+ HelloWorld: helloWorldLLMInstruction,
425
+ }
426
+ );
427
+
428
+ // Default export: components for dynamic import
429
+ export default library.default;
430
+
431
+ // Named exports for static usage
432
+ export const componentConfigs = library.componentConfigs;
433
+ export const componentLLMInstructions = library.componentLLMInstructions;
434
+
435
+ // Re-export individual components
436
+ export { HelloWorld } from './components/HelloWorld';
437
+ `,
438
+ 'src/components/HelloWorld/HelloWorld.tsx': () => `/**
439
+ * HelloWorld Component
440
+ *
441
+ * A simple example component to get you started.
442
+ */
443
+
444
+ import React from 'react';
445
+
446
+ export interface HelloWorldProps {
447
+ /** The name to greet */
448
+ name: string;
449
+ /** Custom greeting message */
450
+ greeting?: string;
451
+ /** Text color */
452
+ color?: string;
453
+ }
454
+
455
+ export const HelloWorld: React.FC<HelloWorldProps> = ({
456
+ name,
457
+ greeting = 'Hello',
458
+ color = 'var(--color-text, #111827)',
459
+ }) => {
460
+ return (
461
+ <div
462
+ style={{
463
+ padding: 'var(--spacing-md, 16px)',
464
+ borderRadius: 'var(--border-radius-md, 8px)',
465
+ backgroundColor: 'var(--color-surface, #ffffff)',
466
+ border: '1px solid var(--color-border, #e5e7eb)',
467
+ fontFamily: 'var(--font-family-primary, system-ui)',
468
+ }}
469
+ >
470
+ <h2
471
+ style={{
472
+ margin: 0,
473
+ color,
474
+ fontSize: 'var(--font-size-xl, 24px)',
475
+ fontWeight: 'var(--font-weight-semibold, 600)',
476
+ }}
477
+ >
478
+ {greeting}, {name}!
479
+ </h2>
480
+ <p
481
+ style={{
482
+ margin: 'var(--spacing-sm, 12px) 0 0',
483
+ color: 'var(--color-text-muted, #6b7280)',
484
+ }}
485
+ >
486
+ Welcome to your Brightsy component library.
487
+ </p>
488
+ </div>
489
+ );
490
+ };
491
+
492
+ export default HelloWorld;
493
+ `,
494
+ 'src/components/HelloWorld/HelloWorld.config.ts': () => `/**
495
+ * HelloWorld Puck Configuration
496
+ */
497
+
498
+ import type { BrightsyComponentConfig } from '@brightsy/component-dev-kit';
499
+ import type { HelloWorldProps } from './HelloWorld';
500
+
501
+ export const helloWorldConfig: BrightsyComponentConfig<HelloWorldProps> = {
502
+ label: 'Hello World',
503
+ category: 'Getting Started',
504
+ icon: '👋',
505
+
506
+ fields: {
507
+ name: {
508
+ type: 'text',
509
+ label: 'Name',
510
+ description: 'The name to greet',
511
+ required: true,
512
+ },
513
+ greeting: {
514
+ type: 'text',
515
+ label: 'Greeting',
516
+ description: 'Custom greeting message',
517
+ placeholder: 'Hello',
518
+ },
519
+ color: {
520
+ type: 'text',
521
+ label: 'Text Color',
522
+ description: 'CSS color for the greeting',
523
+ placeholder: '#111827',
524
+ },
525
+ },
526
+
527
+ defaultProps: {
528
+ name: 'World',
529
+ greeting: 'Hello',
530
+ color: 'var(--color-text, #111827)',
531
+ },
532
+ };
533
+
534
+ export default helloWorldConfig;
535
+ `,
536
+ 'src/components/HelloWorld/HelloWorld.llm.ts': () => `/**
537
+ * HelloWorld LLM Instructions
538
+ */
539
+
540
+ import type { LLMInstruction } from '@brightsy/component-dev-kit';
541
+
542
+ export const helloWorldLLMInstruction: LLMInstruction = \`
543
+ HelloWorld: A simple greeting component.
544
+
545
+ Use this as a template for creating your own components.
546
+
547
+ Props:
548
+ - name (string, required): The name to greet
549
+ - greeting (string, default: "Hello"): Custom greeting message
550
+ - color (string): CSS color for the text
551
+
552
+ Example:
553
+ {
554
+ "type": "HelloWorld",
555
+ "props": {
556
+ "name": "Developer",
557
+ "greeting": "Welcome",
558
+ "color": "#3b82f6"
559
+ }
560
+ }
561
+ \`;
562
+
563
+ export default helloWorldLLMInstruction;
564
+ `,
565
+ 'src/components/HelloWorld/index.ts': () => `export { HelloWorld, type HelloWorldProps } from './HelloWorld';
566
+ export { helloWorldConfig } from './HelloWorld.config';
567
+ export { helloWorldLLMInstruction } from './HelloWorld.llm';
568
+ `,
569
+ 'README.md': (config) => `# ${config.projectName}
570
+
571
+ ${config.description}
572
+
573
+ ## Quick Start
574
+
575
+ \`\`\`bash
576
+ # Install dependencies
577
+ npm install
578
+
579
+ # Start development server
580
+ npm run dev
581
+
582
+ # Build for production
583
+ npm run build
584
+ \`\`\`
585
+
586
+ ## Creating Components
587
+
588
+ Each component should have three files:
589
+
590
+ 1. \`Component.tsx\` - The React component
591
+ 2. \`Component.config.ts\` - Puck editor configuration
592
+ 3. \`Component.llm.ts\` - LLM instructions for AI assistance
593
+
594
+ See \`src/components/HelloWorld\` for an example.
595
+
596
+ ## Deploying to Brightsy
597
+
598
+ 1. Build your library: \`npm run build\`
599
+ 2. Host the \`dist/\` folder on a CDN
600
+ 3. Register the library in Brightsy:
601
+ - Via the Brightsy dashboard
602
+ - Via MCP tools
603
+ - Via the BrightsyClient API
604
+
605
+ ## Theme Variables
606
+
607
+ Use CSS variables for consistent theming:
608
+
609
+ \`\`\`css
610
+ /* Colors */
611
+ --color-primary
612
+ --color-text
613
+ --color-text-muted
614
+ --color-surface
615
+ --color-border
616
+
617
+ /* Spacing */
618
+ --spacing-xs, --spacing-sm, --spacing-md, --spacing-lg, --spacing-xl
619
+
620
+ /* Typography */
621
+ --font-family-primary
622
+ --font-size-sm, --font-size-md, --font-size-lg
623
+
624
+ /* Other */
625
+ --border-radius-sm, --border-radius-md, --border-radius-lg
626
+ \`\`\`
627
+
628
+ ## License
629
+
630
+ MIT
631
+ `,
632
+ };
633
+ // Template files for "app" template (single application component)
634
+ const APP_TEMPLATE = {
635
+ ...COMPONENTS_TEMPLATE,
636
+ 'src/index.tsx': (config) => `/**
637
+ * ${config.projectName}
638
+ *
639
+ * ${config.description}
640
+ *
641
+ * This is a custom application built on Brightsy.
642
+ * The App component is the entire application - it handles routing, state, etc.
643
+ */
644
+
645
+ import { createComponentExports } from '@brightsy/component-dev-kit';
646
+ import { App, appConfig, appLLMInstruction } from './App';
647
+
648
+ const library = createComponentExports(
649
+ { App },
650
+ { App: appConfig },
651
+ { App: appLLMInstruction }
652
+ );
653
+
654
+ export default library.default;
655
+ export const componentConfigs = library.componentConfigs;
656
+ export const componentLLMInstructions = library.componentLLMInstructions;
657
+ export { App } from './App';
658
+ `,
659
+ 'src/App/App.tsx': (config) => `/**
660
+ * Main Application Component
661
+ *
662
+ * This component IS the entire application.
663
+ * It handles its own routing, state, and data fetching.
664
+ */
665
+
666
+ import React, { useState } from 'react';
667
+
668
+ export interface AppProps {
669
+ /** Application title */
670
+ title: string;
671
+ }
672
+
673
+ export const App: React.FC<AppProps> = ({ title }) => {
674
+ const [count, setCount] = useState(0);
675
+
676
+ return (
677
+ <div
678
+ style={{
679
+ minHeight: '100vh',
680
+ fontFamily: 'var(--font-family-primary, system-ui)',
681
+ backgroundColor: 'var(--color-surface, #f9fafb)',
682
+ }}
683
+ >
684
+ {/* Header */}
685
+ <header
686
+ style={{
687
+ padding: 'var(--spacing-md, 16px)',
688
+ backgroundColor: 'var(--color-primary, #3b82f6)',
689
+ color: 'var(--color-primary-foreground, #ffffff)',
690
+ }}
691
+ >
692
+ <h1 style={{ margin: 0, fontSize: 'var(--font-size-xl, 24px)' }}>
693
+ {title}
694
+ </h1>
695
+ </header>
696
+
697
+ {/* Main Content */}
698
+ <main
699
+ style={{
700
+ padding: 'var(--spacing-lg, 24px)',
701
+ maxWidth: '800px',
702
+ margin: '0 auto',
703
+ }}
704
+ >
705
+ <div
706
+ style={{
707
+ padding: 'var(--spacing-lg, 24px)',
708
+ backgroundColor: 'var(--color-surface, #ffffff)',
709
+ borderRadius: 'var(--border-radius-lg, 12px)',
710
+ border: '1px solid var(--color-border, #e5e7eb)',
711
+ textAlign: 'center',
712
+ }}
713
+ >
714
+ <h2 style={{ marginTop: 0 }}>Welcome to your Brightsy App</h2>
715
+ <p style={{ color: 'var(--color-text-muted, #6b7280)' }}>
716
+ This is a custom application built on Brightsy.
717
+ </p>
718
+
719
+ <div style={{ marginTop: 'var(--spacing-lg, 24px)' }}>
720
+ <p>Count: {count}</p>
721
+ <button
722
+ onClick={() => setCount(c => c + 1)}
723
+ style={{
724
+ padding: 'var(--spacing-sm, 12px) var(--spacing-md, 16px)',
725
+ backgroundColor: 'var(--color-primary, #3b82f6)',
726
+ color: 'var(--color-primary-foreground, #ffffff)',
727
+ border: 'none',
728
+ borderRadius: 'var(--border-radius-md, 8px)',
729
+ cursor: 'pointer',
730
+ fontSize: 'var(--font-size-sm, 14px)',
731
+ }}
732
+ >
733
+ Increment
734
+ </button>
735
+ </div>
736
+ </div>
737
+ </main>
738
+ </div>
739
+ );
740
+ };
741
+
742
+ export default App;
743
+ `,
744
+ 'src/App/App.config.ts': (config) => `/**
745
+ * App Configuration
746
+ */
747
+
748
+ import type { BrightsyComponentConfig } from '@brightsy/component-dev-kit';
749
+ import type { AppProps } from './App';
750
+
751
+ export const appConfig: BrightsyComponentConfig<AppProps> = {
752
+ label: '${config.projectName} App',
753
+ category: 'Applications',
754
+ icon: '📱',
755
+
756
+ fields: {
757
+ title: {
758
+ type: 'text',
759
+ label: 'App Title',
760
+ description: 'Title shown in the header',
761
+ required: true,
762
+ },
763
+ },
764
+
765
+ defaultProps: {
766
+ title: '${config.projectName}',
767
+ },
768
+ };
769
+
770
+ export default appConfig;
771
+ `,
772
+ 'src/App/App.llm.ts': () => `/**
773
+ * App LLM Instructions
774
+ */
775
+
776
+ import type { LLMInstruction } from '@brightsy/component-dev-kit';
777
+
778
+ export const appLLMInstruction: LLMInstruction = \`
779
+ App: A custom application component.
780
+
781
+ This is not a composable component - it's an entire application
782
+ that handles its own routing, state, and data fetching.
783
+
784
+ Props:
785
+ - title (string): Application title
786
+
787
+ Deploy on a Brightsy site in "app mode" to use this component
788
+ as the entire site content.
789
+ \`;
790
+
791
+ export default appLLMInstruction;
792
+ `,
793
+ 'src/App/index.ts': () => `export { App, type AppProps } from './App';
794
+ export { appConfig } from './App.config';
795
+ export { appLLMInstruction } from './App.llm';
796
+ `,
797
+ };
798
+ // Main scaffolding function
799
+ async function scaffold(projectName, options) {
800
+ console.log(BANNER);
801
+ // Validate project name
802
+ if (fs.existsSync(projectName)) {
803
+ console.error(chalk.red(`Error: Directory "${projectName}" already exists.`));
804
+ process.exit(1);
805
+ }
806
+ // Prompt for configuration
807
+ const response = await prompts([
808
+ {
809
+ type: options.template ? null : 'select',
810
+ name: 'template',
811
+ message: 'Choose a template:',
812
+ choices: [
813
+ { title: 'Component Library', value: 'components', description: 'Multiple components for page builder' },
814
+ { title: 'Custom Application', value: 'app', description: 'Single app component' },
815
+ ],
816
+ initial: 0,
817
+ },
818
+ {
819
+ type: 'text',
820
+ name: 'description',
821
+ message: 'Project description:',
822
+ initial: 'A Brightsy component library',
823
+ },
824
+ {
825
+ type: 'text',
826
+ name: 'author',
827
+ message: 'Author:',
828
+ initial: '',
829
+ },
830
+ ]);
831
+ const config = {
832
+ projectName,
833
+ template: (options.template || response.template),
834
+ description: response.description,
835
+ author: response.author,
836
+ };
837
+ console.log();
838
+ console.log(chalk.cyan(`Creating ${config.template === 'app' ? 'application' : 'component library'}: ${projectName}`));
839
+ console.log();
840
+ // Select template
841
+ const template = config.template === 'app' ? APP_TEMPLATE : COMPONENTS_TEMPLATE;
842
+ // Create project directory
843
+ fs.mkdirSync(projectName, { recursive: true });
844
+ // Create subdirectories
845
+ if (config.template === 'app') {
846
+ fs.mkdirSync(path.join(projectName, 'src/App'), { recursive: true });
847
+ }
848
+ else {
849
+ fs.mkdirSync(path.join(projectName, 'src/components/HelloWorld'), { recursive: true });
850
+ }
851
+ // Write template files
852
+ for (const [filePath, generator] of Object.entries(template)) {
853
+ const fullPath = path.join(projectName, filePath);
854
+ const dir = path.dirname(fullPath);
855
+ if (!fs.existsSync(dir)) {
856
+ fs.mkdirSync(dir, { recursive: true });
857
+ }
858
+ const content = typeof generator === 'function' ? generator(config) : generator;
859
+ fs.writeFileSync(fullPath, content);
860
+ console.log(chalk.green(' ✓'), filePath);
861
+ }
862
+ console.log();
863
+ console.log(chalk.bold.green('Done!'));
864
+ console.log();
865
+ console.log('Next steps:');
866
+ console.log(chalk.cyan(` cd ${projectName}`));
867
+ console.log(chalk.cyan(' npm install'));
868
+ console.log(chalk.cyan(' npm run dev'));
869
+ console.log();
870
+ console.log('For more info, see:');
871
+ console.log(chalk.blue(' https://docs.brightsy.ai/component-libraries'));
872
+ }
873
+ async function deploy(options) {
874
+ console.log(BANNER);
875
+ console.log(chalk.cyan('Deploying component library to Brightsy...\n'));
876
+ // Ensure user is logged in
877
+ const authConfig = await ensureLoggedIn({ endpoint: options.endpoint });
878
+ if (!authConfig.access_token || !authConfig.account_id) {
879
+ console.error(chalk.red('Error: Authentication failed. Please run: brightsy login'));
880
+ process.exit(1);
881
+ }
882
+ // Check if we're in a valid project directory
883
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
884
+ if (!fs.existsSync(packageJsonPath)) {
885
+ console.error(chalk.red('Error: No package.json found. Run this command from your project root.'));
886
+ process.exit(1);
887
+ }
888
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
889
+ const projectName = packageJson.name;
890
+ // Check if dist/index.js exists, if not, build first
891
+ const distPath = path.join(process.cwd(), 'dist', 'index.js');
892
+ if (!fs.existsSync(distPath)) {
893
+ console.log(chalk.yellow('No build found. Building project first...\n'));
894
+ try {
895
+ execSync('npm run build', { stdio: 'inherit' });
896
+ console.log();
897
+ }
898
+ catch (error) {
899
+ console.error(chalk.red('Build failed. Please fix the errors and try again.'));
900
+ process.exit(1);
901
+ }
902
+ }
903
+ if (!fs.existsSync(distPath)) {
904
+ console.error(chalk.red('Error: dist/index.js not found after build.'));
905
+ process.exit(1);
906
+ }
907
+ // Prompt for missing config
908
+ const prompts_needed = [];
909
+ if (!options.envName) {
910
+ prompts_needed.push({
911
+ type: 'select',
912
+ name: 'envName',
913
+ message: 'Environment:',
914
+ choices: [
915
+ { title: 'Production', value: 'production' },
916
+ { title: 'Development', value: 'development' },
917
+ { title: 'Staging', value: 'staging' },
918
+ ]
919
+ });
920
+ }
921
+ if (!options.version) {
922
+ prompts_needed.push({
923
+ type: 'text',
924
+ name: 'version',
925
+ message: 'Version:',
926
+ initial: packageJson.version || '1.0.0'
927
+ });
928
+ }
929
+ const answers = prompts_needed.length > 0 ? await prompts(prompts_needed) : {};
930
+ const deployConfig = {
931
+ endpoint: authConfig.endpoint || options.endpoint || 'https://brightsy.ai',
932
+ accountId: authConfig.account_id,
933
+ accessToken: authConfig.access_token,
934
+ libId: options.libId,
935
+ envName: options.envName || answers.envName || 'production',
936
+ version: options.version || answers.version || packageJson.version || '1.0.0',
937
+ };
938
+ const isProduction = deployConfig.envName === 'production';
939
+ const uploadPath = `libs/${projectName}/${deployConfig.version}`;
940
+ console.log(chalk.cyan('\nDeployment Configuration:'));
941
+ console.log(` Account: ${chalk.bold(authConfig.account_slug || authConfig.account_id)}`);
942
+ console.log(` Project: ${chalk.bold(projectName)}`);
943
+ console.log(` Version: ${chalk.bold(deployConfig.version)}`);
944
+ console.log(` Environment: ${chalk.bold(deployConfig.envName)}`);
945
+ console.log(` Upload Path: ${chalk.bold(uploadPath)}`);
946
+ console.log();
947
+ try {
948
+ // Step 1: Get upload URL
949
+ console.log(chalk.cyan('Step 1/3: Getting upload URL...'));
950
+ const uploadUrlResponse = await fetch(`${deployConfig.endpoint}/api/v1beta/${deployConfig.accountId}/files/upload-url`, {
951
+ method: 'POST',
952
+ headers: {
953
+ 'Authorization': `Bearer ${deployConfig.accessToken}`,
954
+ 'Content-Type': 'application/json'
955
+ },
956
+ body: JSON.stringify({
957
+ path: uploadPath,
958
+ filename: 'index.js'
959
+ })
960
+ });
961
+ if (!uploadUrlResponse.ok) {
962
+ const error = await uploadUrlResponse.json().catch(() => ({}));
963
+ throw new Error(error.message || `HTTP ${uploadUrlResponse.status}`);
964
+ }
965
+ const { uploadUrl, fileUrl } = await uploadUrlResponse.json();
966
+ console.log(chalk.green(' ✓ Upload URL obtained'));
967
+ // Step 2: Upload the file
968
+ console.log(chalk.cyan('Step 2/3: Uploading library...'));
969
+ const fileContent = fs.readFileSync(distPath);
970
+ const uploadResponse = await fetch(uploadUrl, {
971
+ method: 'PUT',
972
+ body: fileContent,
973
+ headers: {
974
+ 'Content-Type': 'application/javascript'
975
+ }
976
+ });
977
+ if (!uploadResponse.ok) {
978
+ throw new Error(`Upload failed: HTTP ${uploadResponse.status}`);
979
+ }
980
+ console.log(chalk.green(' ✓ Library uploaded'));
981
+ console.log(chalk.dim(` URL: ${fileUrl}`));
982
+ // Step 3: Register/update library environment (if libId provided)
983
+ if (deployConfig.libId) {
984
+ console.log(chalk.cyan('Step 3/3: Registering library environment...'));
985
+ // Try to create/update the environment
986
+ const envResponse = await fetch(`${deployConfig.endpoint}/api/v1beta/${deployConfig.accountId}/libraries/${deployConfig.libId}/environments`, {
987
+ method: 'POST',
988
+ headers: {
989
+ 'Authorization': `Bearer ${deployConfig.accessToken}`,
990
+ 'Content-Type': 'application/json'
991
+ },
992
+ body: JSON.stringify({
993
+ name: deployConfig.envName,
994
+ module_url: fileUrl,
995
+ is_production: isProduction
996
+ })
997
+ });
998
+ if (envResponse.ok) {
999
+ console.log(chalk.green(' ✓ Library environment registered'));
1000
+ }
1001
+ else {
1002
+ const error = await envResponse.json().catch(() => ({}));
1003
+ console.log(chalk.yellow(` ⚠ Could not register environment: ${error.message || 'Unknown error'}`));
1004
+ console.log(chalk.yellow(' You may need to register it manually via MCP or the dashboard.'));
1005
+ }
1006
+ }
1007
+ else {
1008
+ console.log(chalk.dim('Step 3/3: Skipped (no --lib-id provided)'));
1009
+ console.log(chalk.yellow('\nTo complete registration, use MCP tools:'));
1010
+ console.log(chalk.dim(` create_lib_env --lib_id <your-lib-id> --name ${deployConfig.envName} --module_url ${fileUrl} --is_production ${isProduction}`));
1011
+ }
1012
+ console.log();
1013
+ console.log(chalk.bold.green('Deployment complete!'));
1014
+ console.log();
1015
+ console.log('Library URL:');
1016
+ console.log(chalk.blue(` ${fileUrl}`));
1017
+ console.log();
1018
+ if (!deployConfig.libId) {
1019
+ console.log('Next steps:');
1020
+ console.log(chalk.cyan(' 1. Register the library in Brightsy (if not already):'));
1021
+ console.log(chalk.dim(' MCP: create_lib --name "Your Library Name"'));
1022
+ console.log();
1023
+ console.log(chalk.cyan(' 2. Add this environment to the library:'));
1024
+ console.log(chalk.dim(` MCP: create_lib_env --lib_id <lib-uuid> --name ${deployConfig.envName} --module_url ${fileUrl}`));
1025
+ console.log();
1026
+ console.log(chalk.cyan(' 3. Assign the library to a page type:'));
1027
+ console.log(chalk.dim(' MCP: assign_lib_to_page_type --page_type_id <type-uuid> --lib_id <lib-uuid>'));
1028
+ }
1029
+ }
1030
+ catch (error) {
1031
+ console.error(chalk.red(`\nDeployment failed: ${error.message}`));
1032
+ process.exit(1);
1033
+ }
1034
+ }
1035
+ // CLI setup
1036
+ program
1037
+ .name('create-brightsy-component-lib')
1038
+ .description('Scaffold and deploy Brightsy component libraries')
1039
+ .version('0.1.0');
1040
+ // Create command (default)
1041
+ program
1042
+ .command('create <project-name>', { isDefault: true })
1043
+ .description('Scaffold a new Brightsy component library')
1044
+ .option('-t, --template <type>', 'Template type (components, app)')
1045
+ .action(scaffold);
1046
+ // Login command
1047
+ program
1048
+ .command('login')
1049
+ .description('Authenticate with Brightsy using OAuth')
1050
+ .option('-e, --endpoint <url>', 'API endpoint (default: https://brightsy.ai)')
1051
+ .action((options) => {
1052
+ login({ endpoint: options.endpoint }).catch((error) => {
1053
+ console.error(chalk.red(`\nLogin failed: ${error.message}`));
1054
+ process.exit(1);
1055
+ });
1056
+ });
1057
+ // Logout command
1058
+ program
1059
+ .command('logout')
1060
+ .description('Log out of Brightsy')
1061
+ .action(logout);
1062
+ // Whoami command
1063
+ program
1064
+ .command('whoami')
1065
+ .description('Show current authenticated account')
1066
+ .action(() => {
1067
+ console.log(BANNER);
1068
+ const config = loadConfig();
1069
+ if (config.access_token) {
1070
+ console.log(chalk.green('Logged in as:'));
1071
+ console.log(` Account: ${chalk.bold(config.account_slug || config.account_id || 'Unknown')}`);
1072
+ console.log(` Endpoint: ${chalk.dim(config.endpoint || 'https://brightsy.ai')}`);
1073
+ if (config.expires_at) {
1074
+ const expiresIn = Math.round((config.expires_at - Date.now()) / 1000 / 60);
1075
+ if (expiresIn > 0) {
1076
+ console.log(` Token expires in: ${chalk.dim(expiresIn + ' minutes')}`);
1077
+ }
1078
+ else {
1079
+ console.log(` Token: ${chalk.yellow('Expired (will refresh on next command)')}`);
1080
+ }
1081
+ }
1082
+ }
1083
+ else {
1084
+ console.log(chalk.yellow('Not logged in.'));
1085
+ console.log(chalk.dim('Run: create-brightsy-component-lib login'));
1086
+ }
1087
+ });
1088
+ // Deploy command
1089
+ program
1090
+ .command('deploy')
1091
+ .description('Deploy the library to Brightsy file storage')
1092
+ .option('-e, --endpoint <url>', 'API endpoint (default: https://brightsy.ai)')
1093
+ .option('-l, --lib-id <id>', 'Library ID to register environment for')
1094
+ .option('-n, --env-name <name>', 'Environment name (production, development, staging)')
1095
+ .option('-v, --version <version>', 'Version to deploy (default: from package.json)')
1096
+ .action((options) => {
1097
+ deploy({
1098
+ endpoint: options.endpoint,
1099
+ libId: options.libId,
1100
+ envName: options.envName,
1101
+ version: options.version
1102
+ });
1103
+ });
1104
+ program.parse();
1105
+ //# sourceMappingURL=index.js.map