better-commits 1.19.0 → 1.20.0-cli-flags

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.
Files changed (65) hide show
  1. package/.better-commits.json +4 -0
  2. package/.github/workflows/test.yml +27 -0
  3. package/.opencode/package-lock.json +115 -0
  4. package/.opencode/plans/cli-args.md +182 -0
  5. package/.svelte-kit/ambient.d.ts +289 -0
  6. package/.svelte-kit/generated/client/app.js +28 -0
  7. package/.svelte-kit/generated/client/matchers.js +1 -0
  8. package/.svelte-kit/generated/client/nodes/0.js +1 -0
  9. package/.svelte-kit/generated/client/nodes/1.js +1 -0
  10. package/.svelte-kit/tsconfig.json +49 -0
  11. package/0001-feat-branch-124-update-worktrees-feature.patch +316 -0
  12. package/dist/branch.js +27 -1
  13. package/dist/chunk-OFJCRS3N.js +4 -0
  14. package/dist/chunk-SIF4LZUS.js +1 -0
  15. package/dist/index.js +44 -19
  16. package/dist/init.js +1 -1
  17. package/docs/ai-skills.yaml +48 -0
  18. package/docs/clack.md +143 -0
  19. package/docs/valibot.md +228 -0
  20. package/package.json +12 -9
  21. package/readme.md +18 -2
  22. package/src/args.test.ts +102 -0
  23. package/src/args.ts +101 -7
  24. package/src/branch-args.test.ts +72 -0
  25. package/src/branch-args.ts +106 -0
  26. package/src/branch-help.ts +114 -0
  27. package/src/branch.ts +67 -238
  28. package/src/help.ts +131 -0
  29. package/src/index.test.ts +7 -0
  30. package/src/index.ts +73 -492
  31. package/src/prompts/branch-checkout.prompt.ts +36 -0
  32. package/src/prompts/branch-confirm.prompt.ts +134 -0
  33. package/src/prompts/branch-description.prompt.ts +37 -0
  34. package/src/prompts/branch-runnable.ts +13 -0
  35. package/src/prompts/branch-ticket.prompt.ts +41 -0
  36. package/src/prompts/branch-type.prompt.ts +43 -0
  37. package/src/prompts/branch-user.prompt.ts +50 -0
  38. package/src/prompts/branch-version.prompt.ts +41 -0
  39. package/src/prompts/commit-body.prompt.ts +57 -0
  40. package/src/prompts/commit-confirm.prompt.ts +119 -0
  41. package/src/prompts/commit-footer.prompt.ts +195 -0
  42. package/src/prompts/commit-scope.prompt.ts +73 -0
  43. package/src/prompts/commit-status.prompt.ts +75 -0
  44. package/src/prompts/commit-ticket.prompt.ts +82 -0
  45. package/src/prompts/commit-title.prompt.ts +98 -0
  46. package/src/prompts/commit-type.prompt.ts +93 -0
  47. package/src/prompts/runnable.ts +13 -0
  48. package/src/utils/build-branch.test.ts +141 -0
  49. package/src/utils/build-branch.ts +46 -0
  50. package/src/utils/build-commit-string.test.ts +253 -0
  51. package/src/utils/build-commit-string.ts +158 -0
  52. package/src/utils/commit-title-size.ts +24 -0
  53. package/src/utils/infer.test.ts +83 -0
  54. package/src/utils/infer.ts +114 -0
  55. package/src/utils/messages.ts +25 -0
  56. package/src/utils/no-interactive-branch-validation.test.ts +170 -0
  57. package/src/utils/no-interactive-validation.test.ts +174 -0
  58. package/src/utils/no-interactive-validation.ts +190 -0
  59. package/src/utils.ts +59 -66
  60. package/src/valibot-consts.ts +2 -2
  61. package/src/valibot-state.test.ts +48 -0
  62. package/src/valibot-state.ts +133 -130
  63. package/tsconfig.json +3 -2
  64. package/vitest.config.ts +8 -0
  65. package/dist/chunk-K2RPF2JY.js +0 -4
package/dist/init.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #! /usr/bin/env node
2
- import{b as r,f as e,u as i}from"./chunk-K2RPF2JY.js";import*as t from"@clack/prompts";import c from"fs";import o from"picocolors";import{parse as l}from"valibot";try{console.clear(),t.intro(`${o.bgCyan(o.black(" better-commits-init "))}`);let s=`${i()}/${e}`,m=l(r,{});c.writeFileSync(s,JSON.stringify(m,null,4)),t.log.success(`${o.green("Successfully created .better-commits.json")}`),t.outro(`Run ${o.bgBlack(o.white("better-commits"))} to start the CLI`)}catch{t.log.error(`${o.red("Could not determine git root folder. better-commits-init must be used in a git repository")}`)}
2
+ import{b as r,h as e,m as i}from"./chunk-OFJCRS3N.js";import*as t from"@clack/prompts";import c from"fs";import o from"picocolors";import{parse as l}from"valibot";try{console.clear(),t.intro(`${o.bgCyan(o.black(" better-commits-init "))}`);let s=`${i()}/${e}`,m=l(r,{});c.writeFileSync(s,JSON.stringify(m,null,4)),t.log.success(`${o.green("Successfully created .better-commits.json")}`),t.outro(`Run ${o.bgBlack(o.white("better-commits"))} to start the CLI`)}catch{t.log.error(`${o.red("Could not determine git root folder. better-commits-init must be used in a git repository")}`)}
@@ -0,0 +1,48 @@
1
+ program: better-commits
2
+ version: 1
3
+ rules:
4
+ - prefer_non_interactive: true
5
+ - always_dry_run_first: true
6
+ - config_precedence: repo_then_home
7
+ - repo_config_path: .better-commits.json
8
+ - home_config_path: ~/.better-commits.json
9
+
10
+ skills:
11
+ - id: create_commit_non_interactive
12
+ goal: Create a commit without prompts using CLI flags.
13
+ command: better-commits --no-interactive [flags]
14
+ required_flags:
15
+ - --title
16
+ optional_flags:
17
+ - --type
18
+ - --scope
19
+ - --body
20
+ - --ticket
21
+ - --closes
22
+ - --trailer
23
+ - --breaking-title
24
+ - --breaking-body
25
+ - --deprecates-title
26
+ - --deprecates-body
27
+ - --custom-footer
28
+ inference:
29
+ - If --type is missing, infer from branch when possible.
30
+ - If --ticket is missing, infer from branch when possible.
31
+ flow:
32
+ - Run dry-run first.
33
+ - If valid, rerun without --dry-run.
34
+ dry_run_example: better-commits --no-interactive --dry-run --title "add parser"
35
+ commit_example: better-commits --no-interactive --type feat --title "add parser"
36
+
37
+ - id: edit_config_interview
38
+ goal: Create or update .better-commits.json based on user preferences and verify it.
39
+ flow:
40
+ - Choose target config: repo .better-commits.json, else ~/.better-commits.json.
41
+ - If repo config is needed and missing, run better-commits-init in repo root.
42
+ - Ask focused questions and edit JSON only.
43
+ - Preserve unknown keys.
44
+ - Validate with a dry-run commit command.
45
+ validation_command: better-commits --no-interactive --dry-run --type feat --title "config validation"
46
+ success_criteria:
47
+ - command exits successfully
48
+ - no schema or config validation errors
package/docs/clack.md ADDED
@@ -0,0 +1,143 @@
1
+ # Clack Usage
2
+
3
+ `better-commits` uses [`@clack/prompts`](https://github.com/natemoo-re/clack) as its sole interactive UI layer. All terminal prompts, logging, and banners go through clack — there is no `readline`, `inquirer`, or other prompt library in use.
4
+
5
+ ## Packages
6
+
7
+ | Package | Version | Role |
8
+ | ---------------- | -------- | ------------------------------------------------------ |
9
+ | `@clack/prompts` | `^0.7.0` | Directly imported in all source files |
10
+ | `@clack/core` | `^0.3.1` | Peer dependency of `@clack/prompts`, used transitively |
11
+
12
+ All files import clack with the alias:
13
+
14
+ ```ts
15
+ import * as p from "@clack/prompts";
16
+ ```
17
+
18
+ ## Files That Use Clack
19
+
20
+ - `src/index.ts` — main `better-commits` CLI flow
21
+ - `src/branch.ts` — `better-branch` CLI flow
22
+ - `src/utils.ts` — config loading and setup
23
+ - `src/git.ts` — git operation feedback
24
+ - `src/init.ts` — config initialization
25
+
26
+ ## API Reference
27
+
28
+ ### `p.intro(message)`
29
+
30
+ Renders a styled banner at the start of each CLI entrypoint.
31
+
32
+ ```ts
33
+ // src/utils.ts
34
+ p.intro(color.bgCyan(color.black(cli_name)));
35
+
36
+ // src/init.ts
37
+ p.intro(color.bgCyan(color.black(" better-commits-init ")));
38
+ ```
39
+
40
+ ### `p.outro(message)`
41
+
42
+ Renders a closing message at the end of a CLI flow. Only used in `src/init.ts` after writing the config file.
43
+
44
+ ```ts
45
+ p.outro("Run better-commits to start the CLI");
46
+ ```
47
+
48
+ ### `p.note(message, title)`
49
+
50
+ Prints a multi-line boxed panel. Used in `src/index.ts` to display a formatted commit preview before confirmation.
51
+
52
+ ```ts
53
+ p.note(commit_string, "Commit Preview");
54
+ ```
55
+
56
+ ### `p.select(options)`
57
+
58
+ Single-selection list prompt. Used for:
59
+
60
+ | Location | Prompt |
61
+ | --------------- | ----------------------------------------- |
62
+ | `src/index.ts` | "Select a commit type" |
63
+ | `src/index.ts` | "Select a commit scope" |
64
+ | `src/branch.ts` | "Checkout a branch or create a worktree?" |
65
+ | `src/branch.ts` | "Select a branch type" |
66
+
67
+ ### `p.multiselect(options)`
68
+
69
+ Multi-selection checkbox prompt. Used for:
70
+
71
+ | Location | Prompt |
72
+ | -------------- | -------------------------------------------------------------------------------- |
73
+ | `src/index.ts` | "would you like to add them now?" — lists unstaged files |
74
+ | `src/index.ts` | "Select optional footers" — closes, trailer, breaking-change, deprecated, custom |
75
+
76
+ The footers prompt uses `required: false` to make it skippable.
77
+
78
+ ### `p.text(options)`
79
+
80
+ Free-text input with optional `validate` and `initialValue`. Used across 15+ prompts, including:
81
+
82
+ | Location | Prompt |
83
+ | --------------- | ------------------------------------------------------------------ |
84
+ | `src/index.ts` | "Write a custom scope" |
85
+ | `src/index.ts` | "Add ticket / issue" (pre-filled from branch name inference) |
86
+ | `src/index.ts` | "Write a brief title describing the commit" (validates `max_size`) |
87
+ | `src/index.ts` | "Write a detailed description of the changes" |
88
+ | `src/index.ts` | "Breaking changes: Write a short title / summary" |
89
+ | `src/index.ts` | "Breaking Changes: Write a description & migration instructions" |
90
+ | `src/index.ts` | "Deprecated: Write a short title / summary" |
91
+ | `src/index.ts` | "Deprecated: Write a description" |
92
+ | `src/index.ts` | "Write a custom footer" |
93
+ | `src/branch.ts` | "Type your git username" |
94
+ | `src/branch.ts` | "Type ticket / issue number" |
95
+ | `src/branch.ts` | "Type version number" |
96
+ | `src/branch.ts` | "Type a short description" (enforces `max_length`) |
97
+
98
+ ### `p.confirm(options)`
99
+
100
+ Yes/No confirmation prompt. Used once in `src/index.ts`:
101
+
102
+ ```ts
103
+ const confirmed = await p.confirm({ message: "Confirm Commit?" });
104
+ ```
105
+
106
+ Only shown when `config.confirm_commit` is `true`. The boolean result controls whether the git commit executes.
107
+
108
+ ### `p.isCancel(value)`
109
+
110
+ Checks if the user cancelled a prompt (via `Ctrl+C`). Called immediately after **every** prompt throughout the codebase. On cancel, `process.exit(0)` is called for a graceful exit.
111
+
112
+ - Called **19 times** in `src/index.ts`
113
+ - Called **7 times** in `src/branch.ts`
114
+
115
+ Pattern used throughout:
116
+
117
+ ```ts
118
+ const value = await p.text({ message: "..." });
119
+ if (p.isCancel(value)) process.exit(0);
120
+ ```
121
+
122
+ ### `p.log.*`
123
+
124
+ Structured log functions used in place of `console.log` across all files.
125
+
126
+ | Method | Color | Usage |
127
+ | -------------------- | ------- | ----------------------------------------------------------------------------- |
128
+ | `p.log.step(msg)` | neutral | Info messages: "Found global config", "Checking Git Status" |
129
+ | `p.log.success(msg)` | green | "Commit Complete", "Successfully created .better-commits.json" |
130
+ | `p.log.error(msg)` | red | Failed git commands, invalid config, failed staging |
131
+ | `p.log.warn(msg)` | yellow | Missing git root, cache failures, invalid config paths |
132
+ | `p.log.warning(msg)` | yellow | Alias for `warn`; used once in `src/branch.ts` for duplicate branch detection |
133
+ | `p.log.info(msg)` | blue | "Committing changes...", "Exiting without commit", branch creation success |
134
+
135
+ ## CLI Interaction Flow
136
+
137
+ Every CLI command follows this structure:
138
+
139
+ 1. `p.intro()` — welcome banner
140
+ 2. Series of `p.select` / `p.multiselect` / `p.text` / `p.confirm` prompts, each immediately guarded by `p.isCancel()`
141
+ 3. `p.log.*` — structured feedback at each meaningful step (config loading, git operations, errors)
142
+ 4. `p.note()` — optional formatted commit preview before final confirmation (`src/index.ts` only)
143
+ 5. `p.outro()` — closing message (`src/init.ts` only; main flows end with `p.log.success`)
@@ -0,0 +1,228 @@
1
+ # Valibot Usage
2
+
3
+ `better-commits` uses [`valibot`](https://valibot.dev) (`^0.30.0`) for all runtime schema validation, type inference, and config parsing. Valibot defines the shape of every configuration object, commit state, and branch state in the application.
4
+
5
+ ## Files
6
+
7
+ | File | Role |
8
+ | ----------------------- | ------------------------------------------------------------------ |
9
+ | `src/valibot-consts.ts` | Shared picklist schemas and constant arrays |
10
+ | `src/valibot-state.ts` | Core schema definitions for `Config`, `CommitState`, `BranchState` |
11
+ | `src/utils.ts` | Config loading, `parse`, and `ValiError` handling |
12
+ | `src/index.ts` | Parses `CommitState`; uses `Output<>` types |
13
+ | `src/branch.ts` | Parses `BranchState`; uses `Output<>` types |
14
+ | `src/init.ts` | Calls `parse(Config, {})` to generate a default config file |
15
+
16
+ ## Schema Overview
17
+
18
+ ### `src/valibot-consts.ts` — Picklist Constants
19
+
20
+ Defines shared enum-like schemas used across the codebase:
21
+
22
+ ```ts
23
+ export const V_BRANCH_ACTIONS = v.picklist(["branch", "worktree"]);
24
+ export const V_FOOTER_OPTIONS = v.picklist([
25
+ "closes",
26
+ "trailer",
27
+ "breaking-change",
28
+ "deprecated",
29
+ "custom",
30
+ ]);
31
+ export const V_BRANCH_FIELDS = v.picklist([
32
+ "user",
33
+ "version",
34
+ "type",
35
+ "ticket",
36
+ "description",
37
+ ]);
38
+ export const V_BRANCH_CONFIG_FIELDS = v.picklist([
39
+ "branch_user",
40
+ "branch_version",
41
+ "branch_type",
42
+ "branch_ticket",
43
+ "branch_description",
44
+ ]);
45
+ ```
46
+
47
+ Constant arrays are typed using `v.Output<>` to derive element types directly from the schemas:
48
+
49
+ ```ts
50
+ export const FOOTER_OPTION_VALUES: v.Output<typeof V_FOOTER_OPTIONS>[] = [...];
51
+ export const BRANCH_ORDER_DEFAULTS: v.Output<typeof V_BRANCH_FIELDS>[] = [...];
52
+ ```
53
+
54
+ ---
55
+
56
+ ### `src/valibot-state.ts` — Core Schemas
57
+
58
+ #### `Config`
59
+
60
+ The main application config schema. Validates `.better-commits.json`. Parsing an empty object `{}` produces a fully-defaulted config.
61
+
62
+ Key fields and their validators:
63
+
64
+ | Field | Schema | Default |
65
+ | ------------------------ | ------------------------------------------------------------- | ---------- |
66
+ | `check_status` | `v.optional(v.boolean(), true)` | `true` |
67
+ | `cache_last_value` | `v.optional(v.boolean(), true)` | `true` |
68
+ | `confirm_with_editor` | `v.optional(v.boolean(), false)` | `false` |
69
+ | `confirm_commit` | `v.optional(v.boolean(), true)` | `true` |
70
+ | `print_commit_output` | `v.optional(v.boolean(), true)` | `true` |
71
+ | `enable_worktrees` | `v.optional(v.boolean(), true)` | `true` |
72
+ | `branch_pre_commands` | `v.optional(v.array(v.string()), [])` | `[]` |
73
+ | `branch_post_commands` | `v.optional(v.array(v.string()), [])` | `[]` |
74
+ | `worktree_pre_commands` | `v.optional(v.array(v.string()), [])` | `[]` |
75
+ | `worktree_post_commands` | `v.optional(v.array(v.string()), [])` | `[]` |
76
+ | `branch_action_default` | `v.optional(V_BRANCH_ACTIONS, "branch")` | `"branch"` |
77
+ | `branch_order` | `v.optional(v.array(V_BRANCH_FIELDS), BRANCH_ORDER_DEFAULTS)` | all fields |
78
+
79
+ **Notable nested validators:**
80
+
81
+ - `commit_type.options[].emoji` — `v.string([v.emoji()])` validates the value is a real emoji character
82
+ - `commit_type.max_items` / `commit_scope.max_items` — `v.number([v.minValue(1)])` enforces minimum of 1
83
+ - `check_ticket.prepend_hashtag` — `v.picklist(["Never", "Always", "Prompt"])`
84
+ - `check_ticket.surround` — `v.picklist(["", "()", "[]", "{}"])`
85
+ - `check_ticket.title_position` — `v.picklist(["start", "end", "before-colon", "beginning"])`
86
+ - `branch_*.separator` — `v.picklist(["/", "-", "_"])` (branch_description also allows `""`)
87
+
88
+ **Cross-field validation with `v.custom`:**
89
+
90
+ Both `commit_type` and `commit_scope` use `v.custom` to ensure `initial_value` exists within their `options` array:
91
+
92
+ ```ts
93
+ v.custom(
94
+ (val) =>
95
+ !val.initial_value ||
96
+ val.options.some((o) => o.value === val.initial_value),
97
+ (val) =>
98
+ `Type: initial_value "${val.input.initial_value}" must exist in options`,
99
+ );
100
+ ```
101
+
102
+ **Post-parse transformation with `v.transform`:**
103
+
104
+ - `commit_type` — when `append_emoji_to_label` is `true`, prepends emoji to each option's label
105
+ - `commit_scope` — when `custom_scope` is `true`, injects `{ label: "custom", value: "custom", hint: "..." }` into options if not already present
106
+
107
+ ---
108
+
109
+ #### `CommitState`
110
+
111
+ Models the mutable in-memory state of an in-progress commit. All fields are `v.optional(v.string(), "")`.
112
+
113
+ | Field | Purpose |
114
+ | ------------------ | --------------------------- |
115
+ | `type` | Commit type (e.g. `"feat"`) |
116
+ | `scope` | Commit scope (e.g. `"app"`) |
117
+ | `title` | Short commit title |
118
+ | `body` | Long commit body |
119
+ | `closes` | "Closes: " footer token |
120
+ | `ticket` | Issue/ticket number |
121
+ | `breaking_title` | Breaking change summary |
122
+ | `breaking_body` | Breaking change description |
123
+ | `deprecates` | Deprecation marker |
124
+ | `deprecates_title` | Deprecation summary |
125
+ | `deprecates_body` | Deprecation description |
126
+ | `custom_footer` | Custom footer text |
127
+ | `trailer` | Git trailer string |
128
+
129
+ ---
130
+
131
+ #### `BranchState`
132
+
133
+ Models the mutable in-memory state for the `better-branch` CLI flow. All fields are `v.optional(v.string(), "")`.
134
+
135
+ | Field | Purpose |
136
+ | ------------- | ----------------------------------- |
137
+ | `user` | Git username portion of branch name |
138
+ | `type` | Branch type (mirrors commit type) |
139
+ | `ticket` | Ticket/issue number |
140
+ | `description` | Short branch description |
141
+ | `version` | Version number |
142
+
143
+ ---
144
+
145
+ ## Runtime Usage
146
+
147
+ ### Parsing with defaults
148
+
149
+ All three state objects are initialized by parsing an empty object, which triggers all `v.optional` defaults:
150
+
151
+ ```ts
152
+ // src/index.ts
153
+ const commit_state = parse(CommitState, {});
154
+
155
+ // src/branch.ts
156
+ const branch_state = parse(BranchState, {});
157
+
158
+ // src/init.ts
159
+ const default_config = parse(Config, {});
160
+ ```
161
+
162
+ ### Config validation in `src/utils.ts`
163
+
164
+ `validate_config` wraps `parse(Config, config)` to produce human-friendly errors:
165
+
166
+ ```ts
167
+ function validate_config(config: unknown) {
168
+ try {
169
+ return parse(Config, config);
170
+ } catch (err) {
171
+ if (err instanceof ValiError) {
172
+ const path = err.issues[0].path?.map((p) => p.key).join(".");
173
+ p.log.error(`Invalid config at: ${path}`);
174
+ process.exit(1);
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ ### TypeScript types via `Output<>`
181
+
182
+ `Output<typeof Schema>` is used throughout as the TypeScript type for parsed objects, avoiding manual interface duplication:
183
+
184
+ ```ts
185
+ // Function signatures
186
+ function build_commit_string(
187
+ state: Output<typeof CommitState>,
188
+ config: Output<typeof Config>
189
+ ): string { ... }
190
+
191
+ // Variable types
192
+ const checkout_type: Output<typeof V_BRANCH_ACTIONS> = ...;
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Valibot API Summary
198
+
199
+ | API | Purpose |
200
+ | ----------------------------- | ---------------------------------------------------------------------- |
201
+ | `v.object({...})` | Defines all three top-level schemas and all nested sub-objects |
202
+ | `v.optional(schema, default)` | Makes fields optional with a fallback default |
203
+ | `v.boolean()` | Validates boolean config flags |
204
+ | `v.string()` | Validates string fields |
205
+ | `v.number([...])` | Validates number fields with optional pipe validators |
206
+ | `v.array(schema)` | Validates array fields |
207
+ | `v.picklist([...])` | Validates enum-like string fields |
208
+ | `v.minValue(1)` | Pipe validator: enforces a minimum numeric value |
209
+ | `v.emoji()` | Pipe validator: ensures a string is a valid emoji character |
210
+ | `v.custom(fn, msgFn)` | Cross-field validation with a custom predicate |
211
+ | `v.transform(schema, fn)` | Post-parse transformation of validated data |
212
+ | `parse(schema, data)` | Parses and validates data, applying all defaults and transforms |
213
+ | `Output<typeof Schema>` | TypeScript utility to extract the inferred output type |
214
+ | `ValiError` | Error class caught to produce human-readable validation error messages |
215
+
216
+ ---
217
+
218
+ ## Known Quirk
219
+
220
+ `src/branch.ts` contains a bare no-op reference to `CommitState` at the bottom of the file:
221
+
222
+ ```ts
223
+ // TODO: No idea what's happening here
224
+ // If you don't use CommitState, (even in unreachable code), parse fails on Config
225
+ CommitState;
226
+ ```
227
+
228
+ This is a workaround for a bundler/tree-shaking issue: without this reference, `parse(Config, ...)` fails at runtime. The root cause is likely that `CommitState` imports something from `valibot-state.ts` that is needed for `Config` to work correctly at runtime, but the bundler eliminates it as dead code without this reference.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "better-commits",
3
3
  "private": false,
4
- "version": "1.19.0",
4
+ "version": "1.20.0-cli-flags",
5
5
  "description": "A CLI for creating better commits following the conventional commits specification",
6
6
  "author": "Erik Verduin (https://github.com/everduin94)",
7
7
  "type": "module",
@@ -26,18 +26,20 @@
26
26
  "url": "https://github.com/Everduin94/better-commits"
27
27
  },
28
28
  "dependencies": {
29
- "@clack/core": "^0.3.1",
30
- "@clack/prompts": "^0.7.0",
29
+ "@bomb.sh/args": "^0.3.1",
30
+ "@clack/core": "^1.2.0",
31
+ "@clack/prompts": "^1.2.0",
31
32
  "configstore": "^5.0.1",
32
33
  "picocolors": "^1.0.0",
33
- "valibot": "^0.30.0"
34
+ "valibot": "^1.3.1"
34
35
  },
35
36
  "scripts": {
36
- "start": "jiti ./src/index.ts",
37
- "branch": "jiti ./src/branch.ts",
38
- "init": "jiti ./src/init.ts",
37
+ "start": "tsx ./src/index.ts",
38
+ "branch": "tsx ./src/branch.ts",
39
+ "init": "tsx ./src/init.ts",
39
40
  "build": "tsup",
40
- "commit": "jiti ./src/index.ts"
41
+ "commit": "tsx ./src/index.ts",
42
+ "test": "vitest run"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@semantic-release/git": "^10.0.1",
@@ -49,7 +51,8 @@
49
51
  "semantic-release": "^25.0.2",
50
52
  "tsup": "^8.0.2",
51
53
  "tsx": "^3.12.3",
52
- "typescript": "^5.4.5"
54
+ "typescript": "^5.4.5",
55
+ "vitest": "^3.2.4"
53
56
  },
54
57
  "release": {
55
58
  "branches": [
package/readme.md CHANGED
@@ -49,10 +49,14 @@ better-commits # Create a new commit
49
49
  better-branch # Create a new branch
50
50
  ```
51
51
 
52
- `better-commits` will prompt a series of questions. These prompts will build a commit message, which you can preview, before confirming the commit.
52
+ `better-commits` will prompt a series of questions. These prompts will build a commit message, which you can preview, before confirming the commit. - To better understand these prompts and their intention, read [Conventional Commits Summary](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary)
53
+
53
54
  Some of the values in these prompts will be inferred by your branch name and auto populated. You can adjust this in your `.better-commits.json` configuration file.
54
55
 
55
- To better understand these prompts and their intention, read [Conventional Commits Summary](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary)
56
+ For documentation on passing commit values to `better-commits` via the CLI, see [CLI Flags](#cli-flags).
57
+
58
+ > [!TIP]
59
+ > The --no-interactive flag, allows automated workflows or AI agents like OpenCode and Claude Code, to use better-commits to generate consistent commit messages using less tokens.
56
60
 
57
61
  ## ⚙️ Configuration
58
62
 
@@ -438,6 +442,18 @@ You can add this badge to your repository to display that you're using a better-
438
442
  | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
439
443
  | `[![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits)` | [![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits) |
440
444
 
445
+ ### CLI Flags
446
+
447
+ Use CLI flags to pass commit values directly instead of answering prompts.
448
+
449
+ - Use `--no-interactive` to skip prompts, confirmation, and editor flows. This is the recommended mode for OpenCode, Claude Code, and other coding agents.
450
+ - Use `--dry-run` to validate the generated `git commit` command without creating a commit.
451
+ - Supported commit field flags: `--type`, `--scope`, `--title`, `--body`, `--ticket`, `--closes`, `--deprecates`, `--breaking-title`, `--breaking-body`, `--deprecates-title`, `--deprecates-body`, `--custom-footer`, `--trailer`.
452
+
453
+ ```sh
454
+ better-commits --no-interactive --dry-run --type feat --scope cli --title "add parser"
455
+ ```
456
+
441
457
  ### Git Arguments
442
458
 
443
459
  You can pass arguments to `git` through `better-commits` like so:
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse_runtime_flags } from "./args";
3
+
4
+ describe("parse_runtime_flags", () => {
5
+ it("uses interactive mode by default", () => {
6
+ const parsed = parse_runtime_flags([]);
7
+
8
+ expect(parsed.no_interactive).toBe(false);
9
+ expect(parsed.dry_run).toBe(false);
10
+ expect(parsed.git_args).toBe("");
11
+ expect(parsed.commit_state).toEqual({});
12
+ });
13
+
14
+ it("parses --no-interactive and --dry-run", () => {
15
+ const parsed = parse_runtime_flags(["--no-interactive", "--dry-run"]);
16
+
17
+ expect(parsed.no_interactive).toBe(true);
18
+ expect(parsed.dry_run).toBe(true);
19
+ });
20
+
21
+ it("parses --version and -v", () => {
22
+ const long_flag = parse_runtime_flags(["--version"]);
23
+ const short_flag = parse_runtime_flags(["-v"]);
24
+
25
+ expect(long_flag.version).toBe(true);
26
+ expect(short_flag.version).toBe(true);
27
+ });
28
+
29
+ it("maps commit flags into commit_state keys", () => {
30
+ const parsed = parse_runtime_flags([
31
+ "--type",
32
+ "feat",
33
+ "--title",
34
+ "ship feature",
35
+ "--breaking-title",
36
+ "api changed",
37
+ "--custom-footer",
38
+ "Reviewed-by: Jane",
39
+ ]);
40
+
41
+ expect(parsed.commit_state).toEqual({
42
+ type: "feat",
43
+ title: "ship feature",
44
+ breaking_title: "api changed",
45
+ custom_footer: "Reviewed-by: Jane",
46
+ });
47
+ });
48
+
49
+ it("builds git args from --git-dir and --work-tree", () => {
50
+ const parsed = parse_runtime_flags([
51
+ "--git-dir",
52
+ "/tmp/repo/.git",
53
+ "--work-tree",
54
+ "/tmp/repo",
55
+ ]);
56
+
57
+ expect(parsed.git_args).toBe(
58
+ "--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
59
+ );
60
+ });
61
+
62
+ it("builds git args when only one git location flag is provided", () => {
63
+ const with_git_dir = parse_runtime_flags(["--git-dir", "/tmp/repo/.git"]);
64
+ const with_work_tree = parse_runtime_flags(["--work-tree", "/tmp/repo"]);
65
+
66
+ expect(with_git_dir.git_args).toBe("--git-dir=/tmp/repo/.git");
67
+ expect(with_work_tree.git_args).toBe("--work-tree=/tmp/repo");
68
+ });
69
+
70
+ it("maps dashed commit flags to snake_case commit_state keys", () => {
71
+ const parsed = parse_runtime_flags([
72
+ "--breaking-body",
73
+ "major impact",
74
+ "--deprecates-title",
75
+ "legacy endpoint",
76
+ "--deprecates-body",
77
+ "use v2 endpoint",
78
+ "--custom-footer",
79
+ "Reviewed-by: Alex",
80
+ "--breaking-title",
81
+ "v1 removed",
82
+ ]);
83
+
84
+ expect(parsed.commit_state).toEqual({
85
+ breaking_body: "major impact",
86
+ deprecates_title: "legacy endpoint",
87
+ deprecates_body: "use v2 endpoint",
88
+ custom_footer: "Reviewed-by: Alex",
89
+ breaking_title: "v1 removed",
90
+ });
91
+ });
92
+
93
+ it("honors interactive flag semantics", () => {
94
+ const default_flags = parse_runtime_flags([]);
95
+ const explicit_interactive = parse_runtime_flags(["--interactive"]);
96
+ const no_interactive = parse_runtime_flags(["--no-interactive"]);
97
+
98
+ expect(default_flags.no_interactive).toBe(false);
99
+ expect(explicit_interactive.no_interactive).toBe(false);
100
+ expect(no_interactive.no_interactive).toBe(true);
101
+ });
102
+ });