@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 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+. Friend one-liner:
15
+ Node 20+.
16
16
 
17
17
  ```bash
18
- npx @yourfam/yf-commit
18
+ npm install -g @yourfam/yf-commit
19
19
  ```
20
20
 
21
- Or install once:
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` (not `ai-commit` — that name is already used by other packages).
27
+ The command is `yf-commit`.
28
+
29
+ ## Alternate modes of install
29
30
 
30
- ## First run / `yf-commit init`
31
+ **-g** = on **your machine** (global).
32
+ **-D** = in **this project** (devDependency).
31
33
 
32
- The first `yf-commit` with no key **is** setup (same idea as `gh auth login`).
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
- 1. Pick a provider (numbered list): OpenAI, DeepSeek, xAI (Grok), Groq, Ollama (local).
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
- Saved to `~/.yf-commit/config.json` (Windows: `%USERPROFILE%\.yf-commit\config.json`). Mode `0600` where the OS allows. This file lives in your home directory; never commit it.
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 init # wizard
42
- yf-commit init --show # provider, model, base URL, masked key
43
- yf-commit init --reset # delete saved config
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
- Primary env var: **`YF_COMMIT_API_KEY`**. You can also:
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
- export YF_COMMIT_API_KEY=...
67
+ npx @yourfam/yf-commit
50
68
  ```
51
69
 
52
- `OPENAI_API_KEY` is also accepted if the others are unset.
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
- Need a host we don't list? Set `YF_COMMIT_BASE_URL` and `YF_COMMIT_MODEL`.
78
+ Saved to `~/.yf-commit/config.json` (Windows: `%USERPROFILE%\.yf-commit\config.json`). Never commit that file.
55
79
 
56
- | Env | Config key | Default |
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` → lightweight model for that provider |
94
+ | `YF_COMMIT_MODEL` | `model` | `auto` |
95
+
96
+ ## Use
97
+
98
+ Flags combine. Typical paths:
99
+
100
+ **You stage, then generate**
61
101
 
62
- ## Usage
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
- Stage files, then:
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 # print draft in the terminal → confirm → git commit
68
- yf-commit -y # no confirm (also --yes, --auto)
69
- yf-commit --print # print only; do not commit
70
- yf-commit --all # git add -A, then same as default
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–6: chore, docs, feat, fix, refactor, test)
123
+ yf-commit --type 4 # same (1–11)
124
+ yf-commit --all -y --type feat
73
125
  ```
74
126
 
75
- Default: the draft is printed in the terminal. Press Enter or `y` to commit, `n` or Ctrl+C to cancel. It does not open an editor.
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
- If nothing is staged: one-line error, exit non-zero. It will not `git add -A` unless you pass `--all`.
147
+ Nothing staged error, exit 1. No `git add` unless `--all`.
78
148
 
79
- Non-interactive (CI / scripts): pass `--print` or `-y`. It will not hang waiting for a paste or a confirm.
149
+ Scripts / CI: `--print` or `-y`. It will not wait for a paste or a confirm.
80
150
 
81
- ## Message format
151
+ ## Format
82
152
 
83
153
  ```
84
154
  ✨ feat(cli): add --print to skip git commit
85
155
 
86
156
  💡 WHY:
87
- - Friends can preview a message without committing
157
+ - Preview a message without committing
88
158
 
89
159
  🔧 WHAT CHANGED:
90
- - Added a --print flag that writes the draft to stdout and exits
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.1.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": "./bin/yf-commit.js"
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": "YourFam",
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–6.");
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–6)
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: 1500,
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 { TYPE_EMOJI } from "./types.js";
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
- : "Use conventional commit types: feat, fix, refactor, chore, docs, test.";
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
- - Emoji on line 1 (✨ feat, 🐛 fix, ♻️ refactor, 🔧 chore, 📝 docs, 🧪 test).
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
- - Adaptive length: tiny diffs few bullets; large diffs more, grouped if needed. Never pad. Never write "Updated N staged file(s)."
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
- return `Generate a commit message for this staged git diff:\n\n${body}`;
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
- /** Alphabetical 1–6 map: chore, docs, feat, fix, refactor, test */
4
- export const TYPES = ["chore", "docs", "feat", "fix", "refactor", "test"];
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 (/^[1-6]$/.test(s)) return TYPES[Number(s) - 1];
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 feat, fix, refactor, chore, docs, test, or 1–6.`,
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
+ }