git-devflow 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,456 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CopilotService = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const util_1 = require("util");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const config_1 = require("./config");
11
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
12
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
13
+ /** Models that are included with paid plans and don't consume premium requests. */
14
+ const FREE_MODELS = ['gpt-4.1', 'gpt-5-mini'];
15
+ const FREE_FALLBACK_MODEL = FREE_MODELS[0];
16
+ /** Patterns that indicate the user has exceeded their premium request quota. */
17
+ const QUOTA_ERROR_PATTERNS = [
18
+ /premium request/i,
19
+ /rate limit/i,
20
+ /quota/i,
21
+ /exceeded.*allowance/i,
22
+ /budget.*reached/i,
23
+ /limit.*reached/i,
24
+ /too many requests/i,
25
+ /429/,
26
+ ];
27
+ const MODEL_CATALOG = {
28
+ // Free / included models
29
+ 'gpt-4.1': { tag: 'Free', description: 'Included with your plan, no extra cost', multiplier: 0 },
30
+ 'gpt-5-mini': { tag: 'Free', description: 'Included with your plan, fast and lightweight', multiplier: 0 },
31
+ // Cheap & fast
32
+ 'claude-haiku-4.5': { tag: 'Cheap', description: 'Fastest responses, very low cost', multiplier: 0.33 },
33
+ 'gpt-5.1-codex-mini': { tag: 'Cheap', description: 'Fast code generation, very low cost', multiplier: 0.33 },
34
+ // Balanced
35
+ 'claude-sonnet-4': { tag: 'Balanced', description: 'Good quality, standard cost', multiplier: 1 },
36
+ 'claude-sonnet-4.5': { tag: 'Balanced', description: 'Great quality and speed, recommended', multiplier: 1 },
37
+ 'gpt-5': { tag: 'Balanced', description: 'Solid all-rounder, standard cost', multiplier: 1 },
38
+ 'gpt-5.1': { tag: 'Balanced', description: 'Latest GPT, good quality, standard cost', multiplier: 1 },
39
+ 'gpt-5.1-codex': { tag: 'Balanced', description: 'Optimized for code, standard cost', multiplier: 1 },
40
+ 'gpt-5.1-codex-max': { tag: 'Balanced', description: 'Max context for code, standard cost', multiplier: 1 },
41
+ 'gpt-5.2': { tag: 'Balanced', description: 'Newest GPT, standard cost', multiplier: 1 },
42
+ 'gpt-5.2-codex': { tag: 'Balanced', description: 'Newest GPT for code, standard cost', multiplier: 1 },
43
+ 'gemini-3-pro-preview': { tag: 'Balanced', description: 'Google model, good for general tasks', multiplier: 1 },
44
+ // Expensive / highest quality
45
+ 'claude-opus-4.5': { tag: 'Expensive', description: 'Best quality, slowest, costs 3x per prompt', multiplier: 3 },
46
+ };
47
+ class CopilotService {
48
+ /**
49
+ * Fetches available models from the Copilot CLI by parsing `copilot --help` output,
50
+ * enriched with cost/multiplier metadata from GitHub's published rates.
51
+ * Falls back to a minimal hardcoded list if the CLI is unavailable or parsing fails.
52
+ */
53
+ async fetchAvailableModels() {
54
+ let modelIds;
55
+ try {
56
+ const { stdout } = await execAsync('copilot --help', {
57
+ timeout: 10000,
58
+ shell: '/bin/bash'
59
+ });
60
+ // Extract model choices from the --model flag description
61
+ // Format: --model <model> Set the AI model to use (choices: "model1", "model2", ...)
62
+ const modelSection = stdout.match(/--model\s+<model>\s+[\s\S]*?\(choices:\s*([\s\S]*?)\)/);
63
+ if (modelSection) {
64
+ const parsed = modelSection[1]
65
+ .match(/"([^"]+)"/g)
66
+ ?.map(m => m.replace(/"/g, ''));
67
+ modelIds = parsed && parsed.length > 0 ? parsed : this.getFallbackModelIds();
68
+ }
69
+ else {
70
+ modelIds = this.getFallbackModelIds();
71
+ }
72
+ }
73
+ catch {
74
+ modelIds = this.getFallbackModelIds();
75
+ }
76
+ return modelIds.map(id => {
77
+ const info = MODEL_CATALOG[id];
78
+ return {
79
+ id,
80
+ tag: info?.tag ?? 'New',
81
+ description: info?.description ?? 'Recently added model',
82
+ };
83
+ });
84
+ }
85
+ getFallbackModelIds() {
86
+ return [
87
+ 'claude-sonnet-4.5',
88
+ 'claude-haiku-4.5',
89
+ 'gpt-4.1'
90
+ ];
91
+ }
92
+ /**
93
+ * Prints the model name and its cost before a Copilot call.
94
+ */
95
+ printModelCost(model) {
96
+ const meta = MODEL_CATALOG[model];
97
+ const tag = meta?.tag ?? 'Unknown';
98
+ const multiplier = meta?.multiplier;
99
+ let costStr;
100
+ if (multiplier === 0)
101
+ costStr = chalk_1.default.green('free, no premium requests used');
102
+ else if (multiplier !== undefined)
103
+ costStr = chalk_1.default.yellow(`${multiplier} premium request(s) per prompt`);
104
+ else
105
+ costStr = chalk_1.default.dim('unknown cost');
106
+ const tagColor = tag === 'Free' ? chalk_1.default.green(tag) :
107
+ tag === 'Cheap' ? chalk_1.default.cyan(tag) :
108
+ tag === 'Balanced' ? chalk_1.default.yellow(tag) :
109
+ tag === 'Expensive' ? chalk_1.default.red(tag) :
110
+ chalk_1.default.dim(tag);
111
+ console.log(`🤖 Model: ${chalk_1.default.bold(model)} ${tagColor} · ${costStr}`);
112
+ }
113
+ /**
114
+ * Checks whether an error from the Copilot CLI looks like a quota/rate-limit issue.
115
+ */
116
+ isQuotaError(error) {
117
+ const msg = error.message || '';
118
+ return QUOTA_ERROR_PATTERNS.some(p => p.test(msg));
119
+ }
120
+ /**
121
+ * Prints a user-friendly warning when quota is exceeded and we're retrying
122
+ * with a free model.
123
+ */
124
+ printQuotaWarning(currentModel) {
125
+ console.log('');
126
+ console.log(chalk_1.default.yellow('⚠️ Premium request limit reached for ') + chalk_1.default.bold(currentModel));
127
+ console.log(chalk_1.default.dim(' Your monthly quota may be exhausted or the model is rate-limited.'));
128
+ console.log(chalk_1.default.cyan(` Retrying with ${FREE_FALLBACK_MODEL} (free, no premium requests)...\n`));
129
+ this.printModelCost(FREE_FALLBACK_MODEL);
130
+ }
131
+ /**
132
+ * Prints a tip after a successful free-model retry so the user knows
133
+ * how to permanently switch.
134
+ */
135
+ printSwitchTip() {
136
+ console.log(chalk_1.default.dim(`\n Tip: To avoid this, switch your default model:`));
137
+ console.log(chalk_1.default.dim(` Run: `) + chalk_1.default.cyan('devflow config setup') + chalk_1.default.dim(' and pick a Free model.\n'));
138
+ }
139
+ /**
140
+ * Runs a prompt against the Copilot CLI with the given model.
141
+ * Returns stdout on success, or throws on failure.
142
+ */
143
+ async execCopilotPrompt(prompt, model) {
144
+ try {
145
+ // Build args array — passed directly to the binary, no shell interpretation.
146
+ // This avoids issues with $, backticks, quotes etc. in diff content.
147
+ const args = [];
148
+ if (model) {
149
+ args.push('--model', model);
150
+ }
151
+ args.push('-s', '--prompt', prompt);
152
+ const { stdout } = await execFileAsync('copilot', args, {
153
+ maxBuffer: 1024 * 1024,
154
+ timeout: 60000,
155
+ });
156
+ return stdout;
157
+ }
158
+ catch (error) {
159
+ // Build a safe error message without leaking sensitive data
160
+ const stderr = (error.stderr || '').trim();
161
+ const code = error.code;
162
+ const exitCode = error.status ?? error.code;
163
+ if (code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
164
+ throw new Error('Copilot response exceeded buffer size');
165
+ }
166
+ if (error.killed) {
167
+ throw new Error('Copilot request timed out (60s). Try a faster model like gpt-4.1 or claude-haiku-4.5');
168
+ }
169
+ // Pass through stderr if it's short and doesn't contain secrets
170
+ const safeStderr = stderr.length > 0 && stderr.length < 200 && !/token|auth|key|secret|password/i.test(stderr)
171
+ ? stderr
172
+ : `exit code ${exitCode || 'unknown'}`;
173
+ throw new Error(`Copilot CLI error: ${safeStderr}`);
174
+ }
175
+ }
176
+ /**
177
+ * Runs a prompt with the user's configured model.
178
+ * Shows cost info before the call.
179
+ * If a quota/rate-limit error is detected, automatically retries with a free model.
180
+ */
181
+ async execWithQuotaRetry(prompt) {
182
+ const configService = new config_1.ConfigService();
183
+ const model = configService.getCopilotModel();
184
+ const isFreeModel = FREE_MODELS.includes(model);
185
+ // Show model + cost before making the call
186
+ this.printModelCost(model);
187
+ console.log('');
188
+ try {
189
+ return await this.execCopilotPrompt(prompt, model);
190
+ }
191
+ catch (error) {
192
+ // If already on a free model, no point retrying — just throw
193
+ if (isFreeModel)
194
+ throw error;
195
+ // If it looks like a quota issue, retry with the free model
196
+ if (this.isQuotaError(error)) {
197
+ this.printQuotaWarning(model);
198
+ const result = await this.execCopilotPrompt(prompt, FREE_FALLBACK_MODEL);
199
+ this.printSwitchTip();
200
+ return result;
201
+ }
202
+ // Some other error — let it bubble up
203
+ throw error;
204
+ }
205
+ }
206
+ async generateCommitMessage(diff) {
207
+ try {
208
+ const prompt = `Generate 3 commit messages for these git changes. Use conventional commits format (feat/fix/chore/etc).
209
+
210
+ Changes:
211
+ ${diff.substring(0, 2000)}
212
+
213
+ Respond with 3 options:
214
+ 1. [first message]
215
+ 2. [second message]
216
+ 3. [third message]
217
+
218
+ Keep each under 72 characters.`;
219
+ const stdout = await this.execWithQuotaRetry(prompt);
220
+ // Parse messages
221
+ const messages = this.parseCommitMessages(stdout);
222
+ if (messages.length > 0) {
223
+ return messages;
224
+ }
225
+ return this.generateFallbackMessages(diff);
226
+ }
227
+ catch (error) {
228
+ const reason = error instanceof Error ? error.message : 'unknown error';
229
+ console.log(chalk_1.default.yellow(`⚠️ ${reason}`));
230
+ console.log(chalk_1.default.dim(' Using fallback suggestions\n'));
231
+ return this.generateFallbackMessages(diff);
232
+ }
233
+ }
234
+ stripMarkdown(text) {
235
+ return text
236
+ .replace(/\*\*/g, '') // Remove bold **
237
+ .replace(/\*/g, '') // Remove italic *
238
+ .replace(/`/g, '') // Remove code `
239
+ .replace(/^[-•]\s+/, '') // Remove list markers
240
+ .trim();
241
+ }
242
+ parseCommitMessages(response) {
243
+ const messages = [];
244
+ // Split by lines
245
+ const lines = response
246
+ .split('\n')
247
+ .map(l => l.trim())
248
+ .filter(l => l.length > 0);
249
+ for (const line of lines) {
250
+ // Try multiple patterns
251
+ // Pattern 1: "1. message" or "1) message"
252
+ let match = line.match(/^\d+[\.)]\s*(.+)$/);
253
+ if (match) {
254
+ messages.push(this.stripMarkdown(match[1].trim()));
255
+ continue;
256
+ }
257
+ // Pattern 2: Direct conventional commit format
258
+ match = line.match(/^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\(.+?\))?: .+/i);
259
+ if (match) {
260
+ messages.push(this.stripMarkdown(line));
261
+ continue;
262
+ }
263
+ // Pattern 3: "- message" (markdown list)
264
+ match = line.match(/^[-*]\s+(.+)$/);
265
+ if (match && match[1].match(/^(feat|fix|docs|style|refactor|test|chore)/i)) {
266
+ messages.push(this.stripMarkdown(match[1].trim()));
267
+ continue;
268
+ }
269
+ }
270
+ return messages.slice(0, 3);
271
+ }
272
+ generateFallbackMessages(diff) {
273
+ const lines = diff.split('\n');
274
+ const added = lines.filter(l => l.startsWith('+')).length;
275
+ const removed = lines.filter(l => l.startsWith('-')).length;
276
+ return [
277
+ `feat: update files (+${added} -${removed})`,
278
+ `fix: improve code quality`,
279
+ `chore: update documentation`
280
+ ];
281
+ }
282
+ async generatePRDescription(commits, issueContext = '') {
283
+ try {
284
+ const commitList = commits.map(c => `- ${c.message}`).join('\n');
285
+ const prompt = `Generate a PR title and description for these commits:\n${commitList}\n\nFormat:\nTITLE: [title]\nBODY:\n[description]`;
286
+ const stdout = await this.execWithQuotaRetry(prompt);
287
+ // Parse response
288
+ const parsed = this.parsePRDescription(stdout);
289
+ if (parsed.title && parsed.body && parsed.body.length > 50) {
290
+ return parsed;
291
+ }
292
+ // If parsing gave weak results, use fallback
293
+ console.log('⚠️ Weak response, using fallback\n');
294
+ return this.generateSmartFallback(commits, issueContext);
295
+ }
296
+ catch (error) {
297
+ const reason = error instanceof Error ? error.message : 'unknown error';
298
+ console.log(chalk_1.default.yellow(`⚠️ ${reason}`));
299
+ console.log(chalk_1.default.dim(' Using fallback PR description\n'));
300
+ return this.generateSmartFallback(commits, issueContext);
301
+ }
302
+ }
303
+ generateSmartFallback(commits, issueContext) {
304
+ // Use first commit as title if it's good
305
+ const title = commits[0]?.message || 'Update';
306
+ // Group commits by type
307
+ const features = commits.filter(c => c.message.startsWith('feat'));
308
+ const fixes = commits.filter(c => c.message.startsWith('fix'));
309
+ const chores = commits.filter(c => c.message.startsWith('chore'));
310
+ const others = commits.filter(c => !c.message.match(/^(feat|fix|chore)/));
311
+ let body = '## Summary\n\n';
312
+ if (commits.length === 1) {
313
+ body += commits[0].message;
314
+ }
315
+ else {
316
+ body += `This PR includes ${commits.length} commits with `;
317
+ const types = [];
318
+ if (features.length)
319
+ types.push(`${features.length} feature(s)`);
320
+ if (fixes.length)
321
+ types.push(`${fixes.length} fix(es)`);
322
+ if (chores.length)
323
+ types.push(`${chores.length} chore(s)`);
324
+ body += types.join(', ') || 'various changes';
325
+ body += '.';
326
+ }
327
+ body += '\n\n## Changes\n\n';
328
+ if (features.length) {
329
+ body += '### Features\n';
330
+ features.forEach(c => body += `- ${c.message}\n`);
331
+ body += '\n';
332
+ }
333
+ if (fixes.length) {
334
+ body += '### Fixes\n';
335
+ fixes.forEach(c => body += `- ${c.message}\n`);
336
+ body += '\n';
337
+ }
338
+ if (chores.length) {
339
+ body += '### Chores\n';
340
+ chores.forEach(c => body += `- ${c.message}\n`);
341
+ body += '\n';
342
+ }
343
+ if (others.length) {
344
+ body += '### Other Changes\n';
345
+ others.forEach(c => body += `- ${c.message}\n`);
346
+ body += '\n';
347
+ }
348
+ if (issueContext) {
349
+ body += `\n${issueContext}\n`;
350
+ }
351
+ body += '\n## Testing\n\nPlease test these changes in your environment.\n';
352
+ return { title, body };
353
+ }
354
+ parsePRDescription(response) {
355
+ // Try to extract title and body
356
+ const titleMatch = response.match(/TITLE:\s*(.+)/i);
357
+ const bodyMatch = response.match(/BODY:\s*([\s\S]+)/i);
358
+ if (titleMatch && bodyMatch) {
359
+ return {
360
+ title: this.stripMarkdown(titleMatch[1].trim()),
361
+ body: bodyMatch[1].trim()
362
+ };
363
+ }
364
+ // If parsing fails, use first line as title, rest as body
365
+ const lines = response.split('\n').filter(l => l.trim().length > 0);
366
+ return {
367
+ title: this.stripMarkdown(lines[0] || 'Update'),
368
+ body: lines.slice(1).join('\n') || 'Pull request description'
369
+ };
370
+ }
371
+ generateFallbackPR(commits, issueContext) {
372
+ const title = commits[0]?.message || 'Update code';
373
+ const body = `## Summary
374
+ This PR includes ${commits.length} commit(s).
375
+
376
+ ## Changes
377
+ ${commits.map(c => `- ${c.message}`).join('\n')}
378
+
379
+ ${issueContext ? `## Related Issues\n${issueContext}` : ''}
380
+
381
+ ## Testing
382
+ Please test the changes thoroughly.`;
383
+ return { title, body };
384
+ }
385
+ async generateBranchName(description, type = 'feature', issueNumber) {
386
+ try {
387
+ const issuePrefix = issueNumber ? `${issueNumber}-` : '';
388
+ const prompt = `Generate a git branch name from this description: "${description}"
389
+
390
+ Branch type: ${type}
391
+ ${issueNumber ? `Issue number: ${issueNumber}` : ''}
392
+
393
+ Rules:
394
+ - Use format: ${type}/${issuePrefix}[descriptive-name]
395
+ - Use kebab-case (lowercase with hyphens)
396
+ - Keep it concise (max 50 chars)
397
+ - Be descriptive but brief
398
+ - Only alphanumeric and hyphens
399
+
400
+ Example: feature/123-add-user-auth
401
+
402
+ Respond with ONLY the branch name, nothing else.`;
403
+ const stdout = await this.execWithQuotaRetry(prompt);
404
+ // Parse the branch name from response
405
+ let branchName = stdout.trim().split('\n')[0].trim();
406
+ // Remove any markdown formatting
407
+ branchName = branchName.replace(/```/g, '').replace(/`/g, '').trim();
408
+ // Remove quotes if present
409
+ branchName = branchName.replace(/^["']|["']$/g, '');
410
+ // Validate and sanitize
411
+ branchName = this.sanitizeBranchName(branchName, type, issuePrefix);
412
+ return branchName;
413
+ }
414
+ catch (error) {
415
+ const reason = error instanceof Error ? error.message : 'unknown error';
416
+ console.log(chalk_1.default.yellow(`⚠️ ${reason}`));
417
+ console.log(chalk_1.default.dim(' Using fallback branch name\n'));
418
+ return this.generateFallbackBranchName(description, type, issueNumber);
419
+ }
420
+ }
421
+ // Sanitize branch name
422
+ sanitizeBranchName(name, type, issuePrefix) {
423
+ // Remove type prefix if Copilot added it
424
+ name = name.replace(new RegExp(`^${type}/`, 'i'), '');
425
+ // Convert to lowercase and replace spaces/special chars with hyphens
426
+ name = name
427
+ .toLowerCase()
428
+ .replace(/[^a-z0-9-]/g, '-')
429
+ .replace(/-+/g, '-')
430
+ .replace(/^-|-$/g, '');
431
+ // Limit length
432
+ const maxLength = 50 - type.length - issuePrefix.length - 1;
433
+ if (name.length > maxLength) {
434
+ name = name.substring(0, maxLength).replace(/-$/, '');
435
+ }
436
+ return `${type}/${issuePrefix}${name}`;
437
+ }
438
+ // Fallback branch name generation
439
+ generateFallbackBranchName(description, type, issueNumber) {
440
+ const issuePrefix = issueNumber ? `${issueNumber}-` : '';
441
+ // Convert description to kebab-case
442
+ let name = description
443
+ .toLowerCase()
444
+ .replace(/[^a-z0-9\s-]/g, '')
445
+ .replace(/\s+/g, '-')
446
+ .replace(/-+/g, '-')
447
+ .replace(/^-|-$/g, '');
448
+ // Limit length
449
+ const maxLength = 50 - type.length - issuePrefix.length - 1;
450
+ if (name.length > maxLength) {
451
+ name = name.substring(0, maxLength).replace(/-$/, '');
452
+ }
453
+ return `${type}/${issuePrefix}${name}`;
454
+ }
455
+ }
456
+ exports.CopilotService = CopilotService;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GitService = void 0;
7
+ const simple_git_1 = __importDefault(require("simple-git"));
8
+ class GitService {
9
+ constructor() {
10
+ this.git = (0, simple_git_1.default)();
11
+ }
12
+ // Check if we're in a git repository
13
+ async isGitRepo() {
14
+ try {
15
+ await this.git.status();
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ // Get staged changes (git diff --cached)
23
+ async getStagedDiff() {
24
+ try {
25
+ const diff = await this.git.diff(['--cached']);
26
+ return diff;
27
+ }
28
+ catch (error) {
29
+ throw new Error('Failed to get staged changes');
30
+ }
31
+ }
32
+ // Get status
33
+ async getStatus() {
34
+ return await this.git.status();
35
+ }
36
+ // Stage all changes
37
+ async stageAll() {
38
+ await this.git.add('.');
39
+ }
40
+ // Create commit
41
+ async commit(message) {
42
+ await this.git.commit(message);
43
+ }
44
+ // Check if there are staged changes
45
+ async hasStagedChanges() {
46
+ const status = await this.getStatus();
47
+ return status.staged.length > 0;
48
+ }
49
+ // Get current branch name
50
+ async getCurrentBranch() {
51
+ const status = await this.git.status();
52
+ return status.current || 'main';
53
+ }
54
+ // Get base branch (usually main or master)
55
+ async getBaseBranch() {
56
+ try {
57
+ // Try to find main first
58
+ await this.git.raw(['rev-parse', '--verify', 'main']);
59
+ return 'main';
60
+ }
61
+ catch {
62
+ try {
63
+ // Fall back to master
64
+ await this.git.raw(['rev-parse', '--verify', 'master']);
65
+ return 'master';
66
+ }
67
+ catch {
68
+ return 'main'; // Default to main
69
+ }
70
+ }
71
+ }
72
+ // Get commits in current branch (not in base branch)
73
+ async getCommitsSinceBase() {
74
+ const currentBranch = await this.getCurrentBranch();
75
+ const baseBranch = await this.getBaseBranch();
76
+ try {
77
+ const log = await this.git.log({
78
+ from: baseBranch,
79
+ to: currentBranch
80
+ });
81
+ return log.all.map(commit => ({
82
+ hash: commit.hash.substring(0, 7),
83
+ message: commit.message,
84
+ author: commit.author_name,
85
+ date: commit.date
86
+ }));
87
+ }
88
+ catch (error) {
89
+ throw new Error('Failed to get commit history');
90
+ }
91
+ }
92
+ // Extract issue number from branch name or commits
93
+ extractIssueNumber() {
94
+ // Try to extract from branch name (e.g., feature/add-auth-123)
95
+ const branchMatch = this.git.branch().then(b => b.current?.match(/-(\d+)$/));
96
+ // For now, return null (we'll enhance this later)
97
+ return null;
98
+ }
99
+ // Check if current branch exists on remote
100
+ async isBranchPushed(branchName) {
101
+ try {
102
+ const result = await this.git.raw(['ls-remote', '--heads', 'origin', branchName]);
103
+ return result.trim().length > 0;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ // Push current branch to remote
110
+ async pushBranch(branchName) {
111
+ await this.git.push('origin', branchName, ['--set-upstream']);
112
+ }
113
+ // Create and checkout a new branch
114
+ async createAndCheckoutBranch(branchName) {
115
+ try {
116
+ await this.git.checkoutLocalBranch(branchName);
117
+ }
118
+ catch (error) {
119
+ throw new Error(`Failed to create branch: ${error.message}`);
120
+ }
121
+ }
122
+ // Check if branch already exists
123
+ async branchExists(branchName) {
124
+ try {
125
+ const branches = await this.git.branchLocal();
126
+ return branches.all.includes(branchName);
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ }
133
+ exports.GitService = GitService;