VisorVisor

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 init

Options

FlagDescription
--template nextjsScaffold a complete runnable Borealis-native Next.js app in one command
--jsonEmit structured JSON for AI agents — success, files.created, nextSteps

Template scaffolding (one-command flow):

In an empty directory:

npx @loworbitstudio/visor init --template nextjs

This shells out to create-next-app with pinned flags, then layers Visor on top to produce a runnable app:

FileSourcePurpose
package.jsoncreate-next-app + visor depsnext, react, TypeScript, plus @loworbitstudio/visor-core and @loworbitstudio/visor-theme-engine
app/page.tsxcreate-next-appDefault page
app/layout.tsxVisor (overwrites default)Imports globals.css and injects FOWT_SCRIPT inline in <head>
app/globals.cssVisor adapterCSS custom properties generated from .visor.yaml
.visor.yamlVisor starterEditable theme — colors, typography, radius, shadows
tsconfig.json, next.config.ts, .gitignorecreate-next-appStandard scaffolding
.lo/borealis.jsonVisorStamp recording the Visor version that initialized the project
visor.jsonVisorCLI path configuration

After it finishes:

cd my-app && npm run dev

The dev server starts immediately — no FOWT flash on hard reload, no missing tokens, no extra setup steps.

--template nextjs refuses if package.json already 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-debounce

The 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

FlagDescription
--overwriteOverwrite 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 list

visor 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 component

visor 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" --json

Options

FlagDescription
--for <useCase>(Required) Natural language description of what you want to build
--jsonOutput 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 deck

Options

FlagDescription
-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.
--jsonOutput 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-run

Options

FlagDescription
--group <name>(Required) Theme group to register in: Visor, Client, Low Orbit
--dry-runShow what would change without writing files
--jsonOutput 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 entr

Options

FlagDescription
--jsonOutput 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.yaml

visor theme export

Export a minimal .visor.yaml from an existing theme.

npx @loworbitstudio/visor theme export .visor.yaml

visor 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/ --json

Options

FlagDescription
[path]Path to a file or directory to migrate (default: current directory)
--theme-id <id>Theme whose substitution map to apply (default: entr)
--dry-runPreview proposed changes without writing files (this is the default when --apply is omitted)
--applyWrite changes to disk
--jsonOutput 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 primitiveVisor semanticNotes
--panel--surface-cardPrimary surface tier
--panel-2--surface-interactive-defaultInteractive resting state; use --surface-subtle for decorative areas
--panel-3--surface-interactive-activePressed/active state; use --surface-interactive-hover for hover
--text--text-primaryPrimary text
--text-2--text-secondarySecondary/muted text
--text-3--text-tertiaryTertiary/dimmed text
--text-4--text-tertiaryNo dedicated Visor equivalent; maps to tertiary
--mint--accent-primaryBrand accent (fills, icons); for text use --text-success
--mint-soft--surface-accent-subtleAccent tint; use --surface-selected for selected-row highlights
--warn--text-warningWarning text and icons
--warn-soft--surface-warning-subtleWarning 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-primary

JSON 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 entr

Reads 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.

FlagDescription
--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.yamlcustom-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
--overwriteReplace an existing sandbox at this name
--skip-installSkip npm install (test fixture mode)
--jsonEmit 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.htmlstate-coverage-menus, screen-7-edge-states.htmlstate-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-mgmt

Boots 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 --approve

Sandbox captures use a three-state review flow:

  1. Default capture — Shells out to a sandbox-local Playwright install and captures full-page screenshots of every route into captures/pending/<route-slug>.png. If captures/approved/ already contains a baseline, every pending capture is pixel-diffed against it and any differences land in captures/diffs/. The approved baseline is never touched.
  2. Operator review — Eyeball captures/pending/ and captures/diffs/. Re-run capture to iterate until the pending set is correct.
  3. Promotion--approve copies captures/pending/captures/approved/ and clears the pending + diffs directories. Captures live in the approved baseline only after a deliberate operator action.
FlagDescription
--name <name> (required)Sandbox name (from a prior sandbox init)
--approvePromote captures/pending/captures/approved/. Fails with a hint if pending is empty — run the default capture first.
--diffDeprecated. No-op; default capture already pixel-diffs against the approved baseline. Kept for backwards compatibility.
--jsonEmit 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 --overwrite

Known 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:

  1. Use --template nextjs (recommended) — the scaffolded app/globals.css is generated inline by the Next.js adapter. No CSS @import of @loworbitstudio/visor-core is required, so Turbopack's symlink resolver never enters the picture.

    npx @loworbitstudio/visor init --template nextjs
  2. Use a relative path import instead of the package name:

    /* Instead of: @import '@loworbitstudio/visor-core'; */
    @import '../../path-to/visor-core/dist/tokens.css';
  3. Use webpack — run next dev without --turbopack to use webpack's resolver, which handles symlinked CSS correctly.