gflows 0.1.12 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gflows",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "A lightweight CLI for consistent Git branching workflows (main + dev, feature/bugfix/chore/release/hotfix).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,10 @@
10
10
  },
11
11
  "scripts": {
12
12
  "test": "bun test",
13
- "lint": "tsc --noEmit",
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "biome check .",
15
+ "lint:fix": "biome check . --write",
16
+ "format": "biome format . --write",
14
17
  "gflows": "bun run src/cli.ts",
15
18
  "publish:all": "bun run scripts/publish.ts",
16
19
  "publish:npm": "bun run scripts/publish.ts -- --npm-only",
@@ -33,6 +36,7 @@
33
36
  "@inquirer/prompts": "^8"
34
37
  },
35
38
  "devDependencies": {
39
+ "@biomejs/biome": "^2.4.4",
36
40
  "@types/bun": "latest",
37
41
  "typescript": "^5"
38
42
  }
package/src/cli.ts CHANGED
@@ -9,9 +9,9 @@
9
9
  import { existsSync, statSync } from "node:fs";
10
10
  import { resolve } from "node:path";
11
11
  import { parseArgs } from "node:util";
12
- import type { Command, BranchType, ParsedArgs } from "./types.js";
13
- import { EXIT_OK, EXIT_USER, EXIT_GIT } from "./constants.js";
12
+ import { EXIT_GIT, EXIT_OK, EXIT_USER } from "./constants.js";
14
13
  import { exitCodeForError } from "./errors.js";
14
+ import type { BranchType, Command, ParsedArgs } from "./types.js";
15
15
 
16
16
  /** Last parsed args, set at start of run(); used by catch/rejection to respect -v for stack trace. */
17
17
  let lastParsedArgs: ParsedArgs | null = null;
@@ -30,14 +30,7 @@ const COMMANDS: Command[] = [
30
30
  "version",
31
31
  ];
32
32
 
33
- const BRANCH_TYPES: BranchType[] = [
34
- "feature",
35
- "bugfix",
36
- "chore",
37
- "release",
38
- "hotfix",
39
- "spike",
40
- ];
33
+ const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
41
34
 
42
35
  /** Short flag → command (when used as main command). */
43
36
  const SHORT_TO_COMMAND: Record<string, Command> = {
@@ -80,6 +73,7 @@ function buildParseArgsOptions() {
80
73
  // Common
81
74
  push: { type: "boolean" as const, short: "p" },
82
75
  noPush: { type: "boolean" as const, short: "P" },
76
+ "no-push": { type: "boolean" as const },
83
77
  main: { type: "string" as const },
84
78
  dev: { type: "string" as const },
85
79
  remote: { type: "string" as const, short: "R" },
@@ -123,12 +117,57 @@ function resolveCwd(pathFlag: string | undefined): string {
123
117
  return absolute;
124
118
  }
125
119
 
120
+ /**
121
+ * Returns the command name closest to `input` by edit distance, or undefined if no close match.
122
+ * Used for "did you mean?" when the user mistypes a command.
123
+ */
124
+ function closestCommand(input: string): Command | undefined {
125
+ if (!input || input.length < 2) return undefined;
126
+ const target = input.toLowerCase();
127
+ let best: Command | undefined;
128
+ let bestDistance = 3; // only suggest if within 2 edits
129
+ for (const cmd of COMMANDS) {
130
+ const d = editDistance(target, cmd);
131
+ if (d < bestDistance) {
132
+ bestDistance = d;
133
+ best = cmd as Command;
134
+ }
135
+ }
136
+ return best;
137
+ }
138
+
139
+ /** Levenshtein edit distance between two strings. */
140
+ function editDistance(a: string, b: string): number {
141
+ const m = a.length;
142
+ const n = b.length;
143
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
144
+ for (let i = 0; i <= m; i++) {
145
+ const row = dp[i];
146
+ if (row) row[0] = i;
147
+ }
148
+ for (let j = 0; j <= n; j++) {
149
+ const row = dp[0];
150
+ if (row) row[j] = j;
151
+ }
152
+ for (let i = 1; i <= m; i++) {
153
+ for (let j = 1; j <= n; j++) {
154
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
155
+ const v1 = dp[i - 1]?.[j] ?? 0;
156
+ const v2 = dp[i]?.[j - 1] ?? 0;
157
+ const v3 = dp[i - 1]?.[j - 1] ?? 0;
158
+ const rowI = dp[i];
159
+ if (rowI) rowI[j] = Math.min(v1 + 1, v2 + 1, v3 + cost);
160
+ }
161
+ }
162
+ return dp[m]?.[n] ?? 0;
163
+ }
164
+
126
165
  /** Resolve command from positionals and short flags. Short wins if both present. */
127
166
  function resolveCommand(
128
167
  positionals: string[],
129
- values: Record<string, string | boolean | undefined>
168
+ values: Record<string, string | boolean | undefined>,
130
169
  ): Command | undefined {
131
- for (const [short, cmd] of Object.entries(SHORT_TO_COMMAND)) {
170
+ for (const [_short, cmd] of Object.entries(SHORT_TO_COMMAND)) {
132
171
  const key = cmd === "delete" ? "delete" : cmd;
133
172
  if (values[key] === true) return cmd as Command;
134
173
  }
@@ -143,7 +182,7 @@ function resolveCommand(
143
182
  function resolveType(
144
183
  command: Command,
145
184
  positionals: string[],
146
- values: Record<string, string | boolean | undefined>
185
+ values: Record<string, string | boolean | undefined>,
147
186
  ): BranchType | undefined {
148
187
  if (command !== "start" && command !== "finish" && command !== "list") {
149
188
  return undefined;
@@ -179,7 +218,7 @@ function resolveType(
179
218
  function resolveName(
180
219
  command: Command,
181
220
  positionals: string[],
182
- values: Record<string, string | boolean | undefined>
221
+ values: Record<string, string | boolean | undefined>,
183
222
  ): string | undefined {
184
223
  const branch = values.branch;
185
224
  if (typeof branch === "string" && branch.trim() !== "") {
@@ -200,7 +239,7 @@ function resolveName(
200
239
  }
201
240
  if (command === "bump") {
202
241
  const dir = positionals[skip];
203
- const typ = positionals[skip + 1];
242
+ const _typ = positionals[skip + 1];
204
243
  if (dir === "up" || dir === "down") {
205
244
  return dir;
206
245
  }
@@ -215,7 +254,7 @@ function resolveName(
215
254
  /** Resolve bump direction and type from positionals (bump [up|down] [patch|minor|major]). */
216
255
  function resolveBump(
217
256
  positionals: string[],
218
- values: Record<string, string | boolean | undefined>
257
+ _values: Record<string, string | boolean | undefined>,
219
258
  ): { direction?: "up" | "down"; type?: "patch" | "minor" | "major" } {
220
259
  const skip = positionals[0] === "bump" ? 1 : 0;
221
260
  const a = positionals[skip];
@@ -240,7 +279,13 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
240
279
 
241
280
  const command = resolveCommand(positionals, v);
242
281
  if (!command) {
243
- console.error("gflows: missing command. Use 'gflows help' for usage.");
282
+ const first = positionals[0];
283
+ const suggestion = typeof first === "string" ? closestCommand(first) : undefined;
284
+ if (suggestion) {
285
+ console.error(`gflows: unknown command '${first}'. Did you mean '${suggestion}'?`);
286
+ } else {
287
+ console.error("gflows: missing command. Use 'gflows help' for usage.");
288
+ }
244
289
  process.exit(EXIT_USER);
245
290
  }
246
291
 
@@ -259,9 +304,7 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
259
304
  // -r context: for list → includeRemote; for start/finish → already used as type release
260
305
  const includeRemote =
261
306
  command === "list"
262
- ? (v.includeRemote === true ||
263
- v["include-remote"] === true ||
264
- v.release === true)
307
+ ? v.includeRemote === true || v["include-remote"] === true || v.release === true
265
308
  : false;
266
309
 
267
310
  let completionShell: "bash" | "zsh" | "fish" | undefined;
@@ -279,7 +322,7 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
279
322
  bumpDirection,
280
323
  bumpType,
281
324
  push: v.push === true,
282
- noPush: v.noPush === true,
325
+ noPush: v.noPush === true || v["no-push"] === true,
283
326
  main: typeof v.main === "string" && v.main.trim() !== "" ? v.main.trim() : undefined,
284
327
  dev: typeof v.dev === "string" && v.dev.trim() !== "" ? v.dev.trim() : undefined,
285
328
  remote: typeof v.remote === "string" ? v.remote : undefined,
@@ -1,20 +1,51 @@
1
1
  /**
2
- * Bump command: bump or rollback root package version (patch/minor/major).
2
+ * Bump command: bump or rollback package version (patch/minor/major).
3
+ * Supports monorepos: discovers all package.json and jsr.json under cwd and bumps them to the same version.
3
4
  * Keeps package.json and jsr.json in sync; no git operations.
4
5
  * @module commands/bump
5
6
  */
6
7
 
7
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
- import { join } from "node:path";
9
- import type { ParsedArgs } from "../types.js";
10
- import type { BumpDirection, BumpType } from "../types.js";
8
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
+ import { join, relative } from "node:path";
11
10
  import { EXIT_OK, EXIT_USER } from "../constants.js";
12
11
  import { InvalidVersionError } from "../errors.js";
13
12
  import { hint, success } from "../out.js";
13
+ import type { BumpDirection, BumpType, ParsedArgs } from "../types.js";
14
14
 
15
15
  const PACKAGE_JSON = "package.json";
16
16
  const JSR_JSON = "jsr.json";
17
17
 
18
+ /** Directory names to skip when discovering package roots (monorepo). */
19
+ const SKIP_DIRS = new Set(["node_modules", ".git"]);
20
+
21
+ /**
22
+ * Recursively finds all directories under `root` that contain a package.json.
23
+ * Skips node_modules and .git.
24
+ */
25
+ function findPackageRoots(root: string): string[] {
26
+ const acc: string[] = [];
27
+ if (!existsSync(root) || !statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
28
+ return acc;
29
+ }
30
+ if (existsSync(join(root, PACKAGE_JSON))) {
31
+ acc.push(root);
32
+ }
33
+ let entries: Array<{ isDirectory(): boolean; name: string }>;
34
+ try {
35
+ entries = readdirSync(root, { withFileTypes: true }) as Array<{
36
+ isDirectory(): boolean;
37
+ name: string;
38
+ }>;
39
+ } catch {
40
+ return acc;
41
+ }
42
+ for (const e of entries) {
43
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
44
+ acc.push(...findPackageRoots(join(root, e.name)));
45
+ }
46
+ return acc;
47
+ }
48
+
18
49
  /** Semver triplet. */
19
50
  interface Semver {
20
51
  major: number;
@@ -30,18 +61,16 @@ function parseVersion(version: string): Semver {
30
61
  const trimmed = version.trim();
31
62
  const normalized = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
32
63
  const parts = normalized.split(".");
33
- if (
34
- parts.length !== 3 ||
35
- parts.some((p) => !/^\d+$/.test(p))
36
- ) {
64
+ if (parts.length !== 3 || parts.some((p) => !/^\d+$/.test(p))) {
37
65
  throw new InvalidVersionError(
38
- `Invalid version '${version}'. Expected format: X.Y.Z or vX.Y.Z (e.g. 1.2.3 or v1.2.3).`
66
+ `Invalid version '${version}'. Expected format: X.Y.Z or vX.Y.Z (e.g. 1.2.3 or v1.2.3).`,
39
67
  );
40
68
  }
69
+ const [p0, p1, p2] = parts;
41
70
  return {
42
- major: parseInt(parts[0]!, 10),
43
- minor: parseInt(parts[1]!, 10),
44
- patch: parseInt(parts[2]!, 10),
71
+ major: parseInt(p0 ?? "0", 10),
72
+ minor: parseInt(p1 ?? "0", 10),
73
+ patch: parseInt(p2 ?? "0", 10),
45
74
  };
46
75
  }
47
76
 
@@ -103,7 +132,7 @@ function readPackageVersion(dir: string): { raw: string; semver: Semver } {
103
132
  const path = join(dir, PACKAGE_JSON);
104
133
  if (!existsSync(path)) {
105
134
  throw new InvalidVersionError(
106
- `No package.json found at ${path}. Run from project root or use -C <dir>.`
135
+ `No package.json found at ${path}. Run from project root or use -C <dir>.`,
107
136
  );
108
137
  }
109
138
  const raw = readFileSync(path, "utf-8");
@@ -111,7 +140,7 @@ function readPackageVersion(dir: string): { raw: string; semver: Semver } {
111
140
  const version = data.version;
112
141
  if (typeof version !== "string" || version.trim() === "") {
113
142
  throw new InvalidVersionError(
114
- "package.json has no valid 'version' field."
143
+ 'package.json has no valid \'version\' field. Add a "version" field (e.g. "0.0.0") to package.json.',
115
144
  );
116
145
  }
117
146
  const semver = parseVersion(version);
@@ -127,7 +156,7 @@ function writePackageVersion(dir: string, newVersion: string): void {
127
156
  const raw = readFileSync(path, "utf-8");
128
157
  const data = JSON.parse(raw) as Record<string, unknown>;
129
158
  data.version = newVersion;
130
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
159
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
131
160
  }
132
161
 
133
162
  /**
@@ -139,7 +168,7 @@ function syncJsrVersion(dir: string, newVersion: string): boolean {
139
168
  const raw = readFileSync(path, "utf-8");
140
169
  const data = JSON.parse(raw) as Record<string, unknown>;
141
170
  data.version = newVersion;
142
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
171
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
143
172
  return true;
144
173
  }
145
174
 
@@ -154,15 +183,14 @@ export async function run(args: ParsedArgs): Promise<void> {
154
183
  let direction: BumpDirection;
155
184
  let type: BumpType;
156
185
 
157
- const isTTY =
158
- typeof process.stdin.isTTY === "boolean" && process.stdin.isTTY;
186
+ const isTTY = typeof process.stdin.isTTY === "boolean" && process.stdin.isTTY;
159
187
 
160
188
  if (bumpDirection && bumpType) {
161
189
  direction = bumpDirection;
162
190
  type = bumpType;
163
191
  } else if (!isTTY) {
164
192
  console.error(
165
- "gflows bump: when not in a TTY, both direction and type are required. Example: gflows bump up patch"
193
+ "gflows bump: when not in a TTY, both direction and type are required. Example: gflows bump up patch",
166
194
  );
167
195
  process.exit(EXIT_USER);
168
196
  } else {
@@ -184,14 +212,33 @@ export async function run(args: ParsedArgs): Promise<void> {
184
212
  });
185
213
  }
186
214
 
187
- const { raw: oldVersion, semver } = readPackageVersion(cwd);
188
- const newSemver =
189
- direction === "up" ? bumpUp(semver, type) : bumpDown(semver, type);
215
+ let roots = findPackageRoots(cwd);
216
+ roots = [...roots].sort((a, b) => {
217
+ if (a === cwd && b !== cwd) return -1;
218
+ if (a !== cwd && b === cwd) return 1;
219
+ return a.localeCompare(b);
220
+ });
221
+ if (roots.length === 0) {
222
+ throw new InvalidVersionError(
223
+ `No package.json found under ${cwd}. Run from project root or use -C <dir>.`,
224
+ );
225
+ }
226
+ const primaryRoot = roots[0];
227
+ if (primaryRoot === undefined) {
228
+ throw new InvalidVersionError(
229
+ `No package.json found under ${cwd}. Run from project root or use -C <dir>.`,
230
+ );
231
+ }
232
+ const { raw: oldVersion, semver } = readPackageVersion(primaryRoot);
233
+ const newSemver = direction === "up" ? bumpUp(semver, type) : bumpDown(semver, type);
190
234
  const newVersion = formatVersion(newSemver);
191
235
 
192
- const filesToUpdate: string[] = [PACKAGE_JSON];
193
- if (existsSync(join(cwd, JSR_JSON))) {
194
- filesToUpdate.push(JSR_JSON);
236
+ const filesToUpdate: string[] = [];
237
+ for (const dir of roots) {
238
+ filesToUpdate.push(relative(cwd, join(dir, PACKAGE_JSON)) || PACKAGE_JSON);
239
+ if (existsSync(join(dir, JSR_JSON))) {
240
+ filesToUpdate.push(relative(cwd, join(dir, JSR_JSON)) || JSR_JSON);
241
+ }
195
242
  }
196
243
 
197
244
  if (dryRun) {
@@ -202,15 +249,18 @@ export async function run(args: ParsedArgs): Promise<void> {
202
249
  process.exit(EXIT_OK);
203
250
  }
204
251
 
205
- writePackageVersion(cwd, newVersion);
206
- const jsrUpdated = syncJsrVersion(cwd, newVersion);
252
+ const updated: string[] = [];
253
+ for (const dir of roots) {
254
+ writePackageVersion(dir, newVersion);
255
+ updated.push(relative(cwd, join(dir, PACKAGE_JSON)) || PACKAGE_JSON);
256
+ if (syncJsrVersion(dir, newVersion)) {
257
+ updated.push(relative(cwd, join(dir, JSR_JSON)) || JSR_JSON);
258
+ }
259
+ }
207
260
 
208
261
  if (!quiet) {
209
262
  success(`Bumped version: ${oldVersion} → ${newVersion}`);
210
- const updated = [PACKAGE_JSON];
211
- if (jsrUpdated) updated.push(JSR_JSON);
212
263
  success(`Updated: ${updated.join(", ")}`);
213
- // Hint: suggest next step — commit and start release branch
214
264
  hint("Commit the change, then run gflows start release vX.Y.Z to release.");
215
265
  }
216
266
  }
@@ -5,8 +5,8 @@
5
5
  * @module commands/completion
6
6
  */
7
7
 
8
- import type { ParsedArgs } from "../types.js";
9
8
  import { EXIT_USER } from "../constants.js";
9
+ import type { ParsedArgs } from "../types.js";
10
10
 
11
11
  const COMMANDS = [
12
12
  "init",
@@ -22,14 +22,7 @@ const COMMANDS = [
22
22
  "version",
23
23
  ];
24
24
 
25
- const BRANCH_TYPES = [
26
- "feature",
27
- "bugfix",
28
- "chore",
29
- "release",
30
- "hotfix",
31
- "spike",
32
- ];
25
+ const BRANCH_TYPES = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
33
26
 
34
27
  const COMPLETION_SHELLS = ["bash", "zsh", "fish"] as const;
35
28
 
@@ -46,7 +39,7 @@ function bashScript(): string {
46
39
  _gflows() {
47
40
  local cur prev words cword cmd_idx cmd
48
41
  words=(${D}COMP_WORDS[@]})
49
- cword=\$COMP_CWORD
42
+ cword=$COMP_CWORD
50
43
  cur="${D}words[cword]:-}"
51
44
  prev="${D}words[cword-1]:-}"
52
45
 
@@ -72,58 +65,58 @@ _gflows() {
72
65
  break
73
66
  fi
74
67
  done
75
- if [[ -n "\$path" ]]; then
76
- gflows -C "\$path" list 2>/dev/null
68
+ if [[ -n "$path" ]]; then
69
+ gflows -C "$path" list 2>/dev/null
77
70
  else
78
71
  gflows list 2>/dev/null
79
72
  fi
80
73
  }
81
74
 
82
75
  # Completing -C/--path value: suggest directories
83
- if [[ "\$prev" == "-C" ]] || [[ "\$prev" == "--path" ]]; then
76
+ if [[ "$prev" == "-C" ]] || [[ "$prev" == "--path" ]]; then
84
77
  compopt -o dirnames 2>/dev/null
85
- COMPREPLY=($(compgen -d -S / -- "\$cur"))
78
+ COMPREPLY=($(compgen -d -S / -- "$cur"))
86
79
  return
87
80
  fi
88
81
 
89
82
  # First positional: command
90
83
  if (( cword == cmd_idx )); then
91
- COMPREPLY=($(compgen -W "${COMMANDS.join(" ")}" -- "\$cur"))
84
+ COMPREPLY=($(compgen -W "${COMMANDS.join(" ")}" -- "$cur"))
92
85
  return
93
86
  fi
94
87
 
95
88
  # After command: type/name/branches/shell/bump by command
96
89
  case "$cmd" in
97
90
  completion)
98
- COMPREPLY=($(compgen -W "${COMPLETION_SHELLS.join(" ")}" -- "\$cur"))
91
+ COMPREPLY=($(compgen -W "${COMPLETION_SHELLS.join(" ")}" -- "$cur"))
99
92
  ;;
100
93
  start)
101
94
  if (( cword == cmd_idx + 1 )); then
102
- COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "\$cur"))
95
+ COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "$cur"))
103
96
  else
104
97
  COMPREPLY=()
105
98
  fi
106
99
  ;;
107
100
  finish)
108
- if [[ "\$prev" == "-B" ]] || [[ "\$prev" == "--branch" ]]; then
109
- COMPREPLY=($(compgen -W "$(_gflows_path)" -- "\$cur"))
101
+ if [[ "$prev" == "-B" ]] || [[ "$prev" == "--branch" ]]; then
102
+ COMPREPLY=($(compgen -W "$(_gflows_path)" -- "$cur"))
110
103
  elif (( cword == cmd_idx + 1 )); then
111
- COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "\$cur"))
104
+ COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "$cur"))
112
105
  else
113
106
  COMPREPLY=()
114
107
  fi
115
108
  ;;
116
109
  list)
117
- COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "\$cur"))
110
+ COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "$cur"))
118
111
  ;;
119
112
  switch|delete)
120
- COMPREPLY=($(compgen -W "$(_gflows_path)" -- "\$cur"))
113
+ COMPREPLY=($(compgen -W "$(_gflows_path)" -- "$cur"))
121
114
  ;;
122
115
  bump)
123
116
  if (( cword == cmd_idx + 1 )); then
124
- COMPREPLY=($(compgen -W "${BUMP_DIRECTIONS.join(" ")}" -- "\$cur"))
117
+ COMPREPLY=($(compgen -W "${BUMP_DIRECTIONS.join(" ")}" -- "$cur"))
125
118
  elif (( cword == cmd_idx + 2 )); then
126
- COMPREPLY=($(compgen -W "${BUMP_TYPES.join(" ")}" -- "\$cur"))
119
+ COMPREPLY=($(compgen -W "${BUMP_TYPES.join(" ")}" -- "$cur"))
127
120
  else
128
121
  COMPREPLY=()
129
122
  fi
@@ -169,7 +162,7 @@ _gflows() {
169
162
 
170
163
  case $state in
171
164
  args)
172
- cur=\$words[CURRENT]
165
+ cur=$words[CURRENT]
173
166
  cmd_idx=1
174
167
  while (( cmd_idx < CURRENT )); do
175
168
  if [[ "${D}words[cmd_idx]}" == "-C" ]] || [[ "${D}words[cmd_idx]}" == "--path" ]]; then
@@ -190,7 +183,7 @@ _gflows() {
190
183
  ;;
191
184
  finish)
192
185
  if [[ "${D}words[CURRENT-1]}" == "-B" ]] || [[ "${D}words[CURRENT-1]}" == "--branch" ]]; then
193
- _values "branch" \$(_gflows_list_branches)
186
+ _values "branch" $(_gflows_list_branches)
194
187
  else
195
188
  _values "type" ${BRANCH_TYPES.map((t) => `"${t}"`).join(" ")}
196
189
  fi
@@ -199,7 +192,7 @@ _gflows() {
199
192
  _values "type" ${BRANCH_TYPES.map((t) => `"${t}"`).join(" ")}
200
193
  ;;
201
194
  switch|delete)
202
- _values "branch" \$(_gflows_list_branches)
195
+ _values "branch" $(_gflows_list_branches)
203
196
  ;;
204
197
  bump)
205
198
  if [[ "${D}words[CURRENT-1]}" == "up" ]] || [[ "${D}words[CURRENT-1]}" == "down" ]]; then
@@ -326,9 +319,7 @@ complete -c gflows -f -n "__fish_seen_subcommand_from ${COMMANDS.join(" ")}" -l
326
319
  export async function run(args: ParsedArgs): Promise<void> {
327
320
  const shell = args.completionShell;
328
321
  if (!shell) {
329
- console.error(
330
- "gflows: completion requires a shell. Use: gflows completion bash | zsh | fish"
331
- );
322
+ console.error("gflows: completion requires a shell. Use: gflows completion bash | zsh | fish");
332
323
  process.exit(EXIT_USER);
333
324
  }
334
325
 
@@ -3,38 +3,24 @@
3
3
  * @module commands/delete
4
4
  */
5
5
 
6
- import type { BranchType } from "../types.js";
7
- import type { ParsedArgs } from "../types.js";
8
6
  import { resolveConfig } from "../config.js";
9
7
  import { EXIT_OK, EXIT_USER } from "../constants.js";
10
8
  import { CannotDeleteMainOrDevError, NotRepoError } from "../errors.js";
11
- import {
12
- branchList,
13
- deleteBranch,
14
- resolveRepoRoot,
15
- } from "../git.js";
9
+ import { branchList, deleteBranch, resolveRepoRoot } from "../git.js";
16
10
  import { hint, success } from "../out.js";
11
+ import type { BranchType, ParsedArgs } from "../types.js";
17
12
 
18
- const BRANCH_TYPES: BranchType[] = [
19
- "feature",
20
- "bugfix",
21
- "chore",
22
- "release",
23
- "hotfix",
24
- "spike",
25
- ];
13
+ const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
26
14
 
27
15
  /**
28
16
  * Returns local branch names that match any workflow prefix (feature/, bugfix/, etc.).
29
17
  */
30
18
  function getWorkflowBranches(
31
19
  allBranches: string[],
32
- prefixes: Record<BranchType, string>
20
+ prefixes: Record<BranchType, string>,
33
21
  ): string[] {
34
22
  const prefixed = BRANCH_TYPES.map((t) => prefixes[t]).filter(Boolean);
35
- return allBranches.filter((b) =>
36
- prefixed.some((p) => p && b.startsWith(p))
37
- );
23
+ return allBranches.filter((b) => prefixed.some((p) => p && b.startsWith(p)));
38
24
  }
39
25
 
40
26
  /**
@@ -58,16 +44,12 @@ export async function run(args: ParsedArgs): Promise<void> {
58
44
  });
59
45
  const { main, dev, prefixes } = config;
60
46
 
61
- const fromPositionals = (rawBranchNames ?? [])
62
- .map((s) => s.trim())
63
- .filter(Boolean);
47
+ const fromPositionals = (rawBranchNames ?? []).map((s) => s.trim()).filter(Boolean);
64
48
 
65
49
  if (fromPositionals.length > 0) {
66
50
  for (const branch of fromPositionals) {
67
51
  if (branch === main || branch === dev) {
68
- throw new CannotDeleteMainOrDevError(
69
- `Cannot delete the long-lived branch '${branch}'.`
70
- );
52
+ throw new CannotDeleteMainOrDevError(`Cannot delete the long-lived branch '${branch}'.`);
71
53
  }
72
54
  }
73
55
  for (const branch of fromPositionals) {
@@ -89,7 +71,7 @@ export async function run(args: ParsedArgs): Promise<void> {
89
71
  const isTTY = typeof process.stdin.isTTY === "boolean" && process.stdin.isTTY;
90
72
  if (!isTTY) {
91
73
  console.error(
92
- "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."
74
+ "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.",
93
75
  );
94
76
  process.exit(EXIT_USER);
95
77
  }
@@ -103,7 +85,7 @@ export async function run(args: ParsedArgs): Promise<void> {
103
85
  if (workflowBranches.length === 0) {
104
86
  if (!quiet) {
105
87
  console.error(
106
- "No workflow branches to delete. Create one with 'gflows start <type> <name>'."
88
+ "No workflow branches to delete. Create one with 'gflows start <type> <name>'.",
107
89
  );
108
90
  }
109
91
  process.exit(EXIT_USER);
@@ -124,9 +106,7 @@ export async function run(args: ParsedArgs): Promise<void> {
124
106
 
125
107
  for (const branch of chosen) {
126
108
  if (branch === main || branch === dev) {
127
- throw new CannotDeleteMainOrDevError(
128
- `Cannot delete the long-lived branch '${branch}'.`
129
- );
109
+ throw new CannotDeleteMainOrDevError(`Cannot delete the long-lived branch '${branch}'.`);
130
110
  }
131
111
  }
132
112