gh-stack 0.2.1 → 0.2.3

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
@@ -78,12 +78,14 @@ create <branch-name> [--description <desc>]
78
78
  Create a new git branch off the current branch and add it to
79
79
  the stack. The current branch must already be tracked.
80
80
 
81
- submit [-d|--draft] [-n|--no-edit] [--dry-run]
81
+ submit [-d|--draft] [-n|--no-edit] [-t|--title <t>] [-b|--body <b>] [--body-file <f>] [--dry-run]
82
82
  Push all downstack branches to GitHub, create PRs for branches
83
83
  that don't have them, and update all PR descriptions with stack
84
84
  visualization. Idempotent — safe to run repeatedly.
85
85
 
86
- In --yes mode, auto-generates PR titles from branch names.
86
+ --title/-t and --body/-b provide PR details directly (skips prompts).
87
+ --body-file reads body from a file. In --yes mode without --title,
88
+ auto-generates PR titles from branch names.
87
89
  ```
88
90
 
89
91
  ### Stack Navigation
@@ -129,8 +131,9 @@ sync [--dry-run]
129
131
 
130
132
  merge [--dry-run]
131
133
  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.
134
+ then rebases onto main, closes intermediate PRs, pushes the
135
+ base branch, and enables GitHub auto-merge (squash). Archives
136
+ the stack on completion.
134
137
 
135
138
  delete [<branch>]
136
139
  Remove a branch from the stack and re-parent its children.
@@ -251,7 +254,7 @@ gh-stack top # jump to tip
251
254
  gh-stack status
252
255
 
253
256
  # When PRs are approved, merge the stack
254
- gh-stack merge
257
+ gh-stack merge # squash-merges down, pushes, enables auto-merge
255
258
  ```
256
259
 
257
260
  ## Agent/CI Usage
@@ -263,7 +266,8 @@ gh-stack is designed to be used by AI agents and CI pipelines:
263
266
  export GH_STACK_YES=1
264
267
 
265
268
  gh-stack init # No confirmations
266
- gh-stack submit # Auto-generates PR titles
269
+ gh-stack submit -t "Title [WEB-1234]" -b "Description" # Explicit PR details
270
+ gh-stack submit -n # Or auto-generate titles
267
271
  gh-stack sync
268
272
  gh-stack restack
269
273
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-stack",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Stacked PR manager for squash-merge workflows",
5
5
  "author": "Kiliman <kiliman@gmail.com>",
6
6
  "license": "MIT",
@@ -6,6 +6,7 @@ import * as git from "../lib/git.ts";
6
6
  import { findStackForBranch, getOrderedBranches, writeMetadata } from "../lib/metadata.ts";
7
7
  import { ensureMetadata, ensureCleanWorkingTree, ensureValidStack } from "../lib/safety.ts";
8
8
  import { takeSnapshot } from "../lib/snapshot.ts";
9
+ import { $ } from "bun";
9
10
  import { closePr } from "../lib/github.ts";
10
11
  import { confirmAction } from "../lib/ui.ts";
11
12
 
@@ -167,10 +168,40 @@ This keeps all commits local (avoiding orphaned squash commits).
167
168
  }
168
169
  await writeMetadata(meta);
169
170
 
170
- p.outro(pc.green("Stack merge complete! Stack archived."));
171
-
171
+ // Push base branch and enable auto-merge
172
172
  console.log();
173
- console.log(" Next steps:");
174
- console.log(` 1. Push ${pc.yellow(baseBranch)} and let CI run`);
175
- console.log(` 2. Squash-merge ${pc.yellow(baseBranch)} into main via GitHub`);
173
+ const pushAndMerge = await confirmAction(
174
+ `Push ${pc.yellow(baseBranch)} and enable auto-merge (squash)?`,
175
+ );
176
+
177
+ if (pushAndMerge) {
178
+ const pushSpinner = p.spinner();
179
+ pushSpinner.start(`Pushing ${pc.blue(baseBranch)}...`);
180
+ const pushOk = await git.forcePushWithLease(baseBranch);
181
+ if (pushOk) {
182
+ pushSpinner.stop(`Pushed ${pc.blue(baseBranch)}`);
183
+ } else {
184
+ pushSpinner.stop(pc.red(`Failed to push ${baseBranch}`));
185
+ p.outro(
186
+ pc.yellow("Stack merged locally but push failed. Push manually and merge via GitHub."),
187
+ );
188
+ return;
189
+ }
190
+
191
+ // Enable auto-merge via gh CLI
192
+ const basePrNum = stack.branches[baseBranch]?.pr;
193
+ if (basePrNum) {
194
+ const mergeSpinner = p.spinner();
195
+ mergeSpinner.start("Enabling auto-merge...");
196
+ try {
197
+ await $`gh pr merge ${basePrNum} --squash --auto`.quiet();
198
+ mergeSpinner.stop(`Auto-merge enabled for PR #${basePrNum} — will merge when CI passes`);
199
+ } catch {
200
+ mergeSpinner.stop(pc.yellow(`Could not enable auto-merge for PR #${basePrNum}`));
201
+ p.log.info(`Enable manually: ${pc.dim(`gh pr merge ${basePrNum} --squash --auto`)}`);
202
+ }
203
+ }
204
+ }
205
+
206
+ p.outro(pc.green("Stack merge complete! Stack archived."));
176
207
  }
@@ -20,15 +20,19 @@ for branches that don't have them and updating stack visualization
20
20
  in all PR descriptions. Idempotent — safe to run repeatedly.
21
21
 
22
22
  OPTIONS
23
- -d, --draft Create new PRs as drafts
24
- -n, --no-edit Don't prompt for PR titles (auto-generate from branch name)
25
- --dry-run Show what would happen without doing anything
23
+ -d, --draft Create new PRs as drafts
24
+ -n, --no-edit Don't prompt for PR titles (auto-generate from branch name)
25
+ -t, --title <title> PR title for new PRs (skips prompt)
26
+ -b, --body <body> PR body/description for new PRs
27
+ --body-file <f> Read PR body from a file
28
+ --dry-run Show what would happen without doing anything
26
29
 
27
30
  EXAMPLES
28
- gh-stack submit # Push + create PRs interactively
29
- gh-stack submit -n # Push + create PRs with auto-titles
30
- gh-stack submit --draft # Create new PRs as drafts
31
- gh-stack submit --dry-run # Preview what would happen
31
+ gh-stack submit # Push + create PRs interactively
32
+ gh-stack submit -n # Push + create PRs with auto-titles
33
+ gh-stack submit -t "My PR" -b "Description" # Explicit title and body
34
+ gh-stack submit --draft # Create new PRs as drafts
35
+ gh-stack submit --dry-run # Preview what would happen
32
36
  `;
33
37
 
34
38
  /**
@@ -79,7 +83,32 @@ export default async function submit(args: string[]): Promise<void> {
79
83
 
80
84
  const draftFlag = args.includes("--draft") || args.includes("-d");
81
85
  const dryRun = args.includes("--dry-run");
82
- const noEdit = args.includes("--no-edit") || args.includes("-n") || isAutoYes();
86
+
87
+ // Parse value flags
88
+ let titleFlag: string | undefined;
89
+ let bodyFlag: string | undefined;
90
+ let bodyFileFlag: string | undefined;
91
+ for (let i = 0; i < args.length; i++) {
92
+ if ((args[i] === "--title" || args[i] === "-t") && args[i + 1]) {
93
+ titleFlag = args[++i];
94
+ } else if ((args[i] === "--body" || args[i] === "-b") && args[i + 1]) {
95
+ bodyFlag = args[++i];
96
+ } else if (args[i] === "--body-file" && args[i + 1]) {
97
+ bodyFileFlag = args[++i];
98
+ }
99
+ }
100
+
101
+ // Resolve body from file if provided
102
+ if (bodyFileFlag && !bodyFlag) {
103
+ try {
104
+ bodyFlag = await Bun.file(bodyFileFlag).text();
105
+ } catch {
106
+ p.cancel(`Could not read body file: ${bodyFileFlag}`);
107
+ process.exit(1);
108
+ }
109
+ }
110
+
111
+ const noEdit = args.includes("--no-edit") || args.includes("-n") || isAutoYes() || !!titleFlag;
83
112
 
84
113
  const meta = await ensureMetadata();
85
114
  const branch = await git.currentBranch();
@@ -166,7 +195,7 @@ export default async function submit(args: string[]): Promise<void> {
166
195
  }
167
196
  } else {
168
197
  // ── Create new PR ──
169
- const defaultTitle = branchToTitle(branchName);
198
+ const defaultTitle = titleFlag || branchToTitle(branchName);
170
199
  let title = defaultTitle;
171
200
 
172
201
  if (!noEdit && !dryRun) {
@@ -182,6 +211,8 @@ export default async function submit(args: string[]): Promise<void> {
182
211
  title = (input as string).trim() || defaultTitle;
183
212
  }
184
213
 
214
+ const body = bodyFlag ?? "";
215
+
185
216
  if (dryRun) {
186
217
  p.log.step(
187
218
  `${pc.dim("create")} PR for ${branchName} → ${parentBranch} "${title}"${draftFlag ? " (draft)" : ""}`,
@@ -201,7 +232,7 @@ export default async function submit(args: string[]): Promise<void> {
201
232
  "--title",
202
233
  title,
203
234
  "--body",
204
- "",
235
+ body,
205
236
  ];
206
237
  if (draftFlag) ghArgs.push("--draft");
207
238