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

@zuilib/cli

v0.5.0

Published

ZUI — install @zuilib/primitives as source from the ZUI registry (zui init, zui add, zui diff, zui update), manage organisational registries (zui registry add/build, integrity pins), and validate and compare theme files (zui theme check, zui theme diff),

Readme

@zuilib/cli

zui: installs @zuilib/primitives as source from the ZUI registry at https://zuilib.com/r. The files land in your repo and are yours to edit; the CLI keeps a pristine copy of what it wrote, so zui update can three-way merge a newer registry version into a file you changed instead of overwriting it.

Only @zuilib/primitives (and the token css it depends on) is distributed this way. @zuilib/data-grid, charts, ai, apps, workflows, builder and text-editor are npm packages.

Usage

npx @zuilib/cli init          # zui.json + the stylesheet and tokens, wired into your css
npx @zuilib/cli add button    # button.tsx, the helpers it imports, its npm deps
npx @zuilib/cli list          # (alias: ls) every item; ✓ installed, ↑ newer in the registry, (via x) inherited; --json for machines
npx @zuilib/cli diff          # how the installed files differ from the registry; --stat, --json
npx @zuilib/cli status        # drift table; --json for CI, --strict exits 1 on drift
npx @zuilib/cli update        # merge the registry's changes into your files (conflict markers on overlap)
npx @zuilib/cli remove button # (alias: rm) delete an item; --prune drops dependencies nothing else needs

pnpm dlx @zuilib/cli … and bunx @zuilib/cli … work the same. zui --version prints the tool's own version. Node 18+; the only dependency is @zuilib/tokens, for the token contract zui theme check validates against.

Owned but upgradable

add writes each file twice: to your tree, and as a pristine copy (a snapshot) under .zui/snapshots/<item>/<target>. Commit .zui/snapshots with zui.lock.json; together they are the upgrade path. .zui/cache (fetched json with ETags, for --offline) ignores itself.

update looks at every file of every installed item (or the ones you name):

| Your copy | Registry | What happens | |---|---|---| | unchanged | unchanged | nothing | | unchanged | moved | rewritten | | edited | unchanged | left alone (unchanged) | | edited | moved | three-way merged: the snapshot (.zui/snapshots) vs yours vs the registry. Hunks changed on one side only are taken; a hunk both sides changed differently is written as <<<<<<< ours … ======= … >>>>>>> zui@<version> |

A CRLF local copy is merged line by line against the LF registry and stays CRLF. A merge writes the result to the file, your pre-merge copy to <file>.orig, the new registry content to .zui/snapshots, and the item's new version and hashes to the lock. Files with conflicts are listed as conflict and the command exits 1; resolve the markers and commit. update refuses to merge a file that still carries markers. --dry-run prints would merge / conflict per file without touching anything. --force skips the merge and overwrites edited files (keeping <file>.orig).

zui status tells you beforehand: its update column (and merge in --json) is merge-needed when the edits merge cleanly, conflict-likely when they overlap, rewrite when your copy is pristine, and no-snapshot for an item installed by a CLI older than .zui/snapshots. For those, zui snapshot restore seeds the snapshot directory: an unedited file is copied from disk, an edited one is fetched from <registry>/v/<locked version>/<item>.json (for a v1 lock, the registry url the lock recorded) and kept only when its hash matches the lock. Without a snapshot, update refuses to touch an edited file (use --force to overwrite).

add itself never merges: an item already installed and edited is refused unless --force.

Theme validation

npx @zuilib/cli theme init acme        # acme.css: every token with its default, commented out
npx @zuilib/cli theme check acme.css   # exit 1 on any error
npx @zuilib/cli theme check acme.css --json --strict --dark-selector '[data-theme="dark"]'
npx @zuilib/cli theme diff old.css new.css   # what a revision changed; exit 1 on a new contrast failure

check parses the custom-property overrides of a theme file (:root, .theme-x, html[data-zui-theme="x"], and .dark … / @media (prefers-color-scheme: dark) for dark), merges them over the shipped @zuilib/tokens defaults for both modes and runs these rules (the rule field of --json problems):

| Rule | Severity | Catches | |---|---|---| | unknown-token | error | a name not in the contract, with the nearest real one (--primry → --primary); a property the file reads with var() itself is a private helper | | invalid-value | error | a colour token that is not a colour (hex, rgb(), hsl(), oklch(), oklab(), color-mix(), light-dark()), a length token that is not a length, a bad number / font weight / duration | | var-cycle | error | --a: var(--a) or a loop through several overrides | | missing-dark | warn | a colour with distinct shipped light/dark values overridden in light only (the :root value wins in dark too), or in dark only | | contrast | error | text on its surface below WCAG AA 4.5:1 in either mode (every --x-foreground on --x; --foreground, --muted-foreground and every --<hue>-text on --background; sidebar) | | tint-contrast | error | a --<hue>-text below 4.5:1 on its hue at 10% over --background (badges, callouts, bg-<hue>/10) | | cross-surface-contrast | error | --foreground / --muted-foreground below 4.5:1 on the surfaces they travel onto (--muted, --accent, --secondary, --card, --popover) | | ui-contrast | warn | --border, --input or --ring below 3:1 on --background when the theme changes them (WCAG 1.4.11); an error with --strict | | uncheckable-contrast | warn | a pair that resolves to a colour the checker cannot evaluate (relative colours, exotic colour functions, currentColor, an undeclared var()) | | inert-override | warn | an override of a deprecated token (no component reads it, so it changes nothing) | | radius-extreme | warn | --radius over 32px (Card and Popover read it too) |

--strict makes warnings errors; --min-contrast 3 lowers the text bar; --json prints { ok, file, problems: [{ rule, severity, token, message, mode?, line? }] }. The token contract comes from @zuilib/tokens/tokens.json (the CLI's one dependency). Docs: tokens/validation.

Your own rules (themeRules)

An org can gate theme check and theme diff on its own conventions. List rule modules in zui.json:

{ "themeRules": ["./theme-rules/brand.mjs"] }

Each module's default export is one rule — the same shape as the built-in ones — or an array of them:

export default {
  id: 'no-rem-radius',
  severity: 'error', // or 'warn' (promoted to error by --strict)
  describe: 'radii are set in px here',
  run: ({ declarations }) =>
    declarations
      .filter((d) => d.name.includes('radius') && d.value.endsWith('rem'))
      .map((d) => ({ token: d.name, message: 'use px, not rem', mode: d.mode, line: d.line })),
}

run(ctx) gets the file's parsed declarations ({ name, value, mode, scope, line }), the token contract, the per-mode overrides maps, the evaluated contrast ratios, and resolve(name, mode) for the effective value of any token; it returns problems { token, message, mode?, line?, severity? }, reported under the rule's id exactly like the built-ins (including in --json). Paths resolve relative to zui.json and must stay inside the project; a module that is missing a field (or reuses a built-in rule id) fails with an error naming the file. Without a zui.json, theme check runs the built-in rules only.

zui theme diff

zui theme diff old.css new.css is the semantic changelog between two theme files: the --zui-theme-version change, the tokens added / removed / changed per scope and mode grouped by contract section, and the contrast ratio of every pair either file moved — a pair going pass → fail is flagged as a regression. Exits 1 only on a regression (never on the token changes themselves), so a theme revision can gate CI on "did this break contrast" rather than "did this change anything". Both files are parsed with the same contract and cascade as check; --dark-selector, --min-contrast and --json ({ ok, version, sections, contrast, regressions, counts }) work the same.

Several registries, and your own:

npx @zuilib/cli registry add acme https://design.acme.com/r   # an organisational registry that extends ZUI's
npx @zuilib/cli add acme/data-table                          # resolves in acme first, then in what it extends
npx @zuilib/cli list --json                                  # the listing as data: { registries, items: [{ name, key, registry, type, version, description, installed, installedVersion, outdated, via, overrides }] }
npx @zuilib/cli status --json                                # drift table: installed/latest, edited locally, outdated, mergeable
npx @zuilib/cli registry init && npx @zuilib/cli registry build   # build your own registry from registry.config.json

Item refs are button (default registry), acme/data-table (named registry) or a direct item url, which must sit under latest/ or v/<version>/ (https://…/r/v/1.2.0/item.json). A url inside a configured registry is keyed as that registry; one from an unconfigured registry is locked under <registry base>/<item>, which works as a ref again. A private registry reads its bearer token from ZUI_REGISTRY_TOKEN_<NAME> (- becomes _, _ becomes __); the token and the registry's headers go only to urls under that registry's url, never to another path on the same host. See Organisational registries.

Safety

  • Dependency specs. Every npm spec an item declares must match name, @scope/name, optionally @<range>, with no spaces and no leading dash; anything else is rejected before a file is written. The package manager is spawned without a shell (on Windows through cmd.exe /d /s /c with every argument quoted, since pnpm.cmd cannot be spawned directly). A package already in package.json is never reinstalled; when its declared range sits on another major than the item needs (^1.2.0 vs ^2.1.1) the CLI warns, and --strict fails before any file is written. Compound ranges (>=1 <3, 1 || 2) are not judged. The files and the lock are written before the install runs, so a failed install leaves nothing orphaned; rerun the printed command.
  • Index integrity (TOFU-lite). A registry index may carry integrity, the sha256 of its compact json without that field (zui registry build writes it). The CLI recomputes it on every fetch and fails on a mismatch. zui registry add <name> <url> --pin copies the current value into zui.json (registries.<name>.integrity); zui registry pin [<name>] does the same for an already configured registry, the default one when no name is given (registry.integrity), and moves the pin after a change you trust. A pin covers that registry's own index only, not the registries it extends or other versions of it. Trust is placed once, at pin time; there is no key or signature chain.
  • Offline. Fetched documents are cached under .zui/cache with their ETag and revalidated with If-None-Match. --offline on any command reads only the cache and fails on a url never fetched.

zui.json

{
  "registry": {
    "url": "https://zuilib.com/r",
    "version": "latest"
  },
  "paths": {
    "components": "src/components/zui",
    "styles": "src/styles/zui",
    "css": "src/index.css"
  }
}

| Key | Meaning | |---|---| | registry.url | Base url of the registry. A local directory works too (../zui/public-docs/static/r), for developing against an unpublished build | | registry.version | latest, or a pinned @zuilib/primitives version (0.1.0), read from <registry>/v/<version>/ | | registry.integrity | Optional sha256 pin of the default registry's index (see Safety) | | registry.headers | Extra request headers for the default registry | | registries | Named registries besides the default: { "acme": { "url", "version", "headers", "integrity" } }; items are acme/<item>. Managed by zui registry add/remove/list (rm/ls work too) | | paths.components | Where components/zui/… items land. Helpers go to <components>/lib/, so the components' relative imports work unchanged | | paths.styles | Where styles/zui/… items land (zui.css, tokens.css, …) | | paths.css | Your global stylesheet; init adds @import "<styles>/zui.css" after @import "tailwindcss" | | themeRules | Optional array of rule modules (relative to zui.json) merged into the zui theme check / zui theme diff rule set; see Theme validation |

init guesses paths.components/paths.styles (under src/ when it exists) and paths.css (app/globals.css, src/index.css, …); override with --components, --styles, --css, --registry, --registry-version. A zui.json in the old flat layout (a top-level registry url, components, styles, css) is refused with a message spelling out the new shape.

zui.lock.json

Written by add/update/remove ("lockVersion": 2): for every installed item the registry it came from (registryName, url), its version and the sha256 of each file's registry content, the same bytes as .zui/snapshots/<item>/<target>. A file whose hash differs from the lock was edited locally; a registry file whose hash differs from the lock moved. The version of an item only moves when one of its files was written. A version 1 lock (no lockVersion) is read as is and rewritten as version 2 on the next write.

Registry format

The registry is static json, generated by packages/primitives/scripts/build-registry.mjs and served from the docs site:

| Url | Contents | |---|---| | /r/schema.json | JSON schema of the index and the items | | /r/latest/index.json | Every item: name, type (component / lib / theme), version, description, npm dependencies, registryDependencies, file targets | | /r/latest/<name>.json | One item with the file contents and their sha256 | | /r/v/<version>/… | The same, frozen per @zuilib/primitives release |

An organisational registry has the same layout (built by zui registry build) plus extends in its index: item lookups fall through to the listed registries, and an item of the same name in the org registry takes precedence. zui registry build also writes integrity into the index.