@yourfam/yf-commit 0.1.0 → 0.2.0
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 +122 -38
- package/package.json +7 -3
- package/src/args.js +6 -3
- package/src/diff-profile.js +114 -0
- package/src/index.js +4 -1
- package/src/llm.js +2 -1
- package/src/prompt.js +19 -6
- package/src/types.js +63 -4
package/README.md
CHANGED
|
@@ -2,98 +2,182 @@
|
|
|
2
2
|
|
|
3
3
|
AI commit message from your staged git diff.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
npx @yourfam/yf-commit
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Writes a conventional first line (`emoji type(scope): …`) plus WHY, WHAT CHANGED, and FILES IMPACTED. It does **not** run your test suite and does **not** append worktree / branch / machine trailers.
|
|
5
|
+
Writes `emoji type(scope): …` plus WHY, WHAT CHANGED, and FILES IMPACTED. Does **not** run your test suite. Does **not** append worktree / branch / machine trailers.
|
|
10
6
|
|
|
11
7
|
YourFam does not give you an API key. Use your own OpenAI-compatible key.
|
|
12
8
|
|
|
9
|
+
- **Source / README:** https://github.com/YourFam/yf-commit
|
|
10
|
+
- **npm:** https://www.npmjs.com/package/@yourfam/yf-commit
|
|
11
|
+
- **Issues:** https://github.com/YourFam/yf-commit/issues
|
|
12
|
+
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
|
-
Node 20+.
|
|
15
|
+
Node 20+.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
18
|
+
npm install -g @yourfam/yf-commit
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Then from any git repo:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm install -g @yourfam/yf-commit
|
|
25
24
|
yf-commit
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
The command is `yf-commit
|
|
27
|
+
The command is `yf-commit`.
|
|
28
|
+
|
|
29
|
+
## Alternate modes of install
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
**-g** = on **your machine** (global).
|
|
32
|
+
**-D** = in **this project** (devDependency).
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
| | `npm install -g @yourfam/yf-commit` | `npm install -D @yourfam/yf-commit` |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Where it goes | Global npm prefix (your user/system) | `node_modules/` + `package.json` of the current repo |
|
|
37
|
+
| Command | `yf-commit` from any folder | `npx yf-commit` (or a script) **in that repo** |
|
|
38
|
+
| Other repos | Works | Not installed there |
|
|
39
|
+
| Git | Not committed | Listed in `package.json`; teammates get it with `npm install` |
|
|
40
|
+
| Typical use | A CLI you want everywhere | A tool this project uses in scripts/CI |
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
2. Pick a model. First item is always **Auto (recommended)** — a cheap, fast Chat Completions model for summarizing a diff. Named picks stay pinned; Auto tracks our lightweight default.
|
|
36
|
-
3. Paste an API key (hidden). Skipped for Ollama.
|
|
42
|
+
For `yf-commit` as a daily command, use **-g**. Use **-D** only if this one repo should own the tool.
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
With **-D**, the binary lives in that repo’s `node_modules/.bin/`. Your shell PATH does not include that, so **plain `yf-commit` will not work**. From that repo:
|
|
39
45
|
|
|
40
46
|
```bash
|
|
41
|
-
yf-commit
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
npx yf-commit
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A `package.json` script also works, because npm puts `node_modules/.bin` on PATH **for the script**:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
"scripts": {
|
|
54
|
+
"commit": "yf-commit"
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run commit
|
|
44
60
|
```
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
Plain `yf-commit` anywhere on the machine is only after **-g**.
|
|
63
|
+
|
|
64
|
+
No install at all (npm fetches and runs the published package):
|
|
47
65
|
|
|
48
66
|
```bash
|
|
49
|
-
|
|
67
|
+
npx @yourfam/yf-commit
|
|
50
68
|
```
|
|
51
69
|
|
|
52
|
-
|
|
70
|
+
## First run
|
|
71
|
+
|
|
72
|
+
The first `yf-commit` with no key is setup.
|
|
73
|
+
|
|
74
|
+
1. Provider: OpenAI, DeepSeek, xAI (Grok), Groq, or Ollama (local).
|
|
75
|
+
2. Model: **Auto (recommended)**, or a named model.
|
|
76
|
+
3. API key (hidden). Skipped for Ollama.
|
|
53
77
|
|
|
54
|
-
|
|
78
|
+
Saved to `~/.yf-commit/config.json` (Windows: `%USERPROFILE%\.yf-commit\config.json`). Never commit that file.
|
|
55
79
|
|
|
56
|
-
|
|
80
|
+
```bash
|
|
81
|
+
yf-commit init # wizard
|
|
82
|
+
yf-commit init --show # masked status
|
|
83
|
+
yf-commit init --reset # delete config
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or set **`YF_COMMIT_API_KEY`**. `OPENAI_API_KEY` is also accepted if the others are unset.
|
|
87
|
+
|
|
88
|
+
Need a host we don’t list? Set `YF_COMMIT_BASE_URL` and `YF_COMMIT_MODEL`.
|
|
89
|
+
|
|
90
|
+
| Env | Config | Default |
|
|
57
91
|
|---|---|---|
|
|
58
92
|
| `YF_COMMIT_API_KEY` | `apiKey` | — |
|
|
59
93
|
| `YF_COMMIT_BASE_URL` | `baseUrl` | `https://api.openai.com/v1` |
|
|
60
|
-
| `YF_COMMIT_MODEL` | `model` | `auto`
|
|
94
|
+
| `YF_COMMIT_MODEL` | `model` | `auto` |
|
|
95
|
+
|
|
96
|
+
## Use
|
|
97
|
+
|
|
98
|
+
Flags combine. Typical paths:
|
|
99
|
+
|
|
100
|
+
**You stage, then generate**
|
|
61
101
|
|
|
62
|
-
|
|
102
|
+
```bash
|
|
103
|
+
git add path/to/file
|
|
104
|
+
yf-commit # print draft → confirm → git commit
|
|
105
|
+
yf-commit -y # same, skip confirm (also --yes, --auto)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Skip staging — let yf-commit `git add -A`**
|
|
63
109
|
|
|
64
|
-
|
|
110
|
+
`--all` runs `git add -A` first (tracked changes and untracked files, except ignored), then the same as above.
|
|
65
111
|
|
|
66
112
|
```bash
|
|
67
|
-
yf-commit
|
|
68
|
-
yf-commit -y
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
yf-commit --all # add everything → print draft → confirm → commit
|
|
114
|
+
yf-commit --all -y # add everything → commit now (no confirm)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Other flags (mix with `--all` / `-y` as needed)**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
yf-commit --print # print only; do not commit (still needs a staged diff, or add --all)
|
|
121
|
+
yf-commit --all --print
|
|
71
122
|
yf-commit --type fix # force type
|
|
72
|
-
yf-commit --type 4 # same (1–
|
|
123
|
+
yf-commit --type 4 # same (1–11)
|
|
124
|
+
yf-commit --all -y --type feat
|
|
73
125
|
```
|
|
74
126
|
|
|
75
|
-
|
|
127
|
+
The JS does not classify the **type**. It does measure **size** (insertions + deletions, plus file count) and tells the model a length budget: tiny diffs get exactly 1 WHY + 1 WHAT; large diffs get more WHAT, grouped. WHY does not grow with every extra hundred lines. Mixed diffs still get **one** dominant type (`feat` / `fix` usually). Override with `--type`. Default still prints the draft so you can cancel.
|
|
128
|
+
|
|
129
|
+
| # | Type | Meaning |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| 1 | `chore` | Maintenance that is none of the below |
|
|
132
|
+
| 2 | `docs` | Docs only |
|
|
133
|
+
| 3 | `feat` | User-visible new capability |
|
|
134
|
+
| 4 | `fix` | User-visible bug |
|
|
135
|
+
| 5 | `refactor` | Same behavior, different structure |
|
|
136
|
+
| 6 | `test` | Tests only |
|
|
137
|
+
| 7 | `build` | Compile, bundler, packaging |
|
|
138
|
+
| 8 | `ci` | Pipeline / GitHub Actions / hooks |
|
|
139
|
+
| 9 | `perf` | Same behavior, measurably faster |
|
|
140
|
+
| 10 | `style` | Formatting only, zero logic |
|
|
141
|
+
| 11 | `revert` | Undoes a previous commit |
|
|
142
|
+
|
|
143
|
+
Priority on mixed diffs: `revert` → `fix` → `feat` → `test` → `docs` → `perf` → `refactor` → `style` → `ci` → `build` → `chore`.
|
|
144
|
+
|
|
145
|
+
Default prints the draft in the terminal. Enter or `y` to commit, `n` or Ctrl+C to cancel. It does not open an editor.
|
|
76
146
|
|
|
77
|
-
|
|
147
|
+
Nothing staged → error, exit 1. No `git add` unless `--all`.
|
|
78
148
|
|
|
79
|
-
|
|
149
|
+
Scripts / CI: `--print` or `-y`. It will not wait for a paste or a confirm.
|
|
80
150
|
|
|
81
|
-
##
|
|
151
|
+
## Format
|
|
82
152
|
|
|
83
153
|
```
|
|
84
154
|
✨ feat(cli): add --print to skip git commit
|
|
85
155
|
|
|
86
156
|
💡 WHY:
|
|
87
|
-
-
|
|
157
|
+
- Preview a message without committing
|
|
88
158
|
|
|
89
159
|
🔧 WHAT CHANGED:
|
|
90
|
-
-
|
|
160
|
+
- --print writes the draft to stdout and exits
|
|
91
161
|
|
|
92
162
|
📁 FILES IMPACTED:
|
|
93
163
|
- src/index.js
|
|
94
164
|
- README.md
|
|
95
165
|
```
|
|
96
166
|
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
This is the GitHub repo, not a second package. Docs and source live here; `npx` / `npm install` still install whatever version is **published on npm**.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
git clone https://github.com/YourFam/yf-commit.git
|
|
173
|
+
cd yf-commit
|
|
174
|
+
npm install
|
|
175
|
+
npm test
|
|
176
|
+
node ./bin/yf-commit.js --help
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Maintainer: YourFam (`kamal-yourfam` on npm). License MIT.
|
|
180
|
+
|
|
97
181
|
## License
|
|
98
182
|
|
|
99
183
|
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yourfam/yf-commit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI commit message from your staged git diff",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"yf-commit": "
|
|
7
|
+
"yf-commit": "bin/yf-commit.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
"conventional-commits",
|
|
26
26
|
"cli"
|
|
27
27
|
],
|
|
28
|
-
"author":
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "YourFam",
|
|
30
|
+
"email": "kamal@yourfam.co",
|
|
31
|
+
"url": "https://github.com/YourFam"
|
|
32
|
+
},
|
|
29
33
|
"license": "MIT",
|
|
30
34
|
"repository": {
|
|
31
35
|
"type": "git",
|
package/src/args.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
|
-
import { parseType } from "./types.js";
|
|
2
|
+
import { parseType, typeHelpLines } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {object} ParsedArgs
|
|
@@ -58,7 +58,7 @@ export function parseArgs(argv) {
|
|
|
58
58
|
} else if (a === "--type") {
|
|
59
59
|
const value = args[i + 1];
|
|
60
60
|
if (value == null || value.startsWith("-")) {
|
|
61
|
-
throw new CliError("Flag --type requires a value: feat|fix|… or 1–
|
|
61
|
+
throw new CliError("Flag --type requires a value: feat|fix|… or 1–11.");
|
|
62
62
|
}
|
|
63
63
|
result.type = parseType(value);
|
|
64
64
|
i += 1;
|
|
@@ -96,11 +96,14 @@ Usage:
|
|
|
96
96
|
yf-commit -y Commit immediately (also --yes, --auto)
|
|
97
97
|
yf-commit --print Print message only; do not commit
|
|
98
98
|
yf-commit --all git add -A, then same as default
|
|
99
|
-
yf-commit --type fix Force type (name or 1–
|
|
99
|
+
yf-commit --type fix Force type (name or 1–11)
|
|
100
100
|
yf-commit init Provider, model, API key → ~/.yf-commit/config.json
|
|
101
101
|
yf-commit init --show Masked status
|
|
102
102
|
yf-commit init --reset Delete saved config
|
|
103
103
|
|
|
104
|
+
Types (mixed diffs: one dominant type, not a blend):
|
|
105
|
+
${typeHelpLines()}
|
|
106
|
+
|
|
104
107
|
This does not run your test suite. YourFam does not give you an API key.
|
|
105
108
|
|
|
106
109
|
Need a host we don't list? Set YF_COMMIT_BASE_URL and YF_COMMIT_MODEL.`;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const ORDER = ["tiny", "small", "medium", "large", "xl"];
|
|
2
|
+
|
|
3
|
+
export const PROFILES = {
|
|
4
|
+
tiny: {
|
|
5
|
+
id: "tiny",
|
|
6
|
+
why: "exactly 1 bullet",
|
|
7
|
+
what: "exactly 1 bullet",
|
|
8
|
+
groupWhat: false,
|
|
9
|
+
maxTokens: 400,
|
|
10
|
+
},
|
|
11
|
+
small: {
|
|
12
|
+
id: "small",
|
|
13
|
+
why: "1–2 bullets",
|
|
14
|
+
what: "2–3 bullets",
|
|
15
|
+
groupWhat: false,
|
|
16
|
+
maxTokens: 700,
|
|
17
|
+
},
|
|
18
|
+
medium: {
|
|
19
|
+
id: "medium",
|
|
20
|
+
why: "2–3 bullets",
|
|
21
|
+
what: "4–6 bullets",
|
|
22
|
+
groupWhat: false,
|
|
23
|
+
maxTokens: 1200,
|
|
24
|
+
},
|
|
25
|
+
large: {
|
|
26
|
+
id: "large",
|
|
27
|
+
why: "2–3 bullets",
|
|
28
|
+
what: "6–10 bullets, grouped by area if needed",
|
|
29
|
+
groupWhat: true,
|
|
30
|
+
maxTokens: 1800,
|
|
31
|
+
},
|
|
32
|
+
xl: {
|
|
33
|
+
id: "xl",
|
|
34
|
+
why: "2–4 bullets (do not grow WHY with every file)",
|
|
35
|
+
what: "8–14 bullets, grouped by area",
|
|
36
|
+
groupWhat: true,
|
|
37
|
+
maxTokens: 2500,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Count changed lines (+ and −, excluding +++ / --- headers) and files.
|
|
43
|
+
* @param {string} diff
|
|
44
|
+
*/
|
|
45
|
+
export function countDiff(diff) {
|
|
46
|
+
const lines = String(diff).split(/\r?\n/);
|
|
47
|
+
let loc = 0;
|
|
48
|
+
const files = new Set();
|
|
49
|
+
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (line.startsWith("diff --git ")) {
|
|
52
|
+
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
53
|
+
if (m) {
|
|
54
|
+
const path = m[2] === "/dev/null" ? m[1] : m[2];
|
|
55
|
+
files.add(path.replace(/^"|"$/g, ""));
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (line.startsWith("Binary files ")) {
|
|
60
|
+
loc += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
64
|
+
loc += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
68
|
+
loc += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { loc, files: files.size };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function profileIdFromLoc(loc) {
|
|
76
|
+
if (loc <= 20) return "tiny";
|
|
77
|
+
if (loc <= 80) return "small";
|
|
78
|
+
if (loc <= 300) return "medium";
|
|
79
|
+
if (loc <= 800) return "large";
|
|
80
|
+
return "xl";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function applyFileFloor(id, fileCount) {
|
|
84
|
+
let i = ORDER.indexOf(id);
|
|
85
|
+
if (fileCount >= 8) i = Math.max(i, ORDER.indexOf("small"));
|
|
86
|
+
if (fileCount >= 20) i = Math.max(i, ORDER.indexOf("medium"));
|
|
87
|
+
return ORDER[i];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} diff
|
|
92
|
+
*/
|
|
93
|
+
export function diffProfile(diff) {
|
|
94
|
+
const { loc, files } = countDiff(diff);
|
|
95
|
+
const id = applyFileFloor(profileIdFromLoc(loc), files);
|
|
96
|
+
const spec = PROFILES[id];
|
|
97
|
+
return { ...spec, loc, files };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {ReturnType<typeof diffProfile>} profile
|
|
102
|
+
*/
|
|
103
|
+
export function lengthInstruction(profile) {
|
|
104
|
+
const fileNote =
|
|
105
|
+
profile.files >= 8
|
|
106
|
+
? "FILES IMPACTED: list real paths; group under area headings if that helps. Never write \"Updated N files.\""
|
|
107
|
+
: "FILES IMPACTED: list each real path. Never write \"Updated N files.\"";
|
|
108
|
+
|
|
109
|
+
return `Diff size: ${profile.loc} line(s), ${profile.files} file(s). Profile: ${profile.id}.
|
|
110
|
+
WHY: ${profile.why}. Reasons only — do not recap every file.
|
|
111
|
+
WHAT CHANGED: ${profile.what}.
|
|
112
|
+
${fileNote}
|
|
113
|
+
Do not add extra bullets to look complete. Do not pad.`;
|
|
114
|
+
}
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { completeChat } from "./llm.js";
|
|
|
24
24
|
import { readFileSync } from "node:fs";
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { diffProfile, lengthInstruction } from "./diff-profile.js";
|
|
27
28
|
import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
|
|
28
29
|
|
|
29
30
|
const pkg = JSON.parse(
|
|
@@ -74,12 +75,14 @@ export async function main(argv) {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
const profile = diffProfile(diff);
|
|
77
79
|
const message = await completeChat({
|
|
78
80
|
baseUrl: settings.baseUrl,
|
|
79
81
|
model: settings.resolvedModel,
|
|
80
82
|
apiKey: settings.apiKey || "ollama",
|
|
81
83
|
systemPrompt: buildSystemPrompt({ forcedType: args.type }),
|
|
82
|
-
userPrompt: buildUserPrompt(diff),
|
|
84
|
+
userPrompt: buildUserPrompt(diff, lengthInstruction(profile)),
|
|
85
|
+
maxTokens: profile.maxTokens,
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
if (args.print) {
|
package/src/llm.js
CHANGED
|
@@ -15,6 +15,7 @@ export async function completeChat({
|
|
|
15
15
|
apiKey,
|
|
16
16
|
systemPrompt,
|
|
17
17
|
userPrompt,
|
|
18
|
+
maxTokens = 1500,
|
|
18
19
|
}) {
|
|
19
20
|
const root = String(baseUrl || "").replace(/\/+$/, "");
|
|
20
21
|
const url = `${root}/chat/completions`;
|
|
@@ -34,7 +35,7 @@ export async function completeChat({
|
|
|
34
35
|
body: JSON.stringify({
|
|
35
36
|
model,
|
|
36
37
|
temperature: 0.3,
|
|
37
|
-
max_tokens:
|
|
38
|
+
max_tokens: maxTokens,
|
|
38
39
|
messages: [
|
|
39
40
|
{ role: "system", content: systemPrompt },
|
|
40
41
|
{ role: "user", content: userPrompt },
|
package/src/prompt.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
TYPE_DEFS,
|
|
3
|
+
TYPE_EMOJI,
|
|
4
|
+
TYPE_PRIORITY,
|
|
5
|
+
TYPES,
|
|
6
|
+
} from "./types.js";
|
|
2
7
|
|
|
3
8
|
const MAX_DIFF_CHARS = 100_000;
|
|
4
9
|
|
|
10
|
+
const EMOJI_LINE = TYPES.map((t) => `${TYPE_EMOJI[t]} ${t}`).join(", ");
|
|
11
|
+
|
|
12
|
+
const DEF_LINES = TYPES.map((t) => `- ${t}: ${TYPE_DEFS[t]}`).join("\n");
|
|
13
|
+
|
|
5
14
|
export function buildSystemPrompt({ forcedType } = {}) {
|
|
6
15
|
const typeLine = forcedType
|
|
7
16
|
? `You MUST use type "${forcedType}" and emoji ${TYPE_EMOJI[forcedType]} on line 1.`
|
|
8
|
-
:
|
|
17
|
+
: `Use exactly one conventional commit type: ${TYPES.join(", ")}.`;
|
|
9
18
|
|
|
10
19
|
return `You are a commit message generator. Output only the commit message. No preamble, no markdown fences, no extra commentary.
|
|
11
20
|
|
|
@@ -24,22 +33,26 @@ emoji type(scope): brief description
|
|
|
24
33
|
|
|
25
34
|
Rules:
|
|
26
35
|
- ${typeLine}
|
|
27
|
-
-
|
|
36
|
+
- Type meanings:
|
|
37
|
+
${DEF_LINES}
|
|
38
|
+
- ${TYPE_PRIORITY}
|
|
39
|
+
- Emoji on line 1 (${EMOJI_LINE}).
|
|
28
40
|
- First line under 72 characters.
|
|
29
41
|
- WHY: user/business impact, plain language.
|
|
30
42
|
- WHAT CHANGED: technical, from the diff, not staging stats.
|
|
31
43
|
- FILES IMPACTED: paths relative to this repository's root (the current working directory).
|
|
32
|
-
-
|
|
44
|
+
- Obey the length profile in the user message (bullet counts for WHY / WHAT). Never pad. Never write "Updated N staged file(s)."
|
|
33
45
|
- No footer. Do not add WORKTREE, BRANCH, MACHINE, or hostname lines.
|
|
34
46
|
- Do not mention any product or company unless it appears in the diff.`;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
export function buildUserPrompt(diff) {
|
|
49
|
+
export function buildUserPrompt(diff, lengthBlock = "") {
|
|
38
50
|
let body = String(diff);
|
|
39
51
|
if (body.length > MAX_DIFF_CHARS) {
|
|
40
52
|
body = `${body.slice(0, MAX_DIFF_CHARS)}\n\n[diff truncated]`;
|
|
41
53
|
}
|
|
42
|
-
|
|
54
|
+
const prefix = lengthBlock ? `${lengthBlock}\n\n` : "";
|
|
55
|
+
return `${prefix}Generate a commit message for this staged git diff:\n\n${body}`;
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
export function cleanMessage(text) {
|
package/src/types.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* 1–6 stay stable (chore, docs, feat, fix, refactor, test).
|
|
5
|
+
* 7–11 are the extra conventional types.
|
|
6
|
+
*/
|
|
7
|
+
export const TYPES = [
|
|
8
|
+
"chore",
|
|
9
|
+
"docs",
|
|
10
|
+
"feat",
|
|
11
|
+
"fix",
|
|
12
|
+
"refactor",
|
|
13
|
+
"test",
|
|
14
|
+
"build",
|
|
15
|
+
"ci",
|
|
16
|
+
"perf",
|
|
17
|
+
"style",
|
|
18
|
+
"revert",
|
|
19
|
+
];
|
|
5
20
|
|
|
6
21
|
export const TYPE_EMOJI = {
|
|
7
22
|
feat: "✨",
|
|
@@ -10,8 +25,42 @@ export const TYPE_EMOJI = {
|
|
|
10
25
|
chore: "🔧",
|
|
11
26
|
docs: "📝",
|
|
12
27
|
test: "🧪",
|
|
28
|
+
build: "📦",
|
|
29
|
+
ci: "👷",
|
|
30
|
+
perf: "⚡",
|
|
31
|
+
style: "🎨",
|
|
32
|
+
revert: "⏪",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const TYPE_DEFS = {
|
|
36
|
+
feat: "User-visible new capability",
|
|
37
|
+
fix: "User-visible bug",
|
|
38
|
+
docs: "Documentation only, no product code",
|
|
39
|
+
test: "Tests only",
|
|
40
|
+
revert: "Undoes a previous commit",
|
|
41
|
+
perf: "Same behavior, measurably faster",
|
|
42
|
+
refactor: "Same behavior, different structure",
|
|
43
|
+
style: "Formatting only, zero logic",
|
|
44
|
+
ci: "Pipeline / GitHub Actions / hooks only",
|
|
45
|
+
build: "Compile, bundler, packaging only",
|
|
46
|
+
chore: "Maintenance that is none of the above",
|
|
13
47
|
};
|
|
14
48
|
|
|
49
|
+
/** Mixed diffs: one primary type, not a blend. */
|
|
50
|
+
export const TYPE_PRIORITY = `Pick exactly one type for the whole commit. Mixed diffs are normal — choose the dominant type, in this order:
|
|
51
|
+
1. revert — the change undoes a previous commit
|
|
52
|
+
2. fix — any user-visible bug
|
|
53
|
+
3. feat — any user-visible new capability (tests/docs/formatting in the same diff stay feat)
|
|
54
|
+
4. test — tests only
|
|
55
|
+
5. docs — docs only
|
|
56
|
+
6. perf — same feature, measurably faster (not a bugfix)
|
|
57
|
+
7. refactor — same behavior, different structure (not formatting-only)
|
|
58
|
+
8. style — formatting / lint only, zero logic
|
|
59
|
+
9. ci — CI / GitHub Actions / hooks only
|
|
60
|
+
10. build — compile, bundler, native addons, packaging only
|
|
61
|
+
11. chore — everything else (deps, ignore files, version bump with no product change)
|
|
62
|
+
Do not invent extra types. Do not combine types.`;
|
|
63
|
+
|
|
15
64
|
/**
|
|
16
65
|
* @param {string | null | undefined} raw
|
|
17
66
|
* @returns {string | null}
|
|
@@ -19,9 +68,19 @@ export const TYPE_EMOJI = {
|
|
|
19
68
|
export function parseType(raw) {
|
|
20
69
|
if (raw == null || raw === "") return null;
|
|
21
70
|
const s = String(raw).trim().toLowerCase();
|
|
22
|
-
if (
|
|
71
|
+
if (/^\d+$/.test(s)) {
|
|
72
|
+
const n = Number(s);
|
|
73
|
+
if (n >= 1 && n <= TYPES.length) return TYPES[n - 1];
|
|
74
|
+
}
|
|
23
75
|
if (TYPES.includes(s)) return s;
|
|
24
76
|
throw new CliError(
|
|
25
|
-
`Unknown type "${raw}". Use
|
|
77
|
+
`Unknown type "${raw}". Use ${TYPES.join(", ")}, or 1–${TYPES.length}.`,
|
|
26
78
|
);
|
|
27
79
|
}
|
|
80
|
+
|
|
81
|
+
export function typeHelpLines() {
|
|
82
|
+
return TYPES.map((name, i) => {
|
|
83
|
+
const n = i + 1;
|
|
84
|
+
return ` ${n}. ${TYPE_EMOJI[name]} ${name} — ${TYPE_DEFS[name]}`;
|
|
85
|
+
}).join("\n");
|
|
86
|
+
}
|