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.
- package/.prettierignore +3 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/ai/gemini.js +34 -0
- package/dist/ai/ollama.js +36 -0
- package/dist/analyzers/analyzer.js +93 -0
- package/dist/analyzers/compressBranchSummary.js +12 -0
- package/dist/analyzers/configAnalyzer.js +18 -0
- package/dist/analyzers/detectSignals.js +18 -0
- package/dist/analyzers/markupAnalyzer.js +26 -0
- package/dist/commands/commit/customAdd.js +1 -0
- package/dist/commands/commit/startCommit.js +155 -0
- package/dist/commands/commit.js +21 -0
- package/dist/commands/config/deleteKey.js +15 -0
- package/dist/commands/config.js +65 -0
- package/dist/commands/git/startCheckout.js +62 -0
- package/dist/commands/git/startCommit.js +91 -0
- package/dist/commands/git/startPR.js +51 -0
- package/dist/commands/git/startPush.js +24 -0
- package/dist/commands/git/startWorkflow.js +71 -0
- package/dist/commands/github/github.js +63 -0
- package/dist/commands/pr.js +1 -0
- package/dist/index.js +38 -0
- package/dist/utils/ai.js +22 -0
- package/dist/utils/asyncExecuter.js +35 -0
- package/dist/utils/files.js +28 -0
- package/dist/utils/git.js +106 -0
- package/dist/utils/github.js +13 -0
- package/dist/utils/helper.js +130 -0
- package/dist/utils/inputs.js +20 -0
- package/dist/utils/inquirer.js +79 -0
- package/dist/utils/parser.js +54 -0
- package/dist/utils/print.js +25 -0
- package/dist/utils/prompts.js +206 -0
- package/dist/utils/runCommit.js +17 -0
- package/dist/utils/runConfig.js +35 -0
- package/docs/commands.md +106 -0
- package/package.json +44 -0
- package/src/ai/gemini.ts +27 -0
- package/src/ai/ollama.ts +38 -0
- package/src/analyzers/analyzer.ts +117 -0
- package/src/analyzers/compressBranchSummary.ts +16 -0
- package/src/analyzers/configAnalyzer.ts +17 -0
- package/src/analyzers/detectSignals.ts +13 -0
- package/src/analyzers/markupAnalyzer.ts +25 -0
- package/src/commands/commit.ts +18 -0
- package/src/commands/config.ts +73 -0
- package/src/commands/git/startCheckout.ts +97 -0
- package/src/commands/git/startCommit.ts +108 -0
- package/src/commands/git/startPR.ts +66 -0
- package/src/commands/git/startPush.ts +18 -0
- package/src/commands/git/startWorkflow.ts +71 -0
- package/src/commands/github/github.ts +72 -0
- package/src/commands/pr.ts +0 -0
- package/src/index.ts +40 -0
- package/src/utils/ai.ts +30 -0
- package/src/utils/asyncExecuter.ts +39 -0
- package/src/utils/files.ts +30 -0
- package/src/utils/git.ts +108 -0
- package/src/utils/github.ts +19 -0
- package/src/utils/helper.ts +145 -0
- package/src/utils/inputs.ts +15 -0
- package/src/utils/inquirer.ts +99 -0
- package/src/utils/parser.ts +58 -0
- package/src/utils/print.ts +16 -0
- package/src/utils/prompts.ts +234 -0
- 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
|
+
}
|
package/src/ai/gemini.ts
ADDED
|
@@ -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
|
+
};
|
package/src/ai/ollama.ts
ADDED
|
@@ -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
|
+
};
|