@webywz/vibe-commit 0.1.13 → 0.1.15

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
@@ -17,7 +17,7 @@
17
17
  </p>
18
18
 
19
19
  Companion spirit to [diff-vibe](https://github.com/webywz/diff-vibe): commits here, pull requests there.
20
- Pair walkthrough (EN/中文) and a copy-pasteable helper: [`examples/pair-workflow.md`](./examples/pair-workflow.md) · [`examples/commit-msg.sh`](./examples/commit-msg.sh) (ship in the npm tarball).
20
+ Pair walkthrough (EN/中文), helper script, and CI sample: [`examples/pair-workflow.md`](./examples/pair-workflow.md) · [`examples/commit-msg.sh`](./examples/commit-msg.sh) · [`examples/github-actions.yml`](./examples/github-actions.yml) (ship in the npm tarball).
21
21
 
22
22
  与 [diff-vibe](https://github.com/webywz/diff-vibe) 成对:这边写 commit,那边写 PR。
23
23
  成对流程与可复制脚本见 [`examples/`](./examples/)(随 npm 包发布)。
@@ -45,6 +45,7 @@ vibe-commit # propose from staged diff
45
45
  vibe-commit --vibe playful --commit # write the commit
46
46
  vibe-commit --breaking --dry-run # force ! + BREAKING CHANGE footer
47
47
  vibe-commit --dry-run # print only (default; wins over --commit)
48
+ vibe-commit --github --quiet --no-ai # Actions annotations (CI)
48
49
  git commit -m "$(vibe-commit --dry-run)"
49
50
  vibe-commit install # optional prepare-commit-msg hook
50
51
  ```
@@ -56,9 +57,13 @@ vibe-commit install # optional prepare-commit-msg hook
56
57
  | `--type <type>` | Force `feat` / `fix` / `docs` / `style` / `refactor` / `perf` / `test` / `build` / `ci` / `chore` |
57
58
  | `--scope <scope>` | Override inferred scope · 覆盖推断出的 scope |
58
59
  | `--vibe <style>` | `playful` \| `pro` \| `terse` (default: `pro`) |
60
+ | `--locale <locale>` | `en` \| `zh` (default: `en`) · 提交信息主题语言 |
59
61
  | `--no-ai` | Skip the API even if `OPENAI_API_KEY` is set · 即使有 Key 也不走 AI |
60
62
  | `--breaking` | Force breaking (`!` + `BREAKING CHANGE:` footer = **subject**) · 强制破坏性变更(`!` + footer 文本即 **subject**) |
61
63
  | `--json` | Machine-readable proposal · 机器可读提案 |
64
+ | `--github` | Emit GitHub Actions `::notice` / `::warning` / `::error` annotations; write `GITHUB_OUTPUT` + `GITHUB_STEP_SUMMARY` when set · 输出 Actions 注解,并在环境变量存在时写 Job 输出与 Summary |
65
+ | `--quiet` | Suppress the pretty terminal report; `--json` / `--github` still emit · 抑制漂亮终端报告 |
66
+ | `--max-annotations <n>` | Cap `--github` annotation lines (default: 20; invalid → UsageError) · 限制注解条数 |
62
67
  | `install [--path]` | Install the prepare-commit-msg hook · 安装钩子 |
63
68
  | `uninstall [--path]` | Remove the managed hook · 卸载钩子 |
64
69
  | `-h` / `-v` | Help / version · 帮助 / 版本 |
@@ -75,7 +80,7 @@ npx --yes @webywz/vibe-commit --commit
75
80
  npx --yes @webywz/diff-vibe@^0.1.8 --base main --create
76
81
  ```
77
82
 
78
- Commits here · PRs there. Full walkthrough (EN/中文) and `commit-msg.sh` live in [`examples/pair-workflow.md`](./examples/pair-workflow.md) and [`examples/commit-msg.sh`](./examples/commit-msg.sh). Companion-side snippets: [diff-vibe examples](https://github.com/webywz/diff-vibe/tree/main/examples).
83
+ Commits here · PRs there. Full walkthrough (EN/中文), `commit-msg.sh`, and Actions sample live in [`examples/`](./examples/). Companion-side snippets: [diff-vibe examples](https://github.com/webywz/diff-vibe/tree/main/examples).
79
84
 
80
85
  这边写 commit,那边写 PR。完整成对流程与脚本见 [`examples/`](./examples/)。成对仓库示例:[diff-vibe examples](https://github.com/webywz/diff-vibe/tree/main/examples)。
81
86
 
@@ -121,6 +126,45 @@ Hook stays **offline by default** (`VIBE_COMMIT_HOOK_AI=1` to allow AI). Unique
121
126
 
122
127
  钩子默认离线;设 `VIBE_COMMIT_HOOK_AI=1` 才允许 AI。用标记块保证干净卸载。已有外来钩子会备份并链式调用。
123
128
 
129
+ ### Locale support · 语言支持
130
+
131
+ vibe-commit supports English (`en`, default) and Chinese (`zh`) commit subjects:
132
+
133
+ vibe-commit 支持英文(`en`,默认)和中文(`zh`)提交信息主题:
134
+
135
+ ```bash
136
+ vibe-commit --locale zh --dry-run # Chinese subject: 添加/修复/更新...
137
+ vibe-commit --locale en --dry-run # English subject: add/fix/update...
138
+ ```
139
+
140
+ **Configuration precedence** · 配置优先级:
141
+ 1. CLI flag `--locale`
142
+ 2. Environment variable `VIBE_COMMIT_LOCALE`
143
+ 3. Project config (`.vibe-commit.json` or `package.json#vibeCommit`)
144
+ 4. Default `en`
145
+
146
+ ```json
147
+ // .vibe-commit.json
148
+ {
149
+ "locale": "zh",
150
+ "vibe": "pro"
151
+ }
152
+ ```
153
+
154
+ ```json
155
+ // package.json
156
+ {
157
+ "vibeCommit": {
158
+ "locale": "zh",
159
+ "vibe": "terse"
160
+ }
161
+ }
162
+ ```
163
+
164
+ Both heuristic and AI engines respect the locale setting. The `--json` output includes a `locale` field.
165
+
166
+ 启发式引擎和 AI 引擎都会遵循 locale 设置。`--json` 输出包含 `locale` 字段。
167
+
124
168
  ### AI (optional) · AI(可选)
125
169
 
126
170
  ```bash
@@ -128,6 +172,7 @@ export OPENAI_API_KEY=sk-...
128
172
  export OPENAI_MODEL=gpt-4o-mini # optional
129
173
  export OPENAI_BASE_URL=https://api.openai.com/v1
130
174
  export VIBE_COMMIT_VIBE=pro # or VIBE
175
+ export VIBE_COMMIT_LOCALE=zh # or en (default)
131
176
  ```
132
177
 
133
178
  OpenAI-compatible Chat Completions. Missing key, timeout, or 4xx/5xx → silent fallback to the offline heuristic. See [`.env.example`](.env.example). Unstaged files are never sent; huge diffs are truncated.
@@ -164,6 +209,8 @@ console.log(formatCommit(message));
164
209
  **破坏性变更**:删公开 `export`、主版本升级、diff 里已有 footer,或传 `--breaking`。强制 `--breaking` 时 footer 用 **subject**;启发式仍可用更具体的双语原因。干跑说明原因;`--json` 可带 `breakingReason`。
165
210
  5. Managed `prepare-commit-msg` hook via `install` / `uninstall`.
166
211
  通过 `install` / `uninstall` 管理 `prepare-commit-msg` 钩子。
212
+ 6. **GitHub Actions** mode: `--github` emits Checks annotations; optional `--quiet` / `--max-annotations`; writes job outputs + step summary when `GITHUB_OUTPUT` / `GITHUB_STEP_SUMMARY` are set. See [`examples/github-actions.yml`](./examples/github-actions.yml).
213
+ **GitHub Actions** 模式:`--github` 输出 Checks 注解;可选 `--quiet` / `--max-annotations`;环境变量存在时写 Job 输出与 Summary。示例见 [`examples/github-actions.yml`](./examples/github-actions.yml)。
167
214
 
168
215
  ---
169
216
 
@@ -173,6 +220,7 @@ console.log(formatCommit(message));
173
220
  - Source · [github.com/webywz/vibe-commit](https://github.com/webywz/vibe-commit)
174
221
  - Related · [diff-vibe](https://github.com/webywz/diff-vibe) · [prompt-diff](https://github.com/webywz/prompt-diff)
175
222
  - Pair workflow · [examples/pair-workflow.md](./examples/pair-workflow.md)
223
+ - GitHub Actions · [examples/github-actions.yml](./examples/github-actions.yml)
176
224
  - License · [MIT](./LICENSE)
177
225
 
178
226
  ```bash
package/dist/ai.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { ProposedMessage, StagedDiff, Vibe } from "./types.ts";
1
+ import type { Locale, ProposedMessage, StagedDiff, Vibe } from "./types.ts";
2
2
  import type { CommitType } from "./types.ts";
3
3
  export interface AiContext {
4
4
  diff: StagedDiff;
5
5
  vibe: Vibe;
6
+ locale: Locale;
6
7
  type?: CommitType;
7
8
  scope?: string;
8
9
  }
package/dist/ai.js CHANGED
@@ -17,7 +17,7 @@ export async function proposeWithAi(context, env = process.env) {
17
17
  model,
18
18
  temperature: context.vibe === "playful" ? 0.7 : 0.2,
19
19
  messages: [
20
- { role: "system", content: systemPrompt(context.vibe) },
20
+ { role: "system", content: systemPrompt(context.vibe, context.locale) },
21
21
  { role: "user", content: userPrompt(context) },
22
22
  ],
23
23
  }),
@@ -35,24 +35,29 @@ export async function proposeWithAi(context, env = process.env) {
35
35
  parsed.type = context.type;
36
36
  if (context.scope)
37
37
  parsed.scope = context.scope;
38
+ parsed.locale = context.locale;
38
39
  return parsed;
39
40
  }
40
41
  catch {
41
42
  return null;
42
43
  }
43
44
  }
44
- function systemPrompt(vibe) {
45
+ function systemPrompt(vibe, locale) {
45
46
  const vibeLine = vibe === "playful"
46
47
  ? "Tone: witty, still professional; no memes, no emoji, no slang that would embarrass a code review."
47
48
  : vibe === "terse"
48
49
  ? "Tone: as short as possible. Headline only, no body."
49
50
  : "Tone: clear, professional, imperative mood.";
51
+ const langLine = locale === "zh"
52
+ ? "Subject language: Chinese (中文). Use Chinese verbs like 添加/修复/更新/重构/优化/删除/格式化 etc."
53
+ : "Subject language: English.";
50
54
  return `You write conventional commit messages.
51
55
  Return only the commit message, no quotes, no markdown fences.
52
56
  Format: type(scope): subject
53
57
  Optional body after a blank line.
54
58
  Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
55
59
  Subject: lowercase, imperative, no trailing period, <= 72 chars.
60
+ ${langLine}
56
61
  ${vibeLine}`;
57
62
  }
58
63
  function userPrompt(context) {
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import pc from "picocolors";
6
6
  import { formatCommit } from "./format.js";
7
7
  import { commitWithMessage, GitError, inspectStaged } from "./git.js";
8
+ import { appendGitHubJobOutput, appendGitHubStepSummary, formatGitHubAnnotations, } from "./github.js";
8
9
  import { installHook, uninstallHook, runPrepareCommitMsg } from "./hook.js";
9
10
  import { helpText, parseArgs, UsageError } from "./parse-args.js";
10
11
  import { proposeMessage } from "./propose.js";
@@ -45,23 +46,54 @@ export async function main(argv = process.argv.slice(2)) {
45
46
  type: options.type,
46
47
  scope: options.scope,
47
48
  vibe: options.vibe,
49
+ locale: options.locale,
48
50
  preferAi: !options.noAi,
49
51
  breaking: options.breaking,
50
52
  });
51
53
  const formatted = formatCommit(message);
52
54
  const shouldCommit = options.commit && !options.dryRun;
55
+ if (options.github) {
56
+ const ann = formatGitHubAnnotations(message, diff, {
57
+ maxAnnotations: options.maxAnnotations,
58
+ });
59
+ // Keep --json stdout machine-parseable; annotations ride on stderr.
60
+ if (options.json)
61
+ process.stderr.write(ann);
62
+ else
63
+ process.stdout.write(ann);
64
+ try {
65
+ appendGitHubJobOutput(message, diff);
66
+ }
67
+ catch (err) {
68
+ process.stderr.write(`vibe-commit: GITHUB_OUTPUT skipped (${err instanceof Error ? err.message : err})\n`);
69
+ }
70
+ try {
71
+ appendGitHubStepSummary(message, diff);
72
+ }
73
+ catch (err) {
74
+ process.stderr.write(`vibe-commit: GITHUB_STEP_SUMMARY skipped (${err instanceof Error ? err.message : err})\n`);
75
+ }
76
+ }
53
77
  if (options.json) {
54
78
  process.stdout.write(`${JSON.stringify({ ...message, formatted }, null, 2)}\n`);
55
79
  }
56
- else if (piped) {
57
- process.stdout.write(`${formatted}\n`);
58
- }
59
- else {
60
- process.stdout.write(renderProposal(diff, message));
80
+ else if (!options.quiet) {
81
+ if (options.github) {
82
+ process.stdout.write("::group::vibe-commit report\n");
83
+ }
84
+ if (piped) {
85
+ process.stdout.write(`${formatted}\n`);
86
+ }
87
+ else {
88
+ process.stdout.write(renderProposal(diff, message));
89
+ }
90
+ if (options.github) {
91
+ process.stdout.write("::endgroup::\n");
92
+ }
61
93
  }
62
94
  if (shouldCommit) {
63
95
  commitWithMessage(formatted);
64
- if (!piped && !options.json) {
96
+ if (!piped && !options.json && !options.quiet) {
65
97
  process.stdout.write(` ${pc.green("committed")}\n\n`);
66
98
  }
67
99
  }
@@ -0,0 +1,6 @@
1
+ import type { Locale, Vibe } from "./types.ts";
2
+ export interface ProjectConfig {
3
+ locale?: Locale;
4
+ vibe?: Vibe;
5
+ }
6
+ export declare function loadProjectConfig(cwd?: string): ProjectConfig | undefined;
package/dist/config.js ADDED
@@ -0,0 +1,49 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { LOCALES, VIBES } from "./types.js";
4
+ export function loadProjectConfig(cwd = process.cwd()) {
5
+ const vibeCommitPath = join(cwd, ".vibe-commit.json");
6
+ if (existsSync(vibeCommitPath)) {
7
+ try {
8
+ const raw = readFileSync(vibeCommitPath, "utf8");
9
+ const parsed = JSON.parse(raw);
10
+ return normalizeConfig(parsed);
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ const pkgPath = join(cwd, "package.json");
17
+ if (existsSync(pkgPath)) {
18
+ try {
19
+ const raw = readFileSync(pkgPath, "utf8");
20
+ const parsed = JSON.parse(raw);
21
+ if (parsed.vibeCommit &&
22
+ typeof parsed.vibeCommit === "object" &&
23
+ !Array.isArray(parsed.vibeCommit)) {
24
+ return normalizeConfig(parsed.vibeCommit);
25
+ }
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ function normalizeConfig(raw) {
34
+ const config = {};
35
+ if (typeof raw.locale === "string") {
36
+ const locale = raw.locale.trim().toLowerCase();
37
+ if (LOCALES.includes(locale)) {
38
+ config.locale = locale;
39
+ }
40
+ }
41
+ if (typeof raw.vibe === "string") {
42
+ const vibe = raw.vibe.trim().toLowerCase();
43
+ if (VIBES.includes(vibe)) {
44
+ config.vibe = vibe;
45
+ }
46
+ }
47
+ return config;
48
+ }
49
+ //# sourceMappingURL=config.js.map
package/dist/format.js CHANGED
@@ -61,6 +61,7 @@ export function parseCommit(text, engine = "heuristic") {
61
61
  body: rest || undefined,
62
62
  breaking,
63
63
  engine,
64
+ locale: "en",
64
65
  };
65
66
  }
66
67
  export function stripFences(text) {
@@ -0,0 +1,32 @@
1
+ import type { ProposedMessage, StagedDiff } from "./types.ts";
2
+ export interface GitHubFormatOptions {
3
+ /** Soft cap for PR annotation noise. Default 20. */
4
+ maxAnnotations?: number;
5
+ }
6
+ type AnnotationType = "error" | "warning" | "notice";
7
+ /** Escape payload text for workflow command messages. */
8
+ export declare function escapeData(value: string): string;
9
+ /** Escape workflow command property values (file, title, …). */
10
+ export declare function escapeProperty(value: string): string;
11
+ export declare function annotation(type: AnnotationType, message: string, props?: {
12
+ file?: string;
13
+ line?: number;
14
+ title?: string;
15
+ }): string;
16
+ /** Heuristic risk tier → annotation level (breaking / fix weigh heavier). */
17
+ export declare function proposalAnnotationType(proposal: ProposedMessage): AnnotationType;
18
+ /**
19
+ * Emit GitHub Actions workflow commands so PR Checks show the proposed commit.
20
+ * Offline / local-safe: pure string formatting, no network.
21
+ */
22
+ export declare function formatGitHubAnnotations(proposal: ProposedMessage, diff: StagedDiff, options?: GitHubFormatOptions): string;
23
+ /** Append job outputs when running inside Actions (`GITHUB_OUTPUT`). */
24
+ export declare function appendGitHubJobOutput(proposal: ProposedMessage, diff: StagedDiff): void;
25
+ /** Build a short Markdown Job Summary for Actions (no env I/O). */
26
+ export declare function formatGitHubStepSummary(proposal: ProposedMessage, diff: StagedDiff): string;
27
+ /**
28
+ * Append a concise Job Summary when running inside Actions (`GITHUB_STEP_SUMMARY`).
29
+ * No-op when the env var is unset.
30
+ */
31
+ export declare function appendGitHubStepSummary(proposal: ProposedMessage, diff: StagedDiff): void;
32
+ export {};
package/dist/github.js ADDED
@@ -0,0 +1,162 @@
1
+ import { appendFileSync } from "node:fs";
2
+ import { formatCommit } from "./format.js";
3
+ /** Escape payload text for workflow command messages. */
4
+ export function escapeData(value) {
5
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
6
+ }
7
+ /** Escape workflow command property values (file, title, …). */
8
+ export function escapeProperty(value) {
9
+ return escapeData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
10
+ }
11
+ export function annotation(type, message, props = {}) {
12
+ const parts = [];
13
+ if (props.file)
14
+ parts.push(`file=${escapeProperty(props.file)}`);
15
+ if (props.line !== undefined && Number.isFinite(props.line)) {
16
+ parts.push(`line=${Math.max(1, Math.trunc(props.line))}`);
17
+ }
18
+ if (props.title)
19
+ parts.push(`title=${escapeProperty(props.title)}`);
20
+ const meta = parts.length > 0 ? ` ${parts.join(",")}` : "";
21
+ return `::${type}${meta}::${escapeData(message)}`;
22
+ }
23
+ /** Heuristic risk tier → annotation level (breaking / fix weigh heavier). */
24
+ export function proposalAnnotationType(proposal) {
25
+ if (proposal.breaking)
26
+ return "error";
27
+ switch (proposal.type) {
28
+ case "fix":
29
+ return "warning";
30
+ case "feat":
31
+ case "perf":
32
+ case "refactor":
33
+ return "notice";
34
+ default:
35
+ return "notice";
36
+ }
37
+ }
38
+ function oneLine(value, max = 200) {
39
+ const flat = value.replace(/\s+/g, " ").trim();
40
+ if (flat.length <= max)
41
+ return flat;
42
+ return `${flat.slice(0, max - 1)}…`;
43
+ }
44
+ function headerLine(proposal) {
45
+ return formatCommit({ ...proposal, body: undefined }).split("\n")[0] ?? "";
46
+ }
47
+ /**
48
+ * Emit GitHub Actions workflow commands so PR Checks show the proposed commit.
49
+ * Offline / local-safe: pure string formatting, no network.
50
+ */
51
+ export function formatGitHubAnnotations(proposal, diff, options = {}) {
52
+ const max = options.maxAnnotations ?? 20;
53
+ const lines = [];
54
+ const push = (line) => {
55
+ if (lines.length < max)
56
+ lines.push(line);
57
+ };
58
+ const primary = diff.files[0]?.path;
59
+ const title = headerLine(proposal);
60
+ push(annotation(proposalAnnotationType(proposal), title, {
61
+ file: primary,
62
+ title: "vibe-commit · title",
63
+ }));
64
+ if (proposal.breaking) {
65
+ const reason = proposal.breakingReason?.trim() ||
66
+ "Breaking change detected in staged diff.";
67
+ push(annotation("error", oneLine(reason), {
68
+ file: primary,
69
+ title: "vibe-commit · breaking",
70
+ }));
71
+ }
72
+ if (proposal.body?.trim()) {
73
+ push(annotation("notice", oneLine(proposal.body), {
74
+ file: primary,
75
+ title: "vibe-commit · body",
76
+ }));
77
+ }
78
+ for (const file of diff.files) {
79
+ const status = file.status === "A"
80
+ ? "add"
81
+ : file.status === "D"
82
+ ? "delete"
83
+ : file.status === "R"
84
+ ? "rename"
85
+ : "update";
86
+ push(annotation("notice", `${status} \`${file.path}\` (+${file.additions} −${file.deletions})`, {
87
+ file: file.path,
88
+ title: "vibe-commit · file",
89
+ }));
90
+ }
91
+ const totals = diff.files.reduce((acc, file) => {
92
+ acc.add += file.additions;
93
+ acc.del += file.deletions;
94
+ return acc;
95
+ }, { add: 0, del: 0 });
96
+ push(annotation("notice", `${diff.files.length} files · +${totals.add} / −${totals.del} · engine ${proposal.engine}`, { title: "vibe-commit · meta" }));
97
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
98
+ }
99
+ /** Append job outputs when running inside Actions (`GITHUB_OUTPUT`). */
100
+ export function appendGitHubJobOutput(proposal, diff) {
101
+ const path = process.env.GITHUB_OUTPUT;
102
+ if (!path)
103
+ return;
104
+ const body = [
105
+ `title=${oneLine(headerLine(proposal), 256)}`,
106
+ `subject=${oneLine(proposal.subject, 200)}`,
107
+ `type=${proposal.type}`,
108
+ `scope=${proposal.scope ?? ""}`,
109
+ `breaking=${proposal.breaking}`,
110
+ `engine=${proposal.engine}`,
111
+ `findings=${diff.files.length}`,
112
+ ].join("\n");
113
+ appendFileSync(path, `${body}\n`, "utf8");
114
+ }
115
+ /** Build a short Markdown Job Summary for Actions (no env I/O). */
116
+ export function formatGitHubStepSummary(proposal, diff) {
117
+ const totals = diff.files.reduce((acc, file) => {
118
+ acc.add += file.additions;
119
+ acc.del += file.deletions;
120
+ return acc;
121
+ }, { add: 0, del: 0 });
122
+ const lines = [
123
+ "## vibe-commit",
124
+ "",
125
+ `| | |`,
126
+ `| --- | --- |`,
127
+ `| **Title** | ${headerLine(proposal)} |`,
128
+ `| **Type** | \`${proposal.type}\`${proposal.scope ? ` (\`${proposal.scope}\`)` : ""} |`,
129
+ `| **Breaking** | ${proposal.breaking} |`,
130
+ `| **Engine** | ${proposal.engine} |`,
131
+ `| **Files** | ${diff.files.length} (+${totals.add} / −${totals.del}) staged |`,
132
+ "",
133
+ ];
134
+ if (proposal.breakingReason?.trim()) {
135
+ lines.push(`**Breaking:** _${oneLine(proposal.breakingReason, 280)}_`, "");
136
+ }
137
+ else if (proposal.body?.trim()) {
138
+ lines.push(`_${oneLine(proposal.body, 280)}_`, "");
139
+ }
140
+ const findings = [];
141
+ for (const file of diff.files.slice(0, 8)) {
142
+ findings.push(`- \`${file.path}\` (${file.status}, +${file.additions} −${file.deletions})`);
143
+ }
144
+ if (findings.length > 0) {
145
+ lines.push("### Staged files", "", ...findings, "");
146
+ }
147
+ else {
148
+ lines.push("_No staged files._", "");
149
+ }
150
+ return lines.join("\n");
151
+ }
152
+ /**
153
+ * Append a concise Job Summary when running inside Actions (`GITHUB_STEP_SUMMARY`).
154
+ * No-op when the env var is unset.
155
+ */
156
+ export function appendGitHubStepSummary(proposal, diff) {
157
+ const path = process.env.GITHUB_STEP_SUMMARY;
158
+ if (!path)
159
+ return;
160
+ appendFileSync(path, `${formatGitHubStepSummary(proposal, diff)}\n`, "utf8");
161
+ }
162
+ //# sourceMappingURL=github.js.map
@@ -1,9 +1,10 @@
1
- import type { CommitType, DiffHunk, ProposedMessage, StagedDiff, StagedFile, Vibe } from "./types.ts";
1
+ import type { CommitType, DiffHunk, Locale, ProposedMessage, StagedDiff, StagedFile, Vibe } from "./types.ts";
2
2
  export interface HeuristicInput {
3
3
  diff: StagedDiff;
4
4
  type?: CommitType;
5
5
  scope?: string;
6
6
  vibe: Vibe;
7
+ locale: Locale;
7
8
  /** Force breaking: true (ORs with inferBreaking). */
8
9
  breaking?: boolean;
9
10
  }
@@ -12,7 +13,7 @@ export declare function inferType(diff: StagedDiff): CommitType;
12
13
  export declare function primaryRole(path: string): CommitType;
13
14
  export declare function classifyPath(path: string): CommitType[];
14
15
  export declare function inferScope(files: StagedFile[]): string | undefined;
15
- export declare function inferSubject(diff: StagedDiff, type: CommitType, scope?: string): string;
16
+ export declare function inferSubject(diff: StagedDiff, type: CommitType, scope: string | undefined, locale: Locale): string;
16
17
  export type BreakingKind = "export-removed" | "major-bump" | "footer" | "forced";
17
18
  export declare function inferBreaking(diff: StagedDiff): boolean;
18
19
  /** Why breaking was set — heuristic signal first, then --breaking. */
package/dist/heuristic.js CHANGED
@@ -23,10 +23,10 @@ const FEAT_WORD_RE = /\b(add|added|feat|feature|implement|introduce|support|enab
23
23
  const REFACTOR_WORD_RE = /\b(refactor|rename|move|extract|simplify|cleanup|clean up|restructure)\b/i;
24
24
  const PERF_WORD_RE = /\b(perf|performance|optimiz|cache|faster|speed)\b/i;
25
25
  export function proposeHeuristic(input) {
26
- const { diff, vibe } = input;
26
+ const { diff, vibe, locale } = input;
27
27
  const type = input.type ?? inferType(diff);
28
28
  const scope = input.scope ?? inferScope(diff.files);
29
- const subject = inferSubject(diff, type, scope);
29
+ const subject = inferSubject(diff, type, scope, locale);
30
30
  const body = inferBody(diff, type);
31
31
  const forced = Boolean(input.breaking);
32
32
  const kind = detectBreakingKind(diff, forced);
@@ -35,6 +35,7 @@ export function proposeHeuristic(input) {
35
35
  const message = {
36
36
  ...styled,
37
37
  engine: "heuristic",
38
+ locale,
38
39
  breakingReason: kind ? formatBreakingReason(kind) : undefined,
39
40
  };
40
41
  // Forced --breaking: prefer subject in the footer (not bilingual default).
@@ -144,10 +145,11 @@ export function inferScope(files) {
144
145
  return undefined;
145
146
  return name.slice(0, 32);
146
147
  }
147
- export function inferSubject(diff, type, scope) {
148
+ export function inferSubject(diff, type, scope, locale) {
148
149
  const files = diff.files;
149
- if (files.length === 0)
150
- return "update project files";
150
+ if (files.length === 0) {
151
+ return normalizeSubject(locale === "zh" ? "更新项目文件" : "update project files");
152
+ }
151
153
  const added = files.filter((file) => file.status === "A");
152
154
  const deleted = files.filter((file) => file.status === "D");
153
155
  const renamed = files.filter((file) => file.status === "R");
@@ -156,31 +158,67 @@ export function inferSubject(diff, type, scope) {
156
158
  added.length >= files.length - 1 &&
157
159
  !scope &&
158
160
  type === "feat") {
159
- return normalizeSubject("add initial implementation");
161
+ return normalizeSubject(locale === "zh" ? "添加初始实现" : "add initial implementation");
160
162
  }
161
163
  if (renamed.length === 1 && files.length === 1) {
162
164
  const file = renamed[0];
163
- return normalizeSubject(`rename ${stem(file.fromPath ?? file.path)} to ${stem(file.path)}`);
165
+ const fromName = stem(file.fromPath ?? file.path);
166
+ const toName = stem(file.path);
167
+ return normalizeSubject(locale === "zh"
168
+ ? `重命名 ${fromName} 为 ${toName}`
169
+ : `rename ${fromName} to ${toName}`);
164
170
  }
165
171
  if (deleted.length === files.length) {
166
- return normalizeSubject(`remove ${joinNames(deleted.map((file) => stem(file.path)))}`);
172
+ const names = joinNames(deleted.map((file) => stem(file.path)));
173
+ return normalizeSubject(locale === "zh" ? `删除 ${names}` : `remove ${names}`);
167
174
  }
168
175
  if (idents.length > 0 && (type === "feat" || type === "fix")) {
169
- const verb = type === "fix" ? "fix" : "add";
176
+ const verb = type === "fix" ? (locale === "zh" ? "修复" : "fix") : (locale === "zh" ? "添加" : "add");
170
177
  return normalizeSubject(`${verb} ${joinNames(idents.map(humanizeIdent))}`);
171
178
  }
172
179
  if (added.length === 1 && files.length === 1) {
173
- return normalizeSubject(`add ${stem(added[0].path)}`);
180
+ const name = stem(added[0].path);
181
+ return normalizeSubject(locale === "zh" ? `添加 ${name}` : `add ${name}`);
174
182
  }
175
183
  if (added.length > 1 && added.length === files.length) {
176
- return normalizeSubject(`add ${summarizeFiles(added, scope)}`);
184
+ const summary = summarizeFiles(added, scope);
185
+ return normalizeSubject(locale === "zh" ? `添加 ${summary}` : `add ${summary}`);
177
186
  }
178
187
  const focus = files.length === 1 ? stem(files[0].path) : summarizeFiles(files, scope);
179
- return normalizeSubject(verbFor(type, focus, files));
188
+ return normalizeSubject(verbFor(type, focus, files, locale));
180
189
  }
181
- function verbFor(type, focus, files) {
190
+ function verbFor(type, focus, files, locale) {
182
191
  const mostlyAdd = files.reduce((sum, file) => sum + file.additions, 0) >
183
192
  files.reduce((sum, file) => sum + file.deletions, 0) * 1.4;
193
+ if (locale === "zh") {
194
+ switch (type) {
195
+ case "feat":
196
+ return mostlyAdd ? `添加 ${focus}` : `更新 ${focus}`;
197
+ case "fix":
198
+ return `修复 ${focus}`;
199
+ case "docs":
200
+ return files.some((file) => /readme/i.test(file.path))
201
+ ? "更新 readme"
202
+ : `更新 ${focus}`;
203
+ case "test":
204
+ return mostlyAdd ? `添加 ${focus} 测试` : `更新 ${focus} 测试`;
205
+ case "refactor":
206
+ return `重构 ${focus}`;
207
+ case "perf":
208
+ return `优化 ${focus} 性能`;
209
+ case "style":
210
+ return `格式化 ${focus}`;
211
+ case "ci":
212
+ return `更新 ${focus} 工作流`;
213
+ case "build":
214
+ return `更新 ${focus} 构建`;
215
+ case "chore":
216
+ if (files.some((file) => /package(-lock)?\.json|pnpm-lock|yarn\.lock/.test(file.path))) {
217
+ return "更新依赖";
218
+ }
219
+ return `更新 ${focus}`;
220
+ }
221
+ }
184
222
  switch (type) {
185
223
  case "feat":
186
224
  return mostlyAdd ? `add ${focus}` : `update ${focus}`;
package/dist/hook.js CHANGED
@@ -4,8 +4,9 @@ import { dirname, isAbsolute, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { formatCommit } from "./format.js";
6
6
  import { assertRepo, GitError, inspectStaged } from "./git.js";
7
- import { defaultVibeFromEnv } from "./parse-args.js";
7
+ import { defaultLocaleFromEnv, defaultVibeFromEnv } from "./parse-args.js";
8
8
  import { proposeMessage } from "./propose.js";
9
+ import { loadProjectConfig } from "./config.js";
9
10
  export const HOOK_BEGIN = "# BEGIN VIBE-COMMIT";
10
11
  export const HOOK_END = "# END VIBE-COMMIT";
11
12
  export const HOOK_NAME = "prepare-commit-msg";
@@ -42,8 +43,10 @@ export async function runPrepareCommitMsg(messageFile, source, cwd = process.cwd
42
43
  const diff = inspectStaged(cwd);
43
44
  // Avoid hanging `git commit` on network — hooks stay offline unless opted in.
44
45
  const useAi = env.VIBE_COMMIT_HOOK_AI === "1";
46
+ const projectConfig = loadProjectConfig(cwd);
45
47
  const message = await proposeMessage(diff, {
46
- vibe: defaultVibeFromEnv(env),
48
+ vibe: projectConfig?.vibe ?? defaultVibeFromEnv(env),
49
+ locale: projectConfig?.locale ?? defaultLocaleFromEnv(env),
47
50
  preferAi: useAi,
48
51
  }, env);
49
52
  const formatted = formatCommit(message);
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { inspectStaged, commitWithMessage, GitError } from "./git.ts";
4
4
  export { proposeHeuristic, inferType, inferScope, inferSubject, inferBreaking, detectBreakingKind, formatBreakingReason, breakingFooterDetail, DEFAULT_BREAKING_FOOTER_DETAIL, ensureBreakingFooter, classifyPath, } from "./heuristic.ts";
5
5
  export type { BreakingKind } from "./heuristic.ts";
6
6
  export { parseArgs, helpText, UsageError } from "./parse-args.ts";
7
+ export { annotation, appendGitHubJobOutput, appendGitHubStepSummary, escapeData, escapeProperty, formatGitHubAnnotations, formatGitHubStepSummary, proposalAnnotationType, } from "./github.ts";
7
8
  export { assembleDiff, parseUnifiedDiff } from "./parse-diff.ts";
8
9
  export { installHook, uninstallHook, runPrepareCommitMsg, shouldSkipPrepareCommitMsg, hasNonCommentContent, HOOK_BEGIN, HOOK_END, } from "./hook.ts";
9
10
  export { proposeMessage } from "./propose.ts";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { formatCommit, parseCommit, normalizeSubject } from "./format.js";
3
3
  export { inspectStaged, commitWithMessage, GitError } from "./git.js";
4
4
  export { proposeHeuristic, inferType, inferScope, inferSubject, inferBreaking, detectBreakingKind, formatBreakingReason, breakingFooterDetail, DEFAULT_BREAKING_FOOTER_DETAIL, ensureBreakingFooter, classifyPath, } from "./heuristic.js";
5
5
  export { parseArgs, helpText, UsageError } from "./parse-args.js";
6
+ export { annotation, appendGitHubJobOutput, appendGitHubStepSummary, escapeData, escapeProperty, formatGitHubAnnotations, formatGitHubStepSummary, proposalAnnotationType, } from "./github.js";
6
7
  export { assembleDiff, parseUnifiedDiff } from "./parse-diff.js";
7
8
  export { installHook, uninstallHook, runPrepareCommitMsg, shouldSkipPrepareCommitMsg, hasNonCommentContent, HOOK_BEGIN, HOOK_END, } from "./hook.js";
8
9
  export { proposeMessage } from "./propose.js";
@@ -1,8 +1,11 @@
1
- import { type CliOptions, type CommitType, type Vibe } from "./types.ts";
1
+ import { type CliOptions, type CommitType, type Locale, type Vibe } from "./types.ts";
2
2
  export declare function defaultVibeFromEnv(env?: NodeJS.ProcessEnv): Vibe;
3
- export declare function parseArgs(argv: string[], env?: NodeJS.ProcessEnv): CliOptions;
3
+ export declare function defaultLocaleFromEnv(env?: NodeJS.ProcessEnv): Locale;
4
+ export declare function parseArgs(argv: string[], env?: NodeJS.ProcessEnv, cwd?: string): CliOptions;
5
+ export declare function parseMaxAnnotations(value: string | undefined): number;
4
6
  export declare function parseType(value: string | undefined): CommitType;
5
7
  export declare function parseVibe(value: string | undefined): Vibe;
8
+ export declare function parseLocale(value: string | undefined): Locale;
6
9
  export declare function sanitizeScope(value: string): string;
7
10
  export declare class UsageError extends Error {
8
11
  constructor(message: string);
@@ -1,24 +1,32 @@
1
- import { COMMIT_TYPES, VIBES, } from "./types.js";
1
+ import { COMMIT_TYPES, LOCALES, VIBES, } from "./types.js";
2
+ import { loadProjectConfig } from "./config.js";
2
3
  const HELP = `vibe-commit — stylish conventional commits, offline-first
4
+ 有品味的 Conventional Commits(离线优先)
3
5
 
4
- Usage
6
+ Usage · 用法
5
7
  vibe-commit [options]
6
8
  vibe-commit install [--path <repo>]
7
9
  vibe-commit uninstall [--path <repo>]
8
10
 
9
- Commands
11
+ Commands · 命令
10
12
  install Install a prepare-commit-msg hook in the repo
11
13
  uninstall Remove the managed prepare-commit-msg hook
12
14
 
13
- Options
15
+ Options · 参数
14
16
  --commit Create the commit with the proposed message
15
17
  --dry-run Print the message only (default)
16
18
  --type <type> feat | fix | docs | style | refactor | perf | test | build | ci | chore
17
19
  --scope <scope> Override inferred scope
18
20
  --vibe <style> playful | pro | terse (default: pro)
21
+ --locale <locale> en | zh (default: en)
19
22
  --no-ai Skip AI even if OPENAI_API_KEY is set
20
23
  --breaking Force ! + BREAKING CHANGE footer (text = subject)
21
24
  --json Print a machine-readable proposal
25
+ --github Emit GitHub Actions annotations (::notice/warning/error)
26
+ 输出 Actions 注解;有 GITHUB_OUTPUT / GITHUB_STEP_SUMMARY 时写输出与 Job Summary
27
+ --quiet Suppress the pretty terminal report (--json / --github still emit)
28
+ 抑制漂亮终端报告;--json / --github 仍照常
29
+ --max-annotations <n> Cap GitHub annotation lines (default: 20) · 限制注解条数
22
30
  --path <repo> Repo root for install/uninstall (default: cwd)
23
31
  -h, --help Show help
24
32
  -v, --version Show version
@@ -27,19 +35,38 @@ Env
27
35
  OPENAI_API_KEY Optional. When set, draft with an OpenAI-compatible API
28
36
  OPENAI_MODEL Default: gpt-4o-mini
29
37
  OPENAI_BASE_URL Default: https://api.openai.com/v1
30
- VIBE / VIBE_COMMIT_VIBE Default vibe when --vibe is omitted
38
+ VIBE / VIBE_COMMIT_VIBE Default vibe when --vibe is omitted
39
+ VIBE_COMMIT_LOCALE Default locale when --locale is omitted
31
40
  VIBE_COMMIT_HOOK_AI Set to 1 to allow AI inside the git hook (off by default)
32
41
 
42
+ With --github, annotations go to stdout (or stderr when combined with --json) so
43
+ PR Checks can surface the proposed commit title / body / staged files. Job outputs
44
+ title/subject/type/scope/breaking/engine/findings are written when GITHUB_OUTPUT
45
+ is set (findings = staged file count). A short Markdown job summary is appended when
46
+ GITHUB_STEP_SUMMARY is set. --quiet hides the pretty report only.
47
+
48
+ 配合 --github 时注解走 stdout(与 --json 联用则走 stderr,保持 JSON 干净)。
49
+ 有 GITHUB_OUTPUT 时写入 title/subject 等字段;有 GITHUB_STEP_SUMMARY 时追加 Job Summary。
50
+ --quiet 只隐藏漂亮终端报告。
51
+
33
52
  Git hook
34
53
  After \`vibe-commit install\`, plain \`git commit\` (no -m) fills an empty /
35
54
  comment-only message file with a vibe-commit proposal. Skips merge, squash,
36
55
  amend (source=commit), and any message that already has non-comment text.
37
56
 
38
- Examples
57
+ Project config
58
+ .vibe-commit.json or package.json#vibeCommit
59
+ { "locale": "zh", "vibe": "pro" }
60
+ Precedence: CLI > env > project config > default
61
+
62
+ Examples · 示例
39
63
  git add -p
40
64
  vibe-commit
41
65
  vibe-commit --vibe playful --commit
66
+ vibe-commit --locale zh --dry-run
42
67
  vibe-commit --breaking --dry-run
68
+ vibe-commit --github --quiet --no-ai
69
+ vibe-commit --github --json --max-annotations 10
43
70
  git commit -m "$(vibe-commit --dry-run)"
44
71
  vibe-commit install
45
72
  vibe-commit uninstall --path ~/projects/app
@@ -48,14 +75,23 @@ export function defaultVibeFromEnv(env = process.env) {
48
75
  const raw = (env.VIBE_COMMIT_VIBE ?? env.VIBE ?? "pro").trim().toLowerCase();
49
76
  return VIBES.includes(raw) ? raw : "pro";
50
77
  }
51
- export function parseArgs(argv, env = process.env) {
78
+ export function defaultLocaleFromEnv(env = process.env) {
79
+ const raw = (env.VIBE_COMMIT_LOCALE ?? "en").trim().toLowerCase();
80
+ return LOCALES.includes(raw) ? raw : "en";
81
+ }
82
+ export function parseArgs(argv, env = process.env, cwd = process.cwd()) {
83
+ const projectConfig = loadProjectConfig(cwd);
52
84
  const options = {
53
85
  command: "propose",
54
86
  commit: false,
55
87
  dryRun: false,
56
- vibe: defaultVibeFromEnv(env),
88
+ vibe: projectConfig?.vibe ?? defaultVibeFromEnv(env),
89
+ locale: projectConfig?.locale ?? defaultLocaleFromEnv(env),
57
90
  noAi: false,
58
91
  json: false,
92
+ github: false,
93
+ quiet: false,
94
+ maxAnnotations: 20,
59
95
  help: false,
60
96
  version: false,
61
97
  breaking: false,
@@ -96,6 +132,21 @@ export function parseArgs(argv, env = process.env) {
96
132
  options.json = true;
97
133
  continue;
98
134
  }
135
+ if (arg === "--github") {
136
+ options.github = true;
137
+ continue;
138
+ }
139
+ if (arg === "--quiet") {
140
+ options.quiet = true;
141
+ continue;
142
+ }
143
+ if (arg === "--max-annotations" || arg.startsWith("--max-annotations=")) {
144
+ const value = arg.includes("=")
145
+ ? arg.slice("--max-annotations=".length)
146
+ : argv[++i];
147
+ options.maxAnnotations = parseMaxAnnotations(value);
148
+ continue;
149
+ }
99
150
  if (arg === "--breaking") {
100
151
  options.breaking = true;
101
152
  continue;
@@ -117,6 +168,11 @@ export function parseArgs(argv, env = process.env) {
117
168
  options.vibe = parseVibe(value);
118
169
  continue;
119
170
  }
171
+ if (arg === "--locale" || arg.startsWith("--locale=")) {
172
+ const value = arg.includes("=") ? arg.slice("--locale=".length) : argv[++i];
173
+ options.locale = parseLocale(value);
174
+ continue;
175
+ }
120
176
  if (arg === "--path" || arg.startsWith("--path=")) {
121
177
  const value = arg.includes("=") ? arg.slice("--path=".length) : argv[++i];
122
178
  if (!value)
@@ -160,6 +216,16 @@ function parseCommand(value) {
160
216
  return null;
161
217
  }
162
218
  }
219
+ export function parseMaxAnnotations(value) {
220
+ const raw = value?.trim();
221
+ if (!raw)
222
+ throw new UsageError("Missing value for --max-annotations");
223
+ const n = Number(raw);
224
+ if (!Number.isInteger(n) || n < 1) {
225
+ throw new UsageError("--max-annotations must be a positive integer");
226
+ }
227
+ return n;
228
+ }
163
229
  export function parseType(value) {
164
230
  const raw = value?.trim().toLowerCase();
165
231
  if (!raw)
@@ -178,6 +244,15 @@ export function parseVibe(value) {
178
244
  }
179
245
  return raw;
180
246
  }
247
+ export function parseLocale(value) {
248
+ const raw = value?.trim().toLowerCase();
249
+ if (!raw)
250
+ throw new UsageError("Missing value for --locale");
251
+ if (!LOCALES.includes(raw)) {
252
+ throw new UsageError(`Invalid --locale "${value}". Use: ${LOCALES.join(", ")}`);
253
+ }
254
+ return raw;
255
+ }
181
256
  export function sanitizeScope(value) {
182
257
  const scope = value.trim().replace(/^[\s(/]+|[\s)/]+$/g, "");
183
258
  if (!scope)
package/dist/propose.js CHANGED
@@ -6,6 +6,7 @@ export async function proposeMessage(diff, options, env = process.env) {
6
6
  type: options.type,
7
7
  scope: options.scope,
8
8
  vibe: options.vibe,
9
+ locale: options.locale,
9
10
  breaking: options.breaking,
10
11
  });
11
12
  let message = heuristic;
@@ -15,6 +16,7 @@ export async function proposeMessage(diff, options, env = process.env) {
15
16
  const ai = await proposeWithAi({
16
17
  diff,
17
18
  vibe: options.vibe,
19
+ locale: options.locale,
18
20
  type: options.type,
19
21
  scope: options.scope,
20
22
  }, env);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export declare const VIBES: readonly ["playful", "pro", "terse"];
2
2
  export type Vibe = (typeof VIBES)[number];
3
+ export declare const LOCALES: readonly ["en", "zh"];
4
+ export type Locale = (typeof LOCALES)[number];
3
5
  export declare const COMMIT_TYPES: readonly ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore"];
4
6
  export type CommitType = (typeof COMMIT_TYPES)[number];
5
7
  export type FileStatus = "A" | "M" | "D" | "R" | "C" | "T" | "U";
@@ -30,11 +32,13 @@ export interface ProposedMessage {
30
32
  /** Short cause when breaking (export removed / major bump / --breaking / footer). */
31
33
  breakingReason?: string;
32
34
  engine: Engine;
35
+ locale: Locale;
33
36
  }
34
37
  export interface ProposeOptions {
35
38
  type?: CommitType;
36
39
  scope?: string;
37
40
  vibe: Vibe;
41
+ locale: Locale;
38
42
  preferAi?: boolean;
39
43
  /** Force a breaking change (! + BREAKING CHANGE footer). */
40
44
  breaking?: boolean;
@@ -47,8 +51,15 @@ export interface CliOptions {
47
51
  type?: CommitType;
48
52
  scope?: string;
49
53
  vibe: Vibe;
54
+ locale: Locale;
50
55
  noAi: boolean;
51
56
  json: boolean;
57
+ /** Emit GitHub Actions annotations / job outputs / step summary. */
58
+ github: boolean;
59
+ /** Suppress the pretty terminal report (--json / --github still emit). */
60
+ quiet: boolean;
61
+ /** Soft cap for --github annotation lines (default 20). */
62
+ maxAnnotations: number;
52
63
  help: boolean;
53
64
  version: boolean;
54
65
  /** Force breaking change marker and footer. */
package/dist/types.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export const VIBES = ["playful", "pro", "terse"];
2
+ export const LOCALES = ["en", "zh"];
2
3
  export const COMMIT_TYPES = [
3
4
  "feat",
4
5
  "fix",
@@ -0,0 +1,49 @@
1
+ # Example: surface a proposed commit message as Actions annotations + job summary.
2
+ # Copy into your app repo as .github/workflows/vibe-commit.yml
3
+ # (typically on pull_request so Checks show the draft commit).
4
+ #
5
+ # 示例:把拟写的 commit message 做成 Actions 注解 + Job Summary。
6
+ # 复制到业务仓的 .github/workflows/vibe-commit.yml(通常挂在 pull_request 上)。
7
+ #
8
+ # Note: vibe-commit reads *staged* changes. Stage intentionally in CI, or
9
+ # `git add -A` / `git add -u` before the step if you want the working tree.
10
+
11
+ name: Vibe commit
12
+
13
+ on:
14
+ pull_request:
15
+
16
+ jobs:
17
+ vibe-commit:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 20
25
+
26
+ - name: Stage working tree (demo)
27
+ run: git add -A
28
+
29
+ - name: Propose commit (annotations + summary)
30
+ id: vibe
31
+ run: |
32
+ npx --yes @webywz/vibe-commit@^0.1.14 \
33
+ --github \
34
+ --quiet \
35
+ --max-annotations 20 \
36
+ --no-ai
37
+ # --github → ::notice / ::warning / ::error on the PR Checks UI
38
+ # --quiet → suppress the pretty terminal report (annotations / summary / outputs still emit)
39
+ # --max-annotations → soft-cap annotation noise (default 20)
40
+ # When GITHUB_OUTPUT is set, also writes: title, subject, type, scope, breaking, engine, findings
41
+ # findings = staged file count
42
+ # When GITHUB_STEP_SUMMARY is set, appends a short Markdown job summary
43
+
44
+ # Optional: use job outputs from a later step
45
+ # - name: Echo title
46
+ # run: echo "proposed ${{ steps.vibe.outputs.title }} (findings=${{ steps.vibe.outputs.findings }})"
47
+
48
+ # Optional: Job Summary is written automatically when GITHUB_STEP_SUMMARY is set
49
+ # (Actions injects it). No extra step required.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webywz/vibe-commit",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Stylish conventional commit messages with personality. Offline-first, AI-optional.",
5
5
  "keywords": [
6
6
  "git",