ai-ship-cli 0.1.0-beta.1

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.
Files changed (68) hide show
  1. package/.prettierignore +3 -0
  2. package/.prettierrc +7 -0
  3. package/LICENSE +21 -0
  4. package/README.md +156 -0
  5. package/dist/ai/gemini.js +34 -0
  6. package/dist/ai/ollama.js +36 -0
  7. package/dist/analyzers/analyzer.js +93 -0
  8. package/dist/analyzers/compressBranchSummary.js +12 -0
  9. package/dist/analyzers/configAnalyzer.js +18 -0
  10. package/dist/analyzers/detectSignals.js +18 -0
  11. package/dist/analyzers/markupAnalyzer.js +26 -0
  12. package/dist/commands/commit/customAdd.js +1 -0
  13. package/dist/commands/commit/startCommit.js +155 -0
  14. package/dist/commands/commit.js +21 -0
  15. package/dist/commands/config/deleteKey.js +15 -0
  16. package/dist/commands/config.js +65 -0
  17. package/dist/commands/git/startCheckout.js +62 -0
  18. package/dist/commands/git/startCommit.js +91 -0
  19. package/dist/commands/git/startPR.js +51 -0
  20. package/dist/commands/git/startPush.js +24 -0
  21. package/dist/commands/git/startWorkflow.js +71 -0
  22. package/dist/commands/github/github.js +63 -0
  23. package/dist/commands/pr.js +1 -0
  24. package/dist/index.js +38 -0
  25. package/dist/utils/ai.js +22 -0
  26. package/dist/utils/asyncExecuter.js +35 -0
  27. package/dist/utils/files.js +28 -0
  28. package/dist/utils/git.js +106 -0
  29. package/dist/utils/github.js +13 -0
  30. package/dist/utils/helper.js +130 -0
  31. package/dist/utils/inputs.js +20 -0
  32. package/dist/utils/inquirer.js +79 -0
  33. package/dist/utils/parser.js +54 -0
  34. package/dist/utils/print.js +25 -0
  35. package/dist/utils/prompts.js +206 -0
  36. package/dist/utils/runCommit.js +17 -0
  37. package/dist/utils/runConfig.js +35 -0
  38. package/docs/commands.md +106 -0
  39. package/package.json +44 -0
  40. package/src/ai/gemini.ts +27 -0
  41. package/src/ai/ollama.ts +38 -0
  42. package/src/analyzers/analyzer.ts +117 -0
  43. package/src/analyzers/compressBranchSummary.ts +16 -0
  44. package/src/analyzers/configAnalyzer.ts +17 -0
  45. package/src/analyzers/detectSignals.ts +13 -0
  46. package/src/analyzers/markupAnalyzer.ts +25 -0
  47. package/src/commands/commit.ts +18 -0
  48. package/src/commands/config.ts +73 -0
  49. package/src/commands/git/startCheckout.ts +97 -0
  50. package/src/commands/git/startCommit.ts +108 -0
  51. package/src/commands/git/startPR.ts +66 -0
  52. package/src/commands/git/startPush.ts +18 -0
  53. package/src/commands/git/startWorkflow.ts +71 -0
  54. package/src/commands/github/github.ts +72 -0
  55. package/src/commands/pr.ts +0 -0
  56. package/src/index.ts +40 -0
  57. package/src/utils/ai.ts +30 -0
  58. package/src/utils/asyncExecuter.ts +39 -0
  59. package/src/utils/files.ts +30 -0
  60. package/src/utils/git.ts +108 -0
  61. package/src/utils/github.ts +19 -0
  62. package/src/utils/helper.ts +145 -0
  63. package/src/utils/inputs.ts +15 -0
  64. package/src/utils/inquirer.ts +99 -0
  65. package/src/utils/parser.ts +58 -0
  66. package/src/utils/print.ts +16 -0
  67. package/src/utils/prompts.ts +234 -0
  68. package/tsconfig.json +11 -0
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "ai-ship-cli",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "AI-powered Git workflow CLI (commit, branch, PR generation)",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "ai-ship": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "ts-node src/index.ts",
12
+ "format": "prettier --write .",
13
+ "prepare": "npm run build"
14
+ },
15
+ "keywords": [
16
+ "git",
17
+ "cli",
18
+ "ai",
19
+ "commit",
20
+ "developer-tools",
21
+ "productivity"
22
+ ],
23
+ "author": "Diganta Kr Banik",
24
+ "license": "ISC",
25
+ "dependencies": {
26
+ "@google/genai": "^1.44.0",
27
+ "chalk": "^4.1.2",
28
+ "dotenv": "^17.3.1",
29
+ "enquirer": "^2.4.1",
30
+ "inquirer": "^8.2.7",
31
+ "lodash": "^4.17.23",
32
+ "minimatch": "^10.2.4",
33
+ "minimist": "^1.2.8",
34
+ "ora": "^5.4.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/inquirer": "^8.2.12",
38
+ "@types/minimist": "^1.2.5",
39
+ "@types/node": "^25.3.5",
40
+ "prettier": "^3.8.1",
41
+ "ts-node": "^10.9.2",
42
+ "typescript": "^5.9.3"
43
+ }
44
+ }
@@ -0,0 +1,27 @@
1
+ import { GoogleGenAI } from '@google/genai';
2
+ import { getCurrentConfig, saveValueToConfig } from '../utils/helper';
3
+ import { askApiKey } from '../utils/inputs';
4
+ import chalk from 'chalk';
5
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
6
+ let apiKey = getCurrentConfig('geminiApiKey') || GEMINI_API_KEY;
7
+
8
+ let ai = new GoogleGenAI({ apiKey });
9
+ export const generateWithGemini = async (prompt: string) => {
10
+ try {
11
+ if (!apiKey) {
12
+ console.log('Gemini API key not found.');
13
+ apiKey = await askApiKey();
14
+ saveValueToConfig('geminiApiKey', apiKey);
15
+ ai = new GoogleGenAI({ apiKey });
16
+ console.log('API key saved!');
17
+ }
18
+ const response = await ai.models.generateContent({
19
+ model: 'gemini-2.5-flash',
20
+ contents: prompt,
21
+ });
22
+ return response.text || '';
23
+ } catch (err) {
24
+ console.log(chalk.red(`We ran into an error: ${chalk.bold.red(err)}`));
25
+ return '';
26
+ }
27
+ };
@@ -0,0 +1,38 @@
1
+ import chalk from 'chalk';
2
+ import { getCurrentConfig } from '../utils/helper';
3
+
4
+ export const generateWithGemma = async (prompt: string) => {
5
+ try {
6
+ const endpoint = getCurrentConfig('localEndpoint') || 'http://127.0.0.1:11434';
7
+ const model = getCurrentConfig('model') || 'gemma:2b';
8
+
9
+ const res = await fetch(`${endpoint}/api/generate`, {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: JSON.stringify({
15
+ model: model,
16
+ prompt,
17
+ stream: false,
18
+ options: {
19
+ temperature: 0.1,
20
+ },
21
+ }),
22
+ });
23
+
24
+ const data = await res.json();
25
+
26
+ if (!res.ok || data.error) {
27
+ throw new Error(data.error || `HTTP error! status: ${res.status}`);
28
+ }
29
+
30
+ if (data.response) {
31
+ return data.response.trim();
32
+ }
33
+
34
+ throw new Error('Unexpected response format from local model');
35
+ } catch (e) {
36
+ throw new Error(`Failed to connect to local model: ${(e as Error).message}`);
37
+ }
38
+ };
@@ -0,0 +1,117 @@
1
+ import { detectSignals } from './detectSignals';
2
+ import analyzeMarkup from './markupAnalyzer';
3
+ import analyzeConfig from './configAnalyzer';
4
+
5
+ type DiffSummary = {
6
+ file: string;
7
+ additions: number;
8
+ deletions: number;
9
+ signals: string[];
10
+ snippet: string[];
11
+ };
12
+
13
+ const CODE_EXT = [
14
+ '.js',
15
+ '.ts',
16
+ '.jsx',
17
+ '.tsx',
18
+ '.py',
19
+ '.java',
20
+ '.go',
21
+ '.rs',
22
+ '.cpp',
23
+ '.c',
24
+ '.h',
25
+ '.hpp',
26
+ '.cs',
27
+ '.php',
28
+ '.rb',
29
+ ];
30
+
31
+ const MARKUP_EXT = ['.html', '.css', '.scss', '.less'];
32
+
33
+ const CONFIG_EXT = ['.json', '.yaml', '.yml', '.toml'];
34
+
35
+ const getFileType = (file: string) => {
36
+ const lower = file.toLowerCase();
37
+
38
+ if (CODE_EXT.some((ext) => lower.endsWith(ext))) return 'code';
39
+
40
+ if (MARKUP_EXT.some((ext) => lower.endsWith(ext))) return 'markup';
41
+
42
+ if (
43
+ CONFIG_EXT.some((ext) => lower.endsWith(ext)) ||
44
+ lower === 'dockerfile' ||
45
+ lower.includes('package.json')
46
+ )
47
+ return 'config';
48
+
49
+ return 'other';
50
+ };
51
+
52
+ export const analyzeDiff = (diff: string): DiffSummary[] => {
53
+ try {
54
+ const files = diff.split('diff --git').filter(Boolean);
55
+
56
+ const summaries: DiffSummary[] = [];
57
+
58
+ for (const chunk of files) {
59
+ const lines = chunk.split('\n');
60
+
61
+ const fileMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
62
+ const file = fileMatch?.[2] || 'unknown';
63
+
64
+ const fileType = getFileType(file);
65
+
66
+ if (fileType === 'markup') {
67
+ summaries.push(analyzeMarkup(file, lines));
68
+ continue;
69
+ }
70
+
71
+ if (fileType === 'config') {
72
+ summaries.push(analyzeConfig(file, lines));
73
+ continue;
74
+ }
75
+
76
+ // default = code analyzer
77
+ let additions = 0;
78
+ let deletions = 0;
79
+
80
+ const snippet: string[] = [];
81
+ const signals: string[] = [];
82
+
83
+ for (const line of lines) {
84
+ if (line.startsWith('+') && !line.startsWith('+++')) {
85
+ additions++;
86
+
87
+ const code = line.slice(1).trim();
88
+
89
+ if (snippet.length < 20) snippet.push(`+ ${code}`);
90
+
91
+ detectSignals(code, signals);
92
+ }
93
+
94
+ if (line.startsWith('-') && !line.startsWith('---')) {
95
+ deletions++;
96
+
97
+ const code = line.slice(1).trim();
98
+
99
+ if (snippet.length < 20) snippet.push(`- ${code}`);
100
+ }
101
+ }
102
+
103
+ summaries.push({
104
+ file,
105
+ additions,
106
+ deletions,
107
+ signals: [...new Set(signals)],
108
+ snippet,
109
+ });
110
+ }
111
+
112
+ return summaries;
113
+ } catch (e) {
114
+ console.log({ e });
115
+ return [];
116
+ }
117
+ };
@@ -0,0 +1,16 @@
1
+ type DiffSummary = {
2
+ file: string;
3
+ additions: number;
4
+ deletions: number;
5
+ signals: string[];
6
+ snippet: string[];
7
+ };
8
+
9
+ export const compressBranchSummary = (summary: DiffSummary[]) => {
10
+ return summary
11
+ .slice(0, 8) // limit files
12
+ .map((s) => ({
13
+ file: s.file,
14
+ signals: s.signals,
15
+ }));
16
+ };
@@ -0,0 +1,17 @@
1
+ export default (file: string, lines: string[]) => {
2
+ const snippet: string[] = [];
3
+
4
+ for (const line of lines) {
5
+ if (line.startsWith('+') || line.startsWith('-')) {
6
+ if (snippet.length < 30) snippet.push(line);
7
+ }
8
+ }
9
+
10
+ return {
11
+ file,
12
+ additions: snippet.filter((l) => l.startsWith('+')).length,
13
+ deletions: snippet.filter((l) => l.startsWith('-')).length,
14
+ signals: ['config updated'],
15
+ snippet,
16
+ };
17
+ };
@@ -0,0 +1,13 @@
1
+ export const detectSignals = (line: string, signals: string[]) => {
2
+ if (/function\s+\w+/.test(line)) signals.push('function added');
3
+
4
+ if (/class\s+\w+/.test(line)) signals.push('class added');
5
+
6
+ if (/import\s+/.test(line)) signals.push('imports updated');
7
+
8
+ if (/export\s+/.test(line)) signals.push('exports updated');
9
+
10
+ if (/FROM\s+/.test(line)) signals.push('docker base image change');
11
+
12
+ if (/version/.test(line)) signals.push('version updated');
13
+ };
@@ -0,0 +1,25 @@
1
+ export default (file: string, lines: string[]) => {
2
+ const snippet: string[] = [];
3
+ let additions = 0;
4
+ let deletions = 0;
5
+
6
+ for (const line of lines) {
7
+ if (line.startsWith('+') && !line.startsWith('+++')) {
8
+ additions++;
9
+ if (snippet.length < 20) snippet.push(line);
10
+ }
11
+
12
+ if (line.startsWith('-') && !line.startsWith('---')) {
13
+ deletions++;
14
+ if (snippet.length < 20) snippet.push(line);
15
+ }
16
+ }
17
+
18
+ return {
19
+ file,
20
+ additions,
21
+ deletions,
22
+ signals: ['markup updated'],
23
+ snippet,
24
+ };
25
+ };
@@ -0,0 +1,18 @@
1
+ import ora from 'ora';
2
+ import startWorkflow from './git/startWorkflow';
3
+ import { stageAll, stageFiles } from '../utils/git';
4
+
5
+ export default async (payload: string[] = [], flags: any = {}) => {
6
+ const stageSpinner = ora('Staging files...').start();
7
+ if (payload.length > 0) {
8
+ await stageFiles(payload);
9
+ stageSpinner.succeed(`Staged ${payload.length} specified file(s).`);
10
+ } else {
11
+ await stageAll();
12
+ stageSpinner.succeed('Staged all changes.');
13
+ }
14
+
15
+ console.log('');
16
+
17
+ await startWorkflow(flags);
18
+ };
@@ -0,0 +1,73 @@
1
+ import chalk from 'chalk';
2
+ import {
3
+ deleteConfigKey,
4
+ getCurrentConfig,
5
+ jsonConfig,
6
+ log,
7
+ saveValueToConfig,
8
+ verboseConfig,
9
+ } from '../utils/helper';
10
+ import { askApiKey } from '../utils/inputs';
11
+ import type { ParsedArgs } from 'minimist';
12
+ type AIShipConfig = Record<string, any>;
13
+ export default async (args: ParsedArgs) => {
14
+ // `args` now contains elegantly parsed flags from minimist!
15
+ // Example: `--model --local connection.json` becomes `{ model: true, local: 'connection.json' }`
16
+ // Example: `--user-model local` becomes `{ 'user-model': 'local' }`
17
+ const subCommand = args._[0];
18
+ if (subCommand === 'show') {
19
+ const currenConfigs = getCurrentConfig();
20
+ if (args['verbose']) {
21
+ verboseConfig(currenConfigs);
22
+ } else if (args['json']) {
23
+ jsonConfig(currenConfigs);
24
+ }
25
+ return;
26
+ }
27
+
28
+ if (subCommand === 'set') {
29
+ const key = args._[1];
30
+ const value = args._[2];
31
+
32
+ if (!key || !value) {
33
+ console.log('Usage: ai-ship config set <key> <value>');
34
+ return;
35
+ }
36
+
37
+ saveValueToConfig(key, value);
38
+ return;
39
+ }
40
+
41
+ if (subCommand === 'get') {
42
+ const key = args._[1];
43
+
44
+ if (!key) {
45
+ console.log(chalk.yellow('Usage: ai-ship config get <key>'));
46
+ return;
47
+ }
48
+
49
+ const configValue = getCurrentConfig(key);
50
+
51
+ if (configValue === undefined || configValue === null || configValue === '') {
52
+ console.log(chalk.red(`Config "${key}" not found.`));
53
+ return;
54
+ }
55
+
56
+ console.log(`${chalk.cyan(key)}: ${chalk.green(JSON.stringify(configValue, null, 2))}`);
57
+ return;
58
+ }
59
+ if (args['add-key']) {
60
+ const apiKey = await askApiKey();
61
+ saveValueToConfig('geminiApiKey', apiKey);
62
+ log('API key saved!');
63
+ } else if (args['delete-key']) {
64
+ if (deleteConfigKey('geminiApiKey')) {
65
+ log(chalk.green('API Key Deleted'));
66
+ } else {
67
+ log(chalk.red('API Key Could Not Be Deleted. API KEY NOT FOUND!'));
68
+ }
69
+ } else {
70
+ log(chalk.yellow('Unrecognized config option. Here are the extracted args for your logic:'));
71
+ console.log(args);
72
+ }
73
+ };
@@ -0,0 +1,97 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { generateAIResponse, getBranchPrompt } from '../../utils/ai';
4
+ import { interactiveRefinePrompt } from '../../utils/inquirer';
5
+ import { gitCheckoutNewBranch, gitRenameBranch } from '../../utils/git';
6
+
7
+ type BranchParams = {
8
+ branchSummary: any[];
9
+ allBranches: string[];
10
+ currentBranch: string;
11
+ commitMessage: string;
12
+ provider: string;
13
+ flags?: any;
14
+ };
15
+
16
+ export default async ({
17
+ branchSummary,
18
+ allBranches,
19
+ currentBranch,
20
+ commitMessage,
21
+ provider,
22
+ flags = {},
23
+ }: BranchParams) => {
24
+ let branchName = '';
25
+ let branchAccepted = false;
26
+
27
+ const sanitizeBranchName = (name: string) => {
28
+ return name
29
+ .toLowerCase()
30
+ .replace(/['"]/g, '')
31
+ .replace(/\\s+/g, '-')
32
+ .replace(/[^a-z0-9\\-\\/]/g, '')
33
+ .replace(/--+/g, '-')
34
+ .trim();
35
+ };
36
+
37
+ const branchPrompt = getBranchPrompt(
38
+ provider,
39
+ branchSummary,
40
+ allBranches,
41
+ currentBranch,
42
+ commitMessage,
43
+ );
44
+ const allBranchesSet = new Set(allBranches);
45
+
46
+ while (!branchAccepted) {
47
+ const branchSpinner = ora('Generating branch name...').start();
48
+
49
+ const rawBranch = await generateAIResponse(provider, branchPrompt);
50
+
51
+ branchName = sanitizeBranchName(rawBranch);
52
+
53
+ // ensure branch prefix exists
54
+ if (!branchName.includes('/')) {
55
+ branchName = `feature/${branchName}`;
56
+ }
57
+
58
+ // avoid branch collision
59
+ let finalBranch = branchName;
60
+ let counter = 1;
61
+
62
+ while (allBranchesSet.has(finalBranch)) {
63
+ finalBranch = `${branchName}-${counter++}`;
64
+ }
65
+
66
+ branchName = finalBranch;
67
+
68
+ branchSpinner.succeed('Branch name generated:\\n');
69
+
70
+ console.log(chalk.magenta(branchName));
71
+ console.log('');
72
+
73
+ // auto accept if --yes
74
+ if (flags?.yes) {
75
+ branchAccepted = true;
76
+ break;
77
+ }
78
+
79
+ const result = await interactiveRefinePrompt('branch name', branchName);
80
+ if (result.cancel) return;
81
+
82
+ branchAccepted = result.accepted;
83
+ branchName = sanitizeBranchName(result.value);
84
+ }
85
+
86
+ // dry run support
87
+ if (flags?.['dry-run']) {
88
+ console.log(chalk.yellow('Dry run enabled. Branch not created.\n'));
89
+ return;
90
+ }
91
+
92
+ const branchProcessSpinner = ora('Applying branch name...').start();
93
+
94
+ await gitCheckoutNewBranch(branchName);
95
+
96
+ branchProcessSpinner.succeed(`Checked out to ${chalk.bold(branchName)}!`);
97
+ };
@@ -0,0 +1,108 @@
1
+ import chalk from 'chalk';
2
+ import { getProvider, log } from '../../utils/helper';
3
+ import { getFilesChanged, getStagedDiff, gitCommit, unstageFiles } from '../../utils/git';
4
+ import { expandDirectories } from '../../utils/files';
5
+ import { filterNoiseFiles } from '../../utils/parser';
6
+ import { analyzeDiff } from '../../analyzers/analyzer';
7
+ import { generateAIResponse, getCommitPrompt } from '../../utils/ai';
8
+ import { interactiveRefinePrompt } from '../../utils/inquirer';
9
+ import ora from 'ora';
10
+
11
+ let provider = getProvider();
12
+
13
+ export default async (flags: any = {}) => {
14
+ try {
15
+ if (!provider) {
16
+ throw new Error('No provider set');
17
+ }
18
+
19
+ const runProvider = flags.model ? flags.model : provider;
20
+
21
+ console.log('');
22
+ console.log(chalk.bold.bgBlue(' 🚀 AI-SHIP ') + chalk.bold.blue(' Commit Generator '));
23
+ console.log(chalk.dim('==================================='));
24
+ console.log('');
25
+
26
+ // 1️⃣ Get staged files
27
+ const scanSpinner = ora('Scanning staged files...').start();
28
+ const filesChanged = await getFilesChanged();
29
+
30
+ if (!filesChanged.length) {
31
+ scanSpinner.fail(chalk.yellow('No staged files detected.'));
32
+ return;
33
+ }
34
+
35
+ // 2️⃣ Expand directories
36
+ const expandedFiles = expandDirectories(filesChanged);
37
+
38
+ // 3️⃣ Extract filenames
39
+ let filenames = expandedFiles.map((f) => f.file);
40
+
41
+ // 4️⃣ Filter noise files
42
+ filenames = filterNoiseFiles(filenames);
43
+
44
+ scanSpinner.succeed(`Found ${filesChanged.length} staged file(s).`);
45
+
46
+ console.log(chalk.dim('Files changed:'));
47
+ filesChanged.forEach((f) => console.log(chalk.green(` + ${f.file}`)));
48
+ console.log('');
49
+
50
+ // 5️⃣ Analyze diff
51
+ const analyzeSpinner = ora('Analyzing file changes changes').start();
52
+
53
+ const diffs = await getStagedDiff(filenames);
54
+ const diffSummary = analyzeDiff(diffs);
55
+ analyzeSpinner.succeed('Analysis complete.\n');
56
+
57
+ // 6️⃣ Commit message generation
58
+ let commitMessage = '';
59
+ let commitAccepted = false;
60
+
61
+ while (!commitAccepted) {
62
+ const commitSpinner = ora('Generating commit message...').start();
63
+
64
+ const prompt = getCommitPrompt(runProvider, diffSummary);
65
+ commitMessage = await generateAIResponse(runProvider, prompt);
66
+
67
+ commitSpinner.succeed('Commit message generated:\\n');
68
+
69
+ console.log(chalk.cyan(commitMessage));
70
+ console.log('');
71
+
72
+ // dry-run → exit early
73
+ if (flags['dry-run']) {
74
+ console.log(chalk.yellow('Dry run enabled. Commit not executed.\\n'));
75
+ const unstageSpinner = ora('Unstaging files...').start();
76
+ await unstageFiles();
77
+ unstageSpinner.succeed('Files unstaged.');
78
+ return;
79
+ }
80
+
81
+ // skip prompt if --yes
82
+ if (flags['yes']) {
83
+ commitAccepted = true;
84
+ break;
85
+ }
86
+
87
+ const result = await interactiveRefinePrompt('commit message', commitMessage);
88
+ if (result.cancel) return;
89
+
90
+ commitAccepted = result.accepted;
91
+ commitMessage = result.value;
92
+ }
93
+
94
+ // 7️⃣ Commit
95
+ const commitSpinner = ora('Committing changes...').start();
96
+ await gitCommit(commitMessage);
97
+ commitSpinner.succeed('Changes successfully committed!\n');
98
+ return { commitMessage, diffSummary, runProvider };
99
+ } catch (err) {
100
+ console.log(err);
101
+ if ((err as Error).name === 'ExitPromptError') {
102
+ console.log(chalk.yellow('\nProcess aborted using user prompt.\n'));
103
+ return;
104
+ }
105
+
106
+ log(chalk.red(`We ran into an error: ${err}`));
107
+ }
108
+ };
@@ -0,0 +1,66 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { pRPrompt } from '../../utils/prompts';
4
+ import { getCurrentBranchName } from '../../utils/git';
5
+ import { createPR } from '../github/github';
6
+ import { log } from '../../utils/helper';
7
+ import { generateAIResponse } from '../../utils/ai';
8
+
9
+ export const startPR = async ({
10
+ diffSummary,
11
+ commitMessage,
12
+ provider,
13
+ flags,
14
+ }: {
15
+ diffSummary: any;
16
+ commitMessage: string;
17
+ provider: any;
18
+ flags: any;
19
+ }) => {
20
+ try {
21
+ const spinner = ora('Generating PR...').start();
22
+
23
+ const branchName = await getCurrentBranchName();
24
+
25
+ const prompt = pRPrompt({
26
+ commitMessage,
27
+ branchName,
28
+ summary: diffSummary,
29
+ });
30
+
31
+ const response = await generateAIResponse(provider, prompt);
32
+
33
+ spinner.succeed('PR content generated.\n');
34
+
35
+ // 🔥 Parse response
36
+ const titleMatch = response.match(/TITLE:\s*(.*)/);
37
+ const descriptionMatch = response.match(/DESCRIPTION:\s*([\s\S]*)/);
38
+
39
+ if (!titleMatch || !descriptionMatch) {
40
+ throw new Error('Failed to parse PR output');
41
+ }
42
+
43
+ const title = titleMatch[1].trim();
44
+ const body = descriptionMatch[1].trim();
45
+
46
+ const createSpinner = ora('Creating PR...').start();
47
+
48
+ const targetBranch = flags['target-branch'] || flags['base'] || 'main';
49
+
50
+ const prResult = await createPR({
51
+ title,
52
+ body,
53
+ head: branchName,
54
+ base: targetBranch,
55
+ });
56
+
57
+ createSpinner.succeed(chalk.green('PR created successfully!\n'));
58
+
59
+ if (prResult.stdout && prResult.stdout.trim()) {
60
+ console.log(chalk.blue(`🔗 PR Link: ${prResult.stdout.trim()}\n`));
61
+ }
62
+ } catch (err) {
63
+ console.log(err);
64
+ log(chalk.red(`PR generation failed: ${err}`));
65
+ }
66
+ };
@@ -0,0 +1,18 @@
1
+ import ora from 'ora';
2
+ import { push } from '../../utils/git';
3
+ import { interactivePushPrompt } from '../../utils/inquirer';
4
+
5
+ export const gitInteractivePush = async () => {
6
+ const result = await interactivePushPrompt();
7
+ if (result.accepted) {
8
+ const pushSpinner = ora('Pushing changes...').start();
9
+ await push();
10
+ pushSpinner.succeed('Changes pushed to remote repository.\n');
11
+ }
12
+ };
13
+
14
+ export const gitDirectPush = async () => {
15
+ const pushSpinner = ora('Pushing changes...').start();
16
+ await push();
17
+ pushSpinner.succeed('Changes pushed to remote repository.\n');
18
+ };