bit-cli-ai 1.0.5 → 1.0.7

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": "bit-cli-ai",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Git with a brain - intelligent Git wrapper with AI-powered commits, ghost branches, and predictive merge",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -36,28 +36,50 @@ function createGhostBranch(name) {
36
36
  const spinner = ora(`Creating ghost branch: ${name}`).start();
37
37
 
38
38
  try {
39
- const fullName = `${GHOST_PREFIX}${name}`;
39
+ // Get current state
40
+ const currentBranch = getCurrentBranch();
41
+ let currentCommit = '';
42
+ try {
43
+ currentCommit = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
44
+ } catch {
45
+ // If no commits yet, use empty string
46
+ currentCommit = '';
47
+ }
40
48
 
41
- // Create the actual git branch
42
- execSync(`git branch ${fullName}`, { stdio: 'pipe' });
43
-
44
- // Record in metadata
49
+ // Get staged and unstaged files
50
+ const staged = execSync('git diff --cached --name-only', { encoding: 'utf-8' })
51
+ .split('\n')
52
+ .filter(f => f.trim());
53
+ const unstaged = execSync('git diff --name-only', { encoding: 'utf-8' })
54
+ .split('\n')
55
+ .filter(f => f.trim());
56
+
57
+ // DO NOT create git branch - ghost branches are metadata only
58
+ // Record in metadata only
45
59
  const metadata = loadMetadata();
46
60
  metadata.ghostBranches = metadata.ghostBranches || [];
47
61
 
48
- if (!metadata.ghostBranches.find(b => b.name === fullName)) {
62
+ if (!metadata.ghostBranches.find(b => b.name === name)) {
49
63
  metadata.ghostBranches.push({
50
- name: fullName,
64
+ name: name, // NO ghost/ prefix
65
+ baseCommit: currentCommit,
51
66
  createdFrom: currentBranch,
52
67
  createdAt: new Date().toISOString(),
53
- isGhost: true
68
+ isGhost: true,
69
+ status: {
70
+ commit: currentCommit,
71
+ staged: staged,
72
+ unstaged: unstaged
73
+ }
54
74
  });
55
75
  saveMetadata(metadata);
56
76
  }
57
77
 
58
- spinner.succeed(`Ghost branch created: ${chalk.magenta(fullName)}`);
59
- console.log(chalk.gray(` Created from: ${currentBranch}`));
60
- console.log(chalk.gray(` To checkout: bit checkout ${name} --ghost`));
78
+ spinner.succeed(`Ghost branch created: ${chalk.magenta(name)} šŸ‘»`);
79
+ console.log(chalk.gray(` Based on: ${currentCommit.substring(0, 8)}...`));
80
+ console.log(chalk.gray(` Created from: ${chalk.cyan(currentBranch)}`));
81
+ console.log(chalk.gray(` To checkout: ${chalk.yellow('bit checkout --ghost ' + name)}`));
82
+ console.log(chalk.gray(` Note: Ghost branches exist only in metadata, not as git branches`));
61
83
 
62
84
  } catch (error) {
63
85
  spinner.fail('Failed to create ghost branch');
@@ -68,24 +90,24 @@ function createGhostBranch(name) {
68
90
  // List all branches (including ghost)
69
91
  function listAllBranches(showOnlyGhost = false) {
70
92
  try {
71
- // Get all git branches
72
- const branches = execSync('git branch -a', { encoding: 'utf-8' })
73
- .split('\n')
74
- .map(b => b.trim())
75
- .filter(b => b);
76
-
77
93
  const currentBranch = getCurrentBranch();
78
94
  const metadata = loadMetadata();
79
95
  const ghostBranches = metadata.ghostBranches || [];
80
96
 
81
97
  console.log(chalk.cyan('\n--- Branches ---\n'));
82
98
 
83
- // Regular branches
99
+ // Regular branches (from git)
84
100
  if (!showOnlyGhost) {
101
+ const branches = execSync('git branch', { encoding: 'utf-8' })
102
+ .split('\n')
103
+ .map(b => b.trim())
104
+ .filter(b => b && !b.includes('HEAD'));
105
+
85
106
  console.log(chalk.yellow('Regular branches:'));
86
- branches
87
- .filter(b => !b.includes(GHOST_PREFIX) && !b.startsWith('remotes/'))
88
- .forEach(branch => {
107
+ if (branches.length === 0) {
108
+ console.log(chalk.gray(' No branches yet'));
109
+ } else {
110
+ branches.forEach(branch => {
89
111
  const isCurrent = branch.startsWith('*');
90
112
  const name = branch.replace('* ', '');
91
113
  if (isCurrent) {
@@ -94,30 +116,23 @@ function listAllBranches(showOnlyGhost = false) {
94
116
  console.log(chalk.gray(` ${name}`));
95
117
  }
96
118
  });
119
+ }
97
120
  console.log('');
98
121
  }
99
122
 
100
- // Ghost branches
101
- console.log(chalk.magenta('Ghost branches:'));
102
- const gitGhostBranches = branches.filter(b => b.includes(GHOST_PREFIX));
123
+ // Ghost branches (from metadata only)
124
+ console.log(chalk.magenta('Ghost branches: šŸ‘»'));
103
125
 
104
- if (gitGhostBranches.length === 0) {
126
+ if (ghostBranches.length === 0) {
105
127
  console.log(chalk.gray(' No ghost branches'));
128
+ console.log(chalk.gray(` Create one with: ${chalk.yellow('bit branch --ghost <name>')}`));
106
129
  } else {
107
- gitGhostBranches.forEach(branch => {
108
- const isCurrent = branch.startsWith('*');
109
- const name = branch.replace('* ', '').replace(GHOST_PREFIX, '');
110
- const ghostMeta = ghostBranches.find(g => g.name.includes(name));
111
-
112
- if (isCurrent) {
113
- console.log(chalk.green(` * ${name}`));
114
- } else {
115
- console.log(chalk.magenta(` ${name}`));
116
- }
130
+ ghostBranches.forEach((g, i) => {
131
+ const age = Math.round((Date.now() - new Date(g.createdAt).getTime()) / 60000);
132
+ const ageText = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;
117
133
 
118
- if (ghostMeta) {
119
- console.log(chalk.gray(` Created from: ${ghostMeta.createdFrom}`));
120
- }
134
+ console.log(chalk.magenta(` ${g.name}`));
135
+ console.log(chalk.gray(` ${chalk.dim('Based on:')} ${g.baseCommit ? g.baseCommit.substring(0, 8) : 'initial'} ${chalk.dim('•')} Created from: ${g.createdFrom} ${chalk.dim('•')} ${ageText}`));
121
136
  });
122
137
  }
123
138
 
@@ -134,17 +149,26 @@ function deleteGhostBranch(name) {
134
149
  const spinner = ora(`Deleting ghost branch: ${name}`).start();
135
150
 
136
151
  try {
137
- const fullName = name.startsWith(GHOST_PREFIX) ? name : `${GHOST_PREFIX}${name}`;
138
-
139
- // Delete git branch
140
- execSync(`git branch -D ${fullName}`, { stdio: 'pipe' });
152
+ // Find in metadata
153
+ const fullName = name.startsWith(GHOST_PREFIX) ? name.replace(GHOST_PREFIX, '') : name;
141
154
 
142
- // Remove from metadata
155
+ // Remove from metadata only (no git branch to delete)
143
156
  const metadata = loadMetadata();
157
+ const ghostData = metadata.ghostBranches?.find(g => g.name === fullName);
158
+
159
+ if (!ghostData) {
160
+ spinner.fail(`Ghost branch not found: ${fullName}`);
161
+ console.log(chalk.gray('\nAvailable ghost branches:'));
162
+ metadata.ghostBranches?.forEach(b => {
163
+ console.log(chalk.gray(` ${b.name}`));
164
+ });
165
+ return;
166
+ }
167
+
144
168
  metadata.ghostBranches = (metadata.ghostBranches || []).filter(b => b.name !== fullName);
145
169
  saveMetadata(metadata);
146
170
 
147
- spinner.succeed(`Ghost branch deleted: ${chalk.magenta(fullName)}`);
171
+ spinner.succeed(`Ghost branch deleted: ${chalk.magenta(fullName)} šŸ‘»`);
148
172
 
149
173
  } catch (error) {
150
174
  spinner.fail('Failed to delete ghost branch');
@@ -174,17 +198,45 @@ export function checkoutGhost(branch, options) {
174
198
 
175
199
  try {
176
200
  let targetBranch = branch;
201
+ let isGhost = false;
202
+ let ghostData = null;
177
203
 
178
204
  if (options.ghost) {
179
- // It's a ghost branch
180
- targetBranch = branch.startsWith(GHOST_PREFIX) ? branch : `${GHOST_PREFIX}${branch}`;
205
+ // It's a ghost branch - find in metadata
206
+ const metadata = loadMetadata();
207
+ const ghostName = branch.startsWith(GHOST_PREFIX) ? branch.replace(GHOST_PREFIX, '') : branch;
208
+ ghostData = metadata.ghostBranches?.find(g => g.name === ghostName);
209
+
210
+ if (!ghostData) {
211
+ spinner.fail(`Ghost branch not found: ${ghostName}`);
212
+ console.log(chalk.yellow('\nAvailable ghost branches:'));
213
+ metadata.ghostBranches?.forEach(b => {
214
+ console.log(chalk.gray(` ${b.name}`));
215
+ });
216
+ return;
217
+ }
218
+
219
+ targetBranch = ghostData.baseCommit || 'HEAD';
220
+ isGhost = true;
181
221
  }
182
222
 
183
- execSync(`git checkout ${targetBranch}`, { stdio: 'pipe' });
184
- spinner.succeed(`Switched to: ${chalk.cyan(targetBranch)}`);
185
-
186
- if (options.ghost) {
187
- console.log(chalk.magenta(' (This is a ghost branch)'));
223
+ if (isGhost && ghostData) {
224
+ // Checkout in detached HEAD state
225
+ execSync(`git checkout ${targetBranch}`, { stdio: 'pipe' });
226
+
227
+ const age = Math.round((Date.now() - new Date(ghostData.createdAt).getTime()) / 60000);
228
+ const ageText = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;
229
+
230
+ spinner.succeed(`Switched to ghost workspace: ${chalk.cyan(ghostData.name)} šŸ‘»`);
231
+ console.log(chalk.magenta(` šŸŽ­ Ghost Branch (Detached HEAD)`));
232
+ console.log(chalk.gray(` Based on: ${targetBranch.substring(0, 8)}...`));
233
+ console.log(chalk.gray(` Created from: ${chalk.cyan(ghostData.createdFrom)}`));
234
+ console.log(chalk.gray(` Created: ${ageText}`));
235
+ console.log(chalk.yellow(` Note: Changes won't be tracked to any branch until you create a real branch or commit them`));
236
+ } else {
237
+ // Regular branch checkout
238
+ execSync(`git checkout ${targetBranch}`, { stdio: 'pipe' });
239
+ spinner.succeed(`Switched to: ${chalk.cyan(targetBranch)}`);
188
240
  }
189
241
 
190
242
  } catch (error) {
@@ -195,7 +247,7 @@ export function checkoutGhost(branch, options) {
195
247
  console.log(chalk.yellow('\nAvailable ghost branches:'));
196
248
  const metadata = loadMetadata();
197
249
  (metadata.ghostBranches || []).forEach(b => {
198
- console.log(chalk.gray(` ${b.name.replace(GHOST_PREFIX, '')}`));
250
+ console.log(chalk.gray(` ${b.name}`));
199
251
  });
200
252
  }
201
253
  }
@@ -4,172 +4,109 @@ import path from 'path';
4
4
  import chalk from 'chalk';
5
5
  import ora from 'ora';
6
6
 
7
- // Simple AI message generation (without external API)
7
+ /* ------------------ FALLBACK (RULE-BASED) ------------------ */
8
8
  function generateSimpleMessage(diff) {
9
9
  const lines = diff.split('\n');
10
10
  const stats = {
11
11
  additions: 0,
12
12
  deletions: 0,
13
13
  files: new Set(),
14
- types: new Set()
15
14
  };
16
15
 
17
16
  for (const line of lines) {
18
- if (line.startsWith('+') && !line.startsWith('+++')) {
19
- stats.additions++;
20
- } else if (line.startsWith('-') && !line.startsWith('---')) {
21
- stats.deletions++;
22
- } else if (line.startsWith('diff --git')) {
17
+ if (line.startsWith('+') && !line.startsWith('+++')) stats.additions++;
18
+ else if (line.startsWith('-') && !line.startsWith('---')) stats.deletions++;
19
+ else if (line.startsWith('diff --git')) {
23
20
  const match = line.match(/b\/(.+)$/);
24
- if (match) {
25
- stats.files.add(match[1]);
26
- const ext = path.extname(match[1]);
27
- if (ext) stats.types.add(ext);
28
- }
21
+ if (match) stats.files.add(match[1]);
29
22
  }
30
23
  }
31
24
 
32
- // Detect change type
25
+ const files = Array.from(stats.files);
33
26
  let type = 'chore';
34
- const fileList = Array.from(stats.files);
35
-
36
- if (fileList.some(f => f.includes('test') || f.includes('spec'))) {
37
- type = 'test';
38
- } else if (fileList.some(f => f.includes('README') || f.includes('docs'))) {
39
- type = 'docs';
40
- } else if (fileList.some(f => f.includes('fix') || diff.includes('fix'))) {
41
- type = 'fix';
42
- } else if (stats.additions > stats.deletions * 2) {
43
- type = 'feat';
44
- } else if (stats.deletions > stats.additions) {
45
- type = 'refactor';
46
- }
47
27
 
48
- // Generate scope from files
49
- let scope = '';
50
- if (fileList.length === 1) {
51
- scope = path.basename(fileList[0], path.extname(fileList[0]));
52
- } else if (fileList.length > 1) {
53
- const dirs = fileList.map(f => path.dirname(f)).filter(d => d !== '.');
54
- if (dirs.length > 0) {
55
- scope = dirs[0].split('/')[0];
56
- }
57
- }
28
+ if (files.some(f => f.includes('test'))) type = 'test';
29
+ else if (files.some(f => f.includes('README') || f.includes('docs'))) type = 'docs';
30
+ else if (stats.additions > stats.deletions * 2) type = 'feat';
31
+ else if (stats.deletions > stats.additions) type = 'refactor';
58
32
 
59
- // Generate description
60
- let description = '';
61
- if (stats.additions > 0 && stats.deletions === 0) {
62
- description = `add ${fileList.length} file${fileList.length > 1 ? 's' : ''}`;
63
- } else if (stats.deletions > 0 && stats.additions === 0) {
64
- description = `remove ${fileList.length} file${fileList.length > 1 ? 's' : ''}`;
65
- } else {
66
- description = `update ${fileList.length} file${fileList.length > 1 ? 's' : ''} (+${stats.additions}/-${stats.deletions})`;
67
- }
33
+ const scope =
34
+ files.length === 1
35
+ ? path.basename(files[0], path.extname(files[0]))
36
+ : '';
37
+
38
+ const desc =
39
+ stats.additions && !stats.deletions
40
+ ? `add ${files.length} file(s)`
41
+ : `update ${files.length} file(s) (+${stats.additions}/-${stats.deletions})`;
68
42
 
69
- // Format: type(scope): description
70
- return scope ? `${type}(${scope}): ${description}` : `${type}: ${description}`;
43
+ return scope ? `${type}(${scope}): ${desc}` : `${type}: ${desc}`;
71
44
  }
72
45
 
73
- // AI-powered message generation using OpenAI
46
+ /* ------------------ OPENAI GENERATION ------------------ */
74
47
  async function generateAIMessage(diff) {
75
48
  const apiKey = process.env.OPENAI_API_KEY;
76
-
77
- if (!apiKey) {
78
- // Fall back to simple generation
79
- return generateSimpleMessage(diff);
80
- }
49
+ if (!apiKey) return generateSimpleMessage(diff);
81
50
 
82
51
  try {
83
52
  const OpenAI = (await import('openai')).default;
84
53
  const openai = new OpenAI({ apiKey });
85
54
 
86
- const response = await openai.chat.completions.create({
55
+ const res = await openai.responses.create({
87
56
  model: 'gpt-4o-mini',
88
- messages: [
89
- {
90
- role: 'system',
91
- content: `You are a commit message generator. Generate a concise, meaningful commit message following Conventional Commits format.
92
-
93
- Format: type(scope): description
94
-
95
- Types: feat, fix, docs, style, refactor, test, chore
96
- - Keep description under 50 characters
97
- - Use imperative mood ("add" not "added")
98
- - No period at the end
99
- - Be specific about what changed
100
-
101
- Respond with ONLY the commit message, nothing else.`
102
- },
103
- {
104
- role: 'user',
105
- content: `Generate a commit message for this diff:\n\n${diff.slice(0, 4000)}`
106
- }
107
- ],
108
- max_tokens: 100,
109
- temperature: 0.3
57
+ input: `Generate a concise Conventional Commit message for this git diff. Respond ONLY with the commit message itself, no explanations.\n\n${diff.slice(0, 4000)}`,
58
+ temperature: 0.3,
110
59
  });
111
60
 
112
- return response.choices[0].message.content.trim();
61
+ return res.output_text.trim();
113
62
  } catch (error) {
114
- // Fall back to simple generation on error
63
+ console.log(chalk.red(`\nāš ļø AI Generation Failed: ${error.message}`));
64
+ if (error.status === 429) {
65
+ console.log(chalk.yellow('šŸ’” Your OpenAI quota is insufficient. Please check your billing at:'));
66
+ console.log(chalk.yellow(' https://platform.openai.com/account/billing'));
67
+ } else if (error.status === 401) {
68
+ console.log(chalk.yellow('šŸ’” Your API key is invalid. Please check your OPENAI_API_KEY environment variable.'));
69
+ }
70
+ console.log(chalk.gray('\nFalling back to rule-based generator...\n'));
115
71
  return generateSimpleMessage(diff);
116
72
  }
117
73
  }
118
74
 
119
- // Update symbol tracking after commit
75
+ /* ------------------ SYMBOL TRACKING ------------------ */
120
76
  function updateSymbolTracking(diff) {
121
- const symbolsPath = '.bit/symbols.json';
122
-
123
- if (!fs.existsSync(symbolsPath)) return;
77
+ const file = '.bit/symbols.json';
78
+ if (!fs.existsSync(file)) return;
124
79
 
125
80
  try {
126
- const symbols = JSON.parse(fs.readFileSync(symbolsPath, 'utf-8'));
127
-
128
- // Extract function names from diff (simple regex)
129
- const functionMatches = diff.match(/(?:function|const|let|var)\s+(\w+)/g) || [];
130
- const classMatches = diff.match(/class\s+(\w+)/g) || [];
131
-
132
- for (const match of functionMatches) {
133
- const name = match.split(/\s+/)[1];
134
- if (name) {
135
- symbols.functions[name] = symbols.functions[name] || { changes: 0, lastModified: null };
136
- symbols.functions[name].changes++;
137
- symbols.functions[name].lastModified = new Date().toISOString();
138
- }
139
- }
81
+ const db = JSON.parse(fs.readFileSync(file, 'utf-8'));
82
+ const matches = diff.match(/(?:function|class|const)\s+(\w+)/g) || [];
140
83
 
141
- for (const match of classMatches) {
142
- const name = match.split(/\s+/)[1];
143
- if (name) {
144
- symbols.classes[name] = symbols.classes[name] || { changes: 0, lastModified: null };
145
- symbols.classes[name].changes++;
146
- symbols.classes[name].lastModified = new Date().toISOString();
147
- }
148
- }
84
+ matches.forEach(m => {
85
+ const name = m.split(/\s+/)[1];
86
+ db[name] = (db[name] || 0) + 1;
87
+ });
149
88
 
150
- symbols.lastAnalyzed = new Date().toISOString();
151
- fs.writeFileSync(symbolsPath, JSON.stringify(symbols, null, 2));
89
+ fs.writeFileSync(file, JSON.stringify(db, null, 2));
152
90
  } catch (error) {
153
- // Silently fail - don't interrupt commit
91
+ // Silent fail - file corruption not critical
154
92
  }
155
93
  }
156
94
 
157
- export async function aiCommit(options) {
95
+ /* ------------------ MAIN COMMAND ------------------ */
96
+ export async function aiCommit(options = {}) {
158
97
  const spinner = ora('Analyzing changes...').start();
159
98
 
160
99
  try {
161
- // Check if there are staged changes
162
- const staged = execSync('git diff --cached --name-only', { encoding: 'utf-8' }).trim();
163
-
100
+ const staged = execSync('git diff --cached --name-only', {
101
+ encoding: 'utf-8',
102
+ }).trim();
103
+
164
104
  if (!staged) {
165
105
  spinner.fail('No staged changes');
166
- console.log(chalk.yellow('\nStage your changes first:'));
167
- console.log(chalk.gray(' git add <files>'));
168
- console.log(chalk.gray(' git add .'));
106
+ console.log(chalk.yellow('Run: git add .'));
169
107
  return;
170
108
  }
171
109
 
172
- // If manual message provided, use it
173
110
  if (options.message) {
174
111
  spinner.text = 'Committing...';
175
112
  execSync(`git commit -m "${options.message}"`, { stdio: 'inherit' });
@@ -177,35 +114,38 @@ export async function aiCommit(options) {
177
114
  return;
178
115
  }
179
116
 
180
- // Get the diff
181
117
  const diff = execSync('git diff --cached', { encoding: 'utf-8' });
182
118
 
183
- // Generate AI message
119
+ /* -------- AI STATUS (CORRECT PLACE) -------- */
120
+ spinner.stop();
121
+ if (process.env.OPENAI_API_KEY) {
122
+ console.log(chalk.green('🧠 AI ENABLED: OpenAI will generate commit message\n'));
123
+ } else {
124
+ console.log(chalk.blue('ℹ AI DISABLED: Using local rule-based generator'));
125
+ console.log(chalk.gray(' To enable AI commits:'));
126
+ console.log(chalk.gray(' setx OPENAI_API_KEY "your-api-key"'));
127
+ console.log(chalk.gray(' Restart terminal after setting the key.\n'));
128
+ }
129
+ spinner.start();
130
+ /* ------------------------------------------ */
131
+
184
132
  spinner.text = 'Generating commit message...';
185
133
  const message = await generateAIMessage(diff);
186
134
  spinner.succeed(`Generated: ${chalk.cyan(message)}`);
187
135
 
188
- // Show preview and confirm
189
- console.log('\n' + chalk.gray('─'.repeat(50)));
190
- console.log(chalk.yellow('Staged files:'));
191
- staged.split('\n').forEach(file => {
192
- console.log(chalk.gray(` ${file}`));
193
- });
194
- console.log(chalk.gray('─'.repeat(50)));
136
+ console.log('\n' + chalk.gray('Staged files:'));
137
+ staged.split('\n').forEach(f => console.log(chalk.gray(` ${f}`)));
195
138
 
196
- // Commit with generated message
197
139
  spinner.start('Committing...');
198
140
  execSync(`git commit -m "${message}"`, { stdio: 'pipe' });
199
-
200
- // Update symbol tracking
141
+
201
142
  updateSymbolTracking(diff);
202
-
203
- spinner.succeed(chalk.green('Committed successfully!'));
204
- console.log(chalk.gray(`\n ${message}\n`));
205
143
 
206
- } catch (error) {
144
+ spinner.succeed(chalk.green('Committed successfully!'));
145
+ console.log(chalk.gray(`\n${message}\n`));
146
+ } catch (err) {
207
147
  spinner.fail('Commit failed');
208
- console.error(chalk.red(error.message));
148
+ console.error(chalk.red(err.message));
209
149
  process.exit(1);
210
150
  }
211
151
  }
package/src/index.js CHANGED
@@ -1,99 +1,119 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { Command } from 'commander';
4
- import { execSync } from 'child_process';
5
- import chalk from 'chalk';
3
+ import { Command } from "commander";
4
+ import { execSync } from "child_process";
5
+ import chalk from "chalk";
6
+ import { readFileSync } from "fs";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
6
9
 
7
- import { smartInit } from './commands/init.js';
8
- import { aiCommit } from './commands/commit.js';
9
- import { ghostBranch, checkoutGhost } from './commands/branch.js';
10
- import { mergePreview } from './commands/merge.js';
11
- import { checkHotZones } from './commands/hotzone.js';
12
- import { analyzeSymbols } from './commands/analyze.js';
10
+ // ===== Resolve __dirname (ESM-safe) =====
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
13
 
14
+ // ===== Read version from package.json =====
15
+ const pkg = JSON.parse(
16
+ readFileSync(path.join(__dirname, "../package.json"), "utf-8")
17
+ );
18
+
19
+ // ===== Import Bit commands =====
20
+ import { smartInit } from "./commands/init.js";
21
+ import { aiCommit } from "./commands/commit.js";
22
+ import { ghostBranch, checkoutGhost } from "./commands/branch.js";
23
+ import { mergePreview } from "./commands/merge.js";
24
+ import { checkHotZones } from "./commands/hotzone.js";
25
+ import { analyzeSymbols } from "./commands/analyze.js";
26
+
27
+ // ===== Setup CLI =====
14
28
  const program = new Command();
15
29
 
16
30
  program
17
- .name('bit')
18
- .description(chalk.cyan('Bit — Git with a brain'))
19
- .version('1.0.0');
31
+ .name("bit")
32
+ .description(chalk.cyan("Bit — Git with a brain"))
33
+ .version(pkg.version);
20
34
 
21
- // ============ SMART INIT ============
35
+ // ================= INIT =================
22
36
  program
23
- .command('init')
24
- .description('Initialize repo with smart .gitignore and .bit directory')
37
+ .command("init")
38
+ .description("Initialize repo with smart .gitignore and .bit directory")
25
39
  .action(smartInit);
26
40
 
27
- // ============ AI COMMIT ============
41
+ // ================= COMMIT =================
28
42
  program
29
- .command('commit')
30
- .description('AI-powered commit message generation')
31
- .option('-m, --message <msg>', 'Manual commit message (skip AI)')
43
+ .command("commit")
44
+ .description("AI-powered commit message generation")
45
+ .option("-m, --message <msg>", "Manual commit message (skip AI)")
32
46
  .action(aiCommit);
33
47
 
34
- // ============ GHOST BRANCHES ============
48
+ // ================= BRANCH =================
35
49
  program
36
- .command('branch')
37
- .description('List all branches including ghost branches')
38
- .option('--ghost <name>', 'Create a ghost branch')
39
- .option('--list-ghost', 'List only ghost branches')
50
+ .command("branch")
51
+ .description("List all branches including ghost branches")
52
+ .option("--ghost <name>", "Create a ghost branch")
53
+ .option("--list-ghost", "List only ghost branches")
40
54
  .action(ghostBranch);
41
55
 
56
+ // ================= CHECKOUT =================
42
57
  program
43
- .command('checkout')
44
- .description('Checkout a branch')
45
- .argument('<branch>', 'Branch name to checkout')
46
- .option('--ghost', 'Checkout a ghost branch')
58
+ .command("checkout")
59
+ .description("Checkout a branch")
60
+ .argument("<branch>", "Branch name to checkout")
61
+ .option("--ghost", "Checkout a ghost branch")
47
62
  .action(checkoutGhost);
48
63
 
49
- // ============ MERGE PREVIEW ============
64
+ // ================= MERGE =================
50
65
  program
51
- .command('merge')
52
- .description('Merge branches with optional preview')
53
- .argument('<branch>', 'Branch to merge')
54
- .option('--preview', 'Preview merge conflicts without merging')
66
+ .command("merge")
67
+ .description("Merge branches with optional preview")
68
+ .argument("<branch>", "Branch to merge")
69
+ .option("--preview", "Preview merge conflicts without merging")
55
70
  .action(mergePreview);
56
71
 
57
- // ============ HOT ZONE DETECTION ============
72
+ // ================= HOTZONE =================
58
73
  program
59
- .command('hotzone')
60
- .description('Detect overlapping work with other developers')
74
+ .command("hotzone")
75
+ .description("Detect overlapping work with other developers")
61
76
  .action(checkHotZones);
62
77
 
63
- // ============ SYMBOL ANALYSIS ============
78
+ // ================= ANALYZE =================
64
79
  program
65
- .command('analyze')
66
- .description('Analyze code changes at function/symbol level')
80
+ .command("analyze")
81
+ .description("Analyze code changes at function/symbol level")
67
82
  .action(analyzeSymbols);
68
83
 
69
- // ============ STATUS ============
84
+ // ================= STATUS =================
70
85
  program
71
- .command('status')
72
- .description('Enhanced git status with Bit intelligence')
86
+ .command("status")
87
+ .description("Enhanced git status with Bit intelligence")
73
88
  .action(() => {
74
89
  try {
75
- console.log(chalk.cyan('\n--- Bit Status ---\n'));
76
- execSync('git status', { stdio: 'inherit' });
90
+ console.log(chalk.cyan("\n--- Bit Status ---\n"));
91
+ execSync("git status", { stdio: "inherit" });
77
92
  checkHotZones(true); // silent mode
78
93
  } catch {
79
- console.error(chalk.red('Not a git repository'));
94
+ console.error(chalk.red("Not a git repository"));
95
+ process.exit(1);
80
96
  }
81
97
  });
82
98
 
83
- // ============ GIT PASSTHROUGH ============
84
- program
85
- .arguments('[command] [args...]')
86
- .action((command, args) => {
87
- if (!command) {
88
- program.outputHelp();
89
- return;
90
- }
99
+ // ================= GIT PASSTHROUGH =================
100
+ // Forward ANY unknown command directly to git
101
+ program.allowUnknownOption(true);
91
102
 
92
- try {
93
- execSync(`git ${command} ${args.join(' ')}`, { stdio: 'inherit' });
94
- } catch {
95
- process.exit(1);
96
- }
97
- });
103
+ program.action(() => {
104
+ const args = process.argv.slice(2);
105
+
106
+ if (args.length === 0) {
107
+ program.outputHelp();
108
+ return;
109
+ }
110
+
111
+ try {
112
+ execSync(`git ${args.join(" ")}`, { stdio: "inherit" });
113
+ } catch {
114
+ process.exit(1);
115
+ }
116
+ });
98
117
 
118
+ // ================= RUN =================
99
119
  program.parse(process.argv);
package/src/utils/ai.js CHANGED
@@ -7,7 +7,7 @@ import OpenAI from 'openai';
7
7
  import { config } from './config.js';
8
8
  import { log } from './logger.js';
9
9
  import { APIKeyError, RateLimitError, AIError } from './errors.js';
10
-
10
+ import chalk from 'chalk';
11
11
  /**
12
12
  * Get OpenAI client instance
13
13
  */
@@ -40,18 +40,20 @@ export async function generateCommitMessage(diff, options = {}) {
40
40
  log.ai('generate_commit', { model: aiConfig.model, diffLength: diff.length });
41
41
 
42
42
  const systemPrompt = getCommitSystemPrompt(style, maxLength);
43
+ console.log('🧠 OPENAI REQUEST SENT');
43
44
 
44
45
  const response = await client.chat.completions.create({
45
- model: aiConfig.model,
46
- messages: [
47
- { role: 'system', content: systemPrompt },
48
- { role: 'user', content: truncateDiff(diff, 4000) },
49
- ],
50
- max_tokens: aiConfig.maxTokens,
51
- temperature: aiConfig.temperature,
52
- });
53
-
54
- const message = response.choices[0]?.message?.content?.trim();
46
+ model: aiConfig.model,
47
+ messages: [
48
+ { role: 'system', content: systemPrompt },
49
+ { role: 'user', content: truncateDiff(diff, 4000) },
50
+ ],
51
+ max_tokens: aiConfig.maxTokens,
52
+ temperature: aiConfig.temperature,
53
+ });
54
+ console.log('🧠 OPENAI RESPONSE RECEIVED');
55
+ const message = response.choices[0]?.message?.content?.trim() || '';
56
+
55
57
 
56
58
  log.ai('generate_commit_success', { message });
57
59
 
@@ -59,16 +61,20 @@ export async function generateCommitMessage(diff, options = {}) {
59
61
  } catch (error) {
60
62
  log.exception(error, { operation: 'generate_commit' });
61
63
 
62
- if (error.status === 429) {
63
- throw new RateLimitError();
64
- }
64
+ console.log(chalk.red(`\nāš ļø AI Generation Failed: ${error.message}`));
65
65
 
66
- if (error.status === 401) {
67
- throw new APIKeyError('OpenAI');
66
+ if (error.status === 429) {
67
+ log.ai('fallback', { reason: 'insufficient_quota' });
68
+ console.log(chalk.yellow('šŸ’” Your OpenAI quota is insufficient. Please check your billing at:'));
69
+ console.log(chalk.yellow(' https://platform.openai.com/account/billing'));
70
+ } else if (error.status === 401) {
71
+ log.ai('fallback', { reason: 'invalid_api_key' });
72
+ console.log(chalk.yellow('šŸ’” Your API key is invalid. Please check your OPENAI_API_KEY environment variable.'));
73
+ } else {
74
+ log.ai('fallback', { reason: error.message });
75
+ console.log(chalk.gray(' Falling back to rule-based generator...\n'));
68
76
  }
69
77
 
70
- // Fallback to rule-based on any error
71
- log.ai('fallback', { reason: error.message });
72
78
  return generateRuleBasedMessage(diff);
73
79
  }
74
80
  }
@@ -84,26 +90,27 @@ export async function analyzeCodeChanges(diff, options = {}) {
84
90
  }
85
91
 
86
92
  try {
87
- const response = await client.chat.completions.create({
88
- model: config.get('ai.model'),
89
- messages: [
90
- {
91
- role: 'system',
92
- content: `Analyze the following git diff and provide:
93
+ const response = await client.chat.completions.create({
94
+ model: config.get('ai.model'),
95
+ messages: [
96
+ {
97
+ role: 'system',
98
+ content: `Analyze the following git diff and provide:
93
99
  1. A brief summary of changes (1-2 sentences)
94
100
  2. List of modified functions/methods
95
101
  3. Potential risks or areas needing review
96
102
  4. Suggested reviewers based on code areas
97
103
 
98
104
  Format as JSON with keys: summary, modifiedFunctions, risks, suggestedAreas`,
99
- },
100
- { role: 'user', content: truncateDiff(diff, 4000) },
101
- ],
102
- max_tokens: 1000,
103
- temperature: 0.3,
104
- });
105
-
106
- const content = response.choices[0]?.message?.content;
105
+ },
106
+ { role: 'user', content: truncateDiff(diff, 4000) },
107
+ ],
108
+ max_tokens: 1000,
109
+ temperature: 0.3,
110
+ });
111
+
112
+ const content = response.choices[0]?.message?.content || '';
113
+
107
114
 
108
115
  try {
109
116
  return JSON.parse(content);
@@ -112,6 +119,11 @@ Format as JSON with keys: summary, modifiedFunctions, risks, suggestedAreas`,
112
119
  }
113
120
  } catch (error) {
114
121
  log.exception(error, { operation: 'analyze_code' });
122
+ if (error.status === 429) {
123
+ console.log(chalk.yellow('āš ļø Code analysis skipped: Insufficient OpenAI quota'));
124
+ } else if (error.status === 401) {
125
+ console.log(chalk.yellow('āš ļø Code analysis skipped: Invalid API key'));
126
+ }
115
127
  return null;
116
128
  }
117
129
  }