Skip to main content
WireKit
Copy for LLM

wirekit:doctor

A diagnostic command — no side effects unless you ask for one with --fix — that walks your installation and reports any drift between WireKit's expected setup and what your project actually has on disk. Run it after a fresh composer require pushery/wirekit, after every composer update, or whenever components look unstyled / non-interactive.

php artisan wirekit:doctor

wirekit:doctor is an alias of wirekit:verify — same checks, friendlier name. Both commands are idempotent, and both are read-only unless you pass --fix, which is the one flag that writes: it runs vendor:publish --tag=wirekit-assets --force when a published asset is missing and then re-checks.

Flags

Flag What it does
--fix Self-heal missing public/vendor/wirekit/*.{css,js} assets by publishing them, then re-run the asset check. The one flag that modifies your project. Useful right after a fresh clone, where public/vendor/wirekit/ is gitignored and starts empty.
--tier=package|environment Run only one tier of checks. package covers the WireKit install itself, environment covers Laravel host state. Omitted, both run. An unknown value exits 1.
--fail-on=error|warning|none Which severity makes the command exit non-zero. Defaults to error, so only a FAIL gates. warning is the CI setting — config drift and a stale published asset then stop the pipeline. none reports and always exits 0. An unknown value exits 1, with a did-you-mean hint — the same rule as --tier.

The full flag prose, including why a standing reminder is reported as info rather than as a warning, is in the CLI reference.

What it checks

The doctor walks every check in sequence. The sections below group them by what they are about; the CLI reference lists the same checks in the order the command runs them, split by tier.

1 · Asset publishing & freshness

Check Pass criterion
wirekit.min.css published File exists at public/vendor/wirekit/wirekit.min.css — the minified build is the one the directives serve, so it is the one the check looks for
wirekit.js published File exists at public/vendor/wirekit/wirekit.js
Asset freshness md5 hash of published copy matches the package source — catches forgotten vendor:publish --force after composer update
Font assets published When a custom font preset is configured: the font CSS is published, the published copies match the bundled release, and wirekit.fonts.display names a real font-display value
Built CSS contains WireKit utility rules Tailwind @source directive is correctly picking up WireKit blade templates and emitting matching utility classes. Read from the Vite manifest, so it is skipped before your first build
AI manifests not stale .boost/wirekit.json and .wirekit-schema.json are not older than the installed package — a stale one shows an AI tool an API surface that has moved. Skipped when neither has been generated

2 · Integration wiring

Check Pass criterion
Tailwind version tailwindcss resolves to v4 or newer. A pre-v4 install FAILS: WireKit is built on the v4 engine (@theme, @source, color-mix(), @property) and cannot run on v3
Tailwind @source directive resources/css/app.css includes @source '../../vendor/pushery/wirekit/resources/views/**/*.blade.php';. A commented-out line, or an @source not … exclusion, does not count
config/wirekit.php published Config file exists in your config/ directory
Config drift The published config neither predates options this version offers nor carries keys it no longer has. Both directions are warnings — a stale config still resolves
Blade directives present @wirekitStyles and @wirekitScripts each appear in at least one Blade file, and @wirekitScripts comes before @livewireScripts wherever the two meet. A mention inside a comment does not count. Whether they sit in the files your pages actually render is the next row
Page shells load WireKit Every file that closes the <head> or the <body> of a page your app and auth layouts render carries the directive, directly or through a file it includes. A layout that hands the page to another file, as the Livewire starter kit's does, is followed to that file. A warning rather than a failure, because a directive pushed through a stack or a section is invisible to it
Alpine.js available Alpine reaches the page. Passes without further work on Livewire v4+, which bundles it
Bundle config config('wirekit.scripts.bundle') is 'full', 'core' or 'csp'. csp is the Alpine build for a Content-Security-Policy that forbids unsafe-eval — a correct choice, not an invalid one
CSS @import setup path PASSES if wirekit.css is @import-ed in app.css. That is a valid alternative to @wirekitStyles, not an anti-pattern — which is why the @wirekitStyles check above only fails when neither path is present
Published views not stale Warns when views/vendor/wirekit/ exists, with the file count, because every file in it is frozen at the version you published it from
Personalizations extend rather than replace Every WireKit::personalize() block whose value is a finished class string is named. A replacement is valid, but it also ends the flow of later WireKit changes to that block, silently and permanently
Icon-preset packages installed Every preset named in wirekit.icons.presets has its composer package. A configured-but-missing preset resolves an alias onto a glyph that is not there, and that throws when the page renders
Translation keys do not collide No key means one thing in your catalog and another in WireKit while a WireKit component renders it. The warning names the component and the namespaced key to translate instead
Optional dependencies Chart.js adapter, bacon/bacon-qr-code, and the editor / map front-end peers — each reported as info when absent, never as a problem. With the ApexCharts adapter selected this expands into the npm package, the window.ApexCharts assignment, the published wirekit-apex.js, the major version, and the license tier. ApexCharts is non-MIT: see the License section on the Chart overview and apexcharts.com/license for the terms the tier refers to
Chart usage without an adapter <x-wirekit-chart> appears in your views while charts.library is null — the component renders and draws nothing
Chart.js registration With the Chart.js adapter selected, resources/js/app.js registers the controllers. Skipped when Chart is provided as the self-hosted UMD build, which registers its own

3 · Token alignment

Compares your Tailwind-side CSS custom properties against the matching WireKit-side tokens and reports every pair whose values disagree. Example: a --font-sans: 'Inter' declaration in your app.css against WireKit's --font-wk-sans: 'Instrument Sans' is reported as a mismatch. Pairs where either side is a var(...) reference (intentional alias) are skipped.

Token pair What it covers
--font-sans--font-wk-sans Sans serif font family — drives both Tailwind utilities (font-sans) AND WireKit chrome
--font-serif--font-wk-serif Serif font family
--font-mono--font-wk-mono Monospace font family
--color-accent--color-wk-accent Accent color — your brand primary vs. WireKit's primary surface
--color-accent-foreground--color-wk-accent-fg Accent foreground (for contrast against accent)
--radius--radius-wk Border radius scale — keeps Tailwind utilities visually consistent with WireKit components
--shadow--shadow-wk Box-shadow scale

For each pair the doctor emits one of three states:

  • ✓ aligned — the two tokens resolve to the same value (after normalization).
  • ! mismatch — the values differ. Output includes both literal values plus an actionable fix hint.
  • i skippedeither side is a var(...) reference (intentional alias), or a token is unset on one side or on both. Skipped checks don't count as warnings, and the line names the token that was missing rather than the side.

4 · Light/dark token symmetry

Compares the --color-wk-* tokens declared in your :root {} block against those in your .dark {} block. Missing entries on either side mean the corresponding component will inherit a stale value when the theme flips — your end-users see this as "looks wrong in dark mode" without an obvious cause.

Check Pass criterion
:root vs .dark color-token symmetry Every --color-wk-* declared in one block is also declared in the other

Output shapes:

  • — both blocks declare the same set of --color-wk-* tokens (or neither block exists; the check stays silent).
  • ! — at least one token is asymmetric. The warning lists the missing token names per block.

The check is scoped to color tokens only. Font, radius, and shadow tokens are intentionally theme-agnostic (declared once in :root, never repeated in .dark), so the doctor doesn't flag those.

5 · Token overrides in a wrapper that cannot win

Asks the question before the one above: whether an override reaches the page at all. Two wrappers look right, do nothing, and raise no error either way — so the only symptom is a token that keeps its shipped value in both modes.

Check Pass criterion
No --*-wk-* token in a wrapper that cannot win No --<family>-wk-<name> declaration sits inside @theme {}, and none sits inside any @layer on a rule that targets the root

@theme {} is Tailwind's utility-generation block: a theme variable no utility references is dropped, and one that survives is emitted inside @layer theme. Any @layer loses for that reason alone, because wirekit.css ships unlayered and unlayered CSS beats layered CSS whatever the specificity. A token scoped to a component is left alone whether or not it sits in a layer — the cascade compares declarations on the same element, and a custom property on a descendant shadows the inherited one. Theming → How It Works compares all three wrappers side by side.

6 · Alpine plugin cleanup hygiene

Static-analysis scan of your resources/js/ tree for custom Alpine plugins. Flags two anti-patterns that commonly produce silent TypeError console errors after Livewire morph / conditional render / SPA navigation tears down a host element while a browser-queued observer callback is still in flight.

Anti-pattern What the doctor looks for
Observer without destroy() An observer (new IntersectionObserver(...) / new MutationObserver(...) / new ResizeObserver(...)) built per component instance — inside an Alpine.data(...) factory or an init() — in a file with no destroy() lifecycle method. Without destroy(), each instance holds its host-element reference forever and prevents garbage collection. A module-level observer is deliberately exempt: one MutationObserver on <html> inside a top-level IIFE lives as long as the document does and has nothing to tear down, so giving it a destroy() would be the defect rather than the fix.
Unguarded disconnect() A .disconnect() call that nothing guards at its own call site. Judged per call site, not per file: a guard six lines up with a branch in between does not protect this call. Three shapes count as guarded, and all three are accepted — this.observer?.disconnect() on the line itself, if (! this.observer) return; just above, and the positive if (this.observer) { … } wrapping the call. The field may be spelled with or without a leading underscore; an Alpine factory's own state ordinarily has none. Comments do not count as code in any of this: a sentence naming .disconnect(), destroy() or an observer constructor is blanked out before the scan, so prose about the guard neither raises a finding nor hides one. Without one, a callback queued before destroy() throws TypeError: Cannot read properties of null (reading 'disconnect'). The same module-level exemption applies: an observer built once at the top level of a file is never set to null and has no destroy() to race, so its disconnect() needs no guard.

Output shapes:

  • — no anti-patterns detected, OR resources/js/ doesn't exist (developers without custom plugins skip the check entirely).
  • ! — one or more files match a heuristic. The warning lists each file + the specific anti-pattern(s) detected.

Opt out for a file that intentionally uses a pattern the heuristic doesn't recognize (for example, the observer reference is captured in a closure-local variable rather than on this): add this comment marker anywhere in the file:

// wirekit-doctor: cleanup-ok

The doctor will skip that file. Use sparingly — most intentional exemptions in practice turn out to be bugs on closer inspection. See Authoring Custom Alpine Plugins for the canonical defensive-cleanup pattern WireKit's own plugins follow.

7 · Laravel host environment

Everything above is about the WireKit install. These three are about the app around it — they bite during interactive development and in CI even when the package install is spotless, which is why they have their own tier and can be run alone with --tier=environment.

Check Pass criterion
Compiled-views freshness resources/views/ mtimes do not exceed storage/framework/views/ by 60 seconds or more. This is the "I edited the Blade source an hour ago and the test still asserts the old output" failure, and the fix it prints is php artisan view:clear. The 60-second buffer keeps a fast edit cycle from tripping it; a slow filesystem may need more
Installed package matches composer.lock The WireKit in vendor/ is the commit the lockfile names. A path repository is reported as info rather than a warning, because that is a deliberate setup and not drift
Silent prop-validation typos No WireKit [...] ERROR/WARNING lines in storage/logs/laravel*.log — the lines a prop value writes when it fails validation and degrades silently at render time instead of throwing. Reported as info, never as a problem, when the scan cannot run: a non-file log channel, a missing storage/logs, or a transient read error. A gate on your logging setup would be a gate on the wrong thing

Example output

WireKit Integration Check

  ✓ Tailwind CSS v4 (v4+ required)
  ✓ wirekit.min.css published
  ✓ wirekit.js published
  ✓ wirekit.css is up to date
  ✓ wirekit.min.css is up to date
  ✓ wirekit.js is up to date
  ✓ Tailwind @source includes WireKit templates
  ✓ config/wirekit.php published
  ✓ @wirekitStyles directive found
  ✓ @wirekitScripts directive found
  ✓ @wirekitScripts is before @livewireScripts (or no explicit @livewireScripts)
  ✓ Alpine.js provided by Livewire v4+
  ✓ JS bundle configured: full
  ✓ Font assets published (21 font CSS files)
  ! Serif font: mismatch — Tailwind `'Lora', serif` vs WireKit `'Playfair Display', serif`
  Fix: php artisan wirekit:install --font-serif=<key>
  i bacon/bacon-qr-code not installed (optional — only needed for <x-wirekit::qr-code>)

  14 passed, 1 warnings, 0 failed

The glyphs are pass, ! warning, failure and i information. The totals line counts the first three, and an i line is counted in none of them — so the three numbers can add up to fewer entries than the report shows, and on a fresh install they usually do. An i reports something optional that is simply not installed, or a check that had nothing to measure ("skipped", "NOT measured"); neither is a pass, and neither is a problem to fix. What tells you the run is healthy is 0 failed, not the size of the passed count.

Common failures and fixes

Each subsection below quotes the exact doctor line that triggered it (in a code block) followed by what it means and how to fix it. The glyphs are the command's own: a pass, ! a warning, a failure, i informational.

Sans font mismatch

  ! Sans font: mismatch — Tailwind `Inter` vs WireKit `Roboto`. Fix: php artisan wirekit:install --font=<key>

Your font-sans Tailwind utilities will render Inter while WireKit components render Roboto. Pick one and run:

php artisan wirekit:install --font=<key>

(see wirekit:install flag table for available keys)

This writes BOTH --font-sans AND --font-wk-sans to the same family in your app.css — guaranteed alignment.

Accent color mismatch

  ! Accent color: mismatch — Tailwind `oklch(60% 0.2 250)` vs WireKit `oklch(50% 0.3 30)`. Fix: set --color-accent in @theme to match WireKit accent

Tailwind's bg-accent will render blue while <x-wirekit::button> renders red. Either:

  • Update your @theme { --color-accent: ... } block to match WireKit, or
  • Override --color-wk-accent in :root to match your brand

The token alignment doctor doesn't auto-fix color tokens (unlike fonts) because brand identity is too project-specific to template.

Sans font skipped (token unset)

    i Sans font: skipped (--font-sans unset)

You haven't set --font-sans (or --font-wk-sans) — likely you're using the bundled WireKit defaults on both sides. No action required; the skip is informational.

A token override in a wrapper that cannot win

  ! Token wrappers: 2 `--*-wk-*` override(s) in a wrapper that cannot win
    • --color-wk-accent in @theme
    • --color-wk-bg-elevated in @layer … :root
    These compile to nothing, or to a layered declaration that loses to WireKit's
    unlayered default — in BOTH modes, with no error either way.
    Move them to a plain `:root { … }` block, with a plain `.dark { … }` beside it.

You wrote a WireKit token override somewhere it never reaches the page. This is the quietest failure the doctor reports: the page renders, nothing errors, and the token simply keeps its shipped value — so the only symptom is a color that looks like you never changed it.

@theme {} is Tailwind's utility-generation block, not an override route. A theme variable that no utility references is dropped, so the declaration compiles to nothing; one that survives is emitted inside @layer theme, which loses to the unlayered defaults wirekit.css ships. Any other @layer loses for that second reason on its own. Theming → How It Works compares all three wrappers side by side.

Only a rule that targets the root is reported. Setting a token on a component — .my-widget { --size-wk-fab: … } — works whether or not it sits in a layer, because the cascade compares declarations that apply to the same element and a custom property on a descendant shadows the inherited one. That is a supported pattern and the check leaves it alone.

Light/dark color-token asymmetry

  ! Token symmetry: 3 color token(s) overridden in `:root` but not in `.dark`
    • --color-wk-bg-elevated
    • --color-wk-border
    Your `:root` value keeps applying in dark mode — it outranks the `:where(.dark)`
    WireKit declares its own dark value with, so the token never turns over.
    Add matching declarations to your `.dark { … }` block.
    • --color-wk-rail-active-bg
    These resolve at `:root`, where they are declared. On `<html class="dark">` that is
    the same element, so they follow what they point at — but a `.dark` further down
    the tree inherits the value already resolved in light.
    Repeat the same `var()` references under `.dark { … }` so a subtree resolves them too.

Your :root {} declares color tokens that aren't repeated in .dark {}, and the reason it matters depends on what you wrote there — so the report gives the tokens in two groups.

A literal value keeps applying when the theme flips. Every WireKit color default is declared under :where(:root) and :where(.dark), both at specificity zero, so that your own :root {} wins regardless of source order — which also means WireKit's dark value never gets its turn. Developers see this as "the dark variant of this card looks wrong" without an obvious source.

A value that goes through var() is resolved on the element that declares it, so a token you derive in :root follows what it points at only where .dark sits on that same element — the usual <html class="dark">. A .dark further down the tree, a dark panel on a light page, inherits the value that was already resolved in light. Repeating the same references inside .dark {} is what makes the subtree resolve them too.

Every WireKit-shipped color token has both a light and a dark value; the rule of thumb is one declaration per block per token.

resources/css/app.css not found

  !   resources/css/app.css not found — skipping token-alignment checks

You're running the doctor from a fresh checkout that hasn't run wirekit:install yet, OR your CSS lives at a non-standard path. Run wirekit:install first, or set up app.css manually per the Integration guide.

CI integration

wirekit:doctor exits with status code 1 when any check fails. Warnings are reported but do not fail the build by default, which is what --fail-on is for: pass warning and a WARN finding is enough to stop the pipeline, or none to report and always exit 0.

# .github/workflows/test.yml
- name: WireKit integration check
  # 1. --fail-on=warning turns config drift and a stale published asset — both
  #    of which the doctor reports as warnings — into a red build. Drop the flag
  #    to keep the default, where only a hard FAIL stops the pipeline.
  run: php artisan wirekit:doctor --fail-on=warning

Or as a Claude Code stop-hook:

// .claude/settings.json
{
  "hooks": {
    "Stop": [
      { "type": "command", "command": "php artisan wirekit:doctor" }
    ]
  }
}

Aliases and history

  • wirekit:verify is the canonical name — it is the command's registered signature.
  • wirekit:doctor is the alias, and it is the friendlier one, which is why this page is written around it. Same code path, identical output.
  • php artisan list wirekit therefore prints both: wirekit:doctor as its own row, and wirekit:verify with [wirekit:doctor] in front of the description. Two rows, one command.

See Also

  • wirekit:install — initial setup; provides the --font* flags that the doctor's token-alignment checks reference.
  • Integration guide — full setup from composer require through asset wiring.
  • Design Tokens — design-token reference (helpful when interpreting mismatch hints).

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store