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

@classytic/gitkit

v0.2.0

Published

Git-backed repositories for content/config-as-code — CRUD over a versioned file tree, with free history, diff, and PR-review. Built on @classytic/repo-core.

Downloads

333

Readme

@classytic/gitkit

Git-backed repositories for content- and config-as-code. CRUD + real query (filter / sort / page, pluggable search) over a versioned file tree, on the @classytic/repo-core DataAdapter contract — so one defineResource() in @classytic/arc gives you REST + permissions + OpenAPI + MCP agent tools, backed by git instead of a database.

Every write is a commit. You get history, diff, blame, branch-based draft/publish, PR-review approval, and rollback — for free, no audit plugin.

It's a peer of mongokit / sqlitekit / pgkit / prismakit: same repository shape, same query surface, different substrate.

When to reach for it

✅ Docs portals, knowledge bases, marketing/CMS content, prompt libraries, feature flags & app config, GitOps manifests — anything human-edited, bounded (hundreds–low-thousands of docs), where you want git's versioning and review flow.

🚫 Not a database. The query engine is in-memory over the loaded collection — great to a few thousand docs, wrong at 100k+. Writes are commit-paced (not high-write-concurrency). No transactions/aggregation. If you need those, use a DB kit. gitkit is for content, not your orders table.

Install

npm i @classytic/gitkit @classytic/repo-core
# GitHub backend uses the official SDK — bring it yourself (optional peer):
npm i octokit

60-second example — a git-backed arc resource with agent tools

import { defineResource, requireAuth, allowPublic } from '@classytic/arc';
import { createGitAdapter } from '@classytic/gitkit/adapter';
import { frontmatter } from '@classytic/gitkit/serializers';
import { octokitGitBackend } from '@classytic/gitkit/backends/octokit';
import { App } from 'octokit';

type Post = { id: string; title: string; status: 'draft' | 'published'; tags: string[]; content: string };

const app = new App({ appId, privateKey });

export const postResource = defineResource<Post>({
  name: 'post',
  adapter: createGitAdapter<Post>({
    // Per-request/tenant: auth + repo resolution live HERE. gitkit never sees a token.
    resolveBackend: async (scope) => octokitGitBackend({
      octokit: await app.getInstallationOctokit(getInstallationId(scope)),
      owner: 'acme',
      repo: getRepoForTenant(scope),
    }),
    collection: {
      dir: 'content/posts',
      serializer: frontmatter<Post>(),      // YAML frontmatter + mdx body
      idField: 'id',
    },
    // Git docs are schemaless — give the OpenAPI/MCP shape once:
    schema: { entity: POST_JSON_SCHEMA, createBody: POST_JSON_SCHEMA },
  }),
  permissions: { list: allowPublic(), get: allowPublic(), create: requireAuth(), update: requireAuth(), delete: requireAuth() },
});

You now have — over a git repo of .mdx files:

  • GET/POST/PATCH/DELETE /posts with auth + validation + OpenAPI
  • Every mutation is a commit on the tenant's repo (audit + rollback built in)
  • MCP tools auto-generated (post.list, post.get, post.create, …) — an agent can read and edit your content, gated by the same permissions

Query — identical to mongokit, because it is mongokit's engine

getAll delegates filtering to repo-core's canonical matchFilter, so the wire syntax matches every other kit:

GET /posts?filter[status]=published&filter[tags][in]=ai,ml&sort=-updatedAt&page=2&limit=20
import { queryDocs } from '@classytic/gitkit/query';
queryDocs(posts, {
  filters: { status: 'published', tags: { in: ['ai', 'ml'] } },
  sort: '-updatedAt',
  page: 2, limit: 20,
}); // → { method: 'offset', data, page, limit, total, pages, hasNext, hasPrev }

Full-text search is deliberately NOT here — see the Search section. filter is exact/structured matching over already-loaded docs; search is a separate, index-backed concern so it never scans the whole collection per request.

Search — a pluggable provider, indexed on commit (never a full scan)

gitkit does not ship a general search that loads every doc per query — that melts servers at scale. Instead you plug in a SearchProvider (Orama, a vector store, MeiliSearch, git grep/ripgrep over a clone). The git-native pattern:

import { memorySearchProvider } from '@classytic/gitkit/search';

const repo = gitRepository<Post>({
  backend,
  collection: { dir: 'content/posts', serializer: frontmatter() },
  search: memorySearchProvider({ fields: ['title', 'content'] }), // ← swap for Orama/vector at scale
});

await repo.reindex();          // 1. build ONCE at boot (the only full load)
await repo.create(doc);        // 2. writes upsert into the index incrementally…
await repo.delete(id);         //    …and deletes remove from it — a commit tells you exactly what changed
const hits = await repo.search('vector', { limit: 20 });   // 3. query the index — fast, no scan

Without a provider, repo.search() throws (no silent load-all fallback). The SearchProvider seam is three optional hooks + one required search:

interface SearchProvider<TDoc> {
  reset?(docs: TDoc[]): void | Promise<void>;    // reindex()
  upsert?(docs: TDoc[]): void | Promise<void>;   // on create/update/edit
  remove?(ids: string[]): void | Promise<void>;  // on delete
  search(q: string, o?: { limit?: number }): SearchHit<TDoc>[] | Promise<SearchHit<TDoc>[]>;
}

Recipes (implement SearchProvider — gitkit never depends on these):

  • Orama (full-text, in-process): reset/upsert/removeinsert/remove; searchsearch(db, { term }). This is what a docs portal wants.
  • Vector / semantic (RAG, "ask the docs"): upsert → embed + write to your vector store; search → embed the query + similarity search. Async upsert is fine — it runs off the write path if you queue it.
  • ripgrep over a local clone (index-free): implement only search → shell rg --json over the working tree; reset/upsert/remove are no-ops (the clone IS the source, kept fresh by commits/pulls). Fastest to stand up server-side.

Staying fresh when writes come from outside gitkit

Writes through repo.* keep the index incremental automatically. But content can also change out of band — someone edits on github.com, a PR merges, a CI job commits. gitkit doesn't own webhooks (that's your app's concern — verify the signature, own the route); it just gives you the one call to run when a push lands on your branch:

// your webhook handler (host owns auth + signature verification)
app.post('/webhooks/github', verifyGitHubSignature, async (req) => {
  const { ref, repository } = req.body;
  if (ref === `refs/heads/${repository.default_branch}`) {
    await repo.reindex();   // rebuild the SearchProvider from the new tree
    cache.clear();          // and drop any read cache you keep
  }
});

For a big collection, reindex only what the payload says changed (commits[].added / modified / removed) via provider.upsert / provider.remove instead of a full reindex() — the webhook tells you exactly which files moved, same as a local write.

Backends — auth is the backend's job, not the kit's

GitBackend is the entire git surface (3 methods). Pick one or write your own:

| Backend | Import | Auth | Use for | |---|---|---|---| | octokit | @classytic/gitkit/backends/octokit | you pass an authed Octokit (App token / PAT / OAuth) | GitHub, multi-tenant servers | | local | @classytic/gitkit/backends/local | git's own credential helper / SSH | CI, self-host, scripts | | memory | @classytic/gitkit/backends/memory | none | tests, reference impl |

export interface GitBackend {
  readFile(path: string, ref?: string): Promise<string | null>;
  listTree(prefix: string, ref?: string): Promise<string[]>;
  commit(input: { changes: FileChange[]; message: string; ref?: string }): Promise<{ sha: string }>;
}

GitLab/Bitbucket/Gitea? isomorphic-git? Implement those three methods.

Serializers

frontmatter() (.md/.mdx, YAML frontmatter + body), json(), yaml(). Bring a full YAML parser (js-yaml/yaml) via options for rich frontmatter; the built-in handles flat scalars (covers most content files).

Test with zero infrastructure

import { memoryGitBackend } from '@classytic/gitkit/backends/memory';
import { gitRepository } from '@classytic/gitkit/repository';
import { frontmatter } from '@classytic/gitkit/serializers';

const backend = memoryGitBackend({ files: { 'content/posts/hi.mdx': '---\ntitle: Hi\nstatus: published\n---\nbody' } });
const repo = gitRepository<Post>({ backend, collection: { dir: 'content/posts', serializer: frontmatter() } });

const page = await repo.getAll({ filters: { status: 'published' } });

Performance — fast commits, and the recipes for instant editors

The capability (free, in the backend): the octokit backend commits with one GraphQL createCommitOnBranch round-trip — atomic multi-file, GitHub- signed — not the classic 6-call REST dance (getRef → getCommit → createBlob×N → createTree → createCommit → updateRef). The branch head oid is cached and advanced after each commit, so steady-state commits skip the head lookup too; concurrent-write conflicts re-fetch and retry once. Net: a save is ~1 network hop instead of ~6.

Batch related changes to keep it one commit:

await repo.createMany(docs);                 // N files → ONE commit
await repo.edit(id, oldStr, newStr);         // surgical write → ONE commit

The recipes (host owns UX — gitkit gives you the fast primitives):

  1. Optimistic UI — return "saved" and update the editor immediately; the commit is already ~1 hop. On failure, reconcile (toast + revert / retry).
  2. Debounced async flush — don't commit per keystroke. Buffer edits in a fast store and flush with createMany/edit on idle (e.g. 1–2s) or on "publish", so the editor never awaits GitHub. gitkit's commit is the flush primitive; the queue/debounce is yours (a worker, a job queue, etc.).
  3. Cache reads — serve page-data/docs.json/MDX from your cache/CDN/ISR, revalidated on commit — don't hit the API per view. readFile(path, ref) makes the two sides of a cache-vs-source comparison trivial.
  4. Hot editors → the local backend — a server-side working-tree clone commits locally (~ms, no network), pushed async. Fastest per commit; costs you clone management. Cold/many-tenant → stick with octokit.

Bulk & cross-repo

The repository does batch writes atomically:

await repo.createMany(docs);   // N docs → ONE commit (not N)
await repo.loadAll();          // every doc, uncapped, tree read once

Copying content between repos/tenants/providers is the one genuinely cross-repo op (GitHub → local, template → tenant), with an optional transform to rebrand/remap:

import { copyCollection } from '@classytic/gitkit/operations';
await copyCollection(templateRepo, tenantRepo, {
  transform: (doc) => ({ ...doc, brand: 'Acme', status: 'draft' }),
});

There is deliberately no importDocs/exportCollection — those would just be repo.createMany(docs) / repo.loadAll(). And repo lifecycle (clone/fork/provision) stays in the host — gitkit copies content, not repos.

Diff & surgical edit — no arc-ai needed

Every git-content app needs "change this line" and "show what changed" — so those live in gitkit itself (pure, zero-dep, isomorphic). Richer agent tooling (apply_patch v4a, read-before-write) layers on top via arc-ai; you don't need it.

// Surgical edit (search/replace) — committed, re-parsed, occurrence-safe:
import { applyEdit } from '@classytic/gitkit/edit';
await repo.edit(id, 'status: draft', 'status: published');   // one commit
applyEdit(text, 'old', 'new', { replaceAll: true });          // pure string op

// Show what changed — for review/preview UIs, CLIs, agent results:
import { diffDoc, formatUnified } from '@classytic/gitkit/diff';
const hunks = await diffDoc(repo, id, { base: 'main', head: 'draft' });
console.log(formatUnified(hunks));   // git-style unified diff

repo.edit throws EditPatternNotFoundError / EditAmbiguousError so a caller (or an agent driving it as a tool) can add context and retry — the coding-agent edit contract. And because both sides come from readFile(path, ref), diffDoc compares any two branches/commits/tags.

Exports & tree-shaking

No runtime barrel — one subpath per concern, so bundlers chunk and tree-shake exactly what you import (and a browser bundle never sees node:):

| Import | Runtime? | Isomorphic | |---|---|---| | @classytic/gitkit | types only (erased at build) | ✅ | | @classytic/gitkit/repository | GitRepository, gitRepository | ✅ | | @classytic/gitkit/query | queryDocs (filter/sort/page) | ✅ | | @classytic/gitkit/search | SearchProvider seam + memorySearchProvider | ✅ | | @classytic/gitkit/operations | copyCollection (cross-repo) | ✅ | | @classytic/gitkit/edit | applyEdit (surgical search/replace) | ✅ | | @classytic/gitkit/diff | diffLines · formatUnified · diffDoc | ✅ | | @classytic/gitkit/adapter | createGitAdapter | ✅ | | @classytic/gitkit/serializers | frontmatter · json · yaml | ✅ | | @classytic/gitkit/backends/memory | memoryGitBackend | ✅ | | @classytic/gitkit/backends/octokit | octokitGitBackend | ✅ (fetch-based) | | @classytic/gitkit/backends/local | localGitBackend | ⛔ Node-only (git CLI) |

Everything except backends/local runs in Node, browsers, Deno/Bun, and edge/workers — so a React app can CRUD content straight against GitHub with a user's token, and an agent can drive the same API in a container or on the edge.

License

MIT · © Classytic