@siriwatknp/mui-cli
v0.3.0
Published
Agent-facing CLI for theming Material UI components — knows every themable seam and outcome model, emits correct styleOverrides fragments
Maintainers
Readme
@siriwatknp/mui-cli
Theme Material UI components without doc-searching. The mui CLI knows every themable seam (component × slot × variant × CSS property) and every outcome model (how a Button's height is actually built: line-height + padding lever + min-height floor + borders), so it emits the exact styleOverrides fragment that lands your target — including the coupled writes and floor adjustments that hand-written overrides silently miss.
Built for coding agents first: predictable commands, copy-paste ids, --json envelopes everywhere. Humans welcome.
Install
Install as a devDependency inside your MUI project (peers resolve from your project):
npm i -D @siriwatknp/mui-cli
npx mui --helpRequirements per feature:
| Feature | Needs |
| --- | --- |
| component, search, theme (solve + fragment), demos | Node ≥ 20 only |
| --theme <file> (solve against YOUR theme) | @mui/material in your project; typescript v5/v6 for .ts theme files |
| compose, --js | typescript v5/v6 (v7's native compiler drops the JS API these features use) |
| --measure (render + verify) | vite, @vitejs/plugin-react, react, react-dom, @emotion/react, @emotion/styled, playwright-core + Google Chrome installed |
A missing package prints the exact install command — nothing fails with a stack trace.
Agent setup
npx mui install --skill # writes .claude/skills/mui-cli/ (Claude Code)
npx mui doctor # per-feature readiness with exact fixesinstall --skill generates an agent skill from the installed CLI (version-stamped — regenerate after upgrading). doctor checks each feature (solve / --theme / compose / --measure) and prints the exact install command for anything missing.
The 30-second tour
# 1. What can I theme on a component?
mui component Switch
# 2. Make it match a design (multiple outcomes merge into ONE fragment)
mui theme Switch --props size=small width=36 height=20 padding=0
# 3. Verify by rendering (needs Chrome)
mui theme Switch --props size=small width=36 height=20 padding=0 --measure
# 4. Solve against YOUR theme, not vanilla defaults
mui theme Button height=36 --theme ./src/theme.ts
# 5. Grab a runnable docs demo, or compose several into one file
mui component Switch --list-demos
mui compose Switch:BasicSwitches Button:BasicButtons --theme ./src/theme.ts -o demos.tsxEvery command accepts --json for a typed { type, data } envelope.
Commands
component [Name] — discover
mui component— all components with their seam counts and available outcome nouns.mui component Switch— the catalog for one component: outcomes first (each with a runnable command), then its composition tree (what it extends / is part of / where its content font comes from), then every seam grouped by slot. Hidden derived seams are collapsed (--fullshows them).- Wrapper components work too:
mui component TextFieldshows the parts to theme (OutlinedInput, InputLabel, …).
Outcome lines are honest: an outcome that can't work prints REFUSED — <reason> instead of failing later. A note like "content follows theme.typography.button — pass --theme so the solve reads yours" tells you when your typography matters.
theme <Component> [outcome=px ...] — resolve
The core verb. You state outcomes; the CLI solves them against measured component state and emits one correct fragment to spread into createTheme():
mui theme Button height=36 # bare = default size, every variant
mui theme Button height=36 --props size=small # narrow the variant
mui theme Tooltip height=28 paddingInline=10 # merged: ONE fragment, no conflicts
mui theme Tooltip --slot arrow size=12 # slot-addressed outcomes
mui theme Switch --props size=small touch=24 # a11y tap box, visual untouchedWhat makes the output trustworthy:
- The math is printed. Every run shows the model (
height = max(minHeight, lineHeight + 2·paddingBlock + borders)), the current state, and each edit with its reason. - Traps are handled. min-height floors are lowered only when they block (a padding-only edit that silently does nothing is the classic hand-written failure); asymmetric label reserves are preserved; borders join the math; coupled writes (label margins, connector offsets) ride along annotated.
- Refusals are information. "content is 24px — taller than the 20px target" means the font is the blocker; shrinking it is its own decision, never a silent emission.
- Input-family baseline reset. Every input-family height solve (OutlinedInput, FilledInput, Input, Autocomplete) also emits
height: 'auto'on the InputBase input andlineHeight: theme.typography.body1.lineHeighton the InputBase root AND FormLabel root (label metrics match the input line), so field content follows your typography instead of MUI's frozen1.4375emline. The solve accounts for it (content = the body1 line box), and unasked sizes shift by exactly the line-box delta (vanilla: +1px).
Raw seam edits — the full flow
When no outcome covers what you need, edit a seam directly. The id is never guessed — it comes from the catalog:
mui component Accordion
# Accordion · expanded margin
# id: MuiAccordion|root|base|&.Mui-expanded|margin ← copy this
mui theme Accordion --set 'MuiAccordion|root|base|&.Mui-expanded|margin=0px'--set values are verbatim CSS, applied after outcomes (adjustment wins). Hidden seams refuse with the driver id to set instead.
--theme <file> — solve against your theme
mui theme Button height=36 --theme ./src/theme.tsInstant (no browser): the file is imported in-process, resolved with createTheme, and the solve reads your values — typography-driven content sizes, overridden paddings/floors/borders, defaultProps, even function styleOverrides (executed with a synthesized ownerState). Rules:
- Inline JSON works for quick probes —
--theme '{"typography":{"body1":{"fontSize":14}}}'(strict JSON; file paths for anything real). - Default-export either theme options or a
createTheme(...)result — both are detected and handled. .ts/.tsxfiles are transpiled in-process. A file with no imports (a plain options object) works from anywhere; a file that imports (e.g. it exportscreateTheme(...)) resolves those imports by walking up from its own directory — so it must sit somewhere anode_moduleswith@mui/materialis reachable, like any real project file.- Anything statically unknowable (
lineHeight: 'normal', opaque runtime values) is flagged in the rationale with "verify with--measure" — never silently guessed.
--measure — prove it
mui theme Button height=36 --theme ./src/theme.ts --measureRenders baseline + fragment in headless Chrome (under your theme when --theme is given) and reports measured after applying: 36px ✓ per variant. This is the ground truth; exit code is non-zero on a miss.
--compare — see it
mui theme Button height=36 --theme ./new-theme.ts --compare # before = MUI baseline
mui theme Button height=36 --theme ./new-theme.ts --compare ./old.ts # before = old themeOne PNG, two panels: before (baseline or the given theme/inline JSON) and after (your theme + the solved fragment), red height rulers per variant, captions with the measured px (before — 56px / after — 36px ✓). Implies --measure — the verdict still prints and still gates the exit code. Output defaults to mui-compare-<Component>.png (--shot <path> overrides).
Docs demos — --list-demos, --demo, compose
Real runnable usage, synced from the Material UI docs (380 demos across 31 pages):
mui component Button --list-demos # DEMO / SECTION / DESCRIPTION table
mui component Switch --demo ControlledSwitches # prints the .tsx source
mui component Switch --demo ControlledSwitches --js # types stripped, JSX kept
mui component Switch --demo ControlledSwitches --copy # → system clipboard insteadDemo names are the DEMO column verbatim (case-sensitive PascalCase); a miss prints the valid candidates. --copy writes the system clipboard (pbcopy on macOS, clip on Windows, wl-copy/xclip on Linux) and prints a one-line summary instead of the source.
compose merges several demos into ONE runnable file — imports deduped, name collisions auto-suffixed (label → label2, usages rewritten safely via the TypeScript AST), demo default-exports renamed and stacked:
mui compose Switch:BasicSwitches Checkbox:Checkboxes Tabs:BasicTabs \
--theme ./src/theme.ts -o composed-demos.tsx--themewraps everything in<ThemeProvider>—createTheme(options)for an options file, the theme as-is for acreateTheme(...)export; imported relative to-o.- A needs note (
needs: @mui/icons-material/...) prints ONLY when the composition imports packages beyondreact+@mui/material— no note means the file runs on@mui/materialalone. Install the listed packages where the file will run; compose reports, it does not install. - Data files ride along. Demos that import docs data (
./top100Films) pull those files in automatically: with-othey are written next to the output (emitted: top100Films.ts (data file of ComboBox)), including transitive imports (server.ts→movies.ts);--jstranspiles them too. Without-o, adata file needed:note points at-oor--json(whosedata.assetscarries the sources).unresolved: ./xmeans the file was never captured at sync time — the output will not run as-is. - Rename notes (
renamed: label → label2 …) print for every collision fix. With-o/--copyall notes go to stdout after the summary; when printing the source to stdout, notes go to stderr so pipes stay clean. --jsand--copywork here too.
MUI X demos — one resolver, three ways in
The same demo corpus for the four X products (1,168 demos across 158 features). X docs are organized by feature, not component, and X has no themable seams — so mui component <X> shows the product's feature map, and demos live one level down via mui demos.
mui component DataGrid # feature map (FEATURE / PLAN / DEMOS) — X has no seams
mui component DataGrid --list-demos # ALL of the product's demos (grouped by FEATURE)
mui demos data-grid filtering # one feature's demos (DEMO / PLAN / SECTION)
mui demos data-grid filtering QuickFilteringGrid --js # demo name = trailing positional
mui demos tree-view rich-tree-view/items # nested features address with a slash- Any export name is an entry point —
DataGrid,DataGridPro,DataGridPremium,BarChart,DatePicker,RichTreeView… all resolve to their product, matching the package you import. - A PLAN column (
community/pro/premium) is read from each demo's import package — a feature spanning tiers lists exactly the ones present (community,pro). Display-only: nothing is gated; the demo's own import line is the source of truth. composetakes X picks asproduct/feature:Demo:mui compose data-grid/filtering:QuickFilteringGrid,ServerFilterGrid -o Grid.tsx. Imports dedupe, theneeds:note lists the X packages, and data files emit next to-o— identical mechanics to core demos.- Licensing: MUI X is open-core. Community demos are MIT; Pro/Premium demos are example code for commercial packages and require a commercial MUI X license to run. This CLI ships the example source only — the PLAN column tells you which tier a demo needs.
Aliases & the demos shortcut
Every entry point runs through one resolver, so a token can be a canonical export, a friendly alias, or an X product:
mui tag # alias → `mui component Chip`
mui input # alias → TextField (also: icon→SvgIcon, dropdown→Select)
mui demos Button # shortcut → `mui component Button --list-demos`
mui data-grid # bare product → the DataGrid feature mapmui <alias> is shorthand for mui component <alias>. Unrecognized tokens fall through to "unknown command", so typos never silently resolve.
search <query> — find seams across components
mui search "button padding"Reading the output
A fragment always prints ready to paste:
// Spread into your theme: createTheme({ ...,
components: {
MuiButton: { styleOverrides: { root: { variants: [ … ] } } },
},
// })Values are raw CSS. Rows the solver derived (identity fills, coupled writes) are marked derived in --json and yield to your explicit asks when invocations merge.
Versioning honesty
The bundled component-state data is measured against @mui/material 9.2.0 (the component/theme output states this). Solves without --theme reflect 9.x vanilla defaults; pass --theme (and --measure for ground truth) to solve against your project's reality.
License
MIT. The bundled docs-demo corpus (data/demos/) is derived from the Material UI documentation (MIT © Call-Em-All and MUI contributors). Not an official MUI product.
