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.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/package.json +73 -0
- package/src/commands/analyze.js +230 -0
- package/src/commands/branch.js +202 -0
- package/src/commands/commit.js +211 -0
- package/src/commands/hotzone.js +235 -0
- package/src/commands/init.js +233 -0
- package/src/commands/merge.js +191 -0
- package/src/index.js +104 -0
- package/src/utils/ai.js +238 -0
- package/src/utils/config.js +178 -0
- package/src/utils/errors.js +170 -0
- package/src/utils/git.js +241 -0
- package/src/utils/logger.js +94 -0
- package/src/utils/validation.js +108 -0
|
@@ -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
|
+
}
|