codebakers 2.0.4 → 2.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/src/index.ts CHANGED
@@ -23,8 +23,12 @@ import { designCommand } from './commands/design.js';
23
23
  import { prdCommand } from './commands/prd.js';
24
24
  import { advisorsCommand } from './commands/advisors.js';
25
25
  import { migrateCommand } from './commands/migrate.js';
26
+ import { prdMakerCommand } from './commands/prd-maker.js';
27
+ import { buildCommand } from './commands/build.js';
28
+ import { integrateCommand, INTEGRATIONS } from './commands/integrate.js';
29
+ import { parseNaturalLanguage, clarifyCommand, clarifyDeployTarget } from './utils/nlp.js';
26
30
 
27
- const VERSION = '1.0.0';
31
+ const VERSION = '2.1.0';
28
32
 
29
33
  // ASCII art logo
30
34
  const logo = `
@@ -60,9 +64,10 @@ async function showMainMenu(): Promise<void> {
60
64
 
61
65
  // Main menu
62
66
  const action = await p.select({
63
- message: 'What do you want to do?',
67
+ message: 'What do you want to do? (or type naturally)',
64
68
  options: inProject ? [
65
69
  { value: 'code', label: 'šŸ’¬ Code with AI', hint: 'build features, fix bugs' },
70
+ { value: 'integrate', label: 'šŸ”Œ One-Click Integrations', hint: '50+ services, browser auth' },
66
71
  { value: 'check', label: 'šŸ” Check code quality', hint: 'run pattern enforcement' },
67
72
  { value: 'deploy', label: 'šŸš€ Deploy', hint: 'deploy to Vercel' },
68
73
  { value: 'migrate', label: 'šŸ—„ļø Database migrations', hint: 'push, generate, status' },
@@ -77,13 +82,17 @@ async function showMainMenu(): Promise<void> {
77
82
  { value: 'design', label: 'šŸŽØ Design system', hint: 'set profile, colors' },
78
83
  { value: 'separator2', label: '─────────────────────────' },
79
84
  { value: 'new', label: 'šŸ†• Create new project' },
80
- { value: 'prd', label: 'šŸ“„ Build from PRD', hint: 'upload a PRD document' },
85
+ { value: 'build', label: 'šŸ—ļø Parallel Build', hint: '3 agents from PRD (swarm)' },
86
+ { value: 'prd', label: 'šŸ“„ Build from PRD', hint: 'sequential build' },
87
+ { value: 'prd-maker', label: 'āœļø Create PRD', hint: 'interview → generate PRD' },
81
88
  { value: 'advisors', label: '🌟 Dream Team Advisors', hint: 'consult with experts' },
82
89
  { value: 'settings', label: 'āš™ļø Settings' },
83
90
  { value: 'help', label: 'ā“ Help' },
84
91
  ] : [
85
92
  { value: 'new', label: 'šŸ†• Create new project' },
86
- { value: 'prd', label: 'šŸ“„ Build from PRD', hint: 'upload a PRD document' },
93
+ { value: 'build', label: 'šŸ—ļø Parallel Build', hint: '3 agents from PRD (swarm)' },
94
+ { value: 'prd', label: 'šŸ“„ Build from PRD', hint: 'sequential build' },
95
+ { value: 'prd-maker', label: 'āœļø Create PRD', hint: 'interview → generate PRD' },
87
96
  { value: 'advisors', label: '🌟 Dream Team Advisors', hint: 'consult with experts' },
88
97
  { value: 'open', label: 'šŸ“‚ Open existing project' },
89
98
  { value: 'status', label: 'šŸ“Š View all projects' },
@@ -106,6 +115,9 @@ async function showMainMenu(): Promise<void> {
106
115
  case 'code':
107
116
  await codeCommand();
108
117
  break;
118
+ case 'integrate':
119
+ await integrateCommand();
120
+ break;
109
121
  case 'check':
110
122
  await checkCommand();
111
123
  break;
@@ -142,9 +154,15 @@ async function showMainMenu(): Promise<void> {
142
154
  case 'new':
143
155
  await initCommand();
144
156
  break;
157
+ case 'build':
158
+ await buildCommand();
159
+ break;
145
160
  case 'prd':
146
161
  await prdCommand();
147
162
  break;
163
+ case 'prd-maker':
164
+ await prdMakerCommand();
165
+ break;
148
166
  case 'advisors':
149
167
  await advisorsCommand();
150
168
  break;
@@ -301,6 +319,99 @@ program
301
319
  .option('--status', 'Check migration status')
302
320
  .action(migrateCommand);
303
321
 
322
+ program
323
+ .command('prd-maker')
324
+ .alias('create-prd')
325
+ .description('Create a PRD through guided interview (supports voice input)')
326
+ .action(prdMakerCommand);
327
+
328
+ program
329
+ .command('build [prd-file]')
330
+ .alias('swarm')
331
+ .description('Parallel build with 3 AI agents (self-healing)')
332
+ .option('--sequential', 'Disable parallel execution')
333
+ .action(buildCommand);
334
+
335
+ program
336
+ .command('integrate [integration]')
337
+ .alias('add')
338
+ .description('One-click integrations (50+ services with browser auth)')
339
+ .action(integrateCommand);
340
+
341
+ // Natural language processing for free-form input
342
+ async function handleNaturalLanguage(input: string): Promise<void> {
343
+ const config = new Config();
344
+
345
+ console.log(chalk.dim('\n Parsing your request...\n'));
346
+
347
+ const parsed = await parseNaturalLanguage(input, config);
348
+
349
+ if (!parsed) {
350
+ // Fall back to code command
351
+ await codeCommand(input);
352
+ return;
353
+ }
354
+
355
+ // Clarify if needed
356
+ const clarified = await clarifyCommand(parsed);
357
+
358
+ if (clarified.command === 'cancel') {
359
+ return;
360
+ }
361
+
362
+ // Execute the command
363
+ switch (clarified.command) {
364
+ case 'init':
365
+ await initCommand();
366
+ break;
367
+ case 'build':
368
+ await buildCommand(clarified.args[0]);
369
+ break;
370
+ case 'code':
371
+ await codeCommand(clarified.args.join(' '));
372
+ break;
373
+ case 'check':
374
+ await checkCommand();
375
+ break;
376
+ case 'deploy':
377
+ const target = await clarifyDeployTarget();
378
+ if (target) {
379
+ await deployCommand({ preview: target === 'preview' });
380
+ }
381
+ break;
382
+ case 'fix':
383
+ await fixCommand();
384
+ break;
385
+ case 'migrate':
386
+ await migrateCommand();
387
+ break;
388
+ case 'design':
389
+ await designCommand();
390
+ break;
391
+ case 'advisors':
392
+ await advisorsCommand();
393
+ break;
394
+ case 'prd-maker':
395
+ await prdMakerCommand();
396
+ break;
397
+ case 'security':
398
+ await securityCommand();
399
+ break;
400
+ case 'status':
401
+ await statusCommand();
402
+ break;
403
+ case 'setup':
404
+ await setupCommand();
405
+ break;
406
+ case 'help':
407
+ showHelp();
408
+ break;
409
+ default:
410
+ // Unknown command - pass to code agent
411
+ await codeCommand(input);
412
+ }
413
+ }
414
+
304
415
  // Parse args or show menu
305
416
  const args = process.argv.slice(2);
306
417
 
@@ -310,6 +421,25 @@ if (args.length === 0) {
310
421
 
311
422
  // Show interactive menu
312
423
  showMainMenu().catch(console.error);
424
+ } else if (args.length > 0 && args[0] && !args[0].startsWith('-')) {
425
+ // Check if it's a known command
426
+ const knownCommands = [
427
+ 'setup', 'init', 'code', 'check', 'deploy', 'fix', 'generate',
428
+ 'connect', 'status', 'gateway', 'security', 'learn', 'design',
429
+ 'prd', 'advisors', 'migrate', 'prd-maker', 'build', 'swarm',
430
+ 'integrate', 'add', 'help'
431
+ ];
432
+
433
+ const firstArg = args[0] as string;
434
+
435
+ if (knownCommands.includes(firstArg)) {
436
+ program.parse();
437
+ } else {
438
+ // Natural language input
439
+ const input = args.join(' ');
440
+ checkForUpdates().catch(() => {});
441
+ handleNaturalLanguage(input).catch(console.error);
442
+ }
313
443
  } else {
314
444
  program.parse();
315
445
  }
@@ -0,0 +1,418 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import * as fs from 'fs-extra';
4
+ import * as path from 'path';
5
+ import { execa } from 'execa';
6
+
7
+ /**
8
+ * Read content from clipboard
9
+ */
10
+ export async function readClipboard(): Promise<string | null> {
11
+ try {
12
+ if (process.platform === 'win32') {
13
+ // Windows: PowerShell Get-Clipboard
14
+ const result = await execa('powershell', ['-Command', 'Get-Clipboard'], { timeout: 5000 });
15
+ return result.stdout;
16
+ } else if (process.platform === 'darwin') {
17
+ // macOS: pbpaste
18
+ const result = await execa('pbpaste', [], { timeout: 5000 });
19
+ return result.stdout;
20
+ } else {
21
+ // Linux: xclip or xsel
22
+ try {
23
+ const result = await execa('xclip', ['-selection', 'clipboard', '-o'], { timeout: 5000 });
24
+ return result.stdout;
25
+ } catch {
26
+ const result = await execa('xsel', ['--clipboard', '--output'], { timeout: 5000 });
27
+ return result.stdout;
28
+ }
29
+ }
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Read a file from path (supports drag & drop paths)
37
+ */
38
+ export async function readFile(filePath: string): Promise<{ content: string; type: string; name: string } | null> {
39
+ try {
40
+ // Clean up the path (remove quotes, handle drag & drop formatting)
41
+ let cleanPath = filePath.trim();
42
+
43
+ // Remove surrounding quotes (common when drag & dropping)
44
+ if ((cleanPath.startsWith('"') && cleanPath.endsWith('"')) ||
45
+ (cleanPath.startsWith("'") && cleanPath.endsWith("'"))) {
46
+ cleanPath = cleanPath.slice(1, -1);
47
+ }
48
+
49
+ // Handle Windows paths with spaces
50
+ cleanPath = cleanPath.replace(/\\ /g, ' ');
51
+
52
+ // Check if file exists
53
+ if (!await fs.pathExists(cleanPath)) {
54
+ return null;
55
+ }
56
+
57
+ const stats = await fs.stat(cleanPath);
58
+
59
+ // Don't read files larger than 10MB
60
+ if (stats.size > 10 * 1024 * 1024) {
61
+ console.log(chalk.yellow('File too large (max 10MB)'));
62
+ return null;
63
+ }
64
+
65
+ const ext = path.extname(cleanPath).toLowerCase();
66
+ const name = path.basename(cleanPath);
67
+
68
+ // Determine file type
69
+ const textExtensions = [
70
+ '.txt', '.md', '.json', '.js', '.ts', '.tsx', '.jsx',
71
+ '.css', '.scss', '.html', '.xml', '.yaml', '.yml',
72
+ '.py', '.rb', '.go', '.rs', '.java', '.c', '.cpp', '.h',
73
+ '.sql', '.sh', '.bash', '.zsh', '.fish', '.ps1',
74
+ '.env', '.gitignore', '.dockerfile', '.toml', '.ini', '.cfg',
75
+ '.csv', '.log', '.mdx', '.astro', '.vue', '.svelte',
76
+ ];
77
+
78
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp'];
79
+ const pdfExtensions = ['.pdf'];
80
+
81
+ if (textExtensions.includes(ext) || ext === '') {
82
+ // Read as text
83
+ const content = await fs.readFile(cleanPath, 'utf-8');
84
+ return { content, type: 'text', name };
85
+ } else if (imageExtensions.includes(ext)) {
86
+ // Read as base64 for images
87
+ const buffer = await fs.readFile(cleanPath);
88
+ const base64 = buffer.toString('base64');
89
+ const mimeType = getMimeType(ext);
90
+ return {
91
+ content: `data:${mimeType};base64,${base64}`,
92
+ type: 'image',
93
+ name
94
+ };
95
+ } else if (pdfExtensions.includes(ext)) {
96
+ // For PDFs, try to extract text
97
+ const text = await extractPdfText(cleanPath);
98
+ if (text) {
99
+ return { content: text, type: 'pdf', name };
100
+ }
101
+ // Fall back to noting it's a PDF
102
+ return {
103
+ content: `[PDF file: ${name} - text extraction not available]`,
104
+ type: 'pdf',
105
+ name
106
+ };
107
+ } else {
108
+ // Try to read as text anyway
109
+ try {
110
+ const content = await fs.readFile(cleanPath, 'utf-8');
111
+ // Check if it's actually text (not binary)
112
+ if (content.includes('\0')) {
113
+ return {
114
+ content: `[Binary file: ${name}]`,
115
+ type: 'binary',
116
+ name
117
+ };
118
+ }
119
+ return { content, type: 'text', name };
120
+ } catch {
121
+ return {
122
+ content: `[Unsupported file type: ${name}]`,
123
+ type: 'unknown',
124
+ name
125
+ };
126
+ }
127
+ }
128
+ } catch (error) {
129
+ console.log(chalk.red(`Error reading file: ${error instanceof Error ? error.message : 'Unknown error'}`));
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Extract text from PDF using pdftotext or pdf-parse
136
+ */
137
+ async function extractPdfText(filePath: string): Promise<string | null> {
138
+ try {
139
+ // Try pdftotext (poppler-utils)
140
+ const result = await execa('pdftotext', [filePath, '-'], { timeout: 30000 });
141
+ return result.stdout;
142
+ } catch {
143
+ // pdftotext not available
144
+ return null;
145
+ }
146
+ }
147
+
148
+ function getMimeType(ext: string): string {
149
+ const mimeTypes: Record<string, string> = {
150
+ '.png': 'image/png',
151
+ '.jpg': 'image/jpeg',
152
+ '.jpeg': 'image/jpeg',
153
+ '.gif': 'image/gif',
154
+ '.webp': 'image/webp',
155
+ '.svg': 'image/svg+xml',
156
+ '.bmp': 'image/bmp',
157
+ };
158
+ return mimeTypes[ext] || 'application/octet-stream';
159
+ }
160
+
161
+ /**
162
+ * Watch for file drops/pastes and integrate with text input
163
+ * Returns enhanced input with file contents
164
+ */
165
+ export async function textWithFileSupport(options: {
166
+ message: string;
167
+ placeholder?: string;
168
+ }): Promise<{ text: string; files: Array<{ name: string; content: string; type: string }> } | symbol> {
169
+
170
+ const hint = chalk.dim(' (paste file path, or "clip" for clipboard)');
171
+
172
+ const input = await p.text({
173
+ message: options.message + hint,
174
+ placeholder: options.placeholder,
175
+ });
176
+
177
+ if (p.isCancel(input)) return input;
178
+
179
+ const text = (input as string).trim();
180
+ const files: Array<{ name: string; content: string; type: string }> = [];
181
+
182
+ // Check for clipboard command
183
+ if (text.toLowerCase() === 'clip' || text.toLowerCase() === 'clipboard' || text.toLowerCase() === 'paste') {
184
+ const clipContent = await readClipboard();
185
+ if (clipContent) {
186
+ console.log(chalk.green(`\nšŸ“‹ Read ${clipContent.length} characters from clipboard\n`));
187
+
188
+ // Check if clipboard contains a file path
189
+ if (await fs.pathExists(clipContent.trim())) {
190
+ const file = await readFile(clipContent.trim());
191
+ if (file) {
192
+ files.push(file);
193
+ return { text: `[File: ${file.name}]`, files };
194
+ }
195
+ }
196
+
197
+ // Otherwise treat as text
198
+ files.push({ name: 'clipboard', content: clipContent, type: 'text' });
199
+
200
+ const preview = clipContent.length > 100
201
+ ? clipContent.slice(0, 100) + '...'
202
+ : clipContent;
203
+ console.log(chalk.dim(`Preview: ${preview}\n`));
204
+
205
+ // Ask what to do with clipboard content
206
+ const action = await p.select({
207
+ message: 'What would you like me to do with this?',
208
+ options: [
209
+ { value: 'analyze', label: 'šŸ” Analyze this code/text' },
210
+ { value: 'fix', label: 'šŸ”§ Fix any issues' },
211
+ { value: 'explain', label: 'šŸ“– Explain this' },
212
+ { value: 'improve', label: '✨ Improve/refactor' },
213
+ { value: 'convert', label: 'šŸ”„ Convert to different format' },
214
+ { value: 'custom', label: 'āœļø Custom instruction' },
215
+ ],
216
+ });
217
+
218
+ if (p.isCancel(action)) return action;
219
+
220
+ if (action === 'custom') {
221
+ const customPrompt = await p.text({
222
+ message: 'What should I do with this?',
223
+ placeholder: 'Add error handling to this function',
224
+ });
225
+ if (p.isCancel(customPrompt)) return customPrompt;
226
+ return { text: customPrompt as string, files };
227
+ }
228
+
229
+ const actionText: Record<string, string> = {
230
+ analyze: 'Analyze this code/text and explain what it does',
231
+ fix: 'Fix any bugs, issues, or problems in this code',
232
+ explain: 'Explain this code/text in detail',
233
+ improve: 'Improve and refactor this code for better quality',
234
+ convert: 'Convert this to a better format or structure',
235
+ };
236
+
237
+ return { text: actionText[action as string], files };
238
+ } else {
239
+ console.log(chalk.yellow('Clipboard is empty'));
240
+ // Re-prompt
241
+ return textWithFileSupport(options);
242
+ }
243
+ }
244
+
245
+ // Check if input is a file path
246
+ if (await fs.pathExists(text)) {
247
+ const file = await readFile(text);
248
+ if (file) {
249
+ files.push(file);
250
+ console.log(chalk.green(`\nšŸ“„ Read file: ${file.name} (${file.type})\n`));
251
+
252
+ // Ask what to do
253
+ const action = await p.select({
254
+ message: `What should I do with ${file.name}?`,
255
+ options: [
256
+ { value: 'analyze', label: 'šŸ” Analyze this file' },
257
+ { value: 'fix', label: 'šŸ”§ Fix any issues' },
258
+ { value: 'explain', label: 'šŸ“– Explain this' },
259
+ { value: 'improve', label: '✨ Improve/refactor' },
260
+ { value: 'custom', label: 'āœļø Custom instruction' },
261
+ ],
262
+ });
263
+
264
+ if (p.isCancel(action)) return action;
265
+
266
+ if (action === 'custom') {
267
+ const customPrompt = await p.text({
268
+ message: 'What should I do with this file?',
269
+ placeholder: 'Add TypeScript types to this file',
270
+ });
271
+ if (p.isCancel(customPrompt)) return customPrompt;
272
+ return { text: customPrompt as string, files };
273
+ }
274
+
275
+ const actionText: Record<string, string> = {
276
+ analyze: `Analyze this file (${file.name}) and explain what it does`,
277
+ fix: `Fix any bugs or issues in this file (${file.name})`,
278
+ explain: `Explain this file (${file.name}) in detail`,
279
+ improve: `Improve and refactor this file (${file.name})`,
280
+ };
281
+
282
+ return { text: actionText[action as string], files };
283
+ }
284
+ }
285
+
286
+ // Check if input contains multiple file paths (drag & drop multiple)
287
+ const potentialPaths = text.split('\n').map(p => p.trim()).filter(Boolean);
288
+ if (potentialPaths.length > 1) {
289
+ for (const potentialPath of potentialPaths) {
290
+ if (await fs.pathExists(potentialPath)) {
291
+ const file = await readFile(potentialPath);
292
+ if (file) {
293
+ files.push(file);
294
+ }
295
+ }
296
+ }
297
+
298
+ if (files.length > 0) {
299
+ console.log(chalk.green(`\nšŸ“ Read ${files.length} files: ${files.map(f => f.name).join(', ')}\n`));
300
+
301
+ const instruction = await p.text({
302
+ message: 'What should I do with these files?',
303
+ placeholder: 'Combine these into a single component',
304
+ });
305
+
306
+ if (p.isCancel(instruction)) return instruction;
307
+ return { text: instruction as string, files };
308
+ }
309
+ }
310
+
311
+ // Regular text input
312
+ return { text, files };
313
+ }
314
+
315
+ /**
316
+ * Format files for Claude context
317
+ */
318
+ export function formatFilesForContext(files: Array<{ name: string; content: string; type: string }>): string {
319
+ if (files.length === 0) return '';
320
+
321
+ let context = '\n\n--- ATTACHED FILES ---\n\n';
322
+
323
+ for (const file of files) {
324
+ if (file.type === 'image') {
325
+ context += `[Image: ${file.name}]\n`;
326
+ context += `${file.content}\n\n`; // base64 data URL
327
+ } else {
328
+ context += `--- ${file.name} ---\n`;
329
+ context += '```\n';
330
+ context += file.content;
331
+ context += '\n```\n\n';
332
+ }
333
+ }
334
+
335
+ context += '--- END FILES ---\n\n';
336
+
337
+ return context;
338
+ }
339
+
340
+ /**
341
+ * Quick paste detection - check if text looks like it was pasted
342
+ */
343
+ export function looksLikePaste(text: string): boolean {
344
+ // Multi-line content is likely pasted
345
+ if (text.includes('\n') && text.split('\n').length > 3) return true;
346
+
347
+ // Code-like content
348
+ if (text.includes('function ') || text.includes('const ') ||
349
+ text.includes('import ') || text.includes('export ') ||
350
+ text.includes('class ') || text.includes('def ') ||
351
+ text.includes('public ') || text.includes('private ')) return true;
352
+
353
+ // JSON/object-like
354
+ if ((text.startsWith('{') && text.endsWith('}')) ||
355
+ (text.startsWith('[') && text.endsWith(']'))) return true;
356
+
357
+ // Very long single line
358
+ if (text.length > 200 && !text.includes('\n')) return true;
359
+
360
+ return false;
361
+ }
362
+
363
+ /**
364
+ * Handle pasted content intelligently
365
+ */
366
+ export async function handlePastedContent(text: string): Promise<{ prompt: string; context: string } | null> {
367
+ if (!looksLikePaste(text)) {
368
+ return null; // Not pasted content, handle normally
369
+ }
370
+
371
+ console.log(chalk.cyan('\nšŸ“‹ Detected pasted content!\n'));
372
+
373
+ const preview = text.length > 200 ? text.slice(0, 200) + '...' : text;
374
+ console.log(chalk.dim(preview));
375
+ console.log('');
376
+
377
+ const action = await p.select({
378
+ message: 'What should I do with this?',
379
+ options: [
380
+ { value: 'analyze', label: 'šŸ” Analyze this code' },
381
+ { value: 'fix', label: 'šŸ”§ Fix any issues' },
382
+ { value: 'explain', label: 'šŸ“– Explain this' },
383
+ { value: 'improve', label: '✨ Improve/refactor' },
384
+ { value: 'custom', label: 'āœļø Add my own instruction' },
385
+ { value: 'literal', label: 'šŸ“ Use as literal prompt (not code)' },
386
+ ],
387
+ });
388
+
389
+ if (p.isCancel(action)) return null;
390
+
391
+ if (action === 'literal') {
392
+ return { prompt: text, context: '' };
393
+ }
394
+
395
+ if (action === 'custom') {
396
+ const instruction = await p.text({
397
+ message: 'What should I do?',
398
+ placeholder: 'Add error handling and types',
399
+ });
400
+ if (p.isCancel(instruction)) return null;
401
+ return {
402
+ prompt: instruction as string,
403
+ context: `\n\n--- PASTED CODE ---\n\`\`\`\n${text}\n\`\`\`\n--- END ---\n\n`
404
+ };
405
+ }
406
+
407
+ const actionPrompts: Record<string, string> = {
408
+ analyze: 'Analyze this code and explain what it does, any issues, and potential improvements',
409
+ fix: 'Fix any bugs, issues, or problems in this code',
410
+ explain: 'Explain this code in detail, step by step',
411
+ improve: 'Improve and refactor this code for better quality, readability, and performance',
412
+ };
413
+
414
+ return {
415
+ prompt: actionPrompts[action as string],
416
+ context: `\n\n--- PASTED CODE ---\n\`\`\`\n${text}\n\`\`\`\n--- END ---\n\n`
417
+ };
418
+ }