@webywz/vibe-commit 0.1.3 → 0.1.5
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/README.md +7 -1
- package/dist/git.js +2 -1
- package/dist/heuristic.d.ts +15 -0
- package/dist/heuristic.js +161 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/propose.js +16 -15
- package/dist/render.js +4 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,10 @@ vibe-commit install # optional prepare-commit-msg hook
|
|
|
61
61
|
| `uninstall [--path]` | Remove the managed hook · 卸载钩子 |
|
|
62
62
|
| `-h` / `-v` | Help / version · 帮助 / 版本 |
|
|
63
63
|
|
|
64
|
+
Offline heuristic also marks breaking when the staged diff removes public `export`s or bumps a `package.json` major version, and always attaches a `BREAKING CHANGE:` footer when `breaking` is true (so `--json` body matches the formatted message). Dry-run prints a one-line cause; `--json` may include `breakingReason`.
|
|
65
|
+
|
|
66
|
+
离线启发式在删掉公开 `export`、或 `package.json` 主版本升级时也会标 breaking;只要 `breaking` 为真就会补上 `BREAKING CHANGE:` footer(`--json` 的 body 与格式化输出一致)。干跑会打一行原因;`--json` 可带 `breakingReason`。
|
|
67
|
+
|
|
64
68
|
**Vibes** · 语气
|
|
65
69
|
|
|
66
70
|
- **pro** — default. Imperative, review-ready. · 默认:祈使句,可进 review。
|
|
@@ -142,7 +146,9 @@ console.log(formatCommit(message));
|
|
|
142
146
|
- new files / `export function` → `feat`
|
|
143
147
|
3. Optional AI draft with automatic heuristic fallback.
|
|
144
148
|
可选 AI 草稿,失败自动回退启发式。
|
|
145
|
-
4.
|
|
149
|
+
4. **Breaking** when the diff removes a public `export`, bumps a `package.json` major, adds a `BREAKING CHANGE:` footer, or you pass `--breaking`. Dry-run shows why; `--json` may include `breakingReason`.
|
|
150
|
+
**破坏性变更**:删公开 `export`、主版本升级、diff 里已有 footer,或传 `--breaking`。干跑说明原因;`--json` 可带 `breakingReason`。
|
|
151
|
+
5. Managed `prepare-commit-msg` hook via `install` / `uninstall`.
|
|
146
152
|
通过 `install` / `uninstall` 管理 `prepare-commit-msg` 钩子。
|
|
147
153
|
|
|
148
154
|
---
|
package/dist/git.js
CHANGED
|
@@ -13,7 +13,8 @@ export function inspectStaged(cwd = process.cwd()) {
|
|
|
13
13
|
const raw = git(["diff", "--cached", "-M", "--unified=3"], cwd);
|
|
14
14
|
const diff = assembleDiff(nameStatus, numstat, raw);
|
|
15
15
|
if (diff.files.length === 0) {
|
|
16
|
-
throw new GitError("No staged changes. Stage files first: git add -p
|
|
16
|
+
throw new GitError("No staged changes. Stage files first: git add -p (or git add <path>)\n" +
|
|
17
|
+
"暂存区为空。请先暂存:git add -p(或 git add <path>)");
|
|
17
18
|
}
|
|
18
19
|
return diff;
|
|
19
20
|
}
|
package/dist/heuristic.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface HeuristicInput {
|
|
|
4
4
|
type?: CommitType;
|
|
5
5
|
scope?: string;
|
|
6
6
|
vibe: Vibe;
|
|
7
|
+
/** Force breaking: true (ORs with inferBreaking). */
|
|
8
|
+
breaking?: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare function proposeHeuristic(input: HeuristicInput): ProposedMessage;
|
|
9
11
|
export declare function inferType(diff: StagedDiff): CommitType;
|
|
@@ -11,5 +13,18 @@ export declare function primaryRole(path: string): CommitType;
|
|
|
11
13
|
export declare function classifyPath(path: string): CommitType[];
|
|
12
14
|
export declare function inferScope(files: StagedFile[]): string | undefined;
|
|
13
15
|
export declare function inferSubject(diff: StagedDiff, type: CommitType, scope?: string): string;
|
|
16
|
+
export type BreakingKind = "export-removed" | "major-bump" | "footer" | "forced";
|
|
17
|
+
export declare function inferBreaking(diff: StagedDiff): boolean;
|
|
18
|
+
/** Why breaking was set — heuristic signal first, then --breaking. */
|
|
19
|
+
export declare function detectBreakingKind(diff: StagedDiff, forced?: boolean): BreakingKind | undefined;
|
|
20
|
+
/** One-line bilingual cause for dry-run / --json.breakingReason. */
|
|
21
|
+
export declare function formatBreakingReason(kind: BreakingKind): string;
|
|
22
|
+
/** Detail text for the BREAKING CHANGE footer, or undefined when not inferred. */
|
|
23
|
+
export declare function breakingFooterDetail(diff: StagedDiff): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Ensure body carries a conventional BREAKING CHANGE footer when breaking.
|
|
26
|
+
* Skips if the body already mentions one. Uses subject as a last-resort reason.
|
|
27
|
+
*/
|
|
28
|
+
export declare function ensureBreakingFooter(message: ProposedMessage, reason?: string): ProposedMessage;
|
|
14
29
|
export declare function collectAddedIdentifiers(hunks: DiffHunk[]): string[];
|
|
15
30
|
export declare function stem(path: string): string;
|
package/dist/heuristic.js
CHANGED
|
@@ -28,9 +28,16 @@ export function proposeHeuristic(input) {
|
|
|
28
28
|
const scope = input.scope ?? inferScope(diff.files);
|
|
29
29
|
const subject = inferSubject(diff, type, scope);
|
|
30
30
|
const body = inferBody(diff, type);
|
|
31
|
-
const
|
|
31
|
+
const forced = Boolean(input.breaking);
|
|
32
|
+
const kind = detectBreakingKind(diff, forced);
|
|
33
|
+
const breaking = Boolean(kind);
|
|
32
34
|
const styled = applyVibe({ type, scope, subject, body, breaking }, vibe);
|
|
33
|
-
|
|
35
|
+
const message = {
|
|
36
|
+
...styled,
|
|
37
|
+
engine: "heuristic",
|
|
38
|
+
breakingReason: kind ? formatBreakingReason(kind) : undefined,
|
|
39
|
+
};
|
|
40
|
+
return ensureBreakingFooter(message, breakingFooterDetail(diff));
|
|
34
41
|
}
|
|
35
42
|
export function inferType(diff) {
|
|
36
43
|
const files = diff.files;
|
|
@@ -216,8 +223,158 @@ function inferBody(diff, type) {
|
|
|
216
223
|
: "Changed files:";
|
|
217
224
|
return `${lead}\n${listed}${extra}`;
|
|
218
225
|
}
|
|
219
|
-
function inferBreaking(diff) {
|
|
220
|
-
return
|
|
226
|
+
export function inferBreaking(diff) {
|
|
227
|
+
return Boolean(detectBreakingKind(diff, false));
|
|
228
|
+
}
|
|
229
|
+
/** Why breaking was set — heuristic signal first, then --breaking. */
|
|
230
|
+
export function detectBreakingKind(diff, forced = false) {
|
|
231
|
+
if (detectBreakingFooter(diff))
|
|
232
|
+
return "footer";
|
|
233
|
+
if (detectRemovedExports(diff).length > 0)
|
|
234
|
+
return "export-removed";
|
|
235
|
+
if (detectMajorVersionBump(diff))
|
|
236
|
+
return "major-bump";
|
|
237
|
+
if (forced)
|
|
238
|
+
return "forced";
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
/** One-line bilingual cause for dry-run / --json.breakingReason. */
|
|
242
|
+
export function formatBreakingReason(kind) {
|
|
243
|
+
switch (kind) {
|
|
244
|
+
case "export-removed":
|
|
245
|
+
return "export removed · 公开 export 删除";
|
|
246
|
+
case "major-bump":
|
|
247
|
+
return "major bump · package.json 主版本升级";
|
|
248
|
+
case "footer":
|
|
249
|
+
return "BREAKING CHANGE footer in diff";
|
|
250
|
+
case "forced":
|
|
251
|
+
return "forced via --breaking · --breaking 强制";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Detail text for the BREAKING CHANGE footer, or undefined when not inferred. */
|
|
255
|
+
export function breakingFooterDetail(diff) {
|
|
256
|
+
const footer = detectBreakingFooter(diff);
|
|
257
|
+
if (footer)
|
|
258
|
+
return footer;
|
|
259
|
+
const removed = detectRemovedExports(diff);
|
|
260
|
+
if (removed.length > 0) {
|
|
261
|
+
const names = removed.slice(0, 3).join(", ");
|
|
262
|
+
return removed.length === 1
|
|
263
|
+
? `remove public export ${names}`
|
|
264
|
+
: `remove public exports ${names}`;
|
|
265
|
+
}
|
|
266
|
+
const bump = detectMajorVersionBump(diff);
|
|
267
|
+
if (bump)
|
|
268
|
+
return `bump major version to ${bump.to}`;
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Ensure body carries a conventional BREAKING CHANGE footer when breaking.
|
|
273
|
+
* Skips if the body already mentions one. Uses subject as a last-resort reason.
|
|
274
|
+
*/
|
|
275
|
+
export function ensureBreakingFooter(message, reason) {
|
|
276
|
+
if (!message.breaking)
|
|
277
|
+
return message;
|
|
278
|
+
const body = message.body?.trim() ?? "";
|
|
279
|
+
if (/BREAKING CHANGE:/i.test(body)) {
|
|
280
|
+
return body === (message.body ?? "") ? message : { ...message, body };
|
|
281
|
+
}
|
|
282
|
+
const detail = (reason ?? message.subject).replace(/\s+/g, " ").trim() ||
|
|
283
|
+
"incompatible API change";
|
|
284
|
+
const footer = `BREAKING CHANGE: ${detail}`;
|
|
285
|
+
return {
|
|
286
|
+
...message,
|
|
287
|
+
body: body ? `${body}\n\n${footer}` : footer,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function detectBreakingFooter(diff) {
|
|
291
|
+
for (const hunk of diff.hunks) {
|
|
292
|
+
for (const line of hunk.lines) {
|
|
293
|
+
const match = line.match(/^\+\s*BREAKING CHANGE:\s*(.+)$/i);
|
|
294
|
+
if (match?.[1])
|
|
295
|
+
return match[1].trim();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Presence of the token alone still counts as breaking.
|
|
299
|
+
if (diff.hunks.some((hunk) => hunk.lines.some((line) => /^\+\s*BREAKING CHANGE:/i.test(line)))) {
|
|
300
|
+
return "incompatible API change";
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
const DELETED_EXPORT_RE = /^-\s*export(?:\s+default\b|\s*\{|\s+)/;
|
|
305
|
+
function detectRemovedExports(diff) {
|
|
306
|
+
const names = [];
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
for (const hunk of diff.hunks) {
|
|
309
|
+
for (const line of hunk.lines) {
|
|
310
|
+
if (line.startsWith("---"))
|
|
311
|
+
continue;
|
|
312
|
+
if (!DELETED_EXPORT_RE.test(line))
|
|
313
|
+
continue;
|
|
314
|
+
const name = exportNameFromDeletedLine(line);
|
|
315
|
+
const key = name ?? line.slice(1).trim();
|
|
316
|
+
if (seen.has(key))
|
|
317
|
+
continue;
|
|
318
|
+
seen.add(key);
|
|
319
|
+
if (name)
|
|
320
|
+
names.push(name);
|
|
321
|
+
else
|
|
322
|
+
names.push("export");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return names;
|
|
326
|
+
}
|
|
327
|
+
function exportNameFromDeletedLine(line) {
|
|
328
|
+
const content = line.slice(1);
|
|
329
|
+
const named = content.match(/export\s+(?:default\s+)?(?:async\s+)?(?:function|const|let|class|type|interface|enum)\s+([A-Za-z_][\w]*)/);
|
|
330
|
+
if (named?.[1])
|
|
331
|
+
return named[1];
|
|
332
|
+
const braced = content.match(/export\s*\{\s*([^}]+)\s*\}/);
|
|
333
|
+
if (braced?.[1]) {
|
|
334
|
+
const first = braced[1].split(",")[0]?.trim().split(/\s+as\s+/i)[0]?.trim();
|
|
335
|
+
if (first && /^[A-Za-z_][\w]*$/.test(first))
|
|
336
|
+
return first;
|
|
337
|
+
}
|
|
338
|
+
const def = content.match(/export\s+default\s+([A-Za-z_][\w]*)/);
|
|
339
|
+
if (def?.[1])
|
|
340
|
+
return def[1];
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
function detectMajorVersionBump(diff) {
|
|
344
|
+
for (const hunk of diff.hunks) {
|
|
345
|
+
if (!/(^|\/)package\.json$/i.test(hunk.path))
|
|
346
|
+
continue;
|
|
347
|
+
const removed = [];
|
|
348
|
+
const added = [];
|
|
349
|
+
for (const line of hunk.lines) {
|
|
350
|
+
const rem = line.match(/^-\s*"version"\s*:\s*"([^"]+)"/);
|
|
351
|
+
const add = line.match(/^\+\s*"version"\s*:\s*"([^"]+)"/);
|
|
352
|
+
if (rem?.[1])
|
|
353
|
+
removed.push(rem[1]);
|
|
354
|
+
if (add?.[1])
|
|
355
|
+
added.push(add[1]);
|
|
356
|
+
}
|
|
357
|
+
for (const from of removed) {
|
|
358
|
+
for (const to of added) {
|
|
359
|
+
if (isMajorBump(from, to))
|
|
360
|
+
return { from, to };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
function isMajorBump(from, to) {
|
|
367
|
+
const a = parseSemverMajor(from);
|
|
368
|
+
const b = parseSemverMajor(to);
|
|
369
|
+
if (a === undefined || b === undefined)
|
|
370
|
+
return false;
|
|
371
|
+
return b > a;
|
|
372
|
+
}
|
|
373
|
+
function parseSemverMajor(version) {
|
|
374
|
+
const match = version.trim().match(/^v?(\d+)\./);
|
|
375
|
+
if (!match)
|
|
376
|
+
return undefined;
|
|
377
|
+
return Number(match[1]);
|
|
221
378
|
}
|
|
222
379
|
export function collectAddedIdentifiers(hunks) {
|
|
223
380
|
const names = [];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { proposeWithAi } from "./ai.ts";
|
|
2
2
|
export { formatCommit, parseCommit, normalizeSubject } from "./format.ts";
|
|
3
3
|
export { inspectStaged, commitWithMessage, GitError } from "./git.ts";
|
|
4
|
-
export { proposeHeuristic, inferType, inferScope, inferSubject, classifyPath, } from "./heuristic.ts";
|
|
4
|
+
export { proposeHeuristic, inferType, inferScope, inferSubject, inferBreaking, detectBreakingKind, formatBreakingReason, breakingFooterDetail, ensureBreakingFooter, classifyPath, } from "./heuristic.ts";
|
|
5
|
+
export type { BreakingKind } from "./heuristic.ts";
|
|
5
6
|
export { parseArgs, helpText, UsageError } from "./parse-args.ts";
|
|
6
7
|
export { assembleDiff, parseUnifiedDiff } from "./parse-diff.ts";
|
|
7
8
|
export { installHook, uninstallHook, runPrepareCommitMsg, shouldSkipPrepareCommitMsg, hasNonCommentContent, HOOK_BEGIN, HOOK_END, } from "./hook.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { proposeWithAi } from "./ai.js";
|
|
2
2
|
export { formatCommit, parseCommit, normalizeSubject } from "./format.js";
|
|
3
3
|
export { inspectStaged, commitWithMessage, GitError } from "./git.js";
|
|
4
|
-
export { proposeHeuristic, inferType, inferScope, inferSubject, classifyPath, } from "./heuristic.js";
|
|
4
|
+
export { proposeHeuristic, inferType, inferScope, inferSubject, inferBreaking, detectBreakingKind, formatBreakingReason, breakingFooterDetail, ensureBreakingFooter, classifyPath, } from "./heuristic.js";
|
|
5
5
|
export { parseArgs, helpText, UsageError } from "./parse-args.js";
|
|
6
6
|
export { assembleDiff, parseUnifiedDiff } from "./parse-diff.js";
|
|
7
7
|
export { installHook, uninstallHook, runPrepareCommitMsg, shouldSkipPrepareCommitMsg, hasNonCommentContent, HOOK_BEGIN, HOOK_END, } from "./hook.js";
|
package/dist/propose.js
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
import { proposeWithAi } from "./ai.js";
|
|
2
|
-
import { proposeHeuristic } from "./heuristic.js";
|
|
2
|
+
import { breakingFooterDetail, detectBreakingKind, ensureBreakingFooter, formatBreakingReason, proposeHeuristic, } from "./heuristic.js";
|
|
3
3
|
export async function proposeMessage(diff, options, env = process.env) {
|
|
4
4
|
const heuristic = proposeHeuristic({
|
|
5
5
|
diff,
|
|
6
6
|
type: options.type,
|
|
7
7
|
scope: options.scope,
|
|
8
8
|
vibe: options.vibe,
|
|
9
|
+
breaking: options.breaking,
|
|
9
10
|
});
|
|
10
|
-
let message;
|
|
11
|
-
if (options.preferAi
|
|
12
|
-
message = heuristic;
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
11
|
+
let message = heuristic;
|
|
12
|
+
if (options.preferAi !== false) {
|
|
15
13
|
const hasKey = Boolean(env.OPENAI_API_KEY?.trim());
|
|
16
|
-
if (
|
|
17
|
-
message = heuristic;
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
14
|
+
if (hasKey) {
|
|
20
15
|
const ai = await proposeWithAi({
|
|
21
16
|
diff,
|
|
22
17
|
vibe: options.vibe,
|
|
23
18
|
type: options.type,
|
|
24
19
|
scope: options.scope,
|
|
25
20
|
}, env);
|
|
26
|
-
|
|
21
|
+
if (ai)
|
|
22
|
+
message = ai;
|
|
27
23
|
}
|
|
28
24
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
const forced = Boolean(options.breaking);
|
|
26
|
+
const breaking = forced || heuristic.breaking || message.breaking;
|
|
27
|
+
const kind = detectBreakingKind(diff, forced);
|
|
28
|
+
const breakingReason = kind
|
|
29
|
+
? formatBreakingReason(kind)
|
|
30
|
+
: breaking
|
|
31
|
+
? heuristic.breakingReason ?? message.breakingReason
|
|
32
|
+
: undefined;
|
|
33
|
+
return ensureBreakingFooter({ ...message, breaking, breakingReason }, breakingFooterDetail(diff));
|
|
33
34
|
}
|
|
34
35
|
//# sourceMappingURL=propose.js.map
|
package/dist/render.js
CHANGED
|
@@ -31,6 +31,10 @@ export function renderProposal(diff, message, hints = {}) {
|
|
|
31
31
|
...indent(formatted, pc),
|
|
32
32
|
"",
|
|
33
33
|
];
|
|
34
|
+
if (message.breaking && message.breakingReason) {
|
|
35
|
+
lines.push(` ${pc.yellow("breaking")} ${pc.dim(message.breakingReason)}`);
|
|
36
|
+
lines.push("");
|
|
37
|
+
}
|
|
34
38
|
if (hints.committed) {
|
|
35
39
|
lines.push(` ${pc.green("committed")}`);
|
|
36
40
|
lines.push("");
|
package/dist/types.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface ProposedMessage {
|
|
|
27
27
|
subject: string;
|
|
28
28
|
body?: string;
|
|
29
29
|
breaking: boolean;
|
|
30
|
+
/** Short cause when breaking (export removed / major bump / --breaking / footer). */
|
|
31
|
+
breakingReason?: string;
|
|
30
32
|
engine: Engine;
|
|
31
33
|
}
|
|
32
34
|
export interface ProposeOptions {
|