gflows 0.1.16 → 0.1.18

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/README.md CHANGED
@@ -265,7 +265,7 @@ Release and hotfix names must be a version (`vX.Y.Z` or `X.Y.Z`). Branch names u
265
265
  | `init` | `-I` | Ensure main exists; create dev from main. |
266
266
  | `start` | `-S` | Create a workflow branch (requires type + name). |
267
267
  | `finish` | `-F` | Merge branch into target(s), optional tag (release/hotfix), delete, push. |
268
- | `switch` | `-W` | Switch to a workflow branch (picker or name); with uncommitted changes: prompt or `--move` / `--restore` / `--clean` / `--cancel`. |
268
+ | `switch` | `-W` | Switch to a workflow branch (picker or name); with uncommitted changes: prompt or `--move` / `--restore` / `--clean` / `--destroy` / `--cancel`. |
269
269
  | `delete` | `-L` | Delete local workflow branch(es). Never main/dev. |
270
270
  | `list` | `-l` | List workflow branches; optional type filter and remote. |
271
271
  | `bump` | — | Bump or rollback package version (patch/minor/major). |
@@ -404,6 +404,7 @@ Switch to a workflow branch. With TTY and no branch name, shows a picker; otherw
404
404
  | **Move** | Move current changes to the target branch. |
405
405
  | **Restore** | Save changes for this branch; restore target's saved state (if any). |
406
406
  | **Clean** | Discard changes and switch clean at HEAD. |
407
+ | **Destroy** | Delete current branch and switch to the target branch (cannot destroy main or dev). |
407
408
  | **Cancel** | Abort switching. |
408
409
 
409
410
  You can skip the prompt by passing exactly one of the flags below. If the target branch has saved changes and you use **Clean**, a warning is shown (unless `-q`).
@@ -415,6 +416,7 @@ bun gflows switch
415
416
  bun gflows switch feature/auth-refactor
416
417
  bun gflows switch dev --restore
417
418
  bun gflows switch main --clean
419
+ bun gflows switch dev --destroy
418
420
  bun gflows -W feature/auth-refactor
419
421
  ```
420
422
 
@@ -428,6 +430,7 @@ bun gflows -W feature/auth-refactor
428
430
  | `--move` | — | Move current changes to the target branch; no prompt. |
429
431
  | `--restore` | — | Save for this branch; restore target's saved state (if any); no prompt. |
430
432
  | `--clean` | — | Discard changes and switch clean at HEAD; no prompt. |
433
+ | `--destroy` | — | Delete current branch and switch to target; no prompt (not main/dev). |
431
434
  | `--cancel` | — | Abort switching; no prompt. |
432
435
  | `--verbose` | `-v` | Verbose output. |
433
436
  | `--quiet` | `-q` | Minimal output (suppresses Clean warning about saved changes on target). |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gflows",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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",
@@ -14,6 +14,7 @@
14
14
  "lint": "biome check .",
15
15
  "lint:fix": "biome check . --write",
16
16
  "format": "biome format . --write",
17
+ "check": "bun run test && bun run typecheck && bun run lint",
17
18
  "gflows": "bun run src/cli.ts",
18
19
  "publish:all": "bun run scripts/publish.ts",
19
20
  "publish:npm": "bun run scripts/publish.ts -- --npm-only",
package/src/cli.ts CHANGED
@@ -95,11 +95,12 @@ function buildParseArgsOptions() {
95
95
  // list (-r is context-dependent: list → include-remote; start/finish → release)
96
96
  includeRemote: { type: "boolean" as const },
97
97
  "include-remote": { type: "boolean" as const },
98
- // switch: explicit mode (--restore, --clean, --cancel, --move)
98
+ // switch: explicit mode (--restore, --clean, --cancel, --move, --destroy)
99
99
  restore: { type: "boolean" as const },
100
100
  clean: { type: "boolean" as const },
101
101
  cancel: { type: "boolean" as const },
102
102
  move: { type: "boolean" as const },
103
+ destroy: { type: "boolean" as const },
103
104
  },
104
105
  };
105
106
  }
@@ -317,16 +318,17 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
317
318
  else if (command === "completion" && name === "zsh") completionShell = "zsh";
318
319
  else if (command === "completion" && name === "fish") completionShell = "fish";
319
320
 
320
- let switchMode: "restore" | "clean" | "cancel" | "move" | undefined;
321
+ let switchMode: "restore" | "clean" | "cancel" | "move" | "destroy" | undefined;
321
322
  if (command === "switch") {
322
323
  const restore = v.restore === true;
323
324
  const clean = v.clean === true;
324
325
  const cancel = v.cancel === true;
325
326
  const move = v.move === true;
326
- const count = [restore, clean, cancel, move].filter(Boolean).length;
327
+ const destroy = v.destroy === true;
328
+ const count = [restore, clean, cancel, move, destroy].filter(Boolean).length;
327
329
  if (count > 1) {
328
330
  console.error(
329
- "gflows switch: only one of --restore, --clean, --cancel, or --move may be used at a time.",
331
+ "gflows switch: only one of --restore, --clean, --cancel, --move, or --destroy may be used at a time.",
330
332
  );
331
333
  process.exit(EXIT_USER);
332
334
  }
@@ -334,6 +336,7 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
334
336
  else if (clean) switchMode = "clean";
335
337
  else if (cancel) switchMode = "cancel";
336
338
  else if (move) switchMode = "move";
339
+ else if (destroy) switchMode = "destroy";
337
340
  }
338
341
 
339
342
  return {
@@ -160,15 +160,16 @@ function writePackageVersion(dir: string, newVersion: string): void {
160
160
  }
161
161
 
162
162
  /**
163
- * Updates version in jsr.json if the file exists; preserves other keys.
163
+ * Updates version in jsr.json if the file exists. Only the version value is changed;
164
+ * the rest of the file (format, commas, spacing) is left unchanged.
164
165
  */
165
166
  function syncJsrVersion(dir: string, newVersion: string): boolean {
166
167
  const path = join(dir, JSR_JSON);
167
168
  if (!existsSync(path)) return false;
168
169
  const raw = readFileSync(path, "utf-8");
169
- const data = JSON.parse(raw) as Record<string, unknown>;
170
- data.version = newVersion;
171
- writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
170
+ const updated = raw.replace(/"version":\s*"[^"]*"/, `"version": "${newVersion}"`);
171
+ if (updated === raw) return false;
172
+ writeFileSync(path, updated, "utf-8");
172
173
  return true;
173
174
  }
174
175
 
@@ -51,6 +51,7 @@ List: -r, --include-remote Include remote-tracking branches
51
51
  Switch: --move Move current changes to the target branch
52
52
  --restore Save for this branch; restore target's saved state (if any)
53
53
  --clean Discard changes and switch clean at HEAD
54
+ --destroy Delete current branch and switch to target (not main/dev)
54
55
  --cancel Abort switching
55
56
 
56
57
  Exit codes: 0 success, 1 usage/validation, 2 Git or system error.
@@ -5,11 +5,12 @@
5
5
 
6
6
  import { resolveConfig } from "../config.js";
7
7
  import { EXIT_GIT, EXIT_OK, EXIT_USER } from "../constants.js";
8
- import { NotRepoError } from "../errors.js";
8
+ import { CannotDeleteMainOrDevError, NotRepoError } from "../errors.js";
9
9
  import {
10
10
  branchList,
11
11
  checkout,
12
12
  cleanUntracked,
13
+ deleteBranch,
13
14
  findStashRefByBranch,
14
15
  findStashRefByMessage,
15
16
  getCurrentBranch,
@@ -26,7 +27,7 @@ import { hint, success } from "../out.js";
26
27
  import type { BranchType, ParsedArgs } from "../types.js";
27
28
 
28
29
  /** User choice when switching with uncommitted changes. */
29
- type SwitchWhenUncommitted = "cancel" | "restore" | "clean" | "move";
30
+ type SwitchWhenUncommitted = "cancel" | "restore" | "clean" | "move" | "destroy";
30
31
 
31
32
  const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
32
33
 
@@ -87,10 +88,17 @@ export async function run(args: ParsedArgs): Promise<void> {
87
88
  process.exit(EXIT_OK);
88
89
  }
89
90
 
91
+ const currentBranchForPicker = await getCurrentBranch(root, {
92
+ dryRun,
93
+ verbose: args.verbose,
94
+ });
90
95
  const { select } = await import("@inquirer/prompts");
91
96
  const chosen = await select({
92
97
  message: "Switch to branch",
93
- choices: choices.map((b) => ({ name: b, value: b })),
98
+ choices: choices.map((b) => ({
99
+ name: currentBranchForPicker && b === currentBranchForPicker ? `${b} (current)` : b,
100
+ value: b,
101
+ })),
94
102
  });
95
103
 
96
104
  if (typeof chosen !== "string") {
@@ -117,6 +125,10 @@ export async function run(args: ParsedArgs): Promise<void> {
117
125
  value: "restore" as const,
118
126
  },
119
127
  { name: "Clean — Discard changes and switch clean at HEAD", value: "clean" as const },
128
+ {
129
+ name: "Destroy — Delete current branch and switch to the target branch",
130
+ value: "destroy" as const,
131
+ },
120
132
  { name: "Cancel — Abort switching", value: "cancel" as const },
121
133
  ],
122
134
  });
@@ -132,6 +144,31 @@ export async function run(args: ParsedArgs): Promise<void> {
132
144
  process.exit(EXIT_USER);
133
145
  }
134
146
 
147
+ let branchToDestroy: string | undefined;
148
+ if (whenUncommitted === "destroy") {
149
+ const currentBranch = await getCurrentBranch(root, gitOpts);
150
+ if (!currentBranch) {
151
+ console.error("gflows switch: cannot destroy when HEAD is detached.");
152
+ process.exit(EXIT_USER);
153
+ }
154
+ if (currentBranch === config.main || currentBranch === config.dev) {
155
+ throw new CannotDeleteMainOrDevError(
156
+ `Cannot destroy the long-lived branch '${currentBranch}'.`,
157
+ );
158
+ }
159
+ if (currentBranch === targetBranch) {
160
+ console.error(
161
+ "gflows switch: cannot destroy current branch when switching to the same branch.",
162
+ );
163
+ process.exit(EXIT_USER);
164
+ }
165
+ branchToDestroy = currentBranch;
166
+ if (!treeClean) {
167
+ await restoreTracked(root, gitOpts);
168
+ await cleanUntracked(root, gitOpts);
169
+ }
170
+ }
171
+
135
172
  if (!treeClean && whenUncommitted === "move") {
136
173
  await stashPushMove(root, gitOpts);
137
174
  }
@@ -181,26 +218,34 @@ export async function run(args: ParsedArgs): Promise<void> {
181
218
  }
182
219
  }
183
220
 
184
- if (whenUncommitted === "restore") {
221
+ const tryRestoreTargetStash = async (): Promise<void> => {
185
222
  const targetStashRef = await findStashRefByBranch(root, targetBranch, gitOpts);
186
- if (targetStashRef) {
187
- try {
188
- await stashPopRef(root, targetStashRef, gitOpts);
189
- } catch (err) {
190
- const msg = err instanceof Error ? err.message : String(err);
191
- console.error(
192
- `gflows switch: conflicts occurred while restoring saved changes for '${targetBranch}'.`,
193
- );
194
- console.error(
195
- "The stash was not dropped. Resolve conflicts, then commit or run `git stash drop` as needed.",
196
- );
197
- if (args.verbose && msg) console.error(msg);
198
- process.exit(EXIT_GIT);
199
- }
223
+ if (!targetStashRef) return;
224
+ try {
225
+ await stashPopRef(root, targetStashRef, gitOpts);
226
+ } catch (err) {
227
+ const msg = err instanceof Error ? err.message : String(err);
228
+ console.error(
229
+ `gflows switch: conflicts occurred while restoring saved changes for '${targetBranch}'.`,
230
+ );
231
+ console.error(
232
+ "The stash was not dropped. Resolve conflicts, then commit or run `git stash drop` as needed.",
233
+ );
234
+ if (args.verbose && msg) console.error(msg);
235
+ process.exit(EXIT_GIT);
200
236
  }
237
+ };
238
+
239
+ if (whenUncommitted === "restore" || whenUncommitted === undefined) {
240
+ await tryRestoreTargetStash();
201
241
  }
202
242
 
203
- if (!quiet && !dryRun) {
243
+ if (branchToDestroy) {
244
+ await deleteBranch(root, branchToDestroy, gitOpts);
245
+ if (!quiet && !dryRun) {
246
+ success(`Deleted branch '${branchToDestroy}' and switched to '${targetBranch}'.`);
247
+ }
248
+ } else if (!quiet && !dryRun) {
204
249
  success(`Switched to branch '${targetBranch}'.`);
205
250
  hint("Use gflows list to see all workflow branches.");
206
251
  }
package/src/types.ts CHANGED
@@ -120,5 +120,5 @@ export interface ParsedArgs {
120
120
  // list
121
121
  includeRemote: boolean;
122
122
  // switch: explicit mode when uncommitted (overrides prompt)
123
- switchMode?: "restore" | "clean" | "cancel" | "move";
123
+ switchMode?: "restore" | "clean" | "cancel" | "move" | "destroy";
124
124
  }