@ksw8954/git-ai-commit 1.1.0 → 1.1.3
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/CHANGELOG.md +24 -0
- package/Makefile +47 -18
- package/dist/commands/ai.d.ts +1 -1
- package/dist/commands/ai.d.ts.map +1 -1
- package/dist/commands/ai.js +10 -3
- package/dist/commands/ai.js.map +1 -1
- package/dist/commands/commit.d.ts.map +1 -1
- package/dist/commands/commit.js +8 -0
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +32 -34
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/git.d.ts +1 -0
- package/dist/commands/git.d.ts.map +1 -1
- package/dist/commands/git.js +20 -0
- package/dist/commands/git.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +5 -5
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/log.d.ts +9 -1
- package/dist/commands/log.d.ts.map +1 -1
- package/dist/commands/log.js +19 -1
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/prCommand.d.ts.map +1 -1
- package/dist/commands/prCommand.js +8 -4
- package/dist/commands/prCommand.js.map +1 -1
- package/dist/commands/tag.d.ts +1 -0
- package/dist/commands/tag.d.ts.map +1 -1
- package/dist/commands/tag.js +82 -28
- package/dist/commands/tag.js.map +1 -1
- package/dist/prompts/tag.d.ts +1 -1
- package/dist/prompts/tag.d.ts.map +1 -1
- package/dist/prompts/tag.js +35 -3
- package/dist/prompts/tag.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/ai.ts +19 -3
- package/src/commands/commit.ts +8 -0
- package/src/commands/completion.ts +32 -34
- package/src/commands/git.ts +20 -0
- package/src/commands/history.ts +6 -5
- package/src/commands/log.ts +30 -2
- package/src/commands/prCommand.ts +8 -4
- package/src/commands/tag.ts +98 -30
- package/src/prompts/tag.ts +39 -3
package/src/commands/commit.ts
CHANGED
|
@@ -177,6 +177,7 @@ export class CommitCommand {
|
|
|
177
177
|
status: "failure",
|
|
178
178
|
details: diffResult.error,
|
|
179
179
|
durationMs: Date.now() - start,
|
|
180
|
+
model: mergedModel,
|
|
180
181
|
});
|
|
181
182
|
process.exit(1);
|
|
182
183
|
}
|
|
@@ -197,6 +198,7 @@ export class CommitCommand {
|
|
|
197
198
|
status: "failure",
|
|
198
199
|
details: aiResult.error,
|
|
199
200
|
durationMs: Date.now() - start,
|
|
201
|
+
model: mergedModel,
|
|
200
202
|
});
|
|
201
203
|
process.exit(1);
|
|
202
204
|
}
|
|
@@ -214,6 +216,7 @@ export class CommitCommand {
|
|
|
214
216
|
status: "success",
|
|
215
217
|
details: "message-only output",
|
|
216
218
|
durationMs: Date.now() - start,
|
|
219
|
+
model: mergedModel,
|
|
217
220
|
});
|
|
218
221
|
return;
|
|
219
222
|
}
|
|
@@ -231,6 +234,7 @@ export class CommitCommand {
|
|
|
231
234
|
status: "cancelled",
|
|
232
235
|
details: "user declined commit",
|
|
233
236
|
durationMs: Date.now() - start,
|
|
237
|
+
model: mergedModel,
|
|
234
238
|
});
|
|
235
239
|
return;
|
|
236
240
|
}
|
|
@@ -264,6 +268,7 @@ export class CommitCommand {
|
|
|
264
268
|
status: "failure",
|
|
265
269
|
details: "push failed",
|
|
266
270
|
durationMs: Date.now() - start,
|
|
271
|
+
model: mergedModel,
|
|
267
272
|
});
|
|
268
273
|
process.exit(1);
|
|
269
274
|
}
|
|
@@ -276,6 +281,7 @@ export class CommitCommand {
|
|
|
276
281
|
status: "failure",
|
|
277
282
|
details: "git commit failed",
|
|
278
283
|
durationMs: Date.now() - start,
|
|
284
|
+
model: mergedModel,
|
|
279
285
|
});
|
|
280
286
|
process.exit(1);
|
|
281
287
|
}
|
|
@@ -284,6 +290,7 @@ export class CommitCommand {
|
|
|
284
290
|
args: safeArgs,
|
|
285
291
|
status: "success",
|
|
286
292
|
durationMs: Date.now() - start,
|
|
293
|
+
model: mergedModel,
|
|
287
294
|
});
|
|
288
295
|
} catch (error) {
|
|
289
296
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -294,6 +301,7 @@ export class CommitCommand {
|
|
|
294
301
|
status: "failure",
|
|
295
302
|
details: message,
|
|
296
303
|
durationMs: Date.now() - start,
|
|
304
|
+
model: options.model,
|
|
297
305
|
});
|
|
298
306
|
process.exit(1);
|
|
299
307
|
}
|
|
@@ -129,20 +129,15 @@ complete -F _git_ai_commit git-ai-commit
|
|
|
129
129
|
private generateZshCompletion(): string {
|
|
130
130
|
return `#compdef git-ai-commit
|
|
131
131
|
# git-ai-commit zsh completion
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
132
|
+
# Installation:
|
|
133
|
+
# mkdir -p ~/.zsh/completions
|
|
134
|
+
# git-ai-commit completion zsh > ~/.zsh/completions/_git-ai-commit
|
|
135
|
+
# # Add to ~/.zshrc (before compinit): fpath=(~/.zsh/completions \$fpath)
|
|
136
|
+
# # Then restart shell or run: autoload -Uz compinit && compinit
|
|
135
137
|
|
|
136
138
|
_git-ai-commit() {
|
|
137
|
-
local
|
|
138
|
-
|
|
139
|
-
'commit:Generate AI-powered commit message'
|
|
140
|
-
'config:Manage git-ai-commit configuration'
|
|
141
|
-
'pr:Generate a pull request title and summary'
|
|
142
|
-
'tag:Create an annotated git tag with AI-generated notes'
|
|
143
|
-
'history:Manage git-ai-commit command history'
|
|
144
|
-
'completion:Generate shell completion scripts'
|
|
145
|
-
)
|
|
139
|
+
local curcontext="\$curcontext" state line
|
|
140
|
+
typeset -A opt_args
|
|
146
141
|
|
|
147
142
|
_arguments -C \\
|
|
148
143
|
'-v[output the version number]' \\
|
|
@@ -150,14 +145,23 @@ _git-ai-commit() {
|
|
|
150
145
|
'-h[display help]' \\
|
|
151
146
|
'--help[display help]' \\
|
|
152
147
|
'1: :->command' \\
|
|
153
|
-
'*:: :->args'
|
|
148
|
+
'*:: :->args' && return
|
|
154
149
|
|
|
155
150
|
case \$state in
|
|
156
151
|
command)
|
|
152
|
+
local -a commands
|
|
153
|
+
commands=(
|
|
154
|
+
'commit:Generate AI-powered commit message'
|
|
155
|
+
'config:Manage git-ai-commit configuration'
|
|
156
|
+
'pr:Generate a pull request title and summary'
|
|
157
|
+
'tag:Create an annotated git tag with AI-generated notes'
|
|
158
|
+
'history:Manage git-ai-commit command history'
|
|
159
|
+
'completion:Generate shell completion scripts'
|
|
160
|
+
)
|
|
157
161
|
_describe -t commands 'git-ai-commit commands' commands
|
|
158
162
|
;;
|
|
159
163
|
args)
|
|
160
|
-
case \$
|
|
164
|
+
case \$line[1] in
|
|
161
165
|
commit)
|
|
162
166
|
_arguments \\
|
|
163
167
|
'-k[OpenAI API key]:key:' \\
|
|
@@ -190,13 +194,18 @@ _git-ai-commit() {
|
|
|
190
194
|
;;
|
|
191
195
|
pr)
|
|
192
196
|
_arguments \\
|
|
193
|
-
'--base[Base branch to diff against]:branch
|
|
194
|
-
'--compare[Compare branch to describe]:branch
|
|
197
|
+
'--base[Base branch to diff against]:branch:->branches' \\
|
|
198
|
+
'--compare[Compare branch to describe]:branch:->branches' \\
|
|
195
199
|
'-k[Override API key]:key:' \\
|
|
196
200
|
'--api-key[Override API key]:key:' \\
|
|
197
201
|
'-b[Override API base URL]:url:' \\
|
|
198
202
|
'--base-url[Override API base URL]:url:' \\
|
|
199
203
|
'--model[Override AI model]:model:'
|
|
204
|
+
[[ \$state == branches ]] && {
|
|
205
|
+
local -a branches
|
|
206
|
+
branches=(\${(f)"\$(git branch --format='%(refname:short)' 2>/dev/null)"})
|
|
207
|
+
_describe -t branches 'branches' branches
|
|
208
|
+
}
|
|
200
209
|
;;
|
|
201
210
|
tag)
|
|
202
211
|
_arguments \\
|
|
@@ -207,9 +216,14 @@ _git-ai-commit() {
|
|
|
207
216
|
'-m[Model to use]:model:' \\
|
|
208
217
|
'--model[Model to use]:model:' \\
|
|
209
218
|
'--message[Tag message to use directly]:message:' \\
|
|
210
|
-
'-t[Existing tag to diff against]:tag
|
|
211
|
-
'--base-tag[Existing tag to diff against]:tag
|
|
219
|
+
'-t[Existing tag to diff against]:tag:->tags' \\
|
|
220
|
+
'--base-tag[Existing tag to diff against]:tag:->tags' \\
|
|
212
221
|
'--prompt[Additional AI prompt instructions]:text:'
|
|
222
|
+
[[ \$state == tags ]] && {
|
|
223
|
+
local -a tags
|
|
224
|
+
tags=(\${(f)"\$(git tag 2>/dev/null)"})
|
|
225
|
+
_describe -t tags 'tags' tags
|
|
226
|
+
}
|
|
213
227
|
;;
|
|
214
228
|
history)
|
|
215
229
|
_arguments \\
|
|
@@ -226,22 +240,6 @@ _git-ai-commit() {
|
|
|
226
240
|
;;
|
|
227
241
|
esac
|
|
228
242
|
}
|
|
229
|
-
|
|
230
|
-
# Helper function to complete git branches
|
|
231
|
-
__git_branch_names() {
|
|
232
|
-
local -a branches
|
|
233
|
-
branches=(\${(f)"\$(git branch --format='%(refname:short)' 2>/dev/null)"})
|
|
234
|
-
_describe -t branches 'branches' branches
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
# Helper function to complete git tags
|
|
238
|
-
__git_tags() {
|
|
239
|
-
local -a tags
|
|
240
|
-
tags=(\${(f)"\$(git tag 2>/dev/null)"})
|
|
241
|
-
_describe -t tags 'tags' tags
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
_git-ai-commit "\$@"
|
|
245
243
|
`;
|
|
246
244
|
}
|
|
247
245
|
|
package/src/commands/git.ts
CHANGED
|
@@ -264,6 +264,26 @@ export class GitService {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
static async getTagMessage(tagName: string): Promise<string | null> {
|
|
268
|
+
try {
|
|
269
|
+
// Get the tag object content
|
|
270
|
+
const { stdout } = await execFileAsync('git', ['tag', '-l', '-n999', tagName]);
|
|
271
|
+
if (!stdout.trim()) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
// Format: "tagname message line 1\n message line 2..."
|
|
275
|
+
// Remove the tag name prefix and clean up
|
|
276
|
+
const lines = stdout.split('\n');
|
|
277
|
+
const firstLine = lines[0] || '';
|
|
278
|
+
// Remove tag name from first line
|
|
279
|
+
const messageStart = firstLine.replace(new RegExp(`^${tagName}\\s*`), '');
|
|
280
|
+
const restLines = lines.slice(1).map(line => line.replace(/^\s{12}/, ''));
|
|
281
|
+
return [messageStart, ...restLines].join('\n').trim() || null;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
267
287
|
static async remoteTagExists(tagName: string): Promise<boolean> {
|
|
268
288
|
try {
|
|
269
289
|
const { stdout } = await execAsync(`git ls-remote --tags origin refs/tags/${tagName}`);
|
package/src/commands/history.ts
CHANGED
|
@@ -38,11 +38,12 @@ export class HistoryCommand {
|
|
|
38
38
|
|
|
39
39
|
private formatLine(e: any): string {
|
|
40
40
|
const ts = new Date(e.timestamp).toISOString();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const projectName = e.project?.name || 'unknown';
|
|
42
|
+
const model = e.model || 'default';
|
|
43
|
+
const duration = e.durationMs ? ` (${e.durationMs}ms)` : '';
|
|
44
|
+
const details = e.details ? `\n > ${e.details}` : '';
|
|
45
|
+
|
|
46
|
+
return `${ts} [${projectName}] ${e.command} ${e.status}${duration} model=${model}${details}`;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
private async handleHistory(options: HistoryOptions) {
|
package/src/commands/log.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
4
5
|
|
|
5
6
|
export type LogStatus = 'success' | 'failure' | 'cancelled';
|
|
6
7
|
|
|
8
|
+
export interface ProjectInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
gitRemote?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export interface HistoryEntry {
|
|
8
15
|
id: string;
|
|
9
16
|
timestamp: string; // ISO
|
|
@@ -12,6 +19,8 @@ export interface HistoryEntry {
|
|
|
12
19
|
status: LogStatus;
|
|
13
20
|
details?: string;
|
|
14
21
|
durationMs?: number;
|
|
22
|
+
project?: ProjectInfo;
|
|
23
|
+
model?: string;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
function getStorageDir(): string {
|
|
@@ -27,8 +36,25 @@ function genId(): string {
|
|
|
27
36
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
function getProjectInfo(): ProjectInfo {
|
|
40
|
+
const cwd = process.cwd();
|
|
41
|
+
const name = path.basename(cwd);
|
|
42
|
+
|
|
43
|
+
let gitRemote: string | undefined;
|
|
44
|
+
try {
|
|
45
|
+
gitRemote = execSync('git config --get remote.origin.url', {
|
|
46
|
+
encoding: 'utf-8',
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
48
|
+
}).trim() || undefined;
|
|
49
|
+
} catch {
|
|
50
|
+
// Not a git repo or no remote
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { name, path: cwd, gitRemote };
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
export class LogService {
|
|
31
|
-
static async append(entry: Omit<HistoryEntry, 'id' | 'timestamp'> & { timestamp?: string; id?: string }): Promise<void> {
|
|
57
|
+
static async append(entry: Omit<HistoryEntry, 'id' | 'timestamp' | 'project'> & { timestamp?: string; id?: string; model?: string }): Promise<void> {
|
|
32
58
|
const file = getHistoryPath();
|
|
33
59
|
const dir = path.dirname(file);
|
|
34
60
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -40,7 +66,9 @@ export class LogService {
|
|
|
40
66
|
args: entry.args,
|
|
41
67
|
status: entry.status,
|
|
42
68
|
details: entry.details,
|
|
43
|
-
durationMs: entry.durationMs
|
|
69
|
+
durationMs: entry.durationMs,
|
|
70
|
+
project: getProjectInfo(),
|
|
71
|
+
model: entry.model
|
|
44
72
|
};
|
|
45
73
|
|
|
46
74
|
const line = JSON.stringify(finalized) + '\n';
|
|
@@ -50,7 +50,8 @@ export class PullRequestCommand {
|
|
|
50
50
|
command: 'pr',
|
|
51
51
|
args: { ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
52
52
|
status: 'failure',
|
|
53
|
-
details: err
|
|
53
|
+
details: err,
|
|
54
|
+
model: mergedModel
|
|
54
55
|
});
|
|
55
56
|
process.exit(1);
|
|
56
57
|
return;
|
|
@@ -77,7 +78,8 @@ export class PullRequestCommand {
|
|
|
77
78
|
command: 'pr',
|
|
78
79
|
args: { ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
79
80
|
status: 'failure',
|
|
80
|
-
details: err
|
|
81
|
+
details: err,
|
|
82
|
+
model: mergedModel
|
|
81
83
|
});
|
|
82
84
|
process.exit(1);
|
|
83
85
|
return;
|
|
@@ -87,7 +89,8 @@ export class PullRequestCommand {
|
|
|
87
89
|
await LogService.append({
|
|
88
90
|
command: 'pr',
|
|
89
91
|
args: { ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
90
|
-
status: 'success'
|
|
92
|
+
status: 'success',
|
|
93
|
+
model: mergedModel
|
|
91
94
|
});
|
|
92
95
|
} catch (error) {
|
|
93
96
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -96,7 +99,8 @@ export class PullRequestCommand {
|
|
|
96
99
|
command: 'pr',
|
|
97
100
|
args: { ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
98
101
|
status: 'failure',
|
|
99
|
-
details: message
|
|
102
|
+
details: message,
|
|
103
|
+
model: options.model
|
|
100
104
|
});
|
|
101
105
|
process.exit(1);
|
|
102
106
|
}
|
package/src/commands/tag.ts
CHANGED
|
@@ -20,18 +20,29 @@ export class TagCommand {
|
|
|
20
20
|
constructor() {
|
|
21
21
|
this.program = new Command('tag')
|
|
22
22
|
.description('Create an annotated git tag with optional AI-generated notes')
|
|
23
|
-
.argument('
|
|
23
|
+
.argument('[name]', 'Tag name to create (auto-increments patch version if omitted)')
|
|
24
24
|
.option('-k, --api-key <key>', 'OpenAI API key (overrides env var)')
|
|
25
25
|
.option('--base-url <url>', 'Custom API base URL (overrides env var)')
|
|
26
26
|
.option('-m, --model <model>', 'Model to use (overrides env var)')
|
|
27
27
|
.option('--message <message>', 'Tag message to use directly (skips AI generation)')
|
|
28
28
|
.option('-t, --base-tag <tag>', 'Existing tag to diff against when generating notes')
|
|
29
29
|
.option('--prompt <text>', 'Additional instructions to append to the AI prompt for this tag')
|
|
30
|
-
.action(async (tagName: string, options: TagOptions) => {
|
|
30
|
+
.action(async (tagName: string | undefined, options: TagOptions) => {
|
|
31
31
|
await this.handleTag(tagName, options);
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
private incrementPatchVersion(version: string): string | null {
|
|
36
|
+
// Match semver patterns: v1.2.3, 1.2.3, v1.2.3-beta, etc.
|
|
37
|
+
const match = version.match(/^(v?)(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
38
|
+
if (!match) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const [, prefix, major, minor, patch, suffix] = match;
|
|
42
|
+
const newPatch = parseInt(patch, 10) + 1;
|
|
43
|
+
return `${prefix}${major}.${minor}.${newPatch}${suffix}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
private resolveAIConfig(options: TagOptions): AIServiceConfig {
|
|
36
47
|
const storedConfig = ConfigService.getConfig();
|
|
37
48
|
|
|
@@ -52,27 +63,58 @@ export class TagCommand {
|
|
|
52
63
|
};
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
private async handleTag(tagName: string, options: TagOptions): Promise<void> {
|
|
56
|
-
const
|
|
66
|
+
private async handleTag(tagName: string | undefined, options: TagOptions): Promise<void> {
|
|
67
|
+
const storedConfig = ConfigService.getConfig();
|
|
68
|
+
const mergedModel = options.model || storedConfig.model;
|
|
57
69
|
|
|
70
|
+
let trimmedName = tagName?.trim();
|
|
71
|
+
|
|
72
|
+
// If no tag name provided, auto-increment from latest tag
|
|
58
73
|
if (!trimmedName) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
const latestTagResult = await GitService.getLatestTag();
|
|
75
|
+
|
|
76
|
+
if (!latestTagResult.success || !latestTagResult.tag) {
|
|
77
|
+
console.error('No existing tags found. Please provide a tag name explicitly.');
|
|
78
|
+
await LogService.append({
|
|
79
|
+
command: 'tag',
|
|
80
|
+
args: { ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
81
|
+
status: 'failure',
|
|
82
|
+
details: 'no existing tags found for auto-increment',
|
|
83
|
+
model: mergedModel
|
|
84
|
+
});
|
|
85
|
+
process.exit(1);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const newVersion = this.incrementPatchVersion(latestTagResult.tag);
|
|
90
|
+
|
|
91
|
+
if (!newVersion) {
|
|
92
|
+
console.error(`Cannot parse version from tag "${latestTagResult.tag}". Please provide a tag name explicitly.`);
|
|
93
|
+
await LogService.append({
|
|
94
|
+
command: 'tag',
|
|
95
|
+
args: { ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
96
|
+
status: 'failure',
|
|
97
|
+
details: `cannot parse version from tag: ${latestTagResult.tag}`,
|
|
98
|
+
model: mergedModel
|
|
99
|
+
});
|
|
100
|
+
process.exit(1);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`Latest tag: ${latestTagResult.tag}`);
|
|
105
|
+
console.log(`New tag: ${newVersion}`);
|
|
106
|
+
trimmedName = newVersion;
|
|
68
107
|
}
|
|
69
108
|
|
|
70
109
|
// Check if tag already exists locally
|
|
71
110
|
const localTagExists = await GitService.tagExists(trimmedName);
|
|
72
111
|
let remoteTagExists = false;
|
|
73
|
-
let
|
|
112
|
+
let previousTagMessage: string | null = null;
|
|
74
113
|
|
|
75
114
|
if (localTagExists) {
|
|
115
|
+
// Get existing tag message before deletion for reference
|
|
116
|
+
previousTagMessage = await GitService.getTagMessage(trimmedName);
|
|
117
|
+
|
|
76
118
|
console.log(`⚠️ Tag ${trimmedName} already exists locally.`);
|
|
77
119
|
const shouldDelete = await this.confirmTagDelete(trimmedName);
|
|
78
120
|
|
|
@@ -82,7 +124,8 @@ export class TagCommand {
|
|
|
82
124
|
command: 'tag',
|
|
83
125
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
84
126
|
status: 'cancelled',
|
|
85
|
-
details: 'user declined to replace existing tag'
|
|
127
|
+
details: 'user declined to replace existing tag',
|
|
128
|
+
model: mergedModel
|
|
86
129
|
});
|
|
87
130
|
return;
|
|
88
131
|
}
|
|
@@ -103,7 +146,8 @@ export class TagCommand {
|
|
|
103
146
|
command: 'tag',
|
|
104
147
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
105
148
|
status: 'failure',
|
|
106
|
-
details: 'remote tag deletion failed'
|
|
149
|
+
details: 'remote tag deletion failed',
|
|
150
|
+
model: mergedModel
|
|
107
151
|
});
|
|
108
152
|
process.exit(1);
|
|
109
153
|
return;
|
|
@@ -122,13 +166,13 @@ export class TagCommand {
|
|
|
122
166
|
command: 'tag',
|
|
123
167
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
124
168
|
status: 'failure',
|
|
125
|
-
details: 'local tag deletion failed'
|
|
169
|
+
details: 'local tag deletion failed',
|
|
170
|
+
model: mergedModel
|
|
126
171
|
});
|
|
127
172
|
process.exit(1);
|
|
128
173
|
return;
|
|
129
174
|
}
|
|
130
175
|
console.log(`✅ Local tag ${trimmedName} deleted`);
|
|
131
|
-
wasTagReplaced = true;
|
|
132
176
|
}
|
|
133
177
|
|
|
134
178
|
let tagMessage = options.message?.trim();
|
|
@@ -147,6 +191,15 @@ export class TagCommand {
|
|
|
147
191
|
}
|
|
148
192
|
}
|
|
149
193
|
|
|
194
|
+
// Get style reference from base tag (if different from current tag being replaced)
|
|
195
|
+
let styleReferenceMessage: string | null = null;
|
|
196
|
+
if (baseTag && baseTag !== trimmedName) {
|
|
197
|
+
styleReferenceMessage = await GitService.getTagMessage(baseTag);
|
|
198
|
+
if (styleReferenceMessage) {
|
|
199
|
+
console.log(`Using ${baseTag} message as style reference.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
150
203
|
const historyResult = await GitService.getCommitSummariesSince(baseTag);
|
|
151
204
|
if (!historyResult.success || !historyResult.log) {
|
|
152
205
|
console.error('Error:', historyResult.error ?? 'Unable to read commit history.');
|
|
@@ -154,7 +207,8 @@ export class TagCommand {
|
|
|
154
207
|
command: 'tag',
|
|
155
208
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
156
209
|
status: 'failure',
|
|
157
|
-
details: historyResult.error ?? 'Unable to read commit history.'
|
|
210
|
+
details: historyResult.error ?? 'Unable to read commit history.',
|
|
211
|
+
model: mergedModel
|
|
158
212
|
});
|
|
159
213
|
process.exit(1);
|
|
160
214
|
return;
|
|
@@ -171,14 +225,21 @@ export class TagCommand {
|
|
|
171
225
|
command: 'tag',
|
|
172
226
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
173
227
|
status: 'failure',
|
|
174
|
-
details: message
|
|
228
|
+
details: message,
|
|
229
|
+
model: mergedModel
|
|
175
230
|
});
|
|
176
231
|
process.exit(1);
|
|
177
232
|
return;
|
|
178
233
|
}
|
|
179
234
|
|
|
180
235
|
const aiService = new AIService(aiConfig);
|
|
181
|
-
const aiResult = await aiService.generateTagNotes(
|
|
236
|
+
const aiResult = await aiService.generateTagNotes(
|
|
237
|
+
trimmedName,
|
|
238
|
+
historyResult.log,
|
|
239
|
+
options.prompt,
|
|
240
|
+
previousTagMessage,
|
|
241
|
+
styleReferenceMessage
|
|
242
|
+
);
|
|
182
243
|
|
|
183
244
|
if (!aiResult.success || !aiResult.notes) {
|
|
184
245
|
console.error('Error:', aiResult.error ?? 'Failed to generate tag notes.');
|
|
@@ -186,7 +247,8 @@ export class TagCommand {
|
|
|
186
247
|
command: 'tag',
|
|
187
248
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
188
249
|
status: 'failure',
|
|
189
|
-
details: aiResult.error ?? 'Failed to generate tag notes.'
|
|
250
|
+
details: aiResult.error ?? 'Failed to generate tag notes.',
|
|
251
|
+
model: mergedModel
|
|
190
252
|
});
|
|
191
253
|
process.exit(1);
|
|
192
254
|
return;
|
|
@@ -207,7 +269,8 @@ export class TagCommand {
|
|
|
207
269
|
command: 'tag',
|
|
208
270
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
209
271
|
status: 'cancelled',
|
|
210
|
-
details: 'user declined tag creation'
|
|
272
|
+
details: 'user declined tag creation',
|
|
273
|
+
model: mergedModel
|
|
211
274
|
});
|
|
212
275
|
return;
|
|
213
276
|
}
|
|
@@ -221,7 +284,8 @@ export class TagCommand {
|
|
|
221
284
|
command: 'tag',
|
|
222
285
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
223
286
|
status: 'failure',
|
|
224
|
-
details: 'git tag creation failed'
|
|
287
|
+
details: 'git tag creation failed',
|
|
288
|
+
model: mergedModel
|
|
225
289
|
});
|
|
226
290
|
process.exit(1);
|
|
227
291
|
return;
|
|
@@ -238,8 +302,8 @@ export class TagCommand {
|
|
|
238
302
|
const selectedRemotes = await this.selectRemotesForPush(trimmedName, remotes);
|
|
239
303
|
|
|
240
304
|
if (selectedRemotes && selectedRemotes.length > 0) {
|
|
241
|
-
// If tag
|
|
242
|
-
const needsForcePush =
|
|
305
|
+
// If remote tag still exists (user declined to delete), use force push
|
|
306
|
+
const needsForcePush = remoteTagExists;
|
|
243
307
|
|
|
244
308
|
if (needsForcePush) {
|
|
245
309
|
console.log(`\n⚠️ Tag ${trimmedName} may exist on remote. Force push is required.`);
|
|
@@ -251,7 +315,8 @@ export class TagCommand {
|
|
|
251
315
|
command: 'tag',
|
|
252
316
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
253
317
|
status: 'cancelled',
|
|
254
|
-
details: 'user declined force push'
|
|
318
|
+
details: 'user declined force push',
|
|
319
|
+
model: mergedModel
|
|
255
320
|
});
|
|
256
321
|
return;
|
|
257
322
|
}
|
|
@@ -268,7 +333,8 @@ export class TagCommand {
|
|
|
268
333
|
command: 'tag',
|
|
269
334
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
270
335
|
status: 'failure',
|
|
271
|
-
details: `tag force push to ${remote} failed
|
|
336
|
+
details: `tag force push to ${remote} failed`,
|
|
337
|
+
model: mergedModel
|
|
272
338
|
});
|
|
273
339
|
}
|
|
274
340
|
}
|
|
@@ -285,7 +351,8 @@ export class TagCommand {
|
|
|
285
351
|
command: 'tag',
|
|
286
352
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
287
353
|
status: 'failure',
|
|
288
|
-
details: `tag push to ${remote} failed
|
|
354
|
+
details: `tag push to ${remote} failed`,
|
|
355
|
+
model: mergedModel
|
|
289
356
|
});
|
|
290
357
|
}
|
|
291
358
|
}
|
|
@@ -296,7 +363,8 @@ export class TagCommand {
|
|
|
296
363
|
await LogService.append({
|
|
297
364
|
command: 'tag',
|
|
298
365
|
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
299
|
-
status: 'success'
|
|
366
|
+
status: 'success',
|
|
367
|
+
model: mergedModel
|
|
300
368
|
});
|
|
301
369
|
}
|
|
302
370
|
|
package/src/prompts/tag.ts
CHANGED
|
@@ -3,7 +3,9 @@ export type TagPromptLanguage = 'ko' | 'en';
|
|
|
3
3
|
export const generateTagPrompt = (
|
|
4
4
|
tagName: string,
|
|
5
5
|
customInstructions = '',
|
|
6
|
-
language: TagPromptLanguage = 'ko'
|
|
6
|
+
language: TagPromptLanguage = 'ko',
|
|
7
|
+
isImprovement = false,
|
|
8
|
+
hasStyleReference = false
|
|
7
9
|
): string => {
|
|
8
10
|
const titleInstruction = language === 'ko'
|
|
9
11
|
? `첫 줄에 버전 "${tagName}"을 제목으로 작성하세요.`
|
|
@@ -51,15 +53,49 @@ export const generateTagPrompt = (
|
|
|
51
53
|
? '릴리즈 노트를 한국어로 작성하세요.'
|
|
52
54
|
: 'Write the release notes in English.';
|
|
53
55
|
|
|
56
|
+
const improvementInstruction = isImprovement
|
|
57
|
+
? language === 'ko'
|
|
58
|
+
? `\n## 개선 모드
|
|
59
|
+
이전 릴리즈 노트가 제공됩니다. 이를 참고하여:
|
|
60
|
+
- 기존 내용의 구조와 스타일을 유지하면서 개선하세요.
|
|
61
|
+
- 누락된 변경사항이 있으면 추가하세요.
|
|
62
|
+
- 불필요하거나 중복된 내용은 제거하세요.
|
|
63
|
+
- 더 명확하고 사용자 친화적인 표현으로 다듬으세요.\n`
|
|
64
|
+
: `\n## Improvement Mode
|
|
65
|
+
Previous release notes are provided. Use them as reference to:
|
|
66
|
+
- Maintain the structure and style while improving the content.
|
|
67
|
+
- Add any missing changes from the commit log.
|
|
68
|
+
- Remove unnecessary or redundant content.
|
|
69
|
+
- Refine the language to be clearer and more user-friendly.\n`
|
|
70
|
+
: '';
|
|
71
|
+
|
|
72
|
+
const styleReferenceInstruction = hasStyleReference && !isImprovement
|
|
73
|
+
? language === 'ko'
|
|
74
|
+
? `\n## 스타일 참조
|
|
75
|
+
이전 태그의 릴리즈 노트가 스타일 참조용으로 제공됩니다.
|
|
76
|
+
- 제공된 형식과 구조를 따르세요.
|
|
77
|
+
- 제목, 카테고리, 항목 스타일을 일관되게 유지하세요.
|
|
78
|
+
- 언어 톤과 상세도 수준을 맞추세요.\n`
|
|
79
|
+
: `\n## Style Reference
|
|
80
|
+
A previous tag's release notes are provided as style reference.
|
|
81
|
+
- Follow the provided format and structure.
|
|
82
|
+
- Maintain consistency in title, categories, and item styles.
|
|
83
|
+
- Match the language tone and level of detail.\n`
|
|
84
|
+
: '';
|
|
85
|
+
|
|
54
86
|
return `You are an experienced release manager. Produce clear, user-facing release notes in GitHub Release style.
|
|
55
87
|
|
|
56
88
|
## Objective
|
|
57
|
-
|
|
89
|
+
${isImprovement
|
|
90
|
+
? `Improve the existing release notes for ${tagName} based on the commit history and previous notes.`
|
|
91
|
+
: `Create release notes for ${tagName} that describe the meaningful changes since the previous release.`}
|
|
58
92
|
|
|
59
93
|
## Input Context
|
|
60
94
|
- Target tag to publish: ${tagName}
|
|
61
95
|
- Commit history between the previous tag and ${tagName} will be supplied in the user message.
|
|
62
|
-
|
|
96
|
+
${isImprovement ? '- Previous release notes are provided for improvement.' : ''}
|
|
97
|
+
${hasStyleReference && !isImprovement ? '- Style reference from previous tag is provided.' : ''}
|
|
98
|
+
${improvementInstruction}${styleReferenceInstruction}
|
|
63
99
|
${customInstructions ? `## Additional Instructions\n${customInstructions}\n` : ''}
|
|
64
100
|
## Output Format (GitHub Release Style)
|
|
65
101
|
${titleInstruction}
|