gflows 0.1.17 → 1.0.0

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 (48) hide show
  1. package/AGENTS.md +78 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +279 -502
  4. package/llms.txt +22 -0
  5. package/package.json +30 -23
  6. package/src/cli.ts +21 -364
  7. package/src/commands/abort.ts +39 -0
  8. package/src/commands/bump.ts +10 -10
  9. package/src/commands/completion.ts +14 -4
  10. package/src/commands/config.ts +123 -0
  11. package/src/commands/continue.ts +40 -0
  12. package/src/commands/delete.ts +4 -4
  13. package/src/commands/doctor.ts +143 -0
  14. package/src/commands/finish.ts +363 -137
  15. package/src/commands/help.ts +29 -20
  16. package/src/commands/init.ts +99 -18
  17. package/src/commands/list.ts +6 -1
  18. package/src/commands/mcp.ts +238 -0
  19. package/src/commands/pr.ts +120 -0
  20. package/src/commands/schema.ts +98 -0
  21. package/src/commands/start.ts +33 -31
  22. package/src/commands/status.ts +70 -51
  23. package/src/commands/switch.ts +50 -20
  24. package/src/commands/sync.ts +160 -0
  25. package/src/commands/undo.ts +78 -0
  26. package/src/commands/version.ts +3 -15
  27. package/src/commands/viz.ts +18 -0
  28. package/src/dispatch.ts +21 -0
  29. package/src/errors.ts +55 -12
  30. package/src/flow.ts +157 -0
  31. package/src/git.ts +135 -8
  32. package/src/index.ts +24 -1
  33. package/src/interactive.ts +209 -0
  34. package/src/out.ts +11 -4
  35. package/src/package-scripts.ts +73 -0
  36. package/src/parse.ts +430 -0
  37. package/src/prompts.ts +89 -0
  38. package/src/recommend.ts +132 -0
  39. package/src/run-state.ts +165 -0
  40. package/src/tui/HubHome.tsx +343 -0
  41. package/src/tui/HubShell.tsx +186 -0
  42. package/src/tui/flows.tsx +140 -0
  43. package/src/tui/hub.ts +89 -0
  44. package/src/tui/prompts.tsx +207 -0
  45. package/src/types.ts +63 -4
  46. package/src/ui.ts +132 -0
  47. package/src/version.ts +24 -0
  48. package/src/viz.ts +294 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Print a machine-readable catalog of gflows commands, flags, types, and exit codes.
3
+ * @module commands/schema
4
+ */
5
+
6
+ import type { ParsedArgs } from "../types.js";
7
+ import { ALL_COMMANDS, BRANCH_TYPE_SHORTS } from "../types.js";
8
+
9
+ /**
10
+ * Emits JSON schema for agents and tooling.
11
+ */
12
+ export async function run(_args: ParsedArgs): Promise<void> {
13
+ const schema = {
14
+ name: "gflows",
15
+ version: "1.0.0",
16
+ description: "Git branching workflow CLI (main + dev + typed short-lived branches)",
17
+ exitCodes: {
18
+ "0": "success",
19
+ "1": "usage/validation",
20
+ "2": "git/system",
21
+ },
22
+ branchTypes: Object.entries(BRANCH_TYPE_SHORTS).map(([type, short]) => ({
23
+ type,
24
+ short,
25
+ })),
26
+ commands: ALL_COMMANDS.map((command) => ({
27
+ command,
28
+ ...commandMeta(command),
29
+ })),
30
+ flags: {
31
+ common: [
32
+ "-C/--path",
33
+ "-p/--push",
34
+ "-P/--no-push",
35
+ "-y/--yes",
36
+ "-d/--dry-run",
37
+ "-v/--verbose",
38
+ "-q/--quiet",
39
+ "--force",
40
+ "--json",
41
+ "--main",
42
+ "--dev",
43
+ "-R/--remote",
44
+ ],
45
+ finish: [
46
+ "-B/--branch",
47
+ "-D/--delete",
48
+ "-N/--no-delete",
49
+ "--no-ff",
50
+ "--squash",
51
+ "--preview",
52
+ "--bump",
53
+ "-s/--sign",
54
+ "-T/--no-tag",
55
+ "-M/--tag-message",
56
+ "-m/--message",
57
+ ],
58
+ start: ["-o/--from", "--force", "-p/--push"],
59
+ sync: ["--rebase", "--force"],
60
+ list: ["-r/--include-remote", "--json"],
61
+ status: ["--json"],
62
+ },
63
+ agentNotes: [
64
+ "Always pass explicit flags in non-TTY (never assume interactive hub).",
65
+ "Prefer -y to accept finish plan (delete defaults on); pass -P or -p for push.",
66
+ "On conflict: gflows continue | gflows abort | gflows undo.",
67
+ "Use gflows schema and AGENTS.md as source of truth.",
68
+ ],
69
+ };
70
+ console.log(JSON.stringify(schema, null, 2));
71
+ }
72
+
73
+ function commandMeta(command: string): { summary: string; interactiveOk: boolean } {
74
+ const map: Record<string, { summary: string; interactiveOk: boolean }> = {
75
+ init: { summary: "Ensure main; create dev", interactiveOk: true },
76
+ start: { summary: "Create workflow branch", interactiveOk: true },
77
+ finish: { summary: "Merge and close workflow branch", interactiveOk: true },
78
+ switch: { summary: "Switch workflow branch", interactiveOk: true },
79
+ delete: { summary: "Delete local workflow branches", interactiveOk: true },
80
+ list: { summary: "List workflow branches", interactiveOk: false },
81
+ bump: { summary: "Bump package version", interactiveOk: true },
82
+ sync: { summary: "Update branch from base", interactiveOk: true },
83
+ pr: { summary: "Open PR/MR via gh/glab", interactiveOk: true },
84
+ viz: { summary: "Visual branch map and flow diagram", interactiveOk: true },
85
+ doctor: { summary: "Repo health checks", interactiveOk: false },
86
+ config: { summary: "Get/set .gflows.json", interactiveOk: true },
87
+ schema: { summary: "Machine-readable command catalog", interactiveOk: false },
88
+ continue: { summary: "Resume suspended operation", interactiveOk: false },
89
+ undo: { summary: "Undo last completed operation", interactiveOk: false },
90
+ abort: { summary: "Abort suspended operation", interactiveOk: false },
91
+ completion: { summary: "Shell completion script", interactiveOk: false },
92
+ status: { summary: "Current branch flow info", interactiveOk: false },
93
+ help: { summary: "Usage", interactiveOk: false },
94
+ version: { summary: "CLI version", interactiveOk: false },
95
+ mcp: { summary: "Start MCP server for agents", interactiveOk: false },
96
+ };
97
+ return map[command] ?? { summary: command, interactiveOk: false };
98
+ }
@@ -6,7 +6,13 @@
6
6
 
7
7
  import { getPrefixForType, resolveConfig } from "../config.js";
8
8
  import { EXIT_USER, VERSION_REGEX } from "../constants.js";
9
- import { BranchNotFoundError, DirtyWorkingTreeError, InvalidVersionError } from "../errors.js";
9
+ import {
10
+ BranchExistsError,
11
+ BranchNotFoundError,
12
+ DirtyWorkingTreeError,
13
+ InvalidVersionError,
14
+ } from "../errors.js";
15
+ import { getBaseBranchName } from "../flow.js";
10
16
  import {
11
17
  assertNoRebaseOrMerge,
12
18
  assertNotDetached,
@@ -19,35 +25,30 @@ import {
19
25
  runGit,
20
26
  validateBranchName,
21
27
  } from "../git.js";
28
+ import { promptStartArgs } from "../interactive.js";
22
29
  import { hint, success } from "../out.js";
23
- import type { BranchType, ParsedArgs } from "../types.js";
24
-
25
- /**
26
- * Returns the base branch name for the given type and fromMain flag (main vs dev).
27
- */
28
- function getBaseBranch(type: BranchType, fromMain: boolean, main: string, dev: string): string {
29
- if (type === "hotfix") return main;
30
- if (type === "bugfix" && fromMain) return main;
31
- return dev;
32
- }
30
+ import type { ParsedArgs } from "../types.js";
33
31
 
34
32
  /**
35
33
  * Runs the start command: validate pre-conditions, ensure base exists, create branch, optional push.
36
- * Pre-checks: repo is git, not detached HEAD, no rebase/merge in progress, working tree clean (or --force), base exists.
37
- * Requires type and name (exit 1 if missing). For release/hotfix, name must match vX.Y.Z or X.Y.Z.
38
- *
39
- * @param args - Parsed CLI args (cwd, type, name, push, noPush, remote, dryRun, verbose, quiet, force, fromMain).
40
34
  */
41
35
  export async function run(args: ParsedArgs): Promise<void> {
42
- if (!args.type || args.name === undefined || args.name === "") {
36
+ let type = args.type;
37
+ let name = args.name?.trim();
38
+
39
+ if ((!type || !name) && process.stdin.isTTY) {
40
+ const prompted = await promptStartArgs();
41
+ type = type ?? prompted.type;
42
+ name = name || prompted.name;
43
+ }
44
+
45
+ if (!type || name === undefined || name === "") {
43
46
  console.error(
44
47
  "gflows start: requires type and name (e.g. gflows start feature my-feat). Use 'gflows help' for usage.",
45
48
  );
46
49
  process.exit(EXIT_USER);
47
50
  }
48
51
 
49
- const type = args.type;
50
- const name = args.name.trim();
51
52
  const repoRoot = await resolveRepoRoot(args.cwd);
52
53
  const config = resolveConfig(
53
54
  repoRoot,
@@ -60,11 +61,9 @@ export async function run(args: ParsedArgs): Promise<void> {
60
61
  verbose: args.verbose,
61
62
  };
62
63
 
63
- // Pre-checks: not detached HEAD, no rebase/merge in progress
64
64
  await assertNotDetached(repoRoot);
65
65
  assertNoRebaseOrMerge(repoRoot);
66
66
 
67
- // Working tree clean or --force
68
67
  if (!args.force) {
69
68
  const clean = await isClean(repoRoot, { dryRun: false, verbose: opts.verbose });
70
69
  if (!clean) {
@@ -72,7 +71,6 @@ export async function run(args: ParsedArgs): Promise<void> {
72
71
  }
73
72
  }
74
73
 
75
- // Validate name: version for release/hotfix, branch name for others
76
74
  if (type === "release" || type === "hotfix") {
77
75
  if (!VERSION_REGEX.test(name)) {
78
76
  throw new InvalidVersionError(
@@ -83,15 +81,18 @@ export async function run(args: ParsedArgs): Promise<void> {
83
81
  validateBranchName(name);
84
82
  }
85
83
 
86
- const base = getBaseBranch(type, args.fromMain, config.main, config.dev);
84
+ const fromMain =
85
+ args.fromMain ||
86
+ (typeof args.from === "string" && (args.from === config.main || args.from === "main"));
87
+
88
+ const base =
89
+ typeof args.from === "string" && args.from !== "main" && args.from !== config.main
90
+ ? args.from
91
+ : getBaseBranchName(type, fromMain, config);
87
92
 
88
- // Ensure base exists (local or create from remote after fetch)
89
- let _baseExists = false;
90
93
  try {
91
94
  await revParse(repoRoot, base, [], { dryRun: false, verbose: opts.verbose });
92
- _baseExists = true;
93
95
  } catch {
94
- // Fetch and try remote ref
95
96
  await fetch(repoRoot, config.remote, opts);
96
97
  const remoteRef = `${config.remote}/${base}`;
97
98
  try {
@@ -99,7 +100,6 @@ export async function run(args: ParsedArgs): Promise<void> {
99
100
  if (!opts.dryRun) {
100
101
  await runGit(["branch", base, remoteRef], { cwd: repoRoot, ...opts, dryRun: false });
101
102
  }
102
- _baseExists = true;
103
103
  } catch {
104
104
  throw new BranchNotFoundError(
105
105
  `Base branch '${base}' not found locally or on ${config.remote}. Create it or push it first.`,
@@ -112,8 +112,9 @@ export async function run(args: ParsedArgs): Promise<void> {
112
112
 
113
113
  const branches = await branchList(repoRoot, { ...opts, dryRun: false });
114
114
  if (branches.includes(fullBranchName)) {
115
- throw new BranchNotFoundError(
116
- `Branch '${fullBranchName}' already exists. Use a different name or switch to it.`,
115
+ throw new BranchExistsError(
116
+ `Branch '${fullBranchName}' already exists.`,
117
+ `Use a different name, or: gflows switch ${fullBranchName}`,
117
118
  );
118
119
  }
119
120
 
@@ -138,7 +139,8 @@ export async function run(args: ParsedArgs): Promise<void> {
138
139
  }
139
140
 
140
141
  if (!args.quiet && !args.dryRun) {
141
- // Hint: suggest next step — merge branch when done
142
- hint(`When done, run gflows finish ${type} to merge into the target branch.`);
142
+ hint(
143
+ `Commit your work, then gflows sync or gflows finish ${type}. Interactive: just run gflows`,
144
+ );
143
145
  }
144
146
  }
@@ -1,59 +1,26 @@
1
1
  /**
2
- * Status command: show current branch flow info.
3
- * Classifies branch (feature/bugfix/chore/release/hotfix/spike or unknown),
4
- * shows base and merge target(s), and optionally ahead/behind vs base.
5
- * No write operations.
2
+ * Status command: show current branch flow info and recovery hints.
6
3
  * @module commands/status
7
4
  */
8
5
 
9
- import { getBranchTypeMeta, resolveConfig } from "../config.js";
6
+ import { resolveConfig } from "../config.js";
10
7
  import { NotRepoError } from "../errors.js";
8
+ import {
9
+ classifyBranch,
10
+ formatMergeTarget,
11
+ getBaseBranchName,
12
+ resolveMergeTarget,
13
+ } from "../flow.js";
11
14
  import { getAheadBehind, getCurrentBranch, resolveRepoRoot } from "../git.js";
12
15
  import { hint } from "../out.js";
13
- import type { BranchType, ParsedArgs, ResolvedConfig } from "../types.js";
14
-
15
- const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
16
-
17
- /**
18
- * Classifies a branch name into a workflow type, "main", "dev", or null (unknown).
19
- * Uses resolved config for main/dev names and prefixes.
20
- */
21
- function classifyBranch(
22
- branchName: string,
23
- config: ResolvedConfig,
24
- ): BranchType | "main" | "dev" | null {
25
- if (branchName === config.main) return "main";
26
- if (branchName === config.dev) return "dev";
27
- const { prefixes } = config;
28
- for (const type of BRANCH_TYPES) {
29
- const prefix = prefixes[type];
30
- if (prefix && branchName.startsWith(prefix)) {
31
- return type;
32
- }
33
- }
34
- return null;
35
- }
36
-
37
- /**
38
- * Formats merge target for display (actual branch names).
39
- */
40
- function formatMergeTarget(
41
- mergeTarget: "main" | "dev" | "main-then-dev",
42
- config: ResolvedConfig,
43
- ): string {
44
- if (mergeTarget === "main-then-dev") {
45
- return `${config.main}, then ${config.dev}`;
46
- }
47
- return mergeTarget === "main" ? config.main : config.dev;
48
- }
16
+ import { readActiveRun } from "../run-state.js";
17
+ import type { ParsedArgs } from "../types.js";
49
18
 
50
19
  /**
51
20
  * Runs the status command.
52
- * Shows current branch, classification, base, merge target(s), and ahead/behind vs base.
53
- * Output goes to stdout for scripts. On detached HEAD or non-repo, reports clearly and exits.
54
21
  */
55
22
  export async function run(args: ParsedArgs): Promise<void> {
56
- const { cwd, dryRun, verbose, quiet } = args;
23
+ const { cwd, dryRun, verbose, quiet, json } = args;
57
24
 
58
25
  const root = await resolveRepoRoot(cwd).catch((err: unknown) => {
59
26
  if (err instanceof NotRepoError) throw err;
@@ -70,22 +37,62 @@ export async function run(args: ParsedArgs): Promise<void> {
70
37
  verbose: !!verbose,
71
38
  });
72
39
 
40
+ const active = readActiveRun(root);
41
+
42
+ if (json) {
43
+ const payload: Record<string, unknown> = {
44
+ branch: current,
45
+ suspended: active
46
+ ? { command: active.command, status: active.status, nextStep: active.nextStep }
47
+ : null,
48
+ };
49
+ if (current) {
50
+ const classification = classifyBranch(current, config);
51
+ payload.type = classification;
52
+ if (classification && classification !== "main" && classification !== "dev") {
53
+ const mergeTarget = await resolveMergeTarget(root, current, classification, config, {
54
+ dryRun: !!dryRun,
55
+ verbose: !!verbose,
56
+ });
57
+ const base = getBaseBranchName(classification, false, config);
58
+ const ab = await getAheadBehind(root, base, current, {
59
+ dryRun: !!dryRun,
60
+ verbose: !!verbose,
61
+ });
62
+ payload.base = base;
63
+ payload.mergeTarget = mergeTarget;
64
+ payload.mergeTargetDisplay = formatMergeTarget(mergeTarget, config);
65
+ payload.ahead = ab.ahead;
66
+ payload.behind = ab.behind;
67
+ }
68
+ }
69
+ console.log(JSON.stringify(payload, null, 2));
70
+ return;
71
+ }
72
+
73
+ if (active && !quiet) {
74
+ console.log(`Suspended: ${active.command} (${active.status})`);
75
+ hint("Run gflows continue after resolving conflicts, or gflows abort / undo.");
76
+ }
77
+
73
78
  if (current === null) {
74
79
  if (!quiet) {
75
80
  console.log("HEAD is detached.");
81
+ hint("Try: git checkout dev");
76
82
  }
77
83
  return;
78
84
  }
79
85
 
80
- const classification = classifyBranch(current, config);
81
-
82
86
  if (!quiet) {
83
87
  console.log(`Branch: ${current}`);
84
88
  }
85
89
 
90
+ const classification = classifyBranch(current, config);
91
+
86
92
  if (classification === "main") {
87
93
  if (!quiet) {
88
94
  console.log("Type: long-lived (main)");
95
+ hint("Run gflows start feature <name> or gflows start hotfix vX.Y.Z");
89
96
  }
90
97
  return;
91
98
  }
@@ -93,6 +100,7 @@ export async function run(args: ParsedArgs): Promise<void> {
93
100
  if (classification === "dev") {
94
101
  if (!quiet) {
95
102
  console.log("Type: long-lived (dev)");
103
+ hint("Run gflows start feature <name> to begin work.");
96
104
  }
97
105
  return;
98
106
  }
@@ -100,13 +108,21 @@ export async function run(args: ParsedArgs): Promise<void> {
100
108
  if (classification === null) {
101
109
  if (!quiet) {
102
110
  console.log("Type: unknown");
111
+ hint("Use a typed prefix (feature/, bugfix/, …) or gflows start …");
103
112
  }
104
113
  return;
105
114
  }
106
115
 
107
- const meta = getBranchTypeMeta(classification);
108
- const baseBranch = meta.base === "main" ? config.main : config.dev;
109
- const mergeTargetDisplay = formatMergeTarget(meta.mergeTarget, config);
116
+ const mergeTarget = await resolveMergeTarget(root, current, classification, config, {
117
+ dryRun: !!dryRun,
118
+ verbose: !!verbose,
119
+ });
120
+ const baseBranch = getBaseBranchName(
121
+ classification,
122
+ mergeTarget === "main-then-dev" && classification === "bugfix",
123
+ config,
124
+ );
125
+ const mergeTargetDisplay = formatMergeTarget(mergeTarget, config);
110
126
 
111
127
  if (!quiet) {
112
128
  console.log(`Type: ${classification}`);
@@ -121,7 +137,10 @@ export async function run(args: ParsedArgs): Promise<void> {
121
137
 
122
138
  if (!quiet) {
123
139
  console.log(`Ahead/behind: ${ahead} ahead, ${behind} behind`);
124
- // Hint: suggest next step — finish current branch
125
- hint(`Run gflows finish ${classification} to merge into ${mergeTargetDisplay}.`);
140
+ if (ahead === 0) {
141
+ hint("No commits to finish yet commit changes first.");
142
+ } else {
143
+ hint(`Run gflows finish ${classification} to merge into ${mergeTargetDisplay}.`);
144
+ }
126
145
  }
127
146
  }
@@ -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
 
@@ -91,19 +92,14 @@ export async function run(args: ParsedArgs): Promise<void> {
91
92
  dryRun,
92
93
  verbose: args.verbose,
93
94
  });
94
- const { select } = await import("@inquirer/prompts");
95
- const chosen = await select({
95
+ const { selectPrompt } = await import("../prompts.js");
96
+ targetBranch = await selectPrompt({
96
97
  message: "Switch to branch",
97
- choices: choices.map((b) => ({
98
- name: currentBranchForPicker && b === currentBranchForPicker ? `${b} (current)` : b,
98
+ options: choices.map((b) => ({
99
+ label: currentBranchForPicker && b === currentBranchForPicker ? `${b} (current)` : b,
99
100
  value: b,
100
101
  })),
101
102
  });
102
-
103
- if (typeof chosen !== "string") {
104
- process.exit(EXIT_USER);
105
- }
106
- targetBranch = chosen;
107
103
  }
108
104
 
109
105
  const gitOpts = { dryRun, verbose: args.verbose };
@@ -114,17 +110,21 @@ export async function run(args: ParsedArgs): Promise<void> {
114
110
  if (args.switchMode !== undefined) {
115
111
  whenUncommitted = args.switchMode;
116
112
  } else if (!treeClean && isTTY) {
117
- const { select: selectPrompt } = await import("@inquirer/prompts");
118
- whenUncommitted = await selectPrompt({
113
+ const { selectPrompt } = await import("../prompts.js");
114
+ whenUncommitted = await selectPrompt<"move" | "restore" | "clean" | "destroy" | "cancel">({
119
115
  message: "Working tree has uncommitted changes. What do you want to do?",
120
- choices: [
121
- { name: "Move — Move current changes to the target branch", value: "move" as const },
116
+ options: [
117
+ { label: "Move — Move current changes to the target branch", value: "move" },
122
118
  {
123
- name: "Restore — Save changes for this branch; restore target's saved state (if any)",
124
- value: "restore" as const,
119
+ label: "Restore — Save changes for this branch; restore target's saved state (if any)",
120
+ value: "restore",
125
121
  },
126
- { name: "Clean — Discard changes and switch clean at HEAD", value: "clean" as const },
127
- { name: "Cancel — Abort switching", value: "cancel" as const },
122
+ { label: "Clean — Discard changes and switch clean at HEAD", value: "clean" },
123
+ {
124
+ label: "Destroy — Delete current branch and switch to the target branch",
125
+ value: "destroy",
126
+ },
127
+ { label: "Cancel — Abort switching", value: "cancel" },
128
128
  ],
129
129
  });
130
130
  } else if (!treeClean) {
@@ -139,6 +139,31 @@ export async function run(args: ParsedArgs): Promise<void> {
139
139
  process.exit(EXIT_USER);
140
140
  }
141
141
 
142
+ let branchToDestroy: string | undefined;
143
+ if (whenUncommitted === "destroy") {
144
+ const currentBranch = await getCurrentBranch(root, gitOpts);
145
+ if (!currentBranch) {
146
+ console.error("gflows switch: cannot destroy when HEAD is detached.");
147
+ process.exit(EXIT_USER);
148
+ }
149
+ if (currentBranch === config.main || currentBranch === config.dev) {
150
+ throw new CannotDeleteMainOrDevError(
151
+ `Cannot destroy the long-lived branch '${currentBranch}'.`,
152
+ );
153
+ }
154
+ if (currentBranch === targetBranch) {
155
+ console.error(
156
+ "gflows switch: cannot destroy current branch when switching to the same branch.",
157
+ );
158
+ process.exit(EXIT_USER);
159
+ }
160
+ branchToDestroy = currentBranch;
161
+ if (!treeClean) {
162
+ await restoreTracked(root, gitOpts);
163
+ await cleanUntracked(root, gitOpts);
164
+ }
165
+ }
166
+
142
167
  if (!treeClean && whenUncommitted === "move") {
143
168
  await stashPushMove(root, gitOpts);
144
169
  }
@@ -210,7 +235,12 @@ export async function run(args: ParsedArgs): Promise<void> {
210
235
  await tryRestoreTargetStash();
211
236
  }
212
237
 
213
- if (!quiet && !dryRun) {
238
+ if (branchToDestroy) {
239
+ await deleteBranch(root, branchToDestroy, gitOpts);
240
+ if (!quiet && !dryRun) {
241
+ success(`Deleted branch '${branchToDestroy}' and switched to '${targetBranch}'.`);
242
+ }
243
+ } else if (!quiet && !dryRun) {
214
244
  success(`Switched to branch '${targetBranch}'.`);
215
245
  hint("Use gflows list to see all workflow branches.");
216
246
  }