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,134 @@
1
+ import * as p from "@clack/prompts";
2
+ import { execSync } from "child_process";
3
+ import color from "picocolors";
4
+ import { chdir } from "process";
5
+ import { branch_flags } from "../branch-args";
6
+ import { get_git_root } from "../utils";
7
+ import { build_branch, build_worktree_path } from "../utils/build-branch";
8
+ import { BranchRunnable } from "./branch-runnable";
9
+
10
+ export class BranchConfirmPrompt extends BranchRunnable {
11
+ async run(): Promise<void> {
12
+ this.#run_pre_commands();
13
+ this.#run_checkout();
14
+ this.#run_post_commands();
15
+ }
16
+
17
+ get #is_worktree(): boolean {
18
+ return this.branch_state.checkout === "worktree";
19
+ }
20
+
21
+ get #pre_commands(): string[] {
22
+ return this.#is_worktree
23
+ ? this.config.worktree_pre_commands
24
+ : this.config.branch_pre_commands;
25
+ }
26
+
27
+ get #post_commands(): string[] {
28
+ return this.#is_worktree
29
+ ? this.config.worktree_post_commands
30
+ : this.config.branch_post_commands;
31
+ }
32
+
33
+ get #branch_name(): string {
34
+ return build_branch(this.branch_state, this.config);
35
+ }
36
+
37
+ #run_pre_commands(): void {
38
+ this.#run_commands(
39
+ this.#pre_commands,
40
+ "Something went wrong when executing pre-commands: ",
41
+ );
42
+ }
43
+
44
+ #run_checkout(): void {
45
+ const branch_name = this.#branch_name;
46
+ const branch_flag = this.#verify_branch_name(branch_name);
47
+
48
+ if (!this.#is_worktree) {
49
+ try {
50
+ execSync(
51
+ `git ${branch_flags.git_args} checkout ${branch_flag} ${branch_name}`,
52
+ {
53
+ stdio: "inherit",
54
+ },
55
+ );
56
+ p.log.info(
57
+ `Switched to a new branch '${color.bgGreen(
58
+ " " + color.black(branch_name) + " ",
59
+ )}'`,
60
+ );
61
+ } catch (err) {
62
+ process.exit(0);
63
+ }
64
+
65
+ return;
66
+ }
67
+
68
+ try {
69
+ const worktree_name = build_worktree_path(
70
+ this.branch_state,
71
+ this.config,
72
+ get_git_root(branch_flags.git_args),
73
+ );
74
+ execSync(
75
+ `git ${branch_flags.git_args} worktree add ${worktree_name} ${branch_flag} ${branch_name}`,
76
+ {
77
+ stdio: "inherit",
78
+ },
79
+ );
80
+ p.log.info(
81
+ `Created a new worktree ${color.bgGreen(
82
+ " " + color.black(worktree_name) + " ",
83
+ )}, checked out branch ${color.bgGreen(
84
+ " " + color.black(branch_name) + " ",
85
+ )}`,
86
+ );
87
+ p.log.info(
88
+ color.bgMagenta(color.black(` cd ${worktree_name} `)) +
89
+ " to navigate to your new worktree",
90
+ );
91
+ chdir(worktree_name);
92
+ } catch (err) {
93
+ process.exit(0);
94
+ }
95
+ }
96
+
97
+ #run_post_commands(): void {
98
+ this.#run_commands(
99
+ this.#post_commands,
100
+ "Something went wrong when executing post-commands: ",
101
+ );
102
+ }
103
+
104
+ #run_commands(commands: string[], error_message: string): void {
105
+ commands.forEach((command) => {
106
+ try {
107
+ execSync(command, { stdio: "inherit" });
108
+ } catch (err) {
109
+ p.log.error(error_message + err);
110
+ process.exit(0);
111
+ }
112
+ });
113
+ }
114
+
115
+ #verify_branch_name(branch_name: string): string {
116
+ // TODO: There has to be a better way 🤦
117
+ let branch_flag = "";
118
+ try {
119
+ execSync(`git ${branch_flags.git_args} show-ref ${branch_name}`, {
120
+ encoding: "utf-8",
121
+ });
122
+ p.log.warning(
123
+ color.yellow(
124
+ `${branch_name} already exists! Checking out existing branch.`,
125
+ ),
126
+ );
127
+ } catch (err) {
128
+ // Branch does not exist
129
+ branch_flag = "-b";
130
+ }
131
+
132
+ return branch_flag;
133
+ }
134
+ }
@@ -0,0 +1,37 @@
1
+ import * as p from "@clack/prompts";
2
+ import { BranchRunnable } from "./branch-runnable";
3
+
4
+ export class BranchDescriptionPrompt extends BranchRunnable {
5
+ async run(): Promise<void> {
6
+ const description = await p.text({
7
+ message: this.#message,
8
+ placeholder: "",
9
+ validate: (value) => this.#validate(value),
10
+ initialValue: this.branch_state.description,
11
+ });
12
+
13
+ if (p.isCancel(description)) process.exit(0);
14
+ this.#run_post_effects(description ?? "");
15
+ }
16
+
17
+ get #message(): string {
18
+ return "Type a short description";
19
+ }
20
+
21
+ get #max_length(): number {
22
+ return this.config.branch_description.max_length;
23
+ }
24
+
25
+ #validate(value: string | undefined): string | undefined {
26
+ if (!value) return "Please enter a description";
27
+ if (value.length > this.#max_length) {
28
+ return `Exceeded max length. Description max [${this.#max_length}]`;
29
+ }
30
+ }
31
+
32
+ #run_post_effects(prompt_result: string): void {
33
+ this.branch_state.description = prompt_result
34
+ .replace(/\s+/g, "-")
35
+ .toLowerCase();
36
+ }
37
+ }
@@ -0,0 +1,13 @@
1
+ import { InferOutput } from "valibot";
2
+ import { BranchState, Config } from "../valibot-state";
3
+ import Configstore from "configstore";
4
+
5
+ export abstract class BranchRunnable {
6
+ constructor(
7
+ protected config: InferOutput<typeof Config>,
8
+ protected branch_state: InferOutput<typeof BranchState>,
9
+ protected prompt_cache: Configstore,
10
+ ) {}
11
+
12
+ abstract run(): Promise<void>;
13
+ }
@@ -0,0 +1,41 @@
1
+ import * as p from "@clack/prompts";
2
+ import { optional_message } from "../utils/messages";
3
+ import { BranchRunnable } from "./branch-runnable";
4
+
5
+ export class BranchTicketPrompt extends BranchRunnable {
6
+ async run(): Promise<void> {
7
+ if (!this.#is_enabled) return;
8
+
9
+ const ticket = await p.text({
10
+ message: this.#message,
11
+ placeholder: "",
12
+ validate: (value) => this.#validate(value),
13
+ initialValue: this.branch_state.ticket,
14
+ });
15
+
16
+ if (p.isCancel(ticket)) process.exit(0);
17
+ this.#run_post_effects(ticket ?? "");
18
+ }
19
+
20
+ get #is_enabled(): boolean {
21
+ return this.config.branch_ticket.enable;
22
+ }
23
+
24
+ get #is_required(): boolean {
25
+ return this.config.branch_ticket.required;
26
+ }
27
+
28
+ get #message(): string {
29
+ return this.#is_required
30
+ ? "Type ticket / issue number"
31
+ : optional_message("Type ticket / issue number");
32
+ }
33
+
34
+ #validate(value: string | undefined): string | undefined {
35
+ if (this.#is_required && !value) return "Please enter a ticket / issue";
36
+ }
37
+
38
+ #run_post_effects(prompt_result: string): void {
39
+ this.branch_state.ticket = prompt_result;
40
+ }
41
+ }
@@ -0,0 +1,43 @@
1
+ import * as p from "@clack/prompts";
2
+ import { BranchRunnable } from "./branch-runnable";
3
+
4
+ export class BranchTypePrompt extends BranchRunnable {
5
+ async run(): Promise<void> {
6
+ if (!this.#is_enabled) return;
7
+
8
+ const branch_type = await p.select({
9
+ message: this.#message,
10
+ initialValue: this.#initial_value,
11
+ options: this.#options,
12
+ });
13
+
14
+ if (p.isCancel(branch_type)) process.exit(0);
15
+ this.#run_post_effects(branch_type);
16
+ }
17
+
18
+ get #is_enabled(): boolean {
19
+ return this.config.branch_type.enable;
20
+ }
21
+
22
+ get #message(): string {
23
+ return "Select a branch type";
24
+ }
25
+
26
+ get #initial_value(): string {
27
+ return this.branch_state.type || this.config.commit_type.initial_value;
28
+ }
29
+
30
+ get #options(): {
31
+ label?: string | undefined;
32
+ value: string;
33
+ emoji?: string;
34
+ hint?: string;
35
+ trailer?: string;
36
+ }[] {
37
+ return this.config.commit_type.options;
38
+ }
39
+
40
+ #run_post_effects(prompt_result: string): void {
41
+ this.branch_state.type = prompt_result;
42
+ }
43
+ }
@@ -0,0 +1,50 @@
1
+ import * as p from "@clack/prompts";
2
+ import { get_value_from_cache, set_value_cache } from "../utils";
3
+ import { optional_message } from "../utils/messages";
4
+ import { BranchRunnable } from "./branch-runnable";
5
+
6
+ export class BranchUserPrompt extends BranchRunnable {
7
+ async run(): Promise<void> {
8
+ if (!this.#is_enabled) return;
9
+
10
+ const user_name = await p.text({
11
+ message: this.#message,
12
+ placeholder: "",
13
+ initialValue: this.#initial_value,
14
+ validate: (value) => this.#validate(value),
15
+ });
16
+
17
+ if (p.isCancel(user_name)) process.exit(0);
18
+ this.#run_post_effects(user_name ?? "");
19
+ }
20
+
21
+ get #is_enabled(): boolean {
22
+ return this.config.branch_user.enable;
23
+ }
24
+
25
+ get #is_required(): boolean {
26
+ return this.config.branch_user.required;
27
+ }
28
+
29
+ get #message(): string {
30
+ return this.#is_required
31
+ ? "Type your git username"
32
+ : optional_message("Type your git username");
33
+ }
34
+
35
+ get #initial_value(): string {
36
+ return (
37
+ this.branch_state.user ||
38
+ get_value_from_cache(this.prompt_cache, "username")
39
+ );
40
+ }
41
+
42
+ #validate(value: string | undefined): string | undefined {
43
+ if (this.#is_required && !value) return "Please enter a username";
44
+ }
45
+
46
+ #run_post_effects(prompt_result: string): void {
47
+ this.branch_state.user = prompt_result.replace(/\s+/g, "-").toLowerCase();
48
+ set_value_cache(this.prompt_cache, "username", this.branch_state.user);
49
+ }
50
+ }
@@ -0,0 +1,41 @@
1
+ import * as p from "@clack/prompts";
2
+ import { optional_message } from "../utils/messages";
3
+ import { BranchRunnable } from "./branch-runnable";
4
+
5
+ export class BranchVersionPrompt extends BranchRunnable {
6
+ async run(): Promise<void> {
7
+ if (!this.#is_enabled) return;
8
+
9
+ const version = await p.text({
10
+ message: this.#message,
11
+ placeholder: "",
12
+ validate: (value) => this.#validate(value),
13
+ initialValue: this.branch_state.version,
14
+ });
15
+
16
+ if (p.isCancel(version)) process.exit(0);
17
+ this.#run_post_effects(version ?? "");
18
+ }
19
+
20
+ get #is_enabled(): boolean {
21
+ return this.config.branch_version.enable;
22
+ }
23
+
24
+ get #is_required(): boolean {
25
+ return this.config.branch_version.required;
26
+ }
27
+
28
+ get #message(): string {
29
+ return this.#is_required
30
+ ? "Type version number"
31
+ : optional_message("Type version number");
32
+ }
33
+
34
+ #validate(value: string | undefined): string | undefined {
35
+ if (this.#is_required && !value) return "Please enter a version";
36
+ }
37
+
38
+ #run_post_effects(prompt_result: string): void {
39
+ this.branch_state.version = prompt_result;
40
+ }
41
+ }
@@ -0,0 +1,57 @@
1
+ import * as p from "@clack/prompts";
2
+ import { get_value_from_cache, set_value_cache } from "../utils";
3
+ import { cache_message, optional_message } from "../utils/messages";
4
+ import { Runnable } from "./runnable";
5
+
6
+ export class CommitBodyPrompt extends Runnable {
7
+ async run(): Promise<void> {
8
+ if (!this.#is_enabled) return;
9
+
10
+ const { initial_value, message } = this.#get_initial_value();
11
+ const commit_body = await p.text({
12
+ message,
13
+ initialValue: initial_value,
14
+ placeholder: "",
15
+ validate: (value) => this.#validate(value),
16
+ });
17
+
18
+ if (p.isCancel(commit_body)) process.exit(0);
19
+ this.#run_post_effects(commit_body ?? "");
20
+ }
21
+
22
+ get #is_enabled(): boolean {
23
+ return this.config.commit_body.enable;
24
+ }
25
+
26
+ #get_initial_value(): { initial_value: string; message: string } {
27
+ const cache_value = get_value_from_cache(this.prompt_cache, "commit_body");
28
+ if (cache_value) {
29
+ return {
30
+ initial_value: cache_value,
31
+ message: cache_message("Commit body"),
32
+ };
33
+ }
34
+
35
+ return {
36
+ initial_value: "",
37
+ message: optional_message("Write a detailed description of the changes"),
38
+ };
39
+ }
40
+
41
+ #validate(value: string | undefined): string | undefined {
42
+ if (this.config.commit_body.required && !value) {
43
+ return "Please enter a description";
44
+ }
45
+ }
46
+
47
+ #split_by_period(value: string): string {
48
+ if (!this.config.commit_body.split_by_period) return value;
49
+ const sentences = value.split(/\.\s+/).map((sentence) => sentence.trim());
50
+ return sentences.join(".\n");
51
+ }
52
+
53
+ #run_post_effects(prompt_result: string): void {
54
+ set_value_cache(this.prompt_cache, "commit_body", prompt_result);
55
+ this.commit_state.body = this.#split_by_period(prompt_result);
56
+ }
57
+ }
@@ -0,0 +1,119 @@
1
+ import * as p from "@clack/prompts";
2
+ import { StdioOptions, execSync } from "child_process";
3
+ import { flags } from "../args";
4
+ import { Runnable } from "./runnable";
5
+ import { dry_run_message } from "../utils/messages";
6
+ import { build_commit_string } from "../utils/build-commit-string";
7
+
8
+ export class CommitConfirmPrompt extends Runnable {
9
+ async run(): Promise<void> {
10
+ if (this.#confirm_with_editor) {
11
+ execSync(`${this.#commit_command} --edit`, this.#git_command_options);
12
+ process.exit(0);
13
+ }
14
+
15
+ if (this.#print_commit_output) {
16
+ p.note(
17
+ build_commit_string({
18
+ commit_state: this.commit_state,
19
+ config: this.config,
20
+ colorize: true,
21
+ escape_quotes: false,
22
+ include_trailer: true,
23
+ }),
24
+ "Commit Preview",
25
+ );
26
+ }
27
+
28
+ const continue_commit = await this.#get_continue_commit();
29
+ if (!continue_commit) {
30
+ p.log.info("Exiting without commit");
31
+ process.exit(0);
32
+ }
33
+
34
+ try {
35
+ p.log.info(
36
+ flags.dry_run
37
+ ? dry_run_message("Committing changes...")
38
+ : "Committing changes...",
39
+ );
40
+ execSync(
41
+ this.#commit_command,
42
+ flags.dry_run
43
+ ? this.#git_command_options_quiet
44
+ : this.#git_command_options,
45
+ );
46
+ } catch (err) {
47
+ p.log.error("Something went wrong when committing: " + err);
48
+ return;
49
+ }
50
+
51
+ this.#run_post_effects();
52
+ }
53
+
54
+ get #confirm_with_editor(): boolean {
55
+ return flags.interactive && this.config.confirm_with_editor;
56
+ }
57
+
58
+ get #print_commit_output(): boolean {
59
+ return this.config.print_commit_output;
60
+ }
61
+
62
+ get #confirm_commit(): boolean {
63
+ return this.config.confirm_commit;
64
+ }
65
+
66
+ get #git_command_options(): { stdio: StdioOptions; shell?: string } {
67
+ return this.config.overrides.shell
68
+ ? { shell: this.config.overrides.shell, stdio: "inherit" as StdioOptions }
69
+ : { stdio: "inherit" as StdioOptions };
70
+ }
71
+
72
+ get #git_command_options_quiet(): { stdio: StdioOptions; shell?: string } {
73
+ return this.config.overrides.shell
74
+ ? { shell: this.config.overrides.shell, stdio: "pipe" as StdioOptions }
75
+ : { stdio: "pipe" as StdioOptions };
76
+ }
77
+
78
+ get #trailer_arg(): string {
79
+ return this.commit_state.trailer
80
+ ? `--trailer="${this.commit_state.trailer}"`
81
+ : "";
82
+ }
83
+
84
+ get #commit_command(): string {
85
+ return `git ${flags.git_args} commit -m "${build_commit_string({
86
+ commit_state: this.commit_state,
87
+ config: this.config,
88
+ colorize: false,
89
+ escape_quotes: true,
90
+ include_trailer: false,
91
+ })}" ${this.#trailer_arg} ${this.#dry_run_args}`.trim();
92
+ }
93
+
94
+ get #dry_run_args(): string {
95
+ return flags.dry_run ? "--dry-run --porcelain --untracked-files=no" : "";
96
+ }
97
+
98
+ async #get_continue_commit(): Promise<boolean> {
99
+ if (!flags.interactive) return true;
100
+ if (!this.#confirm_commit) return true;
101
+
102
+ // dry_run_message
103
+ const continue_commit = (await p.confirm({
104
+ message: flags.dry_run
105
+ ? dry_run_message("Confirm Commit?")
106
+ : "Confirm Commit?",
107
+ })) as boolean;
108
+ if (p.isCancel(continue_commit)) process.exit(0);
109
+ return continue_commit;
110
+ }
111
+
112
+ #run_post_effects(): void {
113
+ p.log.success("Commit Complete");
114
+
115
+ const user_name = this.prompt_cache.get("username");
116
+ this.prompt_cache.clear();
117
+ if (user_name) this.prompt_cache.set("username", user_name);
118
+ }
119
+ }