Skip to Content
Documentation

CLI

Doctor and inspect

Find VitNode plugin registration, permission and configuration problems with vitnode doctor, read its results and exit codes, and list routes with inspect.

vitnode doctor finds plugin mistakes that compile but break at runtime: a module nobody registered, a route guarded by a permission nobody can be granted, two routes on the same URL, a queue task that never runs. vitnode inspect lists what each plugin registers. Both read source and build output: no server starts, no job runs and no database is needed.

Run them inside a plugin to check just that plugin. Inside an app, they cover every plugin the app configures plus the workspace's other plugins. To pick one plugin, pass its package name, its name without the npm scope, or its folder name: pnpm vitnode doctor site-notes.

Check plugins

pnpm vitnode doctor

This site-notes declares its permissions in src/staff-permissions.ts, but config.api.ts does not pass them to buildApiPlugin({ permissionStaff }), and one admin route has no guard:

ā—† VitNode
  Doctor

  site-notes  .
  āœ“ Package  0.1.0
  āœ“ Build output  config.js, config.api.js
  āœ“ Plugin definition
  āœ“ API definition
  āœ“ Routes  1 route
  āœ“ Translations  en
  āœ– AdminCP navigation
      AdminCP navigation item /admin/site-notes requires the permission notes.can_create,
      but site-notes does not declare it in the admin permissions of its defineStaffPermissions(manifest, {...}).
      dist/src/admin/nav.js
  āœ“ API registration  3 modules, 7 routes reached
  āœ“ Fetcher registry
  ā—‹ App registration  not used by an app in this workspace
  āœ– Staff permission guards
      GET /site-notes/admin/notes: adminStaffPermission requires notes.can_view, which site-notes does not declare in its defineStaffPermissions(manifest, { admin }).
      POST /site-notes/admin/notes: adminStaffPermission requires notes.can_create, which site-notes does not declare in its defineStaffPermissions(manifest, { admin }).
      GET /site-notes/admin/notes/stats is under /admin but declares no adminStaffPermission, so any admin can call it.
  āœ“ Endpoints  7 routes, no duplicates
  āœ“ Background registrations  0 jobs, 0 listeners, all on top-level modules

  10 checks passed, 0 warnings, 2 failed, 0 not run, 1 not applicable.
  Read from each plugin's build output (dist/), declarations only: staff permission guards declared on buildRoute were checked, access checks inside handlers or middleware were not. Rebuild first - doctor cannot tell whether dist/ is older than src/.


āœ– 1 plugin with problems: site-notes

The output above is shortened to two of the five guarded routes. The first checks are the ones vitnode plugin validate runs, with the same rules. Its Routes check counts the plugin's pages; Endpoints below counts API routes. Doctor adds:

CheckReadsFails or warns when
API registrationSourceWarns: a buildModule or buildRoute export under src/api/modules is not reached from config.api.ts
Fetcher registrySourceFails: config.api.ts does not export type VitNodeApiPlugin. Warns: types/api-registry.gen.d.ts is missing or not in tsconfig.json
App registrationSourceFails: the app lists the plugin in vitnode.config.ts but no vitnode.api.config.ts imports <plugin>/config.api. Warns the other way round
Staff permission guardsBuild outputFails: a route's adminStaffPermission or moderatorStaffPermission is not declared by the plugin, or is declared for the other staff type. Warns: a route under admin/ has no adminStaffPermission
EndpointsBuild outputFails: two routes answer the same method and path, whatever their path parameters are called; only the first one is reachable
Background registrationsBuild outputFails: cron jobs, event listeners, queue tasks or WebSockets sit on a nested module, where they never run

A guard that references another plugin's permission is counted but not checked, and the summary says so. The checks read declarations only: a checkStaffPermission call or an ownership rule inside a handler is never verified.

Results and exit codes

With --plain, each check starts with a tag instead of a symbol:

TagSymbolMeaning
OKāœ“The check ran and passed
WARN!The check ran and found something to look at
FAILāœ–The check ran and found an error
BLOCKEDā—‹The check could not run because an earlier one failed. It counts as not run
SKIPā—‹The check does not apply, such as App registration for a plugin no app uses

A plugin without a build shows both kinds of not run:

VitNode - Doctor
site-notes (.)
[OK] Package (0.1.0)
[FAIL] Build output
  dist/src/config.js does not exist - apps load plugins from their build output.
  dist/src/config.api.js does not exist, but src/config.api.ts does - the app's API cannot import site-notes/config.api.
  Run vitnode build in the plugin's folder (or pnpm build:plugins), then check again.
[OK] API registration (3 modules, 7 routes reached)
[OK] Fetcher registry
[SKIP] App registration (not used by an app in this workspace)
[BLOCKED] Staff permission guards (not run - Build output failed)
[BLOCKED] Endpoints (not run - Build output failed)
[BLOCKED] Background registrations (not run - Build output failed)

3 checks passed, 0 warnings, 1 failed, 3 not run, 1 not applicable.
Exit codeWhen
0No check failed and none was blocked. Warnings are allowed, unless you pass --strict
1A check failed, a check was blocked, or, with --strict, a check warned
2A usage error, such as a plugin name that matches nothing: No plugin named "nope" was found.

Fail CI on warnings

--strict treats warnings as failures, so an unguarded admin route stops a pipeline:

pnpm vitnode doctor --strict
  ! Staff permission guards (5 routes with declared guards; checks inside handlers or middleware not verified)
      GET /site-notes/admin/notes/stats is under /admin but declares no adminStaffPermission, so any admin can call it.

  11 checks passed, 1 warning, 0 failed, 0 not run, 1 not applicable.


āœ– 1 plugin with problems: site-notes

  --strict fails on warnings too. Fix them, or run vitnode doctor without --strict to fail on errors only.

Without --strict the same plugin passes with No errors - see the warnings above (--strict fails on them). Run doctor after pnpm build:plugins: it reads dist/, and it cannot tell whether dist/ is older than src/.

Background work on nested modules

buildApiPlugin collects content types and notification types from the whole module tree, but cron jobs, event listeners, queue tasks and WebSockets only from the modules passed to it directly. In production, nested ones are silently ignored, and doctor fails Background registrations:

  āœ– Background registrations
      site-notes: queueTasks "announce-note" on nested module "notes/jobs" never run - buildApiPlugin collects queueTasks from top-level modules only. Move it to buildModule({ queueTasks }) of the top-level module "notes".

In development (NODE_ENV=development), buildApiPlugin throws instead, starting with Plugin "site-notes" declares background work on nested modules., so the mistake shows up the first time pnpm dev loads the API.

When the plugin's factory throws

To read a plugin's API, doctor, vitnode inspect and vitnode plugin validate call the function that the built config.api.js exports. When that call throws, they print the original error under API definition, after dist/src/config.api.js: calling the exported function siteNotesApiPlugin() threw, so no buildApiPlugin({ manifest, ... }) definition could be read:. Staff permission guards, Endpoints and Background registrations need the definition, so they are blocked with not run - API definition failed. Fix the quoted error and build again. A file that exports no factory at all fails with dist/src/config.api.js exports no buildApiPlugin({ manifest, ... }) factory. instead.

List what plugins register

pnpm vitnode inspect routes site-notes
  site-notes  .
  Method   Path                            Staff permission
  ───────────────────────────────────────────────────────────────
  GET      /site-notes/hello               -
  GET      /site-notes/admin/notes         admin notes.can_view
  GET      /site-notes/admin/notes/{id}    admin notes.can_view
  POST     /site-notes/admin/notes         admin notes.can_create
  PATCH    /site-notes/admin/notes/{id}    admin notes.can_edit
  DELETE   /site-notes/admin/notes/{id}    admin notes.can_delete
  GET      /site-notes/admin/notes/stats   -
CommandLists
vitnode inspect routesEvery route's method, path and declared staff permission
vitnode inspect jobsCron jobs with their schedule, and queue tasks with their owner and payload contract, by module
vitnode inspect eventsEvent listeners with the event's owner, contract and listener name, WebSockets by id, and the event definitions the plugin registers, including Content Engine ones

For example, inspect jobs in the newsroom plugin and inspect events in the payments demo:

  newsroom  .
  Kind    Name                  Owner      Contract         Schedule   Module
  ────────────────────────────────────────────────────────────────────────────────
  queue   announce-correction   newsroom   payload schema   -          corrections
  payments-demo  .
  Kind    Event or id         Event owner     Contract         Listener            Module
  ──────────────────────────────────────────────────────────────────────────────────────────
  event   payment.succeeded   @vitnode/core   payload schema   log-paid-purchase   purchases

  No event definitions in buildApiPlugin({ eventDefinitions }).

Each command takes the same optional plugin name as doctor. Paths include the plugin id and sit under the app's API base path, /api in the starter apps. Generated Content Engine routes are listed too, with the permissions the engine assigned.

The Staff permission column shows what buildRoute was given in adminStaffPermission and moderatorStaffPermission. It does not show everything a route checks: a checkStaffPermission call or an ownership rule inside the handler, or a custom middleware, is invisible here.

vitnode inspect reads each plugin's dist folder, so build plugins first (pnpm build:plugins, or keep vitnode dev running). A plugin that is not built is skipped with a note. A plugin whose API definition fails to load is reported, and the command exits with code 1. The output holds names, schedules, paths and permission keys only, never environment values, secrets, payloads or request data. There is no JSON output; script against the exit codes of vitnode doctor.