Skip to content

Latest commit

 

History

History

cli

README.md

@astryxdesign/cli

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

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), the same scoped commands run the version you installed, or use your package manager's runner with the shorter name, such as pnpm exec astryx component Button. Never run the unscoped name through npx before the CLI is installed: astryx alone resolves to an unrelated npm package.

Install it once for your computer

npm i -g @astryxdesign/cli

Then astryx works in any folder, for you and for every coding agent on your computer. In a project, Astryx runs the project's own version, so a global install never changes what a project gets; outside a project, your global version answers. It tells you when a newer version is out, and never upgrades itself or a project.

  • Project install, global install, or both (recommended)? astryx docs global/project-global-or-both compares them.
  • How it works, and what it prints: astryx docs global.
  • Something looks wrong: astryx docs global/problems. Uninstall: astryx docs global/uninstall.

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/field-reference --index: the field 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 authoring: 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: keyword and fuzzy matching (typos included) score each match. Between score bands results keep the lexical order, whose domain priority can put a weaker component, hook, template, or theme above a stronger doc, and a query that is an entry's exact name answers with that entry first. Inside a band, fixed weights order the matches, so a description match can lead a keyword match in the same band. Each result is tagged with its domain plus the follow-up command to run:

$ 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 so they reach the release that answered — the project's runner, such as pnpm exec astryx …, when the project's own CLI answered, astryx … outside a project for the copy installed for your computer, or the package pinned to the answering release, such as npx @astryxdesign/cli@0.7.2 …, otherwise — or with the command an installed integration sets as its cliCommand.)

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

Each command declares what it does in a folder outside any project, and astryx manifest --json publishes it as outsideProject: same gives the answer a new project on this release would get, refuse writes nothing and fails with ERR_PROJECT_REQUIRED (or the code its earlier releases used), and own does something else that its docs (astryx docs cli/commands/<name>) describe.

Command Description Outside a project
blog Read the Astryx blog from the published feed same
build Build a page: the template to start from, or the workflow playbook (no query) same
component List components or print component docs same
discover Browse and search integrations: the ones you have and the ones you could add same
docs Print reference docs same
doctor Diagnose Astryx projects and integration packages own
experimental [experimental] Try features that may change in a patch release: icon sets (find icons, build an icon-set package) by subcommand
gap-report Report a missing component or feature to the package that owns it same
hook List hooks or print hook docs same
init Initialize the design system in your project own
integration Author and verify an Astryx integration package by subcommand
search Search components, hooks, docs, templates, and themes in one ranked list same
swizzle Copy component source for customization refuse
template List, show, or scaffold page and block templates same
theme Add, switch, build, and author themes by subcommand
upgrade Update your code after upgrading Astryx, and refresh ShadCN-copied components own

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:

{"apiVersion": 1, "type": "component.detail", "data": {"name": "Button", ...}, "meta": {"answeredBy": {"cli": "0.7.2", "core": "0.7.2", "copy": "project"}}}

Every successful envelope carries meta.answeredBy: the CLI version that answered, the Core version it paired with (null when there is none), and which copy answered (project, starting, matched, carried, or fallback).

Errors:

{
  "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.

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

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.
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 A codemod or post-codemod hook failed during an upgrade run; the error carries a receipt that lists the files the run already changed.
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.
ERR_VERSIONS_UNAVAILABLE Nothing could answer this run, and nothing was written: the project's own CLI could not start and nothing else could answer, or the Core a command needs outside a project could not be installed.
ERR_PROJECT_REQUIRED The command needs a project (an app with Astryx installed, or an integration package) and was run outside one. Nothing was written.

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.

astryx manifest --json        # dedicated surface — type: "manifest"
astryx --json                 # bare invocation — embeds the same payload under data.manifest

Shape:

{
  "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:

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.

Host entry

A tool that carries this CLI and runs it for its own users starts it through @astryxdesign/cli/host, once per process. The run behaves as the astryx bin does with the same arguments in the same folder: it finds the project there, hands a project's run to the project's own CLI, and owns the process's working folder, arguments, standard streams, and exit.

import {runHostedCli} from '@astryxdesign/cli/host';

const {status} = await runHostedCli({
  argv: process.argv.slice(2),
  cwd: process.cwd(),
  cliCommand: 'acme astryx',
});
process.exitCode = status;
Option Default Meaning
argv none The arguments after astryx.
cwd the process's working folder The folder the run works in. It must exist.
cliCommand none (the package manager's name) The command the tool's users type, such as acme astryx. Printed commands start with it unless the integrations the run loads declare their own cliCommand. A non-blank single line of at most 120 characters, with no surrounding spaces.
declinedProjectCli none When the tool found the project's own CLI and did not run it, why, in its own words, such as untrusted. The run is then never handed to that CLI: its release answers in its place when this copy or a package set's host entry is that release; otherwise this copy answers, with one warning line naming the reason.
roots none The tool's package sets, in its order of preference: folders whose node_modules each hold a CLI, the Core equal to it, and the integrations installed with them. A run never installs a release; when another release answers the folder, the first set whose CLI and Core are that release serves the run.
integrations none Integrations the tool approves for use outside a project, by package name, loaded from the package set that serves the run.
discoverSources none The tool's own discover sources, called after every integration's source and reported as the host's. Another package set's host entry that answers the run gets them too.

It resolves with {status, packages}: the exit status the bin would exit with, and each package the run resolved with its version and whether it came from the project or a package set. When another package set's release answers through its own host entry, that entry runs in the same process with the same inputs, and the call resolves with its result. It rejects with a TypeError before anything runs for an unknown option or an invalid value, and with an Error before anything runs when a run already started in the process.

Like the bin, a run ends the process with its exit status, and the call never resolves, when it prints help or the version (--help, -h, help, --version, -V, --version --json), rejects the command line, reports an error it stops on (every error in --json mode included), meets an unexpected error, or hands the run to another CLI's bin: the project's own CLI when declinedProjectCli is absent, or a package set whose CLI ships no host entry. astryx docs cli/host-entry has the same guide.

Consumer utilities

If you're spawning the CLI as a subprocess rather than importing the API directly:

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:

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.

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 The removal receipt — returned when --remove-agents is set: data.removed is true when a managed agent-docs block was found and removed, and false when there was none to remove.
init.user The computer-level agent text receipt — returned when --user is set: data.files, one {path, action, reason?} per path acted on, the skill file first and then each link to its folder (action written | linked | unchanged | removed | absent | kept; reason only on kept: edited when a person changed the text, not-astryx when Astryx did not write it), data.command (the command the text names), and data.warnings.
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?, replacedPackage?}), 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). A topic or guide that took another's place carries replaces and replacedPackage, the package whose topic or guide it replaced.
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); replaces and replacedPackage when the topic or guide took another's place.
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, and a guide an integration replaced its replaces and replacedPackage) 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 What the project has, the integrations it loads and the official packages it declares, official packages first (package, the npm package and always equal to name; name, category, components, version, a list per other kind they add, and latest when a source knows it); meta.available lists what the project could add, the official packages first, each with its package and the command that adds it, and meta.sources reports each source, the official list last; when it lists no integration it carries meta.configured to tell "nothing configured" from "nothing discovered".
discover.detail One package, for an @scope/name or @scope/name@version query: its package (the npm package the result is about), 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, plus package, the npm package that owns the component.
discover.item One item that is not an installed component, for an @scope/name/ query: its kind, name, package, version, and whether the project has the package. The envelope's package names the same 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). When a search with no type could not obtain @astryxdesign/core, meta.partial is {skipped, reason}: the domains it skipped (component, hook, template) and why.
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; when a loaded integration names its own issues URL, meta.destinations lists every destination ({to, default, issuesUrl}) and meta.ask tells an agent to ask the person.
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; without --to, when a loaded integration names its own issues URL, destinations and ask as on gap-report.categories.
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, or when several lockfiles leave the package manager undetermined). 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.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}, where props and states are its legal override keys.
theme.palette.generate An author-reviewable OKLCH palette response: recipe (astryx-oklch-v1), status (candidate), familyCount, stopCount, modes, candidate (TonalPaletteCandidate), and generationReceipt. The generationReceipt contains schemaVersion (1), recipe, generatorVersion (palette-generator-v1), candidateFormatVersion (palette-typescript-v1 or palette-json-v1), candidateSha256, optional preview, resolved request, and diagnostics. File-write fields are output, receipt, preview, written, and reason (exists or null).
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. no_codemods and config_fixable carry cliBehindCore ({cliVersion, command}) when the installed Core is newer than the CLI, which then has no codemods for the releases after its own version.
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. regenerate lists generated files to rebuild, each with its codemods, commands and messages. When the installed Core is newer than the CLI, it also carries cliBehindCore ({cliVersion, command}): codemods for the releases after cliVersion did not run.
manifest The CLI capability manifest: name, version, apiVersion, description, globalOptions, commands (each name, description, arguments, options, json, aliases?, responseTypes?, examples?, exitCodes? as [{code, when}], outsideProject? (same, refuse, or own; absent on a group), subcommands?), jsonSupported, globalMode ({docs, summary}: what a run may do beyond its command, such as the hand-off to the project's own CLI, the daily update check, and the agent skill file, and the docs topic that explains it), 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, fix?, file?, line?}] (the issues validation finds in the source and, prefixed packed_, in the packed tree, with the fix and the place when the check knows them).
integration.validate The validation result: validated (false when nothing was validated: with no package.json the issues list carries one no_package error so the command exits 1, and with a package.json but no integration manifest the issues list is empty and proves nothing), the package name and version (both null when validated is false) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning | error, message, fix?, file?, line?} (fix: the change that resolves it; file and line: where, relative to the package), and notes when there are any: {code, message, next?} information beside the issues (text output marks a next note [next minor]); a note fails nothing by default, and one with next: true counts as a warning under integration check --next, fails integration check --strict --next, and fails --strict from the next minor release. Notes include invalid_cli_command for a malformed cliCommand and cli_peer_version when the running CLI is outside the @astryxdesign/cli peer range of the package, and the package layout findings added after the last minor release, each with its fix in the message (duplicate_component_doc, symlinked_root, unreadable_file, invalid_package_json, no_provider_id, source_without_component_doc where a doc and its source still pair), and doc_name_mismatch and unknown_block_component, until they become warnings in the next minor release. For the commands a topic writes, runner_command (written with a package runner), plain_command (written bare), package_manager_command (written for one package manager) and doc_token (a token that does not parse) notes each give the file, line and the token to write. A doc_target_not_found note names a doc whose replaces or extends names no topic and no guide of the CLI docs or the docs of the package (such as a namespace), and a doc_section_unplaced note a section whose before or after names no section of what it extends (it is added at the end), and a duplicate_guide_replacement note two docs of the package that replace one guide; they become warnings in the next minor release. When the manifest loads, docTypes checks each doc against its published authoring type: status (checked | skipped), reason, typescript, docs, findings (all warnings, each naming the field and a fix), unchecked, and fixtures (test fixture docs not checked). When the manifest loads, docNames lists the component doc names it could not judge: status (checked), findings (always empty; a doc name the module its import names provably does not export is a doc_name_mismatch note, with its fix in the message) and unchecked ({file, name, reason}: a doc whose name differs from the name it is listed under and whose source exports cannot all be read, such as a re-export from another package). When the manifest loads, blockComponents lists the component names templates give in exampleFor or componentsUsed that could not be judged: status (checked), findings (always empty; a name neither the package nor Core has, and the template does not import, is an unknown_block_component note, with its fix in the message) and unchecked ({file, name, reason}: a name the template may reach through anything other than named imports and re-exports from Core, React, StyleX, local helpers or the entry of the package itself, such as an import from another package, a default or namespace import, an alias of or member read off an imported binding, require() or a dynamic import(), or a local module that cannot be read, or any name Core would have to resolve when Core is not installed). Since 0.7.3 each import apps cannot use is also a note (its fix in the message), listed only and failing --strict from the next minor. So are the examples a package ships: component_without_example (a published component no block shows with exampleFor and isShowcase: true, or names in alsoShowcaseFor), template_category (a category written as a path, such as components/X), showcase_not_standalone (a showcase importing server-only code or reading process.env past NODE_ENV) and template_asset_not_packed (a new URL(…, import.meta.url) asset missing, or left out by npm pack --dry-run). When npm cannot list the files, an asset no files entry can reach is still a note and any other is listed in exampleAssets ({status: checked, findings: [], unchecked}, only when it lists any) as {file, asset, reason}. publicImports ({status: checked, findings: [], unchecked}) lists each import the check could not judge as {file, specifier, reason}; never counted or failed on. For the package being authored, olderCliNotes ({file, kind: template | namespace, field, message}) lists each descriptor field a CLI before 0.7.4 refuses, such as stability on a template or namespace doc; it is present only when it has entries, and is never counted or fails a check.
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. When the manifest loads and declares a components root, propDrift compares documented props with the TypeScript props: status (checked | skipped), reason, typescript, components, typesCompared, typesSkipped, findings (all warnings; a default_mismatch carries docDefault, codeDefault, and a fix), unchecked, typesNotCompared, and propsNotChecked.
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). copiedPassages lists each pair of topics that repeat 10 or more words of prose word for word: topic, section, otherTopic, otherOwner (package | core), words, passages, owner (the topic that should keep the text), text (the passage as the topic writes it), line (where the copy starts in the topic to change, or null), and message (the exact replacement); each is also a copied_passage warning in issues. fluff lists padding phrases and low-density topics: topic, kind (phrase | density), phrase, section, file (relative to the package root), line, density, and message (what to cut or write instead); each is also a doc_fluff warning in issues. strayPointers lists each code span that is exactly astryx docs <name> for a doc that exists, in the prose, description or summary of a topic or the text fields of a component doc: topic (the topic or component doc), kind (topic | component), section, field (the field outside any section, such as description or usage.bestPractices[0].description, else null), file (relative to the package root), line, pointer (the written command), link (the {@link} that replaces it), and message; each is also a stray_doc_pointer warning in issues.
integration.check Every source check of an integration package in one result: validated (false when no integration manifest was found, so nothing was checked), the package name and version, strict, next (true with --next), and checks, one entry per check (validate, components, templates, docs), each with check, errors, warnings, skipped, failed, and response, the envelope that check returns on its own. The totals are errors, warnings, and skipped; unchecked counts what the checks could not compare, and upcoming how many of the validate notes carry next: true. With --next those notes count as warnings in the validate row and the totals. With --strict, failures lists each warning and each piece of skipped work again as an error, in the issue format the checks use, and with --strict --next each next note too; it is empty without --strict. failed decides the exit code.

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

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:

- 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, and each props table against the TypeScript props The integration is invalid; name conflicts and prop differences 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.

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.

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> Custom components for the removed layout expander, keyed by component name. The CLI still validates the field, so an entry without a string from makes astryx.config invalid, but nothing reads it.

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:

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.

export 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.
cliCommand string The command its users run the CLI with, such as acme astryx; every command the CLI prints starts with it.

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:

astryx docs authoring
astryx docs authoring/quick-start