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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import startCommit from './startCommit';
|
|
4
|
+
import startCheckout from './startCheckout';
|
|
5
|
+
import { gitDirectPush, gitInteractivePush } from './startPush';
|
|
6
|
+
import { log } from '../../utils/helper';
|
|
7
|
+
import { getAllBranches, getCurrentBranchName, gitFetch } from '../../utils/git';
|
|
8
|
+
import { compressBranchSummary } from '../../analyzers/compressBranchSummary';
|
|
9
|
+
import { startPR } from './startPR';
|
|
10
|
+
import { interactivePRPrompt } from '../../utils/inquirer';
|
|
11
|
+
|
|
12
|
+
export default async (flags: any = {}) => {
|
|
13
|
+
try {
|
|
14
|
+
const commitResult = await startCommit(flags);
|
|
15
|
+
|
|
16
|
+
// If startCommit returns void/falsy, it means it was aborted, failed, or was a dry-run
|
|
17
|
+
if (!commitResult) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { diffSummary, commitMessage, runProvider } = commitResult;
|
|
22
|
+
|
|
23
|
+
if (flags['new-branch']) {
|
|
24
|
+
const branchAnalyzeSpinner = ora('Checking branches...').start();
|
|
25
|
+
await gitFetch();
|
|
26
|
+
const allBranches = await getAllBranches();
|
|
27
|
+
const currentBranch = await getCurrentBranchName();
|
|
28
|
+
|
|
29
|
+
const branchSummary = compressBranchSummary(diffSummary);
|
|
30
|
+
branchAnalyzeSpinner.succeed('Analysis complete.\\n');
|
|
31
|
+
|
|
32
|
+
await startCheckout({
|
|
33
|
+
branchSummary,
|
|
34
|
+
allBranches,
|
|
35
|
+
currentBranch,
|
|
36
|
+
commitMessage,
|
|
37
|
+
provider: runProvider,
|
|
38
|
+
flags,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (flags['push']) {
|
|
43
|
+
await gitDirectPush();
|
|
44
|
+
} else {
|
|
45
|
+
await gitInteractivePush();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (flags['pr']) {
|
|
49
|
+
await startPR({
|
|
50
|
+
diffSummary,
|
|
51
|
+
commitMessage,
|
|
52
|
+
provider: runProvider,
|
|
53
|
+
flags,
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
const prPromptResult = await interactivePRPrompt(flags['target-branch'] || 'main');
|
|
57
|
+
if (prPromptResult.accepted) {
|
|
58
|
+
flags['target-branch'] = prPromptResult.base;
|
|
59
|
+
await startPR({
|
|
60
|
+
diffSummary,
|
|
61
|
+
commitMessage,
|
|
62
|
+
provider: runProvider,
|
|
63
|
+
flags,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.log(err);
|
|
69
|
+
log(chalk.red(`Workflow encountered an error: ${err}`));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export const createPR = async ({
|
|
6
|
+
title,
|
|
7
|
+
body,
|
|
8
|
+
base = 'main',
|
|
9
|
+
head,
|
|
10
|
+
}: {
|
|
11
|
+
title: string;
|
|
12
|
+
body: string;
|
|
13
|
+
base?: string;
|
|
14
|
+
head: string;
|
|
15
|
+
}): Promise<{ stdout: string; stderr: string }> => {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
try {
|
|
18
|
+
// 🔥 Create temp file for PR body
|
|
19
|
+
const tempFile = path.join(process.cwd(), '.ai-ship-pr.md');
|
|
20
|
+
fs.writeFileSync(tempFile, body);
|
|
21
|
+
|
|
22
|
+
// 🔥 Safe args (NO shell)
|
|
23
|
+
const args = [
|
|
24
|
+
'pr',
|
|
25
|
+
'create',
|
|
26
|
+
'--title',
|
|
27
|
+
title,
|
|
28
|
+
'--body-file',
|
|
29
|
+
tempFile,
|
|
30
|
+
'--base',
|
|
31
|
+
base,
|
|
32
|
+
'--head',
|
|
33
|
+
head,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const child = spawn('gh', args); // ✅ no shell
|
|
37
|
+
|
|
38
|
+
let stdout = '';
|
|
39
|
+
let stderr = '';
|
|
40
|
+
|
|
41
|
+
child.stdout?.on('data', (data) => {
|
|
42
|
+
stdout += data.toString();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.stderr?.on('data', (data) => {
|
|
46
|
+
stderr += data.toString();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
child.on('close', (code) => {
|
|
50
|
+
// 🧹 cleanup temp file
|
|
51
|
+
try {
|
|
52
|
+
fs.unlinkSync(tempFile);
|
|
53
|
+
} catch {}
|
|
54
|
+
|
|
55
|
+
if (code === 0) {
|
|
56
|
+
resolve({ stdout, stderr });
|
|
57
|
+
} else {
|
|
58
|
+
const error = new Error(`Command failed: gh pr create\n${stderr}`);
|
|
59
|
+
(error as any).stdout = stdout;
|
|
60
|
+
(error as any).stderr = stderr;
|
|
61
|
+
reject(error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
child.on('error', (err) => {
|
|
66
|
+
reject(err);
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
reject(err);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
|
File without changes
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import minimist from 'minimist';
|
|
4
|
+
import runConfig from './commands/config';
|
|
5
|
+
import runCommit from './commands/commit';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const startCommandExecution = async () => {
|
|
9
|
+
const args = minimist(process.argv.slice(2));
|
|
10
|
+
|
|
11
|
+
const command = args._[0];
|
|
12
|
+
|
|
13
|
+
const subArgs = {
|
|
14
|
+
...args,
|
|
15
|
+
_: args._.slice(1),
|
|
16
|
+
};
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(chalk.bold.bgBlue(' 🚀 AI-SHIP ') + chalk.bold.blue(' Commit Generator '));
|
|
19
|
+
console.log(chalk.dim('==================================='));
|
|
20
|
+
console.log('');
|
|
21
|
+
|
|
22
|
+
switch (command) {
|
|
23
|
+
case 'commit':
|
|
24
|
+
const { _, ...flags } = subArgs;
|
|
25
|
+
await runCommit(_, flags);
|
|
26
|
+
break;
|
|
27
|
+
|
|
28
|
+
case 'config':
|
|
29
|
+
await runConfig(subArgs);
|
|
30
|
+
break;
|
|
31
|
+
|
|
32
|
+
// case 'pr':
|
|
33
|
+
// await run
|
|
34
|
+
default:
|
|
35
|
+
console.log('Unknown command');
|
|
36
|
+
}
|
|
37
|
+
console.log('');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
startCommandExecution();
|
package/src/utils/ai.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { generateWithGemma } from '../ai/ollama';
|
|
2
|
+
import { generateWithGemini } from '../ai/gemini';
|
|
3
|
+
import {
|
|
4
|
+
buildBranchPrompt,
|
|
5
|
+
buildBranchPromptGemma,
|
|
6
|
+
buildCommitPrompt,
|
|
7
|
+
buildCommitPromptGemma,
|
|
8
|
+
} from './prompts';
|
|
9
|
+
|
|
10
|
+
export const generateAIResponse = async (provider: string, prompt: string) => {
|
|
11
|
+
return provider === 'local' ? await generateWithGemma(prompt) : await generateWithGemini(prompt);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const getCommitPrompt = (provider: string, diffSummary: any[]) => {
|
|
15
|
+
return provider === 'local'
|
|
16
|
+
? buildCommitPromptGemma(diffSummary)
|
|
17
|
+
: buildCommitPrompt(diffSummary);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getBranchPrompt = (
|
|
21
|
+
provider: string,
|
|
22
|
+
branchSummary: any[],
|
|
23
|
+
allBranches: string[],
|
|
24
|
+
currentBranch: string,
|
|
25
|
+
commitMessage: string,
|
|
26
|
+
) => {
|
|
27
|
+
return provider === 'local'
|
|
28
|
+
? buildBranchPromptGemma(branchSummary, allBranches, currentBranch, commitMessage)
|
|
29
|
+
: buildBranchPrompt(branchSummary, allBranches, currentBranch, commitMessage);
|
|
30
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export default async function asyncExecuter(
|
|
4
|
+
command: string,
|
|
5
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(command, { shell: true });
|
|
8
|
+
|
|
9
|
+
let stdout = '';
|
|
10
|
+
let stderr = '';
|
|
11
|
+
|
|
12
|
+
if (child.stdout) {
|
|
13
|
+
child.stdout.on('data', (data) => {
|
|
14
|
+
stdout += data.toString();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (child.stderr) {
|
|
19
|
+
child.stderr.on('data', (data) => {
|
|
20
|
+
stderr += data.toString();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
child.on('close', (code) => {
|
|
25
|
+
if (code === 0) {
|
|
26
|
+
resolve({ stdout, stderr });
|
|
27
|
+
} else {
|
|
28
|
+
const error = new Error(`Command failed: ${command}\n${stderr}`);
|
|
29
|
+
(error as any).stdout = stdout;
|
|
30
|
+
(error as any).stderr = stderr;
|
|
31
|
+
reject(error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on('error', (err) => {
|
|
36
|
+
reject(err);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
type FileChange = {
|
|
5
|
+
status: string;
|
|
6
|
+
file: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const expandDirectories = (files: FileChange[]): FileChange[] => {
|
|
10
|
+
const expanded: FileChange[] = [];
|
|
11
|
+
|
|
12
|
+
files.forEach(({ status, file }) => {
|
|
13
|
+
if (fs.existsSync(file) && fs.lstatSync(file).isDirectory()) {
|
|
14
|
+
const subFiles = fs.readdirSync(file);
|
|
15
|
+
|
|
16
|
+
subFiles.forEach((subFile) => {
|
|
17
|
+
const fullPath = path.join(file, subFile);
|
|
18
|
+
|
|
19
|
+
expanded.push({
|
|
20
|
+
status,
|
|
21
|
+
file: fullPath,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
} else {
|
|
25
|
+
expanded.push({ status, file });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return expanded;
|
|
30
|
+
};
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import asyncExecuter from './asyncExecuter';
|
|
3
|
+
|
|
4
|
+
export const getGitRoot = async () => {
|
|
5
|
+
const { stdout } = await asyncExecuter('git rev-parse --show-toplevel');
|
|
6
|
+
return stdout.trim();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getFilesChanged = async () => {
|
|
10
|
+
const { stdout: filesChanged } = await asyncExecuter('git diff --cached --name-status');
|
|
11
|
+
|
|
12
|
+
if (!filesChanged.trim()) return [];
|
|
13
|
+
|
|
14
|
+
const root = await getGitRoot();
|
|
15
|
+
|
|
16
|
+
return filesChanged
|
|
17
|
+
.trim()
|
|
18
|
+
.split('\n')
|
|
19
|
+
.map((line) => {
|
|
20
|
+
const parts = line.split('\t');
|
|
21
|
+
const status = parts[0]?.trim();
|
|
22
|
+
|
|
23
|
+
let filePath: string | undefined;
|
|
24
|
+
|
|
25
|
+
if (status?.startsWith('R')) {
|
|
26
|
+
// Rename → take NEW path
|
|
27
|
+
filePath = parts[2];
|
|
28
|
+
} else {
|
|
29
|
+
filePath = parts[1];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!filePath) {
|
|
33
|
+
throw new Error(`Invalid git diff line: ${line}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
status,
|
|
38
|
+
file: path.resolve(root, filePath.trim().replace(/^[.\\/]+/, '')),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const stageAll = async () => {
|
|
44
|
+
await asyncExecuter('git add -A');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const stageFiles = async (files: string[]) => {
|
|
48
|
+
if (!files || files.length === 0) return;
|
|
49
|
+
|
|
50
|
+
const filesString = files.map((f) => `"${f}"`).join(' ');
|
|
51
|
+
await asyncExecuter(`git add ${filesString}`);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const getStagedDiff = async (files: string[]) => {
|
|
55
|
+
const filesString = files.map((f) => `"${f}"`).join(' ');
|
|
56
|
+
const { stdout } = await asyncExecuter(`git diff --cached -- ${filesString}`);
|
|
57
|
+
return stdout;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const gitFetch = async () => {
|
|
61
|
+
return await asyncExecuter('git fetch --all');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const getAllBranches = async () => {
|
|
65
|
+
const { stdout: raw } = await asyncExecuter('git branch -a');
|
|
66
|
+
|
|
67
|
+
const branches = raw
|
|
68
|
+
.split('\\n')
|
|
69
|
+
.map((b) => b.replace('*', '').trim())
|
|
70
|
+
.map((b) => b.replace('remotes/origin/', ''))
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
|
|
73
|
+
return [...new Set(branches)];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const getCurrentBranchName = async () => {
|
|
77
|
+
return (await asyncExecuter('git branch --show-current')).stdout.trim();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const gitCommit = async (message: string) => {
|
|
81
|
+
// Use child_process safely by escaping quotes properly, or just use asyncExecuter
|
|
82
|
+
const escapedMessage = message.replace(/(["'$`\\])/g, '\\\\$1');
|
|
83
|
+
await asyncExecuter(`git commit -m "${escapedMessage}"`);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const gitRenameBranch = async (branchName: string) => {
|
|
87
|
+
await asyncExecuter(`git branch -m "${branchName}"`);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const gitCheckoutNewBranch = async (branchName: string) => {
|
|
91
|
+
await asyncExecuter(`git checkout -b ${branchName}`);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const unstageFiles = async () => {
|
|
95
|
+
await asyncExecuter(`git reset`);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const push = async () => {
|
|
99
|
+
try {
|
|
100
|
+
await asyncExecuter(`git push`);
|
|
101
|
+
} catch {
|
|
102
|
+
const { stdout } = await asyncExecuter(`git rev-parse --abbrev-ref HEAD`);
|
|
103
|
+
|
|
104
|
+
const branchName = stdout.trim();
|
|
105
|
+
|
|
106
|
+
await asyncExecuter(`git push --set-upstream origin ${branchName}`);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const createGithubPR = async ({
|
|
2
|
+
title,
|
|
3
|
+
body,
|
|
4
|
+
base = 'main',
|
|
5
|
+
head,
|
|
6
|
+
}: {
|
|
7
|
+
title: string;
|
|
8
|
+
body: string;
|
|
9
|
+
base?: string;
|
|
10
|
+
head: string;
|
|
11
|
+
}) => {
|
|
12
|
+
try {
|
|
13
|
+
await createGithubPR({ title, body, base, head });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error('Failed to create GitHub PR.');
|
|
16
|
+
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
type AIShipConfig = {
|
|
6
|
+
provider: 'local' | 'cloud';
|
|
7
|
+
model: string;
|
|
8
|
+
localEndpoint?: string;
|
|
9
|
+
geminiApiKey?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ALLOWED_KEYS = ['provider', 'model', 'localEndpoint', 'geminiApiKey'];
|
|
13
|
+
|
|
14
|
+
export const log = (data: any) => console.log(data);
|
|
15
|
+
|
|
16
|
+
// config dir for storing api key
|
|
17
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.ai-ship');
|
|
18
|
+
// config file for storing api key
|
|
19
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
20
|
+
export const deleteConfigKey = (key: string): boolean => {
|
|
21
|
+
if (!fs.existsSync(CONFIG_FILE)) return false;
|
|
22
|
+
try {
|
|
23
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
24
|
+
const config = raw ? JSON.parse(raw) : {};
|
|
25
|
+
if (config[key]) {
|
|
26
|
+
delete config[key];
|
|
27
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getCurrentConfig = (key: string = 'all') => {
|
|
37
|
+
if (!fs.existsSync(CONFIG_FILE)) return null;
|
|
38
|
+
|
|
39
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
40
|
+
|
|
41
|
+
if (key === 'all') return config;
|
|
42
|
+
|
|
43
|
+
return config[key];
|
|
44
|
+
};
|
|
45
|
+
export const verboseConfig = (config: any) => {
|
|
46
|
+
const { provider, model, localEndpoint, geminiApiKey } = config;
|
|
47
|
+
|
|
48
|
+
const maskedKey = geminiApiKey
|
|
49
|
+
? geminiApiKey.slice(0, 4) + '...' + geminiApiKey.slice(-4)
|
|
50
|
+
: 'not configured';
|
|
51
|
+
|
|
52
|
+
console.log(chalk.bold('\nAI-Ship Configuration\n'));
|
|
53
|
+
|
|
54
|
+
console.log(chalk.yellow('Provider'));
|
|
55
|
+
console.log(' Current:', provider || 'not set');
|
|
56
|
+
console.log(' Description: Determines where AI runs (local via Ollama or cloud API)\n');
|
|
57
|
+
|
|
58
|
+
console.log(chalk.yellow('Model'));
|
|
59
|
+
console.log(' Current:', model || 'not set');
|
|
60
|
+
console.log(' Description: AI model used for commit and branch generation\n');
|
|
61
|
+
|
|
62
|
+
console.log(chalk.yellow('Local Model Settings'));
|
|
63
|
+
console.log(' Endpoint:', localEndpoint || 'not configured');
|
|
64
|
+
console.log(' Description: URL of the local model server (e.g. http://127.0.0.1:11434)\n');
|
|
65
|
+
|
|
66
|
+
console.log(chalk.yellow('Cloud Model Settings'));
|
|
67
|
+
console.log(' Gemini API Key:', maskedKey);
|
|
68
|
+
console.log(' Description: API key used when provider is set to cloud\n');
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const jsonConfig = (config: any) => {
|
|
72
|
+
const safeConfig = {
|
|
73
|
+
provider: config.provider || null,
|
|
74
|
+
model: config.model || null,
|
|
75
|
+
localEndpoint: config.localEndpoint || null,
|
|
76
|
+
geminiApiKey: config.geminiApiKey
|
|
77
|
+
? config.geminiApiKey.slice(0, 4) + '...' + config.geminiApiKey.slice(-4)
|
|
78
|
+
: null,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
console.log(JSON.stringify(safeConfig, null, 2));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const validateValue = (key: string, value: string) => {
|
|
85
|
+
switch (key) {
|
|
86
|
+
case 'provider':
|
|
87
|
+
if (!['local', 'cloud'].includes(value)) {
|
|
88
|
+
throw new Error("provider must be 'local' or 'cloud'");
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case 'model':
|
|
93
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
94
|
+
throw new Error('model must be a non-empty string');
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'localEndpoint':
|
|
99
|
+
try {
|
|
100
|
+
new URL(value);
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Error('localEndpoint must be a valid URL');
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'geminiApiKey':
|
|
107
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
108
|
+
throw new Error('geminiApiKey must be a valid string');
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const saveValueToConfig = (key: string, value: string) => {
|
|
115
|
+
try {
|
|
116
|
+
if (!ALLOWED_KEYS.includes(key as any)) {
|
|
117
|
+
throw new Error(`Invalid config key: ${key}. Allowed keys: ${ALLOWED_KEYS.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
validateValue(key, value);
|
|
121
|
+
|
|
122
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
123
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let config: Record<string, any> = {};
|
|
127
|
+
|
|
128
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
129
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
130
|
+
config = raw ? JSON.parse(raw) : {};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
config[key] = value;
|
|
134
|
+
|
|
135
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
136
|
+
|
|
137
|
+
console.log(chalk.green(`✔ Config updated: ${chalk.bold.green(key)}`));
|
|
138
|
+
} catch (err: any) {
|
|
139
|
+
console.error(chalk.red(`❌ Failed to save config: ${chalk.bold.red(err.message)}`));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const getProvider = () => {
|
|
144
|
+
return getCurrentConfig('provider');
|
|
145
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
|
|
3
|
+
export const askApiKey = (): Promise<string> => {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
rl.question('Enter your Gemini API key: ', (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer.trim());
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import { Input } from 'enquirer';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export type AIGenerationResult = {
|
|
7
|
+
accepted: boolean;
|
|
8
|
+
value: string;
|
|
9
|
+
cancel: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type pushPromptResult = {
|
|
13
|
+
accepted: boolean;
|
|
14
|
+
cancel: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const interactiveRefinePrompt = async (
|
|
18
|
+
itemType: string,
|
|
19
|
+
initialValue: string,
|
|
20
|
+
): Promise<AIGenerationResult> => {
|
|
21
|
+
const { action } = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: 'list',
|
|
24
|
+
name: 'action',
|
|
25
|
+
message: `What would you like to do with this ${itemType}?`,
|
|
26
|
+
choices: ['Continue', 'Edit', 'Retry', 'Cancel'],
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
if (action === 'Continue') {
|
|
31
|
+
return { accepted: true, value: initialValue, cancel: false };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (action === 'Edit') {
|
|
35
|
+
const promptInput = new Input({
|
|
36
|
+
message: `Edit your ${itemType}:`,
|
|
37
|
+
initial: initialValue,
|
|
38
|
+
});
|
|
39
|
+
const editedValue = (await promptInput.run()) as string;
|
|
40
|
+
return { accepted: true, value: editedValue, cancel: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (action === 'Retry') {
|
|
44
|
+
console.log(chalk.yellow(`Retrying ${itemType}...\\n`));
|
|
45
|
+
return { accepted: false, value: initialValue, cancel: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Cancel
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.yellow(`${itemType.charAt(0).toUpperCase() + itemType.slice(1)} cancelled.\\n`),
|
|
51
|
+
);
|
|
52
|
+
return { accepted: false, value: initialValue, cancel: true };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const interactivePushPrompt = async (): Promise<pushPromptResult> => {
|
|
56
|
+
const { action } = await inquirer.prompt([
|
|
57
|
+
{
|
|
58
|
+
type: 'list',
|
|
59
|
+
name: 'action',
|
|
60
|
+
message: `Do you want to push your committed changes to the remote repository?`,
|
|
61
|
+
choices: ['Yes', 'No'],
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
if (action === 'Yes') {
|
|
66
|
+
return { accepted: true, cancel: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(chalk.yellow(`Push cancelled\n`));
|
|
70
|
+
return { accepted: false, cancel: true };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const interactivePRPrompt = async (
|
|
74
|
+
defaultBase: string = 'main',
|
|
75
|
+
): Promise<{ accepted: boolean; cancel: boolean; base: string }> => {
|
|
76
|
+
const { action } = await inquirer.prompt([
|
|
77
|
+
{
|
|
78
|
+
type: 'list',
|
|
79
|
+
name: 'action',
|
|
80
|
+
message: `Do you want to create a pull request?`,
|
|
81
|
+
choices: ['Yes', 'No'],
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
if (action === 'Yes') {
|
|
86
|
+
const { base } = await inquirer.prompt([
|
|
87
|
+
{
|
|
88
|
+
type: 'input',
|
|
89
|
+
name: 'base',
|
|
90
|
+
message: `Which branch do you want to target?`,
|
|
91
|
+
default: defaultBase,
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
return { accepted: true, cancel: false, base };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(chalk.yellow(`PR creation cancelled\n`));
|
|
98
|
+
return { accepted: false, cancel: true, base: defaultBase };
|
|
99
|
+
};
|