Authoring Reference
Every file an integration author writes, field by field: the integration manifest, astryx.config, codemods, identity, and each doc type.Astryx Integration
The astryx.integration.* manifest that sits beside an integration package's package.json. It can preserve a stable provider identity across a package rename, and points the CLI at the package's components, templates, codemods, doc topics, source themes, managed agent guidance, and issue tracker. Every field is optional.
Applies to: astryx.integration.{ts,mjs,js}
| Field | Type | Required | Description |
|---|---|---|---|
| providerId | string | no | The name that marks this package as the source of everything it contributes. Leave it out to use the package name from package.json. Set it to the old name only during a rename, so the IDs of what the package already contributed stay the same. If two packages use the same name here, the one you are working on wins; otherwise the one the CLI reads first wins, and the CLI warns about the other. Example: '@acme/widgets'. |
| components | string | no | The folder that holds your components and their docs, relative to package.json. Example: './src/components'. |
| templates | string | no | The folder that holds your templates, relative to package.json. Example: './src/templates'. |
| codemods | string | no | The folder that holds your codemods, relative to package.json. Example: './codemods'. |
| docs | string | no | The folder that holds your doc topics, relative to package.json. Every {topic}.doc.{ts,mjs,js} in it shows up in astryx docs next to the built-in topics; a topic can also set replaces or extends to take over a built-in topic or add to it. Example: './docs'. |
| themes | string | no | The folder that holds your themes, relative to package.json, with one folder per theme. Each theme folder has the theme source and a matching .doc.mjs file with the same name. Installed themes show up in astryx theme list; theme add --import imports their built package exports, and theme eject creates an editable local fork. Example: './themes'. |
| agentDocs | { append?: readonly string[] } | no | Lines of guidance your package adds to the end of the agent instructions the CLI manages. The CLI owns the heading, labels, bullets, and which files it writes. Example: { append: ['Run acme verify.'] }. |
| issuesUrl | string | no | Where to file issues/feedback for this integration. Example: 'https://github.com/acme/widgets/issues'. |
jsexport default {components: './src/components',templates: './src/templates',codemods: './codemods',docs: './docs',themes: './themes',agentDocs: {append: ['Run acme verify before finishing.'],},issuesUrl: 'https://github.com/acme/widgets/issues',};
The provider name defaults to the package name in package.json. During a rename, set providerId to the old package name so the IDs of what the package already contributed stay the same. The package version always comes from package.json.
agentDocs.append may contain at most eight lines. Each line is a trimmed, non-blank string of at most 240 Unicode code points with no line separators, control characters, NUL, or Astryx/XDS managed-marker text. The configured project may contain at most 32 integration lines total.
A themes root is forward-compatible but version-gated: a CLI released before this field ignores it with a warning and continues loading every contribution kind it understands. That older CLI cannot list or add the contributed themes.
Validate the manifest with astryx doctor integration validate. At the load boundary, a known field of the wrong type is an error, issuesUrl must be a valid URL, and unknown fields become warnings so an older CLI can still load the fields it understands. Before publishing, also run templates, components, and docs under the same doctor integration group. Those leaves compare authored identities with Core and explain whether an overlap is intentional or needs a rename.
astryx.config
The optional astryx.config.* file at your project root. Declares which integrations to load, where to route issue links, post-codemod hooks, local debug-log and gap-report handlers, and experimental layout components. All optional; {} is valid.
Applies to: astryx.config.{ts,mjs,js}
| Field | Type | Required | Description |
|---|---|---|---|
| integrations | string[] | no | Package names of Astryx integrations to load alongside core. Example: ['@acme/astryx-widgets']. |
| issuesUrl | string | no | URL that "report an issue" affordances link to. |
| hooks | { postCodemod?: PostCodemodHook[] } | no | Lifecycle hooks. |
| hooks.postCodemod | PostCodemodHook[] | no | Commands run after an upgrade applies codemods (e.g. re-run your formatter). Each hook returns a command to execute, or null to skip. |
| debug | (event: DebugEvent) => void | no | Record every command run. The handler is synchronous; promises are not awaited and output goes to stderr. Declare debug directly in this file so early commands can discover it. An integration can supply one too, as a debug named export from its manifest. Every handler runs: yours first, then each integration's in load order (the integrations list, then autolinked ones). A handler that throws is skipped; the others still run and the command's result does not change. Set {"astryx": {"inheritDebug": false}} in package.json to take only your own. Example: event => appendFileSync('runs.ndjson', JSON.stringify(event) + '\n'). |
| gapReport | GapReportHandler | no | Handle explicit gap reports in addition to every loaded integration handler. The project handler runs first. Public handlers require caller consent; internal handlers always run. Example: { audience: 'internal', async handle(report, {signal}) { return sendGap(report, {signal}); } }. |
| discover | DiscoverSource | no | Tell astryx discover which integrations this project could add: an async function that returns a catalog. An integration can provide one too, as a discover named export from its manifest. Discover calls every source, yours first, and one that fails never hides the others. Discover only reads; your package manager installs. Example: async ({signal, package: name, version}) => fetchCatalog({signal, name, version}). |
| experimental | { xle?: { components?: Record<string, XleComponent> } } | no | Unstable features; may change without a breaking bump. |
| experimental.xle.components | Record<string, XleComponent> | no | Custom components the layout expander (XLE) may emit, keyed by tag. |
jsexport default {integrations: ['@acme/astryx-widgets'],};
jsexport default {debug: event => appendFileSync("runs.ndjson", JSON.stringify(event) + "\n"),};
The config is validated at load with a strict schema: unknown keys are errors, so a typo fails fast rather than being silently ignored.
DebugEvent
One recorded CLI run: what a debug handler receives, whether it is set in astryx.config or exported by an integration manifest. There is exactly one event per run, and every field is present by the time a handler sees it.
Applies to: The argument of a debug handler
| Field | Type | Required | Description |
|---|---|---|---|
| schemaVersion | DebugSchemaVersion | yes | Version of the recorded shape: 1, 2, or 3. A field that changes meaning gets a new version, so switch on this. |
| id | string | yes | Unique per run. Deduplicate on it. |
| startedAt | string | yes | When the run started, as ISO 8601. |
| endedAt | string | yes | When the run ended, as ISO 8601. |
| durationMs | number | yes | How long the run took, in milliseconds. |
| command | string | yes | The full command name. Empty when astryx ran without a command. Example: 'theme build'. |
| commandPath | string[] | yes | The command name split into segments. Example: ['theme', 'build']. |
| argv | string[] | yes | The arguments after the binary, scrubbed. |
| args | Record<string, unknown> | yes | Positional arguments, keyed by their declared names. |
| options | Record<string, unknown> | yes | The command options, as the CLI resolved them. |
| optionSources | Record<string, DebugOptionSource> | yes | Where each option value came from: 'cli', 'default', 'env', 'config', or 'implied'. The keys match options. |
| globalOptions | Record<string, unknown> | yes | The root flags, such as --json, --detail, and --lang. |
| outcome | DebugOutcome | yes | How the run ended: 'ok', 'error', 'parse-error', 'fatal', 'rejected', or 'incomplete'. |
| exitCode | number | yes | The process exit code. When a signal ended the run, 128 plus the signal number. |
| signal | string | yes | The signal that ended the run, such as 'SIGINT'. Null otherwise. |
| error | DebugEventError | yes | The failure: name, message, code, and stack. Branch on code, never on message. Null when nothing failed. |
| output | DebugEventOutput | yes | What the run answered with: JSON mode, the envelope types, the result count and kind, and the stdout and stderr it printed, scrubbed and cut at the capture limit. |
| env | DebugEventEnv | yes | Runtime facts: the CLI and Node versions, platform, CI, package manager, and whether a person, an AI agent, or automation ran the command. It never holds a hostname, a user name, or a raw session ID. |
| project | DebugEventProject | yes | The project the command ran in, as flags and counts. It never holds package names. |
| redacted | boolean | yes | Whether the values in the record were scrubbed. It is true on every event a handler receives. When it is false, treat the log as sensitive. |
jsimport {appendFileSync} from 'node:fs';export default {debug: event => {if (event.outcome !== 'ok') {appendFileSync('failed-runs.ndjson', JSON.stringify(event) + '\n');}},};
The shape only grows: fields are added, never removed or given a new meaning. When a meaning must change, schemaVersion gets a new value. In version 3, a null output.resultKind means the run never reached an answer, so do not count it together with a version 1 or 2 null.
The handler runs synchronously as the process exits, so a promise it returns is never awaited. It gets a copy of the event, and anything it throws is ignored.
Import DebugEventOutput, DebugEventEnv, DebugEventProject, and DebugEventError from @astryxdesign/cli/authoring for the fields of output, env, project, and error.
GapReportHandler
A handler for astryx gap-report: a plain object with an audience and a handle function. Set it as gapReport in astryx.config, or export it as gapReport from an integration manifest. Every handler runs: the project handler first, then each integration handler in config order.
Applies to: gapReport in astryx.config.*, or the gapReport named export of astryx.integration.*
| Field | Type | Required | Description |
|---|---|---|---|
| audience | 'internal' | 'public' | yes | Whether the destination is internal to your organization or public. A public handler runs only when the caller agrees; an internal handler always runs. |
| handle | (report: GapReport, context: GapReportHandlerContext) => GapReportHandlerReceipt | Promise<GapReportHandlerReceipt> | yes | Gets the report and returns a receipt. It can be async, and the CLI waits at most 30 seconds for it. A throw, a timeout, or an invalid receipt is a failed delivery, and the other handlers still run. |
| report | GapReport | yes | The report. Each handler gets its own copy. |
| report.schemaVersion | 1 | yes | Version of the report shape. |
| report.component | string | yes | The component the gap is about. |
| report.category | GapReportCategory | yes | The kind of gap: 'missing_component', 'missing_variant', 'layout_gap', 'styling_gap', 'a11y_gap', 'api_friction', 'docs_gap', or 'other'. |
| report.categoryLabel | string | yes | A readable name for the category. |
| report.intention | string | yes | What the caller was trying to do. |
| report.detail | string | no | More detail, when the caller gave some. |
| report.source | DebugInvocationSource | yes | Who sent the report: 'human', 'ai', 'automation', or 'unknown'. |
| report.timestamp | string | yes | When the report was sent, as ISO 8601. |
| report.target | GapReportTarget | yes | The package the report is about. |
| report.target.package | string | yes | The package name. |
| report.target.version | string | yes | The installed version. Null when it is not known. |
| report.target.issuesUrl | string | yes | Where the package takes issues. Null when it does not say. |
| context | GapReportHandlerContext | yes | The context of this call. |
| context.signal | AbortSignal | yes | Aborted when the handler runs past 30 seconds. |
jsexport default {gapReport: {audience: 'internal',async handle(report, {signal}) {const url = await fileIssue(report, {signal});return {status: 'filed', url};},},};
| Receipt field | Type | Required | Description |
|---|---|---|---|
| status | 'filed' | 'routed_only' | 'skipped' | yes | 'filed': the handler created or queued the report. 'routed_only': it points to a place to file the report, and must return url. 'skipped': it chose not to act, for example on a duplicate. |
| url | string | no | An http or https URL. Required when status is routed_only. |
| message | string | no | A readable message of at most 2000 characters. |
The receipt is strict: an unknown field, a URL that is not http or https, or a filed or routed_only receipt with neither url nor message is invalid.
DiscoverSource
A source for astryx discover: an async function that returns a catalog of packages a project could add, their versions, and what each version adds. Set it as discover in astryx.config, or export it as discover from an integration manifest. Discover calls every source, the project one first, and one that throws, runs past 30 seconds, or returns an invalid catalog never hides the others; discover then uses the last good answer it saved for that source. Discover only reads: it prints the command that adds a package and never runs it.
Applies to: discover in astryx.config.*, or the discover named export of astryx.integration.*
| Field | Type | Required | Description |
|---|---|---|---|
| context | DiscoverSourceContext | yes | The one argument the source is called with. |
| context.signal | AbortSignal | yes | Aborted when the source runs past 30 seconds. |
| context.package | string | no | Set when discover shows one package: return that package with every version. |
| context.version | string | no | With package: return that version's contributions. Without it, the latest release's. |
| returns | Promise<DiscoverCatalog> | yes | The catalog. Discover checks it, ignores fields and item kinds it does not know, and refuses any schemaVersion but 1. |
| schemaVersion | 1 | yes | Version of the catalog shape. |
| source | {name: string, generatedAt: string, complete: boolean} | yes | Who answered, when the data was produced (ISO 8601), and false when the source knows its list is partial. |
| packages | DiscoverPackage[] | yes | One entry per npm package. When two sources list the same package, the earlier source wins. |
| packages[].package | string | yes | The npm name. |
| packages[].integration | string | yes | Shared by every npm name that publishes the same integration. Discover lists an integration once. |
| packages[].aliases | string[] | yes | The integration's other npm names. Discover never offers a package the project has under another name. |
| packages[].description | string | no | One line, for the list and search. |
| packages[].latest | string | null | yes | The latest release. Null when there are only prereleases. |
| packages[].versions | DiscoverVersion[] | yes | Every version, newest first: {version, publishedAt, prerelease, status}, where status is ok or why the version could not be read. |
| packages[].contributions | DiscoverContribution[] | yes | What the requested (else latest) version adds: {kind, name, title?, summary?, keywords?}, where kind is component, template, doc, theme, codemod, or agent-doc (a DiscoverKind) and name is the name the CLI uses for it. |
jsexport default {async discover({signal, package: name, version}) {const res = await fetch(catalogUrl(name, version), {signal});return res.json();},};
Astryx Codemod
A codemod module the CLI runs during astryx upgrade. Default-export a plain object with a type discriminant: 'code' rewrites source files, 'config' rewrites the astryx.config.* file. There is no factory to call.
Applies to: the codemods/ dir of an integration
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | yes | Short, human-readable title shown in upgrade output. Example: 'Rename Button prop kind to variant'. |
| description | string | no | Optional longer description. |
| isOptional | boolean | no | When true, the codemod runs only when explicitly requested. Default: false. |
| fileExtensions | string[] | no | File extensions this codemod applies to. Code codemods only; a config codemod always targets astryx.config.*. Example: ['.tsx', '.ts']. |
| transform | (file: AstryxCodemodFile, api: AstryxCodemodApi) => string | null | undefined | yes | The transform. Return the new source to rewrite the file, or null/undefined to leave it unchanged. |
| file | AstryxCodemodFile | no | The source file presented to the transform. |
| file.path | string | yes | Absolute path to the file being transformed. |
| file.source | string | yes | The current source contents of the file. |
| api | AstryxCodemodApi | no | Helpers and context passed as the second argument. |
| api.jscodeshift | unknown | yes | A jscodeshift instance configured with a parser for the file. |
| api.stats | (...args: unknown[]) => void | yes | Report a statistic (no-op-friendly; provided for jscodeshift parity). |
| api.report | (...args: unknown[]) => void | yes | Report progress (no-op-friendly; provided for jscodeshift parity). |
| type | 'code' | 'config' | yes | Discriminant for the file-transforming variant. Use 'config' for a codemod that rewrites astryx.config.* instead (see notes). Example: 'code'. |
jsexport default {type: 'code',title: 'Rename Button prop kind to variant',fileExtensions: ['.tsx', '.ts'],transform(file, api) {const j = api.jscodeshift;const root = j(file.source);// ...rewrite the AST...return root.toSource();},};
jsexport default {type: 'config',title: 'Move issuesUrl into astryx.config',transform(file, api) {// Rewrite the astryx.config.* source; return null to skip.return null;},};
The config-codemod variant (type: 'config') carries the same fields as a code codemod except fileExtensions: it always targets the astryx.config.* file rather than a set of source files.
Authors write a plain object and default-export it with the type discriminant; there is no factory. The CLI validates it at the load boundary via parseCodemod with a strict schema, and isOptional defaults to false.
Provider and artifact identity
Separates stable provider/artifact identity from package instances and runtime lifecycle state. Provider identity names integration manifests and settles provider conflicts. Docs-tree nodes are identified by doc identity; the tree covers the CLI's own docs. Artifact, instance, and compiler-input identities are defined for every provider-owned artifact; no command reads them.
Applies to: Integration manifests, provider conflicts, and docs-tree node ids.
| Field | Type | Required | Description |
|---|---|---|---|
| ProviderId | string | yes | Canonical logical provider ID. It defaults to the lowercase npm package name; Core uses the same model without publishing an integration manifest. |
| ArtifactId | string | yes | Versioned serialization of provider ID, contribution kind, and stable artifact name. Every segment uses RFC 3986 escaping. |
| DocId | ArtifactId | yes | Artifact ID restricted to one authored doc kind. Navigation changes do not change it. |
| ProviderInstance | { id; providerId; packageName; packageVersion; sourceDigest } | yes | One immutable package version and source/content digest. Installed, configured, loaded, selected, and healthy state is a separate runtime overlay. |
| AuthoredDocEntry | { id; provider; kind; stableName; source; authored } | yes | Normalized compiler input. Discovery supplies stableName independently from the authored display name, source paths remain package-relative, and provider provenance plus the authored snapshot are immutable. |
Provider package renames require an explicit mapping: a ProviderInstance may retain its stable providerId while packageName changes. Silent identity changes are not inferred.
Fields every authored doc kind can declare for the docs tree: placement, plus two reserved fields, aliases and audience. The docs tree reads placement for every guide, the CLI's and each integration's. Nothing reads aliases or audience today: a reference topic outside the docs tree that sets one fails to load, and other doc kinds accept them and ignore them.
Applies to: Every supported .doc.mjs object
| Field | Type | Required | Description |
|---|---|---|---|
| placement | DocPlacement | no | Names the doc's one parent in the docs tree: a namespace of the same package, one of its slots, and an order. Read for every guide, the CLI's and each integration's: a guide with placement gets a route in the tree instead of a flat topic name, and cannot also replaces or extends a topic. In the CLI's own topic directory a topic that sets it fails to load: the CLI keeps its guides in its docs tree directory. Commands, API functions, schemas, and enums do not set it: the tree adopts each by its namespace. |
| placement.parent | string | yes | The parent namespace: namespace:<name> in the same package. |
| placement.slot | string | no | A slot the parent namespace declares; it must accept this doc's kind. Optional when the parent has one slot. |
| placement.order | number | no | Integer sibling order within the slot. |
| aliases | string[] | no | Reserved: prior names or routes the docs tree will keep resolving to this doc, without creating another identity. Nothing reads it today, and a topic that sets it fails to load. |
| audience | 'public' | 'internal' | no | Reserved: which docs bundle includes this doc ('public' when omitted). Nothing reads it today, and a topic that sets it fails to load. Default: 'public'. |
Unknown fields: component, function, generic, schema, command and enum docs accept a field they do not know, and nothing reads it, so a doc written for a newer CLI still loads here. page, block and namespace docs refuse one, as do theme descriptors. Sections and content blocks refuse one in every doc.
ComponentDoc
The doc-type for a component directory's {Name}.doc.mjs. A discriminated union of SingleComponentDoc (props on the doc), MultiComponentDoc (a components array), and SubComponentDoc (a subComponentOf pointer). All three share the ComponentBaseDoc fields below; the variant is chosen by which of props / components / subComponentOf you set.
Applies to: {Name}.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'component' | no | Doc-kind discriminant for the stamped default-export format. Optional: legacy export const docs = {...} docs omit it and the parser falls back to shape-sniffing. |
| name | string | yes | Stable machine identity and directory name without the Astryx prefix, PascalCase. e.g. 'Button', 'TextInput', 'AppShell'. Change displayName, not name, to edit the visible label; registry URLs derive from this identity by default. |
| displayName | string | yes | Human-readable display name with spaces between words ('AppShell' → 'App Shell'). Drives the docsite gallery and sidebar label. |
| registry | RegistryDocIdentity | no | Optional public registry identity. The converter derives a stable kebab-case slug from name; set slug only to override it, and keep prior relative paths in aliases after a published rename. |
| import | string | no | Exact public package specifier consumers use to import an integration-owned component. The packed-package gate resolves this specifier and verifies it exports the component name. |
| replaces | string | no | Integration components only: the exact name of the Core ComponentDoc this component takes over for unqualified lookup, so every app that loads the integration gets it from component detail, component lists, search, swizzle <Name>, and issue routing; swizzle --list keeps listing Core names. The Core original stays reachable with --package @astryxdesign/core. It takes effect only when the package's peer range starts at the release that applies it, "@astryxdesign/cli": ">=0.6.7" or later; without such a range the component keeps its own name and doctor integration components warns. Set it only to intentionally own a Core identity; give an alternative or variant its own name instead. |
| keywords | string[] | no | Search keywords for CLI discovery: synonyms and related UI concepts from other design systems (MUI, Chakra, Radix, and others). Lowercase. Used by astryx component <term> fuzzy matching. |
| hiddenComponents | string[] | no | Sub-component names to hide from human-facing UI (CLI listings, docs catalogs). They stay public and importable; agents and tooling can still discover them via source. |
| hidden | boolean | no | Hide this entire component from human-facing UI. It stays public and importable. Use for shared primitives (NavIcon, NavMenu) that only make sense inside their parent. |
| group | string | no | Optional sidebar/docs group. Clusters related components; ungrouped components appear flat in alphabetical order. |
| category | 'Action' | 'Chat' | 'Container' | 'Content' | 'Form Controls' | 'Data Input' | 'Data Visualization' | 'Feedback & Status' | 'Layout' | 'Navigation' | 'Overlay' | 'Table & List' | 'Utility' | no | Overview-gallery category representing the component's functional role. Independent of group (which is for the sidebar). Data Input is a deprecated compatibility alias for Form Controls. |
| isHiddenFromOverview | boolean | no | Exclude from the categorized overview page while keeping the component in the sidebar and CLI. Use for sub-components or internal primitives. |
| theming | { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } | no | Theming configuration: the stable selector surface (xds-* classes + data-attribute reflections) that themes target via @scope selectors in defineTheme. |
| theming.container | boolean | no | When true, container padding props are mapped to container tokens by the theme pipeline instead of emitting raw CSS. |
| theming.targets | ComponentThemingTarget[] | yes | Selector targets rendered by this component. Each entry corresponds to a themeProps() call in the source. |
| theming.vars | ComponentThemingVar[] | no | CSS custom properties exposed for theming. |
| theming.derived | ComponentThemingDerivedVar[] | no | Maps standard CSS properties to internal vars for theme-pipeline expansion. Ordered by priority: earlier entries emit first. |
| usage | UsageDoc | no | Component usage documentation: concise summary, best practices, component-specific accessibility requirements, and optional visual anatomy. Required on a component doc; optional on a sub-component doc (subComponentOf), which uses its description instead. |
| usage.description | string | yes | What the component is and when to use it, in 2-3 short sentences. |
| usage.bestPractices | ComponentBestPractice[] | no | 3-4 do/don't design-guidance items ({guidance: boolean, description: string}). Never start the description with 'Do' or 'Don't'. |
| usage.accessibility | ComponentAccessibilityRequirement[] | no | Component-specific requirements rendered in the shared Accessibility tab. Write at about a grade-7 reading level with short sentences, common words, and active voice. For color contrast, put the ratio in requirement; name the exact foreground, background, state, and any overlay in description; explain exceptions in plain language; and give a human or agent enough detail to reproduce the check. Keep repository audit procedures in the wiki rubric. |
| usage.accessibilityThemeCoverage | ComponentAccessibilityThemeCoverage[] | no | Verified per-theme accessibility measurements rendered in the shared Accessibility tab. Record light and dark mode separately, include rendered color pairs, and mark failed measurements. Put visuals excluded from the audit in notMeasured with a short reason; do not add them as table measurements. Each theme declares applicability for measured values, informed by the component contract and never inferred from the ratio: Conditional is required only in some contexts, Supplemental adds another meaningful cue, and Decorative has no required meaning. These values do not change row status. Provide a complete breakdown when one cell summarizes multiple combinations, and protect derived values with an automated audit. |
| usage.anatomy | ComponentAnatomyElement[] | no | Structural/visual parts in reading order ({name, required, description}). |
| examples | ComponentExampleDoc[] | no | Short code examples ({label?, code}) rendered by the CLI after the props table. |
| playground | ComponentPlaygroundConfig | no | Interactive-preview config: initial prop defaults, overlay for modal-only components, appShellMobile for components gated on AppShell mobile context, and a wrapper for context-dependent sub-components. |
| props | ComponentPropDoc[] | no | SingleComponentDoc variant (required there): all public props for the one primary component. Each prop is {name, type, description, default?, required?, slotElements?}. Skip styling props like xstyle/className/style. Also present on SubComponentDoc. |
| components | (ComponentEntry | ComponentRef)[] | no | MultiComponentDoc variant (required there): one entry per public component/hook exported from the directory. Each entry is a full ComponentEntry (inline: name, displayName, description, props | params+returns) or a name-only ComponentRef pointing at a sibling {Name}.doc.mjs. |
| subComponentOf | string | no | SubComponentDoc variant (required there): the parent component's name (e.g. 'Chat'). Marks this file as a sub-component doc that inherits family fields (group, category, keywords, theming, playground) from the parent. |
| description | string | no | SubComponentDoc variant (required there): one-sentence description of the sub-component's role within the parent composition. Single/Multi docs have no top-level description; they derive their summary from usage. |
js/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export const docs = {name: 'Switch',displayName: 'Switch',category: 'Form Controls',keywords: ['toggle', 'switch', 'on off'],usage: {description:'A Switch toggles a single setting on or off. Use it for instant, binary preferences that apply immediately without a submit step.',bestPractices: [{guidance: true, description: 'Apply the change immediately when toggled.'},{guidance: false, description: 'Use a Switch for actions that need confirmation; prefer a Checkbox in a form.'},],},props: [{name: 'isSelected', type: 'boolean', description: 'Whether the switch is on.', required: true},{name: 'onChange', type: '(isSelected: boolean) => void', description: 'Called when the user toggles the switch.'},{name: 'isDisabled', type: 'boolean', description: 'Prevents interaction and dims the control.', default: 'false'},],};
js/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export const docs = {name: 'Table',displayName: 'Table',category: 'Table & List',usage: {description: 'Displays rows and columns of data. Compose the sub-components to build headers, rows, and cells.'},components: [{name: 'Table', displayName: 'Table', description: 'The table container.', props: []},{name: 'TableRow', displayName: 'Table Row', description: 'A row within the table body.', props: [{name: 'isSelected', type: 'boolean', description: 'Highlights the row as selected.'},]},{name: 'useTableSelection', displayName: 'useTableSelection', description: 'Manages row selection state.',params: [{name: 'rows', type: 'T[]', description: 'The rows to track.', required: true}],returns: [{name: 'selectedIds', type: 'Set<string>', description: 'Currently selected row ids.'}]},],};
When it loads, a stamped component doc is checked as loosely as an unstamped one, so adding type: 'component' to an existing doc never breaks it: displayName may be missing, category may be any string, and usage, theming, playground and examples are not checked. Each entry in a group doc's components must have a name. Write to the type anyway; it is the contract.
ComponentDoc is a discriminated union of three shapes that all extend ComponentBaseDoc. Pick the variant by which key you set: props (single), components (multi), or subComponentOf (sub).
- SingleComponentDoc: one primary component; put props directly on the doc via
props. Use forSwitch,Badge,Spinner,TextInput. - MultiComponentDoc: a directory exporting several components/hooks; list them in
components(inline ComponentEntry or name-only ComponentRef). Use forTable,Dialog,TabList. - SubComponentDoc: a single sub-component in its own {Name}.doc.mjs inside the parent directory; set
subComponentOfto the parent name. It inherits family fields and may omitusage.
js/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export const docs = {name: 'ChatComposer',displayName: 'Chat Composer',subComponentOf: 'Chat',description: 'The message input row within a Chat, with an editor and send affordance.',props: [{name: 'onSend', type: '(text: string) => void', description: 'Called when the user submits a message.', required: true},],};
The stamped format is export default { type: 'component', ... }; legacy docs use export const docs = {...} and omit type (the parser shape-sniffs). A hook that is part of a component API is documented as a ComponentEntry in a MultiComponentDoc components array (with params/returns), not as a standalone HookDoc.
HookDoc
The doc-type for a standalone React hook (e.g. useMediaQuery, useFocusTrap, useOverflow) that gets its own .doc.mjs. Hooks that are part of a component API (e.g. useImperativeDialog) belong in that component's MultiComponentDoc components array instead. HookDoc is the hook-flavored view of the shared type: 'function' kind.
Applies to: {useX}.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'function' | no | Doc-kind discriminant (shared with FunctionDoc). Legacy export const docs = {...} docs omit it. |
| name | string | yes | Stable hook name exactly as exported, e.g. 'useMediaQuery', 'useFocusTrap'. Change displayName, not name, to edit the visible label; registry URLs derive from this identity by default. |
| displayName | string | yes | Human-readable display name. Hooks read better as the raw identifier ('useMediaQuery') than spaced, so keep the identifier verbatim. |
| registry | RegistryDocIdentity | no | Optional public registry identity. The converter derives a stable kebab-case slug from name; set slug only to override it, and keep prior relative paths in aliases after a published rename. |
| group | string | no | Optional group for sidebar/docs organization; same as ComponentDoc.group. |
| keywords | string[] | no | Search keywords for CLI discovery. |
| params | HookParamDoc[] | yes | Hook parameters or options-object fields. |
| params[].name | string | yes | Parameter or option field name. |
| params[].type | string | yes | TypeScript type signature as a string. |
| params[].description | string | yes | What this parameter does. 1-2 sentences. |
| params[].default | string | no | Default value as a string, if optional with a default. |
| params[].required | boolean | no | True if required. Omit if optional. |
| returns | HookReturnDoc[] | yes | Return value documentation. For object returns, list each field; for primitive returns, use a single entry. |
| returns[].name | string | yes | Field name on the returned object, or 'value' for primitive returns. |
| returns[].type | string | yes | TypeScript type. |
| returns[].description | string | yes | What this return value provides. |
| usage | UsageDoc | yes | Usage documentation: description and best practices. |
| usage.description | string | yes | What the hook is and when to use it, in 2-3 short sentences. |
| usage.bestPractices | ComponentBestPractice[] | no | 3-4 do/don't design-guidance items ({guidance: boolean, description: string}). |
| usage.accessibility | ComponentAccessibilityRequirement[] | no | Accessibility requirements specific to using the hook ({name, description}). |
| usage.anatomy | ComponentAnatomyElement[] | no | Structural/visual anatomy elements (rarely used for hooks). |
| relatedComponents | string[] | no | Component names this hook is commonly used with. Enables cross-referencing (e.g. astryx hook useToast links back to Toast). |
| relatedHooks | string[] | no | Other hook names this hook is commonly used with. |
| importPath | string | no | Import path, e.g. '@astryxdesign/core/hooks' or '@astryxdesign/core/Toast'. |
| category | string | no | Category for grouping in listings. |
js/** @type {import('@astryxdesign/cli/authoring').HookDoc} */export const docs = {type: 'function',name: 'useMediaQuery',displayName: 'useMediaQuery',importPath: '@astryxdesign/core/hooks',params: [{name: 'query', type: 'string', description: 'A CSS media query, e.g. "(min-width: 768px)".', required: true},],returns: [{name: 'matches', type: 'boolean', description: 'Whether the query currently matches.'},],usage: {description:'Subscribes to a CSS media query and re-renders when it changes. Use for responsive behavior that CSS alone cannot express, such as swapping components by breakpoint.',},relatedHooks: ['useIsMobile'],};
When it loads, a hook doc may leave out displayName, and its usage is not checked. Write to the type anyway; it is the contract.
A hook's discriminant is type: 'function': HookDoc and FunctionDoc share the generalized function kind. HookDoc is the hook-flavored view: named returns fields and a required usage block.
Only standalone hooks get their own file. A hook that is part of a component API is documented as an entry in that component's MultiComponentDoc components array (with params/returns), so it renders under the component.
FunctionDoc
The generalized type: 'function' doc-type covering both React hooks and CLI/API functions. A hook (HookDoc) is the hook-flavored view of this same kind; FunctionDoc adds the fields an API function needs (a {type, data} return envelope, thrown error codes, the wrapping CLI command). The CLI binding itself lives in a separate CommandDoc.
Applies to: api/<name>/<name>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'function' | no | Doc-kind discriminant (shared with hooks). |
| name | string | yes | Export name, e.g. 'search' | 'useMediaQuery'. |
| displayName | string | yes | Human-readable display name, e.g. 'search()'. |
| kind | 'hook' | 'api' | no | Which flavor; drives docsite sectioning; inferred from importPath if omitted. |
| summary | string | no | One-line summary. |
| description | string | no | Longer description. |
| namespace | string | no | Optional in the type, but every doc the CLI ships declares it. The group that reads this doc. The CLI's API functions use 'cli/api', which the docs tree adopts by kind: each is the leaf cli/api/functions/<name>. Every function doc the CLI ships declares one, and astryx doctor fails on one that is missing or that nothing reads. |
| aliases | string[] | no | Alternate slugs that also resolve to this doc. |
| keywords | string[] | no | Search keywords for discovery. |
| importPath | string | no | Import path, e.g. '@astryxdesign/cli/api' | '@astryxdesign/core/hooks'. |
| signature | string | no | Full signature as a string, e.g. 'search(query, options?): Promise<SearchResponse>'. |
| params | HookParamDoc[] | yes | Parameters / options-object fields. |
| params[].name | string | yes | Parameter or option field name. |
| params[].type | string | yes | TypeScript type signature as a string. |
| params[].description | string | yes | What this parameter does. 1-2 sentences. |
| params[].default | string | no | Default value as a string, if optional with a default. |
| params[].required | boolean | no | True if required. Omit if optional. |
| returns | FunctionReturnDoc[] | yes | Return documentation. Hooks list named return fields (name set); API functions list {type, data} envelope entries where type is the response discriminant and name is omitted. |
| returns[].name | string | no | Field name (hooks); omit for API envelope entries. |
| returns[].type | string | yes | TS type (hooks) or response-type discriminant (API), as a string. |
| returns[].description | string | yes | What this return value / envelope entry provides. |
| throws | FunctionThrowsDoc[] | no | Errors the function throws (API functions), keyed to ERROR_CODES. |
| throws[].code | string | yes | The ERROR_CODES member thrown. |
| throws[].when | string | yes | The condition under which it is thrown. |
| examples | FunctionExampleDoc[] | no | Usage examples. |
| examples[].label | string | no | Optional heading shown above the snippet. |
| examples[].code | string | yes | Language-level usage, e.g. "await search('button')". |
| examples[].result | string | no | Optional sample result. |
| usage | UsageDoc | no | Usage documentation (hooks): description, best practices, accessibility requirements, and anatomy. Same shape as HookDoc.usage. |
| command | string | no | The CLI command that wraps this function, e.g. 'search'. |
| related | string[] | no | Related function names; the CLI links each to its doc. Name the CLI command with command, not here. |
| relatedComponents | string[] | no | Component names this is commonly used with (hooks). |
| relatedHooks | string[] | no | Other hook names this is commonly used with (hooks). |
| category | string | no | Category for grouping in listings. |
js/** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */export const doc = {type: 'function',kind: 'api',name: 'search',displayName: 'search()',namespace: 'cli/api',importPath: '@astryxdesign/cli/api',summary: 'Find components, hooks, docs, and templates by term.',signature: 'search(query, options?): Promise<SearchResponse>',params: [{name: 'query', type: 'string', description: 'The search term.', required: true},{name: 'options.type', type: "'component' | 'hook' | 'doc' | 'template'", description: 'Restrict results to one domain.'},],returns: [{type: 'search', description: 'Envelope with the query and ranked results[].'},],throws: [{code: 'ERR_INVALID_ARGUMENT', when: 'query is empty.'},],examples: [{label: 'Basic', code: "await search('button')", result: "{ type: 'search', data: { results: [...] } }"},],command: 'search',};
When it loads, a stamped function doc may leave out displayName, and its usage is not checked. Write to the type anyway; it is the contract.
The type discriminant is 'function' for both flavors. Set kind: 'hook' or kind: 'api' to drive docsite sectioning; it is inferred from importPath when omitted.
The two flavors differ in returns: a hook lists named fields (name set), while an API function lists {type, data} envelope entries whose type is the response discriminant and whose name is omitted. throws (keyed to ERROR_CODES) applies to API functions.
The function does not know it has a CLI. The terminal binding (args, flags, exit codes) lives in a separate CommandDoc that references this function via fn; here you only note the wrapping command name via command.
CommandDoc
The doc-type for a CLI command: the terminal binding of an operation. A command is not its own behavior; it is a FunctionDoc exposed on the CLI, referenced via fn, carrying only CLI-surface facts (args, flags, subcommands, examples, exit codes). A defineCommand converter turns it into Commander config + --help.
Applies to: clients/cli/commands/<name>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'command' | no | Doc-kind discriminant. Marks the file as a command doc. |
| name | string | yes | Command path, e.g. 'search' | 'theme build'. |
| displayName | string | yes | Human-readable display name, e.g. 'astryx search'. |
| summary | string | yes | One-line summary → Commander .description() + the docs listing. |
| description | string | no | Longer help body / when-to-use. |
| namespace | string | no | Optional in the type, but every doc the CLI ships declares it. The group that reads this doc. The CLI's commands use 'cli/commands', which the docs tree adopts: each is the leaf cli/commands/<name>. Every command doc the CLI ships declares one, and astryx doctor fails on one that is missing or that nothing reads. |
| aliases | string[] | no | Alternate slugs that also resolve to this doc. |
| fn | string | no | Name of the FunctionDoc (and @astryxdesign/cli/api export) this command wraps. Example: 'search'. |
| args | CommandArgDoc[] | no | Positional arguments. |
| args[].name | string | yes | Argument name. |
| args[].param | string | no | FunctionDoc param this arg binds to (inherits its description). |
| args[].description | string | no | Override description (else inherited from the referenced param). |
| args[].required | boolean | no | Whether the positional argument must be supplied. |
| args[].variadic | boolean | no | Whether the argument collects a variable number of values. |
| options | CommandOptionDoc[] | no | Flags/options. |
| options[].flag | string | yes | Commander flag spec, e.g. '-l, --limit <n>' | '--json'. |
| options[].param | string | no | FunctionDoc param this flag maps to (inherits its description). |
| options[].description | string | no | Override/explicit description (required when cliOnly). |
| options[].choices | string[] | no | Allowed values for the flag. |
| options[].default | string | boolean | string[] | no | Default value: a string, a boolean, or a list of strings. |
| options[].cliOnly | boolean | no | True for CLI-only flags with no function param (e.g. --json). |
| subcommands | string[] | no | Subcommand names (for command groups like theme / layout). |
| examples | CommandExampleDoc[] | no | Terminal examples. |
| examples[].label | string | no | Optional heading shown above the invocation. |
| examples[].cli | string | yes | A full terminal invocation, e.g. 'astryx search button --json'. |
| examples[].output | string | no | Optional sample output. |
| exitCodes | { code: number; when: string }[] | no | Documented exit codes. |
| exitCodes[].code | number | yes | The process exit code. |
| exitCodes[].when | string | yes | The condition that produces this exit code. |
| deprecated | string | no | Deprecation notice — a DEP-* id and the replacement. |
| related | string[] | no | Related command names. |
| notes | ReferenceContentBlock[] | no | Freeform prose/notes. |
js/** @type {import('@astryxdesign/cli/authoring').CommandDoc} */export const doc = {type: 'command',name: 'search',displayName: 'astryx search',summary: 'Find components, hooks, docs, and templates.',namespace: 'cli',fn: 'search',args: [{name: 'query', param: 'query', required: true}],options: [{flag: '--type <domain>', param: 'options.type', choices: ['component', 'hook', 'doc', 'template']},{flag: '--json', cliOnly: true, description: 'Emit the typed JSON envelope.'},],examples: [{label: 'Terminal', cli: 'astryx search button --json'}],exitCodes: [{code: 1, when: 'The --type value is not a known domain.'}],related: ['discover'],};
A command carries only CLI-surface facts. Behavior, parameters, returns, and thrown errors live in the FunctionDoc it points at via fn; the function does not know it has a CLI.
Bind an arg/option to a function param via param so it inherits that param's description. Use cliOnly: true for flags with no function param (e.g. --json); those must supply their own description.
EnumDoc
The doc-type for a closed vocabulary: a fixed set of literal values such as error codes or response-type discriminants. Colocated as a .doc.mjs next to the source of truth it documents.
Applies to: <enum>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'enum' | no | Doc-kind discriminant. Marks the file as an enum doc. |
| name | string | yes | URL-safe identifier, used as the docs slug within its namespace. Example: 'error-codes'. |
| displayName | string | yes | Human-readable title. Example: 'Error Codes'. |
| description | string | yes | One-line summary shown in listings. |
| namespace | string | no | Optional in the type, but every doc the CLI ships declares it. The group that reads this doc. The CLI's enums use 'cli/api', which the docs tree adopts by kind: each is the leaf cli/api/enums/<name>. Every enum doc the CLI ships declares one, and astryx doctor fails on one that is missing or that nothing reads. |
| aliases | string[] | no | Alternate slugs that also resolve to this doc. |
| members | EnumMemberDoc[] | yes | The enumerated members: one entry per literal value. |
| members[].value | string | yes | The literal value, e.g. 'ERR_UNKNOWN_TOPIC' | 'component.list'. |
| members[].description | string | yes | What the value means / when it occurs. |
| members[].deprecated | string | no | Deprecation reason, if deprecated. |
js/** @type {import('@astryxdesign/cli/authoring').EnumDoc} */export const doc = {type: 'enum',name: 'error-codes',displayName: 'Error Codes',namespace: 'cli',description: 'Stable error codes thrown by the CLI/API and surfaced in the JSON envelope.',members: [{value: 'ERR_UNKNOWN_TOPIC', description: 'The requested docs topic does not exist.'},{value: 'ERR_INVALID_ARGUMENT', description: 'A required argument was missing or malformed.'},{value: 'ERR_LEGACY', description: 'Old alias.', deprecated: 'Use ERR_INVALID_ARGUMENT.'},],};
An enum doc is the human-readable mirror of a closed vocabulary defined elsewhere in source (e.g. an ERROR_CODES map or a response-type union). Keep the members in sync with that source of truth.
Mark a value with deprecated (a migration hint) rather than deleting it, so old codes stay documented while readers are pointed at the replacement.
NamespaceDoc
Declares one level of the docs tree: named slots, and adoption rules for already-discovered docs. It never scans folders, lists its children, or copies child documents: a child names its parent with placement, or a namespace adopts a discovery group. astryx docs <route> lists each slot's children one level down. The CLI keeps its namespace docs in its docs tree directory; an integration ships its own in its docs directory, and they appear in astryx docs beside the CLI's.
Applies to: <namespace>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'namespace' | yes | Doc-kind discriminant. |
| name | string | yes | Stable provider-local identity. Moving the namespace does not change this value. |
| title | string | yes | Human-readable page title. |
| summary | string | yes | One-line summary used in listings and search results. |
| placement | DocPlacement | no | Optional canonical parent: {parent, slot?, order?}. parent names a namespace of the same package as namespace:<name>. An invalid placement withdraws the namespace with a diagnostic; it never falls back. |
| aliases | string[] | no | Prior names or routes the docs tree will keep resolving to this doc. Not read yet. |
| audience | 'public' | 'internal' | no | Bundle audience. Defaults to 'public'. Default: 'public'. |
| keywords | string[] | no | Search terms not already present in the title or summary. |
| slots | Record<string, NamespaceSlot> | yes | Named placement and collection targets. Each slot declares a title and accepted doc kinds; configured providers require an explicit extension slot. |
| adopts | NamespaceAdoptionRule[] | no | Provider-local rules that adopt otherwise-unplaced docs from a logical discovery group. They never scan a folder. |
| blocks | (ReferenceContentBlock | GraphContentBlock)[] | no | Ordered layout content for the namespace page. Graph-only workflow, collection, and reference blocks are available here without widening the stable ReferenceContentBlock union used by existing topic renderers. Not rendered yet: astryx docs <route> lists every slot and its children in order. |
js/** @type {import('@astryxdesign/cli/authoring').NamespaceDoc} */export const docs = {type: 'namespace',name: 'cli',title: 'Astryx CLI',summary: 'Commands, APIs, and integration authoring.',slots: {guides: {title: 'Guides', accepts: {kinds: ['namespace', 'generic']}},reference: {title: 'Reference',accepts: {kinds: ['namespace', 'command']},},},adopts: [{source: {group: 'cli-commands', kinds: ['command']},into: 'reference',}],};
The docs tree reads namespace docs from the CLI and from every configured integration. An integration's namespace doc is a top-level level of the tree, and its guides name it with placement; a doc can be placed only in a namespace of its own package. When two packages claim one route, the CLI's own docs win, then integrations in configured order, and astryx doctor names the loser.
Child docs request one canonical home with placement. Collections store and render stable references to those docs; they never create a second identity or parent.
| Guidance | Practices |
|---|---|
| Don't | Use a directory as implicit navigation. |
| Don't | Put JSX, HTML, ANSI, callbacks, or custom renderer code in a doc. |
| Don't | Use choice, callout, or checklist blocks before the full block-extension contract exists. |
ReferenceDoc
The doc-type for a reference/topic doc: tokens, principles, theming, patterns, accessibility, migration guides. Unlike ComponentDoc it is not tied to a component: drop a .doc.mjs in the docs directory and it shows up in astryx docs. Content is built from ordered sections of mixed content blocks.
Applies to: assets/docs/<topic>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'generic' | no | Doc-kind discriminant. Stays 'generic' (the reference/topic discriminant). Legacy export const docs = {...} docs omit it. |
| name | string | yes | URL-safe identifier, used as the CLI topic name. e.g. 'tokens', 'principles'. |
| title | string | yes | Human-readable title. e.g. 'All Tokens'. (Reference docs use title, not displayName.) |
| description | string | yes | One-line summary shown in topic listings. |
| category | string | no | Navigation category: 'guide' or 'foundations'. |
| keywords | string[] | no | Words a reader may search for that the title and sections do not use: a synonym, a task, or another library's name for the same thing. astryx search matches each as a keyword of the whole topic, so an exact one ranks the topic like its own title does. Example: ['dark mode', 'color scheme']. |
| replaces | string | no | Name of an existing topic this doc takes the place of. Authored by an integration that serves its own guide instead of the built-in one: on a doc of the same name it swaps the content, and on a doc of another name it also leaves the old name as an alias so astryx docs <old> still resolves. Exclusive with extends. Example: 'getting-started'. |
| extends | string | no | Name of an existing topic this doc merges onto, section by section: a section whose title matches one in the base replaces it, a section the base does not have is appended. The topic keeps the base's title and description. For correcting or adding to a topic rather than owning it. Exclusive with replaces. Example: 'theme'. |
| sections | ReferenceSection[] | yes | Ordered sections that make up the doc. Each becomes an h2 in full output and can be retrieved via astryx docs <topic> <section>. |
| sections[].id | string | no | Stable section anchor. New docs should set this instead of relying on a mutable title. |
| sections[].title | string | yes | Section title, e.g. "Spacing Tokens", "Light/Dark Mode". |
| sections[].category | string | no | Navigation category ('guide' | 'foundations'). Mirrors the parent doc's category so sections can be grouped independently. |
| sections[].content | (ReferenceContentBlock | ReferenceDocBlock)[] | yes | Ordered content blocks: prose, heading, code, table, list, token-ref, and reference. A reference block ({type: 'reference', target, projection?, presentation?}) includes another doc from its canonical source instead of a copy: a schema, command, function, or enum doc as astryx docs prints it, then the command that opens it. projection.fields keeps only those fields of a schema; presentation is full (the default), compact (no code blocks), or summary (only the doc's title, summary, and command). Any other doc shows as a summary. Graph-only workflow and collection blocks are available through GraphContentBlock on NamespaceDoc, without widening the stable ReferenceContentBlock union. Inside text, {@link <target>} links another doc by identity ([<provider>:]<kind>:<name>): the CLI prints the command that opens it, and astryx doctor warns on one that names no doc. |
| sections[].previewType | ReferenceTokenPreviewType | no | Preview type for token tables in this section. When set, the docsite renders a visual preview column from the token's computed value. Omit for non-token sections. |
| tokenCategory | string | no | Token category for foundational docs that map to a token section (e.g. 'color'). Lets the tokens overview link to this doc for detailed guidance. |
js/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */export const docs = {type: 'generic',name: 'spacing',title: 'Spacing',description: 'Spacing tokens for gap, margin, and padding.',category: 'foundations',tokenCategory: 'spacing',sections: [{title: 'Spacing Tokens',content: [{type: 'prose', text: 'Use spacing tokens instead of raw pixel values so layouts stay on the 4px grid.'},{type: 'table', headers: ['Token', 'Value'], rows: [['--spacing-4', '16px']]},],previewType: 'spacing-bar',},],};
A stamped generic doc without title, description or sections still loads, as older codemod output does; its title falls back to displayName or name. Without a description and sections it is not a usable topic, and astryx doctor reports it.
Each sections[].content is an ordered array of ReferenceContentBlock, the stable discriminated union of prose, heading, code, table, list, and token-ref, plus the reference block (ReferenceDocBlock). A read inlines a reference block the way it inlines a token-ref, so astryx docs, in text and in --json, returns only the stable block kinds. Docs-graph-only workflow and collection blocks are exported separately as GraphContentBlock and accepted by NamespaceDoc. choice, callout, and checklist remain invalid. ReferenceContentBlock is also reused by the notes field on SchemaDoc and CommandDoc. Inside text, {@link <target>} links another doc by identity ([<provider>:]<kind>:<name>): the CLI prints the command that opens it, and astryx doctor warns on one that names no doc.
A reference block includes content, where a link only points at it: astryx doctor integration docs fails when its target names no doc, when projection.fields names a field the schema does not have, or when the doc cannot take the projection or presentation it sets, and a read marks the missing content. A target without a provider names a doc of the package that wrote it, so an integration names the CLI's docs with the CLI's provider, as in @astryxdesign/cli:schema:integration.
js{title: 'Point the manifest at your folders',content: [{type: 'prose', text: 'Add these fields to astryx.integration.mjs.'},{type: 'reference',target: '@astryxdesign/cli:schema:integration',projection: {fields: ['components', 'docs']},},],}
tstype ReferenceContentBlock =| { type: 'prose'; text: string }| { type: 'heading'; level: 3 | 4 | 5 | 6; text: string }| { type: 'code'; lang: string; code: string; label?: string }| { type: 'table'; headers: string[]; rows: string[][] }| { type: 'list'; style: 'ordered' | 'unordered' | 'do' | 'dont'; items: string[] }| { type: 'token-ref'; topic: string; section: string };type ReferenceDocBlock = {type: 'reference';target: string;projection?: { fields?: string[]; sections?: string[] };presentation?: 'summary' | 'compact' | 'full';};type GraphContentBlock =| { type: 'workflow'; title?: string; steps: WorkflowStep[] }| { type: 'collection'; source: {slot: string}; presentation?: 'list' | 'cards' | 'compact'; whenEmpty?: 'show' | 'omit' }| ReferenceDocBlock;
A token-ref block (e.g. {type: 'token-ref', topic: 'tokens', section: 'Color Tokens'}) references a token table in another topic; the CLI resolves and inlines it at read time so the docsite can render live theme values.
A section may set previewType to render a visual preview column for token tables: one of 'swatch' | 'shadow-box' | 'radius-box' | 'spacing-bar' | 'size-bar' | 'border-line' | 'duration-bar' | 'easing-curve' | 'font-sample'.
SchemaDoc
The doc-type for documenting an authored/received OBJECT shape (astryx.config, a codemod payload, the doc-types themselves). Colocated as a .doc.mjs next to the schema it describes. Fields nest recursively, so a whole shape is one tree.
Applies to: <schema>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'schema' | no | Doc-kind discriminant. Marks the file as a schema doc. |
| name | string | yes | URL-safe identifier, used as the docs slug within its namespace. Example: 'config'. |
| displayName | string | yes | Human-readable title. Example: 'Astryx Config'. |
| description | string | yes | One-line summary shown in listings. |
| namespace | string | no | The group that reads this doc: 'authoring' for a file an author writes (a section of astryx docs authoring), or 'cli/api' for a shape the CLI returns (the docs tree adopts it by kind, as the leaf cli/api/schemas/<name>). Every schema doc the CLI ships declares one, and astryx doctor fails on one that is missing or that nothing reads. |
| aliases | string[] | no | Alternate slugs that also resolve to this doc (back-compat). |
| appliesTo | string | no | What this schema applies to, e.g. 'astryx.config.{ts,mjs,js}' | 'AstryxConfig'. |
| fields | SchemaFieldDoc[] | yes | The fields that make up the shape. Object-typed fields nest their members recursively via each field's own fields, so the whole shape is one tree. |
| fields[].name | string | yes | Field name, or a dotted path for a nested field (e.g. 'hooks.postCodemod'). |
| fields[].type | string | yes | TypeScript type signature as a string, e.g. 'string[]' | "'a' | 'b'". |
| fields[].description | string | yes | What the field is for, in 1-2 sentences. |
| fields[].required | boolean | no | True if the field must be provided. Omit (don't set false) if optional. |
| fields[].default | string | no | Default value as a string, if any. |
| fields[].example | string | no | A short inline example value. |
| fields[].deprecated | string | no | Deprecation reason, if the field is deprecated. |
| fields[].fields | SchemaFieldDoc[] | no | Nested object fields, for object-typed fields. Recursive. |
| examples | { label?: string; code: string }[] | no | Full example objects/snippets. |
| examples[].label | string | no | Optional heading shown above the snippet. |
| examples[].code | string | yes | The example source. |
| notes | ReferenceContentBlock[] | no | Freeform prose/notes rendered after the field table. Same block union as ReferenceDoc (prose, heading, code, table, list, token-ref). |
js/** @type {import('@astryxdesign/cli/authoring').SchemaDoc} */export const doc = {type: 'schema',name: 'integration',displayName: 'Astryx Integration',namespace: 'cli',description: 'The astryx.integration.* manifest that registers the components a package provides.',appliesTo: 'astryx.integration.{ts,mjs,js}',fields: [{name: 'name', type: 'string', description: 'Package name.', required: true},{name: 'components',type: '{ dir: string }',description: 'Where component sources live.',fields: [{name: 'components.dir', type: 'string', description: 'Glob root for Acme*.tsx files.', required: true},],},],};
SchemaFieldDoc is recursive: an object-typed field lists its members in its own fields, so an entire nested shape (including paths like hooks.postCodemod or experimental.xle.components) is documented as one tree.
Name nested fields with a dotted path from the root (e.g. hooks.postCodemod) so readers can see where each field sits in the shape.
| Guidance | Practices |
|---|---|
| Do | Set |
| Do | Keep |
| Don't | Set |
TemplateDoc
The doc-type for template metadata. A discriminated union of PageTemplateDoc (type: 'page') for full page templates and BlockTemplateDoc (type: 'block') for editable compositions. A block can stand alone or use exampleFor to attach to one component; isShowcase requires that component ownership.
Applies to: <Name>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'page' | 'block' | yes | Discriminant selecting the variant: 'page' for a full page template, 'block' for an editable composition that may be standalone or component-owned. |
| name | string | yes | Stable identifier for block templates; change displayName, not name, to edit their visible label. For page templates it is a human-readable label, while the existing template-directory/CLI slug owns the default registry path. |
| displayName | string | yes | Human-readable label for the gallery/CLI. Spaces out block names that mirror a PascalCase component ('ChatMessageMetadata' → 'Chat Message Metadata'). |
| registry | RegistryDocIdentity | no | Optional public registry identity. The converter derives a stable kebab-case slug from name (or the existing page-template slug); set slug only to override it, and keep prior relative paths in aliases after a published rename. |
| description | string | no | One-sentence description of what the template provides. |
| keywords | string[] | no | Search keywords for CLI discovery: the ideas, domains, and other names a builder might use for what the template serves (e.g. ['monitoring', 'uptime', 'on-call'] for a service-health dashboard). Lowercase. astryx search matches them as it matches the description and astryx build ranks page templates on them, so keep them out of description. Integration templates need @astryxdesign/cli 0.6.6 or later: earlier CLIs reject the field and drop that template, and CLIs before 0.6.4 also hide the package's doc topics. |
| replaces | string | no | Integration templates only: the exact id of the Core template this one replaces for unqualified lookup. Find it with astryx --json template --list --package @astryxdesign/core; the Core original stays selectable with --package @astryxdesign/core. A page replaces only a Core page and a block only a Core block. Needs @astryxdesign/cli 0.6.4 or later: earlier CLIs reject the field, drop that template, and hide the package's doc topics. |
| isReady | boolean | no | Whether the template is ready for use. false shows as '(WIP)' in the gallery and CLI. |
| scaffold | boolean | no | Scaffolding-only template (e.g. blank page): available via the CLI but hidden from browsable galleries. |
| category | TemplateCategory | no | Functional gallery category following a 'Group - Variant' convention (e.g. 'Dashboard - Analytics', 'Table - Basic', 'Form - Wizard'). The overview groups by the text before ' - '. |
| isHiddenFromOverview | boolean | no | Opt out of the Templates overview gallery while staying available via the CLI. Use for duplicate/experimental variants. Scaffold templates are hidden automatically. |
| exampleFor | string | no | Block templates only: optional component ownership. Set this when the block is specifically an example of one component. Omit it for a standalone composition. |
| alsoExampleFor | string[] | no | Block templates only: additional component/hook doc pages whose Examples section should include this block. |
| alsoShowcaseFor | string[] | no | Block templates only: additional doc pages whose hero showcase should reuse this block (secondary placements; does not change the primary showcase). |
| aspectRatio | number | no | Block templates only (required): width-to-height ratio for preview containers (e.g. 16/9, 1, 3/4). |
| scale | number | no | Block templates only: scale factor for the block preview. Default: 1. |
| componentsUsed | string[] | no | Block templates only: component names this block uses, for 'See also'/'Used in' cross-references (not primary attribution). |
| isShowcase | boolean | no | Block templates only: when true this block is the canonical hero showcase for its exampleFor component. Requires exampleFor. |
js/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */export default {type: 'page',name: 'Dashboard',displayName: 'Dashboard',description: 'An analytics dashboard with KPI cards and charts.',category: 'Dashboard - Analytics',isReady: true,};
js/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */export default {type: 'block',name: 'ButtonGroupExample',displayName: 'Button Group Example',description: 'A row of related buttons showing primary and secondary actions.',exampleFor: 'Button',aspectRatio: 16 / 9,componentsUsed: ['Button', 'HStack'],isShowcase: true,};
When it loads, a template may leave out displayName and aspectRatio and use its own category, as integration templates already published do; a block with no aspectRatio previews at 1. Write to the type anyway; it is the contract.
TemplateDoc is a discriminated union keyed by type. Set type: 'page' for a full page template. Set type: 'block' for an editable composition; add exampleFor only when one component owns the example.
- PageTemplateDoc (type: 'page'): a full page template;
namedoubles as its display value. - BlockTemplateDoc (type: 'block'): an editable composition.
exampleForis optional component ownership; omit it for a standalone block.isShowcaserequiresexampleFor.
category uses the shared TemplateCategory taxonomy: 'Group - Variant' strings (e.g. 'Table - Bulk Actions'). Not every value maps to an existing template; unused values are reserved so authors get autocomplete for the full taxonomy.
ThemeDoc
The strongly typed descriptor for one integration or CLI-bundled source theme.
Applies to: <theme-source-stem>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'theme' | yes | Doc-kind discriminant. |
| name | string | yes | Lower-kebab CLI theme identity. Must match the descriptor parent directory. Example: 'ocean'. |
| displayName | string | yes | Human-readable name shown by theme listings. Example: 'Ocean'. |
| description | string | yes | One-line summary shown by theme listings. |
| maintained | boolean | yes | Whether the owning package actively maintains this theme. |
js/** @type {import('@astryxdesign/cli/authoring').ThemeDoc} */export default {type: 'theme',name: 'ocean',displayName: 'Ocean',description: 'Cool blue surfaces with crisp contrast.',maintained: true,};
ThemeDoc is static metadata. Default-export one object with literal string and boolean fields; discovery validates it without executing the descriptor or theme source.
The descriptor and source share one stem. For oceanTheme.doc.mjs, discovery requires exactly one oceanTheme source module and a named oceanTheme runtime export.
The parent theme directory, including this descriptor, is the complete copy and pack boundary. Nested source, palettes, assets, and receipts need no second file list.