@webywz/vibe-commit 0.1.4 → 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 CHANGED
@@ -56,16 +56,15 @@ vibe-commit install # optional prepare-commit-msg hook
56
56
  | `--vibe <style>` | `playful` \| `pro` \| `terse` (default: `pro`) |
57
57
  | `--no-ai` | Skip the API even if `OPENAI_API_KEY` is set · 即使有 Key 也不走 AI |
58
58
  | `--breaking` | Force breaking change (`!` + `BREAKING CHANGE:` footer) · 强制破坏性变更(`!` + footer) |
59
-
60
- 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).
61
-
62
- 离线启发式在删掉公开 `export`、或 `package.json` 主版本升级时也会标 breaking;只要 `breaking` 为真就会补上 `BREAKING CHANGE:` footer(`--json` 的 body 与格式化输出一致)。
63
-
64
59
  | `--json` | Machine-readable proposal · 机器可读提案 |
65
60
  | `install [--path]` | Install the prepare-commit-msg hook · 安装钩子 |
66
61
  | `uninstall [--path]` | Remove the managed hook · 卸载钩子 |
67
62
  | `-h` / `-v` | Help / version · 帮助 / 版本 |
68
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
+
69
68
  **Vibes** · 语气
70
69
 
71
70
  - **pro** — default. Imperative, review-ready. · 默认:祈使句,可进 review。
@@ -147,7 +146,9 @@ console.log(formatCommit(message));
147
146
  - new files / `export function` → `feat`
148
147
  3. Optional AI draft with automatic heuristic fallback.
149
148
  可选 AI 草稿,失败自动回退启发式。
150
- 4. Managed `prepare-commit-msg` hook via `install` / `uninstall`.
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`.
151
152
  通过 `install` / `uninstall` 管理 `prepare-commit-msg` 钩子。
152
153
 
153
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 (or git add <path>)");
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
  }
@@ -13,9 +13,14 @@ export declare function primaryRole(path: string): CommitType;
13
13
  export declare function classifyPath(path: string): CommitType[];
14
14
  export declare function inferScope(files: StagedFile[]): string | undefined;
15
15
  export declare function inferSubject(diff: StagedDiff, type: CommitType, scope?: string): string;
16
+ export type BreakingKind = "export-removed" | "major-bump" | "footer" | "forced";
16
17
  export declare function inferBreaking(diff: StagedDiff): boolean;
17
- /** Short reason for a BREAKING CHANGE footer, or undefined when not breaking. */
18
- export declare function breakingReason(diff: StagedDiff): string | undefined;
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;
19
24
  /**
20
25
  * Ensure body carries a conventional BREAKING CHANGE footer when breaking.
21
26
  * Skips if the body already mentions one. Uses subject as a last-resort reason.
package/dist/heuristic.js CHANGED
@@ -28,10 +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 breaking = Boolean(input.breaking) || inferBreaking(diff);
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
- const message = { ...styled, engine: "heuristic" };
34
- return ensureBreakingFooter(message, breakingReason(diff));
35
+ const message = {
36
+ ...styled,
37
+ engine: "heuristic",
38
+ breakingReason: kind ? formatBreakingReason(kind) : undefined,
39
+ };
40
+ return ensureBreakingFooter(message, breakingFooterDetail(diff));
35
41
  }
36
42
  export function inferType(diff) {
37
43
  const files = diff.files;
@@ -218,10 +224,35 @@ function inferBody(diff, type) {
218
224
  return `${lead}\n${listed}${extra}`;
219
225
  }
220
226
  export function inferBreaking(diff) {
221
- return Boolean(breakingReason(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
+ }
222
253
  }
223
- /** Short reason for a BREAKING CHANGE footer, or undefined when not breaking. */
224
- export function breakingReason(diff) {
254
+ /** Detail text for the BREAKING CHANGE footer, or undefined when not inferred. */
255
+ export function breakingFooterDetail(diff) {
225
256
  const footer = detectBreakingFooter(diff);
226
257
  if (footer)
227
258
  return footer;
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, inferBreaking, ensureBreakingFooter, 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, inferBreaking, ensureBreakingFooter, 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,5 +1,5 @@
1
1
  import { proposeWithAi } from "./ai.js";
2
- import { breakingReason, ensureBreakingFooter, 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,
@@ -22,7 +22,14 @@ export async function proposeMessage(diff, options, env = process.env) {
22
22
  message = ai;
23
23
  }
24
24
  }
25
- const breaking = Boolean(options.breaking) || heuristic.breaking || message.breaking;
26
- return ensureBreakingFooter({ ...message, breaking }, breakingReason(diff));
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));
27
34
  }
28
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webywz/vibe-commit",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Stylish conventional commit messages with personality. Offline-first, AI-optional.",
5
5
  "keywords": [
6
6
  "git",