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}

FieldTypeRequiredDescription
providerIdstringnoStable logical provider ID. Omit to use package.json#name; set it to the prior package name only when an explicit rename must preserve artifact IDs. If two packages claim the same ID, the package being authored is used, otherwise the first-loaded one, and the CLI warns about the other. Example: '@acme/widgets'.
componentsstringnoRelative path to the components/docs root (resolved to absolute). Example: './src/components'.
templatesstringnoRelative path to the templates root (resolved to absolute). Example: './src/templates'.
codemodsstringnoRelative path to the codemods root (resolved to absolute). Example: './codemods'.
docsstringnoRelative path to the reference-docs (topics) root (resolved to absolute). Every {topic}.doc.{ts,mjs,js} under it is served by astryx docs beside the built-in topics; a topic may also declare replaces or extends to take the place of a built-in one or merge onto it. Example: './docs'.
themesstringnoRelative path to a source-theme root with one directory per theme slug. Each directory contains a source module and mandatory same-stem, strongly typed .doc.mjs descriptor. Installed themes appear in astryx theme list and can be copied with astryx theme add. Example: './themes'.
agentDocs{ append?: readonly string[] }noStatic package guidance appended to the end of the managed agent block. The CLI owns the section heading, package labels, bullets, target files, and writes. Example: { append: ['Run acme verify.'] }.
issuesUrlstringnoWhere to file issues/feedback for this integration. Example: 'https://github.com/acme/widgets/issues'.
Typical
js
export 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',
};

Provider identity defaults to package.json#name. During an explicit package rename, set providerId to the prior canonical package name so existing artifact IDs remain stable. 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}

FieldTypeRequiredDescription
integrationsstring[]noPackage names of Astryx integrations to load alongside core. Example: ['@acme/astryx-widgets'].
issuesUrlstringnoURL that "report an issue" affordances link to.
hooks{ postCodemod?: PostCodemodHook[] }noLifecycle hooks.
hooks.postCodemodPostCodemodHook[]noCommands 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) => voidnoRecord 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').
gapReportGapReportHandlernoHandle 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}); } }.
experimental{ xle?: { components?: Record<string, XleComponent> } }noUnstable features; may change without a breaking bump.
experimental.xle.componentsRecord<string, XleComponent>noCustom components the layout expander (XLE) may emit, keyed by tag.
Minimal
js
export default {
integrations: ['@acme/astryx-widgets'],
};
Send every command run somewhere of your own
js
export 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

FieldTypeRequiredDescription
schemaVersionDebugSchemaVersionyesVersion of the recorded shape: 1, 2, or 3. A field that changes meaning gets a new version, so switch on this.
idstringyesUnique per run. Deduplicate on it.
startedAtstringyesWhen the run started, as ISO 8601.
endedAtstringyesWhen the run ended, as ISO 8601.
durationMsnumberyesHow long the run took, in milliseconds.
commandstringyesThe full command name. Empty when astryx ran without a command. Example: 'theme build'.
commandPathstring[]yesThe command name split into segments. Example: ['theme', 'build'].
argvstring[]yesThe arguments after the binary, scrubbed.
argsRecord<string, unknown>yesPositional arguments, keyed by their declared names.
optionsRecord<string, unknown>yesThe command options, as the CLI resolved them.
optionSourcesRecord<string, DebugOptionSource>yesWhere each option value came from: 'cli', 'default', 'env', 'config', or 'implied'. The keys match options.
globalOptionsRecord<string, unknown>yesThe root flags, such as --json, --detail, and --lang.
outcomeDebugOutcomeyesHow the run ended: 'ok', 'error', 'parse-error', 'fatal', 'rejected', or 'incomplete'.
exitCodenumberyesThe process exit code. When a signal ended the run, 128 plus the signal number.
signalstringyesThe signal that ended the run, such as 'SIGINT'. Null otherwise.
errorDebugEventErroryesThe failure: name, message, code, and stack. Branch on code, never on message. Null when nothing failed.
outputDebugEventOutputyesWhat 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.
envDebugEventEnvyesRuntime 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.
projectDebugEventProjectyesThe project the command ran in, as flags and counts. It never holds package names.
redactedbooleanyesWhether 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.
Keep a local log of the runs that failed
js
import {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.*

FieldTypeRequiredDescription
audience'internal' | 'public'yesWhether 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>yesGets 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.
reportGapReportyesThe report. Each handler gets its own copy.
report.schemaVersion1yesVersion of the report shape.
report.componentstringyesThe component the gap is about.
report.categoryGapReportCategoryyesThe kind of gap: 'missing_component', 'missing_variant', 'layout_gap', 'styling_gap', 'a11y_gap', 'api_friction', 'docs_gap', or 'other'.
report.categoryLabelstringyesA readable name for the category.
report.intentionstringyesWhat the caller was trying to do.
report.detailstringnoMore detail, when the caller gave some.
report.sourceDebugInvocationSourceyesWho sent the report: 'human', 'ai', 'automation', or 'unknown'.
report.timestampstringyesWhen the report was sent, as ISO 8601.
report.targetGapReportTargetyesThe package the report is about.
report.target.packagestringyesThe package name.
report.target.versionstringyesThe installed version. Null when it is not known.
report.target.issuesUrlstringyesWhere the package takes issues. Null when it does not say.
contextGapReportHandlerContextyesThe context of this call.
context.signalAbortSignalyesAborted when the handler runs past 30 seconds.
A project handler in astryx.config
js
export default {
gapReport: {
audience: 'internal',
async handle(report, {signal}) {
const url = await fileIssue(report, {signal});
return {status: 'filed', url};
},
},
};
Receipt fieldTypeRequiredDescription
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.
urlstringnoAn http or https URL. Required when status is routed_only.
messagestringnoA 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.

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

FieldTypeRequiredDescription
titlestringyesShort, human-readable title shown in upgrade output. Example: 'Rename Button prop kind to variant'.
descriptionstringnoOptional longer description.
isOptionalbooleannoWhen true, the codemod runs only when explicitly requested. Default: false.
fileExtensionsstring[]noFile 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 | undefinedyesThe transform. Return the new source to rewrite the file, or null/undefined to leave it unchanged.
fileAstryxCodemodFilenoThe source file presented to the transform.
file.pathstringyesAbsolute path to the file being transformed.
file.sourcestringyesThe current source contents of the file.
apiAstryxCodemodApinoHelpers and context passed as the second argument.
api.jscodeshiftunknownyesA jscodeshift instance configured with a parser for the file.
api.stats(...args: unknown[]) => voidyesReport a statistic (no-op-friendly; provided for jscodeshift parity).
api.report(...args: unknown[]) => voidyesReport progress (no-op-friendly; provided for jscodeshift parity).
type'code' | 'config'yesDiscriminant for the file-transforming variant. Use 'config' for a codemod that rewrites astryx.config.* instead (see notes). Example: 'code'.
Code codemod (type: code)
js
export 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();
},
};
Config codemod (type: config)
js
export 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. providerId is in use today: an integration manifest may declare it, and Doctor and every command report two packages that claim one. The artifact, doc, instance, and compiler-input identities are defined for the docs graph, which is not built yet, and no command reads them.

Applies to: Integration manifests and provider conflicts today; the docs graph once it ships

FieldTypeRequiredDescription
ProviderIdstringyesCanonical logical provider ID. It defaults to the lowercase npm package name; Core uses the same model without publishing an integration manifest.
ArtifactIdstringyesVersioned serialization of provider ID, contribution kind, and stable artifact name. Every segment uses RFC 3986 escaping.
DocIdArtifactIdyesArtifact ID restricted to one authored doc kind. Navigation changes do not change it.
ProviderInstance{ id; providerId; packageName; packageVersion; sourceDigest }yesOne 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 }yesNormalized 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.

Authored doc graph fields

Placement, compatibility aliases, and audience: fields every authored doc kind declares for the docs graph. The docs graph is not built yet, so nothing reads them: a reference topic that sets one fails to load, and other doc kinds accept them and ignore them.

Applies to: Every supported .doc.mjs object

FieldTypeRequiredDescription
placementDocPlacementnoRequests one canonical parent in the docs graph. Not read yet: a topic that sets it fails to load.
placement.parentstringyesStable reference to the requested parent namespace.
placement.slotstringnoNamed slot owned by the parent namespace.
placement.ordernumbernoInteger sibling order within the slot.
aliasesstring[]noPrior names or routes the docs graph will keep resolving to this doc, without creating another identity. Not read yet: a topic that sets it fails to load.
audience'public' | 'internal'noWhich docs bundle includes this doc ('public' when omitted). Not read yet: 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

FieldTypeRequiredDescription
type'component'noDoc-kind discriminant for the stamped default-export format. Optional: legacy export const docs = {...} docs omit it and the parser falls back to shape-sniffing.
namestringyesStable 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.
displayNamestringyesHuman-readable display name with spaces between words ('AppShell' → 'App Shell'). Drives the docsite gallery and sidebar label.
registryRegistryDocIdentitynoOptional 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.
importstringnoExact 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.
keywordsstring[]noSearch 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.
hiddenComponentsstring[]noSub-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.
hiddenbooleannoHide 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.
groupstringnoOptional 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'noOverview-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.
isHiddenFromOverviewbooleannoExclude 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[] }noTheming configuration: the stable selector surface (xds-* classes + data-attribute reflections) that themes target via @scope selectors in defineTheme.
theming.containerbooleannoWhen true, container padding props are mapped to container tokens by the theme pipeline instead of emitting raw CSS.
theming.targetsComponentThemingTarget[]yesSelector targets rendered by this component. Each entry corresponds to a themeProps() call in the source.
theming.varsComponentThemingVar[]noCSS custom properties exposed for theming.
theming.derivedComponentThemingDerivedVar[]noMaps standard CSS properties to internal vars for theme-pipeline expansion. Ordered by priority: earlier entries emit first.
usageUsageDocnoComponent 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.descriptionstringyesWhat the component is and when to use it, in 2-3 short sentences.
usage.bestPracticesComponentBestPractice[]no3-4 do/don't design-guidance items ({guidance: boolean, description: string}). Never start the description with 'Do' or 'Don't'.
usage.accessibilityComponentAccessibilityRequirement[]noComponent-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.accessibilityThemeCoverageComponentAccessibilityThemeCoverage[]noVerified 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.anatomyComponentAnatomyElement[]noStructural/visual parts in reading order ({name, required, description}).
examplesComponentExampleDoc[]noShort code examples ({label?, code}) rendered by the CLI after the props table.
playgroundComponentPlaygroundConfignoInteractive-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.
propsComponentPropDoc[]noSingleComponentDoc 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)[]noMultiComponentDoc 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.
subComponentOfstringnoSubComponentDoc 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.
descriptionstringnoSubComponentDoc 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.
SingleComponentDoc (props on the doc)
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'},
],
};
MultiComponentDoc (a components array)
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 for Switch, Badge, Spinner, TextInput.
  • MultiComponentDoc: a directory exporting several components/hooks; list them in components (inline ComponentEntry or name-only ComponentRef). Use for Table, Dialog, TabList.
  • SubComponentDoc: a single sub-component in its own {Name}.doc.mjs inside the parent directory; set subComponentOf to the parent name. It inherits family fields and may omit usage.
SubComponentDoc
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

FieldTypeRequiredDescription
type'function'noDoc-kind discriminant (shared with FunctionDoc). Legacy export const docs = {...} docs omit it.
namestringyesStable 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.
displayNamestringyesHuman-readable display name. Hooks read better as the raw identifier ('useMediaQuery') than spaced, so keep the identifier verbatim.
registryRegistryDocIdentitynoOptional 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.
groupstringnoOptional group for sidebar/docs organization; same as ComponentDoc.group.
keywordsstring[]noSearch keywords for CLI discovery.
paramsHookParamDoc[]yesHook parameters or options-object fields.
params[].namestringyesParameter or option field name.
params[].typestringyesTypeScript type signature as a string.
params[].descriptionstringyesWhat this parameter does. 1-2 sentences.
params[].defaultstringnoDefault value as a string, if optional with a default.
params[].requiredbooleannoTrue if required. Omit if optional.
returnsHookReturnDoc[]yesReturn value documentation. For object returns, list each field; for primitive returns, use a single entry.
returns[].namestringyesField name on the returned object, or 'value' for primitive returns.
returns[].typestringyesTypeScript type.
returns[].descriptionstringyesWhat this return value provides.
usageUsageDocyesUsage documentation: description and best practices.
usage.descriptionstringyesWhat the hook is and when to use it, in 2-3 short sentences.
usage.bestPracticesComponentBestPractice[]no3-4 do/don't design-guidance items ({guidance: boolean, description: string}).
usage.accessibilityComponentAccessibilityRequirement[]noAccessibility requirements specific to using the hook ({name, description}).
usage.anatomyComponentAnatomyElement[]noStructural/visual anatomy elements (rarely used for hooks).
relatedComponentsstring[]noComponent names this hook is commonly used with. Enables cross-referencing (e.g. astryx hook useToast links back to Toast).
relatedHooksstring[]noOther hook names this hook is commonly used with.
importPathstringnoImport path, e.g. '@astryxdesign/core/hooks' or '@astryxdesign/core/Toast'.
categorystringnoCategory for grouping in listings.
A standalone hook doc
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

FieldTypeRequiredDescription
type'function'noDoc-kind discriminant (shared with hooks).
namestringyesExport name, e.g. 'search' | 'useMediaQuery'.
displayNamestringyesHuman-readable display name, e.g. 'search()'.
kind'hook' | 'api'noWhich flavor; drives docsite sectioning; inferred from importPath if omitted.
summarystringnoOne-line summary.
descriptionstringnoLonger description.
namespacestringnoThe astryx docs topic that reads this doc. The CLI's API functions use 'cli/api' (the section api-<name> of the cli topic). Every function doc the CLI ships declares one, and astryx doctor fails on one that is missing or that no topic reads.
aliasesstring[]noAlternate slugs that also resolve to this doc.
keywordsstring[]noSearch keywords for discovery.
importPathstringnoImport path, e.g. '@astryxdesign/cli/api' | '@astryxdesign/core/hooks'.
signaturestringnoFull signature as a string, e.g. 'search(query, options?): Promise<SearchResponse>'.
paramsHookParamDoc[]yesParameters / options-object fields.
params[].namestringyesParameter or option field name.
params[].typestringyesTypeScript type signature as a string.
params[].descriptionstringyesWhat this parameter does. 1-2 sentences.
params[].defaultstringnoDefault value as a string, if optional with a default.
params[].requiredbooleannoTrue if required. Omit if optional.
returnsFunctionReturnDoc[]yesReturn 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[].namestringnoField name (hooks); omit for API envelope entries.
returns[].typestringyesTS type (hooks) or response-type discriminant (API), as a string.
returns[].descriptionstringyesWhat this return value / envelope entry provides.
throwsFunctionThrowsDoc[]noErrors the function throws (API functions), keyed to ERROR_CODES.
throws[].codestringyesThe ERROR_CODES member thrown.
throws[].whenstringyesThe condition under which it is thrown.
examplesFunctionExampleDoc[]noUsage examples.
examples[].labelstringnoOptional heading shown above the snippet.
examples[].codestringyesLanguage-level usage, e.g. "await search('button')".
examples[].resultstringnoOptional sample result.
usageUsageDocnoUsage documentation (hooks): description, best practices, accessibility requirements, and anatomy. Same shape as HookDoc.usage.
commandstringnoThe CLI command that wraps this function, e.g. 'search'.
relatedstring[]noRelated function/command names.
relatedComponentsstring[]noComponent names this is commonly used with (hooks).
relatedHooksstring[]noOther hook names this is commonly used with (hooks).
categorystringnoCategory for grouping in listings.
An API function doc (envelope return, no field name)
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

FieldTypeRequiredDescription
type'command'noDoc-kind discriminant. Marks the file as a command doc.
namestringyesCommand path, e.g. 'search' | 'theme build'.
displayNamestringyesHuman-readable display name, e.g. 'astryx search'.
summarystringyesOne-line summary → Commander .description() + the docs listing.
descriptionstringnoLonger help body / when-to-use.
namespacestringnoThe astryx docs topic that reads this doc. The CLI's commands use 'cli/commands' (the section commands-<name> of the cli topic). Every command doc the CLI ships declares one, and astryx doctor fails on one that is missing or that no topic reads.
aliasesstring[]noAlternate slugs that also resolve to this doc.
fnstringnoName of the FunctionDoc (and @astryxdesign/cli/api export) this command wraps. Example: 'search'.
argsCommandArgDoc[]noPositional arguments.
args[].namestringyesArgument name.
args[].paramstringnoFunctionDoc param this arg binds to (inherits its description).
args[].descriptionstringnoOverride description (else inherited from the referenced param).
args[].requiredbooleannoWhether the positional argument must be supplied.
args[].variadicbooleannoWhether the argument collects a variable number of values.
optionsCommandOptionDoc[]noFlags/options.
options[].flagstringyesCommander flag spec, e.g. '-l, --limit <n>' | '--json'.
options[].paramstringnoFunctionDoc param this flag maps to (inherits its description).
options[].descriptionstringnoOverride/explicit description (required when cliOnly).
options[].choicesstring[]noAllowed values for the flag.
options[].defaultstring | boolean | string[]noDefault value: a string, a boolean, or a list of strings.
options[].cliOnlybooleannoTrue for CLI-only flags with no function param (e.g. --json).
subcommandsstring[]noSubcommand names (for command groups like theme / layout).
examplesCommandExampleDoc[]noTerminal examples.
examples[].labelstringnoOptional heading shown above the invocation.
examples[].clistringyesA full terminal invocation, e.g. 'astryx search button --json'.
examples[].outputstringnoOptional sample output.
exitCodes{ code: number; when: string }[]noDocumented exit codes.
exitCodes[].codenumberyesThe process exit code.
exitCodes[].whenstringyesThe condition that produces this exit code.
relatedstring[]noRelated command names.
notesReferenceContentBlock[]noFreeform prose/notes.
A command that wraps the search() function
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

FieldTypeRequiredDescription
type'enum'noDoc-kind discriminant. Marks the file as an enum doc.
namestringyesURL-safe identifier, used as the docs slug within its namespace. Example: 'error-codes'.
displayNamestringyesHuman-readable title. Example: 'Error Codes'.
descriptionstringyesOne-line summary shown in listings.
namespacestringnoThe astryx docs topic that reads this doc. The CLI's enums use 'cli/api' (the section api-<name> of the cli topic). Every enum doc the CLI ships declares one, and astryx doctor fails on one that is missing or that no topic reads.
aliasesstring[]noAlternate slugs that also resolve to this doc.
membersEnumMemberDoc[]yesThe enumerated members: one entry per literal value.
members[].valuestringyesThe literal value, e.g. 'ERR_UNKNOWN_TOPIC' | 'component.list'.
members[].descriptionstringyesWhat the value means / when it occurs.
members[].deprecatedstringnoDeprecation reason, if deprecated.
Error-codes enum doc
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 named navigation slots and renderer-neutral layout blocks for already-discovered docs. It never scans folders or copies child documents. Not loaded yet: only the docs graph reads namespace docs, and it is not built, so keep them out of an integration's docs directory for now.

Applies to: <namespace>.doc.mjs

FieldTypeRequiredDescription
type'namespace'yesDoc-kind discriminant.
namestringyesStable provider-local identity. Moving the namespace does not change this value.
titlestringyesHuman-readable page title.
summarystringyesOne-line summary used in listings and search results.
placementDocPlacementnoOptional canonical parent request: {parent, slot?, order?}. Invalid explicit placement will fail compilation instead of falling back.
aliasesstring[]noPrior names or routes the docs graph will keep resolving to this doc.
audience'public' | 'internal'noBundle audience. Defaults to 'public'. Default: 'public'.
keywordsstring[]noSearch terms not already present in the title or summary.
slotsRecord<string, NamespaceSlot>yesNamed placement and collection targets. Each slot declares a title and accepted doc kinds; configured providers require an explicit extension slot.
adoptsNamespaceAdoptionRule[]noProvider-local rules that adopt otherwise-unplaced docs from a logical discovery group. They never scan a folder.
blocksReferenceContentBlock[]noOrdered layout content. V1 adds only workflow, collection, and reference to the existing prose, heading, code, table, list, and token-ref blocks.
A CLI namespace with one adopted source group
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',
}],
blocks: [
{type: 'collection', source: {slot: 'guides'}, presentation: 'cards'},
{type: 'collection', source: {slot: 'reference'}, presentation: 'compact'},
],
};

Namespace docs are not loaded yet. Only the docs graph reads them, and it is not built. A namespace doc in an integration's docs directory fails to load as a topic, and with it every topic that package contributes, until the file is removed.

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.

GuidancePractices
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

FieldTypeRequiredDescription
type'generic'noDoc-kind discriminant. Stays 'generic' (the reference/topic discriminant). Legacy export const docs = {...} docs omit it.
namestringyesURL-safe identifier, used as the CLI topic name. e.g. 'tokens', 'principles'.
titlestringyesHuman-readable title. e.g. 'All Tokens'. (Reference docs use title, not displayName.)
descriptionstringyesOne-line summary shown in topic listings.
categorystringnoNavigation category: 'guide' or 'foundations'.
replacesstringnoName 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'.
extendsstringnoName 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'.
sectionsReferenceSection[]yesOrdered sections that make up the doc. Each becomes an h2 in full output and can be retrieved via astryx docs <topic> <section>.
sections[].idstringnoStable section anchor. New docs should set this instead of relying on a mutable title.
sections[].titlestringyesSection title, e.g. "Spacing Tokens", "Light/Dark Mode".
sections[].categorystringnoNavigation category ('guide' | 'foundations'). Mirrors the parent doc's category so sections can be grouped independently.
sections[].contentReferenceContentBlock[]yesOrdered content blocks: prose, heading, code, table, list, and token-ref. workflow, collection, and reference are declared for the docs graph and parse, but a topic that uses one fails to load until the docs graph ships.
sections[].previewTypeReferenceTokenPreviewTypenoPreview 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.
tokenCategorystringnoToken category for foundational docs that map to a token section (e.g. 'color'). Lets the tokens overview link to this doc for detailed guidance.
A reference doc with one section
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, a discriminated union. workflow, collection, and reference are declared for the docs graph: they parse, but topic loading rejects them until the docs graph ships. choice, callout, and checklist remain invalid. The same union is reused by the notes field on SchemaDoc and CommandDoc.

ReferenceContentBlock union
ts
type 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: 'workflow'; title?: string; steps: WorkflowStep[] }
| { type: 'collection'; source: {slot: string}; presentation?: 'list' | 'cards' | 'compact'; whenEmpty?: 'show' | 'omit' }
| { type: 'reference'; target: string; projection?: {fields?: string[]; sections?: string[]} };

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

FieldTypeRequiredDescription
type'schema'noDoc-kind discriminant. Marks the file as a schema doc.
namestringyesURL-safe identifier, used as the docs slug within its namespace. Example: 'config'.
displayNamestringyesHuman-readable title. Example: 'Astryx Config'.
descriptionstringyesOne-line summary shown in listings.
namespacestringnoThe astryx docs topic 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 section api-<name> of the cli topic). Every schema doc the CLI ships declares one, and astryx doctor fails on one that is missing or that no topic reads.
aliasesstring[]noAlternate slugs that also resolve to this doc (back-compat).
appliesTostringnoWhat this schema applies to, e.g. 'astryx.config.{ts,mjs,js}' | 'AstryxConfig'.
fieldsSchemaFieldDoc[]yesThe 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[].namestringyesField name, or a dotted path for a nested field (e.g. 'hooks.postCodemod').
fields[].typestringyesTypeScript type signature as a string, e.g. 'string[]' | "'a' | 'b'".
fields[].descriptionstringyesWhat the field is for, in 1-2 sentences.
fields[].requiredbooleannoTrue if the field must be provided. Omit (don't set false) if optional.
fields[].defaultstringnoDefault value as a string, if any.
fields[].examplestringnoA short inline example value.
fields[].deprecatedstringnoDeprecation reason, if the field is deprecated.
fields[].fieldsSchemaFieldDoc[]noNested object fields, for object-typed fields. Recursive.
examples{ label?: string; code: string }[]noFull example objects/snippets.
examples[].labelstringnoOptional heading shown above the snippet.
examples[].codestringyesThe example source.
notesReferenceContentBlock[]noFreeform prose/notes rendered after the field table. Same block union as ReferenceDoc (prose, heading, code, table, list, token-ref).
A small schema doc with a nested object field
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 XDS*.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.

GuidancePractices
Do

Set required: true only for mandatory fields.

Do

Keep type close to the real TS type; use single quotes for string-literal unions.

Don't

Set required: false for optional fields; omit required entirely instead.

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

FieldTypeRequiredDescription
type'page' | 'block'yesDiscriminant selecting the variant: 'page' for a full page template, 'block' for an editable composition that may be standalone or component-owned.
namestringyesStable 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.
displayNamestringyesHuman-readable label for the gallery/CLI. Spaces out block names that mirror a PascalCase component ('ChatMessageMetadata' → 'Chat Message Metadata').
registryRegistryDocIdentitynoOptional 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.
descriptionstringnoOne-sentence description of what the template provides.
isReadybooleannoWhether the template is ready for use. false shows as '(WIP)' in the gallery and CLI.
scaffoldbooleannoScaffolding-only template (e.g. blank page): available via the CLI but hidden from browsable galleries.
categoryTemplateCategorynoFunctional gallery category following a 'Group - Variant' convention (e.g. 'Dashboard - Analytics', 'Table - Basic', 'Form - Wizard'). The overview groups by the text before ' - '.
isHiddenFromOverviewbooleannoOpt out of the Templates overview gallery while staying available via the CLI. Use for duplicate/experimental variants. Scaffold templates are hidden automatically.
exampleForstringnoBlock templates only: optional component ownership. Set this when the block is specifically an example of one component. Omit it for a standalone composition.
alsoExampleForstring[]noBlock templates only: additional component/hook doc pages whose Examples section should include this block.
alsoShowcaseForstring[]noBlock templates only: additional doc pages whose hero showcase should reuse this block (secondary placements; does not change the primary showcase).
aspectRationumbernoBlock templates only (required): width-to-height ratio for preview containers (e.g. 16/9, 1, 3/4).
scalenumbernoBlock templates only: scale factor for the block preview. Default: 1.
componentsUsedstring[]noBlock templates only: component names this block uses, for 'See also'/'Used in' cross-references (not primary attribution).
isShowcasebooleannoBlock templates only: when true this block is the canonical hero showcase for its exampleFor component. Requires exampleFor.
Page template
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,
};
Block template (component example)
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; name doubles as its display value.
  • BlockTemplateDoc (type: 'block'): an editable composition. exampleFor is optional component ownership; omit it for a standalone block. isShowcase requires exampleFor.

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

FieldTypeRequiredDescription
type'theme'yesDoc-kind discriminant.
namestringyesLower-kebab CLI theme identity. Must match the descriptor parent directory. Example: 'ocean'.
displayNamestringyesHuman-readable name shown by theme listings. Example: 'Ocean'.
descriptionstringyesOne-line summary shown by theme listings.
maintainedbooleanyesWhether the owning package actively maintains this theme.
A theme descriptor beside oceanTheme.ts
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.