@opacityhq/cli
v0.0.2
Published
Opacity CLI: import React components, publish via Opacity, swap local imports
Readme
@opacityhq/cli
The Opacity CLI. Sign in from your terminal, import first-party React components into Opacity, and (optionally) rewrite local imports to consume the published package.
Published to npm as @opacityhq/cli (bin: opacity).
Install
npm install -g @opacityhq/cliInstalls the opacity command globally. Verify with opacity --version.
Quick start
opacity login # sign in via your browser
opacity import # scan ./src, import + publish your components
opacity import --swap # also rewrite local imports to the published packageCommands
opacity login
Opens your browser to the Opacity sign-in flow, polls for completion, and writes credentials with chmod 600. After login, every other command reuses that token + base URL.
Credentials are layered (same precedence as git config local > global):
- Default — global: writes
~/.opacity/credentials.json. One auth covers every Opacity-aware project on this machine. This is the right choice when you batch-run the CLI across many directories (e.g. a sub-agent traversingexamples/apps/*). --local: writes<cwd>/.opacity/credentials.jsoninstead. Use this when you want a different account scoped to a single repo (CI runner, staging vs. prod org, demo recordings). The CLI also writes<cwd>/.opacity/.gitignoreso the token doesn't get committed.
Reads always check local first; falls back to global.
opacity login # global (default)
opacity login --local # project-scoped override
opacity login --base-url https://opacity.example.comDefaults to OPACITY_BASE_URL if set, otherwise https://opacity.com. Override with --base-url (or OPACITY_BASE_URL) to target a local or self-hosted instance.
opacity logout
By default removes the active credentials — project-local if one exists, otherwise global. So opacity logout always signs you out of whatever the next command would have used.
opacity logout— narrow: clear the active layer only.opacity logout --global— force removal of~/.opacity/credentials.jsoneven when a project-local override is also present.
To clear both, run logout twice.
opacity whoami
Prints the active user, which credentials file it came from, and the base URL. Use this to debug "why isn't my login working" — e.g. when a stale .opacity/credentials.json in the project is shadowing the global token.
opacity whoami
# Signed in as [email protected] (global).
# Source: ~/.opacity/credentials.json
# Base URL: https://opacity.comopacity import [path]
Scans a directory of first-party React components, ships an import payload to Opacity, publishes a versioned package, and writes manifests under .opacity/ in your project. Defaults to scanning src.
opacity import # scans ./src
opacity import src/components # scope to a subdirectory
opacity import --swap # also rewrite local imports after publish
opacity import --project-name my-ui # override auto-derived name (first import only)
opacity import --branch main # target a specific branch
opacity import --debug # also write .opacity/debug/payload.json
opacity import --print-issues warn # print per-issue snippet detail at or above level
opacity import --dry-run # preview locally; no API call, no .opacity/ writesOn the first import, the server creates a new project under your account and the assigned IDs are persisted to .opacity/config.json. Subsequent imports re-target that same project automatically.
Notable flags:
--swap— runsopacity swapimmediately after a successful import.--project-name <name>— only used the first time you import in a directory; afterward the project ID is locked into.opacity/config.json.--branch <name>— defaults to the project's saved branch (usuallymain).--base-url <url>— overrides the URL saved at login.--print-issues <level>— also print full snippet + caret detail for every issue at or aboveinfo | warn | error. The summary table always prints; this adds the per-issue source context.--dry-run— run the full local pipeline without publishing. The issues snapshot still gets written to.opacity/debug/issues.jsonso you can preview the file-set delta before a real publish.opacity preflightis the same thing under a more discoverable name.
Components imported directly by an entry file (src/main.tsx, src/index.tsx, etc.) are skipped automatically — translating them would blank your running app.
opacity preflight [path]
Runs the full import pipeline locally without publishing. Equivalent to opacity import --dry-run — it scans, discovers, builds the payload, collects every issue, and writes .opacity/debug/issues.json — but never calls the API and never touches config.json / components.json / manifest.json. Use this to:
- preview what a real
opacity importwould do on a checkout you haven't published from yet, - audit which codes a codebase trips before deciding whether to upgrade the CLI,
- regenerate the issues snapshot after a source change without re-publishing.
opacity preflight # scans ./src
opacity preflight src/components # scope to a subdirectory
opacity preflight --print-issues warn # also print per-issue detail at or above levelThe full code list, grouped by what the swap actually does (cleanly / visibly degrades / refuses), is documented in docs/supported-shapes.md in the repo.
opacity swap [path]
Installs the published Opacity package, wraps your local components with the published equivalents, and hoists dynamic JSX into slots. Reads .opacity/components.json and .opacity/manifest.json written by opacity import.
opacity swap # install package + rewrite imports
opacity swap --dry-run # print diffs only, no install, no writes
opacity swap --print-issues warn # print per-issue snippet detail at or above levelThe dry-run mode is the safe way to preview what swap will do before letting it touch your source tree.
Reading the import output
After every opacity import run (success or failure), the CLI prints a per-component status table summarising what happened in each phase:
Component Import Swap
──────────────────── ────── ────
Avatar █ █
Button █ █ 1 error
Card █ ░ █ 2 warnings, 1 info
ToastProvider █ █
File-level:
src/utils.ts █ 1 warning
src/App.tsx ░ 1 info
1 error, 2 warnings, 1 info — see .opacity/debug/issues.json
View in Opacity: https://opacity.com/projects/<id>/designEach cell is two glyphs: the severity slot on the left summarises the worst outcome for that (row, phase) pair, and the info-note slot on the right lights up when any info-level notes accrued.
| Cell | Meaning |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| █ green | Phase ran cleanly (component imported successfully, no issues). |
| █ yellow | Phase ran with at least one warning. |
| █ red | Phase ran with at least one error. |
| ░ dim | On a component row, phase didn't reach this row (e.g. swap blocked by an import error). On a File-level row, phase ran with only info-level notes (e.g. an entry file that was intentionally skipped). |
| blank | On a File-level row, phase didn't touch this file at all — those rows are sparse so we leave the unused phases empty rather than printing a redundant shade. |
| trailing ░ | Cell accrued at least one info-level note alongside a colored severity block — see the JSON for details. |
The trailing summary line counts errors, warnings, and info notes (zero-counts are omitted, so a clean run with only info notes still shows e.g. 3 info). Full per-issue detail is in the JSON snapshot.
.opacity/debug/issues.json
Every run writes a per-run snapshot of all issues to .opacity/debug/issues.json, grouped by phase × severity. Shape:
{
"schemaVersion": 1,
"generatedAt": "2026-05-04T18:00:00.000Z",
"commitSha": "abc1234+dirty",
"summary": { "total": 12 },
"phases": {
"import": { "total": 11, "issues": { "error": [...], "warn": [...], "info": [...] } },
"package": { "total": 0, "issues": { "error": [], "warn": [], "info": [] } },
"swap": { "total": 1, "issues": { "error": [], "warn": [...], "info": [] } }
}
}Each issue carries a stable code (e.g. DYNAMIC_CLASSNAME_UNRESOLVED, CODEMOD_FILE_REFUSED), a target describing what it applies to (node / component / file / project), an optional location with a 3-line source snippet, and a commitSha stamp. The full code list with severity and phase mappings, grouped by outcome, is documented in docs/supported-shapes.md in the repo.
When the import fails (e.g. the server returns 400 because no components were extractable), the CLI still writes this file and prints the table so you can see exactly which components couldn't be imported and why. Pass --print-issues warn (or info/error) to also print snippet + caret detail inline:
warn Card · import · DYNAMIC_CLASSNAME_UNRESOLVED src/Card.tsx:42:18
Dynamic className expression couldn't be resolved to styles
41 | const cls = `card-${variant}`;
42 | return <div className={cls}>...</div>;
^^^
43 | }What the CLI writes
Global (per-machine, never inside a project):
~/.opacity/credentials.json— auth token + base URL (chmod 600). Written byopacity login(default scope). Used by every project unless overridden.
Project-local (paths relative to the project where you run opacity). The CLI manages a .opacity/.gitignore for you (created on login --local and refreshed on every import) so secrets and per-run debug output stay out of git while project state is committed.
Commit these — they bind the checkout to the Opacity project:
.opacity/config.json— org + project binding, written on firstimport..opacity/components.json— component IDs, package exports, slot anchors. Used byswap..opacity/manifest.json— the published package name, version, and registry URL.
Ignored automatically via .opacity/.gitignore:
.opacity/credentials.json— only present withlogin --local; overrides the global token for this project..opacity/debug/issues.json— every issue emitted during the run, grouped by phase × severity. Overwritten on each run; written even if the import fails..opacity/debug/payload.json— only with--debug; the raw import payload sent to the server.
Environment variables
OPACITY_BASE_URL— default base URL when--base-urlisn't passed and no credentials exist.OPACITY_BRANCH— default branch forimportwhen--branchisn't passed.
