bit-cli-ai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+
6
+ const GHOST_PREFIX = 'ghost/';
7
+ const METADATA_PATH = '.bit/metadata.json';
8
+
9
+ // Load Bit metadata
10
+ function loadMetadata() {
11
+ if (!fs.existsSync(METADATA_PATH)) {
12
+ return { ghostBranches: [] };
13
+ }
14
+ return JSON.parse(fs.readFileSync(METADATA_PATH, 'utf-8'));
15
+ }
16
+
17
+ // Save Bit metadata
18
+ function saveMetadata(metadata) {
19
+ if (!fs.existsSync('.bit')) {
20
+ fs.mkdirSync('.bit');
21
+ }
22
+ fs.writeFileSync(METADATA_PATH, JSON.stringify(metadata, null, 2));
23
+ }
24
+
25
+ // Get current branch
26
+ function getCurrentBranch() {
27
+ try {
28
+ return execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ // Create a ghost branch
35
+ function createGhostBranch(name) {
36
+ const spinner = ora(`Creating ghost branch: ${name}`).start();
37
+
38
+ try {
39
+ const fullName = `${GHOST_PREFIX}${name}`;
40
+
41
+ // Create the actual git branch
42
+ execSync(`git branch ${fullName}`, { stdio: 'pipe' });
43
+
44
+ // Record in metadata
45
+ const metadata = loadMetadata();
46
+ metadata.ghostBranches = metadata.ghostBranches || [];
47
+
48
+ if (!metadata.ghostBranches.find(b => b.name === fullName)) {
49
+ metadata.ghostBranches.push({
50
+ name: fullName,
51
+ createdFrom: currentBranch,
52
+ createdAt: new Date().toISOString(),
53
+ isGhost: true
54
+ });
55
+ saveMetadata(metadata);
56
+ }
57
+
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`));
61
+
62
+ } catch (error) {
63
+ spinner.fail('Failed to create ghost branch');
64
+ console.error(chalk.red(error.message));
65
+ }
66
+ }
67
+
68
+ // List all branches (including ghost)
69
+ function listAllBranches(showOnlyGhost = false) {
70
+ 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
+ const currentBranch = getCurrentBranch();
78
+ const metadata = loadMetadata();
79
+ const ghostBranches = metadata.ghostBranches || [];
80
+
81
+ console.log(chalk.cyan('\n--- Branches ---\n'));
82
+
83
+ // Regular branches
84
+ if (!showOnlyGhost) {
85
+ console.log(chalk.yellow('Regular branches:'));
86
+ branches
87
+ .filter(b => !b.includes(GHOST_PREFIX) && !b.startsWith('remotes/'))
88
+ .forEach(branch => {
89
+ const isCurrent = branch.startsWith('*');
90
+ const name = branch.replace('* ', '');
91
+ if (isCurrent) {
92
+ console.log(chalk.green(` * ${name}`));
93
+ } else {
94
+ console.log(chalk.gray(` ${name}`));
95
+ }
96
+ });
97
+ console.log('');
98
+ }
99
+
100
+ // Ghost branches
101
+ console.log(chalk.magenta('Ghost branches:'));
102
+ const gitGhostBranches = branches.filter(b => b.includes(GHOST_PREFIX));
103
+
104
+ if (gitGhostBranches.length === 0) {
105
+ console.log(chalk.gray(' No ghost branches'));
106
+ } 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
+ }
117
+
118
+ if (ghostMeta) {
119
+ console.log(chalk.gray(` Created from: ${ghostMeta.createdFrom}`));
120
+ }
121
+ });
122
+ }
123
+
124
+ console.log('');
125
+
126
+ } catch (error) {
127
+ console.error(chalk.red('Failed to list branches'));
128
+ console.error(chalk.red(error.message));
129
+ }
130
+ }
131
+
132
+ // Delete a ghost branch
133
+ function deleteGhostBranch(name) {
134
+ const spinner = ora(`Deleting ghost branch: ${name}`).start();
135
+
136
+ 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' });
141
+
142
+ // Remove from metadata
143
+ const metadata = loadMetadata();
144
+ metadata.ghostBranches = (metadata.ghostBranches || []).filter(b => b.name !== fullName);
145
+ saveMetadata(metadata);
146
+
147
+ spinner.succeed(`Ghost branch deleted: ${chalk.magenta(fullName)}`);
148
+
149
+ } catch (error) {
150
+ spinner.fail('Failed to delete ghost branch');
151
+ console.error(chalk.red(error.message));
152
+ }
153
+ }
154
+
155
+ export function ghostBranch(options) {
156
+ if (options.ghost) {
157
+ // Create a ghost branch
158
+ createGhostBranch(options.ghost);
159
+ } else if (options.listGhost) {
160
+ // List only ghost branches
161
+ listAllBranches(true);
162
+ } else {
163
+ // List all branches including ghost
164
+ listAllBranches(false);
165
+ }
166
+ }
167
+
168
+ export function listBranches() {
169
+ listAllBranches(false);
170
+ }
171
+
172
+ export function checkoutGhost(branch, options) {
173
+ const spinner = ora(`Checking out: ${branch}`).start();
174
+
175
+ try {
176
+ let targetBranch = branch;
177
+
178
+ if (options.ghost) {
179
+ // It's a ghost branch
180
+ targetBranch = branch.startsWith(GHOST_PREFIX) ? branch : `${GHOST_PREFIX}${branch}`;
181
+ }
182
+
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)'));
188
+ }
189
+
190
+ } catch (error) {
191
+ spinner.fail('Checkout failed');
192
+ console.error(chalk.red(error.message));
193
+
194
+ if (options.ghost) {
195
+ console.log(chalk.yellow('\nAvailable ghost branches:'));
196
+ const metadata = loadMetadata();
197
+ (metadata.ghostBranches || []).forEach(b => {
198
+ console.log(chalk.gray(` ${b.name.replace(GHOST_PREFIX, '')}`));
199
+ });
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,211 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+
7
+ // Simple AI message generation (without external API)
8
+ function generateSimpleMessage(diff) {
9
+ const lines = diff.split('\n');
10
+ const stats = {
11
+ additions: 0,
12
+ deletions: 0,
13
+ files: new Set(),
14
+ types: new Set()
15
+ };
16
+
17
+ 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')) {
23
+ 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
+ }
29
+ }
30
+ }
31
+
32
+ // Detect change type
33
+ 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
+
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
+ }
58
+
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
+ }
68
+
69
+ // Format: type(scope): description
70
+ return scope ? `${type}(${scope}): ${description}` : `${type}: ${description}`;
71
+ }
72
+
73
+ // AI-powered message generation using OpenAI
74
+ async function generateAIMessage(diff) {
75
+ const apiKey = process.env.OPENAI_API_KEY;
76
+
77
+ if (!apiKey) {
78
+ // Fall back to simple generation
79
+ return generateSimpleMessage(diff);
80
+ }
81
+
82
+ try {
83
+ const OpenAI = (await import('openai')).default;
84
+ const openai = new OpenAI({ apiKey });
85
+
86
+ const response = await openai.chat.completions.create({
87
+ 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
110
+ });
111
+
112
+ return response.choices[0].message.content.trim();
113
+ } catch (error) {
114
+ // Fall back to simple generation on error
115
+ return generateSimpleMessage(diff);
116
+ }
117
+ }
118
+
119
+ // Update symbol tracking after commit
120
+ function updateSymbolTracking(diff) {
121
+ const symbolsPath = '.bit/symbols.json';
122
+
123
+ if (!fs.existsSync(symbolsPath)) return;
124
+
125
+ 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
+ }
140
+
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
+ }
149
+
150
+ symbols.lastAnalyzed = new Date().toISOString();
151
+ fs.writeFileSync(symbolsPath, JSON.stringify(symbols, null, 2));
152
+ } catch (error) {
153
+ // Silently fail - don't interrupt commit
154
+ }
155
+ }
156
+
157
+ export async function aiCommit(options) {
158
+ const spinner = ora('Analyzing changes...').start();
159
+
160
+ try {
161
+ // Check if there are staged changes
162
+ const staged = execSync('git diff --cached --name-only', { encoding: 'utf-8' }).trim();
163
+
164
+ if (!staged) {
165
+ 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 .'));
169
+ return;
170
+ }
171
+
172
+ // If manual message provided, use it
173
+ if (options.message) {
174
+ spinner.text = 'Committing...';
175
+ execSync(`git commit -m "${options.message}"`, { stdio: 'inherit' });
176
+ spinner.succeed('Committed with manual message');
177
+ return;
178
+ }
179
+
180
+ // Get the diff
181
+ const diff = execSync('git diff --cached', { encoding: 'utf-8' });
182
+
183
+ // Generate AI message
184
+ spinner.text = 'Generating commit message...';
185
+ const message = await generateAIMessage(diff);
186
+ spinner.succeed(`Generated: ${chalk.cyan(message)}`);
187
+
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)));
195
+
196
+ // Commit with generated message
197
+ spinner.start('Committing...');
198
+ execSync(`git commit -m "${message}"`, { stdio: 'pipe' });
199
+
200
+ // Update symbol tracking
201
+ updateSymbolTracking(diff);
202
+
203
+ spinner.succeed(chalk.green('Committed successfully!'));
204
+ console.log(chalk.gray(`\n ${message}\n`));
205
+
206
+ } catch (error) {
207
+ spinner.fail('Commit failed');
208
+ console.error(chalk.red(error.message));
209
+ process.exit(1);
210
+ }
211
+ }
@@ -0,0 +1,235 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+
6
+ const HOTZONE_PATH = '.bit/hotzones.json';
7
+ const SYMBOLS_PATH = '.bit/symbols.json';
8
+
9
+ // Simulated team activity (in real world, this would sync across a team)
10
+ function getSimulatedTeamActivity() {
11
+ // Simulate other developers working on files
12
+ // In production, this would pull from a shared server or git notes
13
+ return [
14
+ { developer: 'alice', file: 'src/auth.js', function: 'login', timestamp: Date.now() - 300000 },
15
+ { developer: 'bob', file: 'src/api.js', function: 'fetchData', timestamp: Date.now() - 600000 },
16
+ { developer: 'charlie', file: 'src/utils.js', function: 'formatDate', timestamp: Date.now() - 1800000 },
17
+ ];
18
+ }
19
+
20
+ // Get currently modified files
21
+ function getCurrentlyModifiedFiles() {
22
+ try {
23
+ // Staged files
24
+ const staged = execSync('git diff --cached --name-only', { encoding: 'utf-8' })
25
+ .split('\n')
26
+ .filter(f => f.trim());
27
+
28
+ // Unstaged modified files
29
+ const modified = execSync('git diff --name-only', { encoding: 'utf-8' })
30
+ .split('\n')
31
+ .filter(f => f.trim());
32
+
33
+ return [...new Set([...staged, ...modified])];
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ // Extract functions from a file (simple regex-based)
40
+ function extractFunctions(filePath) {
41
+ if (!fs.existsSync(filePath)) return [];
42
+
43
+ try {
44
+ const content = fs.readFileSync(filePath, 'utf-8');
45
+ const functions = [];
46
+
47
+ // Match various function patterns
48
+ const patterns = [
49
+ /function\s+(\w+)/g, // function name()
50
+ /const\s+(\w+)\s*=\s*(?:async\s*)?\(/g, // const name = () or const name = async (
51
+ /(\w+)\s*:\s*(?:async\s*)?\(/g, // name: () or name: async (
52
+ /async\s+(\w+)\s*\(/g, // async name(
53
+ ];
54
+
55
+ for (const pattern of patterns) {
56
+ let match;
57
+ while ((match = pattern.exec(content)) !== null) {
58
+ if (!functions.includes(match[1])) {
59
+ functions.push(match[1]);
60
+ }
61
+ }
62
+ }
63
+
64
+ return functions;
65
+ } catch {
66
+ return [];
67
+ }
68
+ }
69
+
70
+ // Load hotzone data
71
+ function loadHotzones() {
72
+ if (!fs.existsSync(HOTZONE_PATH)) {
73
+ return { zones: [], lastChecked: null };
74
+ }
75
+ return JSON.parse(fs.readFileSync(HOTZONE_PATH, 'utf-8'));
76
+ }
77
+
78
+ // Save hotzone data
79
+ function saveHotzones(data) {
80
+ if (!fs.existsSync('.bit')) {
81
+ fs.mkdirSync('.bit');
82
+ }
83
+ fs.writeFileSync(HOTZONE_PATH, JSON.stringify(data, null, 2));
84
+ }
85
+
86
+ // Register current work for team awareness
87
+ function registerCurrentWork(files) {
88
+ const hotzones = loadHotzones();
89
+ const username = process.env.USER || process.env.USERNAME || 'you';
90
+
91
+ hotzones.zones = hotzones.zones.filter(z => z.developer !== username);
92
+
93
+ for (const file of files) {
94
+ const functions = extractFunctions(file);
95
+ hotzones.zones.push({
96
+ developer: username,
97
+ file,
98
+ functions,
99
+ timestamp: Date.now()
100
+ });
101
+ }
102
+
103
+ hotzones.lastChecked = new Date().toISOString();
104
+ saveHotzones(hotzones);
105
+ }
106
+
107
+ // Check for conflicts with team
108
+ function detectHotZones(modifiedFiles, silent = false) {
109
+ const teamActivity = getSimulatedTeamActivity();
110
+ const hotzones = loadHotzones();
111
+ const conflicts = [];
112
+
113
+ const username = process.env.USER || process.env.USERNAME || 'you';
114
+
115
+ for (const file of modifiedFiles) {
116
+ const myFunctions = extractFunctions(file);
117
+
118
+ // Check against simulated team activity
119
+ for (const activity of teamActivity) {
120
+ if (activity.file === file || file.includes(activity.file.split('/').pop())) {
121
+ // File-level conflict
122
+ conflicts.push({
123
+ type: 'file',
124
+ file,
125
+ developer: activity.developer,
126
+ function: activity.function,
127
+ age: Math.round((Date.now() - activity.timestamp) / 60000)
128
+ });
129
+ }
130
+
131
+ // Function-level conflict
132
+ if (myFunctions.includes(activity.function)) {
133
+ conflicts.push({
134
+ type: 'function',
135
+ file,
136
+ developer: activity.developer,
137
+ function: activity.function,
138
+ age: Math.round((Date.now() - activity.timestamp) / 60000)
139
+ });
140
+ }
141
+ }
142
+
143
+ // Check against recorded hotzones
144
+ for (const zone of hotzones.zones) {
145
+ if (zone.developer === username) continue;
146
+
147
+ if (zone.file === file) {
148
+ const overlap = myFunctions.filter(f => zone.functions.includes(f));
149
+ if (overlap.length > 0) {
150
+ conflicts.push({
151
+ type: 'function',
152
+ file,
153
+ developer: zone.developer,
154
+ function: overlap[0],
155
+ age: Math.round((Date.now() - zone.timestamp) / 60000)
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ return conflicts;
163
+ }
164
+
165
+ export function checkHotZones(silent = false) {
166
+ if (silent === true) {
167
+ // Silent mode - just check and show warnings
168
+ const modifiedFiles = getCurrentlyModifiedFiles();
169
+ if (modifiedFiles.length === 0) return;
170
+
171
+ const conflicts = detectHotZones(modifiedFiles, true);
172
+ if (conflicts.length > 0) {
173
+ console.log(chalk.yellow('\nHot Zone Warning:'));
174
+ conflicts.slice(0, 2).forEach(c => {
175
+ console.log(chalk.yellow(` ${c.developer} is working on ${c.function || c.file}`));
176
+ });
177
+ }
178
+ return;
179
+ }
180
+
181
+ const spinner = ora('Scanning for hot zones...').start();
182
+
183
+ try {
184
+ const modifiedFiles = getCurrentlyModifiedFiles();
185
+
186
+ if (modifiedFiles.length === 0) {
187
+ spinner.info('No modified files to check');
188
+ return;
189
+ }
190
+
191
+ // Register current work
192
+ registerCurrentWork(modifiedFiles);
193
+
194
+ // Detect conflicts
195
+ const conflicts = detectHotZones(modifiedFiles);
196
+
197
+ spinner.stop();
198
+
199
+ console.log(chalk.cyan('\n--- Hot Zone Analysis ---\n'));
200
+ console.log(chalk.gray(`Files you're working on: ${modifiedFiles.length}`));
201
+ modifiedFiles.forEach(f => console.log(chalk.gray(` ${f}`)));
202
+
203
+ console.log(chalk.gray('\n' + '─'.repeat(50)));
204
+
205
+ if (conflicts.length === 0) {
206
+ console.log(chalk.green('\nNo overlapping work detected!'));
207
+ console.log(chalk.gray('You have clear ownership of these changes.'));
208
+ } else {
209
+ console.log(chalk.yellow(`\nWarning: ${conflicts.length} potential conflict(s) detected!\n`));
210
+
211
+ for (const conflict of conflicts) {
212
+ if (conflict.type === 'function') {
213
+ console.log(chalk.red(` CAUTION: ${conflict.developer} is working on ${conflict.function}()`));
214
+ console.log(chalk.gray(` in ${conflict.file}`));
215
+ console.log(chalk.gray(` Last activity: ${conflict.age} minutes ago`));
216
+ } else {
217
+ console.log(chalk.yellow(` NOTICE: ${conflict.developer} recently modified ${conflict.file}`));
218
+ console.log(chalk.gray(` Last activity: ${conflict.age} minutes ago`));
219
+ }
220
+ console.log('');
221
+ }
222
+
223
+ console.log(chalk.yellow('Recommendations:'));
224
+ console.log(chalk.gray(' - Coordinate with team members before pushing'));
225
+ console.log(chalk.gray(' - Use: bit merge --preview to check for conflicts'));
226
+ console.log(chalk.gray(' - Consider breaking work into smaller commits'));
227
+ }
228
+
229
+ console.log('');
230
+
231
+ } catch (error) {
232
+ spinner.fail('Hot zone check failed');
233
+ console.error(chalk.red(error.message));
234
+ }
235
+ }