@ksw8954/git-ai-commit 1.1.7 → 1.2.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.
- package/.github/workflows/publish.yml +36 -0
- package/CHANGELOG.md +21 -0
- package/README.md +25 -4
- package/dist/commands/ai.d.ts +7 -1
- package/dist/commands/ai.d.ts.map +1 -1
- package/dist/commands/ai.js +144 -22
- package/dist/commands/ai.js.map +1 -1
- package/dist/commands/commit.d.ts +1 -0
- package/dist/commands/commit.d.ts.map +1 -1
- package/dist/commands/commit.js +37 -34
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +15 -5
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/config.d.ts +7 -2
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +37 -15
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/configCommand.d.ts +5 -1
- package/dist/commands/configCommand.d.ts.map +1 -1
- package/dist/commands/configCommand.js +30 -3
- package/dist/commands/configCommand.js.map +1 -1
- package/dist/commands/git.js +3 -3
- package/dist/commands/hookCommand.d.ts +14 -0
- package/dist/commands/hookCommand.d.ts.map +1 -0
- package/dist/commands/hookCommand.js +180 -0
- package/dist/commands/hookCommand.js.map +1 -0
- package/dist/commands/prCommand.d.ts.map +1 -1
- package/dist/commands/prCommand.js +3 -1
- package/dist/commands/prCommand.js.map +1 -1
- package/dist/commands/tag.d.ts.map +1 -1
- package/dist/commands/tag.js +12 -2
- package/dist/commands/tag.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/ai.test.ts +486 -7
- package/src/__tests__/commitCommand.test.ts +111 -0
- package/src/__tests__/config.test.ts +24 -6
- package/src/__tests__/git.test.ts +421 -98
- package/src/__tests__/preCommit.test.ts +19 -0
- package/src/__tests__/tagCommand.test.ts +510 -17
- package/src/commands/ai.ts +175 -24
- package/src/commands/commit.ts +40 -34
- package/src/commands/completion.ts +15 -5
- package/src/commands/config.ts +46 -23
- package/src/commands/configCommand.ts +41 -8
- package/src/commands/git.ts +3 -3
- package/src/commands/hookCommand.ts +193 -0
- package/src/commands/prCommand.ts +3 -1
- package/src/commands/tag.ts +13 -2
- package/src/index.ts +3 -0
- package/src/schema/config.schema.json +72 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { ConfigService, SupportedLanguage } from './config';
|
|
2
|
+
import { ConfigService, SupportedLanguage, AIMode } from './config';
|
|
3
3
|
|
|
4
4
|
export interface ConfigOptions {
|
|
5
5
|
show?: boolean;
|
|
@@ -10,7 +10,10 @@ export interface ConfigOptions {
|
|
|
10
10
|
model?: string;
|
|
11
11
|
fallbackModel?: string;
|
|
12
12
|
reasoningEffort?: string;
|
|
13
|
-
mode?:
|
|
13
|
+
mode?: AIMode;
|
|
14
|
+
coAuthor?: string;
|
|
15
|
+
noCoAuthor?: boolean;
|
|
16
|
+
maxTokens?: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export class ConfigCommand {
|
|
@@ -28,7 +31,10 @@ export class ConfigCommand {
|
|
|
28
31
|
.option('-m, --model <model>', 'Persist default AI model')
|
|
29
32
|
.option('--fallback-model <model>', 'Persist fallback model for rate limit (429) retry')
|
|
30
33
|
.option('--reasoning-effort <level>', 'Thinking effort for reasoning models (minimal | low | medium | high)')
|
|
31
|
-
.option('--mode <mode>', 'Persist AI mode (custom | openai)')
|
|
34
|
+
.option('--mode <mode>', 'Persist AI mode (custom | openai | gemini)')
|
|
35
|
+
.option('--co-author <value>', 'Set co-author for commits (e.g. "Name <email>")')
|
|
36
|
+
.option('--no-co-author', 'Disable co-author')
|
|
37
|
+
.option('--max-tokens <number>', 'Persist max completion tokens for AI responses')
|
|
32
38
|
.action(this.handleConfig.bind(this));
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -42,14 +48,14 @@ export class ConfigCommand {
|
|
|
42
48
|
return normalized;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
private validateMode(mode: string):
|
|
51
|
+
private validateMode(mode: string): AIMode {
|
|
46
52
|
const normalized = mode?.toLowerCase();
|
|
47
|
-
if (normalized !== 'custom' && normalized !== 'openai') {
|
|
48
|
-
console.error('Mode must be
|
|
53
|
+
if (normalized !== 'custom' && normalized !== 'openai' && normalized !== 'gemini') {
|
|
54
|
+
console.error('Mode must be one of: "custom", "openai", "gemini".');
|
|
49
55
|
process.exit(1);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
return normalized;
|
|
58
|
+
return normalized as AIMode;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
private sanitizeStringValue(value?: string): string | undefined {
|
|
@@ -68,9 +74,11 @@ export class ConfigCommand {
|
|
|
68
74
|
model?: string;
|
|
69
75
|
fallbackModel?: string;
|
|
70
76
|
reasoningEffort?: string;
|
|
71
|
-
mode?:
|
|
77
|
+
mode?: AIMode;
|
|
72
78
|
language?: SupportedLanguage;
|
|
73
79
|
autoPush?: boolean;
|
|
80
|
+
coAuthor?: string | false;
|
|
81
|
+
maxCompletionTokens?: number;
|
|
74
82
|
} = {};
|
|
75
83
|
|
|
76
84
|
if (options.language) {
|
|
@@ -110,6 +118,26 @@ export class ConfigCommand {
|
|
|
110
118
|
updates.mode = this.validateMode(options.mode);
|
|
111
119
|
}
|
|
112
120
|
|
|
121
|
+
|
|
122
|
+
if (options.noCoAuthor) {
|
|
123
|
+
updates.coAuthor = false;
|
|
124
|
+
} else if (options.coAuthor !== undefined) {
|
|
125
|
+
const coAuthor = this.sanitizeStringValue(options.coAuthor);
|
|
126
|
+
if (coAuthor && !/.+<.+@.+>/.test(coAuthor)) {
|
|
127
|
+
console.error('Co-author must be in "Name <email>" format.');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
updates.coAuthor = coAuthor;
|
|
131
|
+
}
|
|
132
|
+
if (options.maxTokens !== undefined) {
|
|
133
|
+
const parsed = parseInt(options.maxTokens, 10);
|
|
134
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
135
|
+
console.error('Max tokens must be a positive number.');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
updates.maxCompletionTokens = parsed;
|
|
139
|
+
}
|
|
140
|
+
|
|
113
141
|
const hasUpdates = Object.keys(updates).length > 0;
|
|
114
142
|
|
|
115
143
|
if (!options.show && !hasUpdates) {
|
|
@@ -120,8 +148,11 @@ export class ConfigCommand {
|
|
|
120
148
|
console.log(' git-ai-commit config -k sk-xxx # Persist API key securely on disk');
|
|
121
149
|
console.log(' git-ai-commit config -b https://api.test # Persist custom API base URL');
|
|
122
150
|
console.log(' git-ai-commit config --mode openai # Use OpenAI-compatible environment defaults');
|
|
151
|
+
console.log(' git-ai-commit config --mode gemini # Use Gemini native SDK (GEMINI_API_KEY)');
|
|
123
152
|
console.log(' git-ai-commit config --model gpt-4o-mini # Persist preferred AI model');
|
|
124
153
|
console.log(' git-ai-commit config --fallback-model glm-4-flash # Fallback model for 429 retry');
|
|
154
|
+
console.log(' git-ai-commit config --co-author "Name <email>" # Set co-author for commits');
|
|
155
|
+
console.log(' git-ai-commit config --max-tokens 2000 # Set max completion tokens for AI');
|
|
125
156
|
return;
|
|
126
157
|
}
|
|
127
158
|
|
|
@@ -148,6 +179,8 @@ export class ConfigCommand {
|
|
|
148
179
|
console.log(`Fallback Model: ${config.fallbackModel || 'Not set'}`);
|
|
149
180
|
console.log(`Reasoning Effort: ${config.reasoningEffort || 'Not set (model default)'}`);
|
|
150
181
|
console.log(`Mode: ${config.mode || 'custom (default)'}`);
|
|
182
|
+
console.log(`Co-author: ${config.coAuthor === false ? 'disabled' : config.coAuthor}`);
|
|
183
|
+
console.log(`Max Completion Tokens: ${config.maxCompletionTokens || 'Not set (using per-command defaults)'}`);
|
|
151
184
|
} catch (error) {
|
|
152
185
|
console.error('Error reading configuration:', error instanceof Error ? error.message : error);
|
|
153
186
|
process.exit(1);
|
package/src/commands/git.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { promisify } from 'util';
|
|
|
4
4
|
const execAsync = promisify(exec);
|
|
5
5
|
const execFileAsync = promisify(execFile);
|
|
6
6
|
const GIT_DIFF_MAX_BUFFER = 50 * 1024 * 1024;
|
|
7
|
-
const MAX_DIFF_TOKENS =
|
|
7
|
+
const MAX_DIFF_TOKENS = 25000;
|
|
8
8
|
const APPROX_CHARS_PER_TOKEN = 4;
|
|
9
9
|
const MAX_DIFF_CHARS = MAX_DIFF_TOKENS * APPROX_CHARS_PER_TOKEN;
|
|
10
|
-
const MAX_FILE_LINES =
|
|
11
|
-
const MAX_NEW_FILE_LINES =
|
|
10
|
+
const MAX_FILE_LINES = 200;
|
|
11
|
+
const MAX_NEW_FILE_LINES = 100;
|
|
12
12
|
|
|
13
13
|
const splitDiffSections = (diff: string): string[] => {
|
|
14
14
|
const lines = diff.split('\n');
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
const HOOK_NAME = 'prepare-commit-msg';
|
|
7
|
+
const HOOK_SIGNATURE = '# installed by git-ai-commit';
|
|
8
|
+
|
|
9
|
+
const HOOK_SCRIPT = `#!/bin/sh
|
|
10
|
+
${HOOK_SIGNATURE}
|
|
11
|
+
|
|
12
|
+
COMMIT_MSG_FILE="$1"
|
|
13
|
+
COMMIT_SOURCE="$2"
|
|
14
|
+
|
|
15
|
+
# Skip if message was provided via -m, merge, squash, or amend
|
|
16
|
+
if [ -n "$COMMIT_SOURCE" ]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Skip if no staged changes
|
|
21
|
+
if ! git diff --cached --quiet --exit-code 2>/dev/null; then
|
|
22
|
+
# Generate AI commit message
|
|
23
|
+
AI_MSG=$(git-ai-commit commit --message-only 2>/dev/null)
|
|
24
|
+
if [ $? -eq 0 ] && [ -n "$AI_MSG" ]; then
|
|
25
|
+
# Prepend AI message, keep original commented lines below
|
|
26
|
+
ORIGINAL=$(cat "$COMMIT_MSG_FILE")
|
|
27
|
+
printf '%s\n\n%s' "$AI_MSG" "$ORIGINAL" > "$COMMIT_MSG_FILE"
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
export class HookCommand {
|
|
33
|
+
private program: Command;
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
this.program = new Command('hook')
|
|
37
|
+
.description('Manage git-ai-commit prepare-commit-msg hook');
|
|
38
|
+
|
|
39
|
+
this.program
|
|
40
|
+
.command('install')
|
|
41
|
+
.description('Install prepare-commit-msg hook in the current repository')
|
|
42
|
+
.action(this.handleInstall.bind(this));
|
|
43
|
+
|
|
44
|
+
this.program
|
|
45
|
+
.command('uninstall')
|
|
46
|
+
.description('Remove prepare-commit-msg hook from the current repository')
|
|
47
|
+
.action(this.handleUninstall.bind(this));
|
|
48
|
+
|
|
49
|
+
this.program
|
|
50
|
+
.command('status')
|
|
51
|
+
.description('Show hook installation status')
|
|
52
|
+
.action(this.handleStatus.bind(this));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private getGitRoot(): string | null {
|
|
56
|
+
try {
|
|
57
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
}).trim();
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private getHooksDir(): string | null {
|
|
66
|
+
try {
|
|
67
|
+
const hooksPath = execFileSync('git', ['rev-parse', '--git-path', 'hooks'], {
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
}).trim();
|
|
70
|
+
// git rev-parse --git-path returns relative path; resolve it
|
|
71
|
+
const gitRoot = this.getGitRoot();
|
|
72
|
+
if (!gitRoot) return null;
|
|
73
|
+
return path.resolve(gitRoot, hooksPath);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private getHookPath(): string | null {
|
|
80
|
+
const hooksDir = this.getHooksDir();
|
|
81
|
+
if (!hooksDir) return null;
|
|
82
|
+
return path.join(hooksDir, HOOK_NAME);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private isOurHook(hookPath: string): boolean {
|
|
86
|
+
try {
|
|
87
|
+
const content = fs.readFileSync(hookPath, 'utf-8');
|
|
88
|
+
return content.includes(HOOK_SIGNATURE);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private handleInstall(): void {
|
|
95
|
+
const gitRoot = this.getGitRoot();
|
|
96
|
+
if (!gitRoot) {
|
|
97
|
+
console.error('Error: Not a git repository.');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hookPath = this.getHookPath();
|
|
102
|
+
if (!hookPath) {
|
|
103
|
+
console.error('Error: Could not determine git hooks directory.');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if hook already exists
|
|
108
|
+
if (fs.existsSync(hookPath)) {
|
|
109
|
+
if (this.isOurHook(hookPath)) {
|
|
110
|
+
console.log('Hook is already installed. Use "git-ai-commit hook uninstall" to remove it.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.error(`Error: ${HOOK_NAME} hook already exists and was not installed by git-ai-commit.`);
|
|
114
|
+
console.error(`Path: ${hookPath}`);
|
|
115
|
+
console.error('Remove or rename the existing hook first, then try again.');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create hooks directory if it doesn't exist
|
|
120
|
+
const hooksDir = path.dirname(hookPath);
|
|
121
|
+
if (!fs.existsSync(hooksDir)) {
|
|
122
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
|
|
126
|
+
console.log(`Installed ${HOOK_NAME} hook.`);
|
|
127
|
+
console.log(`Path: ${hookPath}`);
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log('Now "git commit" will auto-generate an AI commit message.');
|
|
130
|
+
console.log('The message will be pre-filled in your editor for review.');
|
|
131
|
+
console.log('Use "git commit -m ..." to skip AI generation.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private handleUninstall(): void {
|
|
135
|
+
const gitRoot = this.getGitRoot();
|
|
136
|
+
if (!gitRoot) {
|
|
137
|
+
console.error('Error: Not a git repository.');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const hookPath = this.getHookPath();
|
|
142
|
+
if (!hookPath) {
|
|
143
|
+
console.error('Error: Could not determine git hooks directory.');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(hookPath)) {
|
|
148
|
+
console.log('No hook installed.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!this.isOurHook(hookPath)) {
|
|
153
|
+
console.error(`Error: ${HOOK_NAME} hook exists but was not installed by git-ai-commit.`);
|
|
154
|
+
console.error('Will not remove hooks installed by other tools.');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.unlinkSync(hookPath);
|
|
159
|
+
console.log(`Removed ${HOOK_NAME} hook.`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private handleStatus(): void {
|
|
163
|
+
const gitRoot = this.getGitRoot();
|
|
164
|
+
if (!gitRoot) {
|
|
165
|
+
console.error('Error: Not a git repository.');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const hookPath = this.getHookPath();
|
|
170
|
+
if (!hookPath) {
|
|
171
|
+
console.error('Error: Could not determine git hooks directory.');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!fs.existsSync(hookPath)) {
|
|
176
|
+
console.log('Not installed.');
|
|
177
|
+
console.log('Run "git-ai-commit hook install" to set up the prepare-commit-msg hook.');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.isOurHook(hookPath)) {
|
|
182
|
+
console.log('Installed.');
|
|
183
|
+
console.log(`Path: ${hookPath}`);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(`A ${HOOK_NAME} hook exists but was not installed by git-ai-commit.`);
|
|
186
|
+
console.log(`Path: ${hookPath}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getCommand(): Command {
|
|
191
|
+
return this.program;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -64,7 +64,9 @@ export class PullRequestCommand {
|
|
|
64
64
|
fallbackModel: existingConfig.fallbackModel,
|
|
65
65
|
reasoningEffort: existingConfig.reasoningEffort,
|
|
66
66
|
language: existingConfig.language,
|
|
67
|
-
verbose: false
|
|
67
|
+
verbose: false,
|
|
68
|
+
mode: existingConfig.mode,
|
|
69
|
+
maxCompletionTokens: existingConfig.maxCompletionTokens,
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
const aiResult = await aiService.generatePullRequestMessage(
|
package/src/commands/tag.ts
CHANGED
|
@@ -68,7 +68,9 @@ export class TagCommand {
|
|
|
68
68
|
model: mergedModel,
|
|
69
69
|
fallbackModel: storedConfig.fallbackModel,
|
|
70
70
|
reasoningEffort: storedConfig.reasoningEffort,
|
|
71
|
-
language: storedConfig.language
|
|
71
|
+
language: storedConfig.language,
|
|
72
|
+
mode: storedConfig.mode,
|
|
73
|
+
maxCompletionTokens: storedConfig.maxCompletionTokens,
|
|
72
74
|
};
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -436,8 +438,17 @@ export class TagCommand {
|
|
|
436
438
|
} else {
|
|
437
439
|
for (const remote of selectedRemotes) {
|
|
438
440
|
console.log(`Pushing tag ${trimmedName} to ${remote}...`);
|
|
439
|
-
|
|
441
|
+
let pushSuccess = await GitService.pushTagToRemote(trimmedName, remote);
|
|
442
|
+
|
|
443
|
+
if (!pushSuccess) {
|
|
444
|
+
console.log(`⚠️ Normal push failed for ${remote}. Force push may be required.`);
|
|
445
|
+
const shouldForcePush = await this.confirmForcePush(trimmedName);
|
|
440
446
|
|
|
447
|
+
if (shouldForcePush) {
|
|
448
|
+
console.log(`Force pushing tag ${trimmedName} to ${remote}...`);
|
|
449
|
+
pushSuccess = await GitService.forcePushTag(trimmedName, remote);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
441
452
|
if (pushSuccess) {
|
|
442
453
|
console.log(`✅ Tag ${trimmedName} pushed to ${remote} successfully!`);
|
|
443
454
|
} else {
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { PullRequestCommand } from './commands/prCommand';
|
|
|
9
9
|
import { TagCommand } from './commands/tag';
|
|
10
10
|
import { HistoryCommand } from './commands/history';
|
|
11
11
|
import { CompletionCommand } from './commands/completion';
|
|
12
|
+
import { HookCommand } from './commands/hookCommand';
|
|
12
13
|
|
|
13
14
|
function getPackageVersion(): string {
|
|
14
15
|
try {
|
|
@@ -36,6 +37,7 @@ const pullRequestCommand = new PullRequestCommand();
|
|
|
36
37
|
const tagCommand = new TagCommand();
|
|
37
38
|
const historyCommand = new HistoryCommand();
|
|
38
39
|
const completionCommand = new CompletionCommand();
|
|
40
|
+
const hookCommand = new HookCommand();
|
|
39
41
|
|
|
40
42
|
program.addCommand(commitCommand.getCommand());
|
|
41
43
|
program.addCommand(configCommand.getCommand());
|
|
@@ -43,5 +45,6 @@ program.addCommand(pullRequestCommand.getCommand());
|
|
|
43
45
|
program.addCommand(tagCommand.getCommand());
|
|
44
46
|
program.addCommand(historyCommand.getCommand());
|
|
45
47
|
program.addCommand(completionCommand.getCommand());
|
|
48
|
+
program.addCommand(hookCommand.getCommand());
|
|
46
49
|
|
|
47
50
|
program.parse();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/onaries/git-ai-commit/main/src/schema/config.schema.json",
|
|
4
|
+
"title": "git-ai-commit configuration",
|
|
5
|
+
"description": "Configuration file for git-ai-commit CLI (~/.git-ai-commit/config.json)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "JSON Schema reference for editor support"
|
|
11
|
+
},
|
|
12
|
+
"apiKey": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "API key for AI requests. Overrides AI_API_KEY / OPENAI_API_KEY environment variables."
|
|
15
|
+
},
|
|
16
|
+
"baseURL": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"format": "uri",
|
|
19
|
+
"description": "Custom API base URL. Overrides AI_BASE_URL / OPENAI_BASE_URL environment variables. Not used in gemini mode."
|
|
20
|
+
},
|
|
21
|
+
"model": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "AI model to use for generation.",
|
|
24
|
+
"default": "zai-org/GLM-4.5-FP8",
|
|
25
|
+
"examples": ["gpt-4o-mini", "gpt-4", "gemini-2.0-flash", "claude-3-haiku"]
|
|
26
|
+
},
|
|
27
|
+
"fallbackModel": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Fallback model used when the primary model returns a 429 (rate limit) error.",
|
|
30
|
+
"examples": ["glm-4-flash", "gpt-4o-mini"]
|
|
31
|
+
},
|
|
32
|
+
"reasoningEffort": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"enum": ["minimal", "low", "medium", "high"],
|
|
35
|
+
"description": "Thinking effort level for reasoning models."
|
|
36
|
+
},
|
|
37
|
+
"mode": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"enum": ["custom", "openai", "gemini"],
|
|
40
|
+
"default": "custom",
|
|
41
|
+
"description": "AI provider mode. 'custom' uses AI_* vars first, 'openai' prefers OPENAI_* vars, 'gemini' uses Google Gemini native SDK."
|
|
42
|
+
},
|
|
43
|
+
"language": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"enum": ["ko", "en"],
|
|
46
|
+
"default": "ko",
|
|
47
|
+
"description": "Default language for AI-generated output."
|
|
48
|
+
},
|
|
49
|
+
"autoPush": {
|
|
50
|
+
"type": "boolean",
|
|
51
|
+
"default": false,
|
|
52
|
+
"description": "Automatically push to remote after a successful commit."
|
|
53
|
+
},
|
|
54
|
+
"coAuthor": {
|
|
55
|
+
"oneOf": [
|
|
56
|
+
{
|
|
57
|
+
"type": "string",
|
|
58
|
+
"pattern": ".+<.+@.+>",
|
|
59
|
+
"description": "Co-authored-by trailer value in 'Name <email>' format."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"type": "boolean",
|
|
63
|
+
"const": false,
|
|
64
|
+
"description": "Set to false to disable the default Co-authored-by trailer."
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"default": "git-ai-commit <git-ai-commit@users.noreply.github.com>",
|
|
68
|
+
"description": "Co-authored-by trailer appended to AI-generated commit messages. Defaults to 'git-ai-commit <git-ai-commit@users.noreply.github.com>'. Set to false to disable."
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"additionalProperties": false
|
|
72
|
+
}
|