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
package/src/utils/ai.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Service for generating commit messages and code analysis
|
|
3
|
+
* Supports multiple AI providers with fallback to rule-based generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import OpenAI from 'openai';
|
|
7
|
+
import { config } from './config.js';
|
|
8
|
+
import { log } from './logger.js';
|
|
9
|
+
import { APIKeyError, RateLimitError, AIError } from './errors.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get OpenAI client instance
|
|
13
|
+
*/
|
|
14
|
+
function getOpenAIClient() {
|
|
15
|
+
const apiKey = config.get('apiKeys.openai');
|
|
16
|
+
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return new OpenAI({ apiKey });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate commit message using AI
|
|
26
|
+
*/
|
|
27
|
+
export async function generateCommitMessage(diff, options = {}) {
|
|
28
|
+
const { maxLength = 72, style = 'conventional' } = options;
|
|
29
|
+
|
|
30
|
+
const aiConfig = config.get('ai');
|
|
31
|
+
const client = getOpenAIClient();
|
|
32
|
+
|
|
33
|
+
// Fallback to rule-based if no API key
|
|
34
|
+
if (!client) {
|
|
35
|
+
log.ai('fallback', { reason: 'no_api_key' });
|
|
36
|
+
return generateRuleBasedMessage(diff);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
log.ai('generate_commit', { model: aiConfig.model, diffLength: diff.length });
|
|
41
|
+
|
|
42
|
+
const systemPrompt = getCommitSystemPrompt(style, maxLength);
|
|
43
|
+
|
|
44
|
+
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();
|
|
55
|
+
|
|
56
|
+
log.ai('generate_commit_success', { message });
|
|
57
|
+
|
|
58
|
+
return message || generateRuleBasedMessage(diff);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
log.exception(error, { operation: 'generate_commit' });
|
|
61
|
+
|
|
62
|
+
if (error.status === 429) {
|
|
63
|
+
throw new RateLimitError();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (error.status === 401) {
|
|
67
|
+
throw new APIKeyError('OpenAI');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Fallback to rule-based on any error
|
|
71
|
+
log.ai('fallback', { reason: error.message });
|
|
72
|
+
return generateRuleBasedMessage(diff);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Analyze code changes for summary
|
|
78
|
+
*/
|
|
79
|
+
export async function analyzeCodeChanges(diff, options = {}) {
|
|
80
|
+
const client = getOpenAIClient();
|
|
81
|
+
|
|
82
|
+
if (!client) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
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
|
+
1. A brief summary of changes (1-2 sentences)
|
|
94
|
+
2. List of modified functions/methods
|
|
95
|
+
3. Potential risks or areas needing review
|
|
96
|
+
4. Suggested reviewers based on code areas
|
|
97
|
+
|
|
98
|
+
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;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(content);
|
|
110
|
+
} catch {
|
|
111
|
+
return { summary: content };
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
log.exception(error, { operation: 'analyze_code' });
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get system prompt for commit message generation
|
|
121
|
+
*/
|
|
122
|
+
function getCommitSystemPrompt(style, maxLength) {
|
|
123
|
+
const basePrompt = `You are a helpful assistant that generates concise git commit messages.
|
|
124
|
+
Analyze the diff and write a clear, descriptive commit message.
|
|
125
|
+
Maximum length: ${maxLength} characters for the first line.`;
|
|
126
|
+
|
|
127
|
+
const stylePrompts = {
|
|
128
|
+
conventional: `${basePrompt}
|
|
129
|
+
Use Conventional Commits format:
|
|
130
|
+
- feat: new feature
|
|
131
|
+
- fix: bug fix
|
|
132
|
+
- docs: documentation changes
|
|
133
|
+
- style: formatting, no code change
|
|
134
|
+
- refactor: code restructuring
|
|
135
|
+
- test: adding tests
|
|
136
|
+
- chore: maintenance tasks
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
- feat: add user authentication flow
|
|
140
|
+
- fix: resolve null pointer in checkout
|
|
141
|
+
- refactor: simplify payment processing logic`,
|
|
142
|
+
|
|
143
|
+
simple: `${basePrompt}
|
|
144
|
+
Write simple, clear messages starting with a verb.
|
|
145
|
+
Examples:
|
|
146
|
+
- Add user authentication
|
|
147
|
+
- Fix checkout bug
|
|
148
|
+
- Update README`,
|
|
149
|
+
|
|
150
|
+
detailed: `${basePrompt}
|
|
151
|
+
Provide a short summary line followed by bullet points of changes.
|
|
152
|
+
Format:
|
|
153
|
+
Summary line (max 50 chars)
|
|
154
|
+
|
|
155
|
+
- Change 1
|
|
156
|
+
- Change 2`,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return stylePrompts[style] || stylePrompts.conventional;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Rule-based commit message generation (fallback)
|
|
164
|
+
*/
|
|
165
|
+
export function generateRuleBasedMessage(diff) {
|
|
166
|
+
const lines = diff.split('\n');
|
|
167
|
+
const stats = {
|
|
168
|
+
additions: 0,
|
|
169
|
+
deletions: 0,
|
|
170
|
+
files: new Set(),
|
|
171
|
+
types: new Set(),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Parse diff
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
if (line.startsWith('+++ b/')) {
|
|
177
|
+
const file = line.replace('+++ b/', '');
|
|
178
|
+
stats.files.add(file);
|
|
179
|
+
|
|
180
|
+
// Detect file type
|
|
181
|
+
if (file.endsWith('.test.js') || file.endsWith('.spec.js')) {
|
|
182
|
+
stats.types.add('test');
|
|
183
|
+
} else if (file.endsWith('.md')) {
|
|
184
|
+
stats.types.add('docs');
|
|
185
|
+
} else if (file.includes('config')) {
|
|
186
|
+
stats.types.add('chore');
|
|
187
|
+
}
|
|
188
|
+
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
189
|
+
stats.additions++;
|
|
190
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
191
|
+
stats.deletions++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Determine commit type
|
|
196
|
+
let type = 'chore';
|
|
197
|
+
if (stats.types.has('test')) {
|
|
198
|
+
type = 'test';
|
|
199
|
+
} else if (stats.types.has('docs')) {
|
|
200
|
+
type = 'docs';
|
|
201
|
+
} else if (stats.additions > stats.deletions * 2) {
|
|
202
|
+
type = 'feat';
|
|
203
|
+
} else if (stats.deletions > stats.additions) {
|
|
204
|
+
type = 'refactor';
|
|
205
|
+
} else if (stats.files.size === 1) {
|
|
206
|
+
type = 'fix';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Generate message
|
|
210
|
+
const fileCount = stats.files.size;
|
|
211
|
+
const fileList = Array.from(stats.files).slice(0, 3);
|
|
212
|
+
|
|
213
|
+
if (fileCount === 1) {
|
|
214
|
+
const fileName = fileList[0].split('/').pop();
|
|
215
|
+
return `${type}: update ${fileName}`;
|
|
216
|
+
} else if (fileCount <= 3) {
|
|
217
|
+
return `${type}: update ${fileList.map((f) => f.split('/').pop()).join(', ')}`;
|
|
218
|
+
} else {
|
|
219
|
+
return `${type}: update ${fileCount} files`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Truncate diff to fit token limit
|
|
225
|
+
*/
|
|
226
|
+
function truncateDiff(diff, maxChars) {
|
|
227
|
+
if (diff.length <= maxChars) {
|
|
228
|
+
return diff;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return diff.slice(0, maxChars) + '\n\n... (diff truncated)';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default {
|
|
235
|
+
generateCommitMessage,
|
|
236
|
+
analyzeCodeChanges,
|
|
237
|
+
generateRuleBasedMessage,
|
|
238
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Bit CLI
|
|
3
|
+
* Handles global config, project config, and environment variables
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Conf from 'conf';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import dotenv from 'dotenv';
|
|
11
|
+
|
|
12
|
+
// Load environment variables
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
15
|
+
// Default configuration values
|
|
16
|
+
const defaults = {
|
|
17
|
+
ai: {
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
model: 'gpt-4o-mini',
|
|
20
|
+
maxTokens: 500,
|
|
21
|
+
temperature: 0.7,
|
|
22
|
+
},
|
|
23
|
+
git: {
|
|
24
|
+
ghostPrefix: 'ghost/',
|
|
25
|
+
autoStage: false,
|
|
26
|
+
signCommits: false,
|
|
27
|
+
},
|
|
28
|
+
ui: {
|
|
29
|
+
colors: true,
|
|
30
|
+
spinners: true,
|
|
31
|
+
verbose: false,
|
|
32
|
+
},
|
|
33
|
+
telemetry: {
|
|
34
|
+
enabled: false,
|
|
35
|
+
anonymousId: null,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Schema for configuration validation
|
|
40
|
+
const schema = {
|
|
41
|
+
ai: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
provider: { type: 'string', enum: ['openai', 'anthropic', 'local'] },
|
|
45
|
+
model: { type: 'string' },
|
|
46
|
+
maxTokens: { type: 'number', minimum: 100, maximum: 4000 },
|
|
47
|
+
temperature: { type: 'number', minimum: 0, maximum: 2 },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
git: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
ghostPrefix: { type: 'string' },
|
|
54
|
+
autoStage: { type: 'boolean' },
|
|
55
|
+
signCommits: { type: 'boolean' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
ui: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
colors: { type: 'boolean' },
|
|
62
|
+
spinners: { type: 'boolean' },
|
|
63
|
+
verbose: { type: 'boolean' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Global configuration (stored in ~/.bit/config.json)
|
|
69
|
+
const globalConfig = new Conf({
|
|
70
|
+
projectName: 'bit-cli',
|
|
71
|
+
defaults,
|
|
72
|
+
schema,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get project-specific configuration from .bit/config.json
|
|
77
|
+
*/
|
|
78
|
+
function getProjectConfig() {
|
|
79
|
+
const projectConfigPath = path.join(process.cwd(), '.bit', 'config.json');
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(projectConfigPath, 'utf-8');
|
|
84
|
+
return JSON.parse(content);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Merge configurations with priority: env > project > global > defaults
|
|
95
|
+
*/
|
|
96
|
+
function getMergedConfig() {
|
|
97
|
+
const projectConfig = getProjectConfig();
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
ai: {
|
|
101
|
+
...defaults.ai,
|
|
102
|
+
...globalConfig.get('ai'),
|
|
103
|
+
...projectConfig.ai,
|
|
104
|
+
// Environment overrides
|
|
105
|
+
provider: process.env.BIT_AI_PROVIDER || globalConfig.get('ai.provider'),
|
|
106
|
+
model: process.env.BIT_AI_MODEL || globalConfig.get('ai.model'),
|
|
107
|
+
},
|
|
108
|
+
git: {
|
|
109
|
+
...defaults.git,
|
|
110
|
+
...globalConfig.get('git'),
|
|
111
|
+
...projectConfig.git,
|
|
112
|
+
},
|
|
113
|
+
ui: {
|
|
114
|
+
...defaults.ui,
|
|
115
|
+
...globalConfig.get('ui'),
|
|
116
|
+
...projectConfig.ui,
|
|
117
|
+
verbose: process.env.BIT_VERBOSE === 'true' || globalConfig.get('ui.verbose'),
|
|
118
|
+
},
|
|
119
|
+
// API Keys from environment only (never stored in config)
|
|
120
|
+
apiKeys: {
|
|
121
|
+
openai: process.env.OPENAI_API_KEY,
|
|
122
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Configuration API
|
|
128
|
+
export const config = {
|
|
129
|
+
// Get merged configuration
|
|
130
|
+
get: (key) => {
|
|
131
|
+
const merged = getMergedConfig();
|
|
132
|
+
if (!key) return merged;
|
|
133
|
+
|
|
134
|
+
return key.split('.').reduce((obj, k) => obj?.[k], merged);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Set global configuration
|
|
138
|
+
set: (key, value) => {
|
|
139
|
+
globalConfig.set(key, value);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Set project configuration
|
|
143
|
+
setProject: (key, value) => {
|
|
144
|
+
const projectConfigPath = path.join(process.cwd(), '.bit', 'config.json');
|
|
145
|
+
const projectConfig = getProjectConfig();
|
|
146
|
+
|
|
147
|
+
// Set nested key
|
|
148
|
+
const keys = key.split('.');
|
|
149
|
+
let obj = projectConfig;
|
|
150
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
151
|
+
obj[keys[i]] = obj[keys[i]] || {};
|
|
152
|
+
obj = obj[keys[i]];
|
|
153
|
+
}
|
|
154
|
+
obj[keys[keys.length - 1]] = value;
|
|
155
|
+
|
|
156
|
+
fs.writeFileSync(projectConfigPath, JSON.stringify(projectConfig, null, 2));
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Reset to defaults
|
|
160
|
+
reset: () => {
|
|
161
|
+
globalConfig.clear();
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Check if API key is configured
|
|
165
|
+
hasApiKey: (provider = 'openai') => {
|
|
166
|
+
const keys = getMergedConfig().apiKeys;
|
|
167
|
+
return !!keys[provider];
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// Get config file paths
|
|
171
|
+
paths: {
|
|
172
|
+
global: globalConfig.path,
|
|
173
|
+
project: path.join(process.cwd(), '.bit', 'config.json'),
|
|
174
|
+
logs: path.join(os.homedir(), '.bit', 'logs'),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default config;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error classes and error handling utilities
|
|
3
|
+
* Provides consistent error handling across the CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { log } from './logger.js';
|
|
8
|
+
|
|
9
|
+
// Base error class for Bit CLI
|
|
10
|
+
export class BitError extends Error {
|
|
11
|
+
constructor(message, code = 'BIT_ERROR', details = {}) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'BitError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.details = details;
|
|
16
|
+
this.timestamp = new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Git-related errors
|
|
21
|
+
export class GitError extends BitError {
|
|
22
|
+
constructor(message, details = {}) {
|
|
23
|
+
super(message, 'GIT_ERROR', details);
|
|
24
|
+
this.name = 'GitError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Not in a git repository
|
|
29
|
+
export class NotARepositoryError extends GitError {
|
|
30
|
+
constructor() {
|
|
31
|
+
super('Not a git repository. Run "bit init" to initialize.', {
|
|
32
|
+
suggestion: 'Run "bit init" to create a new repository',
|
|
33
|
+
});
|
|
34
|
+
this.name = 'NotARepositoryError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Merge conflict error
|
|
39
|
+
export class MergeConflictError extends GitError {
|
|
40
|
+
constructor(files = []) {
|
|
41
|
+
super('Merge conflicts detected', { conflictingFiles: files });
|
|
42
|
+
this.name = 'MergeConflictError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// AI-related errors
|
|
47
|
+
export class AIError extends BitError {
|
|
48
|
+
constructor(message, details = {}) {
|
|
49
|
+
super(message, 'AI_ERROR', details);
|
|
50
|
+
this.name = 'AIError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// API key not configured
|
|
55
|
+
export class APIKeyError extends AIError {
|
|
56
|
+
constructor(provider = 'OpenAI') {
|
|
57
|
+
super(`${provider} API key not configured`, {
|
|
58
|
+
suggestion: `Set ${provider.toUpperCase()}_API_KEY environment variable`,
|
|
59
|
+
});
|
|
60
|
+
this.name = 'APIKeyError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Rate limit exceeded
|
|
65
|
+
export class RateLimitError extends AIError {
|
|
66
|
+
constructor(retryAfter = null) {
|
|
67
|
+
super('API rate limit exceeded', {
|
|
68
|
+
retryAfter,
|
|
69
|
+
suggestion: 'Wait a moment and try again',
|
|
70
|
+
});
|
|
71
|
+
this.name = 'RateLimitError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Configuration errors
|
|
76
|
+
export class ConfigError extends BitError {
|
|
77
|
+
constructor(message, details = {}) {
|
|
78
|
+
super(message, 'CONFIG_ERROR', details);
|
|
79
|
+
this.name = 'ConfigError';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Validation errors
|
|
84
|
+
export class ValidationError extends BitError {
|
|
85
|
+
constructor(message, details = {}) {
|
|
86
|
+
super(message, 'VALIDATION_ERROR', details);
|
|
87
|
+
this.name = 'ValidationError';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Global error handler for CLI
|
|
93
|
+
*/
|
|
94
|
+
export function handleError(error, options = {}) {
|
|
95
|
+
const { exit = true, showStack = false } = options;
|
|
96
|
+
|
|
97
|
+
// Log the error
|
|
98
|
+
log.exception(error, { command: process.argv.slice(2).join(' ') });
|
|
99
|
+
|
|
100
|
+
// Format error output
|
|
101
|
+
console.error('');
|
|
102
|
+
|
|
103
|
+
if (error instanceof BitError) {
|
|
104
|
+
console.error(chalk.red(`Error [${error.code}]: ${error.message}`));
|
|
105
|
+
|
|
106
|
+
if (error.details?.suggestion) {
|
|
107
|
+
console.error(chalk.yellow(`Suggestion: ${error.details.suggestion}`));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (error.details?.conflictingFiles?.length) {
|
|
111
|
+
console.error(chalk.yellow('\nConflicting files:'));
|
|
112
|
+
error.details.conflictingFiles.forEach((file) => {
|
|
113
|
+
console.error(chalk.yellow(` - ${file}`));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Show stack trace in debug mode
|
|
121
|
+
if (showStack || process.env.BIT_DEBUG === 'true') {
|
|
122
|
+
console.error(chalk.gray('\nStack trace:'));
|
|
123
|
+
console.error(chalk.gray(error.stack));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.error('');
|
|
127
|
+
|
|
128
|
+
if (exit) {
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Wrap async functions with error handling
|
|
135
|
+
*/
|
|
136
|
+
export function withErrorHandling(fn) {
|
|
137
|
+
return async (...args) => {
|
|
138
|
+
try {
|
|
139
|
+
return await fn(...args);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
handleError(error);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Try to execute a function, return default value on error
|
|
148
|
+
*/
|
|
149
|
+
export function tryOrDefault(fn, defaultValue = null) {
|
|
150
|
+
try {
|
|
151
|
+
return fn();
|
|
152
|
+
} catch {
|
|
153
|
+
return defaultValue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default {
|
|
158
|
+
BitError,
|
|
159
|
+
GitError,
|
|
160
|
+
NotARepositoryError,
|
|
161
|
+
MergeConflictError,
|
|
162
|
+
AIError,
|
|
163
|
+
APIKeyError,
|
|
164
|
+
RateLimitError,
|
|
165
|
+
ConfigError,
|
|
166
|
+
ValidationError,
|
|
167
|
+
handleError,
|
|
168
|
+
withErrorHandling,
|
|
169
|
+
tryOrDefault,
|
|
170
|
+
};
|