@cvr/stacked 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/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # stacked
2
+
3
+ Branch-based stacked PR manager. Tracks parent-child branch relationships, automates rebasing, and creates/updates GitHub PRs via `gh`.
4
+
5
+ Built with [Effect v4](https://effect.website) and [Bun](https://bun.sh).
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ bun run build # compiles binary to bin/stacked + symlinks to ~/.bun/bin/
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```sh
16
+ # Set trunk branch (default: main)
17
+ stacked trunk develop
18
+
19
+ # Create a stack: branch from trunk
20
+ stacked create feat-auth
21
+
22
+ # Stack another branch on top
23
+ stacked create feat-auth-ui
24
+
25
+ # See the stack
26
+ stacked list
27
+
28
+ # Navigate
29
+ stacked top
30
+ stacked bottom
31
+ stacked checkout feat-auth
32
+
33
+ # After editing mid-stack, rebase children
34
+ stacked restack
35
+
36
+ # Sync entire stack with latest trunk
37
+ stacked sync
38
+
39
+ # Push all branches + create/update PRs
40
+ stacked submit
41
+ stacked submit --draft
42
+ stacked submit --dry-run
43
+
44
+ # Adopt an existing branch into the stack
45
+ stacked adopt existing-branch --after feat-auth
46
+
47
+ # View commits per branch
48
+ stacked log
49
+
50
+ # Remove a branch from the stack
51
+ stacked delete feat-auth-ui
52
+ ```
53
+
54
+ ## Commands
55
+
56
+ | Command | Description |
57
+ | ----------------- | ---------------------------------------- |
58
+ | `trunk [name]` | Get/set trunk branch |
59
+ | `create <name>` | Create branch on top of current |
60
+ | `list` | Show stack with current branch indicator |
61
+ | `checkout <name>` | Switch to branch |
62
+ | `top` | Jump to top of stack |
63
+ | `bottom` | Jump to bottom of stack |
64
+ | `sync` | Fetch + rebase entire stack on trunk |
65
+ | `restack` | Rebase children after mid-stack edits |
66
+ | `delete <name>` | Remove branch from stack + git |
67
+ | `submit` | Push all + create/update PRs via `gh` |
68
+ | `adopt <branch>` | Add existing branch to stack |
69
+ | `log` | Show commits grouped by branch |
70
+
71
+ ## Data Model
72
+
73
+ Stack metadata lives in `.git/stacked.json`. Each branch's parent is implied by array position — `branches[0]`'s parent is trunk, `branches[n]`'s parent is `branches[n-1]`.
74
+
75
+ ## Development
76
+
77
+ ```sh
78
+ bun run dev -- --help # run from source
79
+ bun run gate # typecheck + lint + fmt + test + build
80
+ bun test # tests only
81
+ ```
package/bin/stacked ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@cvr/stacked",
3
+ "version": "0.1.0",
4
+ "bin": {
5
+ "stacked": "./bin/stacked"
6
+ },
7
+ "files": [
8
+ "src",
9
+ "scripts",
10
+ "skills",
11
+ "tsconfig.json"
12
+ ],
13
+ "type": "module",
14
+ "scripts": {
15
+ "dev": "bun run src/main.ts",
16
+ "build": "bun run scripts/build.ts",
17
+ "link": "bun run build",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "oxlint",
20
+ "lint:fix": "oxlint --fix",
21
+ "fmt": "oxfmt",
22
+ "fmt:check": "oxfmt --check",
23
+ "test": "bun test",
24
+ "gate": "concurrently -n type,lint,fmt,test,build -c blue,yellow,magenta,green,cyan \"bun run typecheck\" \"bun run lint\" \"bun run fmt\" \"bun run test\" \"bun run build\"",
25
+ "version": "changeset version",
26
+ "release": "changeset publish",
27
+ "postinstall": "bun run build",
28
+ "prepare": "effect-language-service patch && lefthook install"
29
+ },
30
+ "dependencies": {
31
+ "@effect/platform-bun": "4.0.0-beta.12",
32
+ "effect": "4.0.0-beta.12"
33
+ },
34
+ "devDependencies": {
35
+ "@changesets/changelog-github": "^0.5.2",
36
+ "@changesets/cli": "^2.29.8",
37
+ "@effect/language-service": "^0.76.0",
38
+ "@types/bun": "^1.3.9",
39
+ "concurrently": "^9.2.1",
40
+ "effect-bun-test": "^0.2.1",
41
+ "lefthook": "^2.1.1",
42
+ "oxfmt": "^0.35.0",
43
+ "oxlint": "^1.50.0",
44
+ "typescript": "^5.9.3"
45
+ }
46
+ }
@@ -0,0 +1,53 @@
1
+ import { mkdirSync, lstatSync, unlinkSync, symlinkSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import * as os from "node:os";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const rootDir = join(__dirname, "..");
9
+
10
+ console.log("Building stacked...");
11
+
12
+ const binDir = join(rootDir, "bin");
13
+ mkdirSync(binDir, { recursive: true });
14
+
15
+ const platform =
16
+ process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux";
17
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
18
+
19
+ const buildResult = await Bun.build({
20
+ entrypoints: [join(rootDir, "src/main.ts")],
21
+ target: "bun",
22
+ minify: false,
23
+ compile: {
24
+ target: `bun-${platform}-${arch}`,
25
+ outfile: join(binDir, "stacked"),
26
+ autoloadBunfig: false,
27
+ },
28
+ });
29
+
30
+ if (!buildResult.success) {
31
+ console.error("Build failed:");
32
+ for (const log of buildResult.logs) {
33
+ console.error(log);
34
+ }
35
+ process.exit(1);
36
+ }
37
+
38
+ console.log(`Binary built: ${join(binDir, "stacked")}`);
39
+
40
+ const home = process.env["HOME"] ?? os.homedir();
41
+ const bunBin = join(home, ".bun", "bin", "stacked");
42
+ try {
43
+ try {
44
+ lstatSync(bunBin);
45
+ unlinkSync(bunBin);
46
+ } catch {
47
+ // doesn't exist
48
+ }
49
+ symlinkSync(join(binDir, "stacked"), bunBin);
50
+ console.log(`Symlinked to: ${bunBin}`);
51
+ } catch (e) {
52
+ console.log(`Could not symlink to ${bunBin}: ${e}`);
53
+ }
@@ -0,0 +1,181 @@
1
+ ---
2
+ name: stacked
3
+ description: Use the `stacked` CLI to manage stacked PRs. Use when the user wants to create branch stacks, rebase, sync, submit PRs, or navigate stacked branches. Triggers on "stacked", "stack", "stacked PRs", branch stacking workflows, or any git workflow involving parent-child branch relationships.
4
+ ---
5
+
6
+ # stacked
7
+
8
+ Branch-based stacked PR manager. Manages parent-child branch relationships, automates rebasing, creates/updates GitHub PRs via `gh`.
9
+
10
+ Key idea: **branches** are the unit, not commits. Each branch in a stack has exactly one parent — position in the stack determines lineage.
11
+
12
+ ## Navigation
13
+
14
+ ```
15
+ What do you need?
16
+ ├─ Start a new stack → §Creating a Stack
17
+ ├─ Add branches to a stack → §Creating a Stack
18
+ ├─ See current stack → §Viewing the Stack
19
+ ├─ Navigate between branches → §Navigation
20
+ ├─ Rebase after changes → §Rebasing
21
+ ├─ Push + create PRs → §Submitting
22
+ ├─ Adopt existing branches → §Adopting Branches
23
+ ├─ Remove a branch → §Deleting
24
+ └─ Troubleshooting → §Gotchas
25
+ ```
26
+
27
+ ## Quick Reference
28
+
29
+ | Command | What it does |
30
+ | ------------------------- | ---------------------------------------------- |
31
+ | `stacked trunk [name]` | Get/set trunk branch (default: main) |
32
+ | `stacked create <name>` | Create branch on top of current branch |
33
+ | `stacked list` | Show stack with current branch indicator |
34
+ | `stacked checkout <name>` | Switch to branch in stack |
35
+ | `stacked top` | Jump to top of stack |
36
+ | `stacked bottom` | Jump to bottom of stack |
37
+ | `stacked sync` | Fetch + rebase entire stack on trunk |
38
+ | `stacked restack` | Rebase children after mid-stack edits |
39
+ | `stacked delete <name>` | Remove branch from stack + delete git branch |
40
+ | `stacked submit` | Push all branches + create/update PRs via `gh` |
41
+ | `stacked adopt <branch>` | Add existing git branch into the stack |
42
+ | `stacked log` | Show commits grouped by branch |
43
+
44
+ ## Setup
45
+
46
+ ```sh
47
+ # Set trunk if not "main"
48
+ stacked trunk develop
49
+ ```
50
+
51
+ Requires `gh` CLI installed and authenticated for `submit`.
52
+
53
+ ## Creating a Stack
54
+
55
+ Start from trunk, build upward. Each `create` branches off the current branch.
56
+
57
+ ```sh
58
+ git checkout main
59
+ stacked create feat-auth # branches off main
60
+ # ... make commits ...
61
+ stacked create feat-auth-ui # branches off feat-auth
62
+ # ... make commits ...
63
+ stacked create feat-auth-tests # branches off feat-auth-ui
64
+ ```
65
+
66
+ Result: `main → feat-auth → feat-auth-ui → feat-auth-tests`
67
+
68
+ Use `--from` to branch from a specific branch instead of current:
69
+
70
+ ```sh
71
+ stacked create hotfix --from feat-auth
72
+ ```
73
+
74
+ ## Viewing the Stack
75
+
76
+ ```sh
77
+ stacked list # shows branches with ► on current
78
+ stacked log # shows commits grouped by branch
79
+ ```
80
+
81
+ ## Navigation
82
+
83
+ ```sh
84
+ stacked checkout feat-auth # switch to specific branch
85
+ stacked top # jump to top of stack
86
+ stacked bottom # jump to bottom (trunk-adjacent)
87
+ ```
88
+
89
+ ## Rebasing
90
+
91
+ ### After mid-stack changes
92
+
93
+ Edit a branch mid-stack, then rebase everything above it:
94
+
95
+ ```sh
96
+ stacked checkout feat-auth
97
+ # ... make changes, commit ...
98
+ stacked restack # rebases feat-auth-ui and feat-auth-tests
99
+ ```
100
+
101
+ ### Sync with trunk
102
+
103
+ Pull latest trunk and rebase the entire stack bottom-to-top:
104
+
105
+ ```sh
106
+ stacked sync
107
+ ```
108
+
109
+ This fetches, then rebases each branch onto its parent starting from the bottom.
110
+
111
+ ## Submitting
112
+
113
+ Push all stack branches and create/update GitHub PRs with correct base branches:
114
+
115
+ ```sh
116
+ stacked submit # push + create/update PRs
117
+ stacked submit --draft # create as draft PRs
118
+ stacked submit --force # force push
119
+ stacked submit --dry-run # show what would happen
120
+ ```
121
+
122
+ Each PR targets its parent branch (not trunk), preserving the stack structure on GitHub.
123
+
124
+ ## Adopting Branches
125
+
126
+ Bring an existing git branch into the stack:
127
+
128
+ ```sh
129
+ stacked adopt existing-branch # append to top
130
+ stacked adopt existing-branch --after feat-auth # insert after specific branch
131
+ ```
132
+
133
+ ## Deleting
134
+
135
+ ```sh
136
+ stacked delete feat-auth-ui # removes from stack + deletes git branch
137
+ stacked delete feat-auth-ui --force # skip confirmation
138
+ ```
139
+
140
+ ## Data Model
141
+
142
+ Stack metadata lives in `.git/stacked.json`. Each branch's parent is implied by array position:
143
+
144
+ - `branches[0]` → parent is trunk
145
+ - `branches[n]` → parent is `branches[n-1]`
146
+
147
+ A repo can have multiple independent stacks. The current stack is determined by which branch you're on.
148
+
149
+ ## Typical Workflow
150
+
151
+ ```sh
152
+ # 1. Start a stack
153
+ stacked create feat-auth
154
+ # ... work, commit ...
155
+
156
+ # 2. Stack more branches
157
+ stacked create feat-auth-ui
158
+ # ... work, commit ...
159
+
160
+ # 3. Need to fix something mid-stack
161
+ stacked checkout feat-auth
162
+ # ... fix, commit ...
163
+ stacked restack # rebase children
164
+
165
+ # 4. Sync with latest main
166
+ stacked sync
167
+
168
+ # 5. Submit for review
169
+ stacked submit --draft
170
+
171
+ # 6. After review, final submit
172
+ stacked submit
173
+ ```
174
+
175
+ ## Gotchas
176
+
177
+ - `stacked sync` rebases bottom-to-top — resolve conflicts one branch at a time
178
+ - `stacked submit` requires `gh` CLI authenticated (`gh auth login`)
179
+ - PRs target parent branches, not trunk — this is intentional for stacked review
180
+ - Trunk defaults to `main` — use `stacked trunk <name>` if your default branch differs
181
+ - Rebase conflicts mid-stack will pause the operation — resolve and re-run
@@ -0,0 +1,39 @@
1
+ import { Argument, Command, Flag } from "effect/unstable/cli";
2
+ import { Console, Effect, Option } from "effect";
3
+ import { GitService } from "../services/Git.js";
4
+ import { StackService } from "../services/Stack.js";
5
+ import { StackError } from "../errors/index.js";
6
+
7
+ const branchArg = Argument.string("branch");
8
+ const afterFlag = Flag.string("after").pipe(Flag.optional, Flag.withAlias("a"));
9
+
10
+ export const adopt = Command.make("adopt", { branch: branchArg, after: afterFlag }).pipe(
11
+ Command.withDescription("Adopt existing git branch into current stack"),
12
+ Command.withHandler(({ branch, after }) =>
13
+ Effect.gen(function* () {
14
+ const git = yield* GitService;
15
+ const stacks = yield* StackService;
16
+
17
+ const exists = yield* git.branchExists(branch);
18
+ if (!exists) {
19
+ return yield* new StackError({ message: `Branch "${branch}" does not exist` });
20
+ }
21
+
22
+ const result = yield* stacks.currentStack();
23
+ if (result === null) {
24
+ const trunk = yield* stacks.getTrunk();
25
+ const currentBranch = yield* git.currentBranch();
26
+ if (currentBranch === trunk) {
27
+ yield* stacks.createStack(branch, [branch]);
28
+ } else {
29
+ yield* stacks.createStack(currentBranch, [currentBranch, branch]);
30
+ }
31
+ } else {
32
+ const afterBranch = Option.isSome(after) ? after.value : undefined;
33
+ yield* stacks.addBranch(result.name, branch, afterBranch);
34
+ }
35
+
36
+ yield* Console.log(`Adopted ${branch} into stack`);
37
+ }),
38
+ ),
39
+ );
@@ -0,0 +1,29 @@
1
+ import { Command } from "effect/unstable/cli";
2
+ import { Console, Effect } from "effect";
3
+ import { GitService } from "../services/Git.js";
4
+ import { StackService } from "../services/Stack.js";
5
+
6
+ export const bottom = Command.make("bottom").pipe(
7
+ Command.withDescription("Checkout bottom branch of stack"),
8
+ Command.withHandler(() =>
9
+ Effect.gen(function* () {
10
+ const git = yield* GitService;
11
+ const stacks = yield* StackService;
12
+
13
+ const result = yield* stacks.currentStack();
14
+ if (result === null) {
15
+ yield* Console.error("Not on a stacked branch");
16
+ return;
17
+ }
18
+
19
+ const bottomBranch = result.stack.branches[0];
20
+ if (bottomBranch === undefined) {
21
+ yield* Console.error("Stack is empty");
22
+ return;
23
+ }
24
+
25
+ yield* git.checkout(bottomBranch);
26
+ yield* Console.log(`Switched to ${bottomBranch}`);
27
+ }),
28
+ ),
29
+ );
@@ -0,0 +1,15 @@
1
+ import { Argument, Command } from "effect/unstable/cli";
2
+ import { Effect } from "effect";
3
+ import { GitService } from "../services/Git.js";
4
+
5
+ const nameArg = Argument.string("name");
6
+
7
+ export const checkout = Command.make("checkout", { name: nameArg }).pipe(
8
+ Command.withDescription("Switch to branch in current stack"),
9
+ Command.withHandler(({ name }) =>
10
+ Effect.gen(function* () {
11
+ const git = yield* GitService;
12
+ yield* git.checkout(name);
13
+ }),
14
+ ),
15
+ );
@@ -0,0 +1,46 @@
1
+ import { Argument, Command, Flag } from "effect/unstable/cli";
2
+ import { Console, Effect, Option } from "effect";
3
+ import { GitService } from "../services/Git.js";
4
+ import { StackService } from "../services/Stack.js";
5
+
6
+ const nameArg = Argument.string("name");
7
+ const fromFlag = Flag.string("from").pipe(Flag.optional, Flag.withAlias("f"));
8
+
9
+ export const create = Command.make("create", { name: nameArg, from: fromFlag }).pipe(
10
+ Command.withDescription("Create a new branch on top of current branch in stack"),
11
+ Command.withHandler(({ name, from }) =>
12
+ Effect.gen(function* () {
13
+ const git = yield* GitService;
14
+ const stacks = yield* StackService;
15
+
16
+ const currentBranch = yield* git.currentBranch();
17
+ const baseBranch = Option.isSome(from) ? from.value : currentBranch;
18
+ const trunk = yield* stacks.getTrunk();
19
+
20
+ const data = yield* stacks.load();
21
+ let stackName: string | null = null;
22
+
23
+ for (const [sName, stack] of Object.entries(data.stacks)) {
24
+ if (stack.branches.includes(baseBranch)) {
25
+ stackName = sName;
26
+ break;
27
+ }
28
+ }
29
+
30
+ if (stackName === null) {
31
+ if (baseBranch === trunk) {
32
+ stackName = name;
33
+ yield* stacks.createStack(name, []);
34
+ } else {
35
+ stackName = baseBranch;
36
+ yield* stacks.createStack(baseBranch, [baseBranch]);
37
+ }
38
+ }
39
+
40
+ yield* git.createBranch(name, baseBranch);
41
+ yield* stacks.addBranch(stackName, name, baseBranch === trunk ? undefined : baseBranch);
42
+
43
+ yield* Console.log(`Created branch ${name} on top of ${baseBranch}`);
44
+ }),
45
+ ),
46
+ );
@@ -0,0 +1,55 @@
1
+ import { Argument, Command, Flag } from "effect/unstable/cli";
2
+ import { Console, Effect } from "effect";
3
+ import { GitService } from "../services/Git.js";
4
+ import { StackService } from "../services/Stack.js";
5
+ import { StackError } from "../errors/index.js";
6
+
7
+ const nameArg = Argument.string("name");
8
+ const forceFlag = Flag.boolean("force").pipe(Flag.withAlias("f"));
9
+
10
+ export const deleteCmd = Command.make("delete", { name: nameArg, force: forceFlag }).pipe(
11
+ Command.withDescription("Remove branch from stack and delete git branch"),
12
+ Command.withHandler(({ name, force }) =>
13
+ Effect.gen(function* () {
14
+ const git = yield* GitService;
15
+ const stacks = yield* StackService;
16
+
17
+ const currentBranch = yield* git.currentBranch();
18
+ const data = yield* stacks.load();
19
+
20
+ let stackName: string | null = null;
21
+ for (const [sName, stack] of Object.entries(data.stacks)) {
22
+ if (stack.branches.includes(name)) {
23
+ stackName = sName;
24
+ break;
25
+ }
26
+ }
27
+
28
+ if (stackName === null) {
29
+ return yield* new StackError({ message: `Branch "${name}" not found in any stack` });
30
+ }
31
+
32
+ const stack = data.stacks[stackName];
33
+ if (stack === undefined) {
34
+ return yield* new StackError({ message: `Stack "${stackName}" not found` });
35
+ }
36
+ const idx = stack.branches.indexOf(name);
37
+
38
+ if (idx < stack.branches.length - 1 && !force) {
39
+ return yield* new StackError({
40
+ message: `Branch "${name}" has children. Use --force to delete anyway.`,
41
+ });
42
+ }
43
+
44
+ if (currentBranch === name) {
45
+ const parent = idx === 0 ? data.trunk : (stack.branches[idx - 1] ?? data.trunk);
46
+ yield* git.checkout(parent);
47
+ }
48
+
49
+ yield* stacks.removeBranch(stackName, name);
50
+ yield* git.deleteBranch(name, force);
51
+
52
+ yield* Console.log(`Deleted ${name}`);
53
+ }),
54
+ ),
55
+ );
@@ -0,0 +1,34 @@
1
+ import { Command } from "effect/unstable/cli";
2
+ import { trunk } from "./trunk.js";
3
+ import { create } from "./create.js";
4
+ import { list } from "./list.js";
5
+ import { checkout } from "./checkout.js";
6
+ import { top } from "./top.js";
7
+ import { bottom } from "./bottom.js";
8
+ import { sync } from "./sync.js";
9
+ import { restack } from "./restack.js";
10
+ import { deleteCmd } from "./delete.js";
11
+ import { submit } from "./submit.js";
12
+ import { adopt } from "./adopt.js";
13
+ import { log } from "./log.js";
14
+
15
+ const root = Command.make("stacked").pipe(
16
+ Command.withDescription("Branch-based stacked PR manager"),
17
+ );
18
+
19
+ export const command = root.pipe(
20
+ Command.withSubcommands([
21
+ trunk,
22
+ create,
23
+ list,
24
+ checkout,
25
+ top,
26
+ bottom,
27
+ sync,
28
+ restack,
29
+ deleteCmd,
30
+ submit,
31
+ adopt,
32
+ log,
33
+ ]),
34
+ );
@@ -0,0 +1,50 @@
1
+ import { Command } from "effect/unstable/cli";
2
+ import { Console, Effect } from "effect";
3
+ import { GitService } from "../services/Git.js";
4
+ import { StackService } from "../services/Stack.js";
5
+
6
+ export const list = Command.make("list").pipe(
7
+ Command.withDescription("Show current stack with branch status"),
8
+ Command.withHandler(() =>
9
+ Effect.gen(function* () {
10
+ const git = yield* GitService;
11
+ const stacks = yield* StackService;
12
+
13
+ const currentBranch = yield* git.currentBranch();
14
+ const data = yield* stacks.load();
15
+ const trunk = data.trunk;
16
+
17
+ let currentStackName: string | null = null;
18
+ for (const [name, stack] of Object.entries(data.stacks)) {
19
+ if (stack.branches.includes(currentBranch)) {
20
+ currentStackName = name;
21
+ break;
22
+ }
23
+ }
24
+
25
+ if (currentStackName === null) {
26
+ yield* Console.log("Not on a stacked branch");
27
+ return;
28
+ }
29
+
30
+ const stack = data.stacks[currentStackName];
31
+ if (stack === undefined) return;
32
+ const lines: string[] = [];
33
+
34
+ lines.push(`Stack: ${currentStackName}`);
35
+ lines.push(`Trunk: ${trunk}`);
36
+ lines.push("");
37
+
38
+ for (let i = stack.branches.length - 1; i >= 0; i--) {
39
+ const branch = stack.branches[i];
40
+ if (branch === undefined) continue;
41
+ const isCurrent = branch === currentBranch;
42
+ const marker = isCurrent ? "* " : " ";
43
+ const prefix = i === 0 ? "└─" : "├─";
44
+ lines.push(`${marker}${prefix} ${branch}`);
45
+ }
46
+
47
+ yield* Console.log(lines.join("\n"));
48
+ }),
49
+ ),
50
+ );
@@ -0,0 +1,34 @@
1
+ import { Command } from "effect/unstable/cli";
2
+ import { Console, Effect } from "effect";
3
+ import { GitService } from "../services/Git.js";
4
+ import { StackService } from "../services/Stack.js";
5
+
6
+ export const log = Command.make("log").pipe(
7
+ Command.withDescription("Show commits across all branches in stack"),
8
+ Command.withHandler(() =>
9
+ Effect.gen(function* () {
10
+ const git = yield* GitService;
11
+ const stacks = yield* StackService;
12
+
13
+ const result = yield* stacks.currentStack();
14
+ if (result === null) {
15
+ yield* Console.error("Not on a stacked branch");
16
+ return;
17
+ }
18
+
19
+ const trunk = yield* stacks.getTrunk();
20
+ const { branches } = result.stack;
21
+
22
+ for (let i = 0; i < branches.length; i++) {
23
+ const branch = branches[i];
24
+ if (branch === undefined) continue;
25
+ const base = i === 0 ? trunk : (branches[i - 1] ?? trunk);
26
+ yield* Console.log(`\n── ${branch} ──`);
27
+ const rangeLog = yield* git
28
+ .log(`${base}..${branch}`, { oneline: true })
29
+ .pipe(Effect.catch(() => Effect.succeed("(no commits)")));
30
+ yield* Console.log(rangeLog || "(no new commits)");
31
+ }
32
+ }),
33
+ ),
34
+ );