@codebakers/cli 1.2.1 → 1.3.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.
@@ -0,0 +1,196 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createInterface } from 'readline';
4
+ import { writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { execSync } from 'child_process';
7
+ import * as templates from '../templates/nextjs-supabase.js';
8
+
9
+ async function prompt(question: string): Promise<string> {
10
+ const rl = createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ });
14
+
15
+ return new Promise((resolve) => {
16
+ rl.question(question, (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim());
19
+ });
20
+ });
21
+ }
22
+
23
+ async function confirm(question: string): Promise<boolean> {
24
+ const answer = await prompt(`${question} (Y/n): `);
25
+ return answer.toLowerCase() !== 'n';
26
+ }
27
+
28
+ /**
29
+ * Scaffold a new project with full structure
30
+ */
31
+ export async function scaffold(): Promise<void> {
32
+ console.log(chalk.blue(`
33
+ ╔═══════════════════════════════════════════════════════════╗
34
+ ║ ║
35
+ ║ ${chalk.bold('CodeBakers Project Scaffolding')} ║
36
+ ║ ║
37
+ ║ Create a production-ready project in seconds ║
38
+ ║ ║
39
+ ╚═══════════════════════════════════════════════════════════╝
40
+ `));
41
+
42
+ const cwd = process.cwd();
43
+ const files = readdirSync(cwd);
44
+ const hasFiles = files.filter(f => !f.startsWith('.')).length > 0;
45
+
46
+ if (hasFiles) {
47
+ console.log(chalk.yellow(' ⚠️ This directory is not empty.\n'));
48
+ const proceed = await confirm(' Continue anyway? (Existing files may be overwritten)');
49
+ if (!proceed) {
50
+ console.log(chalk.gray('\n Run this command in an empty directory.\n'));
51
+ return;
52
+ }
53
+ }
54
+
55
+ // Select stack
56
+ console.log(chalk.white('\n Select your stack:\n'));
57
+ console.log(chalk.gray(' 1. ') + chalk.cyan('Next.js + Supabase + Drizzle') + chalk.gray(' (Recommended)'));
58
+ console.log(chalk.gray(' 2. ') + chalk.cyan('Next.js + Prisma') + chalk.gray(' (Coming soon)'));
59
+ console.log(chalk.gray(' 3. ') + chalk.cyan('Express API') + chalk.gray(' (Coming soon)\n'));
60
+
61
+ let stackChoice = '';
62
+ while (!['1', '2', '3'].includes(stackChoice)) {
63
+ stackChoice = await prompt(' Enter 1, 2, or 3: ');
64
+ }
65
+
66
+ if (stackChoice !== '1') {
67
+ console.log(chalk.yellow('\n That stack is coming soon! Using Next.js + Supabase + Drizzle.\n'));
68
+ stackChoice = '1';
69
+ }
70
+
71
+ // Get project name
72
+ const defaultName = cwd.split(/[\\/]/).pop() || 'my-project';
73
+ const projectName = await prompt(` Project name (${defaultName}): `) || defaultName;
74
+
75
+ console.log(chalk.green(`\n Creating ${projectName} with Next.js + Supabase + Drizzle...\n`));
76
+
77
+ // Create project structure
78
+ const spinner = ora(' Creating project structure...').start();
79
+
80
+ try {
81
+ // Create directories
82
+ const dirs = [
83
+ 'src/app',
84
+ 'src/components',
85
+ 'src/lib/supabase',
86
+ 'src/db',
87
+ 'src/db/migrations',
88
+ 'src/services',
89
+ 'src/types',
90
+ 'public',
91
+ ];
92
+
93
+ for (const dir of dirs) {
94
+ const dirPath = join(cwd, dir);
95
+ if (!existsSync(dirPath)) {
96
+ mkdirSync(dirPath, { recursive: true });
97
+ }
98
+ }
99
+
100
+ spinner.text = ' Writing configuration files...';
101
+
102
+ // Write package.json
103
+ const packageJson = { ...templates.PACKAGE_JSON, name: projectName };
104
+ writeFileSync(join(cwd, 'package.json'), JSON.stringify(packageJson, null, 2));
105
+
106
+ // Write .env.example
107
+ writeFileSync(join(cwd, '.env.example'), templates.ENV_EXAMPLE);
108
+ writeFileSync(join(cwd, '.env.local'), templates.ENV_EXAMPLE);
109
+
110
+ // Write config files
111
+ writeFileSync(join(cwd, 'drizzle.config.ts'), templates.DRIZZLE_CONFIG);
112
+ writeFileSync(join(cwd, 'tailwind.config.ts'), templates.TAILWIND_CONFIG);
113
+ writeFileSync(join(cwd, 'postcss.config.mjs'), templates.POSTCSS_CONFIG);
114
+ writeFileSync(join(cwd, 'tsconfig.json'), JSON.stringify(templates.TSCONFIG, null, 2));
115
+ writeFileSync(join(cwd, 'next.config.ts'), templates.NEXT_CONFIG);
116
+ writeFileSync(join(cwd, '.gitignore'), templates.GITIGNORE);
117
+
118
+ spinner.text = ' Writing source files...';
119
+
120
+ // Write Supabase files
121
+ writeFileSync(join(cwd, 'src/lib/supabase/server.ts'), templates.SUPABASE_SERVER);
122
+ writeFileSync(join(cwd, 'src/lib/supabase/client.ts'), templates.SUPABASE_CLIENT);
123
+ writeFileSync(join(cwd, 'src/lib/supabase/middleware.ts'), templates.SUPABASE_MIDDLEWARE);
124
+
125
+ // Write middleware
126
+ writeFileSync(join(cwd, 'middleware.ts'), templates.MIDDLEWARE);
127
+
128
+ // Write database files
129
+ writeFileSync(join(cwd, 'src/db/schema.ts'), templates.DB_SCHEMA);
130
+ writeFileSync(join(cwd, 'src/db/index.ts'), templates.DB_INDEX);
131
+
132
+ // Write app files
133
+ writeFileSync(join(cwd, 'src/app/globals.css'), templates.GLOBALS_CSS);
134
+ writeFileSync(join(cwd, 'src/app/layout.tsx'), templates.LAYOUT_TSX);
135
+ writeFileSync(join(cwd, 'src/app/page.tsx'), templates.PAGE_TSX);
136
+
137
+ // Write utils
138
+ writeFileSync(join(cwd, 'src/lib/utils.ts'), templates.UTILS_CN);
139
+
140
+ spinner.succeed('Project structure created!');
141
+
142
+ // Ask about installing dependencies
143
+ console.log('');
144
+ const installDeps = await confirm(' Install dependencies with npm?');
145
+
146
+ if (installDeps) {
147
+ const installSpinner = ora(' Installing dependencies (this may take a minute)...').start();
148
+ try {
149
+ execSync('npm install', { cwd, stdio: 'pipe' });
150
+ installSpinner.succeed('Dependencies installed!');
151
+ } catch (error) {
152
+ installSpinner.warn('Could not install dependencies automatically');
153
+ console.log(chalk.gray(' Run `npm install` manually.\n'));
154
+ }
155
+ }
156
+
157
+ // Success message
158
+ console.log(chalk.green(`
159
+ ╔═══════════════════════════════════════════════════════════╗
160
+ ║ ║
161
+ ║ ${chalk.bold('✓ Project scaffolded successfully!')} ║
162
+ ║ ║
163
+ ╚═══════════════════════════════════════════════════════════╝
164
+ `));
165
+
166
+ console.log(chalk.white(' Project structure:\n'));
167
+ console.log(chalk.gray(' src/'));
168
+ console.log(chalk.gray(' ├── app/ ') + chalk.cyan('← Pages & layouts'));
169
+ console.log(chalk.gray(' ├── components/ ') + chalk.cyan('← React components'));
170
+ console.log(chalk.gray(' ├── lib/ ') + chalk.cyan('← Utilities & clients'));
171
+ console.log(chalk.gray(' │ └── supabase/ ') + chalk.cyan('← Supabase clients (ready!)'));
172
+ console.log(chalk.gray(' ├── db/ ') + chalk.cyan('← Database schema & queries'));
173
+ console.log(chalk.gray(' ├── services/ ') + chalk.cyan('← Business logic'));
174
+ console.log(chalk.gray(' └── types/ ') + chalk.cyan('← TypeScript types'));
175
+ console.log('');
176
+
177
+ console.log(chalk.white(' Next steps:\n'));
178
+ console.log(chalk.cyan(' 1. ') + chalk.gray('Update .env.local with your Supabase credentials'));
179
+ console.log(chalk.cyan(' 2. ') + chalk.gray('Run `npm run dev` to start development'));
180
+ console.log(chalk.cyan(' 3. ') + chalk.gray('Run `codebakers init` to add CodeBakers patterns'));
181
+ console.log(chalk.cyan(' 4. ') + chalk.gray('Start building with AI assistance!\n'));
182
+
183
+ console.log(chalk.white(' Supabase setup:\n'));
184
+ console.log(chalk.gray(' 1. Create a project at https://supabase.com'));
185
+ console.log(chalk.gray(' 2. Go to Settings → API'));
186
+ console.log(chalk.gray(' 3. Copy URL and anon key to .env.local'));
187
+ console.log(chalk.gray(' 4. Go to Settings → Database → Connection string'));
188
+ console.log(chalk.gray(' 5. Copy DATABASE_URL to .env.local\n'));
189
+
190
+ } catch (error) {
191
+ spinner.fail('Project scaffolding failed');
192
+ const message = error instanceof Error ? error.message : 'Unknown error';
193
+ console.log(chalk.red(`\n Error: ${message}\n`));
194
+ process.exit(1);
195
+ }
196
+ }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ import { init } from './commands/init.js';
11
11
  import { serve } from './commands/serve.js';
12
12
  import { mcpConfig, mcpUninstall } from './commands/mcp-config.js';
13
13
  import { setup } from './commands/setup.js';
14
+ import { scaffold } from './commands/scaffold.js';
14
15
 
15
16
  const program = new Command();
16
17
 
@@ -30,6 +31,12 @@ program
30
31
  .description('Interactive project setup wizard')
31
32
  .action(init);
32
33
 
34
+ program
35
+ .command('scaffold')
36
+ .alias('new')
37
+ .description('Create a new project with full stack scaffolding (Next.js + Supabase + Drizzle)')
38
+ .action(scaffold);
39
+
33
40
  program
34
41
  .command('login')
35
42
  .description('Login with your API key')
package/src/mcp/server.ts CHANGED
@@ -343,6 +343,40 @@ class CodeBakersServer {
343
343
  required: ['patterns'],
344
344
  },
345
345
  },
346
+ {
347
+ name: 'search_patterns',
348
+ description:
349
+ 'Search CodeBakers patterns by keyword or topic. Returns relevant code snippets without reading entire files. Use this when you need specific guidance like "supabase auth setup", "optimistic updates", "soft delete", "form validation".',
350
+ inputSchema: {
351
+ type: 'object' as const,
352
+ properties: {
353
+ query: {
354
+ type: 'string',
355
+ description: 'Search query (e.g., "supabase auth", "stripe checkout", "zod validation", "loading states")',
356
+ },
357
+ },
358
+ required: ['query'],
359
+ },
360
+ },
361
+ {
362
+ name: 'get_pattern_section',
363
+ description:
364
+ 'Get a specific section from a pattern file instead of the whole file. Much faster than get_pattern for targeted lookups.',
365
+ inputSchema: {
366
+ type: 'object' as const,
367
+ properties: {
368
+ pattern: {
369
+ type: 'string',
370
+ description: 'Pattern name (e.g., "02-auth", "03-api")',
371
+ },
372
+ section: {
373
+ type: 'string',
374
+ description: 'Section name or keyword to find within the pattern (e.g., "OAuth", "rate limiting", "error handling")',
375
+ },
376
+ },
377
+ required: ['pattern', 'section'],
378
+ },
379
+ },
346
380
  ],
347
381
  }));
348
382
 
@@ -370,6 +404,12 @@ class CodeBakersServer {
370
404
  case 'get_patterns':
371
405
  return this.handleGetPatterns(args as { patterns: string[] });
372
406
 
407
+ case 'search_patterns':
408
+ return this.handleSearchPatterns(args as { query: string });
409
+
410
+ case 'get_pattern_section':
411
+ return this.handleGetPatternSection(args as { pattern: string; section: string });
412
+
373
413
  default:
374
414
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
375
415
  }
@@ -592,6 +632,210 @@ Show the user what their simple request was expanded into, then proceed with the
592
632
  return response.json();
593
633
  }
594
634
 
635
+ private async handleSearchPatterns(args: { query: string }) {
636
+ const { query } = args;
637
+
638
+ // Call API endpoint for semantic search
639
+ const response = await fetch(`${this.apiUrl}/api/patterns/search`, {
640
+ method: 'POST',
641
+ headers: {
642
+ 'Content-Type': 'application/json',
643
+ Authorization: `Bearer ${this.apiKey}`,
644
+ },
645
+ body: JSON.stringify({ query }),
646
+ });
647
+
648
+ if (!response.ok) {
649
+ // Fallback: If search endpoint doesn't exist, do client-side search
650
+ return this.fallbackSearch(query);
651
+ }
652
+
653
+ const data = await response.json();
654
+
655
+ const results = data.results
656
+ .map((r: { pattern: string; section: string; content: string; relevance: number }) =>
657
+ `### ${r.pattern} - ${r.section}\n\n\`\`\`typescript\n${r.content}\n\`\`\`\n\nRelevance: ${Math.round(r.relevance * 100)}%`
658
+ )
659
+ .join('\n\n---\n\n');
660
+
661
+ return {
662
+ content: [
663
+ {
664
+ type: 'text' as const,
665
+ text: `# Search Results for "${query}"\n\n${results || 'No results found. Try a different query.'}`,
666
+ },
667
+ ],
668
+ };
669
+ }
670
+
671
+ private async fallbackSearch(query: string) {
672
+ // Keyword-based fallback if API search not available
673
+ const keywordPatternMap: Record<string, string[]> = {
674
+ 'auth': ['02-auth'],
675
+ 'login': ['02-auth'],
676
+ 'oauth': ['02-auth'],
677
+ 'supabase': ['02-auth', '01-database'],
678
+ 'database': ['01-database'],
679
+ 'drizzle': ['01-database'],
680
+ 'schema': ['01-database'],
681
+ 'api': ['03-api'],
682
+ 'route': ['03-api'],
683
+ 'validation': ['03-api', '04-frontend'],
684
+ 'zod': ['03-api', '04-frontend'],
685
+ 'frontend': ['04-frontend'],
686
+ 'form': ['04-frontend'],
687
+ 'react': ['04-frontend'],
688
+ 'component': ['04-frontend'],
689
+ 'stripe': ['05-payments'],
690
+ 'payment': ['05-payments'],
691
+ 'checkout': ['05-payments'],
692
+ 'subscription': ['05-payments'],
693
+ 'email': ['06-integrations'],
694
+ 'webhook': ['06-integrations'],
695
+ 'cache': ['07-performance'],
696
+ 'test': ['08-testing'],
697
+ 'playwright': ['08-testing'],
698
+ 'design': ['09-design'],
699
+ 'ui': ['09-design'],
700
+ 'accessibility': ['09-design'],
701
+ 'websocket': ['11-realtime'],
702
+ 'realtime': ['11-realtime'],
703
+ 'notification': ['11-realtime'],
704
+ 'saas': ['12-saas'],
705
+ 'tenant': ['12-saas'],
706
+ 'mobile': ['13-mobile'],
707
+ 'expo': ['13-mobile'],
708
+ 'ai': ['14-ai'],
709
+ 'openai': ['14-ai'],
710
+ 'embedding': ['14-ai'],
711
+ 'analytics': ['26-analytics'],
712
+ 'search': ['27-search'],
713
+ 'animation': ['30-motion'],
714
+ 'framer': ['30-motion'],
715
+ };
716
+
717
+ const lowerQuery = query.toLowerCase();
718
+ const matchedPatterns = new Set<string>();
719
+
720
+ for (const [keyword, patterns] of Object.entries(keywordPatternMap)) {
721
+ if (lowerQuery.includes(keyword)) {
722
+ patterns.forEach(p => matchedPatterns.add(p));
723
+ }
724
+ }
725
+
726
+ if (matchedPatterns.size === 0) {
727
+ return {
728
+ content: [
729
+ {
730
+ type: 'text' as const,
731
+ text: `# No patterns found for "${query}"\n\nTry:\n- "auth" for authentication patterns\n- "api" for API route patterns\n- "form" for frontend form patterns\n- "stripe" for payment patterns\n\nOr use \`list_patterns\` to see all available patterns.`,
732
+ },
733
+ ],
734
+ };
735
+ }
736
+
737
+ const patterns = Array.from(matchedPatterns).slice(0, 3);
738
+ const result = await this.fetchPatterns(patterns);
739
+
740
+ const content = Object.entries(result.patterns || {})
741
+ .map(([name, text]) => `## ${name}\n\n${text}`)
742
+ .join('\n\n---\n\n');
743
+
744
+ return {
745
+ content: [
746
+ {
747
+ type: 'text' as const,
748
+ text: `# Patterns matching "${query}"\n\nFound in: ${patterns.join(', ')}\n\n${content}`,
749
+ },
750
+ ],
751
+ };
752
+ }
753
+
754
+ private async handleGetPatternSection(args: { pattern: string; section: string }) {
755
+ const { pattern, section } = args;
756
+
757
+ // Fetch the full pattern first
758
+ const result = await this.fetchPatterns([pattern]);
759
+
760
+ if (!result.patterns || !result.patterns[pattern]) {
761
+ throw new McpError(
762
+ ErrorCode.InvalidRequest,
763
+ `Pattern "${pattern}" not found. Use list_patterns to see available patterns.`
764
+ );
765
+ }
766
+
767
+ const fullContent = result.patterns[pattern];
768
+
769
+ // Find the section (case-insensitive search for headers or content)
770
+ const sectionLower = section.toLowerCase();
771
+ const lines = fullContent.split('\n');
772
+ const sections: string[] = [];
773
+ let currentSection = '';
774
+ let currentContent: string[] = [];
775
+ let capturing = false;
776
+ let relevanceScore = 0;
777
+
778
+ for (let i = 0; i < lines.length; i++) {
779
+ const line = lines[i];
780
+
781
+ // Check if this is a header
782
+ if (line.match(/^#{1,3}\s/)) {
783
+ // Save previous section if we were capturing
784
+ if (capturing && currentContent.length > 0) {
785
+ sections.push(`### ${currentSection}\n\n${currentContent.join('\n')}`);
786
+ }
787
+
788
+ currentSection = line.replace(/^#+\s*/, '');
789
+ currentContent = [];
790
+
791
+ // Check if this section matches our query
792
+ if (currentSection.toLowerCase().includes(sectionLower)) {
793
+ capturing = true;
794
+ relevanceScore++;
795
+ } else {
796
+ capturing = false;
797
+ }
798
+ } else if (capturing) {
799
+ currentContent.push(line);
800
+ }
801
+
802
+ // Also check content for keyword matches
803
+ if (!capturing && line.toLowerCase().includes(sectionLower)) {
804
+ // Found keyword in content, capture surrounding context
805
+ const start = Math.max(0, i - 5);
806
+ const end = Math.min(lines.length, i + 20);
807
+ const context = lines.slice(start, end).join('\n');
808
+ sections.push(`### Found at line ${i + 1}\n\n${context}`);
809
+ relevanceScore++;
810
+ }
811
+ }
812
+
813
+ // Capture last section if we were still capturing
814
+ if (capturing && currentContent.length > 0) {
815
+ sections.push(`### ${currentSection}\n\n${currentContent.join('\n')}`);
816
+ }
817
+
818
+ if (sections.length === 0) {
819
+ return {
820
+ content: [
821
+ {
822
+ type: 'text' as const,
823
+ text: `# Section "${section}" not found in ${pattern}\n\nThe pattern exists but doesn't contain a section matching "${section}".\n\nTry:\n- A broader search term\n- \`get_pattern ${pattern}\` to see the full content\n- \`search_patterns ${section}\` to search across all patterns`,
824
+ },
825
+ ],
826
+ };
827
+ }
828
+
829
+ return {
830
+ content: [
831
+ {
832
+ type: 'text' as const,
833
+ text: `# ${pattern} - "${section}"\n\n${sections.slice(0, 5).join('\n\n---\n\n')}`,
834
+ },
835
+ ],
836
+ };
837
+ }
838
+
595
839
  async run(): Promise<void> {
596
840
  const transport = new StdioServerTransport();
597
841
  await this.server.connect(transport);