gflows 0.1.1

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.
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Completion command: print shell completion script for bash, zsh, or fish.
3
+ * Supports completion for commands, types (feature, bugfix, etc.), and when
4
+ * applicable branch names from local workflow branches (switch, delete, finish -B).
5
+ * @module commands/completion
6
+ */
7
+
8
+ import type { ParsedArgs } from "../types.js";
9
+ import { EXIT_USER } from "../constants.js";
10
+
11
+ const COMMANDS = [
12
+ "init",
13
+ "start",
14
+ "finish",
15
+ "switch",
16
+ "delete",
17
+ "list",
18
+ "bump",
19
+ "completion",
20
+ "status",
21
+ "help",
22
+ "version",
23
+ ];
24
+
25
+ const BRANCH_TYPES = [
26
+ "feature",
27
+ "bugfix",
28
+ "chore",
29
+ "release",
30
+ "hotfix",
31
+ "spike",
32
+ ];
33
+
34
+ const COMPLETION_SHELLS = ["bash", "zsh", "fish"] as const;
35
+
36
+ const BUMP_DIRECTIONS = ["up", "down"];
37
+ const BUMP_TYPES = ["patch", "minor", "major"];
38
+
39
+ /** Literal "${" for embedding in shell scripts (avoids template interpolation). */
40
+ const D = "${";
41
+
42
+ function bashScript(): string {
43
+ return `# Bash completion for gflows
44
+ # Install: source <(gflows completion bash) (add to .bashrc) or copy to /etc/bash_completion.d/
45
+
46
+ _gflows() {
47
+ local cur prev words cword cmd_idx cmd
48
+ words=(${D}COMP_WORDS[@]})
49
+ cword=\$COMP_CWORD
50
+ cur="${D}words[cword]:-}"
51
+ prev="${D}words[cword-1]:-}"
52
+
53
+ # Find command index (first positional; skip -C/--path and its value)
54
+ cmd_idx=1
55
+ while (( cmd_idx < cword )); do
56
+ if [[ "${D}words[cmd_idx]}" == "-C" ]] || [[ "${D}words[cmd_idx]}" == "--path" ]]; then
57
+ (( cmd_idx += 2 ))
58
+ elif [[ "${D}words[cmd_idx]}" == -* ]]; then
59
+ (( cmd_idx++ ))
60
+ else
61
+ break
62
+ fi
63
+ done
64
+ cmd="${D}words[cmd_idx]:-}"
65
+
66
+ # Resolve -C/--path for gflows list (branch names)
67
+ _gflows_path() {
68
+ local i path=""
69
+ for ((i=1; i<cword; i++)); do
70
+ if [[ "${D}words[i]}" == "-C" ]] || [[ "${D}words[i]}" == "--path" ]]; then
71
+ if ((i+1 < cword)); then path="${D}words[i+1]}"; fi
72
+ break
73
+ fi
74
+ done
75
+ if [[ -n "\$path" ]]; then
76
+ gflows -C "\$path" list 2>/dev/null
77
+ else
78
+ gflows list 2>/dev/null
79
+ fi
80
+ }
81
+
82
+ # Completing -C/--path value: suggest directories
83
+ if [[ "\$prev" == "-C" ]] || [[ "\$prev" == "--path" ]]; then
84
+ compopt -o dirnames 2>/dev/null
85
+ COMPREPLY=($(compgen -d -S / -- "\$cur"))
86
+ return
87
+ fi
88
+
89
+ # First positional: command
90
+ if (( cword == cmd_idx )); then
91
+ COMPREPLY=($(compgen -W "${COMMANDS.join(" ")}" -- "\$cur"))
92
+ return
93
+ fi
94
+
95
+ # After command: type/name/branches/shell/bump by command
96
+ case "$cmd" in
97
+ completion)
98
+ COMPREPLY=($(compgen -W "${COMPLETION_SHELLS.join(" ")}" -- "\$cur"))
99
+ ;;
100
+ start)
101
+ if (( cword == cmd_idx + 1 )); then
102
+ COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "\$cur"))
103
+ else
104
+ COMPREPLY=()
105
+ fi
106
+ ;;
107
+ finish)
108
+ if [[ "\$prev" == "-B" ]] || [[ "\$prev" == "--branch" ]]; then
109
+ COMPREPLY=($(compgen -W "$(_gflows_path)" -- "\$cur"))
110
+ elif (( cword == cmd_idx + 1 )); then
111
+ COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "\$cur"))
112
+ else
113
+ COMPREPLY=()
114
+ fi
115
+ ;;
116
+ list)
117
+ COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "\$cur"))
118
+ ;;
119
+ switch|delete)
120
+ COMPREPLY=($(compgen -W "$(_gflows_path)" -- "\$cur"))
121
+ ;;
122
+ bump)
123
+ if (( cword == cmd_idx + 1 )); then
124
+ COMPREPLY=($(compgen -W "${BUMP_DIRECTIONS.join(" ")}" -- "\$cur"))
125
+ elif (( cword == cmd_idx + 2 )); then
126
+ COMPREPLY=($(compgen -W "${BUMP_TYPES.join(" ")}" -- "\$cur"))
127
+ else
128
+ COMPREPLY=()
129
+ fi
130
+ ;;
131
+ *)
132
+ COMPREPLY=()
133
+ ;;
134
+ esac
135
+ }
136
+
137
+ complete -F _gflows gflows 2>/dev/null || complete -o bashdefault -o default -F _gflows gflows
138
+ `;
139
+ }
140
+
141
+ function zshScript(): string {
142
+ return `# Zsh completion for gflows
143
+ # Install: source <(gflows completion zsh) (add to .zshrc) or save to ${D}fpath[1]}/_gflows
144
+
145
+ _gflows_list_branches() {
146
+ local -a path
147
+ local i=1
148
+ while (( i < CURRENT )); do
149
+ if [[ "${D}words[i]}" == "-C" ]] || [[ "${D}words[i]}" == "--path" ]]; then
150
+ (( i+1 < CURRENT )) && path=(-C "${D}words[i+1]}")
151
+ break
152
+ fi
153
+ (( i++ ))
154
+ done
155
+ (( ${D}#path[@]} > 0 )) && gflows "${D}path[@]}" list 2>/dev/null || gflows list 2>/dev/null
156
+ }
157
+
158
+ _gflows() {
159
+ local cur context state line cmd cmd_idx i
160
+ _arguments -C -s -S \\
161
+ '-C+[run as if in dir]:dir:_files -/' \\
162
+ '--path=+[run as if in dir]:dir:_files -/' \\
163
+ '-h[show help]' \\
164
+ '--help[show help]' \\
165
+ '-V[show version]' \\
166
+ '--version[show version]' \\
167
+ '1:command:(${COMMANDS.join(" ")})' \\
168
+ '*::args:->args'
169
+
170
+ case $state in
171
+ args)
172
+ cur=\$words[CURRENT]
173
+ cmd_idx=1
174
+ while (( cmd_idx < CURRENT )); do
175
+ if [[ "${D}words[cmd_idx]}" == "-C" ]] || [[ "${D}words[cmd_idx]}" == "--path" ]]; then
176
+ (( cmd_idx += 2 ))
177
+ elif [[ "${D}words[cmd_idx]}" == -* ]]; then
178
+ (( cmd_idx++ ))
179
+ else
180
+ cmd="${D}words[cmd_idx]}"
181
+ break
182
+ fi
183
+ done
184
+ case "$cmd" in
185
+ completion)
186
+ _values "shell" ${COMPLETION_SHELLS.map((s) => `"${s}"`).join(" ")}
187
+ ;;
188
+ start)
189
+ _values "type" ${BRANCH_TYPES.map((t) => `"${t}"`).join(" ")}
190
+ ;;
191
+ finish)
192
+ if [[ "${D}words[CURRENT-1]}" == "-B" ]] || [[ "${D}words[CURRENT-1]}" == "--branch" ]]; then
193
+ _values "branch" \$(_gflows_list_branches)
194
+ else
195
+ _values "type" ${BRANCH_TYPES.map((t) => `"${t}"`).join(" ")}
196
+ fi
197
+ ;;
198
+ list)
199
+ _values "type" ${BRANCH_TYPES.map((t) => `"${t}"`).join(" ")}
200
+ ;;
201
+ switch|delete)
202
+ _values "branch" \$(_gflows_list_branches)
203
+ ;;
204
+ bump)
205
+ if [[ "${D}words[CURRENT-1]}" == "up" ]] || [[ "${D}words[CURRENT-1]}" == "down" ]]; then
206
+ _values "bump-type" ${BUMP_TYPES.map((t) => `"${t}"`).join(" ")}
207
+ else
208
+ _values "direction" ${BUMP_DIRECTIONS.map((d) => `"${d}"`).join(" ")}
209
+ fi
210
+ ;;
211
+ esac
212
+ ;;
213
+ esac
214
+ }
215
+
216
+ _gflows
217
+ `;
218
+ }
219
+
220
+ function fishScript(): string {
221
+ return `# Fish completion for gflows
222
+ # Install: gflows completion fish | source (add to ~/.config/fish/config.fish) or save to ~/.config/fish/completions/gflows.fish
223
+
224
+ function __gflows_path
225
+ set -l tokens (commandline -opc)
226
+ set -l i 1
227
+ while test $i -le (count $tokens)
228
+ if test "$tokens[$i]" = "-C"; and test (math $i + 1) -le (count $tokens)
229
+ echo $tokens[(math $i + 1)]
230
+ return
231
+ end
232
+ set i (math $i + 1)
233
+ end
234
+ end
235
+
236
+ function __gflows_list_branches
237
+ set -l path (__gflows_path)
238
+ if test -n "$path"
239
+ gflows -C "$path" list 2>/dev/null
240
+ else
241
+ gflows list 2>/dev/null
242
+ end
243
+ end
244
+
245
+ # Commands
246
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
247
+ -a "init" -d "Ensure main, create dev"
248
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
249
+ -a "start" -d "Create workflow branch"
250
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
251
+ -a "finish" -d "Merge and close branch"
252
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
253
+ -a "switch" -d "Switch branch"
254
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
255
+ -a "delete" -d "Delete local branch(es)"
256
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
257
+ -a "list" -d "List branches by type"
258
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
259
+ -a "bump" -d "Bump or rollback version"
260
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
261
+ -a "completion" -d "Print shell completion script"
262
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
263
+ -a "status" -d "Show current branch flow info"
264
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
265
+ -a "help" -d "Show usage"
266
+ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
267
+ -a "version" -d "Show version"
268
+
269
+ # completion <shell>
270
+ complete -c gflows -f -n "__fish_seen_subcommand_from completion" -a "bash" -d "Bash completion script"
271
+ complete -c gflows -f -n "__fish_seen_subcommand_from completion" -a "zsh" -d "Zsh completion script"
272
+ complete -c gflows -f -n "__fish_seen_subcommand_from completion" -a "fish" -d "Fish completion script"
273
+
274
+ # start <type>
275
+ complete -c gflows -f -n "__fish_seen_subcommand_from start; and not __fish_seen_subcommand_from ${BRANCH_TYPES.join(" ")}" \\
276
+ -a "feature" -d "Feature branch"
277
+ complete -c gflows -f -n "__fish_seen_subcommand_from start; and not __fish_seen_subcommand_from ${BRANCH_TYPES.join(" ")}" \\
278
+ -a "bugfix" -d "Bugfix branch"
279
+ complete -c gflows -f -n "__fish_seen_subcommand_from start; and not __fish_seen_subcommand_from ${BRANCH_TYPES.join(" ")}" \\
280
+ -a "chore" -d "Chore branch"
281
+ complete -c gflows -f -n "__fish_seen_subcommand_from start; and not __fish_seen_subcommand_from ${BRANCH_TYPES.join(" ")}" \\
282
+ -a "release" -d "Release branch"
283
+ complete -c gflows -f -n "__fish_seen_subcommand_from start; and not __fish_seen_subcommand_from ${BRANCH_TYPES.join(" ")}" \\
284
+ -a "hotfix" -d "Hotfix branch"
285
+ complete -c gflows -f -n "__fish_seen_subcommand_from start; and not __fish_seen_subcommand_from ${BRANCH_TYPES.join(" ")}" \\
286
+ -a "spike" -d "Spike/experiment branch"
287
+
288
+ # finish <type> or -B <branch>
289
+ complete -c gflows -f -n "__fish_seen_subcommand_from finish; and not __fish_seen_subcommand_from ${BRANCH_TYPES.join(" ")}; and not __fish_prev_arg -x -l branch -s B" \\
290
+ -a "feature bugfix chore release hotfix spike"
291
+ complete -c gflows -f -n "__fish_seen_subcommand_from finish; and __fish_prev_arg -x -l branch -s B" \\
292
+ -a "(__gflows_list_branches)"
293
+
294
+ # list [type]
295
+ complete -c gflows -f -n "__fish_seen_subcommand_from list" \\
296
+ -a "feature bugfix chore release hotfix spike"
297
+
298
+ # switch <branch>
299
+ complete -c gflows -f -n "__fish_seen_subcommand_from switch" \\
300
+ -a "(__gflows_list_branches)"
301
+
302
+ # delete <branch>
303
+ complete -c gflows -f -n "__fish_seen_subcommand_from delete" \\
304
+ -a "(__gflows_list_branches)"
305
+
306
+ # bump [up|down] [patch|minor|major]
307
+ complete -c gflows -f -n "__fish_seen_subcommand_from bump; and not __fish_seen_subcommand_from ${BUMP_DIRECTIONS.join(" ")}" \\
308
+ -a "up down"
309
+ complete -c gflows -f -n "__fish_seen_subcommand_from bump; and __fish_seen_subcommand_from up down" \\
310
+ -a "patch minor major"
311
+
312
+ # Common flags
313
+ complete -c gflows -x -n "__fish_seen_subcommand_from ${COMMANDS.join(" ")}" -l path -s C -d "Run as if in dir" -a "(__fish_complete_directories)"
314
+ complete -c gflows -f -n "__fish_seen_subcommand_from ${COMMANDS.join(" ")}" -l help -s h -d "Show help"
315
+ complete -c gflows -f -n "__fish_seen_subcommand_from ${COMMANDS.join(" ")}" -l version -s V -d "Show version"
316
+ `;
317
+ }
318
+
319
+ /**
320
+ * Runs the completion command: prints the shell completion script for the given shell.
321
+ * Requires one of: bash, zsh, fish. Script supports commands, types, and when applicable
322
+ * branch names from local workflow branches (via `gflows list`).
323
+ *
324
+ * @param args - Parsed CLI args; args.completionShell must be "bash" | "zsh" | "fish".
325
+ */
326
+ export async function run(args: ParsedArgs): Promise<void> {
327
+ const shell = args.completionShell;
328
+ if (!shell) {
329
+ console.error(
330
+ "gflows: completion requires a shell. Use: gflows completion bash | zsh | fish"
331
+ );
332
+ process.exit(EXIT_USER);
333
+ }
334
+
335
+ let script: string;
336
+ switch (shell) {
337
+ case "bash":
338
+ script = bashScript();
339
+ break;
340
+ case "zsh":
341
+ script = zshScript();
342
+ break;
343
+ case "fish":
344
+ script = fishScript();
345
+ break;
346
+ default: {
347
+ const _: never = shell;
348
+ script = "";
349
+ }
350
+ }
351
+
352
+ process.stdout.write(script);
353
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Delete command: delete local workflow branch(es). Guards main/dev; picker when TTY and no names.
3
+ * @module commands/delete
4
+ */
5
+
6
+ import type { BranchType } from "../types.js";
7
+ import type { ParsedArgs } from "../types.js";
8
+ import { resolveConfig } from "../config.js";
9
+ import { EXIT_OK, EXIT_USER } from "../constants.js";
10
+ import { CannotDeleteMainOrDevError, NotRepoError } from "../errors.js";
11
+ import {
12
+ branchList,
13
+ deleteBranch,
14
+ resolveRepoRoot,
15
+ } from "../git.js";
16
+
17
+ const BRANCH_TYPES: BranchType[] = [
18
+ "feature",
19
+ "bugfix",
20
+ "chore",
21
+ "release",
22
+ "hotfix",
23
+ "spike",
24
+ ];
25
+
26
+ /**
27
+ * Returns local branch names that match any workflow prefix (feature/, bugfix/, etc.).
28
+ */
29
+ function getWorkflowBranches(
30
+ allBranches: string[],
31
+ prefixes: Record<BranchType, string>
32
+ ): string[] {
33
+ const prefixed = BRANCH_TYPES.map((t) => prefixes[t]).filter(Boolean);
34
+ return allBranches.filter((b) =>
35
+ prefixed.some((p) => p && b.startsWith(p))
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Run the delete command.
41
+ * With branch name(s) as positionals: delete those branches (guard main/dev).
42
+ * With no names and TTY: show a checkbox picker of workflow branches; if none, exit 1 with message.
43
+ * With no names and not TTY: exit 1 with message to provide branch name(s).
44
+ * Local delete only; never deletes main or dev.
45
+ */
46
+ export async function run(args: ParsedArgs): Promise<void> {
47
+ const { cwd, branchNames: rawBranchNames, dryRun, quiet } = args;
48
+
49
+ const root = await resolveRepoRoot(cwd).catch((err) => {
50
+ if (err instanceof NotRepoError) throw err;
51
+ throw err;
52
+ });
53
+ const config = resolveConfig(root);
54
+ const { main, dev, prefixes } = config;
55
+
56
+ const fromPositionals = (rawBranchNames ?? [])
57
+ .map((s) => s.trim())
58
+ .filter(Boolean);
59
+
60
+ if (fromPositionals.length > 0) {
61
+ for (const branch of fromPositionals) {
62
+ if (branch === main || branch === dev) {
63
+ throw new CannotDeleteMainOrDevError(
64
+ `Cannot delete the long-lived branch '${branch}'.`
65
+ );
66
+ }
67
+ }
68
+ for (const branch of fromPositionals) {
69
+ await deleteBranch(root, branch, {
70
+ dryRun,
71
+ verbose: args.verbose,
72
+ });
73
+ if (!quiet && !dryRun) {
74
+ console.error(`Deleted branch '${branch}'.`);
75
+ }
76
+ }
77
+ return;
78
+ }
79
+
80
+ const isTTY = typeof process.stdin.isTTY === "boolean" && process.stdin.isTTY;
81
+ if (!isTTY) {
82
+ console.error(
83
+ "gflows delete: no branch name(s) given and stdin is not a TTY. Pass branch name(s) (e.g. gflows delete feature/my-branch) or run from an interactive terminal."
84
+ );
85
+ process.exit(EXIT_USER);
86
+ }
87
+
88
+ const allLocal = await branchList(root, {
89
+ dryRun,
90
+ verbose: args.verbose,
91
+ });
92
+ const workflowBranches = getWorkflowBranches(allLocal, prefixes);
93
+
94
+ if (workflowBranches.length === 0) {
95
+ if (!quiet) {
96
+ console.error(
97
+ "No workflow branches to delete. Create one with 'gflows start <type> <name>'."
98
+ );
99
+ }
100
+ process.exit(EXIT_USER);
101
+ }
102
+
103
+ const { checkbox } = await import("@inquirer/prompts");
104
+ const chosen = await checkbox({
105
+ message: "Delete branch(es)",
106
+ choices: workflowBranches.map((b) => ({ name: b, value: b })),
107
+ });
108
+
109
+ if (!Array.isArray(chosen) || chosen.length === 0) {
110
+ if (!quiet) {
111
+ console.error("No branches selected.");
112
+ }
113
+ process.exit(EXIT_OK);
114
+ }
115
+
116
+ for (const branch of chosen) {
117
+ if (branch === main || branch === dev) {
118
+ throw new CannotDeleteMainOrDevError(
119
+ `Cannot delete the long-lived branch '${branch}'.`
120
+ );
121
+ }
122
+ }
123
+
124
+ for (const branch of chosen) {
125
+ await deleteBranch(root, branch, {
126
+ dryRun,
127
+ verbose: args.verbose,
128
+ });
129
+ if (!quiet && !dryRun) {
130
+ console.error(`Deleted branch '${branch}'.`);
131
+ }
132
+ }
133
+ }