PermDock
CLI

collect

Scan source paths for definePermissions() calls and permission usages, write the catalog and generated barrel, and fail CI on drift.

permdock collect is the build step that turns colocated permission definitions into a central catalog. It is modelled on next-intl's useExtracted extraction: the source of truth is the code where permissions are declared and used, and the catalog is a compile output that is checked in and kept fresh by tooling. See why this model and Larger apps.

Usage

permdock collect                       # write permissions.catalog.json (+ barrel, see below)
permdock collect --check               # exit 1 if the outputs on disk differ from what would be written
permdock collect --src ./src --src ../ui/src --src './node_modules/@acme/*'
permdock collect --watch               # rebuild on change (what the Next.js plugin does in next dev)

Options come from permdock.config.ts (collect.srcPath, catalog.out) and can be overridden with flags: --src (repeatable) for collect.srcPath, --out <file> for catalog.out.

--watch collects once, then again 50 ms after the last change to a srcPath folder, the config file, or the configured permissions and policy modules. A glob entry watches the folder above its first wildcard. Changes to the catalog and barrel it wrote are ignored, and changes during a run queue one more run. Every run after the first loads the project modules again, so an edited import is picked up. Failures are printed and the watcher keeps running; --watch with --check is a usage error.

What it scans

collect parses every .ts and .tsx file under each srcPath entry with oxc-parser and records two things:

  1. Definitions: every definePermissions({...}) and resource(...) call, with its file, export name and the static shape of the tree (resource names, actions, collection, action metadata). Schemas are not evaluated during the scan; the catalog's resource schemas come from executing the merged definition module and calling Standard JSON Schema.
  2. Usages: every member access rooted at a known definition export, such as permissions.post.update or postPermissions.post.read, together with the call it feeds (can, decide, assert, usePermission, protect, allow, registerTool, ...). Usages are what permdock usage and permdock doctor report on.

srcPath entries may point at first-party code, sibling packages in a monorepo and installed packages (./node_modules/@acme/*), exactly as next-intl allows for shared UI packages. Globs are resolved relative to --cwd and match segment by segment (packages/*/src walks only each package's src). Under a glob, node_modules, dist, .next, .turbo and coverage are skipped unless the pattern names them literally; inside an installed package (./node_modules/@acme/*) dist is collected and nested node_modules never is. A file reached twice, for example through a workspace symlink, is collected once.

Usage scanning also records allow(permissions.x.y) calls in policy files, as usages with call: 'allow'. Installed packages are scanned only when a srcPath entry names them.

Scanning is static: dynamic keys, computed member access and permissions resolved through findPermission(permissions, someString) are recorded as dynamic usages and reported separately rather than guessed at.

Outputs

  • permissions.catalog.json: the catalog described on the catalog page, including for each permission its key, scope, arity, metadata, defining file and usage sites. Deterministic ordering and formatting so diffs are meaningful.
  • Generated barrel (src/permissions.generated.ts by default, only when collect.barrel is enabled): a // @generated file that imports permissions from the configured permissions module (permissions in permdock.config.ts, or the first of the conventional paths that exists), relative to the barrel's own folder, and passes it to mergePermissions().

Generated files carry a header with the CLI version and the list of inputs, and are formatted with Oxfmt so they do not churn under the project's formatter.

--check in CI

--check computes the outputs in memory and compares them with the files on disk. Any difference exits 1 and prints a unified diff. This is the permissions equivalent of next-intl checking that target locale files are in sync with the extracted source messages: adding a permission, renaming a resource or changing action metadata without re-running collect fails the build.

- run: pnpm exec permdock collect --check --cwd tests/integration/fixtures/posts
- run: pnpm exec permdock collect --check --cwd apps/examples/monorepo

The Next.js plugin runs collect during next dev and next build, so in a Next.js app the check mostly catches commits made without running the dev server.

Relationship to the runtime

collect does not define permissions. Because permission leaves are runtime values, the merged definition module is the catalog at runtime (listPermissions(permissions)), and collect exists for three things the runtime cannot do: know where permissions are used, know about features that are not yet imported into the app, and produce artifacts (JSON, JSON Schema, Markdown) that CI, docs and agents read without executing code.

Next.js integration

// next.config.ts
import { createPermDockPlugin } from "permdock/next/plugin";

const permdockPlugin = createPermDockPlugin({
  collect: { srcPath: ["./src", "../ui/src", "./node_modules/@acme/*"] },
});

export default permdockPlugin({});

The plugin is a build hook only; it never wires the API or augments module types. See Next.js plugin.

Why this model

next-intl's useExtracted solved the same shape of problem for translations: many colocated typed declarations, one central artifact, client and server consumers. A central string catalog that usages reference by name puts the declaration far from the code, lets keys drift and accumulates dead entries. Extraction flips it, and PermDock takes the same properties:

next-intlPermDock
useExtracted() declaration in a componentdefinePermissions() in a feature folder
Build-time loaderpermdock collect, createPermDockPlugin({ collect })
messages/*.json as a committed compile outputpermissions.catalog.json, checked with --check
Namespaces for shared packagesNested resource groups merged by mergePermissions (permissions.billing.invoice.pay)
useExtracted / getExtractedusePermission / getPermission, usePermDock / getPermDock
Passing one message namespace to the clientsnapshot({ include: [permissions.post] })
Works in tests without the loaderWorks in tests without collect

The analogy stops in three places. next-intl must extract because a string literal is not a queryable runtime value; PermDock's definitions exist at runtime, so the scan is for usage coverage and cross-package discovery, and a failed scan only leaves the catalog stale. Translations sync target locales; permissions have a policy instead, and permdock usage reports the gap (granted-by-no-role, used-but-ungranted) rather than syncing anything. next-intl's loader rewrites source; PermDock only reads it. Generated definitions (permdock rls import, permdock openapi import) merge through mergePermissions like any hand-written feature file.

Last updated on

On this page