codebakers 2.0.4 → 2.0.10

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,11 @@ 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 { parseNaturalLanguage, clarifyCommand, clarifyDeployTarget } from './utils/nlp.js';
26
29
 
27
- const VERSION = '1.0.0';
30
+ const VERSION = '2.0.9';
28
31
 
29
32
  // ASCII art logo
30
33
  const logo = `
@@ -60,7 +63,7 @@ async function showMainMenu(): Promise<void> {
60
63
 
61
64
  // Main menu
62
65
  const action = await p.select({
63
- message: 'What do you want to do?',
66
+ message: 'What do you want to do? (or type naturally)',
64
67
  options: inProject ? [
65
68
  { value: 'code', label: 'šŸ’¬ Code with AI', hint: 'build features, fix bugs' },
66
69
  { value: 'check', label: 'šŸ” Check code quality', hint: 'run pattern enforcement' },
@@ -77,13 +80,17 @@ async function showMainMenu(): Promise<void> {
77
80
  { value: 'design', label: 'šŸŽØ Design system', hint: 'set profile, colors' },
78
81
  { value: 'separator2', label: '─────────────────────────' },
79
82
  { value: 'new', label: 'šŸ†• Create new project' },
80
- { value: 'prd', label: 'šŸ“„ Build from PRD', hint: 'upload a PRD document' },
83
+ { value: 'build', label: 'šŸ—ļø Parallel Build', hint: '3 agents from PRD (swarm)' },
84
+ { value: 'prd', label: 'šŸ“„ Build from PRD', hint: 'sequential build' },
85
+ { value: 'prd-maker', label: 'āœļø Create PRD', hint: 'interview → generate PRD' },
81
86
  { value: 'advisors', label: '🌟 Dream Team Advisors', hint: 'consult with experts' },
82
87
  { value: 'settings', label: 'āš™ļø Settings' },
83
88
  { value: 'help', label: 'ā“ Help' },
84
89
  ] : [
85
90
  { value: 'new', label: 'šŸ†• Create new project' },
86
- { value: 'prd', label: 'šŸ“„ Build from PRD', hint: 'upload a PRD document' },
91
+ { value: 'build', label: 'šŸ—ļø Parallel Build', hint: '3 agents from PRD (swarm)' },
92
+ { value: 'prd', label: 'šŸ“„ Build from PRD', hint: 'sequential build' },
93
+ { value: 'prd-maker', label: 'āœļø Create PRD', hint: 'interview → generate PRD' },
87
94
  { value: 'advisors', label: '🌟 Dream Team Advisors', hint: 'consult with experts' },
88
95
  { value: 'open', label: 'šŸ“‚ Open existing project' },
89
96
  { value: 'status', label: 'šŸ“Š View all projects' },
@@ -142,9 +149,15 @@ async function showMainMenu(): Promise<void> {
142
149
  case 'new':
143
150
  await initCommand();
144
151
  break;
152
+ case 'build':
153
+ await buildCommand();
154
+ break;
145
155
  case 'prd':
146
156
  await prdCommand();
147
157
  break;
158
+ case 'prd-maker':
159
+ await prdMakerCommand();
160
+ break;
148
161
  case 'advisors':
149
162
  await advisorsCommand();
150
163
  break;
@@ -301,6 +314,93 @@ program
301
314
  .option('--status', 'Check migration status')
302
315
  .action(migrateCommand);
303
316
 
317
+ program
318
+ .command('prd-maker')
319
+ .alias('create-prd')
320
+ .description('Create a PRD through guided interview (supports voice input)')
321
+ .action(prdMakerCommand);
322
+
323
+ program
324
+ .command('build [prd-file]')
325
+ .alias('swarm')
326
+ .description('Parallel build with 3 AI agents (self-healing)')
327
+ .option('--sequential', 'Disable parallel execution')
328
+ .action(buildCommand);
329
+
330
+ // Natural language processing for free-form input
331
+ async function handleNaturalLanguage(input: string): Promise<void> {
332
+ const config = new Config();
333
+
334
+ console.log(chalk.dim('\n Parsing your request...\n'));
335
+
336
+ const parsed = await parseNaturalLanguage(input, config);
337
+
338
+ if (!parsed) {
339
+ // Fall back to code command
340
+ await codeCommand(input);
341
+ return;
342
+ }
343
+
344
+ // Clarify if needed
345
+ const clarified = await clarifyCommand(parsed);
346
+
347
+ if (clarified.command === 'cancel') {
348
+ return;
349
+ }
350
+
351
+ // Execute the command
352
+ switch (clarified.command) {
353
+ case 'init':
354
+ await initCommand();
355
+ break;
356
+ case 'build':
357
+ await buildCommand(clarified.args[0]);
358
+ break;
359
+ case 'code':
360
+ await codeCommand(clarified.args.join(' '));
361
+ break;
362
+ case 'check':
363
+ await checkCommand();
364
+ break;
365
+ case 'deploy':
366
+ const target = await clarifyDeployTarget();
367
+ if (target) {
368
+ await deployCommand({ preview: target === 'preview' });
369
+ }
370
+ break;
371
+ case 'fix':
372
+ await fixCommand();
373
+ break;
374
+ case 'migrate':
375
+ await migrateCommand();
376
+ break;
377
+ case 'design':
378
+ await designCommand();
379
+ break;
380
+ case 'advisors':
381
+ await advisorsCommand();
382
+ break;
383
+ case 'prd-maker':
384
+ await prdMakerCommand();
385
+ break;
386
+ case 'security':
387
+ await securityCommand();
388
+ break;
389
+ case 'status':
390
+ await statusCommand();
391
+ break;
392
+ case 'setup':
393
+ await setupCommand();
394
+ break;
395
+ case 'help':
396
+ showHelp();
397
+ break;
398
+ default:
399
+ // Unknown command - pass to code agent
400
+ await codeCommand(input);
401
+ }
402
+ }
403
+
304
404
  // Parse args or show menu
305
405
  const args = process.argv.slice(2);
306
406
 
@@ -310,6 +410,24 @@ if (args.length === 0) {
310
410
 
311
411
  // Show interactive menu
312
412
  showMainMenu().catch(console.error);
413
+ } else if (args.length > 0 && args[0] && !args[0].startsWith('-')) {
414
+ // Check if it's a known command
415
+ const knownCommands = [
416
+ 'setup', 'init', 'code', 'check', 'deploy', 'fix', 'generate',
417
+ 'connect', 'status', 'gateway', 'security', 'learn', 'design',
418
+ 'prd', 'advisors', 'migrate', 'prd-maker', 'build', 'swarm', 'help'
419
+ ];
420
+
421
+ const firstArg = args[0] as string;
422
+
423
+ if (knownCommands.includes(firstArg)) {
424
+ program.parse();
425
+ } else {
426
+ // Natural language input
427
+ const input = args.join(' ');
428
+ checkForUpdates().catch(() => {});
429
+ handleNaturalLanguage(input).catch(console.error);
430
+ }
313
431
  } else {
314
432
  program.parse();
315
433
  }
@@ -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
+ }