@co0ontty/wand 1.21.15 → 1.21.17
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/dist/git-quick-commit.d.ts +7 -1
- package/dist/git-quick-commit.js +143 -47
- package/dist/server-session-routes.js +2 -2
- package/dist/types.d.ts +0 -4
- package/dist/web-ui/content/scripts.js +512 -220
- package/dist/web-ui/content/styles.css +204 -45
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ interface QuickCommitOptions {
|
|
|
6
6
|
autoMessage: boolean;
|
|
7
7
|
customMessage?: string;
|
|
8
8
|
tag?: string;
|
|
9
|
+
/** When `tag` is empty, ask Claude to generate one based on the diff + commit message. */
|
|
9
10
|
autoTag?: boolean;
|
|
10
11
|
push?: boolean;
|
|
11
12
|
}
|
|
@@ -13,6 +14,11 @@ export declare class QuickCommitError extends Error {
|
|
|
13
14
|
readonly code: string;
|
|
14
15
|
constructor(message: string, code: string);
|
|
15
16
|
}
|
|
16
|
-
export
|
|
17
|
+
export interface GenerateCommitMessageResult {
|
|
18
|
+
message: string;
|
|
19
|
+
/** AI-suggested next tag derived from the staged diff and the latest existing tag. */
|
|
20
|
+
suggestedTag?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function generateCommitMessageOnly(cwd: string, language: string): Promise<GenerateCommitMessageResult>;
|
|
17
23
|
export declare function runQuickCommit(opts: QuickCommitOptions): Promise<QuickCommitResult>;
|
|
18
24
|
export {};
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -141,17 +141,6 @@ export function getGitStatus(cwd) {
|
|
|
141
141
|
}
|
|
142
142
|
const allEntries = parsePorcelainV2(porcelain);
|
|
143
143
|
const files = allEntries.slice(0, MAX_FILE_ENTRIES);
|
|
144
|
-
let latestTag;
|
|
145
|
-
let suggestedNextTag;
|
|
146
|
-
try {
|
|
147
|
-
latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd);
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
latestTag = undefined;
|
|
151
|
-
}
|
|
152
|
-
if (latestTag) {
|
|
153
|
-
suggestedNextTag = bumpPatchTag(latestTag);
|
|
154
|
-
}
|
|
155
144
|
return {
|
|
156
145
|
isGit: true,
|
|
157
146
|
branch,
|
|
@@ -160,20 +149,8 @@ export function getGitStatus(cwd) {
|
|
|
160
149
|
head,
|
|
161
150
|
repoRoot,
|
|
162
151
|
initialCommit,
|
|
163
|
-
latestTag,
|
|
164
|
-
suggestedNextTag,
|
|
165
152
|
};
|
|
166
153
|
}
|
|
167
|
-
function bumpPatchTag(tag) {
|
|
168
|
-
const m = tag.match(/^(v?)(\d+)\.(\d+)\.(\d+)(.*)/);
|
|
169
|
-
if (!m)
|
|
170
|
-
return "";
|
|
171
|
-
const prefix = m[1];
|
|
172
|
-
const major = m[2];
|
|
173
|
-
const minor = m[3];
|
|
174
|
-
const patch = parseInt(m[4], 10) + 1;
|
|
175
|
-
return `${prefix}${major}.${minor}.${patch}`;
|
|
176
|
-
}
|
|
177
154
|
export class QuickCommitError extends Error {
|
|
178
155
|
code;
|
|
179
156
|
constructor(message, code) {
|
|
@@ -210,7 +187,7 @@ function callClaudeText(prompt, cwd) {
|
|
|
210
187
|
child.stdin?.end(prompt);
|
|
211
188
|
});
|
|
212
189
|
}
|
|
213
|
-
|
|
190
|
+
function collectStagedDiff(cwd) {
|
|
214
191
|
let diff;
|
|
215
192
|
try {
|
|
216
193
|
diff = runGit(["diff", "--cached", "--submodule=log"], cwd, 5000);
|
|
@@ -229,6 +206,10 @@ async function generateCommitMessage(cwd, language) {
|
|
|
229
206
|
if (diff.length > MAX_DIFF_FOR_AI) {
|
|
230
207
|
diff = diff.slice(0, MAX_DIFF_FOR_AI) + "\n\n... (diff truncated) ...";
|
|
231
208
|
}
|
|
209
|
+
return diff;
|
|
210
|
+
}
|
|
211
|
+
async function generateCommitMessage(cwd, language) {
|
|
212
|
+
const diff = collectStagedDiff(cwd);
|
|
232
213
|
const lang = language.trim() || "中文";
|
|
233
214
|
const prompt = `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。\n\n${diff}`;
|
|
234
215
|
const raw = await callClaudeText(prompt, cwd);
|
|
@@ -238,6 +219,73 @@ async function generateCommitMessage(cwd, language) {
|
|
|
238
219
|
}
|
|
239
220
|
return message;
|
|
240
221
|
}
|
|
222
|
+
function tryParseJson(raw) {
|
|
223
|
+
let text = raw.trim();
|
|
224
|
+
// Strip ```json … ``` or ``` … ``` fences if Claude wrapped the response
|
|
225
|
+
text = text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
226
|
+
// Find the first balanced-looking JSON object substring
|
|
227
|
+
const start = text.indexOf("{");
|
|
228
|
+
const end = text.lastIndexOf("}");
|
|
229
|
+
if (start === -1 || end === -1 || end <= start)
|
|
230
|
+
return null;
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function sanitizeSuggestedTag(value) {
|
|
239
|
+
if (typeof value !== "string")
|
|
240
|
+
return undefined;
|
|
241
|
+
const cleaned = value.trim().replace(/^["'`]+|["'`]+$/g, "").trim();
|
|
242
|
+
if (!cleaned)
|
|
243
|
+
return undefined;
|
|
244
|
+
// Accept common semver-ish forms (v1.2.3, 1.2.3, v1.2.3-rc.1, v1.2.3+build.5)
|
|
245
|
+
if (!/^v?\d+\.\d+\.\d+([.\-+][0-9A-Za-z.\-+]*)?$/.test(cleaned))
|
|
246
|
+
return undefined;
|
|
247
|
+
return cleaned;
|
|
248
|
+
}
|
|
249
|
+
async function generateCommitMessageWithTag(cwd, language) {
|
|
250
|
+
const diff = collectStagedDiff(cwd);
|
|
251
|
+
let latestTag;
|
|
252
|
+
try {
|
|
253
|
+
latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
latestTag = undefined;
|
|
257
|
+
}
|
|
258
|
+
const lang = language.trim() || "中文";
|
|
259
|
+
const tagHint = latestTag
|
|
260
|
+
? `当前最新 tag 是 \`${latestTag}\`,请基于它给出下一个版本号(保持原有前缀风格,例如有 \`v\` 就保留 \`v\`)。`
|
|
261
|
+
: `仓库还没有任何 tag,请直接给一个起始版本号(建议 \`v0.0.1\` / \`v0.1.0\` / \`v1.0.0\` 之一,按改动幅度选择)。`;
|
|
262
|
+
const prompt = `阅读以下 git diff,完成两件事:
|
|
263
|
+
1. 用${lang}写一条简洁的 commit message(祈使句,不超过 50 字,描述「做了什么」)。
|
|
264
|
+
2. 根据改动幅度推荐下一个语义化版本 tag(破坏性变更 → 升 major;新增功能 → 升 minor;修复 / 文档 / 重构 / 维护 → 升 patch)。${tagHint}
|
|
265
|
+
|
|
266
|
+
请严格输出**单行 JSON 对象**,不要 Markdown 代码块、不要任何解释文字、不要多余引号。格式:
|
|
267
|
+
{"message":"...","tag":"v1.2.3"}
|
|
268
|
+
|
|
269
|
+
git diff:
|
|
270
|
+
${diff}`;
|
|
271
|
+
const raw = await callClaudeText(prompt, cwd);
|
|
272
|
+
const parsed = tryParseJson(raw);
|
|
273
|
+
let message;
|
|
274
|
+
let suggestedTag;
|
|
275
|
+
if (parsed && typeof parsed.message === "string") {
|
|
276
|
+
message = parsed.message.replace(/^["'`]+|["'`]+$/g, "").trim();
|
|
277
|
+
suggestedTag = sanitizeSuggestedTag(parsed.tag);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Fallback: treat whole output as message, no tag suggestion
|
|
281
|
+
message = raw.replace(/^["'`]+|["'`]+$/g, "").trim();
|
|
282
|
+
suggestedTag = undefined;
|
|
283
|
+
}
|
|
284
|
+
if (!message) {
|
|
285
|
+
throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
|
|
286
|
+
}
|
|
287
|
+
return { message, suggestedTag };
|
|
288
|
+
}
|
|
241
289
|
export async function generateCommitMessageOnly(cwd, language) {
|
|
242
290
|
if (!cwd || !existsSync(cwd)) {
|
|
243
291
|
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
@@ -248,7 +296,65 @@ export async function generateCommitMessageOnly(cwd, language) {
|
|
|
248
296
|
catch {
|
|
249
297
|
// best-effort staging so the diff is complete
|
|
250
298
|
}
|
|
251
|
-
return
|
|
299
|
+
return generateCommitMessageWithTag(cwd, language);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Ask Claude for a single tag string. Called from `runQuickCommit` after the commit has
|
|
303
|
+
* already landed, so we look at `git show HEAD` and use `HEAD~1` for the previous tag.
|
|
304
|
+
*/
|
|
305
|
+
async function generateTagAfterCommit(cwd, language, commitMessage) {
|
|
306
|
+
let diff;
|
|
307
|
+
try {
|
|
308
|
+
diff = runGit(["show", "HEAD", "--no-color", "--submodule=log"], cwd, 5000);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
diff = "";
|
|
312
|
+
}
|
|
313
|
+
if (!diff) {
|
|
314
|
+
try {
|
|
315
|
+
diff = runGit(["show", "HEAD", "--name-only"], cwd, 3000);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
diff = "(no diff available)";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (diff.length > MAX_DIFF_FOR_AI) {
|
|
322
|
+
diff = diff.slice(0, MAX_DIFF_FOR_AI) + "\n\n... (diff truncated) ...";
|
|
323
|
+
}
|
|
324
|
+
let latestTag;
|
|
325
|
+
try {
|
|
326
|
+
// We just made a commit, so look for the most recent tag reachable from HEAD~1.
|
|
327
|
+
latestTag = runGit(["describe", "--tags", "--abbrev=0", "HEAD~1"], cwd) || undefined;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
latestTag = undefined;
|
|
331
|
+
}
|
|
332
|
+
const lang = language.trim() || "中文";
|
|
333
|
+
const tagHint = latestTag
|
|
334
|
+
? `当前最新 tag 是 \`${latestTag}\`,请基于它给出下一个版本号(保持原有前缀风格,例如有 \`v\` 就保留 \`v\`)。`
|
|
335
|
+
: `仓库还没有任何 tag,请给一个起始版本号(建议 \`v0.0.1\` / \`v0.1.0\` / \`v1.0.0\` 之一,按改动幅度选择)。`;
|
|
336
|
+
const prompt = `根据以下 commit message 和 git diff 推荐一个语义化版本 tag(破坏性变更 → 升 major;新增功能 → 升 minor;修复 / 文档 / 重构 / 维护 → 升 patch)。${tagHint}
|
|
337
|
+
|
|
338
|
+
请用${lang}思考但严格输出**单行 JSON 对象**,不要 Markdown 代码块、不要任何解释文字、不要多余引号。格式:
|
|
339
|
+
{"tag":"v1.2.3"}
|
|
340
|
+
|
|
341
|
+
commit message:${commitMessage}
|
|
342
|
+
|
|
343
|
+
git diff:
|
|
344
|
+
${diff}`;
|
|
345
|
+
const raw = await callClaudeText(prompt, cwd);
|
|
346
|
+
const parsed = tryParseJson(raw);
|
|
347
|
+
let suggested;
|
|
348
|
+
if (parsed && typeof parsed.tag === "string") {
|
|
349
|
+
suggested = sanitizeSuggestedTag(parsed.tag);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
suggested = sanitizeSuggestedTag(raw);
|
|
353
|
+
}
|
|
354
|
+
if (!suggested) {
|
|
355
|
+
throw new QuickCommitError("AI 没有给出合法的 tag,请手动填写。", "INVALID_AI_TAG");
|
|
356
|
+
}
|
|
357
|
+
return suggested;
|
|
252
358
|
}
|
|
253
359
|
// ── Direct git operations ──
|
|
254
360
|
export async function runQuickCommit(opts) {
|
|
@@ -310,29 +416,19 @@ export async function runQuickCommit(opts) {
|
|
|
310
416
|
commitHash = "";
|
|
311
417
|
}
|
|
312
418
|
// Step 5: tag
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
latestTag = undefined;
|
|
326
|
-
}
|
|
327
|
-
tagName = bumpPatchTag(latestTag || "v0.0.0");
|
|
419
|
+
// - explicit `tag` wins
|
|
420
|
+
// - if `tag` is empty and `autoTag` is on, ask Claude to generate one
|
|
421
|
+
// - otherwise no tag
|
|
422
|
+
let tagName = (tag || "").trim();
|
|
423
|
+
if (!tagName && autoTag) {
|
|
424
|
+
tagName = await generateTagAfterCommit(cwd, language, message);
|
|
425
|
+
}
|
|
426
|
+
if (tagName) {
|
|
427
|
+
try {
|
|
428
|
+
runGit(["tag", tagName], cwd);
|
|
328
429
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
runGit(["tag", tagName], cwd);
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
|
|
335
|
-
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
throw new QuickCommitError(`git tag 失败:${getGitErrorMessage(error)}`, "GIT_TAG_FAILED");
|
|
336
432
|
}
|
|
337
433
|
}
|
|
338
434
|
// Step 6: push
|
|
@@ -377,8 +377,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
377
377
|
return;
|
|
378
378
|
}
|
|
379
379
|
try {
|
|
380
|
-
const
|
|
381
|
-
res.json(
|
|
380
|
+
const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "");
|
|
381
|
+
res.json(result);
|
|
382
382
|
}
|
|
383
383
|
catch (error) {
|
|
384
384
|
if (error instanceof QuickCommitError) {
|
package/dist/types.d.ts
CHANGED
|
@@ -169,10 +169,6 @@ export interface GitStatusResult {
|
|
|
169
169
|
repoRoot?: string;
|
|
170
170
|
/** Truthy when the repo has no commits yet (initial state). */
|
|
171
171
|
initialCommit?: boolean;
|
|
172
|
-
/** Most recent reachable tag (e.g. "v1.2.3"). */
|
|
173
|
-
latestTag?: string;
|
|
174
|
-
/** Auto-suggested next tag derived by bumping the patch segment. */
|
|
175
|
-
suggestedNextTag?: string;
|
|
176
172
|
error?: string;
|
|
177
173
|
}
|
|
178
174
|
export interface QuickCommitResult {
|