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,241 @@
1
+ /**
2
+ * Git utilities and helpers
3
+ * Wrapper around git commands with error handling and logging
4
+ */
5
+
6
+ import { execSync, exec } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { log } from './logger.js';
9
+ import { NotARepositoryError, GitError, MergeConflictError } from './errors.js';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ /**
14
+ * Execute a git command synchronously
15
+ */
16
+ export function gitSync(command, options = {}) {
17
+ const { silent = false, throwOnError = true } = options;
18
+
19
+ try {
20
+ log.git('sync', { command });
21
+ const result = execSync(`git ${command}`, {
22
+ encoding: 'utf-8',
23
+ stdio: silent ? 'pipe' : undefined,
24
+ });
25
+ return result?.trim() || '';
26
+ } catch (error) {
27
+ log.exception(error, { command: `git ${command}` });
28
+
29
+ if (error.message?.includes('not a git repository')) {
30
+ throw new NotARepositoryError();
31
+ }
32
+
33
+ if (throwOnError) {
34
+ throw new GitError(`Git command failed: ${command}`, {
35
+ stderr: error.stderr?.toString(),
36
+ });
37
+ }
38
+
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Execute a git command asynchronously
45
+ */
46
+ export async function gitAsync(command, options = {}) {
47
+ const { silent = false, throwOnError = true } = options;
48
+
49
+ try {
50
+ log.git('async', { command });
51
+ const { stdout, stderr } = await execAsync(`git ${command}`);
52
+ return stdout?.trim() || '';
53
+ } catch (error) {
54
+ log.exception(error, { command: `git ${command}` });
55
+
56
+ if (error.message?.includes('not a git repository')) {
57
+ throw new NotARepositoryError();
58
+ }
59
+
60
+ if (throwOnError) {
61
+ throw new GitError(`Git command failed: ${command}`, {
62
+ stderr: error.stderr?.toString(),
63
+ });
64
+ }
65
+
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Check if current directory is a git repository
72
+ */
73
+ export function isGitRepository() {
74
+ try {
75
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get current branch name
84
+ */
85
+ export function getCurrentBranch() {
86
+ return gitSync('rev-parse --abbrev-ref HEAD', { silent: true });
87
+ }
88
+
89
+ /**
90
+ * Get list of all branches
91
+ */
92
+ export function getBranches(options = {}) {
93
+ const { all = false, remote = false } = options;
94
+
95
+ let command = 'branch';
96
+ if (all) command += ' -a';
97
+ else if (remote) command += ' -r';
98
+
99
+ const output = gitSync(command, { silent: true });
100
+ return output
101
+ .split('\n')
102
+ .map((b) => b.trim().replace(/^\* /, ''))
103
+ .filter(Boolean);
104
+ }
105
+
106
+ /**
107
+ * Get staged files
108
+ */
109
+ export function getStagedFiles() {
110
+ const output = gitSync('diff --cached --name-only', { silent: true });
111
+ return output ? output.split('\n').filter(Boolean) : [];
112
+ }
113
+
114
+ /**
115
+ * Get modified files (not staged)
116
+ */
117
+ export function getModifiedFiles() {
118
+ const output = gitSync('diff --name-only', { silent: true });
119
+ return output ? output.split('\n').filter(Boolean) : [];
120
+ }
121
+
122
+ /**
123
+ * Get untracked files
124
+ */
125
+ export function getUntrackedFiles() {
126
+ const output = gitSync('ls-files --others --exclude-standard', { silent: true });
127
+ return output ? output.split('\n').filter(Boolean) : [];
128
+ }
129
+
130
+ /**
131
+ * Get diff of staged changes
132
+ */
133
+ export function getStagedDiff() {
134
+ return gitSync('diff --cached', { silent: true });
135
+ }
136
+
137
+ /**
138
+ * Get commit history
139
+ */
140
+ export function getCommitHistory(options = {}) {
141
+ const { limit = 10, format = '%H|%s|%an|%ar' } = options;
142
+
143
+ const output = gitSync(`log -${limit} --format="${format}"`, { silent: true });
144
+
145
+ return output.split('\n').filter(Boolean).map((line) => {
146
+ const [hash, message, author, date] = line.split('|');
147
+ return { hash, message, author, date };
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Get repository root directory
153
+ */
154
+ export function getRepoRoot() {
155
+ return gitSync('rev-parse --show-toplevel', { silent: true });
156
+ }
157
+
158
+ /**
159
+ * Check for merge conflicts
160
+ */
161
+ export function hasConflicts() {
162
+ try {
163
+ const output = gitSync('diff --name-only --diff-filter=U', { silent: true });
164
+ return output.length > 0;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Get conflicting files
172
+ */
173
+ export function getConflictingFiles() {
174
+ const output = gitSync('diff --name-only --diff-filter=U', { silent: true });
175
+ return output ? output.split('\n').filter(Boolean) : [];
176
+ }
177
+
178
+ /**
179
+ * Preview merge using merge-tree
180
+ */
181
+ export function previewMerge(branch) {
182
+ try {
183
+ const currentBranch = getCurrentBranch();
184
+ const baseCommit = gitSync(`merge-base ${currentBranch} ${branch}`, { silent: true });
185
+ const output = gitSync(`merge-tree ${baseCommit} ${currentBranch} ${branch}`, { silent: true });
186
+
187
+ // Check for conflict markers
188
+ const hasConflicts = output.includes('<<<<<<<') || output.includes('>>>>>>>');
189
+
190
+ return {
191
+ hasConflicts,
192
+ output,
193
+ baseCommit,
194
+ };
195
+ } catch (error) {
196
+ throw new GitError(`Failed to preview merge with ${branch}`, {
197
+ originalError: error.message,
198
+ });
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Get file content at specific commit
204
+ */
205
+ export function getFileAtCommit(file, commit = 'HEAD') {
206
+ try {
207
+ return gitSync(`show ${commit}:${file}`, { silent: true });
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Get remote URL
215
+ */
216
+ export function getRemoteUrl(remote = 'origin') {
217
+ try {
218
+ return gitSync(`remote get-url ${remote}`, { silent: true });
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
223
+
224
+ export default {
225
+ gitSync,
226
+ gitAsync,
227
+ isGitRepository,
228
+ getCurrentBranch,
229
+ getBranches,
230
+ getStagedFiles,
231
+ getModifiedFiles,
232
+ getUntrackedFiles,
233
+ getStagedDiff,
234
+ getCommitHistory,
235
+ getRepoRoot,
236
+ hasConflicts,
237
+ getConflictingFiles,
238
+ previewMerge,
239
+ getFileAtCommit,
240
+ getRemoteUrl,
241
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Centralized logging utility for Bit CLI
3
+ * Supports multiple log levels, file output, and structured logging
4
+ */
5
+
6
+ import winston from 'winston';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+
11
+ // Ensure log directory exists
12
+ const LOG_DIR = path.join(os.homedir(), '.bit', 'logs');
13
+ if (!fs.existsSync(LOG_DIR)) {
14
+ fs.mkdirSync(LOG_DIR, { recursive: true });
15
+ }
16
+
17
+ // Custom format for console output
18
+ const consoleFormat = winston.format.combine(
19
+ winston.format.timestamp({ format: 'HH:mm:ss' }),
20
+ winston.format.printf(({ level, message, timestamp, ...meta }) => {
21
+ const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
22
+ return `[${timestamp}] ${level.toUpperCase()}: ${message} ${metaStr}`;
23
+ })
24
+ );
25
+
26
+ // Custom format for file output
27
+ const fileFormat = winston.format.combine(
28
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
29
+ winston.format.json()
30
+ );
31
+
32
+ // Create logger instance
33
+ const logger = winston.createLogger({
34
+ level: process.env.BIT_LOG_LEVEL || 'info',
35
+ transports: [
36
+ // File transport for all logs
37
+ new winston.transports.File({
38
+ filename: path.join(LOG_DIR, 'bit-error.log'),
39
+ level: 'error',
40
+ format: fileFormat,
41
+ maxsize: 5242880, // 5MB
42
+ maxFiles: 5,
43
+ }),
44
+ new winston.transports.File({
45
+ filename: path.join(LOG_DIR, 'bit-combined.log'),
46
+ format: fileFormat,
47
+ maxsize: 5242880, // 5MB
48
+ maxFiles: 5,
49
+ }),
50
+ ],
51
+ });
52
+
53
+ // Add console transport in development
54
+ if (process.env.BIT_DEBUG === 'true') {
55
+ logger.add(
56
+ new winston.transports.Console({
57
+ format: consoleFormat,
58
+ })
59
+ );
60
+ }
61
+
62
+ // Helper methods for common logging patterns
63
+ export const log = {
64
+ info: (message, meta = {}) => logger.info(message, meta),
65
+ error: (message, meta = {}) => logger.error(message, meta),
66
+ warn: (message, meta = {}) => logger.warn(message, meta),
67
+ debug: (message, meta = {}) => logger.debug(message, meta),
68
+
69
+ // Log command execution
70
+ command: (cmd, args = []) => {
71
+ logger.info('Command executed', { command: cmd, arguments: args });
72
+ },
73
+
74
+ // Log git operations
75
+ git: (operation, details = {}) => {
76
+ logger.info('Git operation', { operation, ...details });
77
+ },
78
+
79
+ // Log AI operations
80
+ ai: (operation, details = {}) => {
81
+ logger.info('AI operation', { operation, ...details });
82
+ },
83
+
84
+ // Log errors with stack trace
85
+ exception: (error, context = {}) => {
86
+ logger.error('Exception occurred', {
87
+ message: error.message,
88
+ stack: error.stack,
89
+ ...context,
90
+ });
91
+ },
92
+ };
93
+
94
+ export default logger;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Input validation using Zod schemas
3
+ * Ensures all user inputs and data are validated before processing
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ // Branch name validation
9
+ export const branchNameSchema = z
10
+ .string()
11
+ .min(1, 'Branch name cannot be empty')
12
+ .max(255, 'Branch name too long')
13
+ .regex(
14
+ /^(?!\/|.*(?:\/\.|\/\/|@\{|\\))[^\x00-\x1f\x7f ~^:?*\[]+(?<!\.lock|\/|\.|\/)$/,
15
+ 'Invalid branch name. Cannot contain special characters or patterns forbidden by Git'
16
+ );
17
+
18
+ // Commit message validation
19
+ export const commitMessageSchema = z
20
+ .string()
21
+ .min(1, 'Commit message cannot be empty')
22
+ .max(500, 'Commit message too long (max 500 characters)')
23
+ .refine(
24
+ (msg) => !msg.startsWith(' '),
25
+ 'Commit message cannot start with a space'
26
+ );
27
+
28
+ // File path validation
29
+ export const filePathSchema = z
30
+ .string()
31
+ .min(1, 'File path cannot be empty')
32
+ .refine(
33
+ (path) => !path.includes('\0'),
34
+ 'File path cannot contain null bytes'
35
+ );
36
+
37
+ // Configuration validation
38
+ export const configSchema = z.object({
39
+ ai: z.object({
40
+ provider: z.enum(['openai', 'anthropic', 'local']).default('openai'),
41
+ model: z.string().default('gpt-4o-mini'),
42
+ maxTokens: z.number().min(100).max(4000).default(500),
43
+ temperature: z.number().min(0).max(2).default(0.7),
44
+ }).optional(),
45
+ git: z.object({
46
+ ghostPrefix: z.string().default('ghost/'),
47
+ autoStage: z.boolean().default(false),
48
+ signCommits: z.boolean().default(false),
49
+ }).optional(),
50
+ ui: z.object({
51
+ colors: z.boolean().default(true),
52
+ spinners: z.boolean().default(true),
53
+ verbose: z.boolean().default(false),
54
+ }).optional(),
55
+ });
56
+
57
+ // Remote URL validation
58
+ export const remoteUrlSchema = z
59
+ .string()
60
+ .refine(
61
+ (url) => {
62
+ // Accept SSH or HTTPS Git URLs
63
+ const sshPattern = /^git@[\w.-]+:[\w./-]+\.git$/;
64
+ const httpsPattern = /^https?:\/\/[\w.-]+\/[\w./-]+\.git$/;
65
+ return sshPattern.test(url) || httpsPattern.test(url);
66
+ },
67
+ 'Invalid Git remote URL'
68
+ );
69
+
70
+ /**
71
+ * Validate input against a schema
72
+ * @param {z.ZodSchema} schema - Zod schema to validate against
73
+ * @param {any} data - Data to validate
74
+ * @returns {{ success: boolean, data?: any, error?: string }}
75
+ */
76
+ export function validate(schema, data) {
77
+ try {
78
+ const result = schema.parse(data);
79
+ return { success: true, data: result };
80
+ } catch (error) {
81
+ if (error instanceof z.ZodError) {
82
+ const messages = error.errors.map((e) => e.message).join(', ');
83
+ return { success: false, error: messages };
84
+ }
85
+ return { success: false, error: 'Validation failed' };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Validate and throw on error
91
+ */
92
+ export function validateOrThrow(schema, data) {
93
+ const result = validate(schema, data);
94
+ if (!result.success) {
95
+ throw new Error(result.error);
96
+ }
97
+ return result.data;
98
+ }
99
+
100
+ export default {
101
+ branchNameSchema,
102
+ commitMessageSchema,
103
+ filePathSchema,
104
+ configSchema,
105
+ remoteUrlSchema,
106
+ validate,
107
+ validateOrThrow,
108
+ };