better-commits 1.23.2 → 1.23.3

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 (68) hide show
  1. package/dist/branch.js +628 -19
  2. package/dist/chunk-GAAS3VS3.js +922 -0
  3. package/dist/chunk-H5CLUQIL.js +313 -0
  4. package/dist/index.js +1122 -41
  5. package/dist/init.js +44 -1
  6. package/package.json +12 -4
  7. package/readme.md +4 -2
  8. package/.better-commits.json +0 -52
  9. package/.github/workflows/publish.yml +0 -34
  10. package/.github/workflows/test.yml +0 -27
  11. package/.prettierignore +0 -5
  12. package/.prettierrc +0 -1
  13. package/dist/chunk-43H72S6V.js +0 -1
  14. package/dist/chunk-B7AGSPP3.js +0 -261
  15. package/src/args.test.ts +0 -128
  16. package/src/args.ts +0 -125
  17. package/src/branch-args.test.ts +0 -75
  18. package/src/branch-args.ts +0 -107
  19. package/src/branch-help.ts +0 -125
  20. package/src/branch.ts +0 -97
  21. package/src/default-config-template.ts +0 -258
  22. package/src/git.test.ts +0 -64
  23. package/src/git.ts +0 -72
  24. package/src/help.ts +0 -138
  25. package/src/index.test.ts +0 -7
  26. package/src/index.ts +0 -101
  27. package/src/init.test.ts +0 -123
  28. package/src/init.ts +0 -46
  29. package/src/prompts/autocomplete-multiselect.test.ts +0 -129
  30. package/src/prompts/autocomplete-multiselect.ts +0 -249
  31. package/src/prompts/branch-checkout.prompt.ts +0 -36
  32. package/src/prompts/branch-confirm.prompt.test.ts +0 -89
  33. package/src/prompts/branch-confirm.prompt.ts +0 -149
  34. package/src/prompts/branch-description.prompt.ts +0 -37
  35. package/src/prompts/branch-runnable.ts +0 -13
  36. package/src/prompts/branch-scope.prompt.ts +0 -59
  37. package/src/prompts/branch-ticket.prompt.ts +0 -41
  38. package/src/prompts/branch-type.prompt.ts +0 -46
  39. package/src/prompts/branch-user.prompt.ts +0 -50
  40. package/src/prompts/branch-version.prompt.ts +0 -41
  41. package/src/prompts/commit-body.prompt.ts +0 -51
  42. package/src/prompts/commit-confirm.prompt.ts +0 -123
  43. package/src/prompts/commit-footer.prompt.ts +0 -195
  44. package/src/prompts/commit-scope.prompt.ts +0 -91
  45. package/src/prompts/commit-status.prompt.ts +0 -66
  46. package/src/prompts/commit-ticket.prompt.ts +0 -82
  47. package/src/prompts/commit-title.prompt.ts +0 -98
  48. package/src/prompts/commit-type.prompt.ts +0 -96
  49. package/src/prompts/runnable.ts +0 -13
  50. package/src/utils/build-branch.test.ts +0 -159
  51. package/src/utils/build-branch.ts +0 -48
  52. package/src/utils/build-commit-string.test.ts +0 -273
  53. package/src/utils/build-commit-string.ts +0 -163
  54. package/src/utils/commit-title-size.ts +0 -24
  55. package/src/utils/infer.test.ts +0 -174
  56. package/src/utils/infer.ts +0 -160
  57. package/src/utils/messages.ts +0 -25
  58. package/src/utils/no-interactive-branch-validation.test.ts +0 -193
  59. package/src/utils/no-interactive-validation.test.ts +0 -174
  60. package/src/utils/no-interactive-validation.ts +0 -213
  61. package/src/utils.test.ts +0 -164
  62. package/src/utils.ts +0 -235
  63. package/src/valibot-consts.ts +0 -117
  64. package/src/valibot-state.test.ts +0 -57
  65. package/src/valibot-state.ts +0 -276
  66. package/tsconfig.json +0 -15
  67. package/tsup.config.ts +0 -12
  68. package/vitest.config.ts +0 -8
package/src/init.test.ts DELETED
@@ -1,123 +0,0 @@
1
- import fs from "fs";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
-
4
- const mocked = vi.hoisted(() => ({
5
- writeFileSync: vi.fn<typeof fs.writeFileSync>(),
6
- intro: vi.fn(),
7
- outro: vi.fn(),
8
- confirm: vi.fn(),
9
- isCancel: vi.fn(() => false),
10
- success: vi.fn(),
11
- error: vi.fn(),
12
- get_git_root: vi.fn(() => "/repo"),
13
- get_repository_config_path: vi.fn<() => string | null>(),
14
- }));
15
-
16
- vi.mock("fs", () => ({
17
- default: {
18
- writeFileSync: mocked.writeFileSync,
19
- },
20
- }));
21
-
22
- vi.mock("@clack/prompts", () => ({
23
- intro: mocked.intro,
24
- outro: mocked.outro,
25
- confirm: mocked.confirm,
26
- isCancel: mocked.isCancel,
27
- log: {
28
- success: mocked.success,
29
- error: mocked.error,
30
- },
31
- }));
32
-
33
- vi.mock("./utils", async () => {
34
- const actual = await vi.importActual<typeof import("./utils")>("./utils");
35
- return {
36
- ...actual,
37
- get_git_root: mocked.get_git_root,
38
- get_repository_config_path: mocked.get_repository_config_path,
39
- };
40
- });
41
-
42
- describe("create_init_config", () => {
43
- beforeEach(() => {
44
- vi.resetModules();
45
- mocked.writeFileSync.mockReset();
46
- mocked.intro.mockReset();
47
- mocked.outro.mockReset();
48
- mocked.confirm.mockReset();
49
- mocked.isCancel.mockReset();
50
- mocked.isCancel.mockReturnValue(false);
51
- mocked.success.mockReset();
52
- mocked.error.mockReset();
53
- mocked.get_git_root.mockReset();
54
- mocked.get_git_root.mockReturnValue("/repo");
55
- mocked.get_repository_config_path.mockReset();
56
- });
57
-
58
- it("creates a new config when none exists", async () => {
59
- mocked.get_repository_config_path.mockReturnValue(null);
60
-
61
- const { create_init_config } = await import("./init");
62
- mocked.writeFileSync.mockClear();
63
- mocked.confirm.mockClear();
64
- mocked.outro.mockClear();
65
-
66
- await create_init_config();
67
-
68
- expect(mocked.confirm).not.toHaveBeenCalled();
69
- expect(mocked.writeFileSync).toHaveBeenCalledTimes(1);
70
- });
71
-
72
- it("asks before overwriting an existing config", async () => {
73
- mocked.get_repository_config_path.mockReturnValue("/repo/.better-commits.jsonc");
74
- mocked.confirm.mockResolvedValue(true);
75
-
76
- const { create_init_config } = await import("./init");
77
- mocked.writeFileSync.mockClear();
78
- mocked.confirm.mockClear();
79
- mocked.outro.mockClear();
80
-
81
- await create_init_config();
82
-
83
- expect(mocked.confirm).toHaveBeenCalledTimes(1);
84
- expect(mocked.writeFileSync).toHaveBeenCalledWith(
85
- "/repo/.better-commits.jsonc",
86
- expect.any(String),
87
- );
88
- });
89
-
90
- it("writes the canonical jsonc file when a legacy json config exists", async () => {
91
- mocked.get_repository_config_path.mockReturnValue("/repo/.better-commits.json");
92
- mocked.confirm.mockResolvedValue(true);
93
-
94
- const { create_init_config } = await import("./init");
95
- mocked.writeFileSync.mockClear();
96
- mocked.confirm.mockClear();
97
- mocked.outro.mockClear();
98
-
99
- await create_init_config();
100
-
101
- expect(mocked.confirm).toHaveBeenCalledTimes(1);
102
- expect(mocked.writeFileSync).toHaveBeenCalledWith(
103
- "/repo/.better-commits.jsonc",
104
- expect.any(String),
105
- );
106
- });
107
-
108
- it("does not overwrite when confirmation is declined", async () => {
109
- mocked.get_repository_config_path.mockReturnValue("/repo/.better-commits.jsonc");
110
- mocked.confirm.mockResolvedValue(false);
111
-
112
- const { create_init_config } = await import("./init");
113
- mocked.writeFileSync.mockClear();
114
- mocked.confirm.mockClear();
115
- mocked.outro.mockClear();
116
-
117
- await create_init_config();
118
-
119
- expect(mocked.confirm).toHaveBeenCalledTimes(1);
120
- expect(mocked.writeFileSync).not.toHaveBeenCalled();
121
- expect(mocked.outro).toHaveBeenCalledWith("Cancelled");
122
- });
123
- });
package/src/init.ts DELETED
@@ -1,46 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- import * as p from "@clack/prompts";
4
- import fs from "fs";
5
- import color from "picocolors";
6
- import { DEFAULT_CONFIG_TEMPLATE } from "./default-config-template";
7
- import {
8
- CONFIG_FILE_NAME,
9
- get_git_root,
10
- get_repository_config_path,
11
- } from "./utils";
12
-
13
- await create_init_config();
14
-
15
- export async function create_init_config() {
16
- console.clear();
17
- p.intro(`${color.bgCyan(color.black(" better-commits-init "))}`);
18
- const root = get_git_root();
19
- const existing_config_path = get_repository_config_path(root);
20
- const root_path = `${root}/${CONFIG_FILE_NAME}`;
21
-
22
- if (existing_config_path) {
23
- const should_overwrite = (await p.confirm({
24
- message: `${existing_config_path.split("/").pop()} already exists. Replace with default ${CONFIG_FILE_NAME}?`,
25
- })) as boolean;
26
-
27
- if (p.isCancel(should_overwrite) || !should_overwrite) {
28
- p.outro("Cancelled");
29
- return;
30
- }
31
- }
32
-
33
- try {
34
- fs.writeFileSync(root_path, DEFAULT_CONFIG_TEMPLATE);
35
- } catch {
36
- p.log.error(
37
- `${color.red("Could not determine git root folder. better-commits-init must be used in a git repository")}`,
38
- );
39
- }
40
- p.log.success(
41
- `${color.green(`Successfully created ${root_path.split("/").pop()}`)}`,
42
- );
43
- p.outro(
44
- `Run ${color.bgBlack(color.white("better-commits"))} to start the CLI`,
45
- );
46
- }
@@ -1,129 +0,0 @@
1
- import type { Key } from "node:readline";
2
- import { PassThrough } from "node:stream";
3
- import { Prompt } from "@clack/core";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import { autocompleteMultiselect } from "./autocomplete-multiselect";
6
-
7
- type InternalPrompt<Value> = {
8
- _isActionKey: (char: string | undefined, key: Key) => boolean;
9
- _setUserInput: (value: string | undefined, write?: boolean) => void;
10
- onKeypress: (char: string | undefined, key: Key) => void;
11
- selectedValues: Value[];
12
- focusedValue: Value | undefined;
13
- userInput: string;
14
- state: string;
15
- rl?: FakeReadLine;
16
- };
17
-
18
- type FakeReadLine = {
19
- line: string;
20
- cursor: number;
21
- write: ReturnType<typeof vi.fn>;
22
- };
23
-
24
- function createOutput(): PassThrough {
25
- const output = new PassThrough();
26
- Object.assign(output, { columns: 80, rows: 24, isTTY: true });
27
- return output;
28
- }
29
-
30
- function createReadLine(line: string): FakeReadLine {
31
- const rl: FakeReadLine = {
32
- line,
33
- cursor: line.length,
34
- write: vi.fn((_: string | null, key?: Key) => {
35
- if (key?.ctrl && key.name === "h") {
36
- rl.line = rl.line.slice(0, -1);
37
- rl.cursor = Math.max(0, rl.cursor - 1);
38
- }
39
-
40
- if (key?.ctrl && key.name === "e") {
41
- rl.cursor = rl.line.length;
42
- }
43
- }),
44
- };
45
-
46
- return rl;
47
- }
48
-
49
- async function createPrompt(): Promise<InternalPrompt<string>> {
50
- vi.spyOn(Prompt.prototype, "prompt").mockImplementation(function () {
51
- return Promise.resolve(this as never);
52
- });
53
-
54
- return (await autocompleteMultiselect({
55
- message: "Pick files",
56
- options: [
57
- { value: "src/foo.ts", label: "src/foo.ts" },
58
- { value: "test/foo.ts", label: "test/foo.ts" },
59
- ],
60
- output: createOutput(),
61
- withGuide: false,
62
- })) as unknown as InternalPrompt<string>;
63
- }
64
-
65
- afterEach(() => {
66
- vi.restoreAllMocks();
67
- });
68
-
69
- describe("autocompleteMultiselect", () => {
70
- it("uses space to toggle the focused option while searching", async () => {
71
- const prompt = await createPrompt();
72
- prompt.state = "active";
73
- prompt._setUserInput("src");
74
- prompt.rl = createReadLine("src ");
75
-
76
- expect(prompt._isActionKey(" ", { name: "space", sequence: " " } as Key)).toBe(true);
77
-
78
- prompt.onKeypress(" ", { name: "space", sequence: " " } as Key);
79
-
80
- expect(prompt.selectedValues).toEqual(["src/foo.ts"]);
81
- expect(prompt.userInput).toBe("src");
82
- expect(prompt.rl.line).toBe("src");
83
- expect(prompt.rl.write).toHaveBeenNthCalledWith(1, null, {
84
- ctrl: true,
85
- name: "h",
86
- });
87
- expect(prompt.rl.write).toHaveBeenNthCalledWith(2, "", {
88
- ctrl: true,
89
- name: "e",
90
- });
91
- });
92
-
93
- it("keeps tab selection behavior unchanged", async () => {
94
- const prompt = await createPrompt();
95
- prompt.state = "active";
96
- prompt._setUserInput("src");
97
- prompt.rl = createReadLine("src\t");
98
-
99
- prompt.onKeypress("\t", { name: "tab", sequence: "\t" } as Key);
100
-
101
- expect(prompt.selectedValues).toEqual(["src/foo.ts"]);
102
- expect(prompt.userInput).toBe("src");
103
- expect(prompt.rl.line).toBe("src");
104
- expect(prompt.rl.write).toHaveBeenCalledTimes(1);
105
- expect(prompt.rl.write).toHaveBeenCalledWith(null, {
106
- ctrl: true,
107
- name: "h",
108
- });
109
- });
110
-
111
- it("does not add whitespace to the filter when nothing matches", async () => {
112
- const prompt = await createPrompt();
113
- prompt.state = "active";
114
- prompt._setUserInput("missing");
115
- prompt.rl = createReadLine("missing ");
116
-
117
- prompt.onKeypress(" ", { name: "space", sequence: " " } as Key);
118
-
119
- expect(prompt.focusedValue).toBeUndefined();
120
- expect(prompt.selectedValues).toEqual([]);
121
- expect(prompt.userInput).toBe("missing");
122
- expect(prompt.rl.line).toBe("missing");
123
- expect(prompt.rl.write).toHaveBeenCalledTimes(1);
124
- expect(prompt.rl.write).toHaveBeenCalledWith(null, {
125
- ctrl: true,
126
- name: "h",
127
- });
128
- });
129
- });
@@ -1,249 +0,0 @@
1
- import type { Key, ReadLine } from "node:readline";
2
- import type { Readable, Writable } from "node:stream";
3
- import { styleText } from "node:util";
4
- import { AutocompletePrompt } from "@clack/core";
5
- import {
6
- S_BAR,
7
- S_BAR_END,
8
- S_CHECKBOX_INACTIVE,
9
- S_CHECKBOX_SELECTED,
10
- limitOptions,
11
- settings,
12
- symbol,
13
- } from "@clack/prompts";
14
-
15
- type Option<Value> = {
16
- value: Value;
17
- label?: string;
18
- hint?: string;
19
- disabled?: boolean;
20
- };
21
-
22
- type Filter<Value> = (search: string, option: Option<Value>) => boolean;
23
-
24
- type AutocompleteMultiselectOptions<Value> = {
25
- message: string;
26
- options:
27
- | Option<Value>[]
28
- | ((this: AutocompletePrompt<Option<Value>>) => Option<Value>[]);
29
- maxItems?: number;
30
- validate?: (value: Value[] | undefined) => string | Error | undefined;
31
- filter?: Filter<Value>;
32
- initialValues?: Value[];
33
- required?: boolean;
34
- input?: Readable;
35
- output?: Writable;
36
- signal?: AbortSignal;
37
- withGuide?: boolean;
38
- };
39
-
40
- function getFilteredOption<Value>(
41
- searchText: string,
42
- option: Option<Value>,
43
- ): boolean {
44
- if (!searchText) return true;
45
-
46
- const label = (option.label ?? String(option.value ?? "")).toLowerCase();
47
- const hint = (option.hint ?? "").toLowerCase();
48
- const value = String(option.value).toLowerCase();
49
- const term = searchText.toLowerCase();
50
-
51
- return label.includes(term) || hint.includes(term) || value.includes(term);
52
- }
53
-
54
- function is_ctrl_a(char: string | undefined, key: Key): boolean {
55
- return char === "\u0001" || (key.ctrl === true && key.name === "a");
56
- }
57
-
58
- /**
59
- * Generated by 🤖. Mostly copying and extending from Clack AutocompletePrompt.
60
- *
61
- * This is to support ctrl+a to select all visible
62
- */
63
- class AutocompleteMultiselectPrompt<Value> extends AutocompletePrompt<
64
- Option<Value>
65
- > {
66
- protected override _isActionKey(char: string | undefined, key: Key): boolean {
67
- return (
68
- super._isActionKey(char, key) ||
69
- (this.multiple && key.name === "space" && char !== undefined && char !== "")
70
- );
71
- }
72
-
73
- constructor(
74
- private readonly promptOptions: AutocompleteMultiselectOptions<Value>,
75
- ) {
76
- super({
77
- options: promptOptions.options,
78
- multiple: true,
79
- filter:
80
- promptOptions.filter ??
81
- ((search, opt) => getFilteredOption(search, opt)),
82
- validate: (value) => {
83
- if (
84
- promptOptions.required &&
85
- (!Array.isArray(value) || value.length === 0)
86
- ) {
87
- return "Please select at least one item";
88
- }
89
-
90
- return promptOptions.validate?.(value as Value[] | undefined);
91
- },
92
- initialValue: promptOptions.initialValues,
93
- signal: promptOptions.signal,
94
- input: promptOptions.input,
95
- output: promptOptions.output,
96
- render() {
97
- const hasGuide = promptOptions.withGuide ?? settings.withGuide;
98
- const title = `${hasGuide ? `${styleText("gray", S_BAR)}\n` : ""}${symbol(this.state)} ${promptOptions.message}\n`;
99
- const userInput = this.userInput;
100
- const searchText = this.userInputWithCursor;
101
-
102
- const options = this.options;
103
- const matches =
104
- this.filteredOptions.length !== options.length
105
- ? styleText(
106
- "dim",
107
- ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? "" : "es"})`,
108
- )
109
- : "";
110
-
111
- switch (this.state) {
112
- case "submit": {
113
- return `${title}${hasGuide ? `${styleText("gray", S_BAR)} ` : ""}${styleText("dim", `${this.selectedValues.length} items selected`)}`;
114
- }
115
-
116
- case "cancel": {
117
- return `${title}${hasGuide ? `${styleText("gray", S_BAR)} ` : ""}${styleText(["strikethrough", "dim"], userInput)}`;
118
- }
119
-
120
- default: {
121
- const barStyle = this.state === "error" ? "yellow" : "cyan";
122
- const guidePrefix = hasGuide
123
- ? `${styleText(barStyle, S_BAR)} `
124
- : "";
125
- const guidePrefixEnd = hasGuide
126
- ? styleText(barStyle, S_BAR_END)
127
- : "";
128
- const instructions = [
129
- `${styleText("dim", "↑/↓")} to navigate`,
130
- `${styleText("dim", "Space/Tab:")} select`,
131
- `${styleText("dim", "Ctrl+a:")} select visible`,
132
- `${styleText("dim", "Enter:")} confirm`,
133
- `${styleText("dim", "Type:")} to search`,
134
- ];
135
- const noResults =
136
- this.filteredOptions.length === 0 && userInput
137
- ? [`${guidePrefix}${styleText("yellow", "No matches found")}`]
138
- : [];
139
- const errorMessage =
140
- this.state === "error"
141
- ? [`${guidePrefix}${styleText("yellow", this.error)}`]
142
- : [];
143
- const headerLines = [
144
- ...`${title}${hasGuide ? styleText(barStyle, S_BAR) : ""}`.split(
145
- "\n",
146
- ),
147
- `${guidePrefix}${styleText("dim", "Search:")} ${searchText}${matches}`,
148
- ...noResults,
149
- ...errorMessage,
150
- ];
151
- const footerLines = [
152
- `${guidePrefix}${instructions.join(" • ")}`,
153
- guidePrefixEnd,
154
- ];
155
- const displayOptions = limitOptions({
156
- cursor: this.cursor,
157
- options: this.filteredOptions,
158
- style: (option, active) => {
159
- const isSelected = this.selectedValues.includes(option.value);
160
- const label = option.label ?? String(option.value ?? "");
161
- const hint =
162
- option.hint &&
163
- this.focusedValue !== undefined &&
164
- option.value === this.focusedValue
165
- ? styleText("dim", ` (${option.hint})`)
166
- : "";
167
- const checkbox = isSelected
168
- ? styleText("green", S_CHECKBOX_SELECTED)
169
- : styleText("dim", S_CHECKBOX_INACTIVE);
170
-
171
- if (option.disabled) {
172
- return `${styleText("gray", S_CHECKBOX_INACTIVE)} ${styleText(["strikethrough", "gray"], label)}`;
173
- }
174
-
175
- if (active) {
176
- return `${checkbox} ${label}${hint}`;
177
- }
178
-
179
- return `${checkbox} ${styleText("dim", label)}`;
180
- },
181
- maxItems: promptOptions.maxItems,
182
- output: promptOptions.output,
183
- rowPadding: headerLines.length + footerLines.length,
184
- });
185
-
186
- return [
187
- ...headerLines,
188
- ...displayOptions.map((option) => `${guidePrefix}${option}`),
189
- ...footerLines,
190
- ].join("\n");
191
- }
192
- }
193
- },
194
- });
195
-
196
- this.on("key", (char, key) => {
197
- if (
198
- key.name === "space" &&
199
- !this.isNavigating &&
200
- this.focusedValue !== undefined
201
- ) {
202
- this.toggleSelected(this.focusedValue);
203
- this.#restore_cursor_to_end();
204
- return;
205
- }
206
-
207
- if (!is_ctrl_a(char, key)) return;
208
-
209
- this.#toggle_all_visible();
210
- this.isNavigating = true;
211
- this.#restore_cursor_to_end();
212
- });
213
- }
214
-
215
- #toggle_all_visible(): void {
216
- const visible_values = this.filteredOptions
217
- .filter((option) => !option.disabled)
218
- .map((option) => option.value);
219
-
220
- if (!visible_values.length) return;
221
-
222
- const every_visible_selected = visible_values.every((value) =>
223
- this.selectedValues.includes(value),
224
- );
225
-
226
- this.selectedValues = every_visible_selected
227
- ? this.selectedValues.filter((value) => !visible_values.includes(value))
228
- : [
229
- ...this.selectedValues,
230
- ...visible_values.filter(
231
- (value) => !this.selectedValues.includes(value),
232
- ),
233
- ];
234
- }
235
-
236
- #restore_cursor_to_end(): void {
237
- const rl = (this as unknown as { rl?: ReadLine }).rl;
238
- rl?.write("", { ctrl: true, name: "e" });
239
- this._cursor = rl?.cursor ?? this.userInput.length;
240
- }
241
- }
242
-
243
- export function autocompleteMultiselect<Value>(
244
- opts: AutocompleteMultiselectOptions<Value>,
245
- ): Promise<Value[] | symbol> {
246
- return new AutocompleteMultiselectPrompt(opts).prompt() as Promise<
247
- Value[] | symbol
248
- >;
249
- }
@@ -1,36 +0,0 @@
1
- import { InferOutput } from "valibot";
2
- import { BranchRunnable } from "./branch-runnable";
3
- import { V_BRANCH_ACTIONS } from "../valibot-consts";
4
- import * as p from "@clack/prompts";
5
- import { BRANCH_ACTION_OPTIONS } from "../utils";
6
-
7
- export class BranchCheckoutPrompt extends BranchRunnable {
8
- async run(): Promise<void> {
9
- if (this.#is_enabled) {
10
- const branch_or_worktree = await p.select({
11
- message: this.#message,
12
- initialValue: this.#initival_values,
13
- options: BRANCH_ACTION_OPTIONS,
14
- });
15
-
16
- if (p.isCancel(branch_or_worktree)) process.exit();
17
- this.#post_run_effects(branch_or_worktree);
18
- }
19
- }
20
-
21
- get #message() {
22
- return `Checkout a branch or create a worktree?`;
23
- }
24
-
25
- get #is_enabled() {
26
- return this.config.worktrees.enable;
27
- }
28
-
29
- get #initival_values() {
30
- return this.branch_state.checkout || this.config.branch_action_default;
31
- }
32
-
33
- #post_run_effects(value: InferOutput<typeof V_BRANCH_ACTIONS>) {
34
- this.branch_state.checkout = value;
35
- }
36
- }
@@ -1,89 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { parse } from "valibot";
3
- import { BranchState, Config } from "../valibot-state";
4
-
5
- const mocked = vi.hoisted(() => ({
6
- dry_run: true,
7
- execSync: vi.fn(),
8
- info: vi.fn(),
9
- }));
10
-
11
- vi.mock("child_process", () => ({
12
- execSync: mocked.execSync,
13
- }));
14
-
15
- vi.mock("@clack/prompts", () => ({
16
- log: {
17
- info: mocked.info,
18
- warning: vi.fn(),
19
- error: vi.fn(),
20
- },
21
- }));
22
-
23
- vi.mock("picocolors", () => ({
24
- default: {
25
- bgGreen: (value: string) => value,
26
- bgMagenta: (value: string) => value,
27
- black: (value: string) => value,
28
- yellow: (value: string) => value,
29
- },
30
- }));
31
-
32
- vi.mock("../branch-args", () => ({
33
- branch_flags: {
34
- git_args: "",
35
- get dry_run() {
36
- return mocked.dry_run;
37
- },
38
- },
39
- }));
40
-
41
- vi.mock("../utils", async () => {
42
- const actual = await vi.importActual<typeof import("../utils")>("../utils");
43
- return {
44
- ...actual,
45
- get_git_root: vi.fn(() => "/tmp/repo"),
46
- };
47
- });
48
-
49
- describe("BranchConfirmPrompt", () => {
50
- beforeEach(() => {
51
- mocked.dry_run = true;
52
- mocked.execSync.mockReset();
53
- mocked.info.mockReset();
54
- });
55
-
56
- it("does not create a worktree during dry run", async () => {
57
- const { BranchConfirmPrompt } = await import("./branch-confirm.prompt");
58
- const config = parse(Config, {
59
- worktrees: {
60
- base_path: "../worktrees",
61
- },
62
- });
63
- const branch_state = parse(BranchState, {
64
- type: "feat",
65
- scope: "cli",
66
- ticket: "TAC-123",
67
- description: "add-parser",
68
- checkout: "worktree",
69
- });
70
-
71
- mocked.execSync.mockImplementation((command: string) => {
72
- if (command.includes("show-ref")) throw new Error("branch missing");
73
- return Buffer.from("");
74
- });
75
-
76
- await new BranchConfirmPrompt(config, branch_state, {} as never).run();
77
-
78
- expect(mocked.execSync).toHaveBeenCalledTimes(1);
79
- expect(mocked.execSync).toHaveBeenCalledWith(
80
- "git show-ref feat/TAC-123-cli-add-parser",
81
- {
82
- encoding: "utf-8",
83
- },
84
- );
85
- expect(mocked.info).toHaveBeenCalledWith(
86
- "Dry run: git worktree add ../worktrees/repo-TAC-123-add-parser -b feat/TAC-123-cli-add-parser",
87
- );
88
- });
89
- });