@codebakers/cli 3.9.32 → 3.9.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "3.9.32",
3
+ "version": "3.9.33",
4
4
  "description": "CodeBakers CLI - Production patterns for AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,455 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'fs';
4
+ import { join, relative, resolve, dirname, basename, extname } from 'path';
5
+
6
+ interface CoherenceIssue {
7
+ category: 'import' | 'export' | 'type' | 'schema' | 'api' | 'env' | 'circular' | 'dead-code';
8
+ severity: 'error' | 'warning' | 'info';
9
+ file: string;
10
+ line?: number;
11
+ message: string;
12
+ fix?: string;
13
+ autoFixable: boolean;
14
+ }
15
+
16
+ interface CoherenceOptions {
17
+ focus?: string;
18
+ fix?: boolean;
19
+ verbose?: boolean;
20
+ }
21
+
22
+ /**
23
+ * CLI coherence command - checks wiring and dependencies
24
+ */
25
+ export async function coherence(options: CoherenceOptions = {}): Promise<void> {
26
+ const { focus = 'all', fix = false, verbose = false } = options;
27
+ const cwd = process.cwd();
28
+
29
+ console.log(chalk.blue(`
30
+ ╔═══════════════════════════════════════════════════════════╗
31
+ ║ ║
32
+ ║ ${chalk.bold.white('🔗 CodeBakers Coherence Audit')} ║
33
+ ║ ║
34
+ ╚═══════════════════════════════════════════════════════════╝
35
+ `));
36
+
37
+ const spinner = ora('Scanning codebase for coherence issues...').start();
38
+
39
+ const issues: CoherenceIssue[] = [];
40
+ const stats = {
41
+ filesScanned: 0,
42
+ importsChecked: 0,
43
+ exportsFound: 0,
44
+ envVarsFound: 0,
45
+ };
46
+
47
+ // Helper to extract imports from a file
48
+ const extractImports = (content: string): Array<{ path: string; names: string[]; line: number }> => {
49
+ const imports: Array<{ path: string; names: string[]; line: number }> = [];
50
+ const lines = content.split('\n');
51
+
52
+ lines.forEach((line, i) => {
53
+ // Match: import { X, Y } from 'path'
54
+ const namedMatch = line.match(/import\s+(type\s+)?{([^}]+)}\s+from\s+['"]([^'"]+)['"]/);
55
+ if (namedMatch) {
56
+ const names = namedMatch[2].split(',').map(n => n.trim().split(' as ')[0].trim()).filter(Boolean);
57
+ imports.push({ path: namedMatch[3], names, line: i + 1 });
58
+ }
59
+
60
+ // Match: import X from 'path'
61
+ const defaultMatch = line.match(/import\s+(type\s+)?(\w+)\s+from\s+['"]([^'"]+)['"]/);
62
+ if (defaultMatch && !namedMatch) {
63
+ imports.push({ path: defaultMatch[3], names: ['default'], line: i + 1 });
64
+ }
65
+ });
66
+
67
+ return imports;
68
+ };
69
+
70
+ // Helper to extract exports from a file
71
+ const extractExports = (content: string): string[] => {
72
+ const exports: string[] = [];
73
+
74
+ // Named exports: export { X, Y }
75
+ const namedExportMatches = content.matchAll(/export\s+{([^}]+)}/g);
76
+ for (const match of namedExportMatches) {
77
+ const names = match[1].split(',').map(n => n.trim().split(' as ').pop()?.trim() || '').filter(Boolean);
78
+ exports.push(...names);
79
+ }
80
+
81
+ // Direct exports: export const/function/class/type/interface X
82
+ const directExportMatches = content.matchAll(/export\s+(const|let|var|function|class|type|interface|enum)\s+(\w+)/g);
83
+ for (const match of directExportMatches) {
84
+ exports.push(match[2]);
85
+ }
86
+
87
+ // Default export
88
+ if (content.includes('export default')) {
89
+ exports.push('default');
90
+ }
91
+
92
+ return exports;
93
+ };
94
+
95
+ // Helper to resolve import path to file
96
+ const resolveImportPath = (importPath: string, fromFile: string): string | null => {
97
+ if (!importPath.startsWith('.') && !importPath.startsWith('@/') && !importPath.startsWith('~/')) {
98
+ return null;
99
+ }
100
+
101
+ let resolvedPath = importPath;
102
+ if (importPath.startsWith('@/')) {
103
+ resolvedPath = join(cwd, 'src', importPath.slice(2));
104
+ } else if (importPath.startsWith('~/')) {
105
+ resolvedPath = join(cwd, importPath.slice(2));
106
+ } else {
107
+ resolvedPath = resolve(dirname(fromFile), importPath);
108
+ }
109
+
110
+ const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];
111
+ for (const ext of extensions) {
112
+ const fullPath = resolvedPath + ext;
113
+ if (existsSync(fullPath) && statSync(fullPath).isFile()) {
114
+ return fullPath;
115
+ }
116
+ }
117
+
118
+ return null;
119
+ };
120
+
121
+ const searchDirs = ['src', 'app', 'lib', 'components', 'services', 'types', 'utils', 'hooks', 'pages'];
122
+ const fileExtensions = ['.ts', '.tsx', '.js', '.jsx'];
123
+
124
+ // Build export map
125
+ const exportMap: Map<string, string[]> = new Map();
126
+ const importGraph: Map<string, string[]> = new Map();
127
+ const usedExports: Set<string> = new Set();
128
+
129
+ // First pass: collect all exports
130
+ const collectExports = (dir: string) => {
131
+ if (!existsSync(dir)) return;
132
+ const entries = readdirSync(dir, { withFileTypes: true });
133
+
134
+ for (const entry of entries) {
135
+ const fullPath = join(dir, entry.name);
136
+
137
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
138
+ collectExports(fullPath);
139
+ } else if (entry.isFile() && fileExtensions.some(ext => entry.name.endsWith(ext))) {
140
+ try {
141
+ const content = readFileSync(fullPath, 'utf-8');
142
+ const exports = extractExports(content);
143
+ exportMap.set(fullPath, exports);
144
+ stats.exportsFound += exports.length;
145
+ stats.filesScanned++;
146
+ } catch {
147
+ // Skip unreadable files
148
+ }
149
+ }
150
+ }
151
+ };
152
+
153
+ spinner.text = 'Collecting exports...';
154
+ for (const dir of searchDirs) {
155
+ collectExports(join(cwd, dir));
156
+ }
157
+
158
+ // Second pass: check imports
159
+ const checkImports = (dir: string) => {
160
+ if (!existsSync(dir)) return;
161
+ const entries = readdirSync(dir, { withFileTypes: true });
162
+
163
+ for (const entry of entries) {
164
+ const fullPath = join(dir, entry.name);
165
+
166
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
167
+ checkImports(fullPath);
168
+ } else if (entry.isFile() && fileExtensions.some(ext => entry.name.endsWith(ext))) {
169
+ try {
170
+ const content = readFileSync(fullPath, 'utf-8');
171
+ const imports = extractImports(content);
172
+ const relativePath = relative(cwd, fullPath);
173
+ const importedFiles: string[] = [];
174
+
175
+ for (const imp of imports) {
176
+ stats.importsChecked++;
177
+ const resolvedPath = resolveImportPath(imp.path, fullPath);
178
+
179
+ if (resolvedPath === null) continue;
180
+
181
+ importedFiles.push(resolvedPath);
182
+
183
+ if (!existsSync(resolvedPath)) {
184
+ if (focus === 'all' || focus === 'imports') {
185
+ issues.push({
186
+ category: 'import',
187
+ severity: 'error',
188
+ file: relativePath,
189
+ line: imp.line,
190
+ message: `Import target not found: '${imp.path}'`,
191
+ fix: `Create the file or update the import path`,
192
+ autoFixable: false,
193
+ });
194
+ }
195
+ continue;
196
+ }
197
+
198
+ const targetExports = exportMap.get(resolvedPath) || [];
199
+
200
+ for (const name of imp.names) {
201
+ if (name === '*' || name === 'default') {
202
+ if (name === 'default' && !targetExports.includes('default')) {
203
+ if (focus === 'all' || focus === 'imports') {
204
+ issues.push({
205
+ category: 'import',
206
+ severity: 'error',
207
+ file: relativePath,
208
+ line: imp.line,
209
+ message: `No default export in '${imp.path}'`,
210
+ fix: `Add 'export default' or change to named import`,
211
+ autoFixable: false,
212
+ });
213
+ }
214
+ }
215
+ continue;
216
+ }
217
+
218
+ usedExports.add(`${resolvedPath}:${name}`);
219
+
220
+ if (!targetExports.includes(name)) {
221
+ if (focus === 'all' || focus === 'imports') {
222
+ issues.push({
223
+ category: 'export',
224
+ severity: 'error',
225
+ file: relativePath,
226
+ line: imp.line,
227
+ message: `'${name}' is not exported from '${imp.path}'`,
228
+ fix: `Add 'export { ${name} }' to ${basename(resolvedPath)} or update import`,
229
+ autoFixable: false,
230
+ });
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ importGraph.set(fullPath, importedFiles);
237
+ } catch {
238
+ // Skip unreadable files
239
+ }
240
+ }
241
+ }
242
+ };
243
+
244
+ spinner.text = 'Checking imports...';
245
+ for (const dir of searchDirs) {
246
+ checkImports(join(cwd, dir));
247
+ }
248
+
249
+ // Check for circular dependencies
250
+ if (focus === 'all' || focus === 'circular') {
251
+ spinner.text = 'Detecting circular dependencies...';
252
+ const visited = new Set<string>();
253
+ const recursionStack = new Set<string>();
254
+ const circularPaths: string[][] = [];
255
+
256
+ const detectCircular = (file: string, pathStack: string[]) => {
257
+ if (recursionStack.has(file)) {
258
+ const cycleStart = pathStack.indexOf(file);
259
+ if (cycleStart !== -1) {
260
+ circularPaths.push(pathStack.slice(cycleStart).concat(file));
261
+ }
262
+ return;
263
+ }
264
+
265
+ if (visited.has(file)) return;
266
+
267
+ visited.add(file);
268
+ recursionStack.add(file);
269
+
270
+ const imports = importGraph.get(file) || [];
271
+ for (const imported of imports) {
272
+ detectCircular(imported, [...pathStack, file]);
273
+ }
274
+
275
+ recursionStack.delete(file);
276
+ };
277
+
278
+ for (const file of importGraph.keys()) {
279
+ detectCircular(file, []);
280
+ }
281
+
282
+ const seenCycles = new Set<string>();
283
+ for (const cycle of circularPaths) {
284
+ const cycleKey = cycle.map(f => relative(cwd, f)).sort().join(' -> ');
285
+ if (!seenCycles.has(cycleKey)) {
286
+ seenCycles.add(cycleKey);
287
+ issues.push({
288
+ category: 'circular',
289
+ severity: 'warning',
290
+ file: relative(cwd, cycle[0]),
291
+ message: `Circular dependency: ${cycle.map(f => basename(f)).join(' → ')}`,
292
+ fix: 'Break the cycle by extracting shared code to a separate module',
293
+ autoFixable: false,
294
+ });
295
+ }
296
+ }
297
+ }
298
+
299
+ // Check environment variables
300
+ if (focus === 'all' || focus === 'env') {
301
+ spinner.text = 'Checking environment variables...';
302
+ const envVarsUsed = new Set<string>();
303
+ const envVarsDefined = new Set<string>();
304
+
305
+ const scanEnvUsage = (dir: string) => {
306
+ if (!existsSync(dir)) return;
307
+ const entries = readdirSync(dir, { withFileTypes: true });
308
+
309
+ for (const entry of entries) {
310
+ const fullPath = join(dir, entry.name);
311
+
312
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
313
+ scanEnvUsage(fullPath);
314
+ } else if (entry.isFile() && fileExtensions.some(ext => entry.name.endsWith(ext))) {
315
+ try {
316
+ const content = readFileSync(fullPath, 'utf-8');
317
+ const envMatches = content.matchAll(/process\.env\.(\w+)|process\.env\[['"](\w+)['"]\]/g);
318
+ for (const match of envMatches) {
319
+ const varName = match[1] || match[2];
320
+ envVarsUsed.add(varName);
321
+ stats.envVarsFound++;
322
+ }
323
+ } catch {
324
+ // Skip
325
+ }
326
+ }
327
+ }
328
+ };
329
+
330
+ for (const dir of searchDirs) {
331
+ scanEnvUsage(join(cwd, dir));
332
+ }
333
+
334
+ const envFiles = ['.env', '.env.local', '.env.example', '.env.development'];
335
+ for (const envFile of envFiles) {
336
+ const envPath = join(cwd, envFile);
337
+ if (existsSync(envPath)) {
338
+ try {
339
+ const content = readFileSync(envPath, 'utf-8');
340
+ const lines = content.split('\n');
341
+ for (const line of lines) {
342
+ const match = line.match(/^([A-Z][A-Z0-9_]*)=/);
343
+ if (match) {
344
+ envVarsDefined.add(match[1]);
345
+ }
346
+ }
347
+ } catch {
348
+ // Skip
349
+ }
350
+ }
351
+ }
352
+
353
+ for (const varName of envVarsUsed) {
354
+ if (varName.startsWith('NEXT_') || varName === 'NODE_ENV') continue;
355
+
356
+ if (!envVarsDefined.has(varName)) {
357
+ issues.push({
358
+ category: 'env',
359
+ severity: 'warning',
360
+ file: '.env.example',
361
+ message: `Environment variable '${varName}' is used but not defined in .env files`,
362
+ fix: `Add ${varName}= to .env.example`,
363
+ autoFixable: true,
364
+ });
365
+ }
366
+ }
367
+ }
368
+
369
+ spinner.succeed('Coherence audit complete!');
370
+
371
+ // Display results
372
+ console.log('');
373
+ console.log(chalk.white(' 📊 Summary'));
374
+ console.log(chalk.gray(' ─────────────────────────────────────'));
375
+ console.log(` Files scanned: ${chalk.cyan(stats.filesScanned)}`);
376
+ console.log(` Imports checked: ${chalk.cyan(stats.importsChecked)}`);
377
+ console.log(` Exports found: ${chalk.cyan(stats.exportsFound)}`);
378
+
379
+ const errors = issues.filter(i => i.severity === 'error');
380
+ const warnings = issues.filter(i => i.severity === 'warning');
381
+ const infos = issues.filter(i => i.severity === 'info');
382
+
383
+ console.log('');
384
+ console.log(` ${chalk.red('🔴 Errors:')} ${errors.length}`);
385
+ console.log(` ${chalk.yellow('🟡 Warnings:')} ${warnings.length}`);
386
+ console.log(` ${chalk.blue('🔵 Info:')} ${infos.length}`);
387
+ console.log('');
388
+
389
+ if (issues.length === 0) {
390
+ console.log(chalk.green(' ✅ No coherence issues found! Your codebase wiring is solid.\n'));
391
+ return;
392
+ }
393
+
394
+ // Group issues by category
395
+ const categories: Record<string, CoherenceIssue[]> = {
396
+ import: [],
397
+ export: [],
398
+ circular: [],
399
+ env: [],
400
+ 'dead-code': [],
401
+ };
402
+
403
+ for (const issue of issues) {
404
+ if (categories[issue.category]) {
405
+ categories[issue.category].push(issue);
406
+ }
407
+ }
408
+
409
+ const categoryLabels: Record<string, string> = {
410
+ import: '🔴 Broken Imports',
411
+ export: '🔴 Missing Exports',
412
+ circular: '⚪ Circular Dependencies',
413
+ env: '🟡 Environment Variables',
414
+ 'dead-code': '🔵 Dead Code',
415
+ };
416
+
417
+ for (const [category, catIssues] of Object.entries(categories)) {
418
+ if (catIssues.length === 0) continue;
419
+
420
+ console.log(chalk.white(` ${categoryLabels[category]} (${catIssues.length})`));
421
+ console.log(chalk.gray(' ─────────────────────────────────────'));
422
+
423
+ const displayIssues = verbose ? catIssues : catIssues.slice(0, 5);
424
+ for (const issue of displayIssues) {
425
+ const severityColor = issue.severity === 'error' ? chalk.red : issue.severity === 'warning' ? chalk.yellow : chalk.blue;
426
+ console.log(` ${severityColor('•')} ${chalk.gray(issue.file)}${issue.line ? chalk.gray(`:${issue.line}`) : ''}`);
427
+ console.log(` ${issue.message}`);
428
+ if (issue.fix) {
429
+ console.log(chalk.gray(` Fix: ${issue.fix}`));
430
+ }
431
+ }
432
+
433
+ if (!verbose && catIssues.length > 5) {
434
+ console.log(chalk.gray(` ... and ${catIssues.length - 5} more (use --verbose to see all)`));
435
+ }
436
+ console.log('');
437
+ }
438
+
439
+ // Save state
440
+ const statePath = join(cwd, '.codebakers', 'coherence-state.json');
441
+ try {
442
+ mkdirSync(dirname(statePath), { recursive: true });
443
+ writeFileSync(statePath, JSON.stringify({
444
+ lastAudit: new Date().toISOString(),
445
+ stats,
446
+ issues,
447
+ summary: { errors: errors.length, warnings: warnings.length, info: infos.length },
448
+ }, null, 2));
449
+ } catch {
450
+ // Ignore write errors
451
+ }
452
+
453
+ console.log(chalk.gray(' Run ') + chalk.cyan('npx tsc --noEmit') + chalk.gray(' to verify TypeScript compiles'));
454
+ console.log(chalk.gray(' Run ') + chalk.cyan('codebakers heal') + chalk.gray(' to auto-fix what\'s possible\n'));
455
+ }
@@ -1562,8 +1562,8 @@ async function showVSCodeClaudeInstructions(): Promise<void> {
1562
1562
  console.log(chalk.gray(' Having issues? Run: ') + chalk.cyan('codebakers doctor') + chalk.gray(' to diagnose\n'));
1563
1563
  }
1564
1564
 
1565
- // v6.16 Bootstrap - SHORT template with magic phrase + rules at START and END
1566
- const V6_CLAUDE_MD = `# CodeBakers v6.16
1565
+ // v6.17 Bootstrap - SHORT template with magic phrase + rules at START and END
1566
+ const V6_CLAUDE_MD = `# CodeBakers v6.17
1567
1567
 
1568
1568
  ## 🪄 MAGIC PHRASE: "codebakers go"
1569
1569
  When user says "codebakers go" in chat, start the onboarding conversation:
@@ -1590,8 +1590,11 @@ When user says "codebakers go" in chat, start the onboarding conversation:
1590
1590
  project_status() → Verify connection FIRST
1591
1591
  discover_patterns({ task: "what you're building" }) → Get patterns BEFORE code
1592
1592
  validate_complete({ feature: "name", files: [...] }) → Validate BEFORE done
1593
+ coherence_audit() → Check wiring & dependencies
1593
1594
  \`\`\`
1594
1595
 
1596
+ Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
1597
+
1595
1598
  Header (after project_status succeeds): 🍪 CodeBakers is working on this...
1596
1599
  Header (if project_status fails): ⚠️ CodeBakers not connected
1597
1600
  Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
@@ -1603,7 +1606,7 @@ Footer (after code): 🍪 **CodeBakers** | Patterns: X | TSC: ✅ | Tests: ✅
1603
1606
  4. Show footer after code responses
1604
1607
  `;
1605
1608
 
1606
- const V6_CURSORRULES = `# CodeBakers v6.16
1609
+ const V6_CURSORRULES = `# CodeBakers v6.17
1607
1610
 
1608
1611
  ## 🪄 "codebakers go" = Start onboarding conversation
1609
1612
  Ask existing/new → Ask what to build → Call init_project() → Help them build
@@ -1620,6 +1623,9 @@ Ask existing/new → Ask what to build → Call init_project() → Help them bui
1620
1623
  3. Show header without project_status succeeding
1621
1624
  4. Skip writing tests for new features
1622
1625
 
1626
+ Commands: /build, /feature, /design, /status, /audit, /coherence, /upgrade
1627
+ Use coherence_audit() to check wiring & dependencies
1628
+
1623
1629
  ## 🚨 ALWAYS (Repeated at End)
1624
1630
  1. project_status() FIRST
1625
1631
  2. discover_patterns() before code
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import { generate } from './commands/generate.js';
18
18
  import { upgrade } from './commands/upgrade.js';
19
19
  import { config } from './commands/config.js';
20
20
  import { audit } from './commands/audit.js';
21
+ import { coherence } from './commands/coherence.js';
21
22
  import { heal, healWatch } from './commands/heal.js';
22
23
  import { pushPatterns, pushPatternsInteractive } from './commands/push-patterns.js';
23
24
  import { go } from './commands/go.js';
@@ -214,12 +215,13 @@ function showWelcome(): void {
214
215
 
215
216
  console.log(chalk.white(' Quality:\n'));
216
217
  console.log(chalk.cyan(' codebakers audit') + chalk.gray(' Run automated code quality checks'));
218
+ console.log(chalk.cyan(' codebakers coherence') + chalk.gray(' Check wiring, imports, and dependencies'));
217
219
  console.log(chalk.cyan(' codebakers heal') + chalk.gray(' Auto-detect and fix common issues'));
218
220
  console.log(chalk.cyan(' codebakers doctor') + chalk.gray(' Check CodeBakers setup\n'));
219
221
 
220
222
  console.log(chalk.white(' All Commands:\n'));
221
223
  console.log(chalk.gray(' go, extend, billing, build, build-status, setup, scaffold, init'));
222
- console.log(chalk.gray(' generate, upgrade, status, audit, heal, doctor, config, login'));
224
+ console.log(chalk.gray(' generate, upgrade, status, audit, coherence, heal, doctor, config, login'));
223
225
  console.log(chalk.gray(' serve, mcp-config, mcp-uninstall\n'));
224
226
 
225
227
  console.log(chalk.gray(' Run ') + chalk.cyan('codebakers <command> --help') + chalk.gray(' for more info\n'));
@@ -357,6 +359,21 @@ program
357
359
  .description('Run automated code quality and security checks')
358
360
  .action(async () => { await audit(); });
359
361
 
362
+ program
363
+ .command('coherence')
364
+ .alias('wiring')
365
+ .description('Check codebase wiring, imports, exports, and dependencies')
366
+ .option('-f, --focus <area>', 'Focus area: all, imports, circular, env (default: all)')
367
+ .option('--fix', 'Automatically fix issues that can be auto-fixed')
368
+ .option('-v, --verbose', 'Show all issues (not just first 5 per category)')
369
+ .action(async (options) => {
370
+ await coherence({
371
+ focus: options.focus,
372
+ fix: options.fix,
373
+ verbose: options.verbose,
374
+ });
375
+ });
376
+
360
377
  program
361
378
  .command('heal')
362
379
  .description('Auto-detect and fix common issues (TypeScript, deps, security)')