gh-stack 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Stacked PR manager for squash-merge workflows.
4
4
 
5
- Manages stacked pull requests with metadata stored in `.git/gh-stack-metadata.json`. Designed for repositories that use squash-merge (where tools like Graphite break down).
5
+ Manages stacked pull requests with metadata stored in `.git/gh-stack-metadata.json`. Designed for repositories that use squash-merge (where tools like Graphite break down). Inspired by [Graphite](https://graphite.dev/) (`gt`) but works directly against GitHub — no backend required.
6
6
 
7
7
  ## Install
8
8
 
@@ -22,68 +22,98 @@ bun install -g gh-stack
22
22
  - [GitHub CLI](https://cli.github.com/) (`gh`) — authenticated
23
23
  - Git 2.30+
24
24
 
25
+ ## Terms
26
+
27
+ | Term | Definition |
28
+ |------|-----------|
29
+ | **trunk** | Base branch of the repository (usually `main`) |
30
+ | **stack** | A chain of dependent branches |
31
+ | **upstack** | Branches that depend on the current branch (children) |
32
+ | **downstack** | Branches the current branch depends on (ancestors) |
33
+
25
34
  ## Quick Start
26
35
 
27
36
  ```bash
28
- # Create a stack from your current branch
29
- gh-stack init --name my-feature
37
+ # You have branches: main → feat-1 → feat-2
38
+ # Go to the top of the chain and init:
39
+ git checkout feat-2
40
+ gh-stack init # Auto-detects the chain!
30
41
 
31
- # Add more branches to the stack
32
- gh-stack add --create kiliman/pr2-WEB-1234
42
+ # Or start fresh:
43
+ git checkout kiliman/first-pr
44
+ gh-stack init # Creates stack with this branch
45
+ gh-stack create kiliman/second-pr # Branch off current, add to stack
46
+
47
+ # Push everything to GitHub and create PRs
48
+ gh-stack submit
33
49
 
34
50
  # View the stack
35
- gh-stack # tree view (default)
36
- gh-stack list # numbered list
51
+ gh-stack log # tree view (default)
52
+ gh-stack ls # numbered list
53
+
54
+ # Navigate
55
+ gh-stack up # move to child branch
56
+ gh-stack down # move to parent branch
57
+ gh-stack top # jump to tip of stack
58
+ gh-stack bottom # jump to base of stack
37
59
 
38
- # Sync with main and restack all branches
60
+ # Sync with main and restack
39
61
  gh-stack sync
40
62
 
41
- # Check PR status across all stacks
63
+ # Check PR status
42
64
  gh-stack status
43
65
  ```
44
66
 
45
67
  ## Commands
46
68
 
47
- ### Stack Management
69
+ ### Core Workflow
48
70
 
49
71
  ```
50
72
  init [--name <name>] [--description <desc>] [--parent <branch>]
51
- Create a new stack and add the current branch as the first entry.
73
+ Create a new stack from the current branch. Auto-detects branch
74
+ chains — if you're at the top of main → feat-1 → feat-2, all
75
+ branches are added to the stack automatically.
52
76
 
53
- add [<branch>] [--parent <branch>] [--create <branch>] [--description <desc>]
54
- Add a branch to the current stack. Defaults to the current branch.
55
- --create <branch> makes a new branch off the top of stack first.
77
+ create <branch-name> [--description <desc>]
78
+ Create a new git branch off the current branch and add it to
79
+ the stack. The current branch must already be tracked.
56
80
 
57
- remove [<branch>]
58
- Remove a branch from the stack and re-parent its children.
59
- If omitted, an interactive selector is shown.
81
+ submit [-d|--draft] [-n|--no-edit] [--dry-run]
82
+ Push all downstack branches to GitHub, create PRs for branches
83
+ that don't have them, and update all PR descriptions with stack
84
+ visualization. Idempotent — safe to run repeatedly.
85
+
86
+ In --yes mode, auto-generates PR titles from branch names.
60
87
  ```
61
88
 
62
- ### Navigation
89
+ ### Stack Navigation
63
90
 
64
91
  ```
65
- show
66
- Display the current stack as a tree with branch numbers,
67
- PR numbers, and descriptions. This is the default command.
92
+ checkout [<branch>]
93
+ Switch to a branch by name, or interactive picker.
94
+ --stack Switch between stacks instead of branches
95
+ (alias: co)
68
96
 
69
- list
70
- List branches with position numbers. Lightweight output for
71
- scripting and agents. (alias: ls)
97
+ up [steps]
98
+ Move to child branch (upstack). Prompts if multiple children.
72
99
 
73
- switch [<number>]
74
- Switch to a branch by position number, or interactive picker.
75
- --stack Switch between stacks instead of branches
100
+ down [steps]
101
+ Move to parent branch (downstack).
76
102
 
77
- status [--current] [--json]
78
- PR dashboard showing review state, CI status, and merge readiness.
79
- --current Show only the current stack or standalone PR
80
- --json Structured JSON output (progress goes to stderr)
103
+ top
104
+ Jump to the tip (leaf) of the current stack.
105
+
106
+ bottom
107
+ Jump to the base (first branch above trunk).
108
+
109
+ ls
110
+ List branches with position numbers.
81
111
  ```
82
112
 
83
- ### Rebase & Sync
113
+ ### Stack Management
84
114
 
85
115
  ```
86
- restack [--resume] [--dry-run] [--verbose] [--yes]
116
+ restack [--resume] [--dry-run] [--verbose]
87
117
  Rebase the current branch and all descendants onto their parents.
88
118
  Uses tag-based references for stable rebasing across the chain.
89
119
 
@@ -93,51 +123,37 @@ restack [--resume] [--dry-run] [--verbose] [--yes]
93
123
 
94
124
  (alias: rebase)
95
125
 
96
- sync [--dry-run] [--yes]
126
+ sync [--dry-run]
97
127
  Fetch main, rebase the base branch onto main, then restack all
98
128
  children. Creates tags for ALL branches before any rebasing starts.
99
- ```
100
-
101
- ### Merge & Ship
102
129
 
103
- ```
104
130
  merge [--dry-run]
105
- Squash-merge the stack top-down locally (PR3 -> PR2 -> PR1),
106
- then optionally rebase onto main. Keeps all commits local to
107
- avoid orphaned squash commits from GitHub-only merges.
108
- Can also close intermediate PRs and archives the stack on completion.
109
-
110
- update-prs
111
- Update all PR descriptions with a stack visualization:
131
+ Squash-merge the stack top-down locally (PR3 → PR2 → PR1),
132
+ then optionally rebase onto main. Closes intermediate PRs and
133
+ archives the stack on completion.
112
134
 
113
- ### 📚 Stacked on
114
- ⚫ main
115
- ┃
116
- ┣━ ✅ #123 Backend models 👈
117
- ┃
118
- ┗━ ⏳ #124 Frontend UI
135
+ delete [<branch>]
136
+ Remove a branch from the stack and re-parent its children.
137
+ Interactive selector if no branch specified.
119
138
  ```
120
139
 
121
- ### Maintenance
140
+ ### Info & Maintenance
122
141
 
123
142
  ```
124
- archive [--restore <name>]
125
- List archived stacks by default, or restore one by name.
143
+ log
144
+ Display the current stack as a tree with branch numbers,
145
+ PR info, and descriptions. This is the default command.
146
+
147
+ status [--current] [--json]
148
+ PR dashboard showing review state, CI status, and merge readiness.
149
+ --current Show only the current stack or standalone PR
150
+ --json Structured JSON output (progress goes to stderr)
126
151
 
127
152
  undo
128
153
  Restore the last snapshot taken before a destructive operation.
129
- ```
130
154
 
131
- ## Not Implemented Yet
132
-
133
- These are still intended, but not shipped in `v0.1.1`:
134
-
135
- ```text
136
- split
137
- Guided helper for splitting a branch/PR into two and updating stack metadata.
138
-
139
- insert
140
- Insert a branch into the middle of an existing stack and re-parent the chain.
155
+ archive [--restore <name>]
156
+ List archived stacks by default, or restore one by name.
141
157
  ```
142
158
 
143
159
  ## Global Options
@@ -157,10 +173,27 @@ GH_STACK_NO_COLOR=1 Disable colored output
157
173
 
158
174
  ## How It Works
159
175
 
176
+ ### Smart Init
177
+
178
+ `gh-stack init` auto-detects branch chains by walking git ancestry. From the top branch, it finds all local branches whose tips are strict ancestors (but not already merged into trunk) and reconstructs the chain with correct parent relationships.
179
+
160
180
  ### Tag-Based Rebasing
161
181
 
162
182
  The critical insight: after rebasing a parent branch, `git merge-base` returns wrong results for its children. gh-stack solves this by creating temporary `stack-sync-*` tags marking each branch's divergence point **before** any rebasing starts, then using those stable references for `git rebase --onto`.
163
183
 
184
+ ### Stack Visualization
185
+
186
+ `submit` automatically adds a stack section to all PR descriptions:
187
+
188
+ ```
189
+ ### 📚 Stacked on
190
+ ⚫ main
191
+ ┃
192
+ ┣━ ✅ #123 Backend models
193
+ ┃
194
+ ┗━ ⏳ #124 Frontend UI 👈
195
+ ```
196
+
164
197
  ### Metadata
165
198
 
166
199
  Stack metadata lives at `.git/gh-stack-metadata.json` (never committed):
@@ -168,21 +201,19 @@ Stack metadata lives at `.git/gh-stack-metadata.json` (never committed):
168
201
  ```json
169
202
  {
170
203
  "version": 2,
171
- "current_stack": "podcast-mvp",
204
+ "current_stack": "kiliman/feature-2",
172
205
  "stacks": {
173
- "podcast-mvp": {
174
- "description": "Podcast MVP features",
175
- "last_branch": "kiliman/pr2-WEB-1234",
206
+ "kiliman/feature-2": {
207
+ "description": "",
208
+ "last_branch": "kiliman/feature-2",
176
209
  "branches": {
177
- "kiliman/pr1-WEB-1234": {
210
+ "kiliman/feature-1": {
178
211
  "parent": "main",
179
- "pr": 21306,
180
- "description": "Backend models"
212
+ "pr": 21729
181
213
  },
182
- "kiliman/pr2-WEB-1234": {
183
- "parent": "kiliman/pr1-WEB-1234",
184
- "pr": 21452,
185
- "description": "Frontend UI"
214
+ "kiliman/feature-2": {
215
+ "parent": "kiliman/feature-1",
216
+ "pr": 21730
186
217
  }
187
218
  }
188
219
  }
@@ -192,28 +223,32 @@ Stack metadata lives at `.git/gh-stack-metadata.json` (never committed):
192
223
 
193
224
  ### Snapshots
194
225
 
195
- Before any destructive operation (restack, sync, merge, remove), gh-stack saves a snapshot of all branch HEADs. Run `gh-stack undo` to restore.
226
+ Before any destructive operation (restack, sync, merge, delete), gh-stack saves a snapshot of all branch HEADs. Run `gh-stack undo` to restore.
196
227
 
197
228
  ## Example Workflow
198
229
 
199
230
  ```bash
200
- # Start a new stack
201
- git checkout -b kiliman/api-layer-WEB-1234
202
- # ... code, commit, push, create PR ...
203
- gh-stack init --name api-feature
231
+ # Start from an existing branch with a PR
232
+ git checkout kiliman/api-layer-WEB-1234
233
+ gh-stack init
204
234
 
205
- # Add second PR on top
206
- gh-stack add --create kiliman/frontend-WEB-1234
207
- # ... code, commit, push, create PR ...
235
+ # Create second PR on top
236
+ gh-stack create kiliman/frontend-WEB-1234
237
+ # ... code, commit ...
208
238
 
209
- # Update PR descriptions with stack visualization
210
- gh-stack update-prs
239
+ # Push everything and create PRs
240
+ gh-stack submit
211
241
 
212
242
  # Later: sync everything with main
213
- gh-stack sync --yes
243
+ gh-stack sync
244
+
245
+ # Navigate the stack
246
+ gh-stack up # go to child
247
+ gh-stack down # go to parent
248
+ gh-stack top # jump to tip
214
249
 
215
250
  # Check status
216
- gh-stack status --current --json
251
+ gh-stack status
217
252
 
218
253
  # When PRs are approved, merge the stack
219
254
  gh-stack merge
@@ -224,18 +259,18 @@ gh-stack merge
224
259
  gh-stack is designed to be used by AI agents and CI pipelines:
225
260
 
226
261
  ```bash
227
- # Non-interactive mode
262
+ # Non-interactive mode — all prompts auto-resolved
228
263
  export GH_STACK_YES=1
264
+
265
+ gh-stack init # No confirmations
266
+ gh-stack submit # Auto-generates PR titles
229
267
  gh-stack sync
230
268
  gh-stack restack
231
269
 
232
270
  # Structured output
233
271
  gh-stack status --json
234
272
  gh-stack status --current --json
235
- gh-stack list
236
-
237
- # Quick branch switching
238
- gh-stack switch 2
273
+ gh-stack ls
239
274
  ```
240
275
 
241
276
  ## License
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "gh-stack",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Stacked PR manager for squash-merge workflows",
5
5
  "author": "Kiliman <kiliman@gmail.com>",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/kiliman/gh-stack"
9
+ "url": "git+https://github.com/kiliman/gh-stack.git"
10
10
  },
11
11
  "keywords": [
12
12
  "git",
@@ -40,7 +40,7 @@
40
40
  "format:check": "oxfmt --check src/",
41
41
  "typecheck": "tsc --noEmit",
42
42
  "check": "bun run lint && bun run typecheck && bun test",
43
- "prepublishOnly": "bun run check",
43
+ "prepublishOnly": "echo 'Checks already ran on push'",
44
44
  "prepare": "husky"
45
45
  },
46
46
  "lint-staged": {
@@ -63,4 +63,4 @@
63
63
  "@clack/prompts": "^1.1.0",
64
64
  "picocolors": "^1.1.1"
65
65
  }
66
- }
66
+ }
@@ -0,0 +1,54 @@
1
+ // gh-stack bottom — Jump to the base of the stack
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import * as git from "../lib/git.ts";
5
+ import { findStackForBranch, getOrderedBranches, writeMetadata } from "../lib/metadata.ts";
6
+ import { ensureMetadata } from "../lib/safety.ts";
7
+
8
+ export default async function bottom(args: string[]): Promise<void> {
9
+ if (args.includes("--help")) {
10
+ console.log(`
11
+ gh-stack bottom — Jump to the base of the stack
12
+
13
+ USAGE
14
+ gh-stack bottom
15
+
16
+ Switches to the bottom-most branch of the current stack
17
+ (the first branch above trunk).
18
+ `);
19
+ return;
20
+ }
21
+
22
+ const meta = await ensureMetadata();
23
+ const currentBranch = await git.currentBranch();
24
+ const stackName = findStackForBranch(meta, currentBranch);
25
+
26
+ if (!stackName) {
27
+ p.cancel(`Branch ${pc.yellow(currentBranch)} is not in any stack`);
28
+ process.exit(1);
29
+ }
30
+
31
+ const stack = meta.stacks[stackName]!;
32
+ const ordered = getOrderedBranches(stack);
33
+
34
+ if (ordered.length === 0) {
35
+ p.log.warn("No branches in stack");
36
+ return;
37
+ }
38
+
39
+ const target = ordered[0]!;
40
+
41
+ if (target === currentBranch) {
42
+ p.log.info(`Already at the bottom: ${pc.yellow(currentBranch)}`);
43
+ return;
44
+ }
45
+
46
+ await git.checkout(target);
47
+
48
+ // Update tracking
49
+ stack.last_branch = target;
50
+ meta.current_stack = stackName;
51
+ await writeMetadata(meta);
52
+
53
+ p.log.success(`On ${pc.yellow(target)}`);
54
+ }
@@ -1,4 +1,4 @@
1
- // gh-stack switch — Switch between branches or stacks
1
+ // gh-stack checkout — Switch between branches or stacks
2
2
  import * as p from "@clack/prompts";
3
3
  import pc from "picocolors";
4
4
  import * as git from "../lib/git.ts";
@@ -6,22 +6,25 @@ import { writeMetadata, getOrderedBranches, findStackForBranch } from "../lib/me
6
6
  import { ensureMetadata } from "../lib/safety.ts";
7
7
  import { selectBranch, selectStack } from "../lib/ui.ts";
8
8
 
9
- export default async function switchCmd(args: string[]): Promise<void> {
9
+ export default async function checkout(args: string[]): Promise<void> {
10
10
  const isStackMode = args.includes("--stack");
11
11
  const numberArg = args.find((a) => /^\d+$/.test(a));
12
12
 
13
13
  if (args.includes("--help")) {
14
14
  console.log(`
15
- gh-stack switch — Switch branch or stack
15
+ gh-stack checkout — Switch branch or stack
16
16
 
17
17
  USAGE
18
- gh-stack switch <number> Switch to branch by position (non-interactive)
19
- gh-stack switch Interactive branch picker (arrow keys)
20
- gh-stack switch --stack Switch between stacks (interactive)
18
+ gh-stack checkout <number> Switch to branch by position (non-interactive)
19
+ gh-stack checkout Interactive branch picker (arrow keys)
20
+ gh-stack checkout --stack Switch between stacks (interactive)
21
+
22
+ ALIASES
23
+ gh-stack co
21
24
 
22
25
  EXAMPLES
23
- gh-stack switch 1 Jump to first branch in stack (base)
24
- gh-stack switch 2 Jump to second branch in stack
26
+ gh-stack checkout 1 Jump to first branch in stack (base)
27
+ gh-stack checkout 2 Jump to second branch in stack
25
28
 
26
29
  TIP
27
30
  Use 'gh-stack list' to see branch numbers.
@@ -0,0 +1,83 @@
1
+ // gh-stack create — Create a new branch and add to the stack
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import * as git from "../lib/git.ts";
5
+ import { findStackForBranch, addBranchToStack } from "../lib/metadata.ts";
6
+ import { ensureMetadata } from "../lib/safety.ts";
7
+ import type { Branch } from "../types.ts";
8
+
9
+ export default async function create(args: string[]): Promise<void> {
10
+ // Parse flags
11
+ let descFlag: string | undefined;
12
+ let branchArg: string | undefined;
13
+
14
+ for (let i = 0; i < args.length; i++) {
15
+ switch (args[i]) {
16
+ case "--description":
17
+ descFlag = args[++i];
18
+ break;
19
+ case "--help":
20
+ console.log(`
21
+ gh-stack create — Create a new branch and add to the stack
22
+
23
+ USAGE
24
+ gh-stack create <branch-name> [options]
25
+
26
+ Creates a new git branch off the current branch and adds it to the
27
+ current stack. The current branch must already be tracked in a stack.
28
+
29
+ OPTIONS
30
+ --description <desc> Description for the branch
31
+
32
+ EXAMPLES
33
+ gh-stack create kiliman/feature-2 # Branch off current, add to stack
34
+ gh-stack create kiliman/api --description "API layer"
35
+ `);
36
+ return;
37
+ default:
38
+ if (!args[i]!.startsWith("--")) {
39
+ branchArg = args[i];
40
+ }
41
+ }
42
+ }
43
+
44
+ // Branch name is required
45
+ if (!branchArg) {
46
+ p.cancel("Branch name is required.\n\n Usage: gh-stack create <branch-name>");
47
+ process.exit(1);
48
+ }
49
+
50
+ const meta = await ensureMetadata();
51
+ const currentBranch = await git.currentBranch();
52
+
53
+ // Current branch must be in a stack
54
+ const stackName = findStackForBranch(meta, currentBranch);
55
+ if (!stackName) {
56
+ p.cancel(
57
+ `Current branch ${pc.yellow(currentBranch)} is not in any stack.\n\n Add it to a stack first with:\n ${pc.green("gh-stack init")}`,
58
+ );
59
+ process.exit(1);
60
+ }
61
+
62
+ // New branch must not already exist
63
+ if (await git.localBranchExists(branchArg)) {
64
+ p.cancel(`Branch ${pc.yellow(branchArg)} already exists.`);
65
+ process.exit(1);
66
+ }
67
+
68
+ p.intro(pc.cyan(`Create branch in ${pc.yellow(stackName)}`));
69
+
70
+ // Create the new branch off current HEAD
71
+ p.log.info(`Creating ${pc.yellow(branchArg)} off ${pc.yellow(currentBranch)}`);
72
+ await git.createBranch(branchArg);
73
+
74
+ // Add to the stack with parent = current branch
75
+ const branchData: Branch = {
76
+ parent: currentBranch,
77
+ ...(descFlag && { description: descFlag }),
78
+ };
79
+
80
+ await addBranchToStack(meta, stackName, branchArg, branchData);
81
+
82
+ p.outro(pc.green(`Created ${pc.yellow(branchArg)} → parent ${pc.yellow(currentBranch)}`));
83
+ }
@@ -1,19 +1,19 @@
1
- // gh-stack remove — Remove a branch from the stack (re-link parent chain)
1
+ // gh-stack delete — Delete a branch from the stack (re-link parent chain)
2
2
  import * as p from "@clack/prompts";
3
3
  import pc from "picocolors";
4
4
  import { removeBranchFromStack, getChildren } from "../lib/metadata.ts";
5
- import { ensureMetadata, ensureCurrentStack } from "../lib/safety.ts";
5
+ import { ensureMetadata, ensureCurrentStack, ensureValidStack } from "../lib/safety.ts";
6
6
  import { takeSnapshot } from "../lib/snapshot.ts";
7
7
  import { selectBranch } from "../lib/ui.ts";
8
8
  import * as git from "../lib/git.ts";
9
9
 
10
- export default async function remove(args: string[]): Promise<void> {
10
+ export default async function deleteCmd(args: string[]): Promise<void> {
11
11
  if (args.includes("--help")) {
12
12
  console.log(`
13
- gh-stack remove — Remove a branch from the stack
13
+ gh-stack delete — Delete a branch from the stack
14
14
 
15
15
  USAGE
16
- gh-stack remove [<branch>]
16
+ gh-stack delete [<branch>]
17
17
 
18
18
  Removes the branch and re-parents its children to its parent.
19
19
  If no branch is specified, shows interactive selector.
@@ -23,15 +23,16 @@ If no branch is specified, shows interactive selector.
23
23
 
24
24
  const meta = await ensureMetadata();
25
25
  const stackName = ensureCurrentStack(meta);
26
+ await ensureValidStack(meta, stackName);
26
27
  const stack = meta.stacks[stackName]!;
27
28
  const currentBranch = await git.currentBranch();
28
29
 
29
- p.intro(pc.cyan("Remove Branch from Stack"));
30
+ p.intro(pc.cyan("Delete Branch from Stack"));
30
31
 
31
- // Get branch to remove
32
+ // Get branch to delete
32
33
  let branchName = args[0];
33
34
  if (!branchName) {
34
- branchName = (await selectBranch(stack, "Select branch to remove", currentBranch)) ?? undefined;
35
+ branchName = (await selectBranch(stack, "Select branch to delete", currentBranch)) ?? undefined;
35
36
  if (!branchName) {
36
37
  p.cancel("Cancelled");
37
38
  process.exit(0);
@@ -61,7 +62,7 @@ If no branch is specified, shows interactive selector.
61
62
 
62
63
  // Confirm
63
64
  const confirmed = await p.confirm({
64
- message: `Remove ${pc.yellow(branchName)} from stack?`,
65
+ message: `Delete ${pc.yellow(branchName)} from stack?`,
65
66
  });
66
67
  if (p.isCancel(confirmed) || !confirmed) {
67
68
  p.cancel("Cancelled");
@@ -69,7 +70,7 @@ If no branch is specified, shows interactive selector.
69
70
  }
70
71
 
71
72
  // Take snapshot before destructive operation
72
- await takeSnapshot(meta, stackName, "remove");
73
+ await takeSnapshot(meta, stackName, "delete");
73
74
 
74
75
  // Remove branch
75
76
  await removeBranchFromStack(meta, stackName, branchName);
@@ -0,0 +1,67 @@
1
+ // gh-stack down — Move to parent branch (downstack)
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import * as git from "../lib/git.ts";
5
+ import { findStackForBranch, writeMetadata } from "../lib/metadata.ts";
6
+ import { ensureMetadata } from "../lib/safety.ts";
7
+
8
+ export default async function down(args: string[]): Promise<void> {
9
+ if (args.includes("--help")) {
10
+ console.log(`
11
+ gh-stack down — Move to parent branch (downstack)
12
+
13
+ USAGE
14
+ gh-stack down [steps]
15
+
16
+ OPTIONS
17
+ [steps] Number of levels to move down (default: 1)
18
+ `);
19
+ return;
20
+ }
21
+
22
+ const steps = parseInt(args[0] || "1", 10);
23
+ if (isNaN(steps) || steps < 1) {
24
+ p.cancel("Invalid step count");
25
+ process.exit(1);
26
+ }
27
+
28
+ const meta = await ensureMetadata();
29
+ let currentBranch = await git.currentBranch();
30
+ const stackName = findStackForBranch(meta, currentBranch);
31
+
32
+ if (!stackName) {
33
+ p.cancel(`Branch ${pc.yellow(currentBranch)} is not in any stack`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const stack = meta.stacks[stackName]!;
38
+
39
+ for (let i = 0; i < steps; i++) {
40
+ const branchMeta = stack.branches[currentBranch];
41
+ if (!branchMeta) {
42
+ p.log.warn(`${pc.yellow(currentBranch)} is not tracked in the stack`);
43
+ return;
44
+ }
45
+
46
+ const parent = branchMeta.parent;
47
+ if (parent === "main" || parent === "master") {
48
+ if (i === 0) {
49
+ p.log.warn(`${pc.yellow(currentBranch)} is already at the bottom of the stack`);
50
+ } else {
51
+ p.log.info(`Reached bottom of stack at ${pc.yellow(currentBranch)} after ${i} step(s)`);
52
+ }
53
+ return;
54
+ }
55
+
56
+ currentBranch = parent;
57
+ }
58
+
59
+ await git.checkout(currentBranch);
60
+
61
+ // Update tracking
62
+ stack.last_branch = currentBranch;
63
+ meta.current_stack = stackName;
64
+ await writeMetadata(meta);
65
+
66
+ p.log.success(`On ${pc.yellow(currentBranch)}`);
67
+ }