# collect

Source: https://permdock.com/docs/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](#why-this-model) and [Larger apps](/docs/getting-started/larger-apps).

## Usage [#usage]

```bash
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 [#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 [#outputs]

* **`permissions.catalog.json`**: the catalog described on the [catalog](/docs/cli/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-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.

```yaml
- 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](/docs/cli/next-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 [#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 [#nextjs-integration]

```ts
// 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](/docs/cli/next-plugin).

## Why this model [#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-intl | PermDock |
| --- | --- |
| `useExtracted()` declaration in a component | `definePermissions()` in a feature folder |
| Build-time loader | `permdock collect`, `createPermDockPlugin({ collect })` |
| `messages/*.json` as a committed compile output | `permissions.catalog.json`, checked with `--check` |
| Namespaces for shared packages | Nested resource groups merged by `mergePermissions` (`permissions.billing.invoice.pay`) |
| `useExtracted` / `getExtracted` | `usePermission` / `getPermission`, `usePermDock` / `getPermDock` |
| Passing one message namespace to the client | `snapshot({ include: [permissions.post] })` |
| Works in tests without the loader | Works 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.

## Related [#related]

* [catalog](/docs/cli/catalog)
* [usage](/docs/cli/usage)
* [Larger apps](/docs/getting-started/larger-apps)
* [Permissions](/docs/concepts/permissions)
* [next-intl message extraction](https://next-intl.dev/docs/usage/extraction)
