CLI
Use the Visor CLI to add components, hooks, and utilities to your project.
Installation
The CLI is distributed as @loworbitstudio/visor-core. No global install required — use npx to run commands directly.
Commands
visor init
Initialize Visor in your project. Creates a visor.json config file with default path mappings.
npx @loworbitstudio/visor initOptions
| Flag | Description |
|---|---|
--template nextjs | Scaffold a complete runnable Borealis-native Next.js app in one command |
--json | Emit structured JSON for AI agents — success, files.created, nextSteps |
Template scaffolding (one-command flow):
In an empty directory:
npx @loworbitstudio/visor init --template nextjsThis shells out to create-next-app with pinned flags, then layers Visor on top to produce a runnable app:
| File | Source | Purpose |
|---|---|---|
package.json | create-next-app + visor deps | next, react, TypeScript, plus @loworbitstudio/visor-core and @loworbitstudio/visor-theme-engine |
app/page.tsx | create-next-app | Default page |
app/layout.tsx | Visor (overwrites default) | Imports globals.css and injects FOWT_SCRIPT inline in <head> |
app/globals.css | Visor adapter | CSS custom properties generated from .visor.yaml |
.visor.yaml | Visor starter | Editable theme — colors, typography, radius, shadows |
tsconfig.json, next.config.ts, .gitignore | create-next-app | Standard scaffolding |
.lo/borealis.json | Visor | Stamp recording the Visor version that initialized the project |
visor.json | Visor | CLI path configuration |
After it finishes:
cd my-app && npm run devThe dev server starts immediately — no FOWT flash on hard reload, no missing tokens, no extra setup steps.
--template nextjsrefuses ifpackage.jsonalready exists in the target directory. To retrofit an existing app, see the Migration Guide.
Default config:
{
"paths": {
"components": "components/ui",
"hooks": "hooks",
"lib": "lib"
}
}Edit visor.json to change where files are placed. For example, a Next.js App Router project might use:
{
"paths": {
"components": "src/components/ui",
"hooks": "src/hooks",
"lib": "src/lib"
}
}visor add
Add one or more components, hooks, or utilities to your project.
npx @loworbitstudio/visor add button
npx @loworbitstudio/visor add button input card
npx @loworbitstudio/visor add use-debounceThe CLI resolves transitive registry dependencies automatically. For example, visor add button also adds the utils library since Button depends on it.
npm dependencies (like @radix-ui/react-dialog or class-variance-authority) are installed automatically.
If visor.json doesn't exist, visor add creates one automatically with default paths. You can skip visor init and go straight to adding components.
Options
| Flag | Description |
|---|---|
--overwrite | Overwrite existing files. Without this flag, existing files are skipped. |
visor list
List all available registry items, grouped by type (Components, Hooks, Utilities). Items already installed in your project are marked.
npx @loworbitstudio/visor listvisor diff
Show unified diffs between your local files and the registry versions. Useful for seeing what's changed upstream since you installed a component.
npx @loworbitstudio/visor diff # Diff all installed items
npx @loworbitstudio/visor diff button # Diff a specific componentvisor suggest
Suggest components, blocks, patterns, and hooks for a natural language use case. Scores all manifest entries by keyword overlap and returns the top 10 ranked results with install commands. Designed for AI agent workflows.
npx @loworbitstudio/visor suggest --for "dropdown with search"
npx @loworbitstudio/visor suggest --for "user login form" --json
npx @loworbitstudio/visor suggest --for "dashboard with sidebar" --jsonOptions
| Flag | Description |
|---|---|
--for <useCase> | (Required) Natural language description of what you want to build |
--json | Output structured JSON (for AI agents) |
JSON output shape
{
"success": true,
"query": "dropdown with search",
"results": [
{
"name": "combobox",
"type": "component",
"category": "form",
"score": 2,
"description": "A combobox/autocomplete component with filterable dropdown...",
"match_reason": "Matched: dropdown, search",
"install_command": "npx visor add combobox"
}
],
"summary": {
"total_searched": 123,
"total_matched": 8
}
}type is one of "component", "block", "pattern", or "hook". install_command is null for patterns (no install; they are reference architectures).
visor theme apply
Generate CSS from a .visor.yaml theme file.
npx @loworbitstudio/visor theme apply .visor.yaml
npx @loworbitstudio/visor theme apply .visor.yaml --adapter nextjs
npx @loworbitstudio/visor theme apply .visor.yaml --adapter nextjs --scope-prefix 'body.my-theme'
npx @loworbitstudio/visor theme apply .visor.yaml --adapter fumadocs
npx @loworbitstudio/visor theme apply .visor.yaml --adapter deckOptions
| Flag | Description |
|---|---|
-o, --output <path> | Custom output file path |
--adapter <name> | Target adapter: nextjs, fumadocs, deck, docs, flutter |
--scope-prefix <selector> | (nextjs) Wrap all rules under a CSS selector (e.g. body.mybrand-theme) so multiple themes can coexist via body-class repaint. Dark-mode block scopes to <selector>.dark. Default: :root. |
--json | Output structured JSON (for AI agents) |
See Adapters for details on each adapter's output.
visor theme register
Register a theme in the Visor docs site in one command. Creates the CSS file, adds the @import to globals.css, and adds the theme entry to theme-config.ts — all in alphabetical order. Idempotent: running it twice produces no changes.
npx @loworbitstudio/visor theme register .visor.yaml --group "Client"
npx @loworbitstudio/visor theme register .visor.yaml --group "Visor" --dry-runOptions
| Flag | Description |
|---|---|
--group <name> | (Required) Theme group to register in: Visor, Client, Low Orbit |
--dry-run | Show what would change without writing files |
--json | Output structured JSON (for AI agents) |
visor theme unregister
Remove a theme from the docs site. Deletes the CSS file, removes the @import from globals.css, and removes the entry from theme-config.ts.
npx @loworbitstudio/visor theme unregister entrOptions
| Flag | Description |
|---|---|
--json | Output structured JSON (for AI agents) |
visor theme validate
Validate a .visor.yaml file against the schema and run WCAG contrast checks.
npx @loworbitstudio/visor theme validate .visor.yamlvisor theme export
Export a minimal .visor.yaml from an existing theme.
npx @loworbitstudio/visor theme export .visor.yamlvisor migrate token-substitution
Mechanically apply the §3.1 V7-primitive → Visor-semantic substitution table across a target directory or file. This is the codemod that makes theme portability automatic — the single highest-leverage step in porting V7-primitive components to Visor-semantic tokens.
Dry-run by default. Use --apply to commit changes to disk. Running twice is a no-op (idempotent).
# Preview proposed changes (dry-run)
npx @loworbitstudio/visor migrate token-substitution components/local/
# Apply changes
npx @loworbitstudio/visor migrate token-substitution components/local/ --apply
# Target a single file
npx @loworbitstudio/visor migrate token-substitution components/local/KpiCard.module.css --apply
# Apply for a specific theme (default: entr)
npx @loworbitstudio/visor migrate token-substitution --theme-id entr --apply
# Machine-readable output for AI agents / CI
npx @loworbitstudio/visor migrate token-substitution components/ --jsonOptions
| Flag | Description |
|---|---|
[path] | Path to a file or directory to migrate (default: current directory) |
--theme-id <id> | Theme whose substitution map to apply (default: entr) |
--dry-run | Preview proposed changes without writing files (this is the default when --apply is omitted) |
--apply | Write changes to disk |
--json | Output structured JSON (for AI agents) |
The §3.1 substitution table (V7 ENTR → Visor semantic)
This table encodes the single highest-leverage round-2 refactor from the Borealis prototype-to-Visor exercise. Applying it at component-build time makes theme swaps produce visibly distinct, structurally correct repaints with zero shim CSS.
| V7 primitive | Visor semantic | Notes |
|---|---|---|
--panel | --surface-card | Primary surface tier |
--panel-2 | --surface-interactive-default | Interactive resting state; use --surface-subtle for decorative areas |
--panel-3 | --surface-interactive-active | Pressed/active state; use --surface-interactive-hover for hover |
--text | --text-primary | Primary text |
--text-2 | --text-secondary | Secondary/muted text |
--text-3 | --text-tertiary | Tertiary/dimmed text |
--text-4 | --text-tertiary | No dedicated Visor equivalent; maps to tertiary |
--mint | --accent-primary | Brand accent (fills, icons); for text use --text-success |
--mint-soft | --surface-accent-subtle | Accent tint; use --surface-selected for selected-row highlights |
--warn | --text-warning | Warning text and icons |
--warn-soft | --surface-warning-subtle | Warning background tint |
Brand-local tokens excluded (intentional): --screen (deepest surface tier), --font-marquee, and the discrete type scale (--text-11 through --text-72) have no Visor semantic equivalent and must remain brand-local.
Adding a theme's substitution map to its .visor.yaml
Themes can declare their own substitution table in the migrate section of their .visor.yaml. The CLI reads from this file first (if found), then falls back to the built-in registry:
name: my-theme
version: 1
colors:
primary: "#10B981"
# ... other theme fields ...
migrate:
token-substitution:
--brand-surface: --surface-card
--brand-text: --text-primary
--brand-accent: --accent-primaryJSON output shape
{
"success": true,
"dryRun": true,
"themeId": "entr",
"targetPath": "/path/to/components",
"filesScanned": 10,
"filesChanged": 3,
"totalSubstitutions": 12,
"files": [
{
"file": "/path/to/components/KpiCard.module.css",
"substitutions": [
{
"line": 2,
"column": 14,
"from": "--panel",
"to": "--surface-card",
"originalLine": " background: var(--panel);",
"replacedLine": " background: var(--surface-card);"
}
],
"newContent": "..."
}
]
}visor sandbox
Scaffold a Next.js app for in-vivo primitive iteration — populated with real Visor primitives (via visor add) plus visible placeholder stubs for primitives not yet shipped. Used by the Low Orbit pattern-build pipeline to iterate on new primitives in their target composition before they're approved and implementation work begins.
The CLI itself doesn't bundle Playwright — the sandbox scaffold declares @playwright/test in its own devDependencies, so sandbox approve runs inside the sandbox and the published @loworbitstudio/visor package stays light.
visor sandbox init <name>
npx @loworbitstudio/visor sandbox init org-mgmt \
--handoff ~/path/to/design-handoff.md \
--theme entrReads a Low Orbit design-handoff.md manifest, scaffolds a Next.js app at .lo/sandbox/<name>/, runs visor add for every shipped primitive declared in the manifest, generates a visible dashed-border stub at components/stubs/<name>.tsx for each gap primitive (each carrying a GAP: VI-<NNN> overlay), applies the named theme, and writes sandbox.json recording the port, primitives, and screens.
| Flag | Description |
|---|---|
--handoff <path> (required) | Path to a Low Orbit design-handoff.md manifest |
--theme <slug-or-path> (required) | Theme slug or path to a .visor.yaml. Resolution order: --theme-file if set → <slug> as a direct path → ${VISOR_THEMES_PRIVATE_PATH}/themes/<slug>/theme.visor.yaml if the env var is set → themes/<slug>.visor.yaml → custom-themes/<slug>.visor.yaml |
--theme-file <path> | Explicit path to a theme.visor.yaml — bypasses name resolution and the VISOR_THEMES_PRIVATE_PATH env var |
--from-html-prototype <path> | Import a Phase 1.5 HTML prototype directory: copies its files into public/prototype/ and pairs each numerically-prefixed screen-N-*.html with the matching manifest screen, rendering it via an iframe in the screen route |
--strip-chrome [selectors] | Strip Phase 1.5 documentary chrome from imported prototype HTML before it lands in public/prototype/. Bare flag uses the default selector list (.state-callout, .state-section__header, .proto-nav, [data-documentary-chrome], [style*="mint"]); a comma-separated list REPLACES the defaults |
--strip-chrome-additional <selectors> | Comma-separated selectors to MERGE with the chosen --strip-chrome base (defaults or replacement). Useful when a pattern has one or two extra chrome variants on top of the standard set |
--overwrite | Replace an existing sandbox at this name |
--skip-install | Skip npm install (test fixture mode) |
--json | Emit structured JSON for AI agents |
Resolving brand themes from a private repo. Set VISOR_THEMES_PRIVATE_PATH to the root of your private themes directory (e.g. ~/Code/low-orbit/visor-themes-private) and --theme <slug> will resolve to ${VISOR_THEMES_PRIVATE_PATH}/themes/<slug>/theme.visor.yaml automatically. For one-off overrides, pass --theme-file <path> to point at any .visor.yaml directly — useful when the theme lives outside the conventional layout.
When --from-html-prototype is set, the sandbox boots with the real Phase 1.5 composition as the baseline — useful for pattern builds whose Phase 1.5 cleared before this CLI shipped. The mapping is order-based: the Nth manifest screen pairs with the Nth screen-*.html file (sorted by numeric prefix). The resolved map is recorded in sandbox.json under fromHtmlPrototype.screenMap.
State-coverage screens are auto-discovered. Any screen-N-*.html files beyond the manifest's named-screen count are appended to the sandbox manifest as state-coverage screens with predictable slugs derived from the filename suffix (e.g., screen-5-menus.html → state-coverage-menus, screen-7-edge-states.html → state-coverage-edge-states). Each gets its own iframe-loading route at /screens/<slug> and a recorded entry in sandbox.json under fromHtmlPrototype.stateCoverageScreens. This powers the Phase 4 state-coverage diff gate (states_prototyped: { menu, feedback, edge }).
Documentary chrome stripping (--strip-chrome) addresses a Phase 1.5 vs Phase 7 mismatch. Phase 1.5 HTML prototypes embed documentary chrome — state callouts, section headers, proto-nav, mint-styled annotation chips — to label states for the design reviewer. The reference build's scaffold never renders those labels, so Phase 7 scaffold-drift checks fail on every screen with documentary chrome unless stripped first. Pass bare --strip-chrome to remove the standard chrome set, --strip-chrome ".a,.b" to swap in a custom list, or --strip-chrome-additional ".x,.y" to merge extras with the base. Supported selector shapes: class selectors (.foo), attribute-presence selectors ([data-foo]), and attribute-substring selectors ([style*="mint"]). The stripper runs over .html files only — sibling CSS/JS assets in the prototype tree are copied byte-for-byte. The resolved selector list is recorded in sandbox.json under fromHtmlPrototype.stripChromeSelectors.
visor sandbox dev
npx @loworbitstudio/visor sandbox dev --name org-mgmtBoots next dev for the sandbox on its allocated port. Auto-allocated ports start at 4060 and never use port 3000 (reserved per the Low Orbit convention). Prints the base URL and one URL per route (/, /primitives/<name>, /screens/<name>) before spawning.
visor sandbox approve
# 1. Capture into pending (auto-diff vs approved if a baseline exists)
npx @loworbitstudio/visor sandbox approve --name org-mgmt
# 2. Review captures/pending/ and captures/diffs/
# 3. Promote pending → approved once the captures look right
npx @loworbitstudio/visor sandbox approve --name org-mgmt --approveSandbox captures use a three-state review flow:
- Default capture — Shells out to a sandbox-local Playwright install and captures full-page screenshots of every route into
captures/pending/<route-slug>.png. Ifcaptures/approved/already contains a baseline, every pending capture is pixel-diffed against it and any differences land incaptures/diffs/. The approved baseline is never touched. - Operator review — Eyeball
captures/pending/andcaptures/diffs/. Re-run capture to iterate until the pending set is correct. - Promotion —
--approvecopiescaptures/pending/→captures/approved/and clears the pending + diffs directories. Captures live in the approved baseline only after a deliberate operator action.
| Flag | Description |
|---|---|
--name <name> (required) | Sandbox name (from a prior sandbox init) |
--approve | Promote captures/pending/ → captures/approved/. Fails with a hint if pending is empty — run the default capture first. |
--diff | Deprecated. No-op; default capture already pixel-diffs against the approved baseline. Kept for backwards compatibility. |
--json | Emit structured JSON for AI agents |
Typical Workflow
# 1. Scaffold a complete runnable Borealis-native Next.js app
npx @loworbitstudio/visor init --template nextjs
# 2. Start the dev server
npm run dev
# 3. Add components
npx @loworbitstudio/visor add button card input
# 4. Customize freely — you own the files
# 5. Later, check for upstream updates
npx @loworbitstudio/visor diff
# 6. Pull updates for specific components
npx @loworbitstudio/visor add button --overwriteKnown Limitations
Turbopack and symlinked packages
Turbopack (used in next dev --turbopack) cannot resolve CSS @import paths
through symlinked node_modules. This affects local file: or link: installs
of @loworbitstudio/visor-core.
Workarounds:
-
Use
--template nextjs(recommended) — the scaffoldedapp/globals.cssis generated inline by the Next.js adapter. No CSS@importof@loworbitstudio/visor-coreis required, so Turbopack's symlink resolver never enters the picture.npx @loworbitstudio/visor init --template nextjs -
Use a relative path import instead of the package name:
/* Instead of: @import '@loworbitstudio/visor-core'; */ @import '../../path-to/visor-core/dist/tokens.css'; -
Use webpack — run
next devwithout--turbopackto use webpack's resolver, which handles symlinked CSS correctly.