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

gitxp

v0.0.1

Published

An alternative GitHub UI that runs locally on top of the GitHub CLI

Readme

gitxp

An alternative GitHub UI that runs on your own machine. npx gitxp starts a local server, authenticates through the GitHub CLI you already have set up, and opens the app in your browser. There is no account to create and no OAuth app to register.

The goal is an interface that feels like Linear rather than a website: the UI reads from a local store and never blocks on a network round trip, and syncing with GitHub happens in the background.

The long-term scope is the daily GitHub workflow: browsing repos, issues and pull requests, reviewing code, discussions and comments, watching CI, and merging. v0 is narrower, see Roadmap.

Requirements

  • Node 22.13 or newer, because the local store uses the built-in node:sqlite module
  • The GitHub CLI, authenticated with gh auth login

Usage

npx gitxp

Options:

  • --port <number> use a specific port instead of a random free one
  • --no-open start the server without opening a browser

How it works

npx gitxp
   |
   +-- bin/gitxp.js       checks `gh auth status`, picks a free port,
   |                      sets HOST=127.0.0.1, boots the built server
   |
   +-- .output/server     TanStack Start server functions talk to the
   |                      GitHub API using the token from `gh auth token`
   |
   +-- browser            React app that reads and writes a local
                          collection, synced against GitHub in the background

The build output is a self-contained Node server produced by Nitro. It imports nothing but node: builtins, so the published package has no runtime dependencies.

Roadmap

v0 is a complete review loop: a notifications inbox, the pull requests waiting on you, the pull request itself with its diff and CI status, and submitting a review with inline comments, approval or changes requested, and merging.

Build order, risky parts first, each step runnable on its own:

  1. SQLite store, migration runner, the two zones
  2. API client: token from gh, ETags, rate limit accounting
  3. Notifications sync loop, one resource proven end to end
  4. SSE and the client mirror, so a background write reaches the UI
  5. Inbox UI: list, preview pane, command palette, row actions
  6. Mutation queue, proven on mark-as-done
  7. Review inbox as the second resource
  8. Pull request detail: @pierre/diffs, existing review comments, CI status
  9. Review submission: inline comments, approve, request changes, merge

After v0, in rough order: issues, repo and file browsing, discussions.

Out of v0 on purpose: search, projects, Actions logs, wikis, gists, org and team views.

None of it is built yet. What is in src/ today is the project scaffold.

Interface

Three columns: a sidebar of views, the list, and a preview pane. Triage happens without leaving the list. A pull request diff breaks out of that layout and takes the full width.

Keyboard first. Cmd+K opens a command palette covering everything, and single letters act on the focused row. The palette is also where an action lives before it earns a shortcut, so nothing ends up reachable only by mouse.

The visual layer stays on stock shadcn for now. It is the one choice here that is cheap to revisit, because retuning spacing and type tokens later does not touch component structure or focus handling.

Cached rows render immediately, with a thin indicator while a background refresh runs. Skeletons appear only where data is genuinely absent. Applied everywhere, that rule is most of what separates this from a website.

Local sync layer

The UI reads from a SQLite database at ~/.gitxp/, written by the server process. It uses the built-in node:sqlite module, so it adds no dependency and the published package stays free of them. One server process means one writer, sync keeps running while no tab is open, and the cache survives clearing browser data.

What gets synced

GitHub is too large to mirror, so the store has a boundary:

  • Synced in the background: your notifications, pull requests waiting on your review, and the repos you explicitly follow in gitxp.
  • Fetched on demand and cached afterwards: file trees, blobs, older comment threads, CI logs, anything you reach by navigating rather than by triage.

Anything outside the synced set is a cache miss on first view. The UI should show it arriving rather than pretending it was already local.

Schema

Each entity keeps GitHub's JSON payload as-is in a raw column, alongside extracted columns for the fields queries filter and sort on. Scope grows toward all of GitHub, and keeping the payload whole means adding a view does not need a migration for every field that view happens to read.

Rows are keyed by GitHub's GraphQL node_id. REST and GraphQL return different identifiers for the same object and REST payloads carry node_id too, so keying on it means a row written by a bulk GraphQL pull and a row written by a REST poll are the same row.

Notification threads are the exception. The REST notifications endpoint returns no node_id, only a string thread id, so notifications is keyed on that.

The store has two zones. Everything cached from GitHub is disposable, can be dropped and resynced, and therefore makes most schema changes free. The mutation queue and your settings cannot be rederived from GitHub, so they live in tables migrations have to preserve. That split is what makes a reset-cache command safe to offer. Migrations run off SQLite's user_version.

Scheduling

Each resource is its own job with its own interval, ETag and backoff, rather than one loop polling everything. Checks on a pull request you are looking at want seconds, followed repos want minutes, and whatever is on screen gets a priority lane.

node:sqlite is synchronous, so a large write batch stalls HTTP responses in the same process. Keep batches small and measure. If the UI stutters, the sync loop moves to a worker thread.

The client mirror

React runs in the browser and cannot read that SQLite file, so every row the UI renders is copied into browser memory first. That copy is a TanStack DB collection holding the slice the current view needs.

Holding a collection, rather than calling a server function per view, is what allows filtering, sorting and joining to happen in the browser with no round trip. It is why an inbox filter can feel instant. Only the bounded triage set is mirrored this way; everything fetched on demand stays in the plain TanStack Query cache.

The server pushes over Server-Sent Events at /api/events, as a plain ReadableStream response. A sync that writes rows emits a change signal naming the resource, and the client refetches that collection through its server function, applying the result inside TanStack DB's begin, truncate, write and commit calls. Mutations travel the other way as ordinary server function calls, so the stream stays one-directional.

The stream carries a signal rather than the changed rows. On localhost the refetch costs a sub-millisecond round trip and reuses the same query path as the initial load, which is worth more than the saved bytes. Streaming rows, and per-subset subscriptions through loadSubset and unloadSubset, are the refinement if a collection ever grows past what a full refetch can carry.

GitHub --poll--> sync loop --write--> SQLite (~/.gitxp)
                                         |
                                  change signal
                                         |
                            SSE stream at /api/events
                                         |
                           client refetches that collection
                                         |
                    SyncConfig: begin / truncate / write / commit
                                         |
                              client collection --> React

Writes

Mutations are queued in the same database and applied in order, so a write survives a page reload. They fall into two classes:

  • Optimistic: comments, labels, review submissions, marking a notification done. The local store updates immediately and rolls back if the request fails.
  • Confirmed: merging, closing, anything awkward to undo or that branch protection and required checks can reject. The UI waits for GitHub and shows the request in flight.

A queued write can fail long after you navigated away, so failed actions need a persistent surface with retry rather than a toast that disappears.

A review in progress stays local until you submit it. Inline comments accumulate in the queue and reach GitHub as one request that creates the review with its comments, which keeps drafting instant and makes submission atomic.

Staying inside the rate limit

The limit is 5000 requests an hour on REST and on GraphQL, and 30 a minute on search, so search is not used for syncing.

  • Background polling uses conditional requests. A 304 does not count against the REST limit.
  • The notifications endpoint returns an X-Poll-Interval header saying how often GitHub wants to be polled. Respect it instead of picking an interval.
  • GraphQL is for composite reads where one query replaces several REST calls, such as a pull request with its reviews and checks.
  • CI status is the hungriest part, because check runs change often. Poll it only for a pull request currently on screen, never globally.

Reviewing pull requests

Rendering is @pierre/diffs rather than a hand-built viewer. It is Apache-2.0, built on Shiki, does stacked and split layouts, and ships an annotation framework meant for review comments and CI annotations. File trees come from @pierre/trees.

@pierre/trees is at 1.0.0-beta.6 and depends on a Preact 11 beta, so a Preact beta ends up bundled next to React 19. Worth watching, and worth pinning the version.

The data still comes from the REST pull request files endpoint, because GitHub has already computed the patch hunks. That endpoint omits the patch for binary files and for files over its size limit, so those need a fallback rather than an empty view.

The preview pane is narrow, so it shows a stacked diff. Split view is available at full width.

Review comments anchor to a path, a line and a side, mapped onto the annotation framework. Comments whose line no longer exists in the current diff are outdated, and the UI has to show them rather than drop them.

Long lists outside the diff still need virtualizing. @tanstack/react-virtual is the fit, and is not installed yet because nothing imports it.

Security

The server holds a real GitHub token, so it is only ever reachable from the machine it runs on. bin/gitxp.js sets HOST=127.0.0.1, because the Nitro default binds every interface.

The token comes from gh auth token and stays on the server side. Server functions read it and the browser never receives it. It is re-read on a 401 rather than cached for the life of the process, and the host comes from gh auth status, so an enterprise host works without extra configuration.

Server functions reject requests whose Origin header is not the app itself. Any page open in your browser can reach 127.0.0.1, and that is the realistic attacker. This is not built yet, and needs to exist before the server starts holding a token.

A local process can read the port and send no Origin at all. Defending against that would need a secret generated at boot and carried in the URL. That is not planned.

Development

npm install
npm run dev          # vite dev server on port 3000
npm run build        # produces .output/
npm start            # runs bin/gitxp.js against .output, same path as npx
npm run lint
npm run typecheck
npm run format

npm run lint does not typecheck. npm run typecheck catches what it misses, including unsound casts on SQLite rows.

Modules under src/server import with explicit .ts extensions and stay inside the TypeScript syntax Node can strip, so they run directly without the bundler:

GITXP_HOME=/tmp/gitxp-scratch node some-script.ts

Publishing

prepublishOnly runs the build, and the files field ships .output and bin. Everything else is a build-time concern and lives in devDependencies.

dependencies is empty on purpose. npm installs a package's dependencies when you run it through npx, so anything listed there is downloaded by every user. Nitro already inlines the framework into .output, which is about 1.4 MB against a 278 MB development tree.

Check what would be published before releasing:

npm pack --dry-run

Project layout

bin/gitxp.js        CLI entry point, boots the built server
src/server/db/      SQLite store, migrations, per-resource sync state
src/server/github/  gh token, API client with ETags and rate limit accounting
src/server/sync/    per-resource sync jobs, scheduler, change emitter
src/functions/      server functions, the boundary the client calls
src/db-collections/ client collections mirroring the local database
src/routes/         file-based routes (TanStack Router)
src/components/ui/  shadcn components, all of them
src/integrations/   TanStack Query setup
.output/            build output, generated

Built with

TanStack Start with Nitro, TanStack Router, Query, DB, Table and Form, Tailwind, and shadcn/ui. Diffs and file trees come from @pierre/diffs and @pierre/trees.

Every shadcn component is installed under src/components/ui. The full list is in AGENTS.md.