npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gh-stack

v0.17.1

Published

Stacked PR manager for squash-merge workflows

Readme

gh-stack

Stacked PR manager for squash-merge workflows.

Manages stacked pull requests with metadata stored locally under .git/.gh-stack/ (never committed). Designed for repositories that use squash-merge (where tools like Graphite break down). Inspired by Graphite (gt) but works directly against GitHub — no backend required.

Install

# From source (requires Bun)
bun install
bun run build
ln -s $(pwd)/dist/gh-stack ~/.local/bin/gh-stack

# Or install globally
bun install -g gh-stack

Prerequisites

Terms

| Term | Definition | |------|-----------| | trunk | Base branch of the repository (usually main) | | stack | A chain of dependent branches | | upstack | Branches that depend on the current branch (children) | | downstack | Branches the current branch depends on (ancestors) |

Quick Start

# You have branches: main → feat-1 → feat-2
# Go to the top of the chain and init:
git checkout feat-2
gh-stack init                    # Auto-detects the chain!

# Or start fresh:
git checkout kiliman/first-pr
gh-stack init                    # Creates stack with this branch
gh-stack create kiliman/second-pr  # Branch off current, add to stack

# Push everything to GitHub and create PRs
gh-stack submit

# View the stack
gh-stack log                     # tree view (default)
gh-stack ls                      # numbered list

# Navigate
gh-stack up                      # move to child branch
gh-stack down                    # move to parent branch
gh-stack top                     # jump to tip of stack
gh-stack bottom                  # jump to base of stack

# Sync with main and restack
gh-stack sync

# Check PR status
gh-stack status

# Split a long in-review chain so new work becomes its own stack
gh-stack split feat-12           # feat-12.. → new stack based on feat-11
# ...later, after the original stack merges to main:
gh-stack restack --onto main     # re-root the split stack onto main

Commands

Core Workflow

init [--name <name>] [--description <desc>] [--parent <branch>]
    Create a new stack from the current branch. Auto-detects branch
    chains — if you're at the top of main → feat-1 → feat-2, all
    branches are added to the stack automatically.

create <branch-name> [--description <desc>]
    Create a new git branch off the current branch and add it to
    the stack. The current branch must already be tracked.

add [branch]
    Track an EXISTING local branch (default: the current branch) in a
    stack — without pushing or opening a PR. Walks local ancestry: if the
    branch sits on top of an existing stack it's adopted into it (parent
    auto-detected); otherwise a new stack is created from the chain.

    Use this for a WIP branch you've created with `git checkout -b` but
    aren't ready to submit. It fills the gap between creating a branch and
    `submit`: until a branch is tracked, `sync`/`restack`/navigation don't
    know about it, and `add` is how you track it without publishing. (When
    you do run `submit`, it auto-tracks too — `add` is the no-push path.)

submit [-d|--draft] [-n|--no-edit] [-t|--title <t>] [-b|--body <b>] [--body-file <f>] [--restack] [--dry-run]
    Push all downstack branches to GitHub, create PRs for branches
    that don't have them, number each PR title with its stack position
    `(N/M)`, and update all PR descriptions with stack visualization.
    Idempotent — safe to run repeatedly.

    --restack: after submitting, restack the branches above the current
    one onto it. submit covers DOWNSTACK (trunk → current); restack covers
    UPSTACK (the children), so together they sync the whole stack from
    wherever you're standing. The everyday loop: drop downstack, fix an
    issue, commit, then `submit --restack` to push the fix and propagate
    it up in one command. Skips cleanly when nothing sits above you.

    Self-healing: if the current branch isn't tracked in a stack yet,
    submit auto-detects the chain from trunk → current, registers (or
    reconciles into) a stack, then pushes + creates PRs for the whole
    chain. You never need to run `gh-stack init` first — running submit
    from the top of a bare chain of local branches converges to the
    expected end state (stack registered, branches pushed, PRs created).

    --title/-t and --body/-b set the title/body for the branch you're on.
    They work whether the PR is new OR already exists — on an existing PR
    they update it in place: the title keeps its (N/M) stack position and
    the body is replaced with the "📚 Stacked on" block re-merged in. Use
    this instead of `gh pr edit` (which would wipe the stack viz block) to
    let an agent or script regenerate a PR's title/description safely.
    --body-file reads the body from a file. In --yes mode without --title,
    titles are auto-generated from branch names.

Stack Navigation

checkout [<branch>]
    Switch to a branch by name, or interactive picker.
    --stack    Switch between stacks instead of branches
    (alias: co)

up [steps]
    Move to child branch (upstack). Prompts if multiple children.

down [steps]
    Move to parent branch (downstack).

top
    Jump to the tip (leaf) of the current stack.

bottom
    Jump to the base (first branch above trunk).

ls
    List branches with position numbers.

Stack Management

restack [--resume] [--dry-run] [--verbose]
    Rebase the current branch and all descendants onto their parents.
    Uses metadata snapshots to recover the correct rebase base even
    after a parent's history has been rewritten.

    On conflict:
        git rebase --continue
        gh-stack restack --resume

    (alias: rebase)

restack --onto <ref>
    Re-root the current stack onto a new base ref, changing the stack's
    base. Use this to move a split stack off its parent-stack branch and
    onto main once the parent stack has merged — only the stack's own
    commits replay onto the new base.

sync [--dry-run]
    Fetch main, rebase the base branch onto main, then restack all
    children. Snapshots every branch's pre-sync tip so children can be
    correctly rebased even though their parent's history was rewritten.
    If the bottom PR(s) already merged on main outside gh-stack (e.g.
    the GitHub web UI), sync detects this and advances the stack past
    them — re-rooting the survivor onto main with `--onto` so only its
    own commits replay — instead of replaying the now-squashed commits
    and conflicting.

merge [--dry-run] [-d|--delete-branch] [--collapse]
merge --approved [--collapse] [--dry-run] [-d|--delete-branch]
merge --base [--dry-run] [-d|--delete-branch]
    Default (top-down collapse): squash-merge the whole stack top-down
    via GitHub (PR3 → PR2 → PR1), then enables auto-merge for the base PR
    into main. The whole stack lands on main as a single squash commit.
    All merges happen on GitHub so PRs show as "Merged", Linear tickets
    close automatically, and GitHub Actions fire normally. Skips already-
    merged PRs (safe to re-run). Waits for GitHub between merges, and
    self-heals its spurious "Head branch is out of date" (a stale PR
    head pointer after a child squash) by re-syncing and retrying. Before
    handing the base PR to auto-merge it confirms the PR is mergeable: a
    real conflict with main stops the merge (the stack is left intact —
    resolve with `gh-stack sync`, then re-run), while merely waiting on
    pending checks is fine (auto-merge gates on them).

    --approved  Collapse only the approved run. Walks approval from the
                BASE up and stops at the first PR that isn't approved
                (contiguous — an unapproved PR4 caps the run at PR3 even
                if PR5 is approved). Collapses base..highest-approved into
                a single squash commit on main and leaves the unapproved
                tail in place; after the base PR lands, run `gh-stack sync`
                to re-root the tail onto main. All approved ⇒ same as a
                full merge.
    --base      Squash-merge ONLY the bottom PR into main as its own
                commit, then re-root the next branch onto main (replaying
                just its own commits). One PR per run — run it again to
                land the next. Approval is enforced by GitHub branch
                protection, not gh-stack. A base PR already merged outside
                gh-stack is detected and advanced past.
    --collapse  With the default or --approved, stop after collapsing into
                the base PR WITHOUT merging it to main, so you can review
                the cumulative diff on GitHub. Re-run `gh-stack merge` to
                finish. Alias: --stop-at-base.

split [<branch>] [--name <name>]
    Cut a stack into two at <branch>. The cut branch and every branch
    above it move into a NEW stack whose base is the cut branch's parent
    (which stays in the original stack). The target stack is the one
    containing <branch>, so this works from anywhere — not only while
    standing on it. Purely a metadata operation — no git branches are
    moved or rebased. Interactive selector (current stack) if no branch
    is given; you can't split at the stack's root.

    Use it when a long chain is in review and can't merge yet, but new
    work is piling on top: split at the first "new work" branch so the
    original stack stays the review unit and the new stack rides on its
    tip. Once the original stack merges, re-root the new stack with
    `gh-stack restack --onto main`.

rename [<old-name>] <new-name>
    Rename a tracked branch (runs `git branch -m`) and re-key the stack
    topology so it stays tracked, re-parenting any children that pointed
    at the old name. With one argument, renames the current branch. Built
    for the throwaway-branch → ticketed-PR flow (`feature-wip` →
    `feature-BEE-1234`) without hand-editing metadata.

    You don't strictly need this command: gh-stack also auto-reconciles
    after a plain `git branch -m` (the per-branch git config is rename-
    proof, so the next gh-stack command heals the name-keyed topology).
    Renaming a branch that already has a PR is local-only — the GitHub PR
    still tracks the old remote branch.

delete [<branch>] [-k|--keep-branch] [--no-remote]
    Remove a branch from its stack and re-parent its children, then
    delete the underlying local git branch (and the remote branch if
    it was pushed). The <branch> argument names its own stack, so this
    works from anywhere. Interactive selector (current stack) if none
    specified.
    --keep-branch leaves the git branches untouched (metadata only);
    --no-remote deletes the local branch but keeps the remote.

Info & Maintenance

log
    Display the current stack as a tree with branch numbers,
    PR info, and descriptions. This is the default command.

    If the current branch isn't tracked yet, log inspects its ancestry
    and points at the next step: if it sits on top of an existing stack,
    `gh-stack submit` to add it; if it's stacked on untracked branches,
    submit to create the stack and open PRs; if it's on trunk, `init`.

update-prs [--force]
    Refresh the "📚 Stacked on" visualization block in every PR
    description for the current stack. PRs whose rendered block is
    unchanged are skipped; --force rewrites all of them. (submit does
    this automatically — use this to refresh without pushing.)

stacks [--current] [--json]
    List all stacks and their topology. Read-only and network-free —
    emits stack membership straight from local metadata, so external
    tools (status scripts, dashboards) can consume --json as a stable
    interface instead of parsing the .git/.gh-stack/ store directly.
    --current  Only the stack containing the current branch
    --json     Topology as JSON: { current_stack, current_branch,
               stacks: [{ name, description, base, is_current,
               branches: [{ branch, parent, pr, description }] }] }

status [--current] [--json]
    PR dashboard showing review state, CI status, and merge readiness.
    Unlike `stacks`, this hits the network to fetch live PR/CI status.
    --current  Show only the current stack or standalone PR
    --json     Structured JSON output (progress goes to stderr)

undo
    Restore the last snapshot taken before a destructive operation.

archive [--restore <name>]
    List archived stacks by default, or restore one by name.

doctor
    Migrate old (v2) metadata to the v3 layout, reconcile git branch
    config against the topology files, and flag stacks whose base
    stack appears already-merged into main. Safe to run repeatedly.

learn [--skill] [--harness <name>] [--global] [--force]
    Teach a coding agent how to drive gh-stack. By default prints the
    canonical skill (Markdown, stamped with the running version) to
    stdout — the text is compiled into the binary, so it never drifts
    out of sync with the installed version. --skill installs it as a
    skill file instead, choosing the path from the harness:
      Claude Code  <root>/.claude/skills/using-gh-stack/SKILL.md
      Codex        <root>/.codex/skills/using-gh-stack/SKILL.md
      Cursor       <root>/.cursor/skills/using-gh-stack/SKILL.md
    --harness  claude | codex | cursor (skips the interactive prompt)
    --global   Install under ~/.<harness>/ instead of the project root
    --force    Overwrite an existing skill file without confirming

Global Options

--yes, -y      Skip all confirmations (for agents/CI)
--plain        Plain output — no spinners, colors, or banner boxes
--help         Show help for a command
--version, -V  Show version

--plain is auto-enabled when --yes / GH_STACK_YES=1 is set, so agents get clean, easily-filtered output by default. Use --plain alone if you want plain output but still interactive prompts.

Environment Variables

GH_STACK_YES=1         Skip all confirmations (same as --yes; also enables --plain)
GH_STACK_PLAIN=1       Plain output (same as --plain)
GH_STACK_NO_COLOR=1    Disable colored output

How It Works

Smart Init

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.

Snapshot-Based Rebasing

The critical insight: after a parent branch's history is rewritten (e.g., rebased onto a new main), git merge-base(child, parent) falls all the way back to the original main — and using that as a rebase base would replay the parent's old commits onto the child, producing ghost-conflicts on the parent's own work.

gh-stack solves this by snapshotting every branch's tip before any destructive operation. When restacking a child whose parent has been rewritten, gh-stack walks the snapshots newest-first and finds the most recent recorded tip that's no longer an ancestor of the parent's current tip. That orphaned SHA is the correct rebase base — git rebase --onto <new-parent-tip> <orphaned-old-tip> <child> replays only the child's unique commits.

Snapshots also power gh-stack undo, so the same data structure does double duty.

Stack Position in PR Titles

submit keeps each PR's title suffixed with its stack position as (N/M), so you can see the order from a bare "needs your review" list — where only titles show, not descriptions — and review bottom-up:

feat(paid-subs): tiers overview list [BEE-20531] (1/4)
feat(paid-subs): tier detail page [BEE-20550] (2/4)
feat(paid-subs): tier create wizard [BEE-20579] (3/4)
feat(paid-subs): advanced settings step [BEE-20587] (4/4)

Parentheses (not brackets) avoid colliding with [BEE-1234] ticket tags. It's self-healing and idempotent: re-running submit after adding or reordering a branch renumbers the whole stack, and a no-op re-submit changes nothing. The position is the only managed part of the title — your text (and ticket tags) are preserved. Single-PR stacks get no suffix.

Stack Visualization

submit also adds a stack section to every PR description:

### 📚 Stacked on
⚫ main
┃
┣━ #123 feat: backend models (1/2)
┃
┗━ #124 feat: frontend UI (2/2) 👈

The block is rendered entirely from local metadata plus one repo-identity lookup for links — no per-PR API call — and submit only rewrites a PR description whose block actually changed (membership/order/title), so a re-submit that touched one branch on a 12-PR stack does ~1 update, not 12. Review/CI status is intentionally not shown here. Run gh-stack update-prs (--force to rewrite all) to refresh manually.

Metadata (v3)

Stack metadata lives under .git/.gh-stack/ (never committed) — a folder of per-stack files plus git-native branch config, rather than a single JSON blob:

.git/.gh-stack/
  current                      derived stack of the current branch (self-heals on read)
  active/<stack>.json          topology of a live stack (ordered branches, base, description)
  archived/<stack>.json        merged/closed stacks
  deleted/<stack>.json         tombstones (recoverable)
  snapshots/<ts>__<stack>.json append-only, retained per-stack
  restack-state.json           in-flight restack/sync resume state

A single active/<stack>.json looks like:

{
  "description": "",
  "last_branch": "kiliman/feature-2",
  "base": "main",
  "branches": {
    "kiliman/feature-1": { "parent": "main", "pr": 21729 },
    "kiliman/feature-2": { "parent": "kiliman/feature-1", "pr": 21730 }
  }
}

Per-branch membership is also recorded in git's own config, so renaming or deleting a branch updates/cleans it automatically:

branch.<name>.ghstack-stack   <stack>
branch.<name>.ghstack-parent  <parent-branch>
branch.<name>.ghstack-pr      <number>
branch.<name>.ghstack-id      <uuid>   stable identity (survives rename)

The two representations cross-check each other; gh-stack doctor reconciles any drift. Why this shape:

  • A stack can't silently vanish — lifecycle transitions are file moves (active/ → archived/ → deleted/), and a stale stack file is tombstoned, never just unlinked.
  • No current_stack drift — the stack you're on is derived from the branch you've checked out (its own membership, or the nearest tracked ancestor for a new branch stacked on top), re-resolved and self-healed on every read, so a written pointer can never contradict reality.
  • No all-or-nothing blast radius — one bad write can't corrupt other stacks.
  • Renames heal themselvesgit branch -m old new moves the whole branch.<old> config section to branch.<new> (id and all), but the JSON is keyed by name and goes stale. On the next command, gh-stack matches the moved config to its stale JSON entry by ghstack-id (or PR number for older branches), re-keys it, and re-points any children. So gh-stack rename — or even a raw git branch -m — never needs manual metadata surgery.

Migrating from v2

Repos created before v3 store a single .git/gh-stack-metadata.json. Run gh-stack doctor once — it fans the monolith out into the layout above, backfills branch config, explodes snapshots into per-file records, and keeps a .bak of the old file. Commands refuse to run on unmigrated metadata and point you at doctor.

Snapshots

Before any destructive operation (restack, sync, merge, delete), gh-stack saves a snapshot of all branch HEADs as an append-only file under snapshots/, retained per-stack. Run gh-stack undo to restore.

Example Workflow

# Start from an existing branch with a PR
git checkout kiliman/api-layer-WEB-1234
gh-stack init

# Create second PR on top
gh-stack create kiliman/frontend-WEB-1234
# ... code, commit ...

# Push everything and create PRs
gh-stack submit

# Later: sync everything with main
gh-stack sync

# Navigate the stack
gh-stack up          # go to child
gh-stack down        # go to parent
gh-stack top         # jump to tip

# Check status
gh-stack status

# When the whole stack is approved, merge it down as one squash commit
gh-stack merge       # squash-merges down, pushes, enables auto-merge

# Or: only the approved part is ready — collapse the approved run into one
# commit and leave the rest stacked (re-root the tail later with `sync`)
gh-stack merge --approved   # collapses base..highest-approved → main, keeps the tail

# Or: land just the bottom PR, one at a time
gh-stack merge --base       # merges the bottom PR → main, re-roots the next

# Or: collapse first to review the cumulative diff before shipping
gh-stack merge --collapse   # squash-merges PRn..PR2 into PR1, stops there
# ...review base PR on GitHub...
gh-stack merge              # finishes base PR → main + archives the stack

Agent/CI Usage

gh-stack is designed to be used by AI agents and CI pipelines. The fastest way to onboard an agent is gh-stack learn — it prints a version-matched skill (or installs one with --skill) so the agent learns the current commands, flags, and gotchas straight from the binary:

# Teach an agent the current API (always in sync with the installed version)
gh-stack learn                              # print the skill to stdout
gh-stack learn --skill --harness claude     # install it for Claude Code
# Non-interactive mode — all prompts auto-resolved
export GH_STACK_YES=1

gh-stack init                    # No confirmations
gh-stack submit -t "Title [WEB-1234]" -b "Description"  # Explicit PR details
gh-stack submit -n               # Or auto-generate titles
gh-stack sync
gh-stack restack

# Structured output
gh-stack status --json
gh-stack status --current --json
gh-stack ls

License

MIT