{"id":46627259,"url":"https://github.com/clerk/pkglab","last_synced_at":"2026-03-11T23:01:04.218Z","repository":{"id":342253482,"uuid":"1173381922","full_name":"clerk/pkglab","owner":"clerk","description":"Fast local package development CLI with an embedded fast NPM registry. Publish, test, and iterate on workspace packages across consumer repos.","archived":false,"fork":false,"pushed_at":"2026-03-10T13:16:32.000Z","size":13055,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-11T01:40:09.759Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/clerk.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-05T09:57:51.000Z","updated_at":"2026-03-10T13:15:17.000Z","dependencies_parsed_at":null,"dependency_job_id":"336b7ee3-3489-4c6c-8a0a-afb570a0ed08","html_url":"https://github.com/clerk/pkglab","commit_stats":null,"previous_names":["clerk/pkglab"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/clerk/pkglab","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clerk%2Fpkglab","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clerk%2Fpkglab/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clerk%2Fpkglab/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clerk%2Fpkglab/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/clerk","download_url":"https://codeload.github.com/clerk/pkglab/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clerk%2Fpkglab/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30406400,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-11T22:36:59.286Z","status":"ssl_error","status_checked_at":"2026-03-11T22:36:57.544Z","response_time":84,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-07T23:00:25.211Z","updated_at":"2026-03-11T23:01:04.127Z","avatar_url":"https://github.com/clerk.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://clerk.com?utm_source=github\u0026utm_medium=clerk_pkglab\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\n    \u003cpicture\u003e\n      \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://images.clerk.com/static/logo-dark-mode-400x400.png\"\u003e\n      \u003cimg src=\"https://images.clerk.com/static/logo-light-mode-400x400.png\" height=\"64\"\u003e\n    \u003c/picture\u003e\n  \u003c/a\u003e\n  \u003cbr /\u003e\n\u003c/p\u003e\n\u003cdiv align=\"center\"\u003e\n  \u003ch1\u003e\n    pkglab\n  \u003c/h1\u003e\n  \u003ca href=\"https://www.npmjs.com/package/pkglab\"\u003e\n    \u003cimg alt=\"Downloads\" src=\"https://img.shields.io/npm/dm/pkglab\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://clerk.com/discord\"\u003e\n    \u003cimg alt=\"Discord\" src=\"https://img.shields.io/discord/856971667393609759?color=7389D8\u0026label\u0026logo=discord\u0026logoColor=ffffff\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://twitter.com/clerkdev\"\u003e\n    \u003cimg alt=\"Twitter\" src=\"https://img.shields.io/twitter/url.svg?label=%40clerkdev\u0026style=social\u0026url=https%3A%2F%2Ftwitter.com%2Fclerkdev\" /\u003e\n  \u003c/a\u003e\n  \u003cbr /\u003e\n  \u003cbr /\u003e\n\u003c/div\u003e\n\nLocal package development CLI with a built-in registry. Publish workspace packages locally and auto-update consumer repos.\n\nTest your library changes in consumer repos the way your users actually install them. pkglab runs a fast Bun-based local npm registry, auto-publishes packages (and their deps/dependent chain) on every edit, and updates all your consumer repos automatically. No `npm link` symlink issues, no manual version juggling.\n\nBuilt for teams publishing npm packages from monorepos who need to test changes in consumer repos before shipping. Used in production by the [Clerk JavaScript SDK](https://github.com/clerk/javascript) monorepo (22+ packages).\n\nAlso available as `pkgl` for short (so efficient ✨).\n\n## Table of contents\n\n- [Features](#features)\n- [Quick start](#quick-start)\n- [Quick start - monorepo edition](#quick-start---monorepo-edition)\n- [Install](#install)\n- [The problem](#the-problem)\n- [How it works](#how-it-works)\n- [Commands](#commands)\n- [Multi-worktree support](#multi-worktree-support)\n- [Catalog support](#catalog-support)\n- [How versioning works](#how-versioning-works)\n- [Design decisions](#design-decisions)\n- [Configuration](#configuration)\n- [Repo hooks](#repo-hooks)\n- [Performance](#performance)\n- [Safety](#safety)\n- [Acknowledgments](#acknowledgments)\n\n## Features\n\nCore workflow\n\n- Real `npm publish` to a local npm registry, so you test the same install your users get\n- Automatic consumer repo updates after every publish\n- Content-aware publishing: unchanged packages are skipped automatically, only packages with real changes (or whose deps changed) get a new version\n- Dependency cascade awareness (change a shared util, all dependent packages get republished)\n- Consistent across runs: on each `pub` invocation, `pkglab` picks the publish strategy that gives your consumer repos all changes without mismatched package versions\n\nPerformance\n\n- Fast, lightweight registry built on `Bun.serve()` with in-memory metadata: starts in ~60ms, uses ~44MB RAM, publishes 22 packages in \u003c1s\n- Fast consumer installs: lockfile patching for pnpm (skips resolution), `--ignore-scripts` + `--prefer-offline` for bun/pnpm, automatic fallback on failure\n- Automatic version pruning per tag so the local registry doesn't grow forever\n- Sync and async APIs (`pub` and `pub --ping`), automatic debounce on rapid build-to-publish loops (common in big monorepos).\n\nMulti-worktree and parallel development\n\n- Publish from multiple git worktrees in parallel without version conflicts, each worktree gets its own version channel\n- Works with most consumer package managers: npm, pnpm, or bun. Yarn works but not tested.\n\nSafety and git hygiene\n\n- Pre-commit safety checks (`pkglab check`) to catch local artifacts before they reach your repo, including lockfile scanning for localhost URLs\n- Automatic pre-commit hook injection on first `pkglab add`, removed on restore\n- Git skip-worktree protection on `.npmrc` so localhost registry URLs don't leak into commits\n- Lockfile sanitization: auto-strips localhost registry URLs from `bun.lock` after installs, preventing CI breakage if the lockfile gets committed\n\n## Quick start\n\n```bash\n# Globally install the binaries\nnpm install -g pkglab\n\n# Publish packages from your library monorepo\npkglab pub # all (from workspace root), or current (if inside package)\npkglab pub @clerk/backend # specific package and cascade\n\n# From a consumer repo, install a package from the local registry\n# Run from workspace root to auto-update all sub-packages that use it\npkglab add # interactive prompt\npkglab add \u003cname\u003e # specific package\npkglab add \u003cname\u003e -p apps/dashboard # target a single sub-package instead\npkglab add --scope clerk # Or replace all packages of a scope at once\n\n# Manage which consumer repos receive auto-updates\npkglab repo on              # interactive picker\npkglab repo on \u003cpath\u003e       # specific repo\npkglab repo off             # interactive picker\n\n# Make changes to the library, then re-publish\n# Fingerprints each package, skips unchanged ones, cascades through deps and dependents\n# Active consumer repos update automatically (see Design decisions for details)\npkglab pub\n\n# When done, restore original versions\npkglab restore @clerk/backend                # one package\npkglab restore @clerk/backend @clerk/shared  # multiple packages\npkglab restore --all                         # everything in this repo\n\n# Stop the registry\npkglab down\n```\n\n## Quick start - monorepo edition\n\nIf your packages have a watch/dev mode (tsup, tsc, rspack, etc.), you can wire pkglab into the workflow so rebuilds automatically publish to the local registry.\n\nThe manual workflow: run your dev server in one package, run `pkglab pub` from the root workspace (or `pkglab pub --root` from any sub workspace) in another when you want to push changes. Since pkglab fingerprints each package, only the ones with actual changes get republished.\n\nWhen multiple packages rebuild at once, the registry server batches them into a single publish cycle instead of separate ones. Pings are debounced with a 150ms window: if several `pub --ping` requests arrive in quick succession (common when a build tool triggers rebuilds across packages), they're collected into one batch before publishing starts. Each tag gets its own queue lane, so worktree publishes don't interfere. The coalescing logic runs inside the registry process, so no separate listener is needed.\n\nFor a fully automated loop, wire `pub --ping` into your dev server's success hook:\n\n```bash\n# Watch builds with auto-publish on each successful rebuild\npnpm dev -- --onSuccess 'pkglab pub --ping'\n```\n\nMost dev servers have some kind of `onSuccess` hook you can run `pkglab pub --ping` in, for example, `tsup` \u0026 `tsdown` are using `onSuccess`, so each change would trigger a `dev build -\u003e pkglab pub --ping -\u003e package publishing` automatically.\n\nYou can also wire `pub --ping` into your build runner so publishes happen automatically after every rebuild. For example, with turbo's `--on-complete` hook:\n\n```bash\nturbo dev --on-complete 'pkglab pub --ping'\n```\n\nFor a real-world example, we use this workflow in the [Clerk JavaScript SDK monorepo](https://github.com/clerk/javascript).\n\n## Install\n\nPrebuilt binaries (no runtime needed):\n\n```\nnpm install -g pkglab\n```\n\nThis installs a native binary for your platform. No Bun or Node.js runtime required to run pkglab itself. npm is only used here as a distribution channel.\n\nUnder the hood, the `pkglab` npm package contains a tiny Node.js wrapper. It declares platform-specific packages (`pkglab-darwin-arm64`, `pkglab-linux-x64`, etc.) as optional dependencies with `os` and `cpu` constraints, so npm only downloads the one matching your machine. When you run `pkglab`, the wrapper resolves the platform binary and execs it. The compiled binary has the Bun runtime embedded, so nothing else needs to be installed.\n\nYour consumer repos can use any package manager: npm, pnpm, yarn, or bun.\n\nSupported on macOS (ARM64, x64) and Linux (x64, ARM64).\n\n## The problem\n\nTesting local package changes across repos is painful. The existing tools all have real problems.\n\n`npm link` creates symlinks into your source directory. React, styled-components, and other libraries that rely on module identity get duplicated: your app loads one copy from `node_modules` and the linked package resolves a second copy from its own `node_modules`. This causes the classic \"hooks can only be called inside the body of a function component\" crash, broken contexts, and `instanceof` checks failing across the boundary. A single `npm install` can also silently blow away your links. There are no lock file entries, so your CI and teammates can't reproduce the setup.\n\n`yalc` improves on this by copying files, but it injects `.yalc` directories and modifies `package.json` with `file:` references that behave differently from real registry installs. Lock files end up with local paths instead of registry URLs. The install you test against is structurally different from what your users will get.\n\n`workspace:^` and `workspace:~` protocols only work within a single workspace. If you need to test a package in a separate consumer repo (the common case), they can't help. Even within a workspace, packages resolve to local copies during install rather than going through the registry, which can mask version resolution bugs that only surface in real published installs. There's also a subtler problem: during snapshot or canary releases, `workspace:^` gets resolved to caret ranges like `^3.0.0-snapshot.xxx`. Semver compares pre-release identifiers lexicographically, so a caret range from one release channel can accidentally match versions from a completely different channel. For example, `^3.0.0-canary.v20251211` will match `3.0.0-snapshot.v20251204` because `snapshot` sorts after `canary`, even though that snapshot is older and from a different release channel entirely.\n\n`pnpm overrides` and `yarn resolutions` require editing the consumer's `package.json`, remembering to undo before committing, and don't auto-update when you change the library. When pointed at local or workspace targets, they bypass registry validation, so you can miss broken exports maps, missing files in the `\"files\"` array, and unresolved `workspace:` / `catalog:` protocols until you actually publish to npm.\n\nA standalone registry gives you half the picture, but you still have to manage the daemon lifecycle, generate versions, manually install in every consumer, track which repos are linked, prune old versions, and protect against committing localhost URLs.\n\n`pkglab` solves all of this by publishing to a real npm registry running on your machine, and ships its own lightweight registry server that starts faster and uses less memory.\n\n## How it works\n\n`pkglab` runs a lightweight npm registry as a background daemon, built on `Bun.serve()` with in-memory package metadata and write-through persistence to disk. Unknown packages are proxied to the upstream npmjs.org registry, so consumer installs work transparently for both local and public packages.\n\nWhen you publish, packages go through a real `npm publish` to this local registry. Exports maps, bundled dependencies, the `\"files\"` array, all validated the same way npm would. Consumer repos install from this registry with a standard `npm install` / `pnpm add`, producing the same `node_modules` tree your users will get. One copy of React. Correct peer dependency resolution. Real lock file entries.\n\nOn top of that, `pkglab` handles automatic consumer updates, dependency cascading, parallel publishes with rollback, `.npmrc` protection, pre-commit checks, version pruning, and multi-worktree tag isolation. See [Features](#features) for the full list.\n\nThe registry server starts in ~60ms, uses ~44MB of memory, and publishes 22 packages in about 1 second. See [Performance](#performance) for full benchmark results.\n\n## Commands\n\nRegistry\n\n- `pkglab up` - start the local registry. Deactivates repos from the previous session, then offers a picker to reactivate the ones you need.\n- `pkglab down` - stop the registry. Restores all consumer repos first (versions, `.npmrc`, pre-commit hooks) and only stops the daemon if all restores succeed. If a restore fails, the daemon stays up so you can fix the issue.\n  - `--force`/`-f` - skip restoration and stop immediately\n- `pkglab status` - show whether the registry is running and on which port.\n  - `--health` - exit 0 if registry is healthy, exit 1 if not (silent, for scripting)\n- `pkglab logs` - tail registry logs. `-f` for follow mode.\n\nPublishing\n\n- `pkglab pub [name...]` - publish packages to the local registry. Accepts multiple names. Publishes the current package (if inside one) or all public packages from the workspace root. Computes transitive dependents and republishes the cascade, including sibling workspace deps of dependents. Fingerprints each package and skips unchanged ones: only packages with content changes or whose deps changed get a new version. Uses mtime+size metadata to skip content hashing on repeat runs. Auto-updates active consumer repos matching the same tag and prunes old versions in the background.\n  - `--tag \u003cname\u003e`/`-t` - publish with a tag\n  - `--worktree`/`-w` - auto-detect tag from branch name\n  - `--root` - publish all packages regardless of cwd\n  - `--ping` - send publish request to the registry server instead of publishing directly\n  - `--single` - skip cascade and fingerprinting\n  - `--shallow` - targets + deps only, no dependent expansion\n  - `--force`/`-f` - ignore fingerprints, republish all\n  - `--no-pm-optimizations` - skip lockfile patching and install optimizations, uses plain `pm install`\n  - `--dry-run` - preview what would be published\n  - `--verbose`/`-v` - includes per-phase timing\n- `pkglab listen` - (deprecated) shows queue status from the registry. Publish coalescing is now built into the registry server.\n\nConsumer packages\n\n- `pkglab add [name[@tag]...]` - install pkglab packages in the current repo. Accepts multiple names, batch installs in one command. Configures `.npmrc`, applies git skip-worktree, and installs using your repo's package manager. No args for an interactive picker. Auto-detects catalog entries and updates the catalog source directly (see [Catalog support](#catalog-support)). In a workspace, auto-scans all sub-packages for the dependency and updates all of them (sub-packages using `catalog:` protocol are skipped, handled by catalog auto-detection). All targets are remembered for restore.\n  - `name@tag` - pin to a specific tag (e.g. `pkglab add @clerk/pkg@feat1`)\n  - `--tag`/`-t` - apply a tag to all packages at once (e.g. `pkglab add pkg --tag feat1`). Cannot combine with inline `@tag` syntax\n  - `--scope`/`-s` - replace all packages of a given scope in the workspace (e.g. `--scope clerk` or `--scope @clerk`), scanning workspace root + sub-packages for matching deps and verifying all are published before modifying files. Cannot combine with positional package names\n  - `--catalog`/`-c` - strict catalog mode, errors if a package is not in any catalog\n  - `--packagejson`/`-p` - target a single sub-package directory instead of workspace scanning (e.g. `-p apps/dashboard`)\n  - `--dry-run` - preview what would be installed\n  - `--verbose`/`-v` - show detailed workspace scanning output\n- `pkglab restore \u003cname...\u003e` - restore pkglab packages to their original versions across all targets updated by `pkglab add` (catalog, sub-packages, or both). Accepts multiple names. Runs pm install to sync node_modules, cleans `.npmrc` if no packages remain, removes skip-worktree.\n  - `--all` - restore every pkglab package in the repo\n  - `--scope \u003cscope\u003e` - restore all packages matching a scope (mirrors add `--scope`)\n  - `--tag`/`-t` - restore only packages installed with a specific tag\n- `pkglab check` - pre-commit safety check. Scans for pkglab artifacts in `package.json` and `.npmrc` (local versions, registry markers, staged files) across workspace root and sub-packages. Also scans staged lockfiles (`bun.lock`, `bun.lockb`, `pnpm-lock.yaml`) for localhost registry URLs. Returns exit code 1 if anything is found.\n\nRepo management\n\n- `pkglab repo ls` - list consumer repos with their active/inactive status and linked packages.\n- `pkglab repo on/off [name...]` - activate or deactivate consumer repos. Accepts multiple paths. `--all` to activate or deactivate every repo. Interactive picker if no name given.\n- `pkglab repo reset [name]` - clear all state for a repo, restoring original package versions and running pm install. `--all` to reset every repo. `--stale` to remove repos whose directories no longer exist.\n- `pkglab pkg ls` - list published packages in the local registry, grouped by tag with the latest version per tag. Checks if the registry is running first.\n- `pkglab pkg rm \u003cname...\u003e` - remove packages from the local registry entirely (API + storage). Accepts multiple names. `--all` removes every package and clears fingerprint state.\n\nMaintenance\n\n- `pkglab doctor` - diagnose your setup. Checks directory structure, daemon health, registry connectivity, and skip-worktree flags across all linked repos. Auto-repairs missing flags. Detects dirty state (daemon not running but repos have active pkglab packages) and suggests recovery steps.\n  - `--lockfile` - sanitize `bun.lock` files in consumer repos by replacing localhost registry URLs with `\"\"` (tells bun to use the default registry)\n- `pkglab reset --hard` - wipe all pkglab data and registry storage. Stops the daemon if running.\n- `pkglab reset --fingerprints` - clear the fingerprint cache. Next `pub` will republish all packages regardless of content changes.\n- `pkglab hooks init` - scaffold `.pkglab/hooks/` in the current repo with type definitions (`payload.d.ts`) and commented-out stubs for all 7 hook events. Hooks let consumer repos run custom scripts at lifecycle moments (before/after add, restore, and publish-triggered updates). Each hook receives a typed JSON payload as its first argument. Supports `.ts` (run with bun), `.sh` (run with bash), and extensionless (direct execution) formats. Pre-hooks can abort operations, post-hooks are advisory. See [Repo hooks](#repo-hooks) for details.\n\n`pkglab add` automatically injects `pkglab check` into your pre-commit hook (Husky, or raw `.git/hooks/pre-commit`). For Lefthook, add it manually:\n\n```yaml\n# lefthook.yml\npre-commit:\n  commands:\n    pkglab-check:\n      run: npx pkglab check\n```\n\n## Multi-worktree support\n\nWhen working on multiple branches simultaneously using git worktrees, you can publish from each worktree with a different tag to avoid version conflicts.\n\n```bash\n# From the main worktree\npkglab pub\n\n# From a feature worktree\npkglab pub --tag feat-auth\n# or auto-detect from branch name\npkglab pub --worktree\n\n# Consumer repos pin to a specific tag\npkglab add @clerk/pkg@feat-auth\n# or use --tag to apply a tag to all packages at once\npkglab add @clerk/pkg --tag feat-auth\n\n# Replace all packages of a scope with their local versions\npkglab add --scope clerk --tag feat-auth\n\n# Or use the interactive picker to browse packages and tags\npkglab add\n```\n\nEach tag gets its own version channel. Publishing untagged only auto-updates consumers that are also untagged, and publishing with `--tag feat-auth` only updates consumers pinned to `feat-auth`. Pruning also respects tags, keeping the N most recent versions per tag per package.\n\nBranch names are sanitized for use as tags: `feat/auth-rewrite` becomes `feat-auth-rewrite`. Tags are capped at 50 characters.\n\n## Catalog support\n\nIf your consumer repo uses the `catalog:` protocol (Bun or pnpm workspaces), pkglab auto-detects catalog entries and updates the catalog source directly, preserving `catalog:` references in individual package.json files.\n\n```bash\n# Auto-detects that these packages are in a catalog and updates the catalog source\npkglab add @clerk/backend @clerk/shared\n\n# Use --catalog for strict mode (errors if a package is not in any catalog)\npkglab add --catalog @clerk/backend @clerk/shared\n\n# Restore puts the original catalog versions back\npkglab restore --all\n```\n\npkglab auto-detects which catalog format your workspace uses:\n\nFor bun/npm workspaces, catalogs live in the root package.json:\n\n```json\n{\n  \"catalog\": {\n    \"@clerk/backend\": \"^3.0.0\",\n    \"@clerk/shared\": \"^2.0.0\"\n  }\n}\n```\n\nFor pnpm workspaces, catalogs live in pnpm-workspace.yaml:\n\n```yaml\ncatalog:\n  '@clerk/backend': '^3.0.0'\n  '@clerk/shared': '^2.0.0'\n```\n\nNamed catalogs (`catalogs.react19`, etc.) are also supported in both formats. pkglab finds which catalog contains each package automatically.\n\nWhen `pkglab pub` auto-updates consumer repos, catalog-linked packages are updated in the catalog (not in individual package.json files), preserving the `catalog:` references.\n\nBun caveat: bun caches registry metadata for up to 5 minutes, so freshly published versions are invisible to a plain `bun install`. pkglab works around this by temporarily setting `disableManifest = true` in `bunfig.toml` during consumer updates, then restoring it. This will become unnecessary once bun supports catalogs in `bun add` directly, at which point pkglab can use the non-catalog install path for bun.\n\n## How versioning works\n\n`pkglab` generates versions in the format `0.0.0-pkglab.{timestamp}` (untagged) or `0.0.0-pkglab-{tag}.{timestamp}` (tagged). The monotonic timestamp ensures versions always increment, even across rapid publishes. The `0.0.0-pkglab` prefix makes these versions instantly recognizable and ensures they sort below any real release.\n\nWhen using tags, each tag gets its own version channel. Publishing with `--tag feat1` only updates consumers pinned to `feat1`, leaving other consumers untouched. This lets you work on multiple branches simultaneously from different git worktrees without version conflicts.\n\n## Design decisions\n\n**The duplicate-instance problem.** In a monorepo like Clerk's, `@clerk/nextjs` depends on both `@clerk/react` and `@clerk/backend`, which both depend on `@clerk/shared`. If you publish `@clerk/react` and cascade to `nextjs`, but `backend` isn't in the publish set, the consumer ends up with two versions of `@clerk/shared`: a new one (from the react cascade) and an old one (from the previous backend publish). Two versions of the same package means two module instances in Node.js, even if the code is identical. Singleton state isn't shared, `instanceof` checks fail across the boundary, and React contexts created by one version are invisible to the other.\n\n**Close under deps.** The cascade now ensures every published package has all its workspace dependencies also in the publish set. If `nextjs` is being published and it depends on `backend`, `backend` gets pulled in. This eliminates floating `workspace:` references that could resolve to stale versions. Any workspace shorthand dep not covered by the cascade is treated as a bug and throws an error at publish time.\n\n**Why not publish the full connected component?** The obvious fix is to publish every package connected through the dependency graph. But in a monorepo where a shared utility connects everything, publishing any one package would publish the entire repo. For Clerk, that's ~15 packages when you only changed one.\n\n**Content-aware publishing.** Instead of giving every package a new version, pkglab fingerprints each package by globbing the publishable file set (the `files` field, always-included files like package.json/README/LICENSE, and entry points from main/module/types/bin/exports) and SHA-256 hashing their contents. Packages are classified in topological order: \"changed\" if the content hash differs from the previous publish, \"propagated\" if the content is the same but a dependency got a new version, or \"unchanged\" if nothing is different. Unchanged packages keep their existing version and are skipped entirely.\n\n**Two-phase cascade.** The cascade doesn't compute all dependents upfront. Instead, it works in two phases: first fingerprint, then expand. This matters because expanding dependents from an unchanged package is pointless (it kept its old version, so nothing downstream needs updating). Consider this workspace:\n\n```\npkgA (shared utility)\n  ├── pkgB ── pkgD (target)\n  └── pkgC\n```\n\nYou run `pkglab pub` from pkgD. Phase one pulls in pkgD's transitive deps (pkgB, pkgA) and fingerprints them. What happens next depends on what actually changed:\n\nScenario 1: you edited pkgA.\n\n```\npkgA (shared utility)    ← changed\n  ├── pkgB ── pkgD       ← propagated, propagated\n  └── pkgC               ← pulled in as dependent of pkgA\n```\n\npkgA is classified as \"changed,\" so phase two expands its dependents. pkgC gets pulled into scope because it depends on the package that changed. All four packages are published with new versions, so consumers see a consistent set.\n\nScenario 2: pkgA is unchanged.\n\n```\npkgA (shared utility)    ← unchanged, skipped\n  ├── pkgB ── pkgD       ← changed (you edited pkgB or pkgD)\n  └── pkgC               ← not in scope, stays on old version\n```\n\npkgA keeps its existing version and pkgC is never pulled in. The cascade stays narrow: only pkgB and pkgD get new versions. Consumers already have a working pkgA, so there's no inconsistency.\n\nThis two-phase approach gives you the best of both worlds. Narrow publishes when nothing upstream changed, automatic expansion when a shared dependency did change. The loop repeats until no new packages are added (a fixpoint), so deeply nested dependency changes propagate correctly through the entire graph.\n\n**The cascade in three steps.**\n\n1. DOWN (deps): pull in transitive deps of the target, unconditionally. These must be in scope so `workspace:^` references resolve.\n2. UP (dependents): expand from packages whose content actually changed. If `shared` is unchanged, don't cascade to `express`/`remix`. If `shared` changed, pull them in.\n3. PUBLISH filter: of the dependents pulled in by step 2, only publish the ones a consumer has installed (via `pkglab add`). `express` not consumed? Skip it.\n\nWhen no consumer repos are active (you haven't run `pkglab add` anywhere), the consumed set is empty and the filter removes all dependents. The result is targets + deps only. Once you add a consumer repo, dependents of changed packages start flowing through.\n\n`--shallow` skips step 2 entirely: targets + their transitive deps, no dependent expansion, regardless of whether consumer repos exist. Useful for a quick publish when you know you only care about the target.\n\nTrade-off: adding a previously-skipped package via `pkglab add` gives the last-published version until the next `pkglab pub`.\n\n**Fast consumer installs.** When auto-updating consumer repos, pkglab writes version updates to package.json (or catalog), then runs a single `pm install --ignore-scripts` per repo. If install fails, it retries with scripts enabled, and rolls back package.json changes if that also fails. For pnpm consumers, pkglab patches pnpm-lock.yaml directly (replacing version strings and integrity hashes) and runs `pnpm install --frozen-lockfile` to skip the expensive resolution phase entirely. Falls back to regular install if patching fails. Use `--no-pm-optimizations` to disable all install optimizations and run a plain `pm install` instead.\n\n## Configuration\n\n`pkglab` stores its state in `~/.pkglab/`. The config file at `~/.pkglab/config.yaml` supports:\n\n- `port`: registry port (default: 16180)\n- `prune_keep`: number of old versions to retain per package (default: 3)\n\nLogs are written to `/tmp/pkglab/registry.log`.\n\nSet `PKGLAB_NO_MTIME_CACHE=1` to disable the mtime-based fingerprint fast path and always do full content hashing.\n\n## Repo hooks\n\nConsumer repos can run custom scripts at lifecycle moments by placing files in `.pkglab/hooks/`. This is useful for setting environment variables, restarting dev servers, or running any repo-specific setup when local packages are installed or removed.\n\n```bash\n# Scaffold the hooks directory with type definitions and example stubs\npkglab hooks init\n```\n\nThis creates `.pkglab/hooks/` with a `payload.d.ts` type definition and commented-out stubs for all 7 events:\n\n- `pre-add` / `post-add` - before/after `pkglab add`\n- `pre-restore` / `post-restore` - before/after `pkglab restore`\n- `pre-update` / `post-update` - before/after `pkglab pub` auto-updates this repo\n- `on-error` - when any operation fails (for cleanup)\n\nHooks can be `.ts` (run with bun), `.sh` (run with bash), or extensionless (direct execution). Each hook receives a typed JSON payload as its first argument with package details, registry URL, tag, and event info.\n\nPre-hooks can abort operations by exiting non-zero. For `add`/`restore`, this stops the command. For `pub` auto-updates, it skips that repo and continues with others. Post-hooks are advisory (failures are logged but don't affect the exit code). The `on-error` hook is best-effort and non-recursive.\n\nExample: point Clerk SDKs to a local API when local packages are installed.\n\n```typescript\n// .pkglab/hooks/post-add.ts\nimport type { PkglabHookPayload } from './payload';\nconst payload: PkglabHookPayload = JSON.parse(process.argv[2]);\n\nconst envFile = Bun.file('.env.local');\nconst existing = await envFile.exists() ? await envFile.text() : '';\nconst cleaned = existing.split('\\n').filter(l =\u003e !l.startsWith('CLERK_API_URL=')).join('\\n');\nawait Bun.write(envFile, cleaned.trimEnd() + '\\nCLERK_API_URL=http://localhost:3100\\n');\n```\n\nHooks persist across add/restore cycles and can be committed to version control. The default timeout is 30 seconds per hook, overridable via `PKGLAB_HOOK_TIMEOUT_MS`.\n\n## Performance\n\npkglab ships its own registry server built on `Bun.serve()`. Package metadata is held in memory with write-through persistence to disk, so reads are served directly from memory without touching the filesystem or making HTTP round-trips.\n\nBenchmarks from the [Clerk JavaScript SDK](https://github.com/clerk/javascript) monorepo (22 packages, Apple M4 Pro, Bun 1.3.9):\n\n- Cold start: 59ms (daemon process to first request)\n- Publish (22 packages): 1.06s (cascade, fingerprinting, parallel publish)\n- Packument GET: 0.09ms (in-memory index lookup)\n- Memory idle (RSS): 44MB\n- Memory after publish: 45MB\n\nMemory stays flat because the server only loads package metadata (packument JSON docs) into memory, not tarballs. Tarballs are served directly from disk via `Bun.file()`. For a typical local development workflow with tens of packages, the memory footprint stays well under 50MB.\n\nYou can run the benchmark yourself:\n\n```bash\nbun run benchmarks/registry-benchmark.ts\n```\n\n## Safety\n\n`pkglab` is designed to prevent local development artifacts from leaking into your codebase:\n\n- `git update-index --skip-worktree .npmrc` is applied automatically when you `pkglab add`, so your localhost registry URL won't show up in `git status` (requires `.npmrc` to be tracked by git)\n- Automatic pre-commit hook injection: on first `pkglab add`, a `pkglab check` call is injected into the consumer's pre-commit hook (Husky, Lefthook, or raw git hooks). Removed automatically on restore when no packages remain\n- Lockfile sanitization: after every pkglab-managed install, `bun.lock` is post-processed to replace localhost registry URLs with `\"\"` (bun resolves from the default registry). This prevents CI hangs if the lockfile gets committed while pkglab is active\n- `pkglab check` scans for any remaining artifacts (package versions, `.npmrc` markers, staged files, lockfile localhost URLs) and returns a non-zero exit code\n- `pkglab restore` restores original package versions and cleans up `.npmrc` and pre-commit hooks\n- `pkglab down` restores all consumer repos before stopping the daemon, preventing broken state\n- `pkglab doctor` verifies and repairs skip-worktree flags, detects dirty state, and can sanitize lockfiles with `--lockfile`\n\n## Acknowledgments\n\n- [Verdaccio](https://github.com/verdaccio/verdaccio) is the project that made local npm registries practical. pkglab started as a wrapper around Verdaccio, and the team's work on registry compatibility and plugin architecture made it possible to prototype quickly before building a custom server.\n- [yalc](https://github.com/wclr/yalc) pioneered the copy-based approach to local package development and showed that symlinks aren't the only way. pkglab takes the idea further by using a real registry, but yalc remains a great lighter-weight option if you don't need registry-level validation. Thank you to the yalc maintainers for paving the way.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclerk%2Fpkglab","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclerk%2Fpkglab","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclerk%2Fpkglab/lists"}