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.
- package/dist/branch.js +628 -19
- package/dist/chunk-GAAS3VS3.js +922 -0
- package/dist/chunk-H5CLUQIL.js +313 -0
- package/dist/index.js +1122 -41
- package/dist/init.js +44 -1
- package/package.json +12 -4
- package/readme.md +4 -2
- package/.better-commits.json +0 -52
- package/.github/workflows/publish.yml +0 -34
- package/.github/workflows/test.yml +0 -27
- package/.prettierignore +0 -5
- package/.prettierrc +0 -1
- package/dist/chunk-43H72S6V.js +0 -1
- package/dist/chunk-B7AGSPP3.js +0 -261
- package/src/args.test.ts +0 -128
- package/src/args.ts +0 -125
- package/src/branch-args.test.ts +0 -75
- package/src/branch-args.ts +0 -107
- package/src/branch-help.ts +0 -125
- package/src/branch.ts +0 -97
- package/src/default-config-template.ts +0 -258
- package/src/git.test.ts +0 -64
- package/src/git.ts +0 -72
- package/src/help.ts +0 -138
- package/src/index.test.ts +0 -7
- package/src/index.ts +0 -101
- package/src/init.test.ts +0 -123
- package/src/init.ts +0 -46
- package/src/prompts/autocomplete-multiselect.test.ts +0 -129
- package/src/prompts/autocomplete-multiselect.ts +0 -249
- package/src/prompts/branch-checkout.prompt.ts +0 -36
- package/src/prompts/branch-confirm.prompt.test.ts +0 -89
- package/src/prompts/branch-confirm.prompt.ts +0 -149
- package/src/prompts/branch-description.prompt.ts +0 -37
- package/src/prompts/branch-runnable.ts +0 -13
- package/src/prompts/branch-scope.prompt.ts +0 -59
- package/src/prompts/branch-ticket.prompt.ts +0 -41
- package/src/prompts/branch-type.prompt.ts +0 -46
- package/src/prompts/branch-user.prompt.ts +0 -50
- package/src/prompts/branch-version.prompt.ts +0 -41
- package/src/prompts/commit-body.prompt.ts +0 -51
- package/src/prompts/commit-confirm.prompt.ts +0 -123
- package/src/prompts/commit-footer.prompt.ts +0 -195
- package/src/prompts/commit-scope.prompt.ts +0 -91
- package/src/prompts/commit-status.prompt.ts +0 -66
- package/src/prompts/commit-ticket.prompt.ts +0 -82
- package/src/prompts/commit-title.prompt.ts +0 -98
- package/src/prompts/commit-type.prompt.ts +0 -96
- package/src/prompts/runnable.ts +0 -13
- package/src/utils/build-branch.test.ts +0 -159
- package/src/utils/build-branch.ts +0 -48
- package/src/utils/build-commit-string.test.ts +0 -273
- package/src/utils/build-commit-string.ts +0 -163
- package/src/utils/commit-title-size.ts +0 -24
- package/src/utils/infer.test.ts +0 -174
- package/src/utils/infer.ts +0 -160
- package/src/utils/messages.ts +0 -25
- package/src/utils/no-interactive-branch-validation.test.ts +0 -193
- package/src/utils/no-interactive-validation.test.ts +0 -174
- package/src/utils/no-interactive-validation.ts +0 -213
- package/src/utils.test.ts +0 -164
- package/src/utils.ts +0 -235
- package/src/valibot-consts.ts +0 -117
- package/src/valibot-state.test.ts +0 -57
- package/src/valibot-state.ts +0 -276
- package/tsconfig.json +0 -15
- package/tsup.config.ts +0 -12
- 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
|
-
});
|