@astryxdesign/cli
Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.The CLI is the primary interface for working with the design system, for humans and machines alike. It provides component documentation, design tokens, page templates, theming tools, and upgrade codemods, all accessible via terminal commands, a typed JSON API, or programmatic imports. AI agents and build tools use the same API that powers the CLI, enabling end-to-end frontend development loops.
Run it one-off with the scoped package (works whether or not it's installed):
bashnpx @astryxdesign/cli --helpnpx @astryxdesign/cli search buttonnpx @astryxdesign/cli component Buttonnpx @astryxdesign/cli docs tokensnpx @astryxdesign/cli docs migrationnpx @astryxdesign/cli template --list
Once it's a project dependency (
npm install -D @astryxdesign/cli), drop the scope and use the shorter astryx — e.g. npx astryx component Button or pnpm exec astryx component Button. Bare astryx resolves to an unrelated npm package until the CLI is installed, so prefer the scoped form above for first-run/one-off use.Reading the CLI's own docs
The CLI documents itself, so these commands print what the installed version does:
astryx <command> --help: one command's arguments and options.astryx manifest --json: every command, option, and response type, as JSON.astryx docs cli: the CLI's docs tree, one level at a time.astryx docs cli/commandslists every command,astryx docs cli/apilists the API's functions, schemas, and enums, and a route such asastryx docs cli/api/functions/searchprints one.astryx docs authoring --index: the authoring reference, with one section for each file an author writes: theastryx.config.*file, theastryx.integration.*manifest, codemods, and every doc type (ComponentDoc,TemplateDoc,ThemeDoc, and the rest). Read one section withastryx docs authoring <section>, for exampleastryx docs authoring config.astryx docs cli/integrations: the guides to building an integration package, from a quick start to publishing.astryx docs: every docs topic, including the design-system guides (for exampletokens,theme, andlayout).
Finding things: astryx search
When you don't know whether what you need is a component, a hook, a docs topic,
or a template, search across all of them at once. Results are ranked by
relevance (name and keyword matches outrank incidental prose mentions, with
fuzzy matching for typos) and tagged with their domain plus the follow-up
command to run:
bash$ astryx search buttonResults for "button" (20 of 239):[component] ButtonButton triggers an action when clicked. Use it for form submissions…→ astryx component Button[component] IconButtonA button that shows only an icon with no visible text…→ astryx component IconButton[hook] useClickableContainerMakes a container element clickable while preserving nested…→ astryx hook useClickableContainer[template] Banner — CollapsibleCombine an action button, dismiss control, and expandable detail area…→ astryx template BannerCollapsibleContent
(The CLI prints the follow-up commands with your actual runner —
npx astryx … when installed, or npx @astryxdesign/cli … when run one-off.)Options:
--type <component|hook|doc|template|theme>: restrict to a single domain (docandthemework outside an app too)--limit <n>: cap the number of results (default 20)--verbose: also print each result's match score and reason--json: typed{ apiVersion, type: 'search', data: { query, matchCount, results } }envelope —matchCountis how many candidates matched in total,resultsthe slice--limitallowed
Commands
| Command | Description |
|---|---|
blog | Read the Astryx blog from the published feed |
build | Build a page: the template to start from, or the workflow playbook (no query) |
component | List components or print component docs |
discover | Browse and search integrations: the ones you have and the ones you could add |
docs | Print reference docs |
doctor | Diagnose Astryx projects and integration packages |
gap-report | Report a missing component or feature to the package that owns it |
hook | List hooks or print hook docs |
init | Initialize the design system in your project |
integration | Author and verify an Astryx integration package |
layout | Generate XDS layouts from compressed expressions (XLE/XLO) [DEPRECATED: DEP-0006: Use astryx build to start from a template, astryx template to scaffold, and astryx docs layout for guidance.] |
search | Search components, hooks, docs, templates, and themes in one ranked list |
swizzle | Copy component source for customization |
template | List, show, or scaffold page and block templates |
theme | Add, switch, build, and author themes |
upgrade | Update your code after upgrading Astryx, and refresh ShadCN-copied components |
Global options
--json works with every command listed in jsonSupported (astryx manifest --json), which is every command except the bare groups such as astryx theme. The other four change only the reads named here; other commands ignore them:--json: Output as typed JSON envelope:{ apiVersion, type, package?, data, meta? }(errors:{ apiVersion, error, code, suggestions? })--detail <level>: Detail level forcomponent,hook, and docs tree reads (such asastryx docs cli/commands/build), increasing in size:brief(names only, default for lists) <compact(names + 1-line descriptions) <full(full docs per entry). Single-item views default tofull.--zh: Simplified Chinese for component reads and for docs topics that have a translation (English otherwise)--dense: Token-efficient dense text forastryx component <Name>andastryx docs <topic>--lang <locale>: Language or format for component and docs reads:en(default),zh(as--zh), ordense(as--dense)
JSON API
Every command supports
--json for machine-readable output. Responses are typed envelopes:json{"apiVersion": 1, "type": "component.detail", "data": {"name": "Button", ...}}
Errors:
json{"apiVersion": 1,"error": "No component named \"Buttn\"","code": "ERR_UNKNOWN_COMPONENT","suggestions": [{"name": "Button", "reason": "similar name"}]}
The
code field is a stable, machine-readable identifier. Branch on it,
never on the human-readable error string, which changes freely as we improve
wording. Every error envelope carries a code (falling back to ERR_UNKNOWN
when no more specific code applies). The same code is exposed on thrown
AstryxError instances from the programmatic API, so both surfaces agree.Codes are append-only: once shipped, a code's meaning never changes and a
code is never removed. New error conditions get new codes.
typescriptimport {isError} from '@astryxdesign/cli/json';const result = parseResponse(raw);if (isError(result)) {switch (result.code) {case 'ERR_UNKNOWN_COMPONENT':// suggest the closest matchbreak;case 'ERR_CORE_NOT_FOUND':// prompt the user to install @astryxdesign/corebreak;default:console.error(result.error);}}
Error codes
| Code | Meaning |
|---|---|
ERR_UNKNOWN | Fallback for any error without a more specific code. |
ERR_UNKNOWN_COMMAND | A top-level command name was not recognized (e.g. astryx bogus). |
ERR_UNKNOWN_SUBCOMMAND | A subcommand under a command group was not recognized (e.g. astryx theme bogus). |
ERR_INVALID_OPTION | An unknown option was passed, --json was given to a command without JSON output, or layout --form got a value other than compact, outline, or auto. |
ERR_INVALID_ARGUMENT | An argument or option value is invalid: wrong type, out of range, an unknown choice, an extra argument, or a conflicting combination. |
ERR_MISSING_ARGUMENT | A required argument or option value was omitted. |
ERR_INVALID_LANG | --lang was given a value outside its choices (en, zh, dense). |
ERR_INVALID_DETAIL | --detail was given a value outside its choices (full, compact, brief). |
ERR_NODE_VERSION | The running Node.js version is below the supported minimum. |
ERR_CORE_NOT_FOUND | @astryxdesign/core could not be located (not installed / not in a monorepo). |
ERR_CORE_INCOMPATIBLE | The installed @astryxdesign/core loaded but is too old for this input — it lacks a capability the CLI must call to emit correct output (upgrade core). |
ERR_UNKNOWN_COMPONENT | No component matched the requested name. |
ERR_UNKNOWN_HOOK | No hook matched the requested name. |
ERR_UNKNOWN_TOPIC | No docs topic matched the requested name. |
ERR_UNKNOWN_SECTION | A docs topic exists but the requested section within it does not. |
ERR_UNKNOWN_CATEGORY | A --category filter value did not match any known category. |
ERR_UNKNOWN_TEMPLATE | No template matched the requested name. |
ERR_AMBIGUOUS_TEMPLATE | A template id matched more than one template (narrow with --type/--package). |
ERR_AMBIGUOUS_COMPONENT | A component name is owned by more than one package (narrow with --package). |
ERR_AMBIGUOUS_THEME | A theme slug is owned by more than one package (narrow with --package). |
ERR_UNKNOWN_THEME | No theme matched the requested slug (theme add). |
ERR_INTEGRATION_ROOT_CONFLICT | An integration manifest already declares a different path for the requested contribution root. |
ERR_INTEGRATION_EXPORT_CONFLICT | A package export already maps a generated contribution subpath to a different target. |
ERR_UNKNOWN_PACKAGE | No package matched the requested name. |
ERR_UNKNOWN_AGENT | An unrecognized --agent value was passed to init. |
ERR_UNKNOWN_FEATURE | An unrecognized --features value was passed to init. |
ERR_UNKNOWN_CODEMOD | A --codemod value did not match any registered codemod (upgrade). |
ERR_CODEMOD_FAILED | One or more codemods failed during an upgrade run. |
ERR_CODEMOD_PROTECTED | A required codemod change remains blocked by a protected consumer file. |
ERR_CODEMOD_PROTECTION_SOURCE | A working-tree protection declaration could not be read or parsed. |
ERR_NOT_FOUND | A generic discover/lookup query matched nothing in any package. |
ERR_NO_DOC | A component exists but has no typed .doc.mjs file. |
ERR_NO_SHOWCASE | No showcase exists for the requested component. |
ERR_NO_SOURCE | No source file could be located for the requested component/template. |
ERR_INVALID_DOC | A component's docs failed validation (malformed .doc.mjs). |
ERR_FILE_NOT_FOUND | A required input file did not exist. |
ERR_FILE_EXISTS | Refused to overwrite an existing file. |
ERR_PATH_TRAVERSAL | A path escaped its allowed root, or a name contained traversal markers. |
ERR_WRITE_FAILED | Writing output files failed (and was rolled back). |
ERR_THEME_INVALID | A theme definition or contributed theme descriptor is invalid. |
ERR_THEME_LOAD | A theme file could not be loaded / parsed into a defineTheme result. |
ERR_PALETTE_GENERATION | A palette generation request or one of its constraints was invalid. |
ERR_VERSION_DETECT | The current @astryxdesign/core version could not be detected. |
ERR_INVALID_VERSION | A --from/--to value was not a valid semver string. |
ERR_DEP_MISSING | A required external dependency (e.g. jscodeshift) is missing. |
ERR_GH_CLI | GitHub CLI (gh) is not installed or not authenticated. |
ERR_UNKNOWN_POST | No blog post matched the requested slug in the feed. |
ERR_FETCH_FAILED | A network fetch (RSS feed or post text) failed. |
ERR_LAYOUT_PARSE | A layout expression failed to parse (syntax error, with line/col). |
ERR_LAYOUT_INVALID | A layout expression parsed but failed validation (unknown component/prop/enum/block). |
ERR_UNCLASSIFIED_EXIT | Recorded in the debug log, never printed: a command exited non-zero without reporting an error code. |
ERR_SIGNAL_TERMINATED | Recorded in the debug log, never printed: the process was ended by a signal (Ctrl-C, SIGTERM) before the command reached a terminal path. |
Capability manifest (agent discovery)
Agents don't have to scrape
--help to learn the CLI. A single call returns a
self-describing manifest: every command, its arguments, flags (with types,
choices, and defaults), whether it supports --json, the response type
discriminators each command can emit, and its documented exit codes. Think of it
as an OpenAPI spec for the CLI.bashastryx manifest --json # dedicated surface — type: "manifest"astryx --json # bare invocation — embeds the same payload under data.manifest
Shape:
jsonc{"apiVersion": 1,"type": "manifest","data": {"name": "astryx","version": "0.0.14","description": "Design system CLI — components, themes, and tooling","globalOptions": [{"flag": "--json","type": "boolean","description": "Output as typed JSON…",},{"flag": "--lang <locale>","type": "enum","choices": ["en", "zh", "dense"],},{"flag": "--detail <level>","type": "enum","choices": ["full", "compact", "brief"],"default": "full",},],"commands": [{"name": "component","description": "List components or print component docs","arguments": [{"name": "name","required": false,"variadic": false,"description": "",},],"options": [{"flag": "--props","type": "boolean","description": "Print only the props table",},],"json": true,"responseTypes": ["component.list","component.detail","component.detail.props","…",],"examples": ["astryx component Button --props --json"],"exitCodes": [{"code": 0, "when": "success"},{"code": 1, "when": "…"},],},// …one entry per command; subcommands (e.g. `theme build`) nest under `subcommands`],"jsonSupported": ["component", "docs", "…"],"responseTypes": {"component": ["component.list", "…"],"theme build": ["theme.build"],},},}
The manifest is derived from Commander metadata (commands, arguments, options)
so it can't drift from the real command definitions. The facts Commander doesn't
track come from each command's docs: examples from its CommandDoc, and emitted
response types from the returns of the FunctionDoc it wraps (plus the few
envelopes the CLI layer builds itself).
--json support comes from the
JSON_SUPPORTED allowlist. Drift tests (manifest.test.mjs) fail CI when a
command is added without describing it.Backwards-compat: the bare
astryx --json envelope keeps type: "help" and its
original shallow fields (name, version, commands as a string[] of names,
jsonSupported); the full structured manifest is additive under data.manifest.
For the standalone manifest envelope (type: "manifest"), use astryx manifest --json.Programmatic API
The same logic that powers
astryx --json is available as importable, type-safe functions:typescriptimport {component,docs,discover,template,hook,search,AstryxError,} from '@astryxdesign/cli/api';// Same result as: astryx --json component Buttonconst btn = await component('Button');btn.type; // 'component.detail'btn.data.name; // 'Button' (typed as ComponentDoc)// Same result as: astryx --json component --listconst list = await component(undefined, {list: true});list.data; // Record<string, string[]>// Same result as: astryx --json docs principlesconst principles = await docs('principles');principles.data.title; // 'Principles'// Same result as: astryx --json hook useMediaQueryconst useMediaQuery = await hook('useMediaQuery');useMediaQuery.data.params; // typed as HookParamDoc[]// Errors throw AstryxError with a stable .code and optional .suggestionstry {await component('Buttn');} catch (e) {e.message; // 'No component named "Buttn"'e.code; // 'ERR_UNKNOWN_COMPONENT' (stable; branch on this)e.suggestions; // [{ name: 'Button', reason: 'similar name' }]}
The CLI command handlers are thin wrappers around these functions: they parse args, call the API, then format the output (JSON or text). This guarantees that
@astryxdesign/cli/api and astryx --json always return identical data.Consumer utilities
If you're spawning the CLI as a subprocess rather than importing the API directly:
typescriptimport {parseResponse, isError} from '@astryxdesign/cli/json';import type {ComponentDetailResponse,ComponentListResponse,DocsListResponse,// ...import the response types for the commands you consume} from '@astryxdesign/cli/json';// parseResponse returns the structural { apiVersion, type, data, meta? }// envelope; `data` is `unknown` until you narrow it. Reconstruct the union you// care about from the per-command response types, then narrow on `type`:type MyResponse =ComponentDetailResponse | ComponentListResponse | DocsListResponse;const result = parseResponse(stdout);if (isError(result)) {console.error(result.error);} else {const r = result as MyResponse;switch (r.type) {case 'component.detail':r.data.name; // narrowed to ComponentDocbreak;}}
Prefer narrowing at the call site? Wrap
assertResponse (which throws on
error/mismatch) with your reconstructed union:typescriptimport {assertResponse} from '@astryxdesign/cli/json';import type {ComponentDetailResponse} from '@astryxdesign/cli/json';type MyResponse = ComponentDetailResponse; /* | ...others */function assertTyped<T extends MyResponse['type']>(raw: unknown, type: T) {return assertResponse(raw, type) as Extract<MyResponse, {type: T}>;}const detail = assertTyped(stdout, 'component.detail');detail.data.name; // narrowed
Migration (removed in the structural-jsonOutrelease): the centralCLIAnyResponse,CLIResponseType, andCLIResponseDataMapexports were removed.parseResponse/assertResponseno longer auto-narrow.data. Rebuild the union from the individual*Responsetypes as shown above — they are all still exported from@astryxdesign/cli/json.
Type discriminators
Every response has a
type discriminant. The full set is below (generated from the manifest). Each command's types are also listed in astryx manifest --json, and the matching *Response TypeScript types (e.g. ComponentDetailResponse) are exported from @astryxdesign/cli/json. Errors use CLIError, and unsupported commands use CLIUnsupportedError.| Type | What data carries |
|---|---|
init.run | The install receipt: the mode (default | features), the features run, agent-doc files written, any soft docsError, whether theme guidance was emitted, the template outcome (workflow | created | skipped) plus its path, and whether the next-steps were emitted. |
init.remove | Confirmation that the managed agent-docs block was removed (data.removed: true) — returned when --remove-agents is set. |
component.list | The component catalog grouped by component group (each component's group field): detail (the level: names | compact | full) and components, the grouped map of names entries ({name, package, and optional canonical import for integrations}), brief entries ({name, package, description, import}), or a full ComponentDoc per entry with its package. |
component.batch | The component specialization of the shared BatchResponse and BatchRow types. An explicit programmatic selector array, or two or more CLI selectors, returns one ordered receipt: count plus results, one row per selector including duplicates. Every row carries selector and status (found | not_found | ambiguous | error). A found row carries result, the same {type, data} response as one selector. An ambiguous row carries code, error, and candidates ({package, component, kind, installed}). Other failed rows carry code, error, and optional suggestions ({name, reason}). |
component.detail | One component's authored ComponentDoc plus ownership fields (package, the owner; import, the specifier; sourceAvailable, whether source exists) and parentDoc (present when the component is documented inside another component's doc, naming that doc). The envelope's package names the same owner. |
component.detail.props | Just one component's props table (ComponentPropDoc[]). The envelope's package names the npm package that owns the component. |
component.detail.source | One component's source file, as {component, source}. The envelope's package names the npm package that owns the component. |
component.detail.showcase | One component's showcase example, as {component, aspectRatio, source}. The envelope's package names the npm package that owns the component. |
component.detail.blocks | One component's example blocks, as {component, showcase, examples, related} of BlockEntry ({name, package, displayName, description, isShowcase, category}); each block names the package that owns it, and the envelope's package names the component's owner. |
docs.list | All reference-doc topics as DocsListEntry[] ({topic, description, package, replaces?}), in read order; meta.namespaces lists the docs tree's top-level namespaces, and meta.notLoaded each package whose docs did not load. |
docs.detail | One topic's full ReferenceDoc (the JSON read of a topic, --full, --dense, or a topic with one section), with token-ref blocks inlined and each section naming the package that wrote it, plus links ({up, previous, next}: the commands that open the level it sits in and its neighbors there). |
docs.index | One topic's section index, the text read of a topic with more than one section (and --index): the topic's name, title, and description, plus sections, each {id, title, package, summary} (pass the id as the section argument; package is the npm package that wrote the section; summary is the section's one-line summary), and links ({up, previous, next}: the commands that open the level it sits in and its neighbors there). |
docs.detail.section | One ReferenceSection of a topic, found by key or title, with token-ref blocks inlined, plus links ({up, previous, next}: the commands that open its topic index and the sections before and after it). The envelope's package names the npm package that wrote the section. |
docs.node | One node of the docs tree, read by its route: its id, kind, package, title, summary, and breadcrumb, plus a namespace's slots with their children (one level down, each naming its package) or a typed doc's content, and links ({up, previous, next, related}: the commands that open its parent, its neighbors, and the docs it names). |
blog.list | The feed URL plus every post parsed from the RSS feed, each with slug, title, description, date, type, authors, link, and plaintext URL. |
blog.detail | One post's metadata plus the feed URL and the post's full plaintext body. |
discover.list | The integrations the project loads (name, category, components, version, a list per other kind they add, and latest when a source knows it); with a discover source, meta.available lists what the project could add and meta.sources reports each source; when empty it carries meta.configured to tell "nothing configured" from "nothing discovered". |
discover.detail | One package, for an @scope/name or @scope/name@version query: what the shown version adds, whether the project has it, and, when a source knows the package, its versions, latest release, and the command that adds it. |
discover.detail.doc | The validated ComponentDoc for one installed component, for an @scope/name/Component query. |
discover.item | One item that is not an installed component, for an @scope/name/<item> query: its kind, name, package, version, and whether the project has the package. |
discover.search | The echoed query plus every matching item and package across all packages, each with its kind and whether the project has it, even when only one matches; total is set when --limit cut the list. |
search | The echoed query, matchCount (total matches, before limit), and results, a ranked SearchResultEntry[] bounded by limit: each {domain, name, package, score, reason, description, command} (package is the npm package that owns the result), plus import (components, hooks), title, parent (the command that opens the level above), and, for a hit on one section, section (docs), or displayName and kind (templates). |
build.help | The how-to-build-a-page playbook, emitted when no query is given: playbook: true, a title, the ordered steps (title, commands, optional returns), the on-system rules, and related lookups. Commands are bare subcommands for the caller to render with its own invocation. |
build.kit | The template to start from and its kit: query, hasResults, matchCount (never a cap), directMatch, start {name, package, command, basis, reason, alternatives, ...} (the start and each alternative name the package that owns the template), pages (search's closest templates), blocks and domain as SearchResultEntry[] (each naming its package), frame and foundation (Core component names), and hint {reason, commands} when thin. |
swizzle.list | The names of swizzlable components discoverable from cwd's @astryxdesign/core. The envelope's package is @astryxdesign/core. |
swizzle.copy | An eject receipt: component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and, when the owner has an issues URL, feedback ({issuesUrl, ghCommand?}): where to report the gap that led to swizzling. The envelope's package names the same owning package. |
gap-report.categories | The fixed gap category values and human-readable labels. |
gap-report.file | An aggregate receipt: overall status, the selected package, issuesUrl (or null), deliveries in handler order, each {handlerType: project | integration | fallback, handler, audience, status, url, message}, and filedCount/routedOnlyCount totals. |
template.list | The effective discovered TemplateListEntry[] for pages and blocks. A winning replacement entry includes optional replaces, naming the Core id omitted from the default list. |
template.show | The resolved template's source, exactly as a copy writes it, plus its description, kind, the component names it composes, and demoMediaReplaced (how many Astryx demo media references were replaced with placeholders for you to swap for your own media). |
template.skeleton | A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes. The envelope's package names the npm package that owns the template. |
template.copy | A scaffold receipt: template id, output directory, written file name, file count, demoMediaReplaced (how many Astryx demo media references were replaced with placeholders), notes (setup notes naming what the template needs that the project lacks — missing packages, missing StyleX compiler; empty when satisfied), missingPackages (external package names the template imports that are not in the project), and installCommand (a ready-to-run install command with the detected package manager and workspace version ranges; null when nothing is missing). The envelope's package names the npm package that owns the template. |
template.cdn | A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not. exists when a file was already there, which is a success. |
hook.list | The hook catalog grouped by category: detail (the level: names | compact | full) and components, the grouped map of hook names, brief entries, or a full HookDoc per entry. The envelope's package is @astryxdesign/core, the only package that ships hooks. |
hook.detail | One hook's full authored HookDoc. The envelope's package is @astryxdesign/core. |
hook.detail.params | Just one hook's parameters table (HookParamDoc[]). The envelope's package is @astryxdesign/core. |
theme.build | A theme build receipt: name, tokenCount and componentCount (override counts), sizeKB, the written outputs {css, cssDts, js, dts, and variantsDts when applicable}, warnings (defects to fix), and notices (advisories about a correct theme, such as a named font it does not load). |
theme.build.check | The --check receipt: theme name, an upToDate flag, the stale outputs (each {path, reason: missing | outdated}), and the full list of checked paths. Writes nothing. |
theme.build.batch | Several themes built in one invocation: count plus one {file, receipt} per theme in argument order, where receipt is that theme's theme.build (or theme.build.check) envelope, or null when it produced no CSS. |
theme.list | Every bundled, installed package, and local theme as a ThemeListEntry[]. Each entry has slug, displayName, description, maintained, owner package or local root, source, added, and default fields. Optional meta.unmigratedCopies names earlier descriptor-less copies, their missing descriptor, and the upgrade command; those copies are not data entries. |
theme.add | The released source-copy receipt: slug, displayName, maintained flag, owner package, outputDir, source entry, exportName, and files. Its additive meta.deprecations entry names DEP-0005 and the source-fork/import replacements. |
theme.app | The app theme record after add, remove, or use. It includes every added theme and its built imports, the default slug, generated module path, and the command change. After add, the envelope package names the npm package that owns the added theme; a local theme has none. |
theme.eject | A local source-fork receipt with the resolved slug, displayName, source theme maintained flag, source-selector package, outputDir, source entry, exportName, and every file written, including the descriptor. The written local descriptor always uses maintained: false. |
theme.template | A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not. exists when a file was already there, which is a success. |
theme.targets | The whole themeable surface: the echoed filter, componentCount, and targets, one per theming target — {key, className, component, props, states, deprecatedFor?}, where props and states are its legal override keys and deprecatedFor names the canonical replacement key. |
theme.palette.generate | An author-reviewable OKLCH palette candidate, its reproducibility receipt, summary counts, and optional candidate/receipt file-write result. |
upgrade.list | Every available codemod, oldest→newest, as {name, package, title, version, optional}; returned for --list without running anything. |
upgrade.registry | The copied-composition receipt for --registry: applied, ok, the counts (found, current, wouldUpdate, updated, wouldMerge, merged, wouldRefreshReceipt, receiptsRefreshed, conflicts, missing, invalid, failed), and items. |
upgrade.status | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
upgrade.run | The run receipt: from/to versions, codemod count, integrations processed, the agent-docs summary, sourcePathFound (false when the resolved source directory does not exist, so no source file was read), and (apply mode) filesChanged, transformsApplied, and per-codemod errors. |
manifest | The CLI capability manifest: name, version, apiVersion, description, globalOptions, commands (each name, description, arguments, options, json, aliases?, responseTypes?, examples?, exitCodes? as [{code, when}], subcommands?), jsonSupported, and the flat responseTypes index. |
help | Help, in one of two shapes. A bare astryx --json returns the root manifest: name, version, commands (the command names), jsonSupported, and manifest (the full payload astryx manifest --json returns). --help --json on any command, or astryx help [command] --json, returns that command's help: command, description, usage, options (each flags, description, and defaultValue and choices when set), and subcommands (each name and description). data.manifest marks the first shape; data.usage marks the second. |
version | The CLI version, for astryx --version --json: {version}. |
doctor | The health-check report: checks (each with id, label, status: pass | warn | fail | info, a message, and an optional fix, always present on warn and fail) plus a summary of counts per status. |
integration.add | A contribution-writer receipt: kind, name, optional root {path, created}, integration-manifest path, every affected project-relative path, written, and dryRun. |
integration.pack-check | The packed-package check: name, version, packable, tarball {filename, fileCount, size, unpackedSize} or null, inventory {manifest, roots [{kind, path, expectedFiles, missingFiles, complete}], expectedFiles, packedFiles}, contributions {local, packed}, each null or {themes [{slug, exportName}], components, templates [{id, type, name}], codemods [{version, id}], docs, agentDocsAppend}, and issues [{code, severity, message}]. |
integration.validate | The validation result: validated (false when no integration manifest was found, so nothing was checked and the empty issues list proves nothing), the package name and version (both null when validated is false) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning | error, message}. |
integration.template-conflicts | validated (false when no integration manifest was found, so nothing was inspected), the integration identity, structural issues, and non-blocking Core template-id conflicts as {id, severity: warning, integrationPackage, integrationType, integrationName, coreMatches, message, command}. |
integration.component-conflicts | validated (false when no integration manifest was found, so nothing was inspected), the integration identity, structural issues, and non-blocking conflicts where an integration component name is also owned by Core; each conflict includes the exact package-qualified command. |
integration.doc-conflicts | validated (false when no integration manifest was found, so nothing was inspected), the integration identity, structural issues, and Core doc overlaps. Each finding includes severity (info | error) and relationship (replaces | extends | accidental). |
layout.expand | The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, written (the output path, or null when nothing was written), and demoMediaReplaced (count of demo media placeholders). Carries meta.deprecations with DEP-0006 and its replacement commands. |
layout.check | The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline). Carries meta.deprecations with DEP-0006 and its replacement commands. |
layout.grammar | The XLE/XLO grammar cheatsheet: a text field with the full reference plus an aliases map (short name → canonical component) generated from this install's registry. Carries meta.deprecations with DEP-0006 and its replacement commands. |
Doctor
astryx doctor runs read-only health checks against your project and
environment. Each record uses [ok], [warn], [fail], or [info], and
includes an actionable fix when one is available. Field names match the
--json keys. The exact checks and values depend on the project; the output
shape is stable:$ astryx doctorastryx doctor - diagnosing your setupid: node-versionstatus: [ok]label: Node.js versionmessage: Node v24.18.1 meets the minimum (>=22.13.0).id: themesstatus: [warn]label: Theme packagesmessage: No @astryxdesign/theme-* packages are installed.fix: Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set astryx.theme....summarypass: 4warn: 2fail: 0info: 2No failures - but review the [warn] warnings above when you can.
Checks
| Check | Status it can return | What it verifies |
|---|---|---|
| Node.js version | pass / fail | Running Node meets the CLI's minimum |
| @astryxdesign/core installed | pass / fail | @astryxdesign/core is resolvable from the project |
| Version alignment | pass / warn / info | Installed @astryxdesign/core is in step with @astryxdesign/cli |
| Theme packages | pass / warn | An @astryxdesign/theme-* package is installed and a theme is wired |
| astryx.config.mjs | pass / fail / info | Config (if present) loads cleanly with a valid shape |
| AI agent docs | pass / warn / info | Agent docs exist and contain the Astryx section markers |
| Peer dependencies | pass / warn / info | @astryxdesign/core's peer deps (react, …) are installed |
| Package manager | info / warn / fail | Reports the selected package manager and ambiguous or contradictory evidence |
CI gate
The exit code is the contract:
astryx doctor exits 0 when there are no
failures (warnings are fine) and 1 when any check fails. That makes it
usable directly as a CI step:yaml- run: npx @astryxdesign/cli doctor
Use
--json for a structured envelope ({ apiVersion, type: "doctor",
data: { checks, summary } }) that AI agents and scripts can parse.Integration authoring
astryx doctor integration checks one integration package without changing it.
Omit [package] to inspect the package at the current directory, or pass an
installed package name:| Command | What it checks | Exit 1 when |
|---|---|---|
doctor integration validate [package] | Manifest shape and every declared contribution | A structural issue has error severity |
doctor integration templates [package] | Template IDs shared with Core | The integration is invalid; ID conflicts are warnings |
doctor integration components [package] | Component names shared with Core | The integration is invalid; name conflicts are warnings |
doctor integration docs [package] | Core topic replacements, extensions, and same-name conflicts | A Core overlap is accidental or the docs are invalid |
Template and component conflicts include the exact
--package command needed
to select the integration contribution. Doc replacements and extensions are
reported as intentional; a same-name topic without an explicit relationship is
an error. Every leaf supports --json.Configuration
The CLI reads an optional
astryx.config.{ts,mjs,js} from your project root
(a sibling of package.json). Every field is optional; with no config file the
CLI runs on defaults.typescriptexport default {integrations: ['@acme/astryx-widgets'],issuesUrl: 'https://github.com/your-org/your-repo/issues',};
There is no factory: write a plain object. For editor autocomplete and
type-checking, annotate it with the
AstryxConfig type exported from
@astryxdesign/cli/authoring.| Field | Type | Purpose |
|---|---|---|
integrations | string[] | Integration package names to load (see Integrations). |
issuesUrl | string | Where "report an issue" links point for your project. Defaults to the core issue tracker. |
hooks.postCodemod | PostCodemodHook[] | Commands to run after astryx upgrade applies codemods (e.g. reinstall, rebuild, reformat). |
experimental.xle.components | Record<string, XleComponent> | Register app-local components so layout (XLE) expressions can reference them by name. Unstable. |
The config is validated against a strict schema when the CLI loads it, so an
unknown field is a hard error rather than a silent no-op.
astryx doctor
reports whether the config loads cleanly.Core codemod authoring
Core codemods live under
packages/cli/assets/codemods/transforms/. Released
codemods are grouped by the target package version (v0.3.0, v0.3.1, ...),
which is the version that first contains the breaking change.Do not guess that version in ordinary feature PRs. Add new codemods to
packages/cli/assets/codemods/transforms/next/ instead:- put transform modules and tests in
transforms/next/; - maintain
transforms/next/index.mjswith the run order for the staged transforms; - leave
transforms/next/README.mdin place; it documents the staging area and is never promoted.
During the Version Packages PR,
pnpm version-packages runs
scripts/promote-codemod-next.mjs after changeset version. The script copies
all staged entries except the README into transforms/v<new-core-version>/,
registers that version in packages/cli/assets/codemods/registry.mjs, and clears
the promoted files from next.This mirrors Changesets: feature PRs stage migration work without knowing the
future release number; the release PR assigns the exact version.
Integrations
An integration is any npm package that contributes its own components,
templates, and upgrade codemods to Astryx. The CLI surfaces them next to core's,
through the same commands, so a consumer can
astryx component,
astryx template, and astryx upgrade across core and every integration
uniformly. Use it to ship a first-party add-on, publish a third-party component
library, or share an internal design-system package across apps.The system runs on two files, each with a small typed API:
| File | Written by | Role |
|---|---|---|
astryx.config.{ts,mjs,js} | Consumer | Lists which integration packages to load. |
astryx.integration.{ts,mjs,js} | Author | Declares what a package contributes. |
The consumer side is the
integrations field of astryx.config.
The author side is the integration manifest below.The integration manifest
A package becomes an integration by exporting a manifest from
astryx.integration.{ts,mjs,js} at its root (a sibling of package.json). The
manifest points at where each kind of contribution lives; identity (name,
version) comes from package.json, not the manifest.typescriptexport default {components: './components',templates: './templates',codemods: './codemods',issuesUrl: 'https://github.com/acme/widgets/issues',};
| Field | Type | Purpose |
|---|---|---|
components | string | Directory holding the package's components and their .doc.* files. |
templates | string | Directory holding the package's page/block templates. |
codemods | string | Directory holding upgrade codemods run by astryx upgrade. |
docs | string | Directory of reference docs; each {topic}.doc.* becomes a topic the CLI serves. |
issuesUrl | string | Where "report an issue" links for this package's contributions point. |
Every field is optional; declare only the roots the package ships. There is no
factory: write a plain object, and annotate it with the
AstryxIntegration type
from @astryxdesign/cli/authoring for editor autocomplete and type-checking.How it works
Every command loads the consumer's
astryx.config, resolves each listed
integration's manifest from node_modules, and discovers its contributions.
Everything is parsed at the load boundary, so the CLI presents core and
integration contributions through a single, uniform surface. A field of the
wrong type fails there; a field this CLI does not know is ignored with a
warning naming it, so a manifest written against a newer CLI still contributes
everything this one understands.Discovery is resilient. A manifest load failure skips that package with a warning.
An invalid contribution kind remains reportable without hiding other valid kinds,
and invalid template or component metadata is omitted without hiding valid siblings.
Warnings go to stderr and never corrupt a
--json envelope. To inspect problems, run
astryx doctor integration validate <package> for structure, then use templates,
components, or docs under the same astryx doctor integration group to check
Core identity overlaps before publishing. Bare astryx doctor checks overall
project health.For the full walkthrough, from an empty folder to a published package, see the
guides:
bashastryx docs cli/integrationsastryx docs cli/integrations/quick-start