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

@kud/gh-cockpit

v0.8.0

Published

A configurable GitHub cockpit for the terminal — your PRs, reviews and issues in one Ink TUI, grouped by whose move it is.

Readme

🛫

Cockpit

TypeScript Node.js npm MIT

A configurable GitHub cockpit for the terminal — your PRs, reviews and issues in one Ink TUI, grouped by whose move it is.

FeaturesQuick StartBuild a CLIAPI Reference

🌟 Features

  • 🎯 Whose-move bands — every tab splits into Your move and Their move, so the two rows you can act on stop hiding among the eighteen you can't
  • 🧭 Standing, not guesswork — the same is your afternoon on a PR you wrote and the author's on one you were asked to review; the band knows which
  • 🔍 --here scoping — resolves the repo from your git remote and scopes every search server-side, so other repos are never fetched at all
  • 🎛️ Pattern filtering--include acme/*,me/acme-* and --exclude, replacing any built-in notion of which repos are "yours"
  • 🔌 Bring your own CI — GitHub Actions logs are built in; register a drill-in for anything else, or let it open in a browser
  • Readable without colour — every state carries its own glyph, so the list survives being piped, logged, or read by someone who can't separate green from orange
  • 🪶 No opinions baked in — repo ranking, checkout locations and cache lifetime are all supplied by the host; the defaults are empty on purpose

🚀 Quick Start

Everything here shells out to the GitHub CLI, so before anything else:

  • gh installed and authenticatedgh auth login. Every fetch and every action runs through it, and an unauthenticated gh is the most likely reason a first run shows nothing.
  • Node 22 or newer.
  • A terminal that renders the glyphs you choose to pass it. Nothing here requires a Nerd Font — the defaults are plain — but a host that supplies one will want a font that has it.
npm install @kud/gh-cockpit

It is a library, not a CLI — there is no bin, because which searches become tabs and which repos rank first are not portable decisions. You write a short entry point; see Build your own CLI for a complete one.

What it renders:

$ cockpit

  🚀 Cockpit    29 items · @you                          updated 2m ago
  Mine (7)  Review (20)  Incoming (4)  Issues (3)  Done (13)
            ───────────

  » Your move (2)

  ── acme/api-gateway ─────
  ◆ ← #1495  support statementPeriodIds in the report      6d
  ── acme/event-bus ───────
  ·   #290   delete the legacy serving path               14h

  » Their move (18)

  ── acme/monorepo ────────
  ~   #10    add internalNotes to Contract                 1w
  ── acme/api-gateway ─────
  ✗ ← #1496  point history at the generic route            2d
  !   #1449  guard null ids before per-id fetches          6w

🛠️ Build your own CLI

The package has no bin, so this is the file you write. It is short — fetch, assemble, render — and everything opinionated lives in it rather than in the package.

#!/usr/bin/env node
import { $ } from "zx"
import React from "react"
import { render } from "ink"
import {
  App,
  buildInboxQuery,
  configureInbox,
  detailFor,
  layoutGHItems,
  signalPath,
  toGHItem,
  withRetry,
  type Section,
} from "@kud/gh-cockpit"

$.verbose = false

// Everything the library refuses to assume. Call once, before rendering.
configureInbox({
  repoPriority: ["acme/monorepo", "acme/", "me/"],
  checkoutDir: `${process.env.HOME}/src`,
  cacheNamespace: "my-cockpit",
  cacheTtlMs: 20 * 60_000,
})

// One GraphQL round trip for every tab. `withRetry` matters: GitHub returns 502
// under load, and a 502 is the server declining to compute an answer, not an answer.
const fetchCockpit = async (): Promise<{ sections: Section[]; login: string }> => {
  const result = await withRetry(() =>
    $`gh api graphql -f query=${buildInboxQuery({})}`.quiet(),
  )
  const data = JSON.parse(result.stdout).data

  const rows = (nodes: any[], standing?: "authored" | "queued" | "spoken") =>
    (nodes ?? []).map((n) => ({ ...toGHItem(n), ...(standing ? { standing } : {}) }))

  return {
    login: data.viewer.login,
    sections: [
      {
        id: "mine",
        label: "Mine",
        items: layoutGHItems(rows(data.myPRs.nodes, "authored"), "mine"),
      },
      {
        // Two searches, one tab. `standing` is stamped per search because nothing
        // on a row can tell them apart afterwards.
        id: "review",
        label: "Review",
        items: layoutGHItems(
          [
            ...rows(data.reviewRequests.nodes, "queued"),
            ...rows(data.reviewed.nodes, "spoken"),
          ],
          "review",
        ),
      },
      { id: "done", label: "Done", items: layoutGHItems(rows(data.recentlyDone.nodes), "done") },
    ].filter((s) => s.items.some((i) => i.kind !== "repo-header")),
  }
}

render(
  <App
    fetcher={fetchCockpit}
    cacheKey="cockpit"
    title="cockpit"
    detailFor={detailFor}
    tabHelp={[
      ["Mine", "your PRs, draft and open"],
      ["Review", "theirs — asked of you, or you reviewed"],
      ["Done", "your PRs closed < 14d"],
    ]}
    emptyHint="Nothing open across your repos."
    watchPath={signalPath()}
    watchDebounceMs={2_000}
  />,
  { alternateScreen: true },
)

Point a bin at it in your own package.json and it is a command.

Refreshing without polling

watchPath is a file the cockpit watches; writing a byte to it makes every open cockpit refetch, debounced. Have whatever mutates GitHub on your behalf — a merge script, an editor hook — write to signalPath().

A poller is wrong twice over: it spends quota on the long stretches where nothing changed, and it is still up to a full interval late when something did. The filesystem is the whole broker — no daemon, no socket, and the fan-out is free.

[!IMPORTANT] Write a byte rather than touching. At second-granularity mtime with no size change, two touches inside the same second are indistinguishable and some watch backends coalesce them away.

📖 API Reference

| Export | Purpose | | ---------------------------------------- | ---------------------------------------------------------------------------------------- | | defineCockpit(config) | Types your own config object. A convenience for hosts — nothing internal reads it | | configureInbox(config) | Host opinions: repoPriority, profiles, checkoutDir, cacheNamespace, cacheTtlMs | | parseArgs(argv) | Parses --here, --include, --exclude and a positional filter name. You decide what they mean | | registerCheckDrills(drills) | Where a CI check drills in; unregistered checks open in a browser | | registerPrompts(forms) | What a delegated agent is told to do. Nothing registered means the agent starts cold, and y copies the row URL | | detailFor(ctx) | The drill-in view for a row — PrView or IssueView | | PrView · IssueView | The drill-in views themselves, if you want to wrap them | | withRetry(fn) | Retries transient 50x responses; GitHub returns them under load | | toGHItem(node) · computeHealth(node) | GraphQL node → row, and the health token behind each glyph |

Whose move is it

| | authoredyour PR | queuedasked of you | spokenyou reviewed | | ----------------------------------------------------- | --------------------- | ------------------------ | ------------------------ | | CI failing · ! conflict · ± changes requested | you | them | them | | · awaiting review · * checks running | them | you | them | | approved | you | you | them | | open threads | you | you | you | | ~ draft | them | them | them |

standing is declared per search rather than inferred, because nothing on a row records it: a PR awaiting review and one you already reviewed are the same PR to GitHub's API, and opposite things to you.

Filtering

cockpit --include 'acme/*,me/acme-*'

* matches within one path segment and never across the /, so acme/* cannot reach another owner and acme will not claim acmecorp. A bare owner is sugar for all its repos. --exclude is applied after --include and wins, so you can take a whole org and drop one repo without listing the rest.

🔧 Development

git clone https://github.com/kud/gh.git
cd gh && npm install
npm run build --workspace @kud/gh-cockpit

| Script | Does | | ------------------- | ---------------------------------- | | npm run build | Compile src/dist/ with tsup | | npm run dev | Same, in watch mode | | npm run typecheck | tsc --noEmit | | npm test | Vitest |

src/
  config.ts        defineCockpit and the config types
  args.ts          --here / --include / --exclude parsing
  lib.ts           shared layer, re-exports @kud/gh-ink
  views/           PrView, IssueView, and the drill-in views
  extensions/      opt-in extensions, exported separately

🏗️ Tech Stack

| | | | -------------------------------------------------------- | -------------------------------------------------- | | @kud/gh | Surface-agnostic core: queries, health computation | | @kud/gh-ink | Ink components, whose-move banding, layout | | @kud/ink-ui | Terminal UI primitives | | zx | Shelling out to gh |


MIT © kud — Made with ❤️