@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 +50 -2
- package/dist/ai.d.ts +2 -1
- package/dist/ai.js +7 -2
- package/dist/cli.js +38 -6
- package/dist/config.d.ts +6 -0
- package/dist/config.js +49 -0
- package/dist/format.js +1 -0
- package/dist/github.d.ts +32 -0
- package/dist/github.js +162 -0
- package/dist/heuristic.d.ts +3 -2
- package/dist/heuristic.js +51 -13
- package/dist/hook.js +5 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/parse-args.d.ts +5 -2
- package/dist/parse-args.js +83 -8
- package/dist/propose.js +2 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/examples/github-actions.yml +49 -0
- package/package.json +1 -1
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
|
|
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/中文)
|
|
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 (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
}
|
package/dist/config.d.ts
ADDED
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
package/dist/github.d.ts
ADDED
|
@@ -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
|
package/dist/heuristic.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/dist/parse-args.d.ts
CHANGED
|
@@ -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
|
|
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);
|
package/dist/parse-args.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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.
|