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

directus-deploy-cli

v0.29.8

Published

Per-entity, non-atomic Directus deployment tool. Reconciles collections/fields/relations/roles/policies/permissions/flows/operations/migrations/seeds from git to any environment.

Readme

directus-deploy-cli

Per-entity, non-atomic Directus deployment tool. Reconciles code + config from a git repository to any Directus environment. Handles collections, fields, relations, roles, policies, permissions, flows, operations, raw-SQL migrations, register manifests, and seed data.

Why not the built-in schema apply?

Directus's /schema/apply is atomic — one edge case ("field already exists", "relationship already associated", dangling meta.group ref, adopted-but-unregistered raw-SQL column) blocks every other change. In a repo with many contributors, multiple environments, and raw-SQL adopted tables, that atomic model becomes a firefight generator.

directus-deploy iterates each entity independently: GET → POST | PATCH | SKIP. If one entity fails, the other 900 still apply. Per-entity report tells you exactly what changed and why the skips/failures happened.

Install

npm install -g directus-deploy-cli

Use

DIRECTUS_URL=https://your-directus DIRECTUS_TOKEN=... \
  directus-deploy verify \
    --snapshot-dir=./directus_config/snapshot \
    --config-dir=./directus_config/collections \
    --seed-dir=./directus_config/seed \
    --migrations-dir=./migrations \
    --register-dir=./migrations/register

Commands: plan (dry-run report), apply (reconcile), verify (drift check; exits non-zero if apply would change anything).

Overview

One matrix over every target: each environment compared against the git ref it deploys from, plus a promotion-queue column showing what sits on develop but hasn't reached master yet.

$ directus-deploy overview

              test                staging             prod                develop → master
              vs origin/develop   vs origin/develop   vs origin/master    promotion queue

  migrations  ✓ 86 applied        ✓ 86 applied        ✓ 85 applied        1 new
  extensions  ✓ 21/21 match       ✗ 1 behind          ✗ 1 behind          3 changed
  config      ✓ in sync           ✗ 10 changes        ✓ in sync           20 file(s)
  seeds       ✓ in sync           ✗ 31 changes        ✓ in sync           3 file(s)

Each target declares its branch in the targets file ("ref": "origin/develop"); the artifacts at that ref are materialized via git archive and dry-run against the env, so the comparison is branch-vs-env, not worktree-vs-env. A target without ref falls back to the working tree. The promotion pair is inferred from the refs (the build_forbidden target pins the destination), widening to the full targets file when only a subset is probed and defaulting to origin/develop → origin/master as a last resort; --from/--to override. It is pure git and never affects the exit code.

Below the matrix, the promotion block is a release preview: the queued commit list (merge subjects carry PR numbers), and per queued extension the expected artifact commit joined with what the destination currently runs (prod runs 5cb20e99 → would get 59ce4ffc) plus the commits in between — the same view a release PR body would show. The join reuses the /_meta sourceCommit already fetched for the extensions row, so it costs no extra network.

Exit codes: 2 a check could not run, 1 drift, 0 in sync — errors outrank drift, because a run with an incomplete check has an incomplete drift picture. --json emits the full report (untruncated detail lists) for dashboards or Slack bots.

Progress and deadlines

Checks announce themselves on stderr — once when they start, once when they finish — so a long run is legible while it runs and stdout stays clean for --json and for piping:

$ directus-deploy overview --targets staging,prod
staging  materialize       ...             origin/develop
staging  config            ...
staging  probe:attributes  ...
staging  migrations        ok        0.9s  88 applied, 0 pending, 0 mutated
staging  probe:attributes  ok        2.3s  in sync
staging  config            ok       86.0s  0 config, 0 seed changes
prod     config            TIMEOUT  30.0s  prod config timed out after 30000ms

Both edges are printed on purpose: the config leg alone runs 70–90s, so waiting for completion before saying anything would reproduce the silence this is meant to fix, just on a shorter clock.

--progress auto|plain|none controls it (auto = plain; none restores silent operation for anything parsing stderr).

--timeout <ms> (default 300000, 0 disables) bounds every check — git, HTTP reconcilers, and drift probes alike. A check that trips it renders as TIMEOUT in the matrix and sets exit 2 rather than stalling the command. A timed-out probe is killed by process group, not by pid: probes run as sh -c "<cmd>", and sh only exec-replaces itself for a single command, so a | b or cd x && a would otherwise leave the real worker orphaned and still holding its connection to the target.

The default is deliberately generous because the config leg legitimately runs 70–90s per target while every other leg finishes inside 4s. The tighter bound lives in the HTTP client, which times out each request after 30s and caps a whole retry sequence at 120s — without that ceiling, a target that 503-flaps costs six retries of exponential backoff per request, which is what made overview look deadlocked with no output at all.

Timeouts never replay a write. A 503 means the server shed the request without processing it, so any method is safe to retry. A timeout is an abort on the client side — the request may have reached the server and committed — so it is replayed only for GET/HEAD. A timed-out POST/PATCH/DELETE surfaces immediately rather than risking a duplicate row.

Custom drift probes

Repos have drift surfaces the CLI can't know about (externally-managed data pipelines, computed caches). Declare them in the targets file and overview grows a row per probe:

"drift_probes": [
  { "name": "attributes", "cmd": "node scripts/attributes-drift.mjs --url {url} --token-env {token_env} --json" }
]

The command runs once per target from the working tree ({url}, {token_env}, {target} substituted) and must print one JSON line: { "clean": true|false, "summary": "…" }. Probe drift counts as drift; probe failures as errors.

Seeds

Data tables managed as code: directus_config/seed/<collection>.json files in the Tractr shape ({collection, meta, data}), applied row-by-row keyed on the collection's real primary key. The reconciler compares only the columns present in the seed rows — runtime columns stay untouched — and fetches exactly those columns (a pgvector column won't blow up your instance).

directus-deploy seeds pull categories \
  --fields id,parent_id,code,title,status --insert-order 50   # bootstrap/refresh from a live env
directus-deploy apply --entities seeds                        # converge an env
directus-deploy apply --entities seeds --prune                # also DELETE server rows absent from
                                                              # seeds with meta.delete=true

seeds pull preserves an existing file's meta, derives the field list from the file on re-pulls, orders parents before children (self-referencing FKs survive fresh-env inserts), and hard-errors on duplicate PKs. Server rows absent from a seed are reported: aggregated and informational for meta.delete: false collections, per-row EXTRA (verify fails) for meta.delete: trueapply --prune deletes those, children-first.

Extension deploy model

One rule: new builds enter only at test; staging and prod replay the archive.

  • extensions push <n> --target test — builds and deploys. With --publish (implied by --via control) the tarball is ALSO archived to gs://<artifact_bucket>/<n>/<sha>.tgz (immutable, first-write-wins) — that archive is what makes the build promotable.
  • extensions promote <n> --target staging|prod — installs the archived artifact for the sha of the current checkout. Never builds; refuses if the artifact is missing. This is the only path to prod (build_forbidden).
  • Transports: --via ssh (default; needs ssh/rsync + port-22 egress) or --via control (HTTPS-only, through the target's control_url function — for sandboxes/agents).

So a test deploy is two uploads of one tarball: bucket (archive, for later promotes) and target (install). Deploys above test are one download (bucket) + one install.

VM control

For environments whose VM sleeps when idle: directus-deploy vm status|start|stop --target <name>. start is an instant no-op when <base_url>/server/health already answers, otherwise dispatches the wake and polls until healthy. Backed by a tiny token-gated cloud function (one per instance — see cloudfunctions/vm-control/ for the 5-minute deploy); the target declares control_url in the targets file and the token comes from DIRECTUS_<TARGET>_CONTROL_TOKEN. Callers never hold GCP credentials.

MCP server

Ships an MCP server exposing the reconciler as structured tools:

  • directus_plan / directus_apply / directus_verify — the write path (plan is a dry-run).
  • directus_overview — read-only deployment matrix (targets vs their refs + promotion queue); the best first call for an agent assessing deployment state.
  • directus_migrations_status, directus_extensions_status, directus_extensions_diff — read-only per-dimension checks.

The server auto-loads .env from its cwd, so pointing cwd at the backend repo gives it the DIRECTUS_<TARGET>_TOKEN vars. Register in your Claude client's .mcp.json:

{
  "mcpServers": {
    "directus-deploy": {
      "command": "node",
      "args": ["<abs-path>/node_modules/directus-deploy-cli/dist/mcp-server.js"],
      "env": { "DIRECTUS_URL": "...", "DIRECTUS_TOKEN": "..." }
    }
  }
}

On-disk layout

Compatible with the directus-sync-style tree:

directus_config/
  snapshot/{collections,fields,relations}/*.json
  collections/{roles,policies,permissions,flows,operations}.json
  seed/*.json
migrations/*.sql
migrations/register/*.json

Register manifests: a collection_meta.note (or icon) containing a ; requires a sql-runner token (SQL_RUNNER_<TARGET>_TOKEN, or SQL_RUNNER_TOKEN) so the adopt INSERT runs unsplit — the raw-query fallback splits on ; and would abort adoption. Apostrophes are always safe (escaped). Without a token, a ;-bearing note fails at author time with an actionable message.

License

MIT