@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):
bash
npx @astryxdesign/cli --help
npx @astryxdesign/cli search button
npx @astryxdesign/cli component Button
npx @astryxdesign/cli docs tokens
npx @astryxdesign/cli docs migration
npx @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/commands lists every command, astryx docs cli/api lists the API's functions, schemas, and enums, and a route such as astryx docs cli/api/functions/search prints one.
  • astryx docs authoring --index: the authoring reference, with one section for each file an author writes: the astryx.config.* file, the astryx.integration.* manifest, codemods, and every doc type (ComponentDoc, TemplateDoc, ThemeDoc, and the rest). Read one section with astryx docs authoring <section>, for example astryx 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 example tokens, theme, and layout).

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 button
​
Results for "button" (20 of 239):
​
[component] Button
Button triggers an action when clicked. Use it for form submissions…
→ astryx component Button
​
[component] IconButton
A button that shows only an icon with no visible text…
→ astryx component IconButton
​
[hook] useClickableContainer
Makes a container element clickable while preserving nested…
→ astryx hook useClickableContainer
​
[template] Banner — Collapsible
Combine 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 (doc and theme work 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 — matchCount is how many candidates matched in total, results the slice --limit allowed

Commands

CommandDescription
blogRead the Astryx blog from the published feed
buildBuild a page: the template to start from, or the workflow playbook (no query)
componentList components or print component docs
discoverBrowse and search integrations: the ones you have and the ones you could add
docsPrint reference docs
doctorDiagnose Astryx projects and integration packages
gap-reportReport a missing component or feature to the package that owns it
hookList hooks or print hook docs
initInitialize the design system in your project
integrationAuthor and verify an Astryx integration package
layoutGenerate 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.]
searchSearch components, hooks, docs, templates, and themes in one ranked list
swizzleCopy component source for customization
templateList, show, or scaffold page and block templates
themeAdd, switch, build, and author themes
upgradeUpdate 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 for component, hook, and docs tree reads (such as astryx 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 to full.
  • --zh: Simplified Chinese for component reads and for docs topics that have a translation (English otherwise)
  • --dense: Token-efficient dense text for astryx component <Name> and astryx docs <topic>
  • --lang <locale>: Language or format for component and docs reads: en (default), zh (as --zh), or dense (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.
typescript
import {isError} from '@astryxdesign/cli/json';
​
const result = parseResponse(raw);
if (isError(result)) {
switch (result.code) {
case 'ERR_UNKNOWN_COMPONENT':
// suggest the closest match
break;
case 'ERR_CORE_NOT_FOUND':
// prompt the user to install @astryxdesign/core
break;
default:
console.error(result.error);
}
}

Error codes

CodeMeaning
ERR_UNKNOWNFallback for any error without a more specific code.
ERR_UNKNOWN_COMMANDA top-level command name was not recognized (e.g. astryx bogus).
ERR_UNKNOWN_SUBCOMMANDA subcommand under a command group was not recognized (e.g. astryx theme bogus).
ERR_INVALID_OPTIONAn 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_ARGUMENTAn argument or option value is invalid: wrong type, out of range, an unknown choice, an extra argument, or a conflicting combination.
ERR_MISSING_ARGUMENTA 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_VERSIONThe 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_INCOMPATIBLEThe 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_COMPONENTNo component matched the requested name.
ERR_UNKNOWN_HOOKNo hook matched the requested name.
ERR_UNKNOWN_TOPICNo docs topic matched the requested name.
ERR_UNKNOWN_SECTIONA docs topic exists but the requested section within it does not.
ERR_UNKNOWN_CATEGORYA --category filter value did not match any known category.
ERR_UNKNOWN_TEMPLATENo template matched the requested name.
ERR_AMBIGUOUS_TEMPLATEA template id matched more than one template (narrow with --type/--package).
ERR_AMBIGUOUS_COMPONENTA component name is owned by more than one package (narrow with --package).
ERR_AMBIGUOUS_THEMEA theme slug is owned by more than one package (narrow with --package).
ERR_UNKNOWN_THEMENo theme matched the requested slug (theme add).
ERR_INTEGRATION_ROOT_CONFLICTAn integration manifest already declares a different path for the requested contribution root.
ERR_INTEGRATION_EXPORT_CONFLICTA package export already maps a generated contribution subpath to a different target.
ERR_UNKNOWN_PACKAGENo package matched the requested name.
ERR_UNKNOWN_AGENTAn unrecognized --agent value was passed to init.
ERR_UNKNOWN_FEATUREAn unrecognized --features value was passed to init.
ERR_UNKNOWN_CODEMODA --codemod value did not match any registered codemod (upgrade).
ERR_CODEMOD_FAILEDOne or more codemods failed during an upgrade run.
ERR_CODEMOD_PROTECTEDA required codemod change remains blocked by a protected consumer file.
ERR_CODEMOD_PROTECTION_SOURCEA working-tree protection declaration could not be read or parsed.
ERR_NOT_FOUNDA generic discover/lookup query matched nothing in any package.
ERR_NO_DOCA component exists but has no typed .doc.mjs file.
ERR_NO_SHOWCASENo showcase exists for the requested component.
ERR_NO_SOURCENo source file could be located for the requested component/template.
ERR_INVALID_DOCA component's docs failed validation (malformed .doc.mjs).
ERR_FILE_NOT_FOUNDA required input file did not exist.
ERR_FILE_EXISTSRefused to overwrite an existing file.
ERR_PATH_TRAVERSALA path escaped its allowed root, or a name contained traversal markers.
ERR_WRITE_FAILEDWriting output files failed (and was rolled back).
ERR_THEME_INVALIDA theme definition or contributed theme descriptor is invalid.
ERR_THEME_LOADA theme file could not be loaded / parsed into a defineTheme result.
ERR_PALETTE_GENERATIONA palette generation request or one of its constraints was invalid.
ERR_VERSION_DETECTThe current @astryxdesign/core version could not be detected.
ERR_INVALID_VERSIONA --from/--to value was not a valid semver string.
ERR_DEP_MISSINGA required external dependency (e.g. jscodeshift) is missing.
ERR_GH_CLIGitHub CLI (gh) is not installed or not authenticated.
ERR_UNKNOWN_POSTNo blog post matched the requested slug in the feed.
ERR_FETCH_FAILEDA network fetch (RSS feed or post text) failed.
ERR_LAYOUT_PARSEA layout expression failed to parse (syntax error, with line/col).
ERR_LAYOUT_INVALIDA layout expression parsed but failed validation (unknown component/prop/enum/block).
ERR_UNCLASSIFIED_EXITRecorded in the debug log, never printed: a command exited non-zero without reporting an error code.
ERR_SIGNAL_TERMINATEDRecorded 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.
bash
astryx 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:
typescript
import {
component,
docs,
discover,
template,
hook,
search,
AstryxError,
} from '@astryxdesign/cli/api';
​
// Same result as: astryx --json component Button
const btn = await component('Button');
btn.type; // 'component.detail'
btn.data.name; // 'Button' (typed as ComponentDoc)
​
// Same result as: astryx --json component --list
const list = await component(undefined, {list: true});
list.data; // Record<string, string[]>
​
// Same result as: astryx --json docs principles
const principles = await docs('principles');
principles.data.title; // 'Principles'
​
// Same result as: astryx --json hook useMediaQuery
const useMediaQuery = await hook('useMediaQuery');
useMediaQuery.data.params; // typed as HookParamDoc[]
​
// Errors throw AstryxError with a stable .code and optional .suggestions
try {
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:
typescript
import {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 ComponentDoc
break;
}
}
Prefer narrowing at the call site? Wrap assertResponse (which throws on error/mismatch) with your reconstructed union:
typescript
import {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-jsonOut release): the central CLIAnyResponse, CLIResponseType, and CLIResponseDataMap exports were removed. parseResponse / assertResponse no longer auto-narrow .data. Rebuild the union from the individual *Response types 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.
TypeWhat data carries
init.runThe 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.removeConfirmation that the managed agent-docs block was removed (data.removed: true) — returned when --remove-agents is set.
component.listThe 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.batchThe 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.detailOne 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.propsJust one component's props table (ComponentPropDoc[]). The envelope's package names the npm package that owns the component.
component.detail.sourceOne component's source file, as {component, source}. The envelope's package names the npm package that owns the component.
component.detail.showcaseOne component's showcase example, as {component, aspectRatio, source}. The envelope's package names the npm package that owns the component.
component.detail.blocksOne 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.listAll 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.detailOne 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.indexOne 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.sectionOne 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.nodeOne 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.listThe feed URL plus every post parsed from the RSS feed, each with slug, title, description, date, type, authors, link, and plaintext URL.
blog.detailOne post's metadata plus the feed URL and the post's full plaintext body.
discover.listThe 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.detailOne 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.docThe validated ComponentDoc for one installed component, for an @scope/name/Component query.
discover.itemOne 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.searchThe 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.
searchThe 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.helpThe 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.kitThe 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.listThe names of swizzlable components discoverable from cwd's @astryxdesign/core. The envelope's package is @astryxdesign/core.
swizzle.copyAn 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.categoriesThe fixed gap category values and human-readable labels.
gap-report.fileAn 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.listThe effective discovered TemplateListEntry[] for pages and blocks. A winning replacement entry includes optional replaces, naming the Core id omitted from the default list.
template.showThe 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.skeletonA 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.copyA 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.cdnA 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.listThe 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.detailOne hook's full authored HookDoc. The envelope's package is @astryxdesign/core.
hook.detail.paramsJust one hook's parameters table (HookParamDoc[]). The envelope's package is @astryxdesign/core.
theme.buildA 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.checkThe --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.batchSeveral 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.listEvery 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.addThe 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.appThe 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.ejectA 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.templateA 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.targetsThe 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.generateAn author-reviewable OKLCH palette candidate, its reproducibility receipt, summary counts, and optional candidate/receipt file-write result.
upgrade.listEvery available codemod, oldest→newest, as {name, package, title, version, optional}; returned for --list without running anything.
upgrade.registryThe copied-composition receipt for --registry: applied, ok, the counts (found, current, wouldUpdate, updated, wouldMerge, merged, wouldRefreshReceipt, receiptsRefreshed, conflicts, missing, invalid, failed), and items.
upgrade.statusA short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary.
upgrade.runThe 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.
manifestThe 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.
helpHelp, 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.
versionThe CLI version, for astryx --version --json: {version}.
doctorThe 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.addA contribution-writer receipt: kind, name, optional root {path, created}, integration-manifest path, every affected project-relative path, written, and dryRun.
integration.pack-checkThe 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.validateThe 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-conflictsvalidated (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-conflictsvalidated (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-conflictsvalidated (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.expandThe 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.checkThe 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.grammarThe 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 doctor
astryx doctor - diagnosing your setup
​
id: node-version
status: [ok]
label: Node.js version
message: Node v24.18.1 meets the minimum (>=22.13.0).
​
id: themes
status: [warn]
label: Theme packages
message: 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.
​
...
​
summary
​
pass: 4
warn: 2
fail: 0
info: 2
​
No failures - but review the [warn] warnings above when you can.

Checks

CheckStatus it can returnWhat it verifies
Node.js versionpass / failRunning Node meets the CLI's minimum
@astryxdesign/core installedpass / fail@astryxdesign/core is resolvable from the project
Version alignmentpass / warn / infoInstalled @astryxdesign/core is in step with @astryxdesign/cli
Theme packagespass / warnAn @astryxdesign/theme-* package is installed and a theme is wired
astryx.config.mjspass / fail / infoConfig (if present) loads cleanly with a valid shape
AI agent docspass / warn / infoAgent docs exist and contain the Astryx section markers
Peer dependenciespass / warn / info@astryxdesign/core's peer deps (react, …) are installed
Package managerinfo / warn / failReports 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:
CommandWhat it checksExit 1 when
doctor integration validate [package]Manifest shape and every declared contributionA structural issue has error severity
doctor integration templates [package]Template IDs shared with CoreThe integration is invalid; ID conflicts are warnings
doctor integration components [package]Component names shared with CoreThe integration is invalid; name conflicts are warnings
doctor integration docs [package]Core topic replacements, extensions, and same-name conflictsA 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.
typescript
export 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.
FieldTypePurpose
integrationsstring[]Integration package names to load (see Integrations).
issuesUrlstringWhere "report an issue" links point for your project. Defaults to the core issue tracker.
hooks.postCodemodPostCodemodHook[]Commands to run after astryx upgrade applies codemods (e.g. reinstall, rebuild, reformat).
experimental.xle.componentsRecord<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.mjs with the run order for the staged transforms;
  • leave transforms/next/README.md in place; it documents the staging area and is never promoted.
During the release branch's Version Packages PR, pnpm version-packages runs scripts/promote-codemod-next.mjs after it writes changelogs under the version already declared at the cut. The script copies all staged entries except the README into transforms/v<declared-core-version>/, registers that version in packages/cli/assets/codemods/registry.mjs, and clears the promoted files from next.
Feature PRs stage migration work without guessing a version. The marked release branch assigns the exact cut declaration; the later merge-back carries those exact promoted files to main without regenerating them.

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:
FileWritten byRole
astryx.config.{ts,mjs,js}ConsumerLists which integration packages to load.
astryx.integration.{ts,mjs,js}AuthorDeclares 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.
typescript
export default {
components: './components',
templates: './templates',
codemods: './codemods',
issuesUrl: 'https://github.com/acme/widgets/issues',
};
FieldTypePurpose
componentsstringDirectory holding the package's components and their .doc.* files.
templatesstringDirectory holding the package's page/block templates.
codemodsstringDirectory holding upgrade codemods run by astryx upgrade.
docsstringDirectory of reference docs; each {topic}.doc.* becomes a topic the CLI serves.
issuesUrlstringWhere "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:
bash
astryx docs cli/integrations
astryx docs cli/integrations/quick-start