bonzai-burn 1.0.23 → 1.0.24

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,16 +1,13 @@
1
1
  {
2
2
  "name": "bonzai-burn",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "Git branch-based cleanup tool with bburn, baccept, and brevert commands",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
8
8
  "bonzai-burn": "./src/index.js",
9
9
  "bburn": "./src/bburn.js",
10
- "baccept": "./src/baccept.js",
11
- "brevert": "./src/brevert.js",
12
- "bgraph": "./src/bgraph.js",
13
- "bonzai-mcp": "./src/mcp-server.js"
10
+ "bgraph": "./src/bgraph.js"
14
11
  },
15
12
  "keywords": [
16
13
  "git",
@@ -1,15 +1,24 @@
1
1
  {
2
- "provider": "cursor",
3
- "debugMode": true,
4
- "autoBurn": false,
2
+ "customChecks": {
3
+ "requirements": "Remove unused imports and variables. Remove all console log statements."
4
+ },
5
5
  "lineLimit": {
6
- "enabled": false,
7
- "limit": 70,
6
+ "enabled": true,
7
+ "limit": 300,
8
8
  "prompt": "Split any file with over {{ linelimit }} lines into smaller files."
9
9
  },
10
10
  "folderLimit": {
11
- "enabled": false,
12
- "limit": 10,
11
+ "enabled": true,
12
+ "limit": 15,
13
13
  "prompt": "Split any folder with over {{ folderlimit }} items into smaller, compartmentalized folders."
14
+ },
15
+ "testCheck": {
16
+ "enabled": false,
17
+ "patterns": {
18
+ ".vue": ".test.js",
19
+ ".jsx": ".test.jsx",
20
+ ".tsx": ".test.tsx"
21
+ },
22
+ "prompt": "Create test files for components that are missing them."
14
23
  }
15
24
  }
@@ -0,0 +1,362 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Static analyzer for codebase
7
+ * Runs ESLint, TypeScript, and custom checks based on config.json
8
+ */
9
+
10
+ /**
11
+ * List all files recursively, respecting common ignore patterns
12
+ */
13
+ function listAllFiles(dir, basePath = '') {
14
+ const ignorePatterns = ['node_modules', '.git', '.DS_Store', 'dist', 'build', 'coverage', 'bonzai'];
15
+ let results = [];
16
+
17
+ try {
18
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
19
+
20
+ for (const entry of entries) {
21
+ const fullPath = path.join(dir, entry.name);
22
+ const relativePath = path.join(basePath, entry.name);
23
+
24
+ if (ignorePatterns.some(p => entry.name === p) || entry.name.startsWith('.')) {
25
+ continue;
26
+ }
27
+
28
+ if (entry.isDirectory()) {
29
+ results = results.concat(listAllFiles(fullPath, relativePath));
30
+ } else {
31
+ results.push({
32
+ path: relativePath,
33
+ fullPath: fullPath
34
+ });
35
+ }
36
+ }
37
+ } catch (e) {
38
+ // Directory access error, skip
39
+ }
40
+
41
+ return results;
42
+ }
43
+
44
+ /**
45
+ * Run ESLint to detect unused imports and variables
46
+ */
47
+ function runEslintAnalysis(rootDir) {
48
+ const issues = [];
49
+
50
+ try {
51
+ execSync('which eslint', { encoding: 'utf-8', stdio: 'pipe' });
52
+ } catch {
53
+ return { issues, skipped: true, reason: 'ESLint not installed' };
54
+ }
55
+
56
+ try {
57
+ const result = execSync(
58
+ `eslint "${rootDir}" --format json --rule "no-unused-vars: error" 2>/dev/null || true`,
59
+ { encoding: 'utf-8', stdio: 'pipe', maxBuffer: 50 * 1024 * 1024 }
60
+ );
61
+
62
+ if (result.trim()) {
63
+ const eslintOutput = JSON.parse(result);
64
+
65
+ for (const file of eslintOutput) {
66
+ for (const msg of file.messages || []) {
67
+ if (msg.ruleId && msg.ruleId.includes('no-unused')) {
68
+ issues.push({
69
+ file: path.relative(rootDir, file.filePath),
70
+ line: msg.line,
71
+ message: msg.message,
72
+ rule: msg.ruleId
73
+ });
74
+ }
75
+ }
76
+ }
77
+ }
78
+ } catch (e) {
79
+ return { issues, skipped: true, reason: 'ESLint analysis failed' };
80
+ }
81
+
82
+ return { issues, skipped: false };
83
+ }
84
+
85
+ /**
86
+ * Run TypeScript compiler to check for unused locals
87
+ */
88
+ function runTypeScriptAnalysis(rootDir) {
89
+ const issues = [];
90
+ const tsconfigPath = path.join(rootDir, 'tsconfig.json');
91
+
92
+ if (!fs.existsSync(tsconfigPath)) {
93
+ return { issues, skipped: true, reason: 'No tsconfig.json found' };
94
+ }
95
+
96
+ try {
97
+ execSync('which tsc', { encoding: 'utf-8', stdio: 'pipe' });
98
+ } catch {
99
+ return { issues, skipped: true, reason: 'TypeScript not installed' };
100
+ }
101
+
102
+ try {
103
+ const result = execSync(
104
+ `cd "${rootDir}" && tsc --noEmit --noUnusedLocals --noUnusedParameters 2>&1 || true`,
105
+ { encoding: 'utf-8', stdio: 'pipe', maxBuffer: 50 * 1024 * 1024 }
106
+ );
107
+
108
+ const lines = result.split('\n');
109
+ const errorRegex = /^(.+)\((\d+),(\d+)\):\s*error\s+TS(\d+):\s*(.+)$/;
110
+
111
+ for (const line of lines) {
112
+ const match = line.match(errorRegex);
113
+ if (match) {
114
+ const [, filePath, lineNum, , errorCode, message] = match;
115
+ // TS6133 = unused variable, TS6196 = unused parameter
116
+ if (['6133', '6196', '6198'].includes(errorCode)) {
117
+ issues.push({
118
+ file: path.relative(rootDir, filePath),
119
+ line: parseInt(lineNum, 10),
120
+ message: message,
121
+ rule: `TS${errorCode}`
122
+ });
123
+ }
124
+ }
125
+ }
126
+ } catch (e) {
127
+ return { issues, skipped: true, reason: 'TypeScript analysis failed' };
128
+ }
129
+
130
+ return { issues, skipped: false };
131
+ }
132
+
133
+ /**
134
+ * Check files against line limit
135
+ */
136
+ function checkLineLimits(files, config) {
137
+ const issues = [];
138
+ const cfg = config.lineLimit || {};
139
+
140
+ if (!cfg.enabled) {
141
+ return { issues, skipped: true, reason: 'Disabled in config' };
142
+ }
143
+
144
+ const maxLines = cfg.limit || 500;
145
+
146
+ for (const file of files) {
147
+ // Skip non-code files
148
+ if (file.path.endsWith('.json') || file.path.endsWith('.lock') || file.path.endsWith('.css')) {
149
+ continue;
150
+ }
151
+
152
+ try {
153
+ const content = fs.readFileSync(file.fullPath, 'utf-8');
154
+ const lineCount = content.split('\n').length;
155
+
156
+ if (lineCount > maxLines) {
157
+ issues.push({
158
+ file: file.path,
159
+ count: lineCount,
160
+ limit: maxLines
161
+ });
162
+ }
163
+ } catch (e) {
164
+ // Can't read file, skip
165
+ }
166
+ }
167
+
168
+ issues.sort((a, b) => b.count - a.count);
169
+ return { issues, skipped: false, prompt: cfg.prompt };
170
+ }
171
+
172
+ /**
173
+ * Check folders against item limit
174
+ */
175
+ function checkFolderLimits(files, config) {
176
+ const issues = [];
177
+ const cfg = config.folderLimit || {};
178
+
179
+ if (!cfg.enabled) {
180
+ return { issues, skipped: true, reason: 'Disabled in config' };
181
+ }
182
+
183
+ const maxItems = cfg.limit || 20;
184
+ const folderCounts = {};
185
+
186
+ for (const file of files) {
187
+ const dir = path.dirname(file.path);
188
+ if (!folderCounts[dir]) {
189
+ folderCounts[dir] = 0;
190
+ }
191
+ folderCounts[dir]++;
192
+ }
193
+
194
+ for (const [folder, count] of Object.entries(folderCounts)) {
195
+ if (count > maxItems) {
196
+ issues.push({
197
+ file: folder,
198
+ count: count,
199
+ limit: maxItems
200
+ });
201
+ }
202
+ }
203
+
204
+ issues.sort((a, b) => b.count - a.count);
205
+ return { issues, skipped: false, prompt: cfg.prompt };
206
+ }
207
+
208
+ /**
209
+ * Check for missing test files
210
+ */
211
+ function checkMissingTests(files, config) {
212
+ const issues = [];
213
+ const cfg = config.testCheck || {};
214
+
215
+ if (!cfg.enabled) {
216
+ return { issues, skipped: true, reason: 'Disabled in config' };
217
+ }
218
+
219
+ const patterns = cfg.patterns || {
220
+ '.vue': '.test.js',
221
+ '.jsx': '.test.jsx',
222
+ '.tsx': '.test.tsx'
223
+ };
224
+
225
+ const testFiles = new Set(
226
+ files
227
+ .filter(f => f.path.includes('.test.') || f.path.includes('.spec.'))
228
+ .map(f => f.path.toLowerCase())
229
+ );
230
+
231
+ for (const file of files) {
232
+ const ext = path.extname(file.path);
233
+ const testExt = patterns[ext];
234
+
235
+ if (!testExt) continue;
236
+ if (file.path.includes('.test.') || file.path.includes('.spec.')) continue;
237
+ if (!file.path.startsWith('src/') && !file.path.startsWith('components/')) continue;
238
+
239
+ const baseName = path.basename(file.path, ext);
240
+ const hasTest = [...testFiles].some(t => t.includes(baseName.toLowerCase()) && t.includes('.test.'));
241
+
242
+ if (!hasTest) {
243
+ issues.push({
244
+ file: file.path,
245
+ expectedTest: `${baseName}${testExt}`
246
+ });
247
+ }
248
+ }
249
+
250
+ return { issues, skipped: false, prompt: cfg.prompt };
251
+ }
252
+
253
+ /**
254
+ * Main analyzer function
255
+ */
256
+ export async function analyze(rootDir = process.cwd(), config = {}) {
257
+ const startTime = Date.now();
258
+ const files = listAllFiles(rootDir);
259
+
260
+ // Run all checks
261
+ const eslint = runEslintAnalysis(rootDir);
262
+ const typescript = runTypeScriptAnalysis(rootDir);
263
+ const lineLimit = checkLineLimits(files, config);
264
+ const folderLimit = checkFolderLimits(files, config);
265
+ const missingTests = checkMissingTests(files, config);
266
+
267
+ const duration = Date.now() - startTime;
268
+
269
+ return {
270
+ eslint,
271
+ typescript,
272
+ lineLimit,
273
+ folderLimit,
274
+ missingTests,
275
+ customRequirements: config.customChecks?.requirements || null,
276
+ filesScanned: files.length,
277
+ durationMs: duration
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Format analysis results for display
283
+ */
284
+ export function formatAnalysisResults(results) {
285
+ let output = '';
286
+ let totalIssues = 0;
287
+
288
+ // ESLint issues
289
+ if (!results.eslint.skipped && results.eslint.issues.length > 0) {
290
+ output += `šŸ—‘ļø UNUSED CODE (ESLint) - ${results.eslint.issues.length} issues\n`;
291
+ for (const issue of results.eslint.issues.slice(0, 15)) {
292
+ output += ` ${issue.file}:${issue.line} - ${issue.message}\n`;
293
+ }
294
+ if (results.eslint.issues.length > 15) {
295
+ output += ` ... and ${results.eslint.issues.length - 15} more\n`;
296
+ }
297
+ output += '\n';
298
+ totalIssues += results.eslint.issues.length;
299
+ }
300
+
301
+ // TypeScript issues
302
+ if (!results.typescript.skipped && results.typescript.issues.length > 0) {
303
+ output += `šŸ”· UNUSED CODE (TypeScript) - ${results.typescript.issues.length} issues\n`;
304
+ for (const issue of results.typescript.issues.slice(0, 15)) {
305
+ output += ` ${issue.file}:${issue.line} - ${issue.message}\n`;
306
+ }
307
+ if (results.typescript.issues.length > 15) {
308
+ output += ` ... and ${results.typescript.issues.length - 15} more\n`;
309
+ }
310
+ output += '\n';
311
+ totalIssues += results.typescript.issues.length;
312
+ }
313
+
314
+ // Line limit issues
315
+ if (!results.lineLimit.skipped && results.lineLimit.issues.length > 0) {
316
+ output += `šŸ“ FILES OVER LINE LIMIT - ${results.lineLimit.issues.length} files\n`;
317
+ for (const issue of results.lineLimit.issues) {
318
+ output += ` ${issue.file} - ${issue.count} lines (limit: ${issue.limit})\n`;
319
+ }
320
+ if (results.lineLimit.prompt) {
321
+ output += `\n → ${results.lineLimit.prompt.replace(/\{\{\s*linelimit\s*\}\}/gi, results.lineLimit.issues[0]?.limit || '')}\n`;
322
+ }
323
+ output += '\n';
324
+ totalIssues += results.lineLimit.issues.length;
325
+ }
326
+
327
+ // Folder limit issues
328
+ if (!results.folderLimit.skipped && results.folderLimit.issues.length > 0) {
329
+ output += `šŸ“ FOLDERS OVER ITEM LIMIT - ${results.folderLimit.issues.length} folders\n`;
330
+ for (const issue of results.folderLimit.issues) {
331
+ output += ` ${issue.file}/ - ${issue.count} items (limit: ${issue.limit})\n`;
332
+ }
333
+ if (results.folderLimit.prompt) {
334
+ output += `\n → ${results.folderLimit.prompt.replace(/\{\{\s*folderlimit\s*\}\}/gi, results.folderLimit.issues[0]?.limit || '')}\n`;
335
+ }
336
+ output += '\n';
337
+ totalIssues += results.folderLimit.issues.length;
338
+ }
339
+
340
+ // Missing tests
341
+ if (!results.missingTests.skipped && results.missingTests.issues.length > 0) {
342
+ output += `🧪 MISSING TESTS - ${results.missingTests.issues.length} files\n`;
343
+ for (const issue of results.missingTests.issues.slice(0, 10)) {
344
+ output += ` ${issue.file} → needs ${issue.expectedTest}\n`;
345
+ }
346
+ if (results.missingTests.issues.length > 10) {
347
+ output += ` ... and ${results.missingTests.issues.length - 10} more\n`;
348
+ }
349
+ output += '\n';
350
+ totalIssues += results.missingTests.issues.length;
351
+ }
352
+
353
+ // Custom requirements
354
+ if (results.customRequirements) {
355
+ output += `šŸ“‹ CUSTOM REQUIREMENTS\n`;
356
+ output += ` ${results.customRequirements}\n\n`;
357
+ }
358
+
359
+ return { output, totalIssues };
360
+ }
361
+
362
+ export default { analyze, formatAnalysisResults };
package/src/bburn.js CHANGED
@@ -1,499 +1,58 @@
1
1
  #!/usr/bin/env node
2
- import { execSync, spawn } from 'child_process';
3
- import crypto from 'crypto';
4
2
  import fs from 'fs';
5
- import { join, dirname } from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
3
+ import { join } from 'path';
4
+ import { analyze, formatAnalysisResults } from './analyzer.js';
10
5
 
11
6
  const BONZAI_DIR = 'bonzai';
12
- const SPECS_FILE = 'specs.md';
13
7
  const CONFIG_FILE = 'config.json';
14
8
 
15
- // Template folder in the package (ships as payload-bonzai, copied as bonzai)
16
- const TEMPLATE_DIR = join(__dirname, '..', 'payload-bonzai');
17
-
18
- // Parse --provider / -p argument, with config as fallback
19
- function parseProvider(configDefault = 'claude') {
20
- const args = process.argv.slice(2);
21
- let provider = null;
22
-
23
- for (let i = 0; i < args.length; i++) {
24
- if (args[i] === '--provider' || args[i] === '-p') {
25
- provider = args[i + 1];
26
- break;
27
- }
28
- if (args[i].startsWith('--provider=')) {
29
- provider = args[i].split('=')[1];
30
- break;
31
- }
32
- }
33
-
34
- // Use config default if no CLI arg provided
35
- if (!provider) {
36
- provider = configDefault;
37
- }
38
-
39
- const validProviders = ['claude', 'cursor'];
40
- if (!validProviders.includes(provider)) {
41
- console.error(`āŒ Invalid provider: "${provider}". Must be one of: ${validProviders.join(', ')}`);
42
- process.exit(1);
43
- }
44
-
45
- return provider;
46
- }
47
-
48
- function initializeBonzai() {
49
- const bonzaiPath = join(process.cwd(), BONZAI_DIR);
50
- const specsPath = join(bonzaiPath, SPECS_FILE);
51
- const configPath = join(bonzaiPath, CONFIG_FILE);
52
-
53
- // Check if bonzai/ folder exists
54
- if (!fs.existsSync(bonzaiPath)) {
55
- fs.mkdirSync(bonzaiPath, { recursive: true });
56
- console.log(`šŸ“ Created ${BONZAI_DIR}/ folder`);
57
- }
58
-
59
- // Copy specs.md from package template
60
- if (!fs.existsSync(specsPath)) {
61
- fs.copyFileSync(join(TEMPLATE_DIR, SPECS_FILE), specsPath);
62
- console.log(`šŸ“ Created ${BONZAI_DIR}/${SPECS_FILE}`);
63
- }
64
-
65
- // Copy config.json from package template
66
- if (!fs.existsSync(configPath)) {
67
- fs.copyFileSync(join(TEMPLATE_DIR, CONFIG_FILE), configPath);
68
- console.log(`āš™ļø Created ${BONZAI_DIR}/${CONFIG_FILE}`);
69
- console.log(`\nāš ļø Please edit ${BONZAI_DIR}/${SPECS_FILE} to define your cleanup rules before running bburn.\n`);
70
- process.exit(0);
71
- }
72
- }
73
-
74
- function ensureBonzaiDir() {
75
- const bonzaiPath = join(process.cwd(), BONZAI_DIR);
76
- const specsPath = join(bonzaiPath, SPECS_FILE);
77
- const configPath = join(bonzaiPath, CONFIG_FILE);
78
-
79
- if (!fs.existsSync(bonzaiPath)) {
80
- fs.mkdirSync(bonzaiPath, { recursive: true });
81
- console.log(`šŸ“ Created ${BONZAI_DIR}/ folder\n`);
82
- }
83
-
84
- if (!fs.existsSync(specsPath)) {
85
- fs.copyFileSync(join(TEMPLATE_DIR, SPECS_FILE), specsPath);
86
- console.log(`šŸ“ Created ${BONZAI_DIR}/${SPECS_FILE} - edit this file to define your cleanup specs\n`);
87
- }
9
+ /**
10
+ * Load project config from bonzai/config.json
11
+ * This is the source of truth for all burn configuration
12
+ */
13
+ function loadConfig() {
14
+ const configPath = join(process.cwd(), BONZAI_DIR, CONFIG_FILE);
88
15
 
89
16
  if (!fs.existsSync(configPath)) {
90
- fs.copyFileSync(join(TEMPLATE_DIR, CONFIG_FILE), configPath);
91
- console.log(`āš™ļø Created ${BONZAI_DIR}/${CONFIG_FILE}\n`);
17
+ console.error(`āŒ No config found at ${BONZAI_DIR}/${CONFIG_FILE}`);
18
+ console.error(` Run 'bonzai-burn' to initialize.\n`);
19
+ process.exit(1);
92
20
  }
93
21
 
94
- return { specsPath, configPath };
95
- }
96
-
97
- function loadConfig(configPath) {
98
22
  try {
99
23
  const content = fs.readFileSync(configPath, 'utf-8');
100
24
  return JSON.parse(content);
101
- } catch {
102
- return { debugMode: false };
103
- }
104
- }
105
-
106
- function loadSpecs(specsPath, config) {
107
- let content = fs.readFileSync(specsPath, 'utf-8');
108
-
109
- // Process lineLimit if enabled
110
- if (config.lineLimit?.enabled) {
111
- content = content.replace(/\{\{\s*linelimit\s*\}\}/gi, config.lineLimit.limit);
112
- } else {
113
- // Remove lines containing {{ linelimit }} if disabled
114
- content = content.split('\n')
115
- .filter(line => !/\{\{\s*linelimit\s*\}\}/i.test(line))
116
- .join('\n');
117
- }
118
-
119
- // Process folderLimit if enabled
120
- if (config.folderLimit?.enabled) {
121
- content = content.replace(/\{\{\s*folderlimit\s*\}\}/gi, config.folderLimit.limit);
122
- } else {
123
- // Remove lines containing {{ folderlimit }} if disabled
124
- content = content.split('\n')
125
- .filter(line => !/\{\{\s*folderlimit\s*\}\}/i.test(line))
126
- .join('\n');
25
+ } catch (e) {
26
+ console.error(`āŒ Could not parse ${BONZAI_DIR}/${CONFIG_FILE}`);
27
+ process.exit(1);
127
28
  }
128
-
129
- return `You are a code cleanup assistant. Follow these specifications:\n\n${content}`;
130
- }
131
-
132
- function exec(command) {
133
- return execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();
134
29
  }
135
30
 
136
- function execVisible(command) {
137
- execSync(command, { stdio: 'inherit' });
138
- }
139
-
140
- function executeClaude(requirements, config) {
141
- // Check if Claude CLI exists
142
- try {
143
- execSync('which claude', { encoding: 'utf-8', stdio: 'pipe' });
144
- } catch (error) {
145
- throw new Error(
146
- 'Claude Code CLI not found.\n' +
147
- 'Install it with: npm install -g @anthropic-ai/claude-code'
148
- );
149
- }
150
-
151
- const debugMode = config.debugMode === true || config.debugMode === 'true';
152
-
153
- // Debug mode: run Claude with visible output
154
- if (debugMode) {
155
- console.log('šŸ› Running in debug mode...\n');
156
- return new Promise((resolve, reject) => {
157
- const args = [
158
- '-p', requirements,
159
- '--allowedTools', 'Read,Write,Edit,Bash',
160
- '--permission-mode', 'dontAsk'
161
- ];
162
-
163
- const claude = spawn('claude', args, {
164
- stdio: 'inherit'
165
- });
166
-
167
- claude.on('close', (code) => {
168
- if (code === 0) {
169
- resolve();
170
- } else {
171
- reject(new Error(`Claude exited with code ${code}`));
172
- }
173
- });
174
-
175
- claude.on('error', (err) => {
176
- reject(new Error(`Failed to execute Claude: ${err.message}`));
177
- });
178
- });
179
- }
180
-
181
- // Headless mode with token tracking
182
- let totalInputTokens = 0;
183
- let totalOutputTokens = 0;
184
- let lastToolName = '';
185
-
186
- return new Promise((resolve, reject) => {
187
- const args = [
188
- '-p', requirements,
189
- '--allowedTools', 'Read,Write,Edit,Bash',
190
- '--permission-mode', 'dontAsk',
191
- '--output-format', 'stream-json',
192
- '--verbose'
193
- ];
194
-
195
- const claude = spawn('claude', args, {
196
- stdio: ['inherit', 'pipe', 'pipe']
197
- });
198
-
199
- let buffer = '';
200
-
201
- claude.stdout.on('data', (data) => {
202
- buffer += data.toString();
203
-
204
- // Process complete JSON lines
205
- const lines = buffer.split('\n');
206
- buffer = lines.pop(); // Keep incomplete line in buffer
207
-
208
- for (const line of lines) {
209
- if (!line.trim()) continue;
210
-
211
- try {
212
- const event = JSON.parse(line);
31
+ async function main() {
32
+ console.log('\nšŸ”„ Bonzai Burn - Code Analysis\n');
213
33
 
214
- // Track tokens from assistant messages
215
- if (event.type === 'assistant' && event.message?.usage) {
216
- const usage = event.message.usage;
217
- if (usage.input_tokens) totalInputTokens += usage.input_tokens;
218
- if (usage.output_tokens) totalOutputTokens += usage.output_tokens;
219
- }
34
+ // Load config - source of truth
35
+ const config = loadConfig();
220
36
 
221
- // Show tool usage updates
222
- if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
223
- lastToolName = event.content_block.name || '';
224
- }
37
+ console.log('Scanning...\n');
225
38
 
226
- if (event.type === 'content_block_stop' && lastToolName) {
227
- const icon = getToolIcon(lastToolName);
228
- console.log(` ${icon} ${lastToolName}`);
229
- lastToolName = '';
230
- }
39
+ // Run analysis
40
+ const results = await analyze(process.cwd(), config);
41
+ const { output, totalIssues } = formatAnalysisResults(results);
231
42
 
232
- // Show result events with file info
233
- if (event.type === 'result') {
234
- if (event.usage) {
235
- // Final usage stats
236
- totalInputTokens = event.usage.input_tokens || totalInputTokens;
237
- totalOutputTokens = event.usage.output_tokens || totalOutputTokens;
238
- }
239
- }
240
-
241
- } catch (e) {
242
- // Not valid JSON, skip
243
- }
244
- }
245
- });
246
-
247
- claude.stderr.on('data', (data) => {
248
- // Show errors but don't clutter with minor stderr
249
- const msg = data.toString().trim();
250
- if (msg && !msg.includes('ExperimentalWarning')) {
251
- console.error(msg);
252
- }
253
- });
254
-
255
- claude.on('close', (code) => {
256
- // Print token summary
257
- console.log(`\nšŸ“Š Tokens: ${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out`);
258
-
259
- if (code === 0) {
260
- resolve();
261
- } else {
262
- reject(new Error(`Claude exited with code ${code}`));
263
- }
264
- });
265
-
266
- claude.on('error', (err) => {
267
- reject(new Error(`Failed to execute Claude: ${err.message}`));
268
- });
269
- });
270
- }
271
-
272
- function getToolIcon(toolName) {
273
- const icons = {
274
- 'Read': 'šŸ“–',
275
- 'Write': 'āœļø',
276
- 'Edit': 'šŸ”§',
277
- 'Bash': 'šŸ’»',
278
- 'Glob': 'šŸ”',
279
- 'Grep': 'šŸ”Ž'
280
- };
281
- return icons[toolName] || 'šŸ”¹';
282
- }
283
-
284
- function executeCursor(requirements, config) {
285
- // Check if cursor-agent CLI exists
286
- try {
287
- execSync('which cursor-agent', { encoding: 'utf-8', stdio: 'pipe' });
288
- } catch (error) {
289
- throw new Error(
290
- 'cursor-agent CLI not found.\n' +
291
- 'Install it with: npm install -g cursor-agent'
292
- );
293
- }
294
-
295
- const debugMode = config.debugMode === true || config.debugMode === 'true';
296
-
297
- // Debug mode: run cursor-agent with verbose output
298
- if (debugMode) {
299
- console.log('šŸ› Running in debug mode...\n');
300
- return new Promise((resolve, reject) => {
301
- const args = ['-p', requirements];
302
-
303
- const cursor = spawn('cursor-agent', args, {
304
- stdio: 'inherit'
305
- });
306
-
307
- cursor.on('close', (code) => {
308
- if (code === 0) {
309
- resolve();
310
- } else {
311
- reject(new Error(`cursor-agent exited with code ${code}`));
312
- }
313
- });
314
-
315
- cursor.on('error', (err) => {
316
- reject(new Error(`Failed to execute cursor-agent: ${err.message}`));
317
- });
318
- });
43
+ // Display results
44
+ if (totalIssues > 0 || results.customRequirements) {
45
+ console.log(output);
46
+ } else {
47
+ console.log('āœ“ No issues found\n');
319
48
  }
320
49
 
321
- // Headless mode with token tracking
322
- let totalInputTokens = 0;
323
- let totalOutputTokens = 0;
324
- let lastToolName = '';
325
-
326
- return new Promise((resolve, reject) => {
327
- const args = [
328
- '-p', requirements,
329
- '--output-format', 'stream-json'
330
- ];
331
-
332
- const cursor = spawn('cursor-agent', args, {
333
- stdio: ['inherit', 'pipe', 'pipe']
334
- });
335
-
336
- let buffer = '';
337
-
338
- cursor.stdout.on('data', (data) => {
339
- buffer += data.toString();
340
-
341
- // Process complete JSON lines
342
- const lines = buffer.split('\n');
343
- buffer = lines.pop(); // Keep incomplete line in buffer
344
-
345
- for (const line of lines) {
346
- if (!line.trim()) continue;
347
-
348
- try {
349
- const event = JSON.parse(line);
350
-
351
- // Track tokens from assistant messages
352
- if (event.type === 'assistant' && event.message?.usage) {
353
- const usage = event.message.usage;
354
- if (usage.input_tokens) totalInputTokens += usage.input_tokens;
355
- if (usage.output_tokens) totalOutputTokens += usage.output_tokens;
356
- }
357
-
358
- // Show tool usage updates
359
- if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
360
- lastToolName = event.content_block.name || '';
361
- }
362
-
363
- if (event.type === 'content_block_stop' && lastToolName) {
364
- const icon = getToolIcon(lastToolName);
365
- console.log(` ${icon} ${lastToolName}`);
366
- lastToolName = '';
367
- }
368
-
369
- // Show result events with usage info
370
- if (event.type === 'result') {
371
- if (event.usage) {
372
- totalInputTokens = event.usage.input_tokens || totalInputTokens;
373
- totalOutputTokens = event.usage.output_tokens || totalOutputTokens;
374
- }
375
- }
376
-
377
- } catch (e) {
378
- // Not valid JSON, skip
379
- }
380
- }
381
- });
382
-
383
- cursor.stderr.on('data', (data) => {
384
- const msg = data.toString().trim();
385
- if (msg && !msg.includes('ExperimentalWarning')) {
386
- console.error(msg);
387
- }
388
- });
389
-
390
- cursor.on('close', (code) => {
391
- // Print token summary
392
- console.log(`\nšŸ“Š Tokens: ${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out`);
393
-
394
- if (code === 0) {
395
- resolve();
396
- } else {
397
- reject(new Error(`cursor-agent exited with code ${code}`));
398
- }
399
- });
400
-
401
- cursor.on('error', (err) => {
402
- reject(new Error(`Failed to execute cursor-agent: ${err.message}`));
403
- });
404
- });
405
- }
406
-
407
- async function burn() {
408
- try {
409
- // Initialize bonzai folder and specs.md on first execution
410
- initializeBonzai();
411
-
412
- // Ensure bonzai directory and specs file exist
413
- const { specsPath, configPath } = ensureBonzaiDir();
414
- const config = loadConfig(configPath);
415
- const specs = loadSpecs(specsPath, config);
416
-
417
- // Determine provider: CLI arg overrides config
418
- const provider = parseProvider(config.provider || 'claude');
419
-
420
- // Check if in git repo
421
- try {
422
- exec('git rev-parse --git-dir');
423
- } catch {
424
- console.error('āŒ Not a git repository');
425
- process.exit(1);
426
- }
427
-
428
- // Get current branch
429
- const originalBranch = exec('git branch --show-current');
430
-
431
- // Handle uncommitted changes - auto-commit to current branch
432
- const hasChanges = exec('git status --porcelain') !== '';
433
- let madeWipCommit = false;
434
-
435
- if (hasChanges) {
436
- const timestamp = Date.now();
437
- console.log('šŸ’¾ Auto-committing your work...');
438
- exec('git add -A');
439
- exec(`git commit -m "WIP: pre-burn checkpoint ${timestamp}"`);
440
- madeWipCommit = true;
441
- console.log(`āœ“ Work saved on ${originalBranch}\n`);
442
- }
443
-
444
- // Generate unique branch name with short UUID
445
- const shortId = crypto.randomUUID().slice(0, 8);
446
- const burnBranch = `bonzai-burn-${shortId}`;
447
-
448
- console.log(`šŸ“ Starting from: ${originalBranch}`);
449
- console.log(`🌿 Creating: ${burnBranch}\n`);
450
-
451
- // Create burn branch from current position
452
- exec(`git checkout -b ${burnBranch}`);
453
-
454
- // Save metadata for revert
455
- exec(`git config bonzai.originalBranch ${originalBranch}`);
456
- exec(`git config bonzai.burnBranch ${burnBranch}`);
457
- exec(`git config bonzai.madeWipCommit ${madeWipCommit}`);
458
-
459
- console.log(`šŸ“‹ Specs loaded from: ${BONZAI_DIR}/${SPECS_FILE}`);
460
- console.log(`šŸ¤– Provider: ${provider}`);
461
- console.log(`šŸ› Debug mode: ${config.debugMode === true || config.debugMode === 'true' ? 'on' : 'off'}`);
462
- console.log('šŸ”„ Running Bonzai burn...\n');
463
-
464
- const startTime = Date.now();
465
-
466
- // Execute with the selected provider
467
- if (provider === 'cursor') {
468
- await executeCursor(specs, config);
469
- } else {
470
- await executeClaude(specs, config);
471
- }
472
-
473
- const duration = Math.round((Date.now() - startTime) / 1000);
474
-
475
- console.log(`\nāœ“ Burn complete (${duration}s)\n`);
476
-
477
- // Commit burn changes
478
- const burnTimestamp = Date.now();
479
- exec('git add -A');
480
- exec(`git commit -m "bonzai burn ${burnTimestamp}" --allow-empty`);
481
-
482
- console.log('Files changed from original:');
483
- execVisible(`git diff --stat ${originalBranch}..${burnBranch}`);
484
-
485
- console.log(`\nāœ… Changes applied on: ${burnBranch}`);
486
- console.log(`šŸ“Š Full diff: git diff ${originalBranch}`);
487
- console.log(`\nāœ“ Keep changes: baccept`);
488
- console.log(`āœ— Discard: brevert\n`);
489
-
490
- } catch (error) {
491
- console.error('āŒ Burn failed:', error.message);
492
- if (error.message.includes('Claude Code CLI not found')) {
493
- console.error('\n' + error.message);
494
- }
495
- process.exit(1);
496
- }
50
+ // Summary
51
+ console.log('─'.repeat(50));
52
+ console.log(`Found ${totalIssues} issues across ${results.filesScanned} files (${results.durationMs}ms)\n`);
497
53
  }
498
54
 
499
- burn();
55
+ main().catch((error) => {
56
+ console.error('Error:', error.message);
57
+ process.exit(1);
58
+ });
package/src/index.js CHANGED
@@ -19,11 +19,10 @@ function init() {
19
19
  }
20
20
 
21
21
  mkdirSync(bonzaiPath, { recursive: true });
22
- copyFileSync(join(TEMPLATE_DIR, 'specs.md'), join(bonzaiPath, 'specs.md'));
23
22
  copyFileSync(join(TEMPLATE_DIR, 'config.json'), join(bonzaiPath, 'config.json'));
24
- console.log(`šŸ“ Created ${BONZAI_DIR}/ folder with specs.md and config.json`);
25
- console.log(`šŸ“ Edit ${BONZAI_DIR}/specs.md to define your cleanup rules`);
26
- console.log(`šŸ”„ Run 'bburn' to start a burn session`);
23
+ console.log(`šŸ“ Created ${BONZAI_DIR}/ folder with config.json`);
24
+ console.log(`šŸ“ Edit ${BONZAI_DIR}/config.json to configure your burn rules`);
25
+ console.log(`šŸ”„ Run 'bburn' to analyze your codebase`);
27
26
  console.log('');
28
27
  console.log('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”');
29
28
  console.log('│ │');
@@ -1,9 +0,0 @@
1
- # Bonzai Specs
2
-
3
- Define your cleanup requirements below. bburn will follow these instructions.
4
-
5
- ## Custom Requirements:
6
- - Remove unused imports and variables
7
- - Remove all console log statements
8
- - Split any file with over {{ linelimit }} lines into smaller files
9
- - Split any folder with over {{ folderlimit }} items into smaller, compartmentalized folders
package/src/baccept.js DELETED
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env node
2
- import { execSync } from 'child_process';
3
-
4
- function exec(command) {
5
- return execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();
6
- }
7
-
8
- function execVisible(command) {
9
- execSync(command, { stdio: 'inherit' });
10
- }
11
-
12
- async function accept() {
13
- try {
14
- // Get saved metadata
15
- let originalBranch;
16
- let burnBranch;
17
- let madeWipCommit;
18
-
19
- try {
20
- originalBranch = exec('git config bonzai.originalBranch');
21
- burnBranch = exec('git config bonzai.burnBranch');
22
- madeWipCommit = exec('git config bonzai.madeWipCommit') === 'true';
23
- } catch {
24
- console.error('āŒ No burn to accept');
25
- console.error('Run bburn first');
26
- process.exit(1);
27
- }
28
-
29
- console.log(`āœ… Accepting burn changes...`);
30
- console.log(` Merging: ${burnBranch} → ${originalBranch}\n`);
31
-
32
- // Checkout original branch
33
- execVisible(`git checkout ${originalBranch}`);
34
-
35
- // Merge burn branch into original
36
- execVisible(`git merge ${burnBranch} -m "Accept bonzai burn from ${burnBranch}"`);
37
-
38
- // If we made a WIP commit, we need to handle it
39
- // The merge already includes the burn changes on top of the WIP commit
40
- // So we can optionally squash or leave as-is
41
- if (madeWipCommit) {
42
- console.log('\nšŸ’” Note: Your pre-burn WIP commit was preserved in the history.');
43
- }
44
-
45
- // Clean up metadata
46
- exec('git config --unset bonzai.originalBranch');
47
- exec('git config --unset bonzai.burnBranch');
48
- exec('git config --unset bonzai.madeWipCommit');
49
-
50
- console.log(`\nāœ“ Burn accepted and merged`);
51
- console.log(`Now on: ${originalBranch}`);
52
- console.log(`Branch kept: ${burnBranch}\n`);
53
-
54
- } catch (error) {
55
- console.error('āŒ Accept failed:', error.message);
56
- process.exit(1);
57
- }
58
- }
59
-
60
- accept();
package/src/brevert.js DELETED
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env node
2
- import { execSync } from 'child_process';
3
-
4
- function exec(command) {
5
- return execSync(command, { encoding: 'utf-8', stdio: 'pipe' }).trim();
6
- }
7
-
8
- function execVisible(command) {
9
- execSync(command, { stdio: 'inherit' });
10
- }
11
-
12
- async function revert() {
13
- try {
14
- // Get saved metadata
15
- let originalBranch;
16
- let burnBranch;
17
- let madeWipCommit;
18
-
19
- try {
20
- originalBranch = exec('git config bonzai.originalBranch');
21
- burnBranch = exec('git config bonzai.burnBranch');
22
- madeWipCommit = exec('git config bonzai.madeWipCommit') === 'true';
23
- } catch {
24
- console.error('āŒ No burn to revert');
25
- console.error('Run bburn first');
26
- process.exit(1);
27
- }
28
-
29
- console.log(`šŸ”™ Reverting burn...`);
30
- console.log(` Discarding: ${burnBranch}\n`);
31
-
32
- // Checkout original branch
33
- execVisible(`git checkout ${originalBranch}`);
34
-
35
- // Delete burn branch
36
- execVisible(`git branch -D ${burnBranch}`);
37
-
38
- // Undo WIP commit if we made one
39
- if (madeWipCommit) {
40
- console.log('ā†©ļø Undoing WIP commit...');
41
- exec('git reset HEAD~1');
42
- console.log('āœ“ Back to uncommitted changes\n');
43
- }
44
-
45
- // Clean up metadata
46
- exec('git config --unset bonzai.originalBranch');
47
- exec('git config --unset bonzai.burnBranch');
48
- exec('git config --unset bonzai.madeWipCommit');
49
-
50
- console.log(`āœ“ Burn fully reverted`);
51
- console.log(`Back on: ${originalBranch}\n`);
52
-
53
- } catch (error) {
54
- console.error('āŒ Revert failed:', error.message);
55
- process.exit(1);
56
- }
57
- }
58
-
59
- revert();