@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.
@@ -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 declare function generateCommitMessageOnly(cwd: string, language: string): Promise<string>;
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 {};
@@ -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
- async function generateCommitMessage(cwd, language) {
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 generateCommitMessage(cwd, language);
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
- const makeTag = !!(autoTag || (tag && tag.trim()));
314
- let tagName = "";
315
- if (makeTag) {
316
- if (tag && tag.trim()) {
317
- tagName = tag.trim();
318
- }
319
- else {
320
- let latestTag;
321
- try {
322
- latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd);
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
- if (tagName) {
330
- try {
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 message = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "");
381
- res.json({ message });
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 {