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/src/branch.ts CHANGED
@@ -1,263 +1,92 @@
1
1
  #! /usr/bin/env node
2
2
 
3
- import * as p from "@clack/prompts";
4
- import { execSync } from "child_process";
5
3
  import Configstore from "configstore";
6
- import color from "picocolors";
7
4
  import { chdir } from "process";
8
- import { Output, parse } from "valibot";
9
- import {
10
- V_BRANCH_ACTIONS,
11
- V_BRANCH_CONFIG_FIELDS,
12
- V_BRANCH_FIELDS,
13
- } from "./valibot-consts";
5
+ import { InferOutput, ValiError, parse } from "valibot";
14
6
  import { BranchState, CommitState, Config } from "./valibot-state";
15
7
  import {
16
- BRANCH_ACTION_OPTIONS,
17
- CACHE_PROMPT,
18
- OPTIONAL_PROMPT,
19
8
  load_setup,
20
9
  get_git_root,
10
+ NOOP_PROMPT_CACHE,
11
+ ConfigSource,
21
12
  } from "./utils";
22
- import { flags } from "./args";
23
-
24
- main(load_setup(" better-branch "));
25
-
26
- async function main(config: Output<typeof Config>) {
27
- const branch_state = parse(BranchState, {});
28
- chdir(get_git_root());
29
-
30
- let checkout_type: Output<typeof V_BRANCH_ACTIONS> = "branch";
31
- if (config.worktrees.enable) {
32
- const branch_or_worktree = await p.select({
33
- message: `Checkout a branch or create a worktree?`,
34
- initialValue: config.branch_action_default,
35
- options: BRANCH_ACTION_OPTIONS,
36
- });
37
-
38
- if (p.isCancel(branch_or_worktree)) process.exit();
39
- checkout_type = branch_or_worktree;
40
- }
41
-
42
- if (config.branch_user.enable) {
43
- const cache_user_name = get_user_from_cache();
44
- const user_name_required = config.branch_user.required;
45
- const user_name = await p.text({
46
- message: `Type your git username ${
47
- user_name_required ? "" : OPTIONAL_PROMPT
48
- } ${CACHE_PROMPT}`.trim(),
49
- placeholder: "",
50
- initialValue: cache_user_name,
51
- validate: (val) => {
52
- if (user_name_required && !val) return "Please enter a username";
53
- },
54
- });
55
- if (p.isCancel(user_name)) process.exit(0);
56
- branch_state.user = user_name?.replace(/\s+/g, "-")?.toLowerCase() ?? "";
57
- set_user_cache(branch_state.user);
58
- }
13
+ import { BranchRunnable } from "./prompts/branch-runnable";
14
+ import { BranchCheckoutPrompt } from "./prompts/branch-checkout.prompt";
15
+ import { BranchUserPrompt } from "./prompts/branch-user.prompt";
16
+ import { BranchTypePrompt } from "./prompts/branch-type.prompt";
17
+ import { BranchTicketPrompt } from "./prompts/branch-ticket.prompt";
18
+ import { BranchVersionPrompt } from "./prompts/branch-version.prompt";
19
+ import { BranchDescriptionPrompt } from "./prompts/branch-description.prompt";
20
+ import { BranchConfirmPrompt } from "./prompts/branch-confirm.prompt";
21
+ import { branch_flags } from "./branch-args";
22
+ import { print_help_text } from "./branch-help";
23
+ import { create_strict_branch_state } from "./utils/no-interactive-validation";
24
+ import { get_package_version } from "./utils";
25
+ import * as p from "@clack/prompts";
59
26
 
60
- if (config.branch_type.enable) {
61
- let initial_value = config.commit_type.initial_value;
62
- const commit_type = await p.select({
63
- message: `Select a branch type`,
64
- initialValue: initial_value,
65
- options: config.commit_type.options,
66
- });
67
- if (p.isCancel(commit_type)) process.exit(0);
68
- branch_state.type = commit_type;
69
- }
27
+ type PromptCtor = new (
28
+ config: InferOutput<typeof Config>,
29
+ commit_state: InferOutput<typeof BranchState>,
30
+ prompt_cache: Configstore,
31
+ ) => BranchRunnable;
32
+
33
+ const promptCtors: PromptCtor[] = [
34
+ BranchCheckoutPrompt,
35
+ BranchUserPrompt,
36
+ BranchTypePrompt,
37
+ BranchTicketPrompt,
38
+ BranchVersionPrompt,
39
+ BranchDescriptionPrompt,
40
+ BranchConfirmPrompt,
41
+ ];
42
+
43
+ const { config, config_source } = load_setup(
44
+ " better-branch ",
45
+ branch_flags.git_args,
46
+ );
47
+
48
+ main(config, config_source);
49
+
50
+ async function main(
51
+ config: InferOutput<typeof Config>,
52
+ config_source: ConfigSource,
53
+ ) {
54
+ chdir(get_git_root(branch_flags.git_args));
70
55
 
71
- if (config.branch_ticket.enable) {
72
- const ticked_required = config.branch_ticket.required;
73
- const ticket = await p.text({
74
- message: `Type ticket / issue number ${
75
- ticked_required ? "" : OPTIONAL_PROMPT
76
- }`.trim(),
77
- placeholder: "",
78
- validate: (val) => {
79
- if (ticked_required && !val) return "Please enter a ticket / issue";
80
- },
81
- });
82
- if (p.isCancel(ticket)) process.exit(0);
83
- branch_state.ticket = ticket;
56
+ if (branch_flags.version) {
57
+ const version = get_package_version();
58
+ p.log.step("Better Commits v" + version);
59
+ return;
84
60
  }
85
61
 
86
- if (config.branch_version.enable) {
87
- const version_required = config.branch_version.required;
88
- const version = await p.text({
89
- message: `Type version number ${
90
- version_required ? "" : OPTIONAL_PROMPT
91
- }`.trim(),
92
- placeholder: "",
93
- validate: (val) => {
94
- if (version_required && !val) return "Please enter a version";
95
- },
96
- });
97
- if (p.isCancel(version)) process.exit(0);
98
- branch_state.version = version;
62
+ if (branch_flags.help) {
63
+ print_help_text(config, config_source);
64
+ return;
99
65
  }
100
66
 
101
- const description_max_length = config.branch_description.max_length;
102
- const description = await p.text({
103
- message: "Type a short description",
104
- placeholder: "",
105
- validate: (value) => {
106
- if (!value) return "Please enter a description";
107
- if (value.length > description_max_length)
108
- return `Exceeded max length. Description max [${description_max_length}]`;
109
- },
110
- });
111
- if (p.isCancel(description)) process.exit(0);
112
- branch_state.description =
113
- description?.replace(/\s+/g, "-")?.toLowerCase() ?? "";
67
+ const branch_state = parse(BranchState, branch_flags.branch_state);
114
68
 
115
- const pre_commands =
116
- checkout_type === "worktree"
117
- ? config.worktree_pre_commands
118
- : config.branch_pre_commands;
119
- pre_commands.forEach((command) => {
69
+ if (!branch_flags.interactive) {
120
70
  try {
121
- execSync(command, { stdio: "inherit" });
122
- } catch (err) {
123
- p.log.error("Something went wrong when executing pre-commands: " + err);
124
- process.exit(0);
125
- }
126
- });
127
-
128
- const branch_name = build_branch(branch_state, config);
129
- const branch_flag = verify_branch_name(branch_name);
130
- if (checkout_type === "branch") {
131
- try {
132
- execSync(`git ${flags.git_args} checkout ${branch_flag} ${branch_name}`, {
133
- stdio: "inherit",
134
- });
135
- p.log.info(
136
- `Switched to a new branch '${color.bgGreen(
137
- " " + color.black(branch_name) + " ",
138
- )}'`,
139
- );
140
- } catch (err) {
141
- process.exit(0);
142
- }
143
- } else {
144
- try {
145
- const worktree_name = build_worktree_path(branch_state, config);
146
- execSync(
147
- `git worktree add ${worktree_name} ${branch_flag} ${branch_name}`,
148
- {
149
- stdio: "inherit",
150
- },
151
- );
152
- p.log.info(
153
- `Created a new worktree ${color.bgGreen(
154
- +" " + color.black(worktree_name) + " ",
155
- )}, checked out branch ${color.bgGreen(
156
- " " + color.black(branch_name) + " ",
157
- )}`,
158
- );
159
- p.log.info(
160
- color.bgMagenta(color.black(` cd ${worktree_name} `)) +
161
- " to navigate to your new worktree",
162
- );
163
- chdir(worktree_name);
71
+ parse(create_strict_branch_state(config), branch_state);
164
72
  } catch (err) {
73
+ if (err instanceof ValiError) {
74
+ p.log.error(`Invalid branch input: ${err.message}`);
75
+ } else {
76
+ p.log.error(`Failed to validate branch input: ${err}`);
77
+ }
165
78
  process.exit(0);
166
79
  }
167
80
  }
168
81
 
169
- const post_commands =
170
- checkout_type === "worktree"
171
- ? config.worktree_post_commands
172
- : config.branch_post_commands;
173
- post_commands.forEach((command) => {
174
- try {
175
- execSync(command, { stdio: "inherit" });
176
- } catch (err) {
177
- p.log.error("Something went wrong when executing post-commands: " + err);
178
- process.exit(0);
179
- }
180
- });
181
- }
182
-
183
- function build_branch(
184
- branch: Output<typeof BranchState>,
185
- config: Output<typeof Config>,
186
- ) {
187
- let res = "";
188
- config.branch_order.forEach((b: Output<typeof V_BRANCH_FIELDS>) => {
189
- const config_key: Output<typeof V_BRANCH_CONFIG_FIELDS> = `branch_${b}`;
190
- if (branch[b]) res += branch[b] + config[config_key].separator;
191
- });
192
- if (res.endsWith("-") || res.endsWith("/") || res.endsWith("_")) {
193
- return res.slice(0, -1).trim();
194
- }
195
- return res.trim();
196
- }
197
-
198
- function build_worktree_path(
199
- branch_state: Output<typeof BranchState>,
200
- config: Output<typeof Config>,
201
- ): string {
202
- const gitRoot = get_git_root();
203
- const repo_name = gitRoot.split("/").pop() || "repo";
204
-
205
- let worktree_name = config.worktrees.folder_template;
206
-
207
- worktree_name = worktree_name
208
- .replace("{{repo_name}}", repo_name)
209
- .replace("{{branch_description}}", branch_state.description)
210
- .replace("{{user}}", branch_state.user || "")
211
- .replace("{{type}}", branch_state.type || "")
212
- .replace("{{ticket}}", branch_state.ticket || "")
213
- .replace("{{version}}", branch_state.version || "");
214
-
215
- worktree_name = worktree_name
216
- .replace(/\s/g, "")
217
- .replace(/--+/g, "-")
218
- .replace(/^-+|-+$/g, "");
219
-
220
- const base_path = config.worktrees.base_path;
221
- return `${base_path}${base_path.endsWith("/") ? "" : "/"}${worktree_name}`;
222
- }
223
-
224
- function get_user_from_cache(): string {
225
- try {
226
- const config_store = new Configstore("better-commits");
227
- return config_store.get("username") ?? "";
228
- } catch (err) {
229
- p.log.warn(
230
- 'There was an issue accessing username from cache. Check that the folder "~/.config" exists',
231
- );
232
- }
233
-
234
- return "";
235
- }
236
-
237
- function verify_branch_name(branch_name: string): string {
238
- // TODO: There has to be a better way 🤦
239
- let branch_flag = "";
240
- try {
241
- execSync(`git show-ref ${branch_name}`, { encoding: "utf-8" });
242
- p.log.warning(
243
- color.yellow(
244
- `${branch_name} already exists! Checking out existing branch.`,
245
- ),
246
- );
247
- } catch (err) {
248
- // Branch does not exist
249
- branch_flag = "-b";
250
- }
251
-
252
- return branch_flag;
253
- }
254
-
255
- function set_user_cache(val: string): void {
256
- try {
257
- const config_store = new Configstore("better-commits");
258
- config_store.set("username", val);
259
- } catch (err) {
260
- // fail silently, user has likely already seen guidance via get exceptions at this point
82
+ const prompt_cache = config.cache_last_value
83
+ ? new Configstore("better-commits")
84
+ : NOOP_PROMPT_CACHE;
85
+ const prompts_to_run = branch_flags.interactive
86
+ ? promptCtors
87
+ : [BranchConfirmPrompt];
88
+ for (const Prompt of prompts_to_run) {
89
+ await new Prompt(config, branch_state, prompt_cache).run();
261
90
  }
262
91
  }
263
92
 
package/src/help.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { execSync } from "child_process";
2
+ import { InferOutput } from "valibot";
3
+ import { flags } from "./args";
4
+ import { get_package_version } from "./utils";
5
+ import { infer_ticket_from_git, infer_type_from_git } from "./utils/infer";
6
+ import { Config } from "./valibot-state";
7
+ import color from "picocolors";
8
+
9
+ const ADDITIONAL_COMMAND_DEFINITIONS: Record<string, string> = {
10
+ "better-branch": "Create a branch or worktree from a guided prompt flow.",
11
+ "better-commits-init":
12
+ "Create a .better-commits.json config in this repository.",
13
+ };
14
+
15
+ const CLI_FLAG_DEFINITIONS: Record<string, string> = {
16
+ "--interactive": "Run in interactive prompt mode (default behavior).",
17
+ "--dry-run": "Print the commit command without creating a commit.",
18
+ "--help": "Show help information and exit.",
19
+ };
20
+
21
+ const COMMIT_FLAG_DEFINITIONS: Record<string, string> = {
22
+ "--type": "Set commit type (for example feat, fix, docs).",
23
+ "--scope": "Set commit scope.",
24
+ "--title": "Set commit title/description.",
25
+ "--body": "Set commit body text.",
26
+ "--closes": "Set issue/ticket id for a closes footer.",
27
+ "--ticket": "Set ticket value used in the title.",
28
+ "--trailer": "Set trailer footer value.",
29
+ "--deprecates": "Set issue/ticket id for a deprecates footer.",
30
+ "--breaking-title": "Set breaking-change title footer.",
31
+ "--breaking-body": "Set breaking-change body footer.",
32
+ "--deprecates-title": "Set deprecates footer title text.",
33
+ "--deprecates-body": "Set deprecates footer body text.",
34
+ "--custom-footer": "Set a custom footer line.",
35
+ };
36
+
37
+ const GIT_FLAG_DEFINITIONS: Record<string, string> = {
38
+ "--git-dir": "Set the path to the .git directory.",
39
+ "--work-tree": "Set the path to the working tree root.",
40
+ };
41
+
42
+ function to_definition_lines(definitions: Record<string, string>): string {
43
+ const description_column = 26;
44
+ const minimum_spacing = 2;
45
+ const indent = " ";
46
+
47
+ return Object.entries(definitions)
48
+ .map(([name, description]) => {
49
+ const spaces = Math.max(
50
+ minimum_spacing,
51
+ description_column - name.length,
52
+ );
53
+ return `${indent}${name}${" ".repeat(spaces)}${description}`;
54
+ })
55
+ .join("\n");
56
+ }
57
+
58
+ export function print_help_text(
59
+ config: InferOutput<typeof Config>,
60
+ config_source: "repository" | "global" | "none",
61
+ ) {
62
+ const version = get_package_version();
63
+
64
+ let branch = "(none)";
65
+ try {
66
+ branch =
67
+ execSync(`git ${flags.git_args} branch --show-current`, { stdio: "pipe" })
68
+ .toString()
69
+ .trim() || "(none)";
70
+ } catch {
71
+ // noop
72
+ }
73
+
74
+ const inferred_type =
75
+ infer_type_from_git(config.commit_type.options, flags.git_args) ||
76
+ "Unknown";
77
+ const inferred_ticket = config.check_ticket.infer_ticket
78
+ ? infer_ticket_from_git(
79
+ {
80
+ append_hashtag: config.check_ticket.append_hashtag,
81
+ prepend_hashtag: config.check_ticket.prepend_hashtag,
82
+ },
83
+ flags.git_args,
84
+ ) || "Unknown"
85
+ : "Infer Disabled";
86
+
87
+ const types = config.commit_type.options
88
+ .map((option) => option.value)
89
+ .join(", ")
90
+ .trim();
91
+ const scopes = config.commit_scope.options
92
+ .map((option) => option.value)
93
+ .join(", ")
94
+ .trim();
95
+ const cli_flags = to_definition_lines(CLI_FLAG_DEFINITIONS);
96
+ const git_flags = to_definition_lines(GIT_FLAG_DEFINITIONS);
97
+ const commit_flags = to_definition_lines(COMMIT_FLAG_DEFINITIONS);
98
+ const additional_commands = to_definition_lines(
99
+ ADDITIONAL_COMMAND_DEFINITIONS,
100
+ );
101
+
102
+ console.log(`
103
+ ${color.green(" better-commits")} ${color.gray("v" + version)}
104
+
105
+ ${color.gray("BRANCH")}
106
+ ${branch}
107
+ ${color.gray("Type")} ${color.blue(inferred_type)} ${color.gray("·")} ${color.gray("Ticket")} ${color.magenta(inferred_ticket)}
108
+
109
+ ${color.gray("CONFIGURATION")}
110
+ ${config_source}
111
+
112
+ ${color.gray("Types")}
113
+ ${types}
114
+
115
+ ${color.gray("Scopes")}
116
+ ${scopes}
117
+
118
+ ${color.gray("CLI FLAGS")}
119
+ ${cli_flags}
120
+
121
+ ${color.gray("Commit Flags")}
122
+ ${commit_flags}
123
+
124
+ ${color.gray("Git Flags (Advanced)")}
125
+ ${git_flags}
126
+
127
+ ${color.gray("ADDITIONAL COMMANDS")}
128
+ ${additional_commands}
129
+
130
+ `);
131
+ }
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ describe("project test setup", () => {
4
+ it("runs Vitest in this TypeScript project", () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ });