better-commits 1.19.1 → 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 -495
- 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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { InferOutput } from "valibot";
|
|
3
|
+
import { V_FOOTER_OPTIONS } from "../valibot-consts";
|
|
4
|
+
import {
|
|
5
|
+
COMMIT_FOOTER_OPTIONS,
|
|
6
|
+
get_value_from_cache,
|
|
7
|
+
set_value_cache,
|
|
8
|
+
} from "../utils";
|
|
9
|
+
import {
|
|
10
|
+
cache_message,
|
|
11
|
+
optional_message,
|
|
12
|
+
space_to_select_message,
|
|
13
|
+
} from "../utils/messages";
|
|
14
|
+
import { Runnable } from "./runnable";
|
|
15
|
+
|
|
16
|
+
type FooterOption = InferOutput<typeof V_FOOTER_OPTIONS>;
|
|
17
|
+
type FooterPromptOption = {
|
|
18
|
+
value: FooterOption;
|
|
19
|
+
label: string;
|
|
20
|
+
hint: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type FooterSelection = {
|
|
24
|
+
includes_breaking_change: boolean;
|
|
25
|
+
includes_deprecated: boolean;
|
|
26
|
+
includes_closes: boolean;
|
|
27
|
+
includes_custom: boolean;
|
|
28
|
+
includes_trailer: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type FooterInputs = {
|
|
32
|
+
breaking_title: string;
|
|
33
|
+
breaking_body: string;
|
|
34
|
+
deprecated_title: string;
|
|
35
|
+
deprecated_body: string;
|
|
36
|
+
custom_footer: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export class CommitFooterPrompt extends Runnable {
|
|
40
|
+
async run(): Promise<void> {
|
|
41
|
+
if (!this.#is_enabled) return;
|
|
42
|
+
|
|
43
|
+
const { initial_values, message } = this.#get_initial_value();
|
|
44
|
+
const commit_footer = (await p.multiselect({
|
|
45
|
+
message,
|
|
46
|
+
initialValues: initial_values,
|
|
47
|
+
options: this.#options,
|
|
48
|
+
required: false,
|
|
49
|
+
})) as FooterOption[];
|
|
50
|
+
if (p.isCancel(commit_footer)) process.exit(0);
|
|
51
|
+
|
|
52
|
+
const selection = this.#get_selection(commit_footer);
|
|
53
|
+
const footer_inputs = await this.#get_footer_inputs(selection);
|
|
54
|
+
this.#run_post_effects(commit_footer, selection, footer_inputs);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get #is_enabled(): boolean {
|
|
58
|
+
return this.config.commit_footer.enable;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get #options(): FooterPromptOption[] {
|
|
62
|
+
const allowed_values = new Set(this.config.commit_footer.options);
|
|
63
|
+
return COMMIT_FOOTER_OPTIONS.filter((option) =>
|
|
64
|
+
allowed_values.has(option.value as FooterOption),
|
|
65
|
+
) as FooterPromptOption[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get #available_option_values(): FooterOption[] {
|
|
69
|
+
return this.#options.map((option) => option.value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#get_initial_value(): { initial_values: FooterOption[]; message: string } {
|
|
73
|
+
const cache_value = get_value_from_cache(
|
|
74
|
+
this.prompt_cache,
|
|
75
|
+
"commit_footer",
|
|
76
|
+
);
|
|
77
|
+
if (cache_value) {
|
|
78
|
+
return {
|
|
79
|
+
initial_values: this.#parse_cache_value(cache_value),
|
|
80
|
+
message: space_to_select_message(cache_message("Commit footers")),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const initial_values = this.config.commit_footer.initial_value.filter(
|
|
85
|
+
(value) => this.#available_option_values.includes(value),
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
initial_values,
|
|
89
|
+
message: space_to_select_message(
|
|
90
|
+
optional_message("Select optional footers"),
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#parse_cache_value(cache_value: string): FooterOption[] {
|
|
96
|
+
return cache_value
|
|
97
|
+
.split(",")
|
|
98
|
+
.map((value) => value.trim())
|
|
99
|
+
.filter((value): value is FooterOption =>
|
|
100
|
+
this.#available_option_values.includes(value as FooterOption),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#get_selection(commit_footer: FooterOption[]): FooterSelection {
|
|
105
|
+
return {
|
|
106
|
+
includes_breaking_change: commit_footer.includes("breaking-change"),
|
|
107
|
+
includes_deprecated: commit_footer.includes("deprecated"),
|
|
108
|
+
includes_closes: commit_footer.includes("closes"),
|
|
109
|
+
includes_custom: commit_footer.includes("custom"),
|
|
110
|
+
includes_trailer: commit_footer.includes("trailer"),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async #get_footer_inputs(selection: FooterSelection): Promise<FooterInputs> {
|
|
115
|
+
const footer_inputs: FooterInputs = {
|
|
116
|
+
breaking_title: "",
|
|
117
|
+
breaking_body: "",
|
|
118
|
+
deprecated_title: "",
|
|
119
|
+
deprecated_body: "",
|
|
120
|
+
custom_footer: "",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (selection.includes_breaking_change) {
|
|
124
|
+
footer_inputs.breaking_title = await this.#required_text(
|
|
125
|
+
"Breaking changes: Write a short title / summary",
|
|
126
|
+
);
|
|
127
|
+
footer_inputs.breaking_body = await this.#optional_text(
|
|
128
|
+
optional_message(
|
|
129
|
+
"Breaking Changes: Write a description & migration instructions",
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (selection.includes_deprecated) {
|
|
135
|
+
footer_inputs.deprecated_title = await this.#required_text(
|
|
136
|
+
"Deprecated: Write a short title / summary",
|
|
137
|
+
);
|
|
138
|
+
footer_inputs.deprecated_body = await this.#optional_text(
|
|
139
|
+
optional_message("Deprecated: Write a description"),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (selection.includes_custom) {
|
|
144
|
+
footer_inputs.custom_footer = await this.#optional_text(
|
|
145
|
+
"Write a custom footer",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return footer_inputs;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async #required_text(message: string): Promise<string> {
|
|
153
|
+
const response = await p.text({
|
|
154
|
+
message,
|
|
155
|
+
placeholder: "",
|
|
156
|
+
validate: (value) => {
|
|
157
|
+
if (!value) return "Please enter a title / summary";
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
if (p.isCancel(response)) process.exit(0);
|
|
161
|
+
return response ?? "";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async #optional_text(message: string): Promise<string> {
|
|
165
|
+
const response = await p.text({
|
|
166
|
+
message,
|
|
167
|
+
placeholder: "",
|
|
168
|
+
});
|
|
169
|
+
if (p.isCancel(response)) process.exit(0);
|
|
170
|
+
return response ?? "";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#run_post_effects(
|
|
174
|
+
commit_footer: FooterOption[],
|
|
175
|
+
selection: FooterSelection,
|
|
176
|
+
footer_inputs: FooterInputs,
|
|
177
|
+
): void {
|
|
178
|
+
set_value_cache(
|
|
179
|
+
this.prompt_cache,
|
|
180
|
+
"commit_footer",
|
|
181
|
+
commit_footer.join(","),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
this.commit_state.breaking_title = footer_inputs.breaking_title;
|
|
185
|
+
this.commit_state.breaking_body = footer_inputs.breaking_body;
|
|
186
|
+
this.commit_state.deprecates_title = footer_inputs.deprecated_title;
|
|
187
|
+
this.commit_state.deprecates_body = footer_inputs.deprecated_body;
|
|
188
|
+
this.commit_state.custom_footer = footer_inputs.custom_footer;
|
|
189
|
+
|
|
190
|
+
this.commit_state.closes = selection.includes_closes ? "Closes:" : "";
|
|
191
|
+
if (!selection.includes_trailer) {
|
|
192
|
+
this.commit_state.trailer = "";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { CUSTOM_SCOPE_KEY } from "../valibot-consts";
|
|
3
|
+
import { get_value_from_cache, set_value_cache } from "../utils";
|
|
4
|
+
import { cache_message } from "../utils/messages";
|
|
5
|
+
import { Runnable } from "./runnable";
|
|
6
|
+
|
|
7
|
+
export class CommitScopePrompt extends Runnable {
|
|
8
|
+
async run(): Promise<void> {
|
|
9
|
+
if (!this.#is_enabled) return;
|
|
10
|
+
const { initial_value, message } = this.#get_initial_value();
|
|
11
|
+
|
|
12
|
+
let commit_scope = await p.select({
|
|
13
|
+
message,
|
|
14
|
+
initialValue: initial_value,
|
|
15
|
+
maxItems: this.#max_items,
|
|
16
|
+
options: this.#options,
|
|
17
|
+
});
|
|
18
|
+
if (p.isCancel(commit_scope)) process.exit(0);
|
|
19
|
+
|
|
20
|
+
await this.#post_run_effects(commit_scope);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get #is_enabled(): boolean {
|
|
24
|
+
return this.config.commit_scope.enable;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#get_initial_value(): { initial_value: string; message: string } {
|
|
28
|
+
const cache_value = get_value_from_cache(this.prompt_cache, "commit_scope");
|
|
29
|
+
if (cache_value) {
|
|
30
|
+
return {
|
|
31
|
+
initial_value: cache_value,
|
|
32
|
+
message: cache_message("Commit scope"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
initial_value: this.config.commit_scope.initial_value,
|
|
38
|
+
message: "Select a commit scope",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get #max_items(): number | undefined {
|
|
43
|
+
return this.config.commit_scope.max_items;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get #options(): {
|
|
47
|
+
label?: string | undefined;
|
|
48
|
+
value: string;
|
|
49
|
+
hint?: string;
|
|
50
|
+
}[] {
|
|
51
|
+
return this.config.commit_scope.options;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get #custom_scope_enabled(): boolean {
|
|
55
|
+
return this.config.commit_scope.custom_scope;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async #post_run_effects(prompt_result: string): Promise<void> {
|
|
59
|
+
set_value_cache(this.prompt_cache, "commit_scope", prompt_result);
|
|
60
|
+
|
|
61
|
+
let commit_scope_value = prompt_result;
|
|
62
|
+
if (commit_scope_value === CUSTOM_SCOPE_KEY && this.#custom_scope_enabled) {
|
|
63
|
+
const commit_scope = await p.text({
|
|
64
|
+
message: "Write a custom scope",
|
|
65
|
+
placeholder: "",
|
|
66
|
+
});
|
|
67
|
+
if (p.isCancel(commit_scope)) process.exit(0);
|
|
68
|
+
commit_scope_value = commit_scope ?? "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.commit_state.scope = commit_scope_value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import color from "picocolors";
|
|
3
|
+
import { addNewLine } from "../utils";
|
|
4
|
+
import { space_to_select_message } from "../utils/messages";
|
|
5
|
+
import { git_add, git_status } from "../git";
|
|
6
|
+
import { Runnable } from "./runnable";
|
|
7
|
+
|
|
8
|
+
export class CommitStatusPrompt extends Runnable {
|
|
9
|
+
async run(): Promise<void> {
|
|
10
|
+
if (!this.#is_enabled) return;
|
|
11
|
+
|
|
12
|
+
const status = git_status();
|
|
13
|
+
this.#log_status(status);
|
|
14
|
+
|
|
15
|
+
if (status.work_tree.length) {
|
|
16
|
+
const selected_for_staging = await this.#select_for_staging(
|
|
17
|
+
status.work_tree,
|
|
18
|
+
);
|
|
19
|
+
if (selected_for_staging.length) {
|
|
20
|
+
git_add(selected_for_staging);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.#ensure_staged_changes();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get #is_enabled(): boolean {
|
|
28
|
+
return this.config.check_status;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#log_status(status: { index: string[]; work_tree: string[] }): void {
|
|
32
|
+
p.log.step(color.black(color.bgGreen(" Checking Git Status ")));
|
|
33
|
+
|
|
34
|
+
const staged_files = this.#format_files(status.index, color.green);
|
|
35
|
+
p.log.success("Changes to be committed:\n" + staged_files);
|
|
36
|
+
|
|
37
|
+
if (!status.work_tree.length) return;
|
|
38
|
+
const unstaged_files = this.#format_files(status.work_tree, color.red);
|
|
39
|
+
p.log.error("Changes not staged for commit:\n" + unstaged_files);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#format_files(files: string[], colorize: (value: string) => string): string {
|
|
43
|
+
return files.reduce(
|
|
44
|
+
(acc, curr, index) => colorize(acc + curr + addNewLine(files, index)),
|
|
45
|
+
"",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async #select_for_staging(work_tree: string[]): Promise<string[]> {
|
|
50
|
+
const selected_for_staging = (await p.multiselect({
|
|
51
|
+
message: space_to_select_message(
|
|
52
|
+
"Some files have not been staged, would you like to add them now?",
|
|
53
|
+
),
|
|
54
|
+
options: [
|
|
55
|
+
{ value: ".", label: "." },
|
|
56
|
+
...work_tree.map((v) => ({ value: v, label: v })),
|
|
57
|
+
],
|
|
58
|
+
required: false,
|
|
59
|
+
})) as string[];
|
|
60
|
+
if (p.isCancel(selected_for_staging)) process.exit(0);
|
|
61
|
+
return selected_for_staging;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#ensure_staged_changes(): void {
|
|
65
|
+
const updated_status = git_status();
|
|
66
|
+
if (updated_status.index.length) return;
|
|
67
|
+
|
|
68
|
+
p.log.error(
|
|
69
|
+
color.red(
|
|
70
|
+
'no changes added to commit (use "git add" and/or "git commit -a")',
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { flags } from "../args";
|
|
3
|
+
import { get_value_from_cache, set_value_cache } from "../utils";
|
|
4
|
+
import { infer_ticket_from_git } from "../utils/infer";
|
|
5
|
+
import {
|
|
6
|
+
cache_message,
|
|
7
|
+
inferred_message,
|
|
8
|
+
optional_message,
|
|
9
|
+
} from "../utils/messages";
|
|
10
|
+
import { Runnable } from "./runnable";
|
|
11
|
+
|
|
12
|
+
export class CommitTicketPrompt extends Runnable {
|
|
13
|
+
async run(): Promise<void> {
|
|
14
|
+
const { initial_value, message } = this.#get_initial_value();
|
|
15
|
+
this.commit_state.ticket = initial_value;
|
|
16
|
+
|
|
17
|
+
if (this.#confirm_ticket_enabled) {
|
|
18
|
+
const user_commit_ticket = await p.text({
|
|
19
|
+
message,
|
|
20
|
+
placeholder: "",
|
|
21
|
+
initialValue: initial_value,
|
|
22
|
+
});
|
|
23
|
+
if (p.isCancel(user_commit_ticket)) process.exit(0);
|
|
24
|
+
set_value_cache(this.prompt_cache, "commit_ticket", user_commit_ticket);
|
|
25
|
+
this.commit_state.ticket = user_commit_ticket ?? "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
this.#prepend_hashtag_always &&
|
|
30
|
+
this.commit_state.ticket &&
|
|
31
|
+
!this.commit_state.ticket.startsWith("#")
|
|
32
|
+
) {
|
|
33
|
+
this.commit_state.ticket = "#" + this.commit_state.ticket;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get #infer_ticket_enabled(): boolean {
|
|
38
|
+
return this.config.check_ticket.infer_ticket;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get #confirm_ticket_enabled(): boolean {
|
|
42
|
+
return this.config.check_ticket.confirm_ticket;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get #prepend_hashtag_always(): boolean {
|
|
46
|
+
return this.config.check_ticket.prepend_hashtag === "Always";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#get_initial_value(): { initial_value: string; message: string } {
|
|
50
|
+
const cache_value = get_value_from_cache(
|
|
51
|
+
this.prompt_cache,
|
|
52
|
+
"commit_ticket",
|
|
53
|
+
);
|
|
54
|
+
if (cache_value) {
|
|
55
|
+
return {
|
|
56
|
+
initial_value: cache_value,
|
|
57
|
+
message: cache_message("Ticket / issue"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.#infer_ticket_enabled) {
|
|
62
|
+
const inferred_value = infer_ticket_from_git(
|
|
63
|
+
{
|
|
64
|
+
append_hashtag: this.config.check_ticket.append_hashtag,
|
|
65
|
+
prepend_hashtag: this.config.check_ticket.prepend_hashtag,
|
|
66
|
+
},
|
|
67
|
+
flags.git_args,
|
|
68
|
+
);
|
|
69
|
+
if (inferred_value) {
|
|
70
|
+
return {
|
|
71
|
+
initial_value: inferred_value,
|
|
72
|
+
message: inferred_message("Ticket / issue"),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
initial_value: this.commit_state.ticket,
|
|
79
|
+
message: optional_message("Add ticket / issue"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import {
|
|
3
|
+
clean_commit_title,
|
|
4
|
+
get_value_from_cache,
|
|
5
|
+
set_value_cache,
|
|
6
|
+
} from "../utils";
|
|
7
|
+
import { cache_message } from "../utils/messages";
|
|
8
|
+
import { get_commit_title_size } from "../utils/commit-title-size";
|
|
9
|
+
import { Runnable } from "./runnable";
|
|
10
|
+
|
|
11
|
+
export class CommitTitlePrompt extends Runnable {
|
|
12
|
+
async run(): Promise<void> {
|
|
13
|
+
const { initial_value, message } = this.#get_initial_value();
|
|
14
|
+
const commit_title = await p.text({
|
|
15
|
+
message,
|
|
16
|
+
initialValue: initial_value,
|
|
17
|
+
placeholder: "",
|
|
18
|
+
validate: (value) => this.#validate(value),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (p.isCancel(commit_title)) process.exit(0);
|
|
22
|
+
this.#run_post_effects(commit_title ?? "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#get_initial_value(): { initial_value: string; message: string } {
|
|
26
|
+
const cache_value = get_value_from_cache(this.prompt_cache, "commit_title");
|
|
27
|
+
if (cache_value) {
|
|
28
|
+
return {
|
|
29
|
+
initial_value: cache_value,
|
|
30
|
+
message: cache_message("Commit title"),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// this.commit_state.title will pull from flag if populated
|
|
35
|
+
return {
|
|
36
|
+
initial_value: this.commit_state.title,
|
|
37
|
+
message: "Write a brief title describing the commit",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#validate(value: string | undefined): string | undefined {
|
|
42
|
+
if (!value) return "Please enter a title";
|
|
43
|
+
|
|
44
|
+
if (this.#get_size(value) > this.#max_size) {
|
|
45
|
+
return `Exceeded max length. Title max [${this.#max_size}]`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get #max_size(): number {
|
|
50
|
+
return this.config.commit_title.max_size;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#get_size(value: string): number {
|
|
54
|
+
return get_commit_title_size(
|
|
55
|
+
{
|
|
56
|
+
type: this.commit_state.type,
|
|
57
|
+
scope: this.commit_state.scope,
|
|
58
|
+
ticket: this.commit_state.ticket,
|
|
59
|
+
title: value,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
include_ticket: this.config.check_ticket.add_to_title,
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// TODO: Extract to a Runnable abstract function?
|
|
68
|
+
get #value_to_data(): Record<string, { emoji: string }> {
|
|
69
|
+
return this.config.commit_type.options.reduce(
|
|
70
|
+
(acc, curr) => ({
|
|
71
|
+
...acc,
|
|
72
|
+
[curr.value]: {
|
|
73
|
+
emoji: curr.emoji ?? "",
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
{},
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#title_with_emoji(title: string): string {
|
|
81
|
+
if (
|
|
82
|
+
this.config.commit_type.append_emoji_to_commit &&
|
|
83
|
+
this.config.commit_type.emoji_commit_position === "After-Colon"
|
|
84
|
+
) {
|
|
85
|
+
const emoji = this.#value_to_data[this.commit_state.type]?.emoji ?? "";
|
|
86
|
+
return `${emoji} ${title}`.trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return title;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#run_post_effects(prompt_result: string): void {
|
|
93
|
+
set_value_cache(this.prompt_cache, "commit_title", prompt_result);
|
|
94
|
+
this.commit_state.title = clean_commit_title(
|
|
95
|
+
this.#title_with_emoji(prompt_result),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { flags } from "../args";
|
|
3
|
+
import { get_value_from_cache, set_value_cache } from "../utils";
|
|
4
|
+
import { infer_type_from_git } from "../utils/infer";
|
|
5
|
+
import { cache_message, inferred_message } from "../utils/messages";
|
|
6
|
+
import { Runnable } from "./runnable";
|
|
7
|
+
|
|
8
|
+
export class CommitTypePrompt extends Runnable {
|
|
9
|
+
async run() {
|
|
10
|
+
if (this.#is_enabled) {
|
|
11
|
+
const { initial_value, message } = this.#initial_value;
|
|
12
|
+
const commit_type = await p.select({
|
|
13
|
+
message,
|
|
14
|
+
initialValue: initial_value,
|
|
15
|
+
maxItems: this.#max_items,
|
|
16
|
+
options: this.#options,
|
|
17
|
+
});
|
|
18
|
+
if (p.isCancel(commit_type)) process.exit(0);
|
|
19
|
+
|
|
20
|
+
this.#run_post_effects(commit_type);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get #is_enabled(): boolean {
|
|
25
|
+
return this.config.commit_type.enable;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get #initial_value(): { initial_value: string | undefined; message: string } {
|
|
29
|
+
const cache_value = get_value_from_cache(this.prompt_cache, "commit_type");
|
|
30
|
+
if (cache_value)
|
|
31
|
+
return {
|
|
32
|
+
initial_value: cache_value,
|
|
33
|
+
message: cache_message("Commit type"),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (this.config.commit_type.infer_type_from_branch) {
|
|
37
|
+
const type_from_branch = infer_type_from_git(
|
|
38
|
+
this.#options,
|
|
39
|
+
flags.git_args,
|
|
40
|
+
);
|
|
41
|
+
if (type_from_branch) {
|
|
42
|
+
return {
|
|
43
|
+
message: inferred_message("Commit type"),
|
|
44
|
+
initial_value: type_from_branch,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
initial_value: this.config.commit_type.initial_value,
|
|
51
|
+
message: "Select a commit type",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get #options(): {
|
|
56
|
+
label: string | undefined;
|
|
57
|
+
value: string;
|
|
58
|
+
emoji?: string;
|
|
59
|
+
hint?: string;
|
|
60
|
+
trailer?: string;
|
|
61
|
+
}[] {
|
|
62
|
+
return this.config.commit_type.options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get #value_to_data(): Record<string, { emoji: string; trailer: string }> {
|
|
66
|
+
return this.#options.reduce(
|
|
67
|
+
(acc, curr) => ({
|
|
68
|
+
...acc,
|
|
69
|
+
[curr.value]: {
|
|
70
|
+
emoji: curr.emoji ?? "",
|
|
71
|
+
trailer: curr.trailer ?? "",
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
{},
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get #max_items(): number | undefined {
|
|
79
|
+
return this.config.commit_type.max_items;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#run_post_effects(prompt_result: string): void {
|
|
83
|
+
set_value_cache(this.prompt_cache, "commit_type", prompt_result);
|
|
84
|
+
|
|
85
|
+
const value_to_data = this.#value_to_data;
|
|
86
|
+
this.commit_state.trailer = value_to_data[prompt_result].trailer;
|
|
87
|
+
this.commit_state.type =
|
|
88
|
+
this.config.commit_type.append_emoji_to_commit &&
|
|
89
|
+
this.config.commit_type.emoji_commit_position === "Start"
|
|
90
|
+
? `${value_to_data[prompt_result].emoji} ${prompt_result}`.trim()
|
|
91
|
+
: prompt_result;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { InferOutput } from "valibot";
|
|
2
|
+
import { CommitState, Config } from "../valibot-state";
|
|
3
|
+
import Configstore from "configstore";
|
|
4
|
+
|
|
5
|
+
export abstract class Runnable {
|
|
6
|
+
constructor(
|
|
7
|
+
protected config: InferOutput<typeof Config>,
|
|
8
|
+
protected commit_state: InferOutput<typeof CommitState>,
|
|
9
|
+
protected prompt_cache: Configstore,
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
abstract run(): Promise<void>;
|
|
13
|
+
}
|