@webywz/vibe-commit 0.1.14 → 0.1.16
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 +41 -0
- package/dist/ai.d.ts +2 -1
- package/dist/ai.js +7 -2
- package/dist/cli.js +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +49 -0
- package/dist/format.js +1 -0
- package/dist/heuristic.d.ts +3 -2
- package/dist/heuristic.js +51 -13
- package/dist/hook.js +5 -2
- package/dist/parse-args.d.ts +4 -2
- package/dist/parse-args.js +33 -4
- package/dist/propose.js +2 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ vibe-commit install # optional prepare-commit-msg hook
|
|
|
57
57
|
| `--type <type>` | Force `feat` / `fix` / `docs` / `style` / `refactor` / `perf` / `test` / `build` / `ci` / `chore` |
|
|
58
58
|
| `--scope <scope>` | Override inferred scope · 覆盖推断出的 scope |
|
|
59
59
|
| `--vibe <style>` | `playful` \| `pro` \| `terse` (default: `pro`) |
|
|
60
|
+
| `--locale <locale>` | `en` \| `zh` (default: `en`) · 提交信息主题语言 |
|
|
60
61
|
| `--no-ai` | Skip the API even if `OPENAI_API_KEY` is set · 即使有 Key 也不走 AI |
|
|
61
62
|
| `--breaking` | Force breaking (`!` + `BREAKING CHANGE:` footer = **subject**) · 强制破坏性变更(`!` + footer 文本即 **subject**) |
|
|
62
63
|
| `--json` | Machine-readable proposal · 机器可读提案 |
|
|
@@ -125,6 +126,45 @@ Hook stays **offline by default** (`VIBE_COMMIT_HOOK_AI=1` to allow AI). Unique
|
|
|
125
126
|
|
|
126
127
|
钩子默认离线;设 `VIBE_COMMIT_HOOK_AI=1` 才允许 AI。用标记块保证干净卸载。已有外来钩子会备份并链式调用。
|
|
127
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
|
+
|
|
128
168
|
### AI (optional) · AI(可选)
|
|
129
169
|
|
|
130
170
|
```bash
|
|
@@ -132,6 +172,7 @@ export OPENAI_API_KEY=sk-...
|
|
|
132
172
|
export OPENAI_MODEL=gpt-4o-mini # optional
|
|
133
173
|
export OPENAI_BASE_URL=https://api.openai.com/v1
|
|
134
174
|
export VIBE_COMMIT_VIBE=pro # or VIBE
|
|
175
|
+
export VIBE_COMMIT_LOCALE=zh # or en (default)
|
|
135
176
|
```
|
|
136
177
|
|
|
137
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.
|
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
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/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/parse-args.d.ts
CHANGED
|
@@ -1,9 +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;
|
|
4
5
|
export declare function parseMaxAnnotations(value: string | undefined): number;
|
|
5
6
|
export declare function parseType(value: string | undefined): CommitType;
|
|
6
7
|
export declare function parseVibe(value: string | undefined): Vibe;
|
|
8
|
+
export declare function parseLocale(value: string | undefined): Locale;
|
|
7
9
|
export declare function sanitizeScope(value: string): string;
|
|
8
10
|
export declare class UsageError extends Error {
|
|
9
11
|
constructor(message: string);
|
package/dist/parse-args.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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
|
|
3
4
|
有品味的 Conventional Commits(离线优先)
|
|
4
5
|
|
|
@@ -17,6 +18,7 @@ Options · 参数
|
|
|
17
18
|
--type <type> feat | fix | docs | style | refactor | perf | test | build | ci | chore
|
|
18
19
|
--scope <scope> Override inferred scope
|
|
19
20
|
--vibe <style> playful | pro | terse (default: pro)
|
|
21
|
+
--locale <locale> en | zh (default: en)
|
|
20
22
|
--no-ai Skip AI even if OPENAI_API_KEY is set
|
|
21
23
|
--breaking Force ! + BREAKING CHANGE footer (text = subject)
|
|
22
24
|
--json Print a machine-readable proposal
|
|
@@ -33,7 +35,8 @@ Env
|
|
|
33
35
|
OPENAI_API_KEY Optional. When set, draft with an OpenAI-compatible API
|
|
34
36
|
OPENAI_MODEL Default: gpt-4o-mini
|
|
35
37
|
OPENAI_BASE_URL Default: https://api.openai.com/v1
|
|
36
|
-
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
|
|
37
40
|
VIBE_COMMIT_HOOK_AI Set to 1 to allow AI inside the git hook (off by default)
|
|
38
41
|
|
|
39
42
|
With --github, annotations go to stdout (or stderr when combined with --json) so
|
|
@@ -51,10 +54,16 @@ Git hook
|
|
|
51
54
|
comment-only message file with a vibe-commit proposal. Skips merge, squash,
|
|
52
55
|
amend (source=commit), and any message that already has non-comment text.
|
|
53
56
|
|
|
57
|
+
Project config
|
|
58
|
+
.vibe-commit.json or package.json#vibeCommit
|
|
59
|
+
{ "locale": "zh", "vibe": "pro" }
|
|
60
|
+
Precedence: CLI > env > project config > default
|
|
61
|
+
|
|
54
62
|
Examples · 示例
|
|
55
63
|
git add -p
|
|
56
64
|
vibe-commit
|
|
57
65
|
vibe-commit --vibe playful --commit
|
|
66
|
+
vibe-commit --locale zh --dry-run
|
|
58
67
|
vibe-commit --breaking --dry-run
|
|
59
68
|
vibe-commit --github --quiet --no-ai
|
|
60
69
|
vibe-commit --github --json --max-annotations 10
|
|
@@ -66,12 +75,18 @@ export function defaultVibeFromEnv(env = process.env) {
|
|
|
66
75
|
const raw = (env.VIBE_COMMIT_VIBE ?? env.VIBE ?? "pro").trim().toLowerCase();
|
|
67
76
|
return VIBES.includes(raw) ? raw : "pro";
|
|
68
77
|
}
|
|
69
|
-
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);
|
|
70
84
|
const options = {
|
|
71
85
|
command: "propose",
|
|
72
86
|
commit: false,
|
|
73
87
|
dryRun: false,
|
|
74
|
-
vibe: defaultVibeFromEnv(env),
|
|
88
|
+
vibe: projectConfig?.vibe ?? defaultVibeFromEnv(env),
|
|
89
|
+
locale: projectConfig?.locale ?? defaultLocaleFromEnv(env),
|
|
75
90
|
noAi: false,
|
|
76
91
|
json: false,
|
|
77
92
|
github: false,
|
|
@@ -153,6 +168,11 @@ export function parseArgs(argv, env = process.env) {
|
|
|
153
168
|
options.vibe = parseVibe(value);
|
|
154
169
|
continue;
|
|
155
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
|
+
}
|
|
156
176
|
if (arg === "--path" || arg.startsWith("--path=")) {
|
|
157
177
|
const value = arg.includes("=") ? arg.slice("--path=".length) : argv[++i];
|
|
158
178
|
if (!value)
|
|
@@ -224,6 +244,15 @@ export function parseVibe(value) {
|
|
|
224
244
|
}
|
|
225
245
|
return raw;
|
|
226
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
|
+
}
|
|
227
256
|
export function sanitizeScope(value) {
|
|
228
257
|
const scope = value.trim().replace(/^[\s(/]+|[\s)/]+$/g, "");
|
|
229
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,6 +51,7 @@ 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;
|
|
52
57
|
/** Emit GitHub Actions annotations / job outputs / step summary. */
|
package/dist/types.js
CHANGED