gh-stack 0.1.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/LICENSE +21 -0
- package/README.md +229 -0
- package/package.json +66 -0
- package/src/commands/add.ts +137 -0
- package/src/commands/archive.ts +60 -0
- package/src/commands/init.ts +156 -0
- package/src/commands/list.ts +64 -0
- package/src/commands/merge.ts +175 -0
- package/src/commands/remove.ts +78 -0
- package/src/commands/restack.ts +476 -0
- package/src/commands/show.ts +73 -0
- package/src/commands/status.ts +372 -0
- package/src/commands/switch.ts +114 -0
- package/src/commands/sync.ts +189 -0
- package/src/commands/undo.ts +64 -0
- package/src/commands/update-prs.ts +177 -0
- package/src/index.ts +146 -0
- package/src/lib/git.ts +237 -0
- package/src/lib/github.ts +184 -0
- package/src/lib/metadata.ts +307 -0
- package/src/lib/safety.ts +98 -0
- package/src/lib/snapshot.ts +74 -0
- package/src/lib/ui.ts +234 -0
- package/src/types.ts +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kiliman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# gh-stack
|
|
2
|
+
|
|
3
|
+
Stacked PR manager for squash-merge workflows.
|
|
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).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# From source (requires Bun)
|
|
11
|
+
bun install
|
|
12
|
+
bun run build
|
|
13
|
+
ln -s $(pwd)/dist/gh-stack ~/.local/bin/gh-stack
|
|
14
|
+
|
|
15
|
+
# Or install globally
|
|
16
|
+
bun install -g gh-stack
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Prerequisites
|
|
20
|
+
|
|
21
|
+
- [Bun](https://bun.sh) runtime
|
|
22
|
+
- [GitHub CLI](https://cli.github.com/) (`gh`) — authenticated
|
|
23
|
+
- Git 2.30+
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Create a stack from your current branch
|
|
29
|
+
gh-stack init --name my-feature
|
|
30
|
+
|
|
31
|
+
# Add more branches to the stack
|
|
32
|
+
gh-stack add --create kiliman/pr2-WEB-1234
|
|
33
|
+
|
|
34
|
+
# View the stack
|
|
35
|
+
gh-stack # tree view (default)
|
|
36
|
+
gh-stack list # numbered list
|
|
37
|
+
|
|
38
|
+
# Sync with main and restack all branches
|
|
39
|
+
gh-stack sync
|
|
40
|
+
|
|
41
|
+
# Check PR status across all stacks
|
|
42
|
+
gh-stack status
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
### Stack Management
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
init [--name <name>] [--description <desc>]
|
|
51
|
+
Create a new stack and add the current branch as the first entry.
|
|
52
|
+
|
|
53
|
+
add [<branch>] [--parent <branch>] [--create] [--description <desc>]
|
|
54
|
+
Add a branch to the current stack. Defaults to the current branch.
|
|
55
|
+
--create makes a new branch off the top of stack first.
|
|
56
|
+
|
|
57
|
+
remove <branch>
|
|
58
|
+
Remove a branch from the stack and re-parent its children.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Navigation
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
show
|
|
65
|
+
Display the current stack as a tree with PR numbers, titles,
|
|
66
|
+
review status, and CI state. This is the default command.
|
|
67
|
+
|
|
68
|
+
list
|
|
69
|
+
List branches with position numbers. Lightweight output for
|
|
70
|
+
scripting and agents. (alias: ls)
|
|
71
|
+
|
|
72
|
+
switch [<number>]
|
|
73
|
+
Switch to a branch by position number, or interactive picker.
|
|
74
|
+
--stack Switch between stacks instead of branches
|
|
75
|
+
|
|
76
|
+
status [--current] [--json]
|
|
77
|
+
PR dashboard showing review state, CI status, and merge readiness.
|
|
78
|
+
--current Show only the current stack or standalone PR
|
|
79
|
+
--json Structured JSON output (progress goes to stderr)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Rebase & Sync
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
restack [--resume] [--dry-run] [--verbose] [--yes]
|
|
86
|
+
Rebase the current branch and all descendants onto their parents.
|
|
87
|
+
Uses tag-based references for stable rebasing across the chain.
|
|
88
|
+
|
|
89
|
+
On conflict:
|
|
90
|
+
git rebase --continue
|
|
91
|
+
gh-stack restack --resume
|
|
92
|
+
|
|
93
|
+
(alias: rebase)
|
|
94
|
+
|
|
95
|
+
sync [--dry-run] [--yes]
|
|
96
|
+
Fetch main, rebase the base branch onto main, then restack all
|
|
97
|
+
children. Creates tags for ALL branches before any rebasing starts.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Merge & Ship
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
merge [--dry-run]
|
|
104
|
+
Squash-merge the stack top-down locally (PR3 -> PR2 -> PR1),
|
|
105
|
+
then optionally rebase onto main. Keeps all commits local to
|
|
106
|
+
avoid orphaned squash commits from GitHub-only merges.
|
|
107
|
+
|
|
108
|
+
update-prs
|
|
109
|
+
Update all PR descriptions with a stack visualization:
|
|
110
|
+
|
|
111
|
+
### 📚 Stacked on
|
|
112
|
+
⚫ main
|
|
113
|
+
┃
|
|
114
|
+
┣━ ✅ #123 Backend models 👈
|
|
115
|
+
┃
|
|
116
|
+
┗━ ⏳ #124 Frontend UI
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Maintenance
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
archive [--list] [--restore <name>]
|
|
123
|
+
Manage archived stacks.
|
|
124
|
+
|
|
125
|
+
undo
|
|
126
|
+
Restore the last snapshot taken before a destructive operation.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Global Options
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
--yes, -y Skip all confirmations (for agents/CI)
|
|
133
|
+
--help Show help for a command
|
|
134
|
+
--version, -V Show version
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Environment Variables
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
GH_STACK_YES=1 Skip all confirmations (same as --yes)
|
|
141
|
+
GH_STACK_NO_COLOR=1 Disable colored output
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## How It Works
|
|
145
|
+
|
|
146
|
+
### Tag-Based Rebasing
|
|
147
|
+
|
|
148
|
+
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`.
|
|
149
|
+
|
|
150
|
+
### Metadata
|
|
151
|
+
|
|
152
|
+
Stack metadata lives at `.git/gh-stack-metadata.json` (never committed):
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"version": 2,
|
|
157
|
+
"current_stack": "podcast-mvp",
|
|
158
|
+
"stacks": {
|
|
159
|
+
"podcast-mvp": {
|
|
160
|
+
"description": "Podcast MVP features",
|
|
161
|
+
"last_branch": "kiliman/pr2-WEB-1234",
|
|
162
|
+
"branches": {
|
|
163
|
+
"kiliman/pr1-WEB-1234": {
|
|
164
|
+
"parent": "main",
|
|
165
|
+
"pr": 21306,
|
|
166
|
+
"description": "Backend models"
|
|
167
|
+
},
|
|
168
|
+
"kiliman/pr2-WEB-1234": {
|
|
169
|
+
"parent": "kiliman/pr1-WEB-1234",
|
|
170
|
+
"pr": 21452,
|
|
171
|
+
"description": "Frontend UI"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Snapshots
|
|
180
|
+
|
|
181
|
+
Before any destructive operation (restack, sync, merge, remove), gh-stack saves a snapshot of all branch HEADs. Run `gh-stack undo` to restore.
|
|
182
|
+
|
|
183
|
+
## Example Workflow
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Start a new stack
|
|
187
|
+
git checkout -b kiliman/api-layer-WEB-1234
|
|
188
|
+
# ... code, commit, push, create PR ...
|
|
189
|
+
gh-stack init --name api-feature
|
|
190
|
+
|
|
191
|
+
# Add second PR on top
|
|
192
|
+
gh-stack add --create kiliman/frontend-WEB-1234
|
|
193
|
+
# ... code, commit, push, create PR ...
|
|
194
|
+
|
|
195
|
+
# Update PR descriptions with stack visualization
|
|
196
|
+
gh-stack update-prs
|
|
197
|
+
|
|
198
|
+
# Later: sync everything with main
|
|
199
|
+
gh-stack sync --yes
|
|
200
|
+
|
|
201
|
+
# Check status
|
|
202
|
+
gh-stack status --current --json
|
|
203
|
+
|
|
204
|
+
# When PRs are approved, merge the stack
|
|
205
|
+
gh-stack merge
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Agent/CI Usage
|
|
209
|
+
|
|
210
|
+
gh-stack is designed to be used by AI agents and CI pipelines:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Non-interactive mode
|
|
214
|
+
export GH_STACK_YES=1
|
|
215
|
+
gh-stack sync
|
|
216
|
+
gh-stack restack
|
|
217
|
+
|
|
218
|
+
# Structured output
|
|
219
|
+
gh-stack status --json
|
|
220
|
+
gh-stack status --current --json
|
|
221
|
+
gh-stack list
|
|
222
|
+
|
|
223
|
+
# Quick branch switching
|
|
224
|
+
gh-stack switch 2
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gh-stack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stacked PR manager for squash-merge workflows",
|
|
5
|
+
"author": "Kiliman <kiliman@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/kiliman/gh-stack"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"git",
|
|
13
|
+
"github",
|
|
14
|
+
"stacked-prs",
|
|
15
|
+
"squash-merge",
|
|
16
|
+
"cli",
|
|
17
|
+
"pull-requests",
|
|
18
|
+
"rebase",
|
|
19
|
+
"bun"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"gh-stack": "src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"!src/**/__tests__",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"bun": ">=1.0.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "bun run src/index.ts",
|
|
36
|
+
"build": "bun build --compile src/index.ts --outfile dist/gh-stack",
|
|
37
|
+
"test": "bun test",
|
|
38
|
+
"lint": "oxlint src/",
|
|
39
|
+
"format": "oxfmt --write src/",
|
|
40
|
+
"format:check": "oxfmt --check src/",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"check": "bun run lint && bun run typecheck && bun test",
|
|
43
|
+
"prepublishOnly": "bun run check",
|
|
44
|
+
"prepare": "husky"
|
|
45
|
+
},
|
|
46
|
+
"lint-staged": {
|
|
47
|
+
"*.ts": [
|
|
48
|
+
"oxlint",
|
|
49
|
+
"oxfmt --write"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "latest",
|
|
54
|
+
"husky": "^9.1.7",
|
|
55
|
+
"lint-staged": "^16.3.3",
|
|
56
|
+
"oxfmt": "^0.40.0",
|
|
57
|
+
"oxlint": "^1.55.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"typescript": "^5"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@clack/prompts": "^1.1.0",
|
|
64
|
+
"picocolors": "^1.1.1"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// gh-stack add — Add a branch to the current stack
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import * as git from "../lib/git.ts";
|
|
5
|
+
import { addBranchToStack, getTopOfStack, getOrderedBranches } from "../lib/metadata.ts";
|
|
6
|
+
import { ensureMetadata, ensureCurrentStack } from "../lib/safety.ts";
|
|
7
|
+
import { getPrNumber } from "../lib/github.ts";
|
|
8
|
+
import { selectParent } from "../lib/ui.ts";
|
|
9
|
+
import type { Branch } from "../types.ts";
|
|
10
|
+
|
|
11
|
+
export default async function add(args: string[]): Promise<void> {
|
|
12
|
+
// Parse flags
|
|
13
|
+
let parentFlag: string | undefined;
|
|
14
|
+
let descFlag: string | undefined;
|
|
15
|
+
let createFlag: string | undefined;
|
|
16
|
+
let branchArg: string | undefined;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
switch (args[i]) {
|
|
20
|
+
case "--parent":
|
|
21
|
+
parentFlag = args[++i];
|
|
22
|
+
break;
|
|
23
|
+
case "--description":
|
|
24
|
+
descFlag = args[++i];
|
|
25
|
+
break;
|
|
26
|
+
case "--create":
|
|
27
|
+
createFlag = args[++i];
|
|
28
|
+
break;
|
|
29
|
+
case "--help":
|
|
30
|
+
console.log(`
|
|
31
|
+
gh-stack add — Add a branch to the current stack
|
|
32
|
+
|
|
33
|
+
USAGE
|
|
34
|
+
gh-stack add [<branch>] [options]
|
|
35
|
+
|
|
36
|
+
OPTIONS
|
|
37
|
+
--parent <branch> Parent branch (default: top of stack)
|
|
38
|
+
--create <branch> Create new branch off top of stack and add it
|
|
39
|
+
--description <desc> Description for the branch
|
|
40
|
+
`);
|
|
41
|
+
return;
|
|
42
|
+
default:
|
|
43
|
+
if (!args[i]!.startsWith("--")) {
|
|
44
|
+
branchArg = args[i];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const meta = await ensureMetadata();
|
|
50
|
+
const stackName = ensureCurrentStack(meta);
|
|
51
|
+
const stack = meta.stacks[stackName]!;
|
|
52
|
+
|
|
53
|
+
p.intro(pc.cyan(`Add branch to ${pc.yellow(stackName)}`));
|
|
54
|
+
|
|
55
|
+
let branchToAdd: string;
|
|
56
|
+
|
|
57
|
+
// --create: make a new branch off the top of the stack
|
|
58
|
+
if (createFlag) {
|
|
59
|
+
const topBranch = getTopOfStack(stack);
|
|
60
|
+
if (!topBranch) {
|
|
61
|
+
p.cancel("Stack has no branches yet. Add the base branch first.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentBranch = await git.currentBranch();
|
|
66
|
+
if (currentBranch !== topBranch) {
|
|
67
|
+
p.log.info(`Switching to ${pc.yellow(topBranch)}...`);
|
|
68
|
+
await git.checkout(topBranch);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
p.log.info(`Creating branch off top of stack: ${pc.yellow(topBranch)}`);
|
|
72
|
+
await git.createBranch(createFlag);
|
|
73
|
+
p.log.success(`Created branch: ${pc.yellow(createFlag)}`);
|
|
74
|
+
|
|
75
|
+
branchToAdd = createFlag;
|
|
76
|
+
parentFlag = topBranch;
|
|
77
|
+
} else {
|
|
78
|
+
branchToAdd = branchArg || (await git.currentBranch());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if branch is already in the stack
|
|
82
|
+
if (stack.branches[branchToAdd]) {
|
|
83
|
+
p.cancel(`Branch ${pc.yellow(branchToAdd)} is already in stack ${pc.blue(stackName)}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(` Adding: ${pc.yellow(branchToAdd)}`);
|
|
88
|
+
console.log();
|
|
89
|
+
|
|
90
|
+
// Get parent
|
|
91
|
+
let parent = parentFlag;
|
|
92
|
+
if (!parent) {
|
|
93
|
+
// Default to top of stack
|
|
94
|
+
const ordered = getOrderedBranches(stack);
|
|
95
|
+
|
|
96
|
+
if (ordered.length === 0) {
|
|
97
|
+
parent = "main";
|
|
98
|
+
} else {
|
|
99
|
+
parent = (await selectParent(stack, branchToAdd)) ?? undefined;
|
|
100
|
+
if (!parent) {
|
|
101
|
+
p.cancel("Cancelled");
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
p.log.info(`Parent: ${pc.yellow(parent)}`);
|
|
108
|
+
|
|
109
|
+
// Auto-detect PR number
|
|
110
|
+
const s = p.spinner();
|
|
111
|
+
s.start("Looking for PR...");
|
|
112
|
+
const prNumber = await getPrNumber(branchToAdd);
|
|
113
|
+
s.stop(prNumber ? `Found PR #${prNumber}` : "No PR found");
|
|
114
|
+
|
|
115
|
+
// Get description
|
|
116
|
+
let description = descFlag;
|
|
117
|
+
if (description === undefined && !createFlag) {
|
|
118
|
+
const result = await p.text({
|
|
119
|
+
message: "Branch description (optional)",
|
|
120
|
+
placeholder: "e.g., API layer",
|
|
121
|
+
});
|
|
122
|
+
if (!p.isCancel(result)) {
|
|
123
|
+
description = (result as string) || undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add branch
|
|
128
|
+
const branchData: Branch = {
|
|
129
|
+
parent,
|
|
130
|
+
...(prNumber && { pr: prNumber }),
|
|
131
|
+
...(description && { description }),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
await addBranchToStack(meta, stackName, branchToAdd, branchData);
|
|
135
|
+
|
|
136
|
+
p.outro(pc.green(`Added ${pc.yellow(branchToAdd)} to stack ${pc.blue(stackName)}`));
|
|
137
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// gh-stack archive — Manage archived stacks
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { writeMetadata } from "../lib/metadata.ts";
|
|
5
|
+
import { ensureMetadata } from "../lib/safety.ts";
|
|
6
|
+
|
|
7
|
+
export default async function archive(args: string[]): Promise<void> {
|
|
8
|
+
const restoreIdx = args.indexOf("--restore");
|
|
9
|
+
const restoreName = restoreIdx !== -1 ? args[restoreIdx + 1] : undefined;
|
|
10
|
+
|
|
11
|
+
if (args.includes("--help")) {
|
|
12
|
+
console.log(`
|
|
13
|
+
gh-stack archive — Manage archived stacks
|
|
14
|
+
|
|
15
|
+
USAGE
|
|
16
|
+
gh-stack archive [--list] List archived stacks
|
|
17
|
+
gh-stack archive --restore <name> Restore an archived stack
|
|
18
|
+
`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const meta = await ensureMetadata();
|
|
23
|
+
|
|
24
|
+
if (restoreName) {
|
|
25
|
+
// Restore an archived stack
|
|
26
|
+
if (!meta.archive || !meta.archive[restoreName]) {
|
|
27
|
+
p.cancel(`Archived stack "${restoreName}" not found`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
meta.stacks[restoreName] = meta.archive[restoreName]!;
|
|
32
|
+
delete meta.archive[restoreName];
|
|
33
|
+
meta.current_stack = restoreName;
|
|
34
|
+
await writeMetadata(meta);
|
|
35
|
+
|
|
36
|
+
p.log.success(`Restored stack ${pc.yellow(restoreName)} from archive`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// List archived stacks
|
|
41
|
+
p.intro(pc.cyan("Archived Stacks"));
|
|
42
|
+
|
|
43
|
+
if (!meta.archive || Object.keys(meta.archive).length === 0) {
|
|
44
|
+
p.log.info("No archived stacks");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [name, stack] of Object.entries(meta.archive)) {
|
|
49
|
+
const branchCount = Object.keys(stack.branches).length;
|
|
50
|
+
console.log(
|
|
51
|
+
` ${pc.blue("○")} ${pc.yellow(name)} (${branchCount} branch${branchCount !== 1 ? "es" : ""})`,
|
|
52
|
+
);
|
|
53
|
+
if (stack.description) {
|
|
54
|
+
console.log(` ${pc.dim(stack.description)}`);
|
|
55
|
+
}
|
|
56
|
+
console.log();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(pc.dim(` Restore with: gh-stack archive --restore <name>`));
|
|
60
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// gh-stack init — Create a new stack with the current branch
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import * as git from "../lib/git.ts";
|
|
5
|
+
import {
|
|
6
|
+
readMetadata,
|
|
7
|
+
initMetadata,
|
|
8
|
+
createStack,
|
|
9
|
+
addBranchToStack,
|
|
10
|
+
metadataExists,
|
|
11
|
+
} from "../lib/metadata.ts";
|
|
12
|
+
import { getPrNumber } from "../lib/github.ts";
|
|
13
|
+
import { selectParent } from "../lib/ui.ts";
|
|
14
|
+
import type { StackMetadata, Branch } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
export default async function init(args: string[]): Promise<void> {
|
|
17
|
+
const branch = await git.currentBranch();
|
|
18
|
+
|
|
19
|
+
// Parse flags
|
|
20
|
+
let nameFlag: string | undefined;
|
|
21
|
+
let descFlag: string | undefined;
|
|
22
|
+
let parentFlag: string | undefined;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
switch (args[i]) {
|
|
26
|
+
case "--name":
|
|
27
|
+
nameFlag = args[++i];
|
|
28
|
+
break;
|
|
29
|
+
case "--description":
|
|
30
|
+
descFlag = args[++i];
|
|
31
|
+
break;
|
|
32
|
+
case "--parent":
|
|
33
|
+
parentFlag = args[++i];
|
|
34
|
+
break;
|
|
35
|
+
case "--help":
|
|
36
|
+
console.log(`
|
|
37
|
+
gh-stack init — Create a new stack
|
|
38
|
+
|
|
39
|
+
USAGE
|
|
40
|
+
gh-stack init [--name <name>] [--description <desc>] [--parent <branch>]
|
|
41
|
+
|
|
42
|
+
OPTIONS
|
|
43
|
+
--name <name> Stack name (skip prompt)
|
|
44
|
+
--description <desc> Stack description
|
|
45
|
+
--parent <branch> Parent branch for current branch (default: main)
|
|
46
|
+
`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
p.intro(pc.cyan("Create New Stack"));
|
|
52
|
+
|
|
53
|
+
console.log(` Initializing stack with: ${pc.yellow(branch)}`);
|
|
54
|
+
console.log();
|
|
55
|
+
|
|
56
|
+
// Get or create metadata
|
|
57
|
+
let meta: StackMetadata;
|
|
58
|
+
if (await metadataExists()) {
|
|
59
|
+
meta = (await readMetadata())!;
|
|
60
|
+
} else {
|
|
61
|
+
meta = await initMetadata();
|
|
62
|
+
p.log.success("Initialized empty stack metadata");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get stack name
|
|
66
|
+
let stackName = nameFlag;
|
|
67
|
+
if (!stackName) {
|
|
68
|
+
const result = await p.text({
|
|
69
|
+
message: "Stack name",
|
|
70
|
+
placeholder: "e.g., podcast-mvp",
|
|
71
|
+
validate: (val = "") => {
|
|
72
|
+
if (!val.trim()) return "Stack name is required";
|
|
73
|
+
if (meta.stacks[val.trim()]) return `Stack "${val}" already exists`;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
if (p.isCancel(result)) {
|
|
77
|
+
p.cancel("Cancelled");
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
stackName = result as string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if stack already exists
|
|
84
|
+
if (meta.stacks[stackName]) {
|
|
85
|
+
p.cancel(`Stack "${stackName}" already exists`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get stack description
|
|
90
|
+
let description = descFlag;
|
|
91
|
+
if (description === undefined) {
|
|
92
|
+
const result = await p.text({
|
|
93
|
+
message: "Stack description (optional)",
|
|
94
|
+
placeholder: "e.g., Podcast MVP features",
|
|
95
|
+
});
|
|
96
|
+
if (p.isCancel(result)) {
|
|
97
|
+
p.cancel("Cancelled");
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
description = (result as string) || "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create the stack
|
|
104
|
+
meta = await createStack(meta, stackName, description);
|
|
105
|
+
p.log.success(`Created stack: ${pc.blue(stackName)}`);
|
|
106
|
+
|
|
107
|
+
// Get parent branch
|
|
108
|
+
let parent: string | undefined = parentFlag;
|
|
109
|
+
if (!parent) {
|
|
110
|
+
parent = (await selectParent(null, branch)) ?? undefined;
|
|
111
|
+
if (!parent) {
|
|
112
|
+
p.cancel("Cancelled");
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
p.log.info(`Parent: ${pc.yellow(parent)}`);
|
|
118
|
+
|
|
119
|
+
// Auto-detect PR number
|
|
120
|
+
const s = p.spinner();
|
|
121
|
+
s.start("Looking for PR...");
|
|
122
|
+
const prNumber = await getPrNumber(branch);
|
|
123
|
+
s.stop(prNumber ? `Found PR #${prNumber}` : "No PR found");
|
|
124
|
+
|
|
125
|
+
// Get branch description
|
|
126
|
+
let branchDesc: string | undefined;
|
|
127
|
+
if (!nameFlag) {
|
|
128
|
+
// Only prompt for branch description in interactive mode
|
|
129
|
+
const result = await p.text({
|
|
130
|
+
message: "Branch description (optional)",
|
|
131
|
+
placeholder: "e.g., Backend models",
|
|
132
|
+
});
|
|
133
|
+
if (!p.isCancel(result)) {
|
|
134
|
+
branchDesc = (result as string) || undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Add branch to stack
|
|
139
|
+
const branchData: Branch = {
|
|
140
|
+
parent,
|
|
141
|
+
...(prNumber && { pr: prNumber }),
|
|
142
|
+
...(branchDesc && { description: branchDesc }),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
meta = await addBranchToStack(meta, stackName, branch, branchData);
|
|
146
|
+
|
|
147
|
+
p.log.success(`Added ${pc.yellow(branch)} to stack`);
|
|
148
|
+
|
|
149
|
+
p.outro(pc.green("Stack initialized!"));
|
|
150
|
+
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(" Next steps:");
|
|
153
|
+
console.log(` ${pc.blue("gh-stack add")} — Add more branches`);
|
|
154
|
+
console.log(` ${pc.blue("gh-stack")} — View stack`);
|
|
155
|
+
console.log(` ${pc.blue("gh-stack restack")} — Sync stack`);
|
|
156
|
+
}
|