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.
- package/.better-commits.json +4 -0
- package/.github/workflows/test.yml +27 -0
- package/.opencode/package-lock.json +115 -0
- package/.opencode/plans/cli-args.md +182 -0
- package/.svelte-kit/ambient.d.ts +289 -0
- package/.svelte-kit/generated/client/app.js +28 -0
- package/.svelte-kit/generated/client/matchers.js +1 -0
- package/.svelte-kit/generated/client/nodes/0.js +1 -0
- package/.svelte-kit/generated/client/nodes/1.js +1 -0
- package/.svelte-kit/tsconfig.json +49 -0
- package/0001-feat-branch-124-update-worktrees-feature.patch +316 -0
- package/dist/branch.js +27 -1
- package/dist/chunk-OFJCRS3N.js +4 -0
- package/dist/chunk-SIF4LZUS.js +1 -0
- package/dist/index.js +44 -19
- package/dist/init.js +1 -1
- package/docs/ai-skills.yaml +48 -0
- package/docs/clack.md +143 -0
- package/docs/valibot.md +228 -0
- package/package.json +12 -9
- package/readme.md +18 -2
- package/src/args.test.ts +102 -0
- package/src/args.ts +101 -7
- package/src/branch-args.test.ts +72 -0
- package/src/branch-args.ts +106 -0
- package/src/branch-help.ts +114 -0
- package/src/branch.ts +67 -238
- package/src/help.ts +131 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +73 -492
- package/src/prompts/branch-checkout.prompt.ts +36 -0
- package/src/prompts/branch-confirm.prompt.ts +134 -0
- package/src/prompts/branch-description.prompt.ts +37 -0
- package/src/prompts/branch-runnable.ts +13 -0
- package/src/prompts/branch-ticket.prompt.ts +41 -0
- package/src/prompts/branch-type.prompt.ts +43 -0
- package/src/prompts/branch-user.prompt.ts +50 -0
- package/src/prompts/branch-version.prompt.ts +41 -0
- package/src/prompts/commit-body.prompt.ts +57 -0
- package/src/prompts/commit-confirm.prompt.ts +119 -0
- package/src/prompts/commit-footer.prompt.ts +195 -0
- package/src/prompts/commit-scope.prompt.ts +73 -0
- package/src/prompts/commit-status.prompt.ts +75 -0
- package/src/prompts/commit-ticket.prompt.ts +82 -0
- package/src/prompts/commit-title.prompt.ts +98 -0
- package/src/prompts/commit-type.prompt.ts +93 -0
- package/src/prompts/runnable.ts +13 -0
- package/src/utils/build-branch.test.ts +141 -0
- package/src/utils/build-branch.ts +46 -0
- package/src/utils/build-commit-string.test.ts +253 -0
- package/src/utils/build-commit-string.ts +158 -0
- package/src/utils/commit-title-size.ts +24 -0
- package/src/utils/infer.test.ts +83 -0
- package/src/utils/infer.ts +114 -0
- package/src/utils/messages.ts +25 -0
- package/src/utils/no-interactive-branch-validation.test.ts +170 -0
- package/src/utils/no-interactive-validation.test.ts +174 -0
- package/src/utils/no-interactive-validation.ts +190 -0
- package/src/utils.ts +59 -66
- package/src/valibot-consts.ts +2 -2
- package/src/valibot-state.test.ts +48 -0
- package/src/valibot-state.ts +133 -130
- package/tsconfig.json +3 -2
- package/vitest.config.ts +8 -0
- 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,
|
|
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`)
|
package/docs/valibot.md
ADDED
|
@@ -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.
|
|
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
|
-
"@
|
|
30
|
-
"@clack/
|
|
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": "^
|
|
34
|
+
"valibot": "^1.3.1"
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
|
-
"start": "
|
|
37
|
-
"branch": "
|
|
38
|
-
"init": "
|
|
37
|
+
"start": "tsx ./src/index.ts",
|
|
38
|
+
"branch": "tsx ./src/branch.ts",
|
|
39
|
+
"init": "tsx ./src/init.ts",
|
|
39
40
|
"build": "tsup",
|
|
40
|
-
"commit": "
|
|
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
|
-
|
|
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
|
| `[](https://github.com/Everduin94/better-commits)` | [](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:
|
package/src/args.test.ts
ADDED
|
@@ -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
|
+
});
|