Skip to content

node ace adonia:doctor

Diagnose installer artifacts, runtime wiring, packages, storage, and queues.

The doctor command diagnoses installer and runtime configuration and aggregates boot-time warnings. The implementation lives in packages/devtools/src/doctor/.

node ace adonia:doctor
node ace adonia:doctor --only=shield-xsrf --only=cookie-domain

The command never boots the application. Every finding is derived from the file system and from static analysis (ts-morph) of the app’s own TypeScript — which is the point: most of what it diagnoses is a reason the app fails to boot, or boots wrong and silent.

Exit code is 1 when any check fails, 0 otherwise. Warnings never fail CI: a warn means “suspicious, or undecidable statically” (a computed cookie.domain, a missing config file), and a doctor that fails the build on “I could not tell” is a doctor people disable.

Checks

id Fails when Spec
init-hook hooks.init in adonisrc.ts has no indexAdoniaResources() (warns when it runs before indexEntities()) §3.1, §14
stale-codegen .adonisjs/adonia/* is missing, older than the newest app/adonia/** file, or byte-different from a fresh generation §14
panel-access-middleware start/kernel.ts’s router.named({ … }) lacks adonia.panel-access §3.7, §5.2
cookie-domain a domain-mounted panel’s host is not covered by config/session.ts’s cookie.domain §5.2, §19
vite-allowed-hosts vite.config.ts’s server.allowedHosts misses a panel domain §5.2
inertia-layout inertia/app.tsx wraps every resolved page — adonia/* included — in the app’s own layout §13.4
shield-xsrf Shield’s csrf.enabled or csrf.enableXsrfCookie is off §19
authorization-fallback authorization.fallback resolves to 'allow' in production §4, §12, §20
unscoped-resources tenancy is configured but a reachable resource declares no static tenantScope §5.3
action-events-table action events are enabled but no migration creates adonia_action_events §15.1

Notes on the sharper ones

stale-codegen compares timestamps first (cheap), then re-runs the §14 pipeline in memory and diffs the result against what is committed. The regeneration evaluates resource modules in the codegen worker — the same isolated evaluation the init hook performs, not an app boot. A byte difference is always real: §14 guarantees deterministic output.

cookie-domain requires the leading dot when the cookie is scoped to a parent domain. A panel on admin.acme.com with cookie.domain: 'acme.com' fails; '.acme.com' passes. No domain at all is fine — a host-only cookie is returned to whatever host served it, including every tenant host of a :tenant.acme.com pattern.

inertia-layout exists because the symptom has no error attached to it: packaged pages are self-chroming (they render PanelShell with the panel’s brand, nav, breadcrumbs and toaster), so an app layout on top of them shows the site header around the panel and flashes every message twice — once as the app’s toast, once as Adonia’s. The installer applies the withAdoniaLayout guard on a fresh app (§3 step 6); this check covers apps installed before it, apps whose entry the installer could not recognize, and apps that grew a layout afterwards. An entry shape the analysis cannot model warns rather than fails, and always prints the exact edit.

authorization-fallback understands both fallback: 'allow' written literally and fallback: env.get('KEY', …) resolved against .env.production / .env.production.local.

unscoped-resources is a static preview of the boot error E_ADONIA_UNSCOPED_RESOURCE (§5.3 fails closed). Tenancy is detected by a panel builder calling .tenant()/.tenantParam(); the manifest’s panel → resource mapping narrows the blame to resources that tenant panel registers, falling back to every resource module when the manifest cannot answer.

Adding a check

One module per check under src/doctor/, exporting a DoctorCheck:

import type { DoctorCheck } from '@adonia/devtools/doctor'

export const myCheck: DoctorCheck = {
  id: 'my-check',
  title: 'Human-readable heading',
  async run(context) {
    const config = await context.parse('config/adonia.ts')
    return config ? { status: 'ok', message: '…' } : { status: 'warn', message: '…', fix: '…' }
  },
}

Then add it to DOCTOR_CHECKS in src/doctor/registry.ts, and add a fixture app state under tests/fixtures/doctor/<check-id>/{pass,fail}/ holding only the files the check reads — which is also the proof it reads nothing else. A check that throws is reported as a failing finding naming itself; it never crashes the command.

DoctorContext gives you the app root, the parsed adonisrc.ts, the §14 manifest (plus manifestError when the file is present but unusable), and memoized read/parse/list/ newest/mtime accessors. Static-analysis helpers for config literals — defaultExportObject, propertyAt, stringValue, booleanValue, arrayElements, envReference — live in src/doctor/source.ts and return undefined for anything computed, which the caller reports as a warn rather than a guess.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close