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
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse } from "valibot";
3
+ import { BranchState, Config } from "../valibot-state";
4
+ import { build_branch, build_worktree_path } from "./build-branch";
5
+
6
+ function make_config(input: Record<string, unknown> = {}) {
7
+ return parse(Config, input);
8
+ }
9
+
10
+ function make_branch(input: Record<string, unknown>) {
11
+ return parse(BranchState, input);
12
+ }
13
+
14
+ describe("build_branch", () => {
15
+ it("builds branch with default order and separators", () => {
16
+ const branch = make_branch({
17
+ user: "erik",
18
+ version: "1.2.0",
19
+ type: "feat",
20
+ ticket: "ABC-1",
21
+ description: "add-parser",
22
+ });
23
+ const config = make_config();
24
+
25
+ expect(build_branch(branch, config)).toBe(
26
+ "erik/1.2.0/feat/ABC-1-add-parser",
27
+ );
28
+ });
29
+
30
+ it("omits empty fields without leaving trailing separators", () => {
31
+ const branch = make_branch({
32
+ user: "erik",
33
+ type: "fix",
34
+ description: "repair-parser",
35
+ });
36
+ const config = make_config();
37
+
38
+ expect(build_branch(branch, config)).toBe("erik/fix/repair-parser");
39
+ });
40
+
41
+ it("respects custom branch order", () => {
42
+ const branch = make_branch({
43
+ type: "feat",
44
+ ticket: "ABC-1",
45
+ description: "add-parser",
46
+ user: "erik",
47
+ });
48
+ const config = make_config({
49
+ branch_order: ["type", "ticket", "description", "user"],
50
+ branch_description: { separator: "/" },
51
+ });
52
+
53
+ expect(build_branch(branch, config)).toBe("feat/ABC-1-add-parser/erik");
54
+ });
55
+
56
+ it("uses per-field separator config", () => {
57
+ const branch = make_branch({
58
+ user: "erik",
59
+ type: "feat",
60
+ ticket: "ABC-1",
61
+ description: "parser",
62
+ });
63
+ const config = make_config({
64
+ branch_user: { separator: "_" },
65
+ branch_type: { separator: "-" },
66
+ branch_ticket: { separator: "_" },
67
+ branch_description: { separator: "" },
68
+ branch_order: ["user", "type", "ticket", "description"],
69
+ });
70
+
71
+ expect(build_branch(branch, config)).toBe("erik_feat-ABC-1_parser");
72
+ });
73
+ });
74
+
75
+ describe("build_worktree_path", () => {
76
+ it("builds path from template tokens", () => {
77
+ const branch = make_branch({
78
+ user: "erik",
79
+ type: "feat",
80
+ ticket: "ABC-1",
81
+ description: "add-parser",
82
+ version: "1.2.0",
83
+ });
84
+ const config = make_config({
85
+ worktrees: {
86
+ base_path: "../worktrees",
87
+ folder_template: "{{repo_name}}-{{ticket}}-{{branch_description}}",
88
+ },
89
+ });
90
+
91
+ const result = build_worktree_path(branch, config, "/tmp/my-repo");
92
+ expect(result).toBe("../worktrees/my-repo-ABC-1-add-parser");
93
+ });
94
+
95
+ it("handles base_path with trailing slash", () => {
96
+ const branch = make_branch({
97
+ description: "add-parser",
98
+ });
99
+ const config = make_config({
100
+ worktrees: {
101
+ base_path: "../wt/",
102
+ folder_template: "{{repo_name}}-{{branch_description}}",
103
+ },
104
+ });
105
+
106
+ const result = build_worktree_path(branch, config, "/tmp/repo");
107
+ expect(result).toBe("../wt/repo-add-parser");
108
+ });
109
+
110
+ it("normalizes repeated dashes and trims edges", () => {
111
+ const branch = make_branch({
112
+ ticket: "",
113
+ description: "parser",
114
+ });
115
+ const config = make_config({
116
+ worktrees: {
117
+ base_path: "../wt",
118
+ folder_template: "-{{repo_name}}--{{ticket}}--{{branch_description}}-",
119
+ },
120
+ });
121
+
122
+ const result = build_worktree_path(branch, config, "/tmp/repo");
123
+ expect(result).toBe("../wt/repo-parser");
124
+ });
125
+
126
+ it("removes whitespace from rendered worktree name", () => {
127
+ const branch = make_branch({
128
+ user: "erik",
129
+ description: "new feature",
130
+ });
131
+ const config = make_config({
132
+ worktrees: {
133
+ base_path: "../wt",
134
+ folder_template: "{{repo_name}}-{{user}}-{{branch_description}}",
135
+ },
136
+ });
137
+
138
+ const result = build_worktree_path(branch, config, "/tmp/repo");
139
+ expect(result).toBe("../wt/repo-erik-newfeature");
140
+ });
141
+ });
@@ -0,0 +1,46 @@
1
+ import { InferOutput } from "valibot";
2
+ import { V_BRANCH_CONFIG_FIELDS, V_BRANCH_FIELDS } from "../valibot-consts";
3
+ import { BranchState, Config } from "../valibot-state";
4
+
5
+ export function build_branch(
6
+ branch: InferOutput<typeof BranchState>,
7
+ config: InferOutput<typeof Config>,
8
+ ): string {
9
+ let res = "";
10
+ config.branch_order.forEach((b: InferOutput<typeof V_BRANCH_FIELDS>) => {
11
+ const config_key: InferOutput<typeof V_BRANCH_CONFIG_FIELDS> = `branch_${b}`;
12
+ if (branch[b]) res += branch[b] + config[config_key].separator;
13
+ });
14
+
15
+ if (res.endsWith("-") || res.endsWith("/") || res.endsWith("_")) {
16
+ return res.slice(0, -1).trim();
17
+ }
18
+
19
+ return res.trim();
20
+ }
21
+
22
+ export function build_worktree_path(
23
+ branch_state: InferOutput<typeof BranchState>,
24
+ config: InferOutput<typeof Config>,
25
+ git_root: string,
26
+ ): string {
27
+ const repo_name = git_root.split("/").pop() || "repo";
28
+
29
+ let worktree_name = config.worktrees.folder_template;
30
+
31
+ worktree_name = worktree_name
32
+ .replace("{{repo_name}}", repo_name)
33
+ .replace("{{branch_description}}", branch_state.description)
34
+ .replace("{{user}}", branch_state.user || "")
35
+ .replace("{{type}}", branch_state.type || "")
36
+ .replace("{{ticket}}", branch_state.ticket || "")
37
+ .replace("{{version}}", branch_state.version || "");
38
+
39
+ worktree_name = worktree_name
40
+ .replace(/\s/g, "")
41
+ .replace(/--+/g, "-")
42
+ .replace(/^-+|-+$/g, "");
43
+
44
+ const base_path = config.worktrees.base_path;
45
+ return `${base_path}${base_path.endsWith("/") ? "" : "/"}${worktree_name}`;
46
+ }
@@ -0,0 +1,253 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { InferOutput, parse } from "valibot";
3
+ import { CommitState, Config } from "../valibot-state";
4
+ import { build_commit_string } from "./build-commit-string";
5
+
6
+ type ConfigOutput = InferOutput<typeof Config>;
7
+ type ConfigOverrides = Partial<
8
+ Omit<ConfigOutput, "check_ticket" | "breaking_change">
9
+ > & {
10
+ check_ticket?: Partial<ConfigOutput["check_ticket"]>;
11
+ breaking_change?: Partial<ConfigOutput["breaking_change"]>;
12
+ };
13
+
14
+ function make_config(overrides?: ConfigOverrides): ConfigOutput {
15
+ const base = parse_config({});
16
+ if (!overrides) return base;
17
+
18
+ return {
19
+ ...base,
20
+ ...overrides,
21
+ check_ticket: {
22
+ ...base.check_ticket,
23
+ ...(overrides.check_ticket ?? {}),
24
+ },
25
+ breaking_change: {
26
+ ...base.breaking_change,
27
+ ...(overrides.breaking_change ?? {}),
28
+ },
29
+ };
30
+ }
31
+
32
+ function parse_config(input: Record<string, unknown>) {
33
+ return parse(Config, input);
34
+ }
35
+
36
+ function make_state(input: Record<string, unknown>) {
37
+ return parse(CommitState, input);
38
+ }
39
+
40
+ describe("build_commit_string", () => {
41
+ it("builds a basic conventional commit title", () => {
42
+ const result = build_commit_string({
43
+ commit_state: make_state({
44
+ type: "feat",
45
+ scope: "api",
46
+ title: "add endpoint",
47
+ }),
48
+ config: make_config(),
49
+ });
50
+
51
+ expect(result).toBe("feat(api): add endpoint");
52
+ });
53
+
54
+ it("adds ticket at start of title by default", () => {
55
+ const result = build_commit_string({
56
+ commit_state: make_state({
57
+ type: "fix",
58
+ title: "handle null values",
59
+ ticket: "ABC-123",
60
+ }),
61
+ config: make_config(),
62
+ });
63
+
64
+ expect(result).toBe("fix: ABC-123 handle null values");
65
+ });
66
+
67
+ it("supports ticket before colon", () => {
68
+ const result = build_commit_string({
69
+ commit_state: make_state({
70
+ type: "feat",
71
+ scope: "core",
72
+ title: "ship parser",
73
+ ticket: "PROJ-9",
74
+ }),
75
+ config: make_config({
76
+ check_ticket: {
77
+ title_position: "before-colon",
78
+ },
79
+ }),
80
+ });
81
+
82
+ expect(result).toBe("feat(core) PROJ-9: ship parser");
83
+ });
84
+
85
+ it("supports ticket wrapping when surround is configured", () => {
86
+ const result = build_commit_string({
87
+ commit_state: make_state({
88
+ type: "chore",
89
+ title: "update docs",
90
+ ticket: "42",
91
+ }),
92
+ config: make_config({
93
+ check_ticket: {
94
+ surround: "[]",
95
+ },
96
+ }),
97
+ });
98
+
99
+ expect(result).toBe("chore: [42] update docs");
100
+ });
101
+
102
+ it("supports ticket at end of title", () => {
103
+ const result = build_commit_string({
104
+ commit_state: make_state({
105
+ type: "chore",
106
+ title: "cleanup docs",
107
+ ticket: "DOC-7",
108
+ }),
109
+ config: make_config({
110
+ check_ticket: {
111
+ title_position: "end",
112
+ },
113
+ }),
114
+ });
115
+
116
+ expect(result).toBe("chore: cleanup docs DOC-7");
117
+ });
118
+
119
+ it("supports ticket at beginning of commit header", () => {
120
+ const result = build_commit_string({
121
+ commit_state: make_state({
122
+ type: "feat",
123
+ scope: "api",
124
+ title: "add endpoint",
125
+ ticket: "ABC-1",
126
+ }),
127
+ config: make_config({
128
+ check_ticket: {
129
+ title_position: "beginning",
130
+ },
131
+ }),
132
+ });
133
+
134
+ expect(result).toBe("ABC-1 feat(api): add endpoint");
135
+ });
136
+
137
+ it("appends body and footer blocks with spacing", () => {
138
+ const result = build_commit_string({
139
+ commit_state: make_state({
140
+ type: "feat",
141
+ title: "add cli",
142
+ body: "line 1\\nline 2",
143
+ custom_footer: "Refs: ABC-1",
144
+ }),
145
+ config: make_config(),
146
+ });
147
+
148
+ expect(result).toBe("feat: add cli\n\nline 1\nline 2\n\nRefs: ABC-1");
149
+ });
150
+
151
+ it("includes trailer only when include_trailer is true", () => {
152
+ const state = make_state({
153
+ type: "feat",
154
+ title: "add cli",
155
+ trailer: "Signed-off-by: Erik",
156
+ });
157
+ const config = make_config();
158
+
159
+ const no_trailer = build_commit_string({
160
+ commit_state: state,
161
+ config,
162
+ include_trailer: false,
163
+ });
164
+ const with_trailer = build_commit_string({
165
+ commit_state: state,
166
+ config,
167
+ include_trailer: true,
168
+ });
169
+
170
+ expect(no_trailer).toBe("feat: add cli");
171
+ expect(with_trailer).toBe("feat: add cli\n\nSigned-off-by: Erik");
172
+ });
173
+
174
+ it("skips title ticket when add_to_title is false but keeps closes footer", () => {
175
+ const result = build_commit_string({
176
+ commit_state: make_state({
177
+ type: "fix",
178
+ title: "repair parser",
179
+ ticket: "BUG-99",
180
+ closes: "closes",
181
+ }),
182
+ config: make_config({
183
+ check_ticket: {
184
+ add_to_title: false,
185
+ },
186
+ }),
187
+ });
188
+
189
+ expect(result).toBe("fix: repair parser\n\ncloses BUG-99");
190
+ });
191
+
192
+ it("adds breaking exclamation when enabled", () => {
193
+ const result = build_commit_string({
194
+ commit_state: make_state({
195
+ type: "feat",
196
+ title: "replace api",
197
+ breaking_title: "v1 removed",
198
+ }),
199
+ config: make_config({
200
+ breaking_change: {
201
+ add_exclamation_to_title: true,
202
+ },
203
+ }),
204
+ });
205
+
206
+ expect(result).toBe("feat!: replace api\n\nBREAKING CHANGE: v1 removed");
207
+ });
208
+
209
+ it("renders deprecation title and body blocks", () => {
210
+ const result = build_commit_string({
211
+ commit_state: make_state({
212
+ type: "chore",
213
+ title: "remove legacy helper",
214
+ deprecates_title: "legacy helper",
215
+ deprecates_body: "use modern helper",
216
+ }),
217
+ config: make_config(),
218
+ });
219
+
220
+ expect(result).toBe(
221
+ "chore: remove legacy helper\n\nDEPRECATED: legacy helper\n\nuse modern helper",
222
+ );
223
+ });
224
+
225
+ it("formats before-colon ticket without type and scope", () => {
226
+ const result = build_commit_string({
227
+ commit_state: make_state({
228
+ title: "ship parser",
229
+ ticket: "PROJ-9",
230
+ }),
231
+ config: make_config({
232
+ check_ticket: {
233
+ title_position: "before-colon",
234
+ },
235
+ }),
236
+ });
237
+
238
+ expect(result).toBe("PROJ-9: ship parser");
239
+ });
240
+
241
+ it("escapes double quotes and backticks for shell-safe command composition", () => {
242
+ const result = build_commit_string({
243
+ commit_state: make_state({
244
+ type: "fix",
245
+ title: 'handle "quoted" `inline` value',
246
+ }),
247
+ config: make_config(),
248
+ escape_quotes: true,
249
+ });
250
+
251
+ expect(result).toBe('fix: handle \\"quoted\\" \\`inline\\` value');
252
+ });
253
+ });
@@ -0,0 +1,158 @@
1
+ import color from "picocolors";
2
+ import { InferOutput } from "valibot";
3
+ import { CommitState, Config } from "../valibot-state";
4
+
5
+ type BuildCommitStringInput = {
6
+ commit_state: InferOutput<typeof CommitState>;
7
+ config: InferOutput<typeof Config>;
8
+ colorize?: boolean;
9
+ escape_quotes?: boolean;
10
+ include_trailer?: boolean;
11
+ };
12
+
13
+ export function build_commit_string({
14
+ commit_state,
15
+ config,
16
+ colorize = false,
17
+ escape_quotes = false,
18
+ include_trailer = false,
19
+ }: BuildCommitStringInput): string {
20
+ let commit_string = "";
21
+ if (commit_state.type) {
22
+ commit_string += colorize
23
+ ? color.blue(commit_state.type)
24
+ : commit_state.type;
25
+ }
26
+
27
+ if (commit_state.scope) {
28
+ const scope = colorize
29
+ ? color.cyan(commit_state.scope)
30
+ : commit_state.scope;
31
+ commit_string += `(${scope})`;
32
+ }
33
+
34
+ let title_ticket = commit_state.ticket;
35
+ const surround = config.check_ticket.surround;
36
+ if (commit_state.ticket && surround) {
37
+ const open_token = surround.charAt(0);
38
+ const close_token = surround.charAt(1);
39
+ title_ticket = `${open_token}${commit_state.ticket}${close_token}`;
40
+ }
41
+
42
+ const position_beginning = config.check_ticket.title_position === "beginning";
43
+ if (title_ticket && config.check_ticket.add_to_title && position_beginning) {
44
+ commit_string = `${colorize ? color.magenta(title_ticket) : title_ticket} ${commit_string}`;
45
+ }
46
+
47
+ const position_before_colon =
48
+ config.check_ticket.title_position === "before-colon";
49
+ if (
50
+ title_ticket &&
51
+ config.check_ticket.add_to_title &&
52
+ position_before_colon
53
+ ) {
54
+ const spacing =
55
+ commit_state.scope || (commit_state.type && !config.check_ticket.surround)
56
+ ? " "
57
+ : "";
58
+ commit_string += colorize
59
+ ? color.magenta(spacing + title_ticket)
60
+ : spacing + title_ticket;
61
+ }
62
+
63
+ if (
64
+ commit_state.breaking_title &&
65
+ config.breaking_change.add_exclamation_to_title
66
+ ) {
67
+ commit_string += colorize ? color.red("!") : "!";
68
+ }
69
+
70
+ if (
71
+ commit_state.scope ||
72
+ commit_state.type ||
73
+ (title_ticket && position_before_colon)
74
+ ) {
75
+ commit_string += ": ";
76
+ }
77
+
78
+ const position_start = config.check_ticket.title_position === "start";
79
+ const position_end = config.check_ticket.title_position === "end";
80
+ if (title_ticket && config.check_ticket.add_to_title && position_start) {
81
+ commit_string += colorize
82
+ ? color.magenta(title_ticket) + " "
83
+ : title_ticket + " ";
84
+ }
85
+
86
+ if (commit_state.title) {
87
+ commit_string += colorize
88
+ ? color.reset(commit_state.title)
89
+ : commit_state.title;
90
+ }
91
+
92
+ if (title_ticket && config.check_ticket.add_to_title && position_end) {
93
+ commit_string +=
94
+ " " + (colorize ? color.magenta(title_ticket) : title_ticket);
95
+ }
96
+
97
+ if (commit_state.body) {
98
+ const temp = commit_state.body.split("\\n");
99
+ const res = temp
100
+ .map((value) => (colorize ? color.reset(value.trim()) : value.trim()))
101
+ .join("\n");
102
+ commit_string += `\n\n${res}`;
103
+ }
104
+
105
+ if (commit_state.breaking_title) {
106
+ const title = colorize
107
+ ? color.red(`BREAKING CHANGE: ${commit_state.breaking_title}`)
108
+ : `BREAKING CHANGE: ${commit_state.breaking_title}`;
109
+ commit_string += `\n\n${title}`;
110
+ }
111
+
112
+ if (commit_state.breaking_body) {
113
+ const body = colorize
114
+ ? color.red(commit_state.breaking_body)
115
+ : commit_state.breaking_body;
116
+ commit_string += `\n\n${body}`;
117
+ }
118
+
119
+ if (commit_state.deprecates_title) {
120
+ const title = colorize
121
+ ? color.yellow(`DEPRECATED: ${commit_state.deprecates_title}`)
122
+ : `DEPRECATED: ${commit_state.deprecates_title}`;
123
+ commit_string += `\n\n${title}`;
124
+ }
125
+
126
+ if (commit_state.deprecates_body) {
127
+ const body = colorize
128
+ ? color.yellow(commit_state.deprecates_body)
129
+ : commit_state.deprecates_body;
130
+ commit_string += `\n\n${body}`;
131
+ }
132
+
133
+ if (commit_state.custom_footer) {
134
+ const temp = commit_state.custom_footer.split("\\n");
135
+ const res = temp
136
+ .map((value) => (colorize ? color.reset(value.trim()) : value.trim()))
137
+ .join("\n");
138
+ commit_string += `\n\n${res}`;
139
+ }
140
+
141
+ if (commit_state.closes && commit_state.ticket) {
142
+ commit_string += colorize
143
+ ? `\n\n${color.reset(commit_state.closes)} ${color.magenta(commit_state.ticket)}`
144
+ : `\n\n${commit_state.closes} ${commit_state.ticket}`;
145
+ }
146
+
147
+ if (include_trailer && commit_state.trailer) {
148
+ commit_string += colorize
149
+ ? `\n\n${color.dim(commit_state.trailer)}`
150
+ : `\n\n${commit_state.trailer}`;
151
+ }
152
+
153
+ if (escape_quotes) {
154
+ commit_string = commit_string.replaceAll('"', '\\"').replaceAll("`", "\\`");
155
+ }
156
+
157
+ return commit_string;
158
+ }
@@ -0,0 +1,24 @@
1
+ type CommitTitleSizeInput = {
2
+ type?: string;
3
+ scope?: string;
4
+ ticket?: string;
5
+ title?: string;
6
+ };
7
+
8
+ type CommitTitleSizeOptions = {
9
+ include_ticket: boolean;
10
+ };
11
+
12
+ export function get_commit_title_size(
13
+ val: CommitTitleSizeInput,
14
+ options: CommitTitleSizeOptions,
15
+ ): number {
16
+ const commit_scope_size = val.scope ? val.scope.length + 2 : 0;
17
+ const commit_type_size = val.type?.length ?? 0;
18
+ const commit_ticket_size = options.include_ticket
19
+ ? (val.ticket?.length ?? 0)
20
+ : 0;
21
+ const title_size = val.title?.length ?? 0;
22
+
23
+ return commit_scope_size + commit_type_size + commit_ticket_size + title_size;
24
+ }
@@ -0,0 +1,83 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("child_process", () => ({
4
+ execSync: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("../args", () => ({
8
+ flags: {
9
+ interactive: false,
10
+ git_args: "--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
11
+ },
12
+ }));
13
+
14
+ import { execSync } from "child_process";
15
+ import { parse } from "valibot";
16
+ import { Config } from "../valibot-state";
17
+ import {
18
+ infer_not_interactive,
19
+ infer_ticket_from_git,
20
+ infer_type_from_git,
21
+ } from "./infer";
22
+
23
+ const execSyncMock = vi.mocked(execSync);
24
+
25
+ describe("infer", () => {
26
+ beforeEach(() => {
27
+ execSyncMock.mockReset();
28
+ });
29
+
30
+ it('prepends hashtags for inferred tickets when prepend_hashtag is "Always"', () => {
31
+ execSyncMock.mockReturnValue(Buffer.from("feat/127-cli-bombshell-args\n"));
32
+
33
+ const result = infer_ticket_from_git(
34
+ {
35
+ append_hashtag: false,
36
+ prepend_hashtag: "Always",
37
+ },
38
+ "--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
39
+ );
40
+
41
+ expect(result).toBe("#127");
42
+ });
43
+
44
+ it('returns plain inferred tickets when prepend_hashtag is "Never"', () => {
45
+ execSyncMock.mockReturnValue(Buffer.from("feat/ABC-123-add-parser\n"));
46
+
47
+ const result = infer_ticket_from_git(
48
+ {
49
+ append_hashtag: false,
50
+ prepend_hashtag: "Never",
51
+ },
52
+ "--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
53
+ );
54
+
55
+ expect(result).toBe("ABC-123");
56
+ });
57
+
58
+ it("infers commit types from the current branch", () => {
59
+ execSyncMock.mockReturnValue(Buffer.from("everduin94/feat/ABC-123-add-parser\n"));
60
+
61
+ const result = infer_type_from_git(
62
+ [{ value: "feat" }, { value: "fix" }],
63
+ "--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
64
+ );
65
+
66
+ expect(result).toBe("feat");
67
+ });
68
+
69
+ it("builds no-interactive inferred state using current config settings", () => {
70
+ execSyncMock.mockReturnValue(Buffer.from("feat/127-cli-bombshell-args\n"));
71
+
72
+ const config = parse(Config, {
73
+ check_ticket: {
74
+ prepend_hashtag: "Always",
75
+ },
76
+ });
77
+
78
+ expect(infer_not_interactive(config)).toEqual({
79
+ ticket: "#127",
80
+ type: "feat",
81
+ });
82
+ });
83
+ });