Authoring
Build an npm package that adds components, templates, themes, docs, and codemods to Astryx apps, and look up every field of every file it holds.Overview
In global mode: users can install your integration for their computer, beside the global CLI. It then applies in folders outside any project, and inside a project only if the project installs it. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
An integration is a way to share things with Astryx users: a package you own and maintain, built on a framework Astryx gives you. Publish one, many, or any mix, and people install it in one step. It works with the Astryx CLI alongside Core. See each kind under npx @astryxdesign/cli docs authoring/building-blocks.
For example, ship a carousel as an integration. Any app that installs it can find it with npx @astryxdesign/cli search carousel, and the CLI uses it like any Astryx Core component.
An integration can also replace a built-in template or doc. To start, install @astryxdesign/cli and open npx @astryxdesign/cli docs authoring/quick-start.
Every field of every file you write for an integration is in npx @astryxdesign/cli docs authoring/field-reference.
Every integration runs npx @astryxdesign/cli integration check --strict in its own build: it is required, not optional, so a doc, prop, or link that stops matching the code fails your build instead of your users' apps. See npx @astryxdesign/cli docs authoring/ship/checks.
Quick start
Make the package
An integration starts as a normal npm package. Make a folder for it, create a package.json inside that folder, give the package a name, make it an ES module, then install the Astryx CLI, Core, and TypeScript for development.
bashmkdir acme-widgets && cd acme-widgetsnpm init -ynpm pkg set name=@acme/astryx-widgets type=modulenpm pkg set 'exports={}' --jsonnpm install -D @astryxdesign/cli @astryxdesign/core typescript@6
- Set
"type": "module": the.jsfiles the CLI generates, such as a built theme, are ES modules, and Node fails to load them in a package marked"type": "commonjs", whichnpm init -ywrites on npm 11. - Start with
"exports": {}: each component and template you add then writes the public import thatintegration verifyresolves. - Run the CLI as
npx @astryxdesign/cli, which runs the@astryxdesign/cliyou installed as a devDependency. - Component commands read Core, so they need
@astryxdesign/coreinstalled. - The checks compare your docs with your code through TypeScript's JavaScript compiler API, which
typescript5.0 through 6 has. TypeScript 7 does not ship that API: on TypeScript 7, also install@typescript/typescript6, its TypeScript 6 API package, and the checks use it.
Add your first integration item
An integration can ship several kinds of items. Start with a component named AcmeCarousel.
bashnpx @astryxdesign/cli integration add component AcmeCarousel
textcomponent contribution added[ok] AcmeCarouselDeclare component root ./components in astryx.integration.mjs.- components/AcmeCarousel.doc.mjs- components/AcmeCarousel.tsx- package.json- astryx.integration.mjs
integration add creates the component source, its .doc.mjs, the package export, and astryx.integration.mjs. Update the generated doc so the CLI understands the component, then write the component itself.
Read it back the way an app will.
bashnpx @astryxdesign/cli component AcmeCarousel
You now have an integration package with one exported component.
Check it in every build (required)
Run integration check --strict in every build (npx @astryxdesign/cli docs authoring/ship/checks): a doc, prop, template, or link that drifts from the code then fails your build, not your users' apps. Set it up now, as the typecheck script, and run it.
bashnpm pkg set scripts.typecheck="astryx integration check --strict"npm run typecheck
text[ok] 0 errors, 0 warnings, 0 skipped. Every check passed.
- Run
typecheckin CI and before every publish. When you add atsconfig.json, puttsc --noEmit &&in front of the check so the same script checks your types. - What each check covers, and what
--strictadds:npx @astryxdesign/cli docs authoring/ship/checks.
Verify and pack the package
Run integration verify to check that an app would see the component, then make the .tgz file you install next. --pack-destination .. writes it beside the package folder, so the next pack does not ship it.
bashnpx @astryxdesign/cli integration verifynpm pack --pack-destination ..
textIntegration package ready[ok] @acme/astryx-widgets@1.0.04 packed files; 3/3 required files present.
integration verify installs the packed package in a temporary app and confirms the component resolves through its public import there (npx @astryxdesign/cli docs authoring/ship/checks). It publishes nothing and leaves no .tgz file, so npm pack writes ../acme-astryx-widgets-1.0.0.tgz for the next step.
Use it in an app
Install Core, the CLI, and your .tgz file in a new app. The app loads your package because it is a dependency, with no config.
bashcd ..mkdir my-app && cd my-appnpm init -ynpm install @astryxdesign/core @astryxdesign/cli ../acme-astryx-widgets-1.0.0.tgznpx @astryxdesign/cli component AcmeCarousel
text**Import:** `import {AcmeCarousel} from '@acme/astryx-widgets/components/AcmeCarousel';`
The next sections explain each package field and the integration file. To build the real component, continue with npx @astryxdesign/cli docs authoring/building-blocks/components.
Fill in package.json
An integration is an ordinary npm package. These six fields control how it is named, developed, checked, and published.
peerDependencies are packages the app supplies. devDependencies are packages you use while building the integration.
| Field | Set it to | Why |
|---|---|---|
name | Your package name, such as @acme/astryx-widgets | Each add writes it into the import of the doc it generates. Rename before you add, or update each import after. |
version | The release you publish, such as 1.0.0 | Apps see it; astryx.integration.mjs never repeats it. |
exports | Start with {} | Each component and template add writes its public import here, and integration verify resolves it. |
files | Optional: the paths to publish | Keeps private files out. When the list exists, each add appends its root and astryx.integration.mjs. |
peerDependencies | @astryxdesign/core when your code imports it; @astryxdesign/cli, optional, when you ship a feature that needs a newer CLI (npx @astryxdesign/cli docs authoring/ship/versioning) | The app supplies these packages for your integration. |
devDependencies | @astryxdesign/cli, @astryxdesign/core, and typescript | Lets you run the CLI, build components, and run the checks while working on the integration. |
Peer ranges and releases are covered in npx @astryxdesign/cli docs authoring/ship/versioning; files and publishing in npx @astryxdesign/cli docs authoring/ship/publishing. Add "typecheck": "tsc --noEmit && astryx integration check --strict" to scripts so each run checks your types and that your docs, props, and links still match the code. It needs typescript and a tsconfig.json; see npx @astryxdesign/cli docs authoring/ship/checks.
The integration file
The first integration add creates astryx.integration.mjs, and each later add updates it. You do not need to write or edit this file during the quick start.
Files in the package
package.jsondefines the npm package, including its name, version, dependencies, published files, and exports.astryx.integration.mjstells the CLI where the package keeps each kind of integration item.- The integration item files live under those roots. For example, a component can have
AcmeWidget.tsxfor its source andAcmeWidget.doc.mjsfor its documentation.
Fields in the integration file
javascript/** @type {import('@astryxdesign/cli/authoring').AstryxIntegration} */export default {components: './components',};
The file tells the CLI where this package keeps its integration items. Edit it only when you want a custom root or another optional setting. Paths are relative to package.json, and later adds keep a custom path you already set.
Every field is in npx @astryxdesign/cli docs authoring integration.
Use the CLI
You can edit the package files by hand, but the CLI handles the normal workflow. These links always open the canonical command docs.
astryx integration add: Add one working contribution to an integration package. Read it with npx @astryxdesign/cli docs cli/commands/integration-add.
astryx integration verify: Check the package the way npm will publish it, before you publish. Read it with npx @astryxdesign/cli docs cli/commands/integration-verify.
astryx doctor integration: Check an integration package while authoring it. Read it with npx @astryxdesign/cli docs cli/commands/doctor-integration.
In global mode
In global mode: users can also install your package for their computer, beside the global CLI (npm i -g @astryxdesign/cli your-package). It then applies in folders outside any project, and inside a project only if the project installs it. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Building Blocks
In global mode: every kind your package adds also applies outside projects when users install it for their computer, beside the global CLI; inside a project, only if the project installs it. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
These are everything you can add to an integration. Mix them however you like. You are not limited to any one thing.
- One component, or a whole library of them.
- Components together with themes and templates.
- Only docs, if that is all you need.
- Agent guidance, so the AI agents in an app follow your library.
- Codemods that update app code across your breaking changes.
Each kind below is its own guide. Open one to learn, in depth, how to add it and how to keep it healthy as your package grows.
Components
In global mode: components from an integration installed for the computer show up outside projects; inside a project, only components from integrations the project installs. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Pick a component name
Choose a unique PascalCase name, such as AcmeCarousel. The name is the component's public export and CLI identity, so keep it stable. Use displayName when you only want to change the label people read.
- Prefer a name that describes the component's purpose in your product.
- Avoid a Core component name unless you are deliberately replacing that component.
- If replacement is truly required, read
npx @astryxdesign/cli docs authoring/building-blocks/components/replace-a-core-component.
Run the add command
Run integration add component in your package with the name. It writes the component, its doc file, and the public import apps use.
bashnpx @astryxdesign/cli integration add component AcmeCarousel
textcomponent contribution added[ok] AcmeCarouselDeclare component root ./components in astryx.integration.mjs.- components/AcmeCarousel.doc.mjs- components/AcmeCarousel.tsx- package.json- astryx.integration.mjs
integration add starts with one source export and one single-component .doc.mjs. Keep that shape for one public component. If the source exposes a family, choose the correct shape in npx @astryxdesign/cli docs authoring/building-blocks/components/describe-the-component.
Add never overwrites a file, and --dry-run shows what it would write. Component commands read Core, so install @astryxdesign/core in the package first (npx @astryxdesign/cli docs authoring/quick-start).
Document a component you already have
To document a component your package already has, write its doc beside the source file and name the doc after that file, such as acme-gallery.doc.mjs beside acme-gallery.tsx. Set name to the export apps import, such as name: 'AcmeGallery', and keep both files inside the components root that astryx.integration.mjs declares. The file does not have to be named after the component; npx @astryxdesign/cli docs authoring/building-blocks/components/component-imports shows how apps then find it.
In global mode
In global mode: when users install your package for their computer, beside the global CLI, your components show up in folders outside any project; inside a project, only if the project installs your package. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Describe the component
A component's .doc.mjs is part of the integration's public contract, not optional commentary. Astryx uses it for CLI output and search, and people and agents read it to decide whether the component fits and how to use it.
- Change the source and its
.doc.mjstogether. - Update the doc whenever the public name, import, behavior, props, defaults, examples, or accessibility requirements change.
integration verifychecks the doc shape and packed import, anddoctor integration componentscompares each props table with the component's TypeScript props (npx @astryxdesign/cli docs authoring/building-blocks/components/describe-the-component/check-the-props). Neither can prove the prose still matches the component.
Every component doc has one stable identity and enough usage guidance for a reader to choose it correctly. Pick the shape below that matches your module.
After every source or doc change, read the component back. This output is what people and agents receive.
bashnpx @astryxdesign/cli component AcmeCarousel
Start from the generated doc
integration add component creates the normal doc for one public component. Keep the generated identity and import, then replace its sample text and props with the component's real public contract.
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeCarousel',displayName: 'Acme Carousel',import: '@acme/astryx-widgets/components/AcmeCarousel',usage: {description:'Cycles through slides one at a time. Use it for a small set of related cards.',},props: [{name: 'slides',type: 'ReactNode[]',description: 'The slides to show, in order.',required: true,},],};
- Keep
propson the top-level doc. Private implementation helpers do not need entries. - Name the doc after the source file beside it, such as
AcmeCarousel.doc.mjsbesideAcmeCarousel.tsx, and change both in the same pull request. - If the module later exposes several related public exports, adapt this doc with
npx @astryxdesign/cli docs authoring/building-blocks/components/describe-the-component/component-family.
Every field is in npx @astryxdesign/cli docs authoring component-doc.
Explain when to use it
Write usage.description so a person or agent can decide whether this is the right component without opening its source. Say what it does, when to use it, and the most important boundary with a nearby alternative.
javascriptusage: {description:'Cycles through slides one at a time. Use it for a small set of related cards. Use a static list when every item should stay visible.',bestPractices: [{guidance: true, description: 'Keep the slide order stable while someone interacts with the carousel.'},{guidance: false, description: 'Hide information that must remain visible for comparison.'},],},
Set keywords to the words people search for: synonyms and the task the component does, such as keywords: ['slider', 'slideshow', 'rotating cards']. npx @astryxdesign/cli search ranks a match on a component's name or keywords above one in its description, so without keywords a search for the component's task can list Core components first. Check with the words a person would type:
bashnpx @astryxdesign/cli search "rotating cards" --type component
Document every public prop
Copy the public prop names and types from the source. Explain the behavior a caller controls, not only the TypeScript type.
javascriptprops: [{name: 'slides',type: 'ReactNode[]',description: 'The slides to show, in order.',required: true,},{name: 'interval',type: 'number',description: 'Milliseconds between automatic slide changes.',default: '5000',},],
- Set
required: trueonly when every caller must pass the prop. - Write
defaultexactly as the value should appear in documentation. - Skip styling escape hatches such as
xstyle,className, andstyle. npx @astryxdesign/cli doctor integration componentslists a documented prop the code lacks, a public prop the doc misses, and a wrongrequired. Seenpx @astryxdesign/cli docs authoring/building-blocks/components/describe-the-component/check-the-props.
Add focused examples
Add short examples for important usage that the prop table does not make obvious. Each example should teach one complete pattern and use only public imports.
javascriptexamples: [{label: 'Automatic rotation',code: '<AcmeCarousel slides={slides} interval={5000} />',},],
Read the result
Read the component after every source or doc change. Confirm that its purpose, import, props, defaults, and examples match the source, then verify the packed package.
bashnpx @astryxdesign/cli component AcmeCarouselnpx @astryxdesign/cli doctor integration componentsnpx @astryxdesign/cli integration verify
Choose the family shape
Start with a complete single-component doc. Convert its top-level props into a components array only when one source module or component directory exposes several public components or hooks as one family. The family doc keeps the shared usage guidance; each array entry owns one public export.
- Put the primary or most-used export first.
- Use a full entry when this file owns that export's description and signature.
- Use
propsfor a component entry. Useparamsandreturnsfor a hook entry. - Do not add private implementation helpers or exports that people should not use directly.
Every field is in npx @astryxdesign/cli docs authoring component-doc.
Document the family inline
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeTabs',displayName: 'Acme Tabs',import: '@acme/astryx-widgets/components/AcmeTabs',usage: {description:'Switches between related views without leaving the page.',},components: [{name: 'AcmeTabs',displayName: 'Acme Tabs',description: 'Owns selection and lays out the tab list and panels.',props: [{name: 'value',type: 'string',description: 'The selected tab value.',required: true,},],},{name: 'AcmeTab',displayName: 'Acme Tab',description: 'Selects one view in Acme Tabs.',props: [{name: 'value',type: 'string',description: 'The value this tab selects.',required: true,},],},],};
The public module named by import must export every component or hook named by a full entry. Apps then list, search and open each component in the family by its own name. A full entry the module does not export is not listed, and integration verify does not report it: check the family with npx @astryxdesign/cli component --list. When import names output your pack lifecycle builds (such as dist), the members are listed once that output exists, and integration verify compares them as they are listed after the pack builds it.
A member's source is the .tsx file in the family doc's folder, or below it, that declares it: the one the family's own file or the module import names reaches, else the one in that folder that declares the name, as when import names built output. A member that only a bundle or a file outside that folder declares is still listed, with no source for --source or swizzle. A member is not listed when a doc's file or a Core name your package replaces already has its name. A member that shares a name with Core opens with --package.
Give a member its own file
When one family member needs its own doc, replace its full entry with {name: 'MemberName'} and move the details into a sibling doc. The parent keeps the family relationship without copying the child's content. Continue with npx @astryxdesign/cli docs authoring/building-blocks/components/describe-the-component/subcomponent.
Choose a separate doc
Start with a component family doc. Move one public member into a sibling doc when it has its own source and enough behavior, props, or usage guidance to maintain separately. The parent still lists the member, but only by name.
- Keep a small member inline when its whole contract stays clear in the family doc.
- Use a sibling doc when the member needs focused search results, examples, usage guidance, or independent maintenance.
- Give each member one documentation owner. Do not keep a full parent entry and a sibling doc for the same name.
Reference it from the parent
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeDialog',displayName: 'Acme Dialog',import: '@acme/astryx-widgets/components/AcmeDialog',usage: {description: 'Presents a focused task above the current page.'},components: [{name: 'AcmeDialog',displayName: 'Acme Dialog',description: 'Owns the modal surface and open state.',props: [],},{name: 'AcmeDialogHeader'},],};
The name-only entry keeps AcmeDialogHeader in the family. Its description and props come only from the sibling file.
Write the subcomponent doc
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeDialogHeader',displayName: 'Acme Dialog Header',import: '@acme/astryx-widgets/components/AcmeDialogHeader',subComponentOf: 'AcmeDialog',description: 'Labels an Acme Dialog and holds its close action.',props: [{name: 'title',type: 'string',description: 'The dialog title.',required: true,},],};
Every field is in npx @astryxdesign/cli docs authoring component-doc.
subComponentOfmust exactly match the parent doc'sname.descriptionexplains this member's role in the family.usageis optional; add it when the member needs guidance beyond that sentence.- The CLI does not copy family fields such as
group,category, orkeywordsfrom the parent to the child. Set any the child needs in its own doc.
Run the props check
doctor integration components compares every props table in your component docs, hidden docs and every family entry included, with the props the component's TypeScript declares. Run it in your package after you change a component or its doc.
bashnpx @astryxdesign/cli doctor integration components
textseverity: [warn]component: AcmeCarouselprop: loopkind: not_in_codemessage: "loop" is documented, but AcmeCarousel's props have no "loop".doc: components/AcmeCarousel.doc.mjssource: components/AcmeCarousel.tsxdocType: boolean1 prop(s) differ between the docs and the code in 1 of 1 component(s) compared (TypeScript 6.0.3). These are warnings: update the doc, or the code if the doc is right.
- A doc's own
propsdescribe the export itsnamenames. In a family doc, each entry describes its own export. A hook entry documents parameters and returns, so it is not compared. - It reads props with the
typescriptyour package installs, and reads your package'stsconfig.jsonthe waytsc -pdoes. It finds TypeScript the way the docs check does, TypeScript 7 included (npx @astryxdesign/cli docs authoring/building-blocks/docs/check-your-docs). Without a usable one it prints[skip] Props not comparedand the reason. - Findings are warnings and leave the exit code at 0. Read them before you publish. With
--json, they are indata.propDrift.findings.
Fix each finding
Each finding's kind says what differs. Fix the doc, or the code if the doc is right.
| Kind | Means | Fix |
|---|---|---|
not_in_code | The doc lists a prop the component's props do not have | Remove it from the doc, or add it to the code. A name an index signature takes, such as data-*, counts as present |
not_in_doc | Your package declares a prop the doc does not list, or builds one with a mapped type, such as Partial<Record<'sm' | 'md', string>> | Document it. Props from React, the DOM, or another package are never asked for, and neither are props whose keys another package defines (keyof CSSProperties, behind an alias or not), className, style, xstyle, data-testid, ref, key, or a prop whose JSDoc has @internal. A deprecated prop is still a prop: document it until you remove it. A prop a mapped type builds from keys the check cannot trace (a template literal, say) is listed under [info] Props not checked for a doc entry instead |
required_mismatch | required disagrees with the code | Set required: true exactly when every caller must pass the prop. One your defaultProps fills counts as optional, as it is to JSX callers, and so does one only some members of a union require, since a caller can leave it out: say in its description which variants need it. An entry that names several props (value / onChange) makes the same claim about each: give them separate entries when they differ |
type_mismatch | The documented type differs from the code's, where both read exactly | Copy the type from the source, or fix the code. The next section says which types are judged |
default_mismatch | The documented default differs from the literal default the component writes in its props destructuring, such as size = 'md' | Copy the default from the source, quoted as code: default: "'md'". Quotes and spacing are ignored, and a default that is not a literal on either side (a constant, a call, prose) is not compared |
Write types the check can judge
For a prop your package declares, the check also compares the documented type, but only where both sides read exactly. It never guesses, so a type it cannot judge is listed, not reported. It needs TypeScript 4.8 or later: with an older one it prints [skip] Prop types not judged and judges none.
textseverity: [warn]component: AcmeCarouselprop: intervalkind: type_mismatchmessage: "interval" is documented as string, but the code declares number.doc: components/AcmeCarousel.doc.mjssource: components/AcmeCarousel.tsxdocType: stringcodeType: number
- Write the type as the source writes it, such as
ReactNode[]or(index: number) => void. Parameter names, member order, quotes and whitespace may differ.Textthat writesreadonlymust match exactly, parameter names included; only whitespace may differ.React.Xnames React's ownXwhere the prop is declared, so it matches a bareXonly where that name is React's:React.KeyboardEventis not the DOM'sKeyboardEvent. - Name a type as the file that declares the prop names it, so an import renamed there (
Item as BaseItem) is written as that file writes it. A field (cfg.level) takes its prop's file, even when the field's own type is declared in another file. - A type written another way is judged only when both sides are built from exact parts: primitives such as
stringandnumber, string, number and boolean literals, string enum members, arrays and unions of these, and type names that stand for them. Union members may come in any order, and a member another covers adds nothing, as TypeScript passes it there:undefinedbesidevoid, orstring[]beside(string | number)[]. - A mutable array type matches a readonly one, because callers can pass it. A readonly one documented for a mutable array is a mismatch.
- When a prop's variants give a field different types, as
{value: string} | {value: number}does, document every variant's type (string | number). The types of only some variants are listed: the prop's variants give this field different types. With more than six variant types, any other type is listed too. - To name an alias and show its values, write
AcmeSize = 'sm' | 'md'orAcmeSize ('sm' | 'md'). Both parts must match. - An optional prop's
undefinedis set aside on both sides. WithexactOptionalPropertyTypes, the doc showsundefinedexactly when the code declares it. - Anything else, such as an object, a tuple, a generic with arguments,
keyof, an intersection, or a mapped or conditional type written another way, is not compared.
The check prints how many documented types it judged, then lists the rest by reason: not a form the check judges exactly, a field the prop's variants give different types, a standard prop, or a prop declared outside the package. With --json, they are in data.propDrift.typesNotCompared.
text[info] Documented prop types judged: 2; not compared: 0. A type is judged when the doc writes it the same as the source (exactly, if it contains `readonly`), or when both are primitives, literals, arrays and unions of those, or names for them.
When a component is not compared
When the check cannot read a component's props exactly, it lists the component under [info] Props not compared, with the reason, instead of reporting findings that might be wrong.
text[info] Props not compared for 1 documented component(s):component: AcmeCarouseldoc: components/AcmeCarousel.doc.mjsreason: AcmeCarousel depends on `@acme/motion` (imported in AcmeCarousel.tsx), which the compiler could not resolve, usually a dependency that is not installed. Install the package's dependencies and run this check again
| Reason | Fix |
|---|---|
| It depends on a package or file the compiler could not resolve | Install the package's dependencies, then run the check again |
| It depends on a type the compiler can't work out | Make the types compile with npx tsc --noEmit, then run the check again |
| The package's TypeScript config has an error | Fix tsconfig.json until npx tsc -p . reads it |
Its props are typed any | Give the component a props type |
| Some prop names come from a type parameter | Nothing to fix: a component that renders an element the caller picks takes props known only where it is used |
The file has no export named by the doc's name | Change name or the export so they agree |
Export the component
Apps copy the component doc's import field into their code, so that exact specifier must resolve from your packed package. integration add writes it together with an exports entry in package.json.
json"exports": {"./components/AcmeCarousel": "./components/AcmeCarousel.tsx"}
Add writes the entry only when package.json already has an exports map, so start every package with "exports": {}.
A file does not have to be named after its component. When Slides.tsx exports AcmeCarousel and nothing named Slides, keep the doc beside it as Slides.doc.mjs and set its name to AcmeCarousel: apps list, search and open the component by that name, and integration verify checks that its import exports AcmeCarousel. A file that exports its own name keeps that name. A package that names its files in kebab case works the same way: acme-gallery.doc.mjs beside acme-gallery.tsx sets name: 'AcmeGallery'.
The file name still opens the doc. A name that Core or another doc's file already has stays theirs, and so does a Core name your package replaces: with Badge.doc.mjs naming AcmeBadge, npx @astryxdesign/cli component Badge opens Core's Badge, and --package with your package name reaches yours.
Verify the packed import
integration verify unpacks the packed package into a temporary app, resolves each documented import, and checks that the module exports the documented component name. Its --json output names each failure by code.
bashnpx @astryxdesign/cli integration verify
component_import_unresolvablemeans the exports map has no entry for the documented import.component_export_missingmeans the module does not export the documented name, or package.json has no exports map.typescript_extension_in_specifiermeans the public import ends in.tsor.tsx.
To import from the package root, set import: '@acme/astryx-widgets' and re-export the component from the file that exports["."] points to. See npx @astryxdesign/cli docs cli/commands/integration-verify.
In global mode
In global mode: outside a project, imports the CLI prints name packages installed for the computer, which a new app must install before it can build them; inside a project, only packages the project installs are printed. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Read the installed component
An app that installs your package sees the component beside Core components, with its package name and public import. The app needs no config.
bashnpx @astryxdesign/cli component AcmeCarouselnpx @astryxdesign/cli component --listnpx @astryxdesign/cli search carousel
textpackage: @acme/astryx-widgets# AcmeCarouselCycles through slides one at a time. Use it for a small set of related cards.**Import:** `import {AcmeCarousel} from '@acme/astryx-widgets/components/AcmeCarousel';`
component --list shows it as import: @acme/astryx-widgets/components/AcmeCarousel [@acme/astryx-widgets]. The same commands work in your package while you build it.
Test the packed package
Install the packed package in a test app before publishing. Then run the same detail, list, and search commands there. See npx @astryxdesign/cli docs authoring/ship/test-in-an-app.
In global mode
In global mode: to check your package in an app, install it in that app; a copy installed for your computer is not used inside a project. To check it outside projects, install it for your computer beside the CLI. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Decide whether to replace
Most components should not use replaces. Give a new component its own name and let apps choose it explicitly. Replace Core only when your component must become the default for one Core identity everywhere the integration is active.
Use it when
- Your component intentionally serves the same role as one specific Core component.
- Every app that loads the integration should get your component from unqualified component detail,
component --list, search,swizzle <Name>, and issue routing. - You have tested both the replacement and explicit access to the original Core component.
Do not use it when
- Your component is an alternative, variant, wrapper, or product-specific extension. Give it a unique name instead.
- You only need to resolve an accidental name collision.
- You want the replacement in only one screen or workflow. Replacement applies across the app wherever the integration is active.
Set the replacement
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeSideNav',displayName: 'Acme Side Nav',replaces: 'SideNav',import: '@acme/astryx-widgets/components/AcmeSideNav',usage: {description: 'Product navigation for Acme apps.'},props: [],};
Every field is in npx @astryxdesign/cli docs authoring component-doc.
The replacement turns on only when your package declares the first CLI release that applies it. Add this to its package.json:
json{"peerDependencies": {"@astryxdesign/cli": ">=0.6.7"},"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}}
- Any
@astryxdesign/clirange that starts at that release or later turns it on, including the>=0.7.0thatintegration add doc --parentwrote in published 0.6.4 and 0.6.5, andintegration add themein published 0.6.5. - Without such a range, the component keeps its own name, the Core component stays selected (a component named like its target stays ambiguous by that bare name, as before), and
doctor integration componentswarns with the range to add. Apps that load such a package see no change. replacesnames the CoreComponentDocidentity, not its display label, import path, or a standalone hook.- Your component may keep a distinct name or use the same name as the target. A distinct name remains directly addressable on older CLIs that ignore
replaces. --package @astryxdesign/corealways selects the original Core component, and--packagewith your package selects your component by either name.- Replacing
SideNavreplaces only that name.SideNavItemand the otherSideNavparts stay Core components; document any parts your package provides under their own names. swizzle --listlists Core components, including the one you replace.swizzle SideNavcopies your component, andswizzle SideNav --package @astryxdesign/corecopies the original.
Check replacement resolution
bashnpx @astryxdesign/cli doctor integration componentsnpx @astryxdesign/cli component SideNavnpx @astryxdesign/cli component AcmeSideNavnpx @astryxdesign/cli component SideNav --package @astryxdesign/core
- Once the package declares the range, a missing target, invalid value, second replacement for one target in the same package, or a replacement named after a different Core component is an error. Without the range,
doctor integration componentsreports each of these as a warning. - When several integrations replace one target, explicit configuration beats the automatic pick. Among explicitly configured integrations, the later package wins and Doctor warns. Among autolinked integrations alone, the dependency listed later in
package.jsonwins and Doctor warns.
In global mode
In global mode: a replacement from a package installed for the computer applies in folders outside any project; inside a project, only replacements from packages the project installs apply. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Templates
In global mode: templates from an integration installed for the computer show up outside projects; inside a project, only templates from integrations the project installs. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
What a template is
Unlike a component, a template becomes app code: an app copies it into its own code and adapts it to its product.
A component stays a package dependency and updates with the package. A copied template does not, so updating the package never rewrites the app's copy.
bash# How an app finds a template and copies itnpx @astryxdesign/cli template --listnpx @astryxdesign/cli template acme-dashboard src/app/dashboard
Templates help other people build apps faster while keeping a consistent visual language across products. Share one when people need more than one component to get started: a template brings the right components, layout, content structure, and interaction wiring already assembled.
Do not turn a product-specific page into a rigid component only to share its structure. Share it as a template and let each app adapt its copy.
Choose a page or block
| Kind | Use it for |
|---|---|
| Page | A complete screen, such as a dashboard, settings page, or checkout flow. |
| Block | A smaller section that fits inside a page, such as a hero, form, or data panel. A block can also be the example or showcase for a component. |
Pick a template id
Choose a stable lowercase kebab-case id, such as acme-dashboard. The id becomes the source and doc file name, the package export, and the value apps pass to npx @astryxdesign/cli template. To change a label, edit the doc (npx @astryxdesign/cli docs authoring/building-blocks/templates/document-the-template/template-doc-overview); never rename the id.
- Start with your product or package name, then name the reusable pattern, so the id stays distinct from Core and other integrations.
- Do not end the id with
-page,-app,-view, or-screen. - Do not reuse a Core id by accident: the bare id becomes ambiguous in every app that installs your package. To take over a Core template on purpose, declare
replaces(npx @astryxdesign/cli docs authoring/building-blocks/templates/document-the-template/replace-a-core-template).
bash# See the Core idsnpx @astryxdesign/cli --json template --list --package @astryxdesign/core
Run the add command
Pages are the default. Pass --type block to add a block.
bashnpx @astryxdesign/cli integration add template acme-dashboardnpx @astryxdesign/cli integration add template acme-stat-card --type block
texttemplate contribution added[ok] acme-dashboardDeclare template root ./templates in astryx.integration.mjs.- templates/acme-dashboard.doc.mjs- templates/acme-dashboard.tsx- package.json- astryx.integration.mjs
The command writes templates/<id>.tsx for the UI and templates/<id>.doc.mjs for its metadata, and declares the templates directory in astryx.integration.mjs. When package.json has an exports map, it also adds the ./templates/<id> export (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/export-template-assets).
It never overwrites an existing source or doc file. Add --dry-run to see every planned write first.
In global mode
In global mode: when users install your package for their computer, beside the global CLI, your templates show up in folders outside any project; inside a project, only if the project installs your package. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Understand the two files
Every template has a source file and a doc file. For acme-dashboard, the source is templates/acme-dashboard.tsx and the doc is templates/acme-dashboard.doc.mjs. Both begin with acme-dashboard, which is how Astryx knows they belong together.
| File | What it controls |
|---|---|
templates/acme-dashboard.tsx | The UI source that an app copies and then owns. |
templates/acme-dashboard.doc.mjs | How Astryx names, describes, categorizes, previews, and resolves the template before it is copied. |
The integration manifest points Astryx to the templates directory; it does not list each template. Change the source and doc together whenever the purpose, preview, readiness, or replacement behavior changes. No automated check can tell whether the doc still describes the rendered UI.
The fields below apply to every page and block. The page, block, and replacement guides add the fields unique to each.
Every field is in npx @astryxdesign/cli docs authoring template-doc.
- Set
typeto the kind you chose innpx @astryxdesign/cli docs authoring/building-blocks/templates/start-a-template. It decides which other fields the doc accepts. - The file name sets the template id (
npx @astryxdesign/cli docs authoring/building-blocks/templates/start-a-template). Labels live in the doc: the terminal list printsname, and JSON listings printdisplayName. Changing either never changes the id. - Write the description for someone choosing between templates. The Doc metadata category in
npx @astryxdesign/cli docs authoring/building-blocks/templates/write-good-templates/template-grading-rubricdefines what a complete description covers. - Name the ideas the template serves in
keywords: the domains, tasks, and other names a builder might use for it, in lowercase, such as['monitoring', 'uptime', 'on-call']for a service-health dashboard. How search and build use them is innpx @astryxdesign/cli docs authoring/field-reference, so the description can stay about the layout. - Add
isReady: falseas soon as you generate the doc, because a doc withoutisReadyis listed as ready. Keep it until the template passesnpx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/test-template-in-appand the quality review.
Note: keywords needs @astryxdesign/cli 0.6.6 or later. A stable CLI before 0.6.6 rejects the field and drops that template, and one before 0.6.4 also hides your doc topics: template --list and search print one warning, and docs and build say nothing. Declare the CLI floor as an optional peer (npx @astryxdesign/cli docs authoring/ship/versioning); integration verify fails until you do.
Check how Astryx lists it
Read the package-scoped template list after every doc change. Confirm the id, visible name, description, type, readiness, and package.
bashnpx @astryxdesign/cli --json template --list --package @acme/astryx-templates
json{"id": "acme-dashboard","name": "acme-dashboard","displayName": "Acme Dashboard","description": "An analytics dashboard for reviewing account health and recent trends.","type": "page","package": "@acme/astryx-templates","isReady": false}
Start from the generated page doc
integration add template writes a page doc by default. Replace its sample description, then add the page-only fields below.
javascript/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */export default {type: 'page',name: 'acme-dashboard',displayName: 'Acme Analytics Dashboard',description:'A filterable account-health dashboard with headline metrics, trends, and a recent-activity table. Dashboard, reporting, analytics, or status overview.',category: 'Dashboard - Analytics',isReady: false,};
Help people find the page
Every field is in npx @astryxdesign/cli docs authoring template-doc.
- Set
categoryto the most specific supported{Group} - {Variant}value. Its words become search terms fornpx @astryxdesign/cli search, and it appears in the JSON template list. - Set
scaffold: trueonly for a deliberately sparse starting shell. The JSON template list reports it, so tools and galleries can tell a scaffold from a finished page. - Fields that only Astryx's own gallery reads, such as
isHiddenFromOverview, have no effect on integration templates.
Start with a standalone block
Most blocks stand alone. Start with this shape unless the block is specifically the example or showcase for one component.
javascript/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */export default {type: 'block',name: 'acme-stat-card',displayName: 'Acme Stat Card',description:'A compact metric card with a current value, period delta, and supporting trend. KPI, scorecard, summary, or dashboard statistic.',isReady: false,aspectRatio: 4 / 3,componentsUsed: ['Card', 'HStack', 'Text', 'VStack'],};
Do not add exampleFor only because the block uses a component. componentsUsed records composition; exampleFor declares that one component owns the example.
Choose the component relationship
Every field is in npx @astryxdesign/cli docs authoring template-doc.
Keep one clear primary owner. Use the also* fields only for intentional secondary placements.
javascript/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */export default {type: 'block',name: 'acme-status-card-showcase',displayName: 'Acme Status Card Showcase',description:'An account-health summary that demonstrates status, trend, and action states for AcmeStatusCard.',isReady: true,exampleFor: 'AcmeStatusCard',isShowcase: true,alsoExampleFor: ['AcmeDashboard'],aspectRatio: 4 / 3,componentsUsed: ['AcmeStatusCard', 'Button', 'HStack', 'VStack'],};
Then check the package-scoped template list (npx @astryxdesign/cli docs authoring/building-blocks/templates/document-the-template/template-doc-overview) and confirm the entry carries the relationship you set. npx @astryxdesign/cli component does not show integration blocks, so the list is where to check.
Choose a useful preview
Every field is in npx @astryxdesign/cli docs authoring template-doc.
Set aspectRatio from the value for the closest shape below, render the block at that ratio, and adjust it until it neither clips nor leaves large empty space. These are starting points, not contract defaults.
| Block shape | Starting value |
|---|---|
| Wide navigation, banner, toolbar, or tabs | 16 / 4 |
| Square button, badge, avatar, icon, spinner, or status | 1 |
| Tall navigation, calendar, list, or tree | 3 / 4 |
| Content card, dialog, table, or form group | 4 / 3 |
scale affects only Astryx's own block previews. Integration blocks can leave it out.
Replace only on purpose
Use with care: replace a Core template only when every app using this integration should receive your source for an existing Core id by default. An alternative, a product-specific variation, or a different kind of template gets its own id instead (npx @astryxdesign/cli docs authoring/building-blocks/templates/start-a-template).
- Sharing a Core id without
replacesdoes not replace Core. It makes the bare id ambiguous, sonpx @astryxdesign/cli template <id>fails until the app adds--package, anddoctor integration templatesreports an accidental collision. - A replacement can keep the Core id or use its own.
replacesis what makes it the default.
Declare the replacement
Find the exact Core id and kind first.
bashnpx @astryxdesign/cli --json template --list --package @astryxdesign/core
javascript/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */export default {type: 'page',name: 'acme-app-shell',displayName: 'Acme App Shell',description:'An Acme application shell with product navigation, account controls, and a responsive content region.',replaces: 'shell-side-nav',isReady: true,category: 'Shell - Left Sidebar',};
Every field is in npx @astryxdesign/cli docs authoring template-doc.
Declare replaces on the template doc, never in astryx.integration.mjs.
Require a compatible CLI
Declare the CLI floor that integration verify names as an optional peer (npx @astryxdesign/cli docs authoring/ship/versioning).
json{"peerDependencies": {"@astryxdesign/cli": ">=0.6.4"},"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}}
integration verify reports replaces_needs_cli when that peer is missing or too old. Without it, published 0.6.3 and earlier drop that template and hide your doc topics.
What apps receive
bash# Receives the active replacementnpx @astryxdesign/cli template shell-side-nav src/app# Receives the original Core templatenpx @astryxdesign/cli template shell-side-nav --package @astryxdesign/core src/app
When several integrations replace the same Core template, one wins:
- An integration explicitly listed in
astryx.config.mjswins over one that is only installed and picked automatically. - When several explicitly configured integrations replace the same target, the one listed later wins and Astryx reports the ambiguity.
- When no integration is configured explicitly, the package listed later in the app's package.json dependencies wins. Configure the intended integration explicitly instead of relying on that order.
Check the replacement
bashnpx @astryxdesign/cli doctor integration templates
| Issue | Meaning |
|---|---|
missing_template_replacement_target | replaces does not name a Core template id. |
invalid_template_replacement | The replacement is unusable or its page/block kind differs from Core. |
ambiguous_template_replacement | One package declares two replacements for a target, or several active packages contend for it. |
Replacement resolution is safe by default. A missing source, invalid doc, missing target, wrong kind, or conflicting declaration never hands the Core id to a questionable replacement: Astryx reports the issue and keeps the Core template. Fix every issue before publishing.
Know what gets copied
npx @astryxdesign/cli template copies exactly one source file. It does not copy sibling helpers, stylesheets, fonts, icons, images, or media. Plan for that before you add supporting files.
| Template | Directory target | Explicit file target |
|---|---|---|
| Page | Writes <target>/page.tsx | Writes the exact file path |
| Block | Writes <target>/<source-basename>.tsx | Writes the exact file path |
bash# Page: src/app/account/page.tsxnpx @astryxdesign/cli template acme-account src/app/account# Block: src/features/account/acme-stat-card.tsxnpx @astryxdesign/cli template acme-stat-card src/features/account# Exact destination for either kindnpx @astryxdesign/cli template acme-stat-card src/features/account/StatusCard.tsx
Astryx refuses to replace an existing file unless the caller passes --overwrite or -f.
Keep edited code in one file
Put every helper that the app is expected to edit in the template source. Small local components, example data, constants, and event handlers can live above or below the default component in the same file.
- Inline a helper when it is part of the editable starting point.
- Move a helper into the integration package only when it should stay package-owned and update with the package.
- Do not import
./helper,./styles, or any other sibling file. - Do not create routes, configuration, or extra files at runtime. Give the app one explicit source file to own.
Use imports that work in the app
After the copy, imports resolve from the app, not from the template directory. Every bare import must name a package the app installs, and every package path must be public.
tsximport {Button} from '@astryxdesign/core/Button';import {Card} from '@astryxdesign/core/Card';import {HStack} from '@astryxdesign/core/HStack';import {Text} from '@astryxdesign/core/Text';import {VStack} from '@astryxdesign/core/VStack';import {AcmeStatusCard} from '@acme/astryx-widgets/components/AcmeStatusCard';const metrics = [{label: 'Healthy accounts', value: '1,248'},{label: 'Needs review', value: '37'},];function MetricRow({label, value}: {label: string; value: string}) {return (<HStack justify="between"><Text>{label}</Text><Text>{value}</Text></HStack>);}export default function AcmeAccountSummary() {return (<Card><VStack gap={4}><AcmeStatusCard />{metrics.map(metric => <MetricRow key={metric.label} {...metric} />)}<Button label="Review accounts" /></VStack></Card>);}
- Import Astryx components from their public Core paths.
- Import integration-owned components, helpers, icons, or styles only through paths the package exports (
npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/export-template-assets). - Avoid adding another library when Astryx or platform APIs already provide the behavior.
Export one component
The source must default-export one React component. Named helpers inside the file are fine.
- Add
use clientas the first statement only when hooks, event handlers, browser APIs, or a component passed as a prop, such asicon={BellIcon}, need a client boundary. - Keep a static template server-compatible when it needs no client behavior.
Assets
The copied file does not bring sibling assets with it (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/write-the-template-file). Give every asset exactly one owner:
| Owner | Use when | How the copied source reaches it |
|---|---|---|
| Copied source | The value is small, editable, and belongs to the starting point | Keep it in the .tsx file |
| Integration package | The asset should stay centrally maintained with the package | Import a stable public package path |
| App | The app must provide product-specific content or branding | Use an explicit placeholder or app public URL and say what to replace |
Do not make a template look self-contained while it relies on an undocumented package file or app convention.
Start with Astryx
Use Astryx component props, layout primitives, and design tokens before writing custom CSS. This keeps the copied result aligned with the host theme and reduces the styling contract an app inherits.
- Use
StackandGridspacing props instead of margins between children. - Use component variants, sizes, padding, and alignment props before restyling internals.
- Use public theme token exports for a value that Astryx does not expose as a prop.
- Do not copy private generated class names or target component internals with selectors.
Keep editable styles with the source
When a style belongs to the editable starting point, define it in the template file.
tsximport * as stylex from '@stylexjs/stylex';import {colorVars} from '@astryxdesign/core/theme/tokens.stylex';const styles = stylex.create({trend: {color: colorVars['--color-success'],minWidth: 0,},});// Later: <Text xstyle={styles.trend}>Up 12%</Text>
Use custom declarations only when no Astryx prop or token fits. Each one counts in the Custom CSS category of npx @astryxdesign/cli docs authoring/building-blocks/templates/write-good-templates/template-grading-rubric.
When a stylesheet must stay package-owned, import it from the template by its public package path.
tsximport '@acme/astryx-widgets/styles/acme-dashboard.css';
Then export that path and include the file in the package (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/export-template-assets). TypeScript 6 and later check side-effect imports by default, so this import type-checks only when the app or its framework declares *.css modules.
Prefer host typography
Use Astryx typography components and theme values unless the template must demonstrate a specific licensed typeface. A copied template should normally inherit the app typography instead of installing a new global font.
- Do not add a font only to make sample content look more polished.
- Do not override the app body font from a template.
- Use a custom face only for a real product or content requirement that the integration package owns.
Package a required font
Keep the font files beside a package-owned stylesheet. The stylesheet can use relative url() paths because it stays in the package, and the template imports the stylesheet by its public path (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/template-assets/template-styles).
css@font-face {font-family: 'Acme Sans';src: url('../assets/fonts/acme-sans-regular.woff2') format('woff2');font-style: normal;font-weight: 400;font-display: swap;}.acme-dashboard-title {font-family: 'Acme Sans', sans-serif;}
- Ship WOFF2 when it satisfies the supported browsers. Add another format only when those browsers require it.
- Declare every weight and style the template uses. Do not make the browser synthesize bold or italic because a file is missing.
- Use
font-display: swapunless the product requirement and performance test justify another value. - Confirm that the font license permits redistribution in the integration package.
Exporting the stylesheet does not publish the font files it points to. Include the font directory in the package as well (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/export-template-assets).
Check fonts in the app
When you test the template in an app (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/test-template-in-app), also confirm the following.
- Every font request succeeds from the built asset path.
- Computed styles show the requested family, weight, and style.
Textstays readable with the fallback stack when a font loads slowly or fails.
Use Astryx Icon
Use Icon for a standalone icon. For a component icon prop, pass what that prop accepts: most, such as Button and IconButton, take an element like <Icon icon={BellIcon} />, and some menu and navigation items also accept the icon definition itself. This keeps sizing, color, and accessibility aligned with the system.
tsximport {Button} from '@astryxdesign/core/Button';import {Icon} from '@astryxdesign/core/Icon';import {BellIcon} from '@heroicons/react/24/outline';<Icon icon={BellIcon} size="sm" /><Button label="Alerts" icon={<Icon icon={BellIcon} />} />
- Use one supported icon set consistently inside a template.
- Give an icon-only control its required visible or assistive label through the Astryx control API.
- Do not paste raw
svgandpathmarkup when an existing icon definition fits. - Do not render a library icon directly when
Iconor the receiving Astryx component can render it. - Declare an icon library other than Astryx as a dependency (
npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/export-template-assets).
Ship a custom icon deliberately
A product mark or missing domain symbol can stay package-owned. Export a compatible icon definition from a public package path, then import that path from the template.
tsximport {Icon} from '@astryxdesign/core/Icon';import {AcmePulseIcon} from '@acme/astryx-widgets/icons/AcmePulseIcon';<Icon icon={AcmePulseIcon} size="sm" label="Account health" />
Export the icon path and include its file in the package (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/export-template-assets).
Check icons in the app
When you test the template in an app (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/test-template-in-app), also confirm the following.
- Every icon state renders, including selected, disabled, loading, and narrow layouts.
- Meaningful icons have an accessible name, and decorative icons add no duplicate spoken text.
- No missing icon leaves an empty control.
Do not rely on sibling media
A reference such as ./hero.png or ../assets/demo.mp4 stays unchanged in the copied file and normally breaks.
- Use a stable public URL when the integration owner will continue to host the media.
- Use an explicit app-owned placeholder when every app must supply product-specific media.
- Use a package-owned JS or CSS entrypoint only when every supported app toolchain can resolve and emit the underlying asset.
- Do not assume that a file inside
node_modulesis automatically served at a browser URL.
Understand `/template-assets`
/template-assets/<path> is a preview-only fixture path. During copy, Astryx replaces a complete static image reference with an inline neutral SVG placeholder. It replaces a complete static video reference with an empty string because it cannot create a valid inline video.
| Source reference | Copied result |
|---|---|
/template-assets/hero.png | Inline SVG data URL placeholder |
/template-assets/demo.mp4 | Empty string |
./hero.png or /product/hero.png | Unchanged |
https://example.com/template-assets/hero.png | Unchanged |
- Supported preview image suffixes are SVG, PNG, JPG, JPEG, GIF, WebP, AVIF, and ICO.
- Supported preview video suffixes are MP4, WebM, MOV, OGV, and M4V.
- Keep the preview path as one complete static string. Concatenation, interpolation, method calls, module imports, and unknown file formats are rejected when Astryx cannot replace them safely.
- Use this path only when a placeholder is the intended copied result. It is not a way to ship required product media.
- Astryx serves
/template-assetsonly in its own docs. A preview you host must serve that path itself.
Design a useful placeholder
When the app must provide the final asset, keep the replacement point obvious in the copied source. Preserve enough surrounding layout and example data that the template still teaches the pattern without pretending the final content is included.
tsxconst HERO_IMAGE = '/acme-dashboard-hero.jpg';// Replace HERO_IMAGE with an app-owned public URL before shipping the page.<imgsrc={HERO_IMAGE}alt="Account health trends for the current quarter"width={1200}height={675}/>
- Use accurate alternative text for informative images and
alt=""for decoration. - Provide dimensions or an aspect-ratio container so loading does not shift the page.
- For video, provide controls, a useful poster, and captions when the content requires them.
- Do not leave an inaccessible, expiring, authenticated, or environment-specific URL in copied source.
Check media in the app
When you test the template in an app (npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/test-template-in-app), also confirm the following.
- The copied source shows the placeholder substitutions you expect.
- The network log has no missing, redirected, blocked, or unauthorized media requests.
- Each placeholder keeps the layout and says what the app must replace.
Keep the generated template export
integration verify resolves each template through a public package path without a file extension, such as ./templates/acme-dashboard, and fails when that path is missing.
json{"exports": {"./templates/acme-dashboard": "./templates/acme-dashboard.tsx"}}
integration addwrites this entry whenpackage.jsonhas anexportsmap (npx @astryxdesign/cli docs authoring/building-blocks/templates/start-a-template). Without a map it writes nothing, because creating one would make existing deep imports private. Add the map yourself, then the entry.- Keep the public path without
.tsx. Only the target file ends in.tsx. - Do not point the entry at a barrel file that loses the template default export.
Export package-owned support files
Add a public path for every package-owned component, helper, icon, or stylesheet that the copied source imports. Keep implementation-only files private.
json{"exports": {"./templates/acme-dashboard": "./templates/acme-dashboard.tsx","./components/AcmeStatusCard": "./components/AcmeStatusCard.tsx","./icons/AcmePulseIcon": "./icons/AcmePulseIcon.tsx","./styles/acme-dashboard.css": "./styles/acme-dashboard.css"}}
If a block is also loaded directly as a live .tsx preview, add a pattern that keeps the extension, such as "./templates/*.tsx": "./templates/*.tsx". Keep the generated entry too; integration verify resolves the template through it.
Include every file
An export can point to a file that npm leaves out. When package.json#files exists, list the integration manifest, the templates directory, and every directory the copied file depends on.
json{"files": ["astryx.integration.mjs","templates","components","icons","styles","assets/fonts"],"sideEffects": ["**/*.css"]}
integration addappends the manifest and templates directory when afileslist already exists. Add the other directories yourself.- If
package.jsonsetssideEffects, include CSS in it, as above, so bundlers keep side-effect stylesheet imports. - Run
npm pack --dry-runand read the real file list instead of reasoning from the workspace.
Declare what the copied file imports
A copied file imports from the app, so the app must install every package it imports. Declare the Astryx peers as described in npx @astryxdesign/cli docs authoring/ship/versioning. For other packages:
| Field | Use it for |
|---|---|
peerDependencies | Libraries the copied source imports, such as an icon library. List them in devDependencies too, so the package itself builds. |
dependencies | Runtime packages used only by package-owned modules, which should travel with the integration |
devDependencies | Everything needed to build and test the integration itself |
Use only ranges you test. A package that resolves only because a workspace hoisted it is not declared.
Run the package check
bashnpx @astryxdesign/cli integration verify
integration verify runs npm pack, including lifecycle scripts, unpacks the .tgz file into a temporary app without installing its dependencies, and checks the unpacked integration. It publishes nothing and removes the .tgz file and the temporary app.
- It validates the manifest, every template doc, its source file, and the templates directory.
- It checks that the manifest and every template file are in the
.tgzfile. - It checks that each packed template keeps the same id, type,
name, andreplacesas your working copy. It does not compare template source or other doc fields. - It resolves each template through its public package path in the unpacked package.
- It checks that the resolved template module has a default export.
- It checks the CLI peer that features such as template replacement require.
Each failure prints a [fail] line with its message; add --json to see each issue code. npx @astryxdesign/cli docs authoring/help/troubleshooting lists the common messages and their fixes. Fix every error, and review every warning before publishing.
Know what it does not prove
integration verify checks the packed templates and their public entrypoints. It does not copy a template into an app or run a browser build, so it cannot show the following.
- Whether a relative helper or stylesheet survives the copy.
- Whether the copied file type-checks in every supported app toolchain.
- Whether package-owned CSS, fonts, icons, images, or media load in a browser.
- Whether layout, color modes, keyboard and pointer input, and real content work.
- Whether a template id collides with Core, or whether a
replacestarget exists. Runnpx @astryxdesign/cli doctor integration templatesfor those. - How the template scores on quality.
Install the packed package
Install the packed package in a clean app as described in npx @astryxdesign/cli docs authoring/ship/test-in-an-app. Also install every library the copied source imports, such as its icon library. Pack and reinstall after every change.
Copy the template
bash# Confirm discovery and metadatanpx @astryxdesign/cli --json template --list --package @acme/astryx-widgets# Inspect source without writingnpx @astryxdesign/cli template acme-dashboard --package @acme/astryx-widgets# Copy a page and a block to their real app locationsnpx @astryxdesign/cli template acme-dashboard --package @acme/astryx-widgets src/app/acme-dashboardnpx @astryxdesign/cli template acme-stat-card --package @acme/astryx-widgets src/components
- Confirm the copied files:
src/app/acme-dashboard/page.tsxandsrc/components/acme-stat-card.tsx. - Inspect the copied source for placeholder substitutions and relative references.
- Import the copied block into a real page. A file that is never imported has not been tested.
Build every supported app
Run the type check (npx tsc --noEmit, or the app's own script) and the production build in every framework or bundler the integration supports. One toolchain passing does not prove another.
bashnpx tsc --noEmitnpm run build
- Every import resolves from the copied location.
- Production builds keep required side-effect CSS.
- No import bypasses
exports, and no dependency resolves only through workspace hoisting.
Render the behavior matrix
| Dimension | Required checks |
|---|---|
| Width | Wide and narrow; include the smallest supported viewport |
| Color | Every supported color mode and theme |
| Input | Keyboard and pointer for the primary task |
| Content | Realistic, empty, long, loading, and error states that the pattern supports |
| Assets | Successful CSS, font, icon, image, poster, and video requests |
| Runtime | No console error, hydration mismatch, or missing-provider failure |
Check the rendered hierarchy, reading order, focus order, clipping, overflow, and fallback states. A screenshot of one default state is not a complete test.
Grade the result
Grade the exact packed revision using the evidence from this test (npx @astryxdesign/cli docs authoring/building-blocks/templates/write-good-templates).
How to write good templates
A good template does more than render. It gives an app a clear product starting point that is easy to understand, safe to change, and complete after Astryx copies it out of the package. A rubric scores that quality so you can grade as you build.
- Its purpose and primary task are clear before someone reads every detail.
- It composes Astryx components instead of rebuilding their behavior with raw HTML or custom styles.
- Its hierarchy, spacing, interactions, and reading order still work at narrow widths and in every supported color mode.
- Its source, imports, styles, fonts, icons, images, and media still work from the copied location.
- Its metadata makes the template easy to find and accurately explains when to use it.
- Its example content is realistic enough to expose overflow, empty-space, and hierarchy problems.
Grade the first runnable version, again after each source, doc, dependency, or asset change, and in full before every release. Grade the source, doc, copied file, and rendered app together; none of them is enough alone.
- Read every scoring rule in
npx @astryxdesign/cli docs authoring/building-blocks/templates/write-good-templates/template-grading-rubric. - Fix publication blockers first, then every reasonable deduction.
- Repeat the full grade on the packed package in a clean app (
npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/test-template-in-app). - To have an agent grade and improve the template, use
npx @astryxdesign/cli docs authoring/building-blocks/templates/write-good-templates/grade-template-with-agent.
Keep each scorecard with its package revision and rubric version.
Understand the score
Start with npx @astryxdesign/cli docs is-my-code-good: it owns the shared Astryx quality criteria. This rubric adds template weights, counting rules, and template-only checks, and each category that scores a shared criterion names it.
Template rubric 1.4 scores seven categories for 100 points. Aim for 100; B (75) is the publication floor, not the target. Each category below lists its weight. Never award points for anything you did not inspect.
| Grade | Score | Meaning |
|---|---|---|
| A | 90-100 | Exemplary. Copy-ready with no known quality problems. |
| B | 75-89 | Good. Minor issues may remain, but the template is usable. |
| C | 60-74 | Needs work before publication. |
| D | 40-59 | Poor. Significant rewrites are needed. |
| F | 0-39 | Failing. The template teaches or produces bad patterns. |
Keep improving while a deduction has a reasonable fix. A score below 100 is fine only when the remaining tradeoff is intentional and recorded. A template is not ready if the copied file fails to build or a required asset is missing, whatever its score.
Version 1.4 counts public integration components as Astryx components and grades assets and imports after the copy. Record the version with every score so results stay comparable.
Astryx component purity: 30 points
This category scores the Astryx components criterion in npx @astryxdesign/cli docs is-my-code-good. Count every JSX opening tag in the copied .tsx source. An Astryx element is a component imported from @astryxdesign/core or from a public component export of the integration package. A raw HTML element is any lowercase intrinsic JSX tag. Count occurrences, not only unique tag names.
Do not count fragments or a PascalCase helper defined in the same file. Inspect that helper and count the raw HTML inside it. For each raw element, decide with that guide whether it is necessary because Astryx has no equivalent, or unnecessary because an Astryx component can replace it.
| Raw HTML elements | Points |
|---|---|
| 0 | 30 |
| 1-2, all necessary | 25 |
| 1-2, any unnecessary | 20 |
| 3-5 | 15 |
| 6-10 | 8 |
| 11-20 | 4 |
| 21 or more | 0 |
- Do not deduct for an
imgwhen no general Astryx image component fits and its source passes the Image handling category. - Do not deduct for a
formthat wrapsFormLayoutto provide native submission semantics. - Do not deduct for
input type="hidden"when it carries native form state.
Icon purity: 15 points
This category scores the Icons criterion in npx @astryxdesign/cli docs is-my-code-good. Count raw icon markup in the copied file. npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/template-assets/template-icons shows how to render icons through Astryx instead.
- Count every raw
svg,path,circle,rect,line,polyline,polygon,ellipse, orgused as an icon. - Count an inline SVG component defined in the template.
- Count an icon component rendered directly instead of through
Iconor an Astryx icon prop.
| Raw SVG icon instances | Points |
|---|---|
| 0 | 15 |
| 1-2 | 10 |
| 3-5 | 5 |
| 6 or more | 0 |
Custom CSS: 15 points
This category scores the Styling criterion in npx @astryxdesign/cli docs is-my-code-good. Count individual CSS properties inside stylex.create and inline style objects. Count each className and stylex.props use once. Do not count Astryx props such as gap, padding, variant, size, color, level, columns, contentPadding, or height.
| Custom style declarations | Points |
|---|---|
| 0 | 15 |
| 1-3, all justified because no Astryx alternative exists | 12 |
| 1-3, any unjustified because an Astryx prop exists | 8 |
| 4-10 | 5 |
| 11-20 | 2 |
| 21 or more | 0 |
This category scores styles authored in the copied source. A package stylesheet is graded through its effect on portability and the rendered app, not as a way to hide custom declarations from this count.
Layout and structure: 15 points
Page templates
- Choose the page root with the
Layoutcriterion innpx @astryxdesign/cli docs is-my-code-good. In a template, only a category that starts withShell -owns global chrome, so it usesAppShell; every other page template usesLayoutorCenter. - Outside a
Shell -template, leave global navigation to the host app. Put in-page navigation in aLayoutPaneland page headings inLayoutHeader. - Use
Gridwithcolumns={{minWidth: 280}}for responsive collections. Do not fix the column count or rebuild the grid in raw CSS. - Use
Centerfor centered content instead of custom flexbox workarounds. - Render one page from one source file. Links may be inert examples, but the template does not create nested routes or router integration.
| Page condition | Points |
|---|---|
| Correct root, responsive grids, proper centering, and one page | 15 |
| Valid root with a smaller issue such as fixed columns or multi-page behavior | 8 |
| Wrong root for the category, a raw layout root, or no Astryx root | 0 |
Block templates
- Do not wrap a block in
AppShell. A block renders inside a preview or page container. - Keep the block focused on one pattern or component usage.
- Keep the composition substantial enough to teach the pattern and small enough to adapt. About 20-100 lines is the normal range.
Doc metadata: 10 points
Score field accuracy for 6 points, the description for 3 points, and naming for 1 point. Read the source and doc together. A field that exists but disagrees with the source is not complete.
Fields: 6 points
- Page:
type,name,displayName,description, explicitisReady, and a specificcategorywhen the page is meant for browsing. - Block:
type,name,displayName,description, explicitisReady, positiveaspectRatio, and completecomponentsUsed. - Relationship and preview fields follow
npx @astryxdesign/cli docs authoring/building-blocks/templates/document-the-template/block-template. A value that contradicts that guide is an inaccuracy.
| Field condition | Points |
|---|---|
| All applicable fields are present and accurate | 6 |
| All fields are present with one inaccuracy | 4 |
| One required field is missing | 2 |
| Two or more required fields are missing, or no doc exists | 0 |
Description: 3 points
A strong description covers four slots: the archetype, the job someone does, the structural or behavioral differentiator, and the alternate words people may search. Use at least six distinct content words after removing generic words such as page, screen, app, view, and component names. Describe the reusable shape, not only the sample data.
| Description condition | Points |
|---|---|
| All four slots, 6 or more distinct content words, and clear separation from sibling templates | 3 |
| Names and differentiates the pattern but misses one slot or leaves a synonym implicit | 2 |
| Generic, repeats component names, or describes only the sample data | 1 |
| Missing or restates the name | 0 |
Naming: 1 point
The id follows npx @astryxdesign/cli docs authoring/building-blocks/templates/start-a-template, displayName is readable, and a browsable page has a specific category (npx @astryxdesign/cli docs authoring/building-blocks/templates/document-the-template/page-template). Slug length is guidance, not a scored condition.
| Naming condition | Points |
|---|---|
| Id, display name, and applicable category follow the convention | 1 |
| Any naming or category requirement is missed | 0 |
Image handling: 5 points
This category scores the Images and assets criterion in npx @astryxdesign/cli docs is-my-code-good. Inspect every image reference in the copied file against npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/template-assets/template-images-media.
| Image condition | Points |
|---|---|
| No image is needed, or every image still works after copy and the demo-placeholder behavior is intentional | 5 |
| One optional demo image is missing in preview, or a placeholder service remains | 2 |
| An essential image breaks after copy, uses a package-relative path, or depends on an inaccessible URL | 0 |
Code quality: 10 points
This category scores the Code health criterion in npx @astryxdesign/cli docs is-my-code-good, plus conditions only a template has. Award 2 points for each condition. Grade the copied file, not only the package source.
| Condition | Points | How to verify |
|---|---|---|
| Correct client boundary | 2 | use client is the first executable statement when hooks, event handlers, or browser APIs require it. A static template does not add it without need. |
| Default export | 2 | The copied file has one default-exported React component. |
| Self-contained imports | 2 | Every import resolves from the copied location through React, a public Astryx path, a public integration-package export, or an app dependency the template explicitly requires. |
| Realistic example data | 2 | Content has realistic names, amounts, dates, lengths, and states instead of lorem ipsum or numbered placeholders. |
| No dead code | 2 | There are no unused imports or variables, commented-out blocks, or helpers that are never called. |
Give the agent the rubric
Grade one exact package revision against template rubric 1.4. Give the agent the template id, the package name, and a clean app that can install the packed package, then use this prompt. Replace every angle-bracket value. The first pass is read-only so the original score and findings stay visible.
textGrade integration template <id> from <package> at <revision>.Before scoring:1. Read `npx @astryxdesign/cli docs authoring/building-blocks/templates/write-good-templates --full`.2. Read `npx @astryxdesign/cli docs authoring/building-blocks/templates/write-good-templates/template-grading-rubric --full` and every guide it links.3. Inspect the template source, its matching .doc.mjs file, package.json exports and files, and astryx.integration.mjs.4. Run `npx @astryxdesign/cli integration verify` in the package.5. Follow `npx @astryxdesign/cli docs authoring/building-blocks/templates/build-the-template/package-and-test/test-template-in-app --full`: install the packed package in the clean app, copy the template, build the app, and render the behavior matrix.Score all seven rubric categories. Cite file and line evidence for every deduction. Do not award points for a state you did not inspect. If browser or build evidence is unavailable, say so and mark the template not publishable.Return the rubric version, package revision, numeric score, letter grade, category scorecard, publishable yes/no verdict, detailed findings, and the top three fixes. Treat 100 as the target. Name every remaining deduction and say whether it is an intentional tradeoff or still needs work. Do not edit files during this first pass.
Require a scorecard
markdown# Template grade: <id>**Rubric:** 1.4**Package revision:** <revision>**Type:** page | block**Grade:** <letter> (<score>/100)**Publishable:** yes | no| Category | Points | Max | Evidence || --- | ---: | ---: | --- || Astryx component purity | X | 30 | || Icon purity | X | 15 | || Custom CSS | X | 15 | || Layout & structure | X | 15 | || Doc metadata | X | 10 | || Image handling | X | 5 | || Code quality | X | 10 | || **Total** | **X** | **100** | |## Detailed findings- <file:line, rubric rule, observed problem, and deduction>## Publication blockers- <failed pack, copy, build, asset, browser, theme, responsive, or input check>## Top three fixes1. <highest-value fix>2. <second fix>3. <third fix>
The category scores must add exactly to the total, and the grade comes from the published bands. The verdict is yes only when the score is 75 or higher and every package, copy, build, asset, and rendered-app check passes.
Improve and regrade
- Fix publication blockers first.
- Fix every reasonable deduction without hiding it behind helper components or package CSS.
- Pack again, copy into a fresh app, and have the agent repeat the full grade. Keep the earlier scorecard so the improvement is visible.
- Stop only when no reasonable fix remains, and record every intentional tradeoff that still costs points.
Themes
In global mode: outside projects, the theme list includes themes from integrations installed for the computer, and theme add there only shows them; inside a project, it lists only themes from integrations the project installs. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
A theme contribution has editable defineTheme source plus a built module and production CSS. integration add theme scaffolds the source and declares the package exports; theme build writes the built outputs. An app installs the package, runs theme add, applies the theme from its generated app module, and customizes it with extends instead of copying source.
These guides cover the whole path: scaffold the source, generate its palette, define and build the theme, ship fonts and assets, run integration verify, and document how apps add and extend it. Applying a theme, including mode and SSR, works the same for every theme; see npx @astryxdesign/cli docs use-a-theme.
Add a theme
Run integration add theme with a lowercase kebab-case slug. It writes a blank theme — a defineTheme skeleton and its descriptor — into a folder named after the slug.
bashnpx @astryxdesign/cli integration add theme ocean
texttheme contribution added[ok] oceanDeclare theme root ./themes in astryx.integration.mjs.- themes/ocean/oceanTheme.ts- themes/ocean/oceanTheme.doc.mjs- package.json- astryx.integration.mjs
oceanTheme.ts exports oceanTheme, a defineTheme source, and oceanTheme.doc.mjs is the descriptor that theme list reads. The command also declares ./themes/ocean and ./themes/ocean.css package exports. Every field is in npx @astryxdesign/cli docs authoring theme-doc.
The source and built outputs ship in your package. Build the source with theme build, then run integration verify before publishing. An app runs theme add to record and import the built module and stylesheets; theme eject creates an independent source fork. See npx @astryxdesign/cli docs authoring/building-blocks/themes/use-a-theme-in-an-app.
Start from an existing theme
To begin from an existing theme instead of a blank one, pass --from. It copies that theme as your starting point to diverge from — a fork, with no link back.
bashnpx @astryxdesign/cli integration add theme ocean --from neutral
The copy keeps the packages the base theme uses, such as lucide-react for its icons: --from adds them to your dependencies, so an app that installs your package gets them too. Install your dependencies again before you build the theme.
Use --from when you want to change a lot. For a small change that should stay linked to a base theme, use extends instead (npx @astryxdesign/cli docs authoring/building-blocks/themes/define-the-theme).
Ship several themes
A package can ship more than one theme. Run integration add theme once per slug; each theme gets its own folder and descriptor, and theme list shows them all.
In global mode
In global mode: when users install your package for their computer, beside the global CLI, your themes are listed in folders outside any project, where theme add lists but writes nothing; inside a project, only if the project installs your package. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Generate a palette
A theme needs dozens of related colors — shades of each brand color for backgrounds, borders, text, and states, in both light and dark mode. A palette is that full set of shades. Rather than hand-pick every one, you name a few seed colors and generate the rest, so the shades stay consistent.
You write a short config naming each color family and its seed, then run theme palette generate. It expands each seed into a full light and dark ramp — the scale of shades — and writes the result into the theme folder, where your theme tokens point at it (see npx @astryxdesign/cli docs authoring/building-blocks/themes/define-the-theme). Keep the config and the generated palette together in that folder.
The smallest config is one color family: an id to name it and a seed color to grow it from. Save it as themes/ocean/palette.config.json:
json{"families": [{"id": "ocean", "seed": "#0074e2"}]}
bash# Print the palette without writing filesnpx @astryxdesign/cli theme palette generate themes/ocean/palette.config.json# Write the palette and its receiptnpx @astryxdesign/cli theme palette generate themes/ocean/palette.config.json \--out themes/ocean/tokens/ocean.palette.ts
text[ok] Wrote themes/ocean/tokens/ocean.palette.ts[ok] Wrote themes/ocean/tokens/ocean.palette.receipt.jsonReview and edit the candidate before adopting it as theme-owned palette data.
The command writes two files. ocean.palette.ts is the palette your theme tokens will read: it exports black, white, and palette — a ramp of 21 shades (stops 0 to 100) for both light and dark. The .receipt.json beside it records the exact seeds and settings, so you can regenerate the same palette later. A second run leaves both files alone unless you pass --overwrite.
Add --preview <file>.html to also get a web page showing every shade, so you can eyeball the palette in a browser. Write it outside themes/ so it does not ship in your package. Every option is in npx @astryxdesign/cli docs cli/commands/theme-palette-generate, and the config it reads is described in npx @astryxdesign/cli docs cli/api/functions/generate-tonal-palette.
Map the palette to tokens
A theme token is a named slot that Astryx components read for a value — --color-accent for the accent color, and so on. Components never read your palette directly; they read tokens. Defining a theme means pointing each token at a palette shade, so the components wear your colors.
Import the palette in oceanTheme.ts and point your tokens at its stops. Each token takes a [light, dark] pair — the shade to use in light mode and the one in dark mode.
ts// themes/ocean/oceanTheme.tsimport {defineTheme} from '@astryxdesign/core/theme';import {palette} from './tokens/ocean.palette';const {light, dark} = palette.ocean;export const oceanTheme = defineTheme({name: 'ocean',tokens: {'--color-accent': [light['45'], dark['70']],},});
Local imports must stay inside the theme folder. One that leaves it, such as ../../shared/colors, fails with invalid_theme, and every theme in the package disappears from theme list. npx @astryxdesign/cli theme template writes a file that explains every defineTheme field. For the full token set and component theming, read npx @astryxdesign/cli docs author-a-theme.
Build on another theme
To base a theme on an existing one, extends it: import the base theme and override only the tokens you change. Add the derived theme with integration add theme ocean-contrast, and import the base from its built package export, since local imports cannot leave the theme folder. The derived theme keeps a live link to the base and picks up its later changes when you rebuild it — unlike --from, which forks a copy (npx @astryxdesign/cli docs authoring/building-blocks/themes/add-a-theme).
ts// themes/ocean-contrast/oceanContrastTheme.tsimport {defineTheme} from '@astryxdesign/core/theme';import {oceanTheme} from '@acme/astryx-widgets/themes/ocean';export const oceanContrastTheme = defineTheme({name: 'ocean-contrast',extends: oceanTheme,tokens: {'--color-accent': ['#0051a3', '#4aa3ff'],},});
Keep light-dark() to colors
Each [light, dark] pair compiles to a light-dark() value, which switches only colors. Give it colors. For a value that is not a plain color — a gradient — put light-dark() on each color stop, not around the whole value: light-dark() takes two colors, so wrapping whole gradients makes the value invalid where the token is used, and theme build does not warn.
Do not set Core private variables
Core private variables start with --_, such as --_field-radius. They are internals and can change in any Core release, so theme build reports each one it finds as an [error]. Set the standard CSS property instead — borderRadius, padding — and let the build emit the internal variable where Core needs it.
Declare the peer dependencies
The theme imports @astryxdesign/core/theme, so declare Core as a peer dependency, with the range of Core versions you test the theme against — a range, not an exact version, so a Core patch release does not force a republish. integration add theme already declared the optional @astryxdesign/cli peer that reads themes:
json"peerDependencies": {"@astryxdesign/core": "^0.7.0","@astryxdesign/cli": ">=0.6.4"},"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}
Name the font
A theme names its typefaces in typography: body, heading, and code, each with a family and fallbacks. These set the --font-family-* tokens every component reads. Always give real fallbacks so text stays readable before the font loads, or if it never does. heading inherits family and fallbacks from body when you omit them.
ts// themes/ocean/oceanTheme.tsexport const oceanTheme = defineTheme({name: 'ocean',typography: {body: {family: 'Acme Sans', fallbacks: 'system-ui, sans-serif'},code: {family: 'Acme Mono', fallbacks: 'ui-monospace, monospace'},},// ...your tokens});
The full type scale and font roles are in npx @astryxdesign/cli docs author-a-theme.
Ship the font loader
Naming a family does not load it. When the theme uses non-system fonts, add <slug>.fonts.css beside the built module and production CSS, then export it as ./themes/<slug>.fonts.css. theme add imports that stylesheet with the built theme.
json"exports": {"./themes/ocean": "./themes/ocean/ocean.js","./themes/ocean.css": "./themes/ocean/ocean.css","./themes/ocean.fonts.css": "./themes/ocean/ocean.fonts.css"}
The stylesheet can contain self-hosted @font-face rules or import a hosted stylesheet. A hosted @import is simple, but it delays loading compared with a preconnected <link>; choose that trade-off deliberately. Doctor warns when it cannot prove the loader — a hosted @import, or no font stylesheet while the app loads the family itself — but the warning does not fail the app.
css/* themes/ocean/ocean.fonts.css */@font-face {font-family: 'Acme Sans';src: url('./fonts/acme-sans.woff2') format('woff2');font-weight: 100 900;font-style: normal;font-display: swap;unicode-range: U+0000-00FF, U+0131, U+0152-0153;}
- Load every weight and style the theme uses. Do not let the browser synthesize bold or italic.
- Include self-hosted font files and their licenses in the packed package.
- Keep CSS and font assets side-effectful so a bundler does not remove the loader.
- Run
integration verify; it fails when an exported font stylesheet cannot resolve, but it does not check the font files the stylesheet names.
Verify in an app
Before publishing, run integration verify. Then install the package in a clean app, run theme add, apply the generated theme, and open it in a browser. The source, packed exports, and applied result form one chain; check the last step too.
Textrenders in the named families at every weight and style — the italic face resolves, not a synthesized slant.- Every font request succeeds; nothing silently falls back to a system font.
- Color pairs still meet contrast in both light and dark mode.
Apply the theme
Install the integration, then run theme add with its slug and package. The command records the package owner and regenerates one app theme module that imports the built theme, production CSS, and optional font CSS. Use theme eject only to copy source into an independent local fork.
bashnpm install @acme/astryx-widgetsnpx @astryxdesign/cli theme add ocean --package @acme/astryx-widgets
tsximport {Theme} from '@astryxdesign/core/theme';import {themes, defaultThemeSlug} from './astryx-themes';<Theme theme={themes[defaultThemeSlug]}><App /></Theme>
theme list shows the available, added, and default themes. Use theme use <slug> to change the default, theme remove <slug> to stop importing one, and theme eject <slug> only when the app needs an independent source fork. Applying a theme — mode, SSR, and the production build — works the same for every theme; see npx @astryxdesign/cli docs use-a-theme.
Extend to customize
For ordinary customization, do not copy the package source. Import the built theme and derive a new one with defineTheme({extends: importedTheme, ...}), overriding only the tokens the app changes. Use theme eject only when the app must own an independent source fork. See npx @astryxdesign/cli docs authoring/building-blocks/themes/define-the-theme.
tsimport {defineTheme} from '@astryxdesign/core/theme';import {oceanTheme} from '@acme/astryx-widgets/themes/ocean';export const productTheme = defineTheme({name: 'product',extends: oceanTheme,tokens: {'--color-accent': ['#0051a3', '#4aa3ff']},});
In global mode
In global mode: install the theme package in each app that uses the theme; inside a project, the CLI ignores packages installed for the computer. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Extend the core theme topic
Give your theme a doc topic that extends the core theme topic, so an app that lists your package sees your theme at the end of npx @astryxdesign/cli docs theme. Add it with extends: 'theme' (npx @astryxdesign/cli docs authoring/building-blocks/docs/extend-or-replace).
javascript// docs/ocean-theme.doc.mjs/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */export default {type: 'generic',name: 'ocean-theme',extends: 'theme',title: 'Ocean theme',description: 'Use the Ocean theme from @acme/astryx-widgets.',sections: [{id: 'use-the-ocean-theme',title: 'Use the Ocean theme',content: [{type: 'prose',text: 'Install @acme/astryx-widgets, then run astryx theme add ocean --package @acme/astryx-widgets and apply it from the generated app theme module.',},],},],};
Keep the section short: how to install the package, run theme add, apply the generated module (npx @astryxdesign/cli docs authoring/building-blocks/themes/use-a-theme-in-an-app), and customize with extends. When the theme uses a custom font, name the families and the optional exported font stylesheet that theme add imports (npx @astryxdesign/cli docs authoring/building-blocks/themes/fonts-and-assets). Extend theme rather than a topic another package replaces, or an app that loads that package after yours drops your section.
Not agent guidance
Do not put theme usage in agentDocs. Agent lines land in every app's agent file and are for guidance needed every session; a theme's install-and-use steps belong in a doc topic that people and agents read on demand. See npx @astryxdesign/cli docs authoring/building-blocks/configuration/agent-guidance.
Docs
In global mode: docs from an integration installed for the computer show up outside projects; inside a project, only docs from integrations the project installs. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Add a topic with the CLI
Run integration add doc in your package to add a topic. It writes the topic file and declares the docs root in astryx.integration.mjs.
bashnpx @astryxdesign/cli integration add doc deploying# Read it the way an app willnpx @astryxdesign/cli docs deploying
textdoc contribution added[ok] deployingDeclare doc root ./docs in astryx.integration.mjs.- docs/deploying.doc.mjs- astryx.integration.mjs
- Name the topic in lowercase kebab-case, such as
deploying. Readers type the name as a command argument, so it holds only letters, digits,_, and-. - Keep the name stable: readers and links find the topic by it.
- Pick a name
npx @astryxdesign/cli docsdoes not already list: no Core topic, and no section such astokensorlayout. To take over or add to a Core topic, seenpx @astryxdesign/cli docs authoring/building-blocks/docs/extend-or-replace.
Write the sections
A topic is a plain object with type: 'generic', a name, a title, a one-sentence description, and sections. Each section has a title and a list of content blocks.
javascript/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */export default {type: 'generic',name: 'deploying',title: 'Deploying',description: 'Ship an app built with Acme widgets.',sections: [{id: 'build-before-you-ship',title: 'Build before you ship',content: [{type: 'prose', text: 'Build the app, then upload the `dist` folder.'},{type: 'code', lang: 'bash', code: 'npm run build'},{type: 'list', style: 'unordered', items: ['Keep `dist` out of git.']},{type: 'table', headers: ['Variable', 'Value'], rows: [['`NODE_ENV`', '`production`']]},],}],};
The sample sets a section id, so package.json needs the CLI peer "@astryxdesign/cli": ">=0.6.4". Published 0.6.3 and earlier reject id and hide every doc topic your package ships, and integration verify fails without that peer; see npx @astryxdesign/cli docs authoring/ship/versioning.
- Replace the
Overviewplaceholderintegration addwrites. - Every field is in
npx @astryxdesign/cli docs authoring reference-doc.
Write commands without a runner
Start every CLI command in a doc with the word astryx alone, with no runner such as npx or pnpm exec in front. The CLI prefixes each one when it prints the doc, with the command the reader runs: pnpm exec astryx or yarn astryx from their package manager, or the cliCommand an integration sets. See npx @astryxdesign/cli docs authoring/building-blocks/configuration/cli-command. doctor integration validate (and so integration check) lists each command written with a runner as a runner_command note, with its file, line and the bare command to write. A note fails nothing. An install step, which runs before the CLI is installed, is the one exception: write the scoped package name, @astryxdesign/cli, after the download runner. The unscoped name after a download runner is always listed, because it fetches an unrelated package.
javascript{type: 'prose', text: 'Check the app with `astryx doctor` before you ship.'},{type: 'code', lang: 'bash', code: 'astryx doctor'},
textCheck the app with `acme astryx doctor` before you ship.```bashacme astryx doctor```
- The CLI prefixes an inline code span that starts with the word astryx, a command at the start of a line, or after
&&,||,;or|, in abash,sh,shell,zsh,console,textormarkdowncode block or one with no language, and a table cell that holds only a command. Codein other languages, such as ajsexample, and the word astryx in plain prose stay as written.--jsonreturns the doc as written, so a reader that shows it prefixes the commands itself.
Pick the doc kind
Every doc is a .doc.mjs file whose type says what it describes. integration add writes the right type and file for each kind.
| You document | `type` | File that `integration add` writes |
|---|---|---|
| A guide or topic | 'generic' | docs/deploying.doc.mjs |
| Your package's docs section | 'namespace' | docs/acme.doc.mjs |
| A component | 'component' | components/AcmeCarousel.doc.mjs, beside AcmeCarousel.tsx |
| A template | 'page' or 'block' | templates/acme-dashboard.doc.mjs, beside acme-dashboard.tsx |
| A theme | 'theme' | themes/ocean/oceanTheme.doc.mjs, beside oceanTheme.ts |
Only guides, topics, and your docs section go in the docs root. The others have their own guides: npx @astryxdesign/cli docs authoring/building-blocks/components, npx @astryxdesign/cli docs authoring/building-blocks/templates, and npx @astryxdesign/cli docs authoring/building-blocks/themes.
Keep topics in the docs root
The docs field in astryx.integration.mjs names the folder that holds your topics. The CLI reads every .doc.mjs file under it, in subfolders too.
javascriptexport default {docs: './docs'};
- Readers open a topic by its
name, not its file name. Name the file after the doc,<name>.doc.mjs, so each one is easy to find. - A topic with no
placementis a flat topic: the docs list shows it under Topics, and readers open it by its name. - For a section of your own in the docs tree, see
npx @astryxdesign/cli docs authoring/building-blocks/docs/sections-and-placement.
In global mode
In global mode: when users install your package for their computer, beside the global CLI, your doc topics show up in folders outside any project; inside a project, only if the project installs your package. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Add a docs section
Give your package its own section in the docs tree with integration add doc <name> --parent <section>. The first run also writes the section's namespace doc.
bashnpx @astryxdesign/cli integration add doc deploying --parent acme# Open your sectionnpx @astryxdesign/cli docs acme
javascript/** @type {import('@astryxdesign/cli/authoring').NamespaceDoc} */export default {type: 'namespace',name: 'acme',title: 'Acme',summary: 'Guides for Acme.',slots: {guides: {title: 'Guides', accepts: {kinds: ['generic']}},},};
- Edit its
titleandsummary: readers see them in the docs list and at the top of your section. - The guide,
docs/deploying.doc.mjs, getsplacement: {parent: 'namespace:acme', slot: 'guides'}. Later runs with--parent acmereuse the namespace doc. package.jsongets the optional peer"@astryxdesign/cli": ">=0.6.4", because an older CLI does not read sections; seenpx @astryxdesign/cli docs authoring/ship/versioning.
Place a doc
A guide names its one home with placement: a namespace of your package, a slot in it, and an order. Its route is the section name, then the guide name.
javascriptplacement: {parent: 'namespace:acme', slot: 'guides', order: 10}, // route: acme/deploying
- Every field is in
npx @astryxdesign/cli docs authoring authored-doc-graph-fields. - A placed guide opens only by its route,
acme/deploying. Its bare name no longer opens it.
bashnpx @astryxdesign/cli docs acme/deploying
Fix a failed placement
A failed placement hides the doc: it gets no route and does not show in the docs list. doctor integration docs fails with invalid_doc_graph and names what to fix.
bashnpx @astryxdesign/cli doctor integration docs
textseverity: [fail]code: invalid_doc_graphmessage: @acme/astryx-widgets/deploying.doc.mjs: placement.parent "namespace:cli" names no namespace; @acme/astryx-widgets declares "acme".
- A
parentyour package does not ship, such asnamespace:cli, "names no namespace". - A
slotthe namespace does not declare "is not a slot of namespace"; the message lists the declared slots. - The check exits 1; see
npx @astryxdesign/cli docs authoring/building-blocks/docs/check-your-docs.
Link another doc
Write {@link [provider:]kind:name} in prose, list items, and table cells to link another doc. The CLI prints the command that opens it, so the link keeps working when the doc moves.
javascript{type: 'prose', text: 'Before you ship, read {@link generic:deploying}.'},{type: 'list', style: 'unordered', items: ['All guides: {@link namespace:acme}.']},{type: 'table', headers: ['Task', 'Guide'], rows: [['Ship', '{@link generic:deploying}']]},
Each link reads as a command. This link, npx @astryxdesign/cli docs authoring/building-blocks/docs/extend-or-replace, opens the next guide.
- The kind is
genericfor a topic or guide,namespacefor a docs section,commandorfunctionfor a CLI command or API function,schemaorenumfor a schema or a fixed set of values,componentfor a component, andpageorblockfor a template. - Name a component by its export name and a template by its id. Its link reads as the command that opens it, such as
npx @astryxdesign/cli component AcmeCarousel --package @acme/widgets. - Inside backticks or a code block, link syntax prints as written.
Link another package's docs
A link without a provider resolves against your own package. To link the CLI's docs, or another package's, start the target with that package's name, such as @astryxdesign/cli:.
javascript// Resolves: the CLI's doctor command{type: 'prose', text: 'Check the app with {@link @astryxdesign/cli:command:doctor}.'},// Does not resolve: looks for a doctor command in your package{type: 'prose', text: 'Check the app with {@link command:doctor}.'},
- Name a CLI command the way you type it, spaces included, such as
@astryxdesign/cli:command:doctor integration docs. - In a topic that
extendsanother package's topic, your sections still resolve against your package, so a link to the base topic's docs needs its provider.
Fix a link that names no doc
A link that names no doc prints as written, and doctor integration docs warns. The warning names a search that finds the right target.
bashnpx @astryxdesign/cli doctor integration docs
textseverity: [warn]code: invalid_doc_graphmessage: acme/deploying § check-before-you-ship: "command:doctor" names no doc. Find it with `npx @astryxdesign/cli search doctor --type doc`, then name it as `[<provider>:]<kind>:<name>`.
- The warning keeps exit code 0, so read the report before you ship; see
npx @astryxdesign/cli docs authoring/building-blocks/docs/check-your-docs. - Published 0.6.4 and later read links. An older CLI, such as published 0.6.3, prints each one as written; see
npx @astryxdesign/cli docs authoring/ship/versioning.
Replace a topic
Set replaces to take over an existing topic, such as Core's getting-started. Readers who open the old name get your topic.
bashnpx @astryxdesign/cli integration add doc acme-getting-started --replaces getting-started# The old name now opens your topicnpx @astryxdesign/cli docs getting-started
javascriptexport default {type: 'generic',name: 'acme-getting-started',replaces: 'getting-started',title: 'Acme getting started',description: 'Install Acme widgets and render your first carousel.',sections: [/* ... */],};
The docs list shows your topic in place of the old one. Because your topic has its own name, the old name keeps resolving to it, so links and agents that learned the old name still land on your topic.
Extend a topic
Set extends to merge sections into an existing topic instead of owning it. A section replaces the base section it matches, and a section that matches none is added at the end.
bashnpx @astryxdesign/cli integration add doc acme-theming --extends themenpx @astryxdesign/cli docs theme --index
javascriptexport default {type: 'generic',name: 'acme-theming',extends: 'theme',title: 'Acme theming',description: 'Theme an app that uses Acme widgets.',sections: [{id: 'quick-start', title: 'Quick Start', content: [/* replaces the base section */]},{id: 'use-the-ocean-theme', title: 'Use the ocean theme', content: [/* added at the end */]},],};
- Read the base topic's keys with
--index, and give a section that replaces one that key as itsid. A section without anidmatches only a base section with exactly its title, or one whoseidis the key its title makes, soIntegration Themesis added beside a baseIntegration themesthat has noid. - The topic keeps the base's title and description, and readers open it by the base's name.
- Replace the
Overviewplaceholder thatintegration addwrites, or it is added to the base topic. - Extend a topic to correct or add to it. A copy made with
replacesstops getting the owner's fixes.
Check overlaps with Core topics
A topic sets replaces or extends, never both, and a placed guide sets neither. A topic that uses a Core topic's name with neither is an accidental conflict: apps keep reading the Core topic.
bashnpx @astryxdesign/cli doctor integration docs
textseverity: [info]topic: acme-getting-startedrelationship: replacescoreTopic: getting-startedmessage: Intentional override: "acme-getting-started" replaces the Core topic "getting-started".severity: [fail]topic: colorrelationship: accidentalcoreTopic: colormessage: Accidental conflict: "color" is already a Core topic. Rename it, declare replaces: 'color' to take it over, or declare extends: 'color' to merge sections.
- An intentional overlap prints as
[info]. An accidental one fails with exit code 1. - A topic that sets both fails as
invalid_doc, and so does a placed guide that sets either one.
In global mode
In global mode: a doc that extends or replaces a topic from a package installed for the computer applies in folders outside any project; inside a project, only those from packages the project installs apply. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Keep each read short
Readers open one section at a time, so give each section one idea and keep it to about 30 lines. npx @astryxdesign/cli doctor warns on any read over 32 KB.
- When a section needs a second idea, split it into two sections.
- Keep a topic to a few sections. When it grows past five, split it into more guides in your docs section.
- A topic with more than one section reads as its section list. Readers open one section by its key, or the whole topic with
--full.
bash# The section listnpx @astryxdesign/cli docs acme/deploying# One sectionnpx @astryxdesign/cli docs acme/deploying check-before-you-ship# Everythingnpx @astryxdesign/cli docs acme/deploying --full
Lead with the summary
A section's first prose block, or its first list item, is its summary in section lists and search results. Make it answer the section's question in one or two sentences.
- The summary is cut at about 240 characters.
- A code block first does not count: the summary comes from the next prose block.
- Open with the answer, not with background.
textbuild-before-you-ship Build before you ship - Build the app, then upload the `dist` folder.
Make docs findable
In a topic with more than one section, search ranks a section whose title matches the query, or that names it in backticks, above words in body text. Title each section with the task a reader searches for.
- Name the task in the words a reader types, such as "Deploy to production". Avoid titles such as "Overview" or "Details".
- Write field names, file names, and error codes in backticks, such as
deployTarget: in a topic with more than one section, search treats each one as a keyword. - Other words in the summary and body match too, but rank below titles and identifiers. The summary shows under each hit, so make it answer the query.
Test with search
Test a doc the way a new reader finds it: search for the question, and check that the first hit answers it. Quote a query of more than one word.
bashnpx @astryxdesign/cli search "place a doc" --type doc
- This query returns the section of these guides that shows how to place a doc.
- Run the same search in an app that installs your package; see
npx @astryxdesign/cli docs authoring/ship/test-in-an-app.
Run the docs check
Run doctor integration docs in your package to check the docs tree, every link, and overlaps with Core topics, and to list any passage two topics repeat. Pass a package name to check an installed package.
bashnpx @astryxdesign/cli doctor integration docs# In an app, check an installed packagenpx @astryxdesign/cli doctor integration docs @acme/astryx-widgets
textChecking integration docs: @acme/astryx-widgets@1.0.0[ok] The docs tree and every link in these docs check out.[ok] No doc topics overlap with Core.[ok] No topic copies a passage from another.[ok] No filler phrases or low-density topics.
Write each fact in one topic. When two of your topics, or one of yours and a Core topic, repeat 10 or more words of prose word for word, the check lists the pair as a copied_passage warning. It names the topic that keeps the text (a Core topic, or of your two the one the passage fits) and the line in the other where to put the link instead (npx @astryxdesign/cli docs authoring/building-blocks/docs/links). Code and examples are not compared, and a topic may carry the text of the Core topic it replaces. A run with fewer than 4 distinct content words (words other than function words such as "the", "in" or "it"), or one that three or more topics share (a series lead-in), is not listed. Each listed pair is also a warning (copied_passage): doctor integration docs still exits 0, and integration check --strict fails until you fix it.
Keep docs dense. The check lists, as a doc_fluff warning, each padding phrase in your prose, with its file line and what to write instead, and each topic of 80 or more prose words in which fewer than half the words carry content (words other than function words such as the, and, in), with its measured share and the floor of 0.5. doctor integration docs still exits 0, and integration check --strict fails until you cut it. Code, examples, and quoted text are not read. The phrases: it's important to note, it is important to note, important to note that, it's worth noting, it is worth noting, worth noting that, it should be noted, please note, note that (opening a sentence), as mentioned above, as mentioned earlier, as mentioned before, as noted above, as you can see, needless to say, in order to, due to the fact that, for the purpose of, at this point in time, in other words, basically, essentially, obviously.
Point at another doc with a link, never by writing npx @astryxdesign/cli docs <name> in prose: {@link generic:<name>} for one of your package's topics, or {@link @astryxdesign/cli:generic:<name>} for a Core doc (namespace: for a Core namespace). The CLI prints the link as the command that opens the doc, and doctor checks that the doc exists. The check lists each written pointer to a doc that exists as a stray_doc_pointer warning, with the link to write instead. A command with more words, or one in a code block, is a walkthrough and is not listed.
Its arguments and exit codes are in npx @astryxdesign/cli docs cli/commands/doctor-integration-docs.
Know what fails the check
A doc that does not load, an accidental Core overlap, a reference block that names no doc, or a failed namespace or placement fails the check with exit code 1. A link that names no doc only warns, and the exit code stays 0.
| Problem | Reported as | Exit code |
|---|---|---|
| A doc that does not load, such as an unknown section field or block type | [fail] invalid_doc | 1 |
A topic with a Core topic's name and no replaces or extends | [fail] accidental | 1 |
| A placement that fails, which hides the doc | [fail] invalid_doc_graph | 1 |
A reference block whose target names no doc | [fail] invalid_doc_reference | 1 |
| A link that names no doc | [warn] invalid_doc_graph | 0 |
A topic that sets replaces or extends | [info] replaces or extends | 0 |
Read the warnings before you publish. With --json, they are in data.issues, with severity: "warning".
Check each doc against its type
doctor integration validate reads every doc your package ships against the type @astryxdesign/cli/authoring publishes for its kind, the way tsc would: a hidden doc and a second doc for one name too. Test fixtures under __tests__ in the components or docs root, which those readers skip, are counted, not checked. It reports a field the type does not have (unknown_field), a missing required field (missing_field), and a value outside the field's type (wrong_type).
bashnpx @astryxdesign/cli doctor integration validate
textChecking 1 doc(s) against the authoring types (TypeScript 6.0.3)severity: [warn]doc: components/AcmeCarousel.doc.mjsline: 7export: defaulttype: ComponentDocfield: summaryproblem: unknown_fieldmessage: Object literal may only specify known properties, and 'summary' does not exist in type 'SingleComponentDoc'.fix: Remove `summary`: SingleComponentDoc has no field `summary`.
- The folder decides the type:
ComponentDocin your components root,IntegrationTemplateDocin your templates root,ThemeDocin your themes root, and in your docs root the type the doc'stypenames. Keep the@typecomment above each doc's export too, so your editor shows the same problems as you type. - Every finding is a warning, so the exit code stays 0. With
--json, they are indata.docTypes.findings. - It uses the
typescriptyour package installs. Wheretypescriptis missing or has no JavaScript compiler API, as TypeScript 7 does, it uses@typescript/typescript6, the TypeScript 6 API package, if your package installs it. Without either, it prints[skip] Docs not type-checkedand the reason. - Every field of every doc type is in
npx @astryxdesign/cli docs authoring/field-reference.
Check read size
npx @astryxdesign/cli doctor also measures every read and warns on one over 32 KB. Run it in your package or in an app that installs it; doctor integration docs does not check size.
bashnpx @astryxdesign/cli doctor
textid: docs-progressive-disclosurestatus: [warn]label: Documentation navigation and sizemessage: acme/deploying build-before-you-ship: 46 KB, over the 32 KB one read may return
Split a section over the limit into smaller ones, each with its own key. See npx @astryxdesign/cli docs cli/commands/doctor.
Check the CLI peer
integration verify fails a package that ships a docs section, a placed guide, or a doc section with an id without an @astryxdesign/cli peer of >=0.6.4. It does not run the docs check, so run both.
bashnpx @astryxdesign/cli integration verify
text- [fail] The package ships a namespace doc or a placed guide but declares no @astryxdesign/cli peer. A stable CLI before 0.6.4 does not read the docs tree, and can hide every doc topic the package ships. Declare "@astryxdesign/cli": ">=0.6.4" in peerDependencies (optional in peerDependenciesMeta, if the CLI is not required).
integration add doc --parentwrites the peer for you.integration verifypasses a hidden guide and an accidental Core overlap; onlydoctor integration docscatches them.- Everything else it checks is in
npx @astryxdesign/cli docs cli/commands/integration-verifyandnpx @astryxdesign/cli docs authoring/ship/checks.
Add a codemod
integration add codemod writes a codemod into a folder named after a version. Apps run it with npx @astryxdesign/cli upgrade to migrate their code.
bashnpx @astryxdesign/cli integration add codemod rename-delay --to 0.7.0
textcodemods/0.7.0/rename-delay.mjs # the codemodrename-delay.test.mjs # skipped: a test file__tests__/ # skipped: a test folder
The first add declares codemods: './codemods' in astryx.integration.mjs. The codemod's id is its path inside the version folder, without the extension: rename-delay. An id must be unique across all version folders in the package.
The loader skips *.test.*, *.spec.*, and *.fixture.* files and everything under __tests__/ or __fixtures__/, so tests can sit beside the codemod. The folder name decides when an app runs the codemod; see "Choose when a codemod runs".
Write the transform
A codemod default-exports a plain object with a type, a title, and a transform function. transform returns the new source, or null to leave the file as it is.
js// codemods/0.7.0/rename-delay.mjs/** @type {import('@astryxdesign/cli/authoring').AstryxCodemod} */export default {type: 'code',title: 'Rename AcmeCarousel delay to interval',description: 'Renames the delay prop on AcmeCarousel.',fileExtensions: ['.tsx', '.jsx'],transform(file, api) {const j = api.jscodeshift;const root = j(file.source);const props = root.find(j.JSXOpeningElement, {name: {name: 'AcmeCarousel'}}).find(j.JSXAttribute, {name: {name: 'delay'}});if (props.size() === 0) return null;props.forEach(path => {path.node.name.name = 'interval';});return root.toSource();},};
type: 'code' rewrites the app's source files that match fileExtensions. type: 'config' rewrites the app's astryx.config file instead, and runs before code codemods. title shows in the upgrade output, and api.jscodeshift is a jscodeshift instance for the file. Every field is in npx @astryxdesign/cli docs authoring codemod.
Choose when a codemod runs
Two rules decide whether an app's npx @astryxdesign/cli upgrade runs your codemods: the app's Core versions, and whether the app names your package.
- Version folders are matched against the app's
@astryxdesign/coreversions, not your package's version.upgrade --from <version>runs each folder above--from, up to and including the Core version installed in the app. With Core 0.7.0 installed,--from 0.6.3runs0.7.0/, and--from 0.7.0runs nothing. A folder named after your own release, such as1.0.0/, waits until the app has Core 1.0.0. upgraderuns your codemods only when the app lists your package inintegrationsin itsastryx.config, or passes--integration @acme/astryx-widgets. Having your package installed is not enough: the run then skips your codemods with no warning.
Run codemods in an app
An app previews codemods with npx @astryxdesign/cli upgrade and writes the changes with --apply. Without --apply, nothing on disk changes.
bash# Preview each codemod and the files it would changenpx @astryxdesign/cli upgrade --from 0.6.3 --integration @acme/astryx-widgets# Write the changesnpx @astryxdesign/cli upgrade --from 0.6.3 --integration @acme/astryx-widgets --apply
textIntegrations: @acme/astryx-widgets<n> codemods to run (dry run)Applying integration codemods...Rename AcmeCarousel delay to interval (v0.7.0, @acme/astryx-widgets)! ~ src/Hero.tsx (would change)
<n> counts Core's codemods for the same versions too: they run first and print above Integrations:. To run only yours, as when you test it, add --codemod rename-delay.
--from is the Core version the app had before it upgraded. The run scans ./src unless the app passes --path, and it never writes a file the app marks as generated, vendored, or ignored; see npx @astryxdesign/cli docs cli/commands/upgrade. upgrade --list shows only Core codemods.
An integration manifest has no hooks field. Commands that run after codemods, such as a formatter, are the app's to set, in hooks.postCodemod in its astryx.config.
In global mode
In global mode: inside an app, upgrade runs with the release equal to the app's Core, whichever copy of the CLI the user starts, and only the codemods of integrations the app installs run. Outside a project, upgrade lists codemods and migrates nothing. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs cli/commands/upgrade.
Configuration
In global mode: installed for the computer, beside the global CLI, your package's manifest is read as it is in a project and applies in folders outside any project; peer ranges are checked against the global CLI's release. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Beyond the things you ship, an integration can set how it behaves in an app: the guidance it gives the AI agents working there, the command its users run the CLI with, and the record it keeps of each CLI run. Open one below to set it up.
Add a line of guidance
integration add agent-doc adds one line for AI agents that work in apps using your package. Apps show it in their agent file, such as AGENTS.md.
bashnpx @astryxdesign/cli integration add agent-doc 'Use AcmeCarousel for rotating content.'
The line lands in agentDocs.append in your manifest:
js// astryx.integration.mjsexport default {components: './components',agentDocs: {append: ['Use AcmeCarousel for rotating content.'],},};
Adding the same line again changes nothing. To reword or remove a line, edit agentDocs.append by hand.
Keep within the limits
Every line costs space in each app's agent file, so the CLI keeps guidance short and few. Put detail in your docs instead.
| Limit | What happens past it |
|---|---|
| 8 lines per package | integration add fails: append may contain at most 8 lines. |
| 240 characters per line | integration add fails: must contain at most 240 Unicode code points. |
| 32 lines per app, from all its packages | init writes no agent block and prints Could not install agent docs. |
A line is one line of plain text. Line breaks, control characters, spaces at either end, and managed-block markers such as <!-- ASTRYX:START --> are rejected.
Start a command in a line with the word astryx alone, with no runner in front, as in: Run astryx doctor before finishing. The block names the command the app runs once, at its top, for every command below it, so your line works in every app. See npx @astryxdesign/cli docs authoring/building-blocks/configuration/cli-command.
See what agents read
An app's npx @astryxdesign/cli init writes your lines at the end of its managed agent block, each labeled with your package name. The CLI owns the heading, the labels, and the markers.
bashnpx @astryxdesign/cli init --features agents
textINTEGRATIONS:- `@acme/astryx-widgets`: Use AcmeCarousel for rotating content.<!-- ASTRYX:END -->
The block collects lines from every integration the app has installed, with or without an astryx.config entry.
Refresh the block in an app
The block changes only when the app runs init, or upgrade --from <version> --apply. After an app installs a version of your package with new lines, it runs one of them.
bashnpx @astryxdesign/cli init --features agents# Or: --from takes the Core version the app had beforenpx @astryxdesign/cli upgrade --from <version> --apply
text[ok] Agent docs refreshed -> AGENTS.md
upgrade --apply prints that line; init prints [ok] AI agent docs installed -> AGENTS.md. Without --apply, upgrade only reports Agent docs differ from the installed Astryx and integration configuration. It refreshes the block even when Core did not change. See npx @astryxdesign/cli docs cli/commands/upgrade.
In global mode
In global mode: the agent guide the CLI keeps for the computer carries no integration's guidance, because it serves every folder; your guidance reaches agents through the agent docs init writes in an app that installs your package. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs global/computer-agent-guide.
When to set it
The CLI ends most answers with the next command to run: a hint, an error fix, a Doctor fix, or a doc example. By default each one starts with a command that reaches the release that answered: what the app's package manager runs, such as pnpm exec astryx or yarn astryx, when the app's own CLI answered, and otherwise the CLI package pinned to that release, such as npx @astryxdesign/cli@0.7.2.
Set cliCommand only when your users run the CLI some other way, such as a wrapper your company ships that runs it with your package loaded. Otherwise the commands the CLI prints send them, and their agents, to a CLI that answers without your package. Most packages leave it out.
Set it
Add cliCommand to astryx.integration.mjs: the command, without a subcommand.
js// astryx.integration.mjsexport default {components: './components',cliCommand: 'acme astryx',};
In an app that installs your package, the CLI then prints every command with it:
textError: Theme "ocean" is not added. Run `acme astryx theme add ocean` first.
That covers hints, errors, suggestions, Doctor fixes, --help, the setup reminder, the agent block init writes, and doc text. --json output keeps every command without a runner, as in "command": "astryx theme add ocean", so a script reads the same fields in every app.
How the CLI picks the command
- The
cliCommandof the packages the app loads, when they name one command. - The command a host tool names when it runs the CLI for the app. Nothing in a plain project names one.
- A command that reaches the release that answered: what the app's package manager runs when the app's own CLI answered, such as
pnpm exec astryxoryarn astryx;npx @astryxdesign/clioutside a project when the CLI installed for the computer answered; otherwise the CLI package pinned to that release, such asnpx @astryxdesign/cli@0.7.2(seenpx @astryxdesign/cli docs global/who-answers).
Two packages that set the same command agree, and the CLI uses it.
When two packages disagree
If two packages an app loads set different commands, the CLI uses neither: it prints commands with what the app's package manager runs, even when a host tool named a command. npx @astryxdesign/cli doctor warns in its cli-command check and names both packages:
textstatus: [warn]label: CLI commandmessage: Loaded integrations declare different CLI commands: @acme/astryx-widgets (`acme astryx`), @other/kit (`other astryx`). Neither is used, so printed commands start with `pnpm exec astryx`.
The app fixes it by removing the package that does not belong, or the packages agree on one command.
Check it
An invalid value is ignored, and the rest of your manifest still loads. npx @astryxdesign/cli doctor integration validate reports it as invalid_cli_command. A valid cliCommand is one line of plain text without spaces at either end, at most 120 characters.
In an app that installs your package, npx @astryxdesign/cli doctor passes its cli-command check and names your package as the one that set the command. A CLI released before this field reports cliCommand as an unknown key and prints the package manager's command.
bashnpx @astryxdesign/cli doctor integration validate# In an app that installs the packagenpx @astryxdesign/cli doctor
In global mode
In global mode: outside a project, the cliCommand of integrations installed for the computer applies; when none is declared, commands start with the plain command of the CLI installed for the computer. Inside a project, only the project's integrations count, so a cliCommand from a package installed for the computer never changes the project's printed commands. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Record runs with debug
Export a debug function from astryx.integration.mjs; the CLI calls it once per command run in an app loading your package.
js// astryx.integration.mjsimport {appendFileSync} from 'node:fs';/** @param {import('@astryxdesign/cli/authoring').DebugEvent} event */export function debug(event) {if (event.outcome !== 'ok') {appendFileSync('acme-failed-runs.ndjson', JSON.stringify(event) + '\n');}}export default {components: './components',};
The event is a DebugEvent with command, outcome, exitCode, durationMs, error, and more, its values scrubbed (redacted: true). Every field is in npx @astryxdesign/cli docs authoring debug-event.
Keep the function synchronous: the CLI calls it as the process exits and never waits for a promise. The app's own debug handler runs first, then yours. A handler that throws is skipped, and the command's output and exit code stay the same.
An app records every command only when its astryx.config names integrations or debug, as listing your package does. Otherwise your handler runs only for commands loading the app's project, such as component and docs, never for --version or a mistyped command. In an app whose config names neither word, those commands also print a stderr warning.
debug is a named export, not a manifest field, so an older CLI ignores it and loads the rest of your manifest.
Turn off debug in an app
An app can refuse every integration's debug handler and keep its own. It sets inheritDebug in its package.json:
json{"astryx": {"inheritDebug": false}}
Your handler then no longer runs in that app.
Handle gap reports
Export a gapReport handler, and npx @astryxdesign/cli gap-report in an app sends it each gap report, such as a missing component or variant. The handler files the report and returns a receipt.
js/** @type {import('@astryxdesign/cli/authoring').GapReportHandler} */export const gapReport = {audience: 'public',async handle(report, {signal}) {if (report.target.package !== '@acme/astryx-widgets') return {status: 'skipped'};const body = JSON.stringify(report);const response = await fetch('https://tracker.example.com/issues', {method: 'POST', body, signal});const {url} = await response.json();return {status: 'filed', url};},};
Every handler in the app gets every report, so check report.target.package and skip reports about other packages.
Every field is in npx @astryxdesign/cli docs authoring gap-report-handler.
The CLI waits 30 seconds, then aborts signal. A throw, a timeout, or an invalid receipt fails your delivery, and the command exits 1; the other handlers still run. Like debug, gapReport is a named export that older CLIs ignore.
Ask before filing in public
Set audience: 'public' when your handler writes somewhere the public can read. The CLI runs it only when the caller passes --confirm-public; an 'internal' handler always runs.
bashnpx @astryxdesign/cli gap-report AcmeCarousel --category missing_variant --reason 'Need a vertical layout'
texthandlerType: integrationhandler: @acme/astryx-widgetsaudience: publicstatus: consent_requiredmessage: Rerun with --confirm-public to file this report.
With --confirm-public, the same delivery reads status: filed and shows your url. The report goes to the package named by --package, else the package that owns the component, else Core.
Fall back to issuesUrl
With no gapReport handler in the app, the CLI routes the report to the target package's manifest issuesUrl.
- A GitHub issues URL, such as
https://github.com/acme/widgets/issues, gets an issue filed with the GitHub CLI,gh, once the caller passes--confirm-public. - Any other URL comes back as a
routed_onlyreceipt for the caller to open. - With no
issuesUrl, the command fails:Package "@acme/astryx-widgets" provides neither a report handler nor an issues URL.
One handler anywhere in the app, from the app or from any package, turns the fallback off for every report. So does a listed package that fails to load: the CLI cannot tell whether it has a handler, so the report fails for that package instead of going to another tracker. See npx @astryxdesign/cli docs cli/commands/gap-report.
Let the person choose where a report goes
When an app loads packages naming their own issuesUrl, a report can go to the default (the handlers, or the fallback above) or to one of those packages. npx @astryxdesign/cli gap-report --list-categories and the receipt of a report filed without --to list every destination, with a line telling the agent to ask the person.
- An agent asks the person where to file. It never picks a package destination on its own.
--to <package>carries the person's choice: the report goes only to that package'sissuesUrl, under the same--confirm-publicrule, and no handler runs.- Without a choice, leave out
--to; the report goes to the default. - A
--tothat names no destination fails before anything is sent and lists the destinations.
In global mode
In global mode: when users install your package for their computer, beside the global CLI, its debug function receives runs in folders outside any project too; inside a project, only if the project installs it. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
First: your peer ranges must include the global CLI's version
Your @astryxdesign/core (and, if you use it, @astryxdesign/cli) peerDependencies ranges must include the version of the CLI installed globally. If they do not, Astryx skips your package outside projects, and npx @astryxdesign/cli doctor warns and names the range.
Inside a project that does not install your package, npx @astryxdesign/cli discover offers it, with the command that adds it to that project, once your peer range admits the project's release.
Where your integration works
- Installed in an app: it works in that app, as always, whichever
npx @astryxdesign/clithe person runs, because the app's version of Astryx answers there. - Installed next to the global install (
npm i -g @astryxdesign/cli your-package), or in the same folder as the CLI by a tool that installs them together: it works in every folder outside a project. People and agents there see its components, templates, docs, themes, codemods, and discover source. - Installed only globally, inside an app that does not install it: it adds nothing there. When it supports the app's version and loads,
npx @astryxdesign/cli discoverlists it as one the app could add, with the app's install command. - Installed both ways: the app's copy is the one used in that app. In your own package folder, your working copy is used.
Astryx loads a global integration with the same loader and checks as an app's, so nothing extra goes in your manifest.
Checklist
- Declare the
@astryxdesign/core(and, if you use it,@astryxdesign/cli)peerDependenciesrange your package really supports. Outside a project, Astryx skips your package when that range leaves out the global version, andnpx @astryxdesign/cli doctorsays why: too narrow, and global users lose it after an upgrade; too wide, and it loads against a Core it may not build with. - Keep imports in your docs and templates to packages an app installs with yours, since outside a project there is no app to check them against.
- If you set
cliCommand, know that outside a project it starts every command Astryx prints, and that two global integrations naming different commands cancel out, falling back tonpx @astryxdesign/cli(npx @astryxdesign/cli docs authoring/building-blocks/configuration/cli-command). Inside an app, only the app's integrations count. - Try it globally before you publish, with the steps below.
Try it globally
Pack your package, install it next to a global CLI, and look at it from an empty folder:
bashnpm packnpm i -g @astryxdesign/cli ./your-package-1.0.0.tgzmkdir -p /tmp/try-global && cd /tmp/try-globalnpx @astryxdesign/cli discovernpx @astryxdesign/cli docsnpx @astryxdesign/cli doctor
npx @astryxdesign/cli discoverlists your package as installed, with what it adds.npx @astryxdesign/cli docslists your topics, andnpx @astryxdesign/cli search <one of your component names>finds them.npx @astryxdesign/cli doctor'scomputer-integrationscheck lists your package as used, with no warning.- In an app that does not install it,
npx @astryxdesign/cli discoverlists it as one the app could add, with the app's install command, and none of its components or docs show up.
Remove it again with npm uninstall -g your-package.
More on global mode
How global mode works for the people who install your package: npx @astryxdesign/cli docs global. Their side of global integrations: npx @astryxdesign/cli docs global/computer-integrations.
Install the packed package in an app
Pack the package beside its folder, so the next pack does not ship it, then install it in an app together with Core and the CLI. The app loads your package because it is a dependency.
bash# In the packagenpm pack --pack-destination ..# In an app folder beside itnpm install @astryxdesign/core @astryxdesign/cli ../acme-astryx-widgets-1.0.0.tgz
- Install the CLI in the app too:
npx @astryxdesign/cliruns the CLI from the app'snode_modules. - The app holds a copy, not a link. After each change, pack again and run the same
npm install. - Reading your work inside the package shows your source. The app shows what npm would publish.
Check what the app sees
From the app folder, run the commands your users run. Each should show your contribution under your package name.
bashnpx @astryxdesign/cli component AcmeCarouselnpx @astryxdesign/cli template --list --package @acme/astryx-widgetsnpx @astryxdesign/cli theme list --package @acme/astryx-widgetsnpx @astryxdesign/cli docs acmenpx @astryxdesign/cli search deploying --type doc
| Command | Look for |
|---|---|
component AcmeCarousel | import {AcmeCarousel} from '@acme/astryx-widgets/components/AcmeCarousel'; |
template --list --package | An acme-dashboard entry whose package is @acme/astryx-widgets |
theme list --package | An ocean entry whose package is @acme/astryx-widgets |
docs acme | Your section, with guides such as deploying |
search deploying --type doc | acme/deploying as the first result |
--package narrows template --list and theme list to one package. component --list ignores it and lists Core too, so look a component up by name. Last, import AcmeCarousel in the app's code and build the app, as your users will.
Run doctor in the app
npx @astryxdesign/cli doctor checks the whole app, including every package it loaded. doctor integration validate checks your package as the app installed it.
bashnpx @astryxdesign/cli doctornpx @astryxdesign/cli doctor integration validate @acme/astryx-widgets
textid: implicit-integrationsstatus: [info]label: Implicitly linked integrationsmessage: 1 integration loaded from installed dependencies with no astryx.config entry: @acme/astryx-widgets@1.0.0 from dependencies, contributing components, templates, themes, docs, codemods.
- The
implicit-integrationsrow names each package the app loads with no config.[info]is expected. - The
docs-progressive-disclosurerow warns when one of your doc sections is over 32 KB. Thedoctor integrationchecks do not measure size. doctor integration validate @acme/astryx-widgetsprints[ok] No integration issues found.when the installed copy is sound.
Pick a check
Run these in the package folder. The four doctor integration checks read your source, and integration check runs all four; integration verify checks the package npm would publish.
| Command | Proves | Exits 1 when | Only warns when |
|---|---|---|---|
npx @astryxdesign/cli doctor integration validate | The manifest loads, each root holds contributions the CLI can read, and each doc matches its authoring type. Notes, which fail nothing, list a malformed cliCommand and each command a topic writes with a package runner (runner_command) | A declared root is missing (missing_root), a contribution does not load (invalid_doc, invalid_component, invalid_theme), or two templates in the package replace one Core id (ambiguous_template_replacement) | The manifest has a key this CLI does not know (unknown_manifest_key), or a doc field does not match the doc's authoring type: a field the type does not have, a missing required field, or a value outside the field's type. It also warns on what no reader can serve: a prop listed twice (duplicate_prop), a propDescriptions key that names no documented prop (unknown_translated_prop), a section with no content (empty_section), two sections of a topic with one key (duplicate_section_key), or a .doc.zh.mjs or .doc.dense.mjs overlay beside no topic file (orphan_doc_overlay). Since 0.7.3 it also lists, from source and without failing, what the package tells apps to import that they cannot: a component's import (the doc's import, or the one the CLI derives) or a template's public import whose subpath package.json exports does not declare (component_import_not_exported, template_import_not_exported) or that names another package (component_import_other_package); an example that imports an undeclared subpath (example_import_not_exported); a template that imports a path outside the templates root (template_import_outside_root) or a path its package or Core does not export (template_private_import). It lists the same way a component's import whose module does not export the component (component_import_missing_name), an example or a template that imports a name its package does not export (example_import_missing_name, template_import_missing_name), and a template source with no default export (template_no_default_export). Each is a note, printed with its fix, that fails --strict from the next minor; an import it cannot judge, such as an exports target not built yet or a deep path into a dist/ not built yet, is listed under publicImports as not checked. It checks docs with your installed typescript, or, where that is missing or has no JavaScript compiler API (TypeScript 7), with an installed @typescript/typescript6; without either, it prints [skip] Docs not type-checked. With no astryx.integration.mjs it prints a hint and exits 0 |
npx @astryxdesign/cli doctor integration components | No component name clashes with a Core component, each replaces names one Core component, and each component doc's props match the component's TypeScript props | Core is not installed (core_not_found). In a package that declares the CLI range that turns replacement on, a replaces target is missing (missing_component_replacement_target) or invalid (invalid_component_replacement), or two components replace one Core component (ambiguous_component_replacement) | A name clashes with Core, a component sets replaces in a package without that range (inactive_component_replacement, plus any missing, invalid, or duplicate replaces reported as a warning), or a documented prop differs from the code: it is missing there (not_in_code), the doc omits a prop the package declares (not_in_doc), or its required (required_mismatch), type (type_mismatch), or literal default (default_mismatch) disagrees, or a {@link} in a component doc names no doc (invalid_doc_graph). It reads props with your installed typescript; without it, it prints [skip] Props not compared |
npx @astryxdesign/cli doctor integration templates | Each replaces names a Core template of the same type | A replaces target is missing (missing_template_replacement_target) or of the other type (invalid_template_replacement), or two templates replace one id (ambiguous_template_replacement) | A template id matches a Core id without replaces |
npx @astryxdesign/cli doctor integration docs | Your docs tree, every link, topic names against Core, passages two topics repeat word for word, padding phrases and low-density topics, and written pointers to docs | A topic takes a Core topic name without replaces or extends, a doc is invalid (invalid_doc), or a namespace or placement fails, which hides the doc (invalid_doc_graph) | A link names no doc (invalid_doc_graph), two topics repeat a passage word for word (copied_passage), prose pads with a listed phrase or falls under the density floor (doc_fluff), or prose, a description or summary, or the text of a component doc writes npx @astryxdesign/cli docs <name> where a link would be checked (stray_doc_pointer) |
npx @astryxdesign/cli integration check | Everything the four doctor integration checks prove, in one command, for your build | Any of the four would exit 1 on its own. With --strict, also any warning, and any check that did not run, such as a package with no astryx.integration.* | Anything the four warn about, unless you pass --strict |
npx @astryxdesign/cli integration verify | The packed package holds every file, shows the same contributions, resolves every public import, and declares the CLI it needs | Anything validate fails on, no manifest, a file left out of the .tgz file, an import that does not resolve, or a missing CLI peer | Anything validate warns about, or a component sets replaces and the package does not declare the CLI range that turns it on (component_replaces_needs_cli) |
Pass a package name, such as npx @astryxdesign/cli doctor integration validate @acme/astryx-widgets, to check an installed copy from an app instead. To fix a props finding, see npx @astryxdesign/cli docs authoring/building-blocks/components/describe-the-component/check-the-props.
Run every check in every build (required)
Every integration runs integration check --strict in its own build, so every build keeps your docs, types, and links in sync; this is required, not optional. The strict check fails on every copied passage the docs check lists (npx @astryxdesign/cli docs authoring/building-blocks/docs/check-your-docs). Run integration verify before you publish. Install devDependencies first, because the components check needs Core, and tsc --noEmit needs typescript and a tsconfig.json. In package.json:
json"typecheck": "tsc --noEmit && astryx integration check --strict"
integration checkrunsvalidate,components,templates, anddocsin that order, prints each result, then a summary, and exits 1 when any of them would.--strictalso exits 1 on any warning and on any check that did not run. Without it, warnings keep exit code 0: use that while you fix the warnings a package already has.- Its exit codes and JSON are a contract for your build: a new check, or a new failure code, joins only in a minor release that names it in the release notes. A patch can still ship a Core component, template, or CLI topic whose name conflicts with one of yours.
integration verifyrunsvalidatebut none of the other three: a Core name clash, areplacesthat names no Core template, a topic that takes a Core name, or a hidden guide still passes it. Run both before you publish:npx @astryxdesign/cli integration check --strict && astryx integration verify.- Bare
npx @astryxdesign/cli doctorin the package also warns about a doc section over 32 KB, which no check above measures.
Verify the packed package
integration verify packs your package with npm, unpacks it into a temporary app, and checks that the app sees everything your source has. It publishes nothing and leaves no .tgz file or temporary folder behind.
- It runs
npm packthe waynpm publishwould, including yourprepackscript. - It checks that every contribution file is in the
.tgzfile. A root missing fromfilesfails withAdd "templates" to "files" in package.json. - It lists the components, templates, themes, docs, and codemods the temporary app sees, and compares them with your source.
- It resolves each component's
import, and each template's public import, the way Node does, and checks that the module exports the component under the name its doc documents, or a default export for a template. A type-only export does not count. - For each theme, it rebuilds the source, compares the built module and production CSS with the authored outputs, and proves the module, CSS, and optional font CSS exports resolve from the packed package.
- It fails a package that needs an
@astryxdesign/clipeer and lacks it:>=0.6.6for a template that setskeywords, and>=0.6.4for a template that setsreplaces, a docs section, a placed guide, a doc section with anid, or a theme.
integration pack --check, the name this check had before published 0.6.5, still runs it and prints a note; it will be removed in a later release. The options and exit codes are in npx @astryxdesign/cli docs cli/commands/integration-verify.
Pick a version
Version your package with semver: a major version for a breaking change, a minor version for a new feature, and a patch for a fix.
bash# 1.0.0 -> 2.0.0: a breaking changenpm version major# 1.0.0 -> 1.1.0: a new featurenpm version minor# 1.0.0 -> 1.0.1: a fixnpm version patch
- Before 1.0.0, npm treats each minor version as breaking: an app that asks for
^0.6.0never gets0.7.0. Bump the minor version for a breaking change, and the patch for anything else. - Ship a codemod with each breaking change so apps can migrate; see
npx @astryxdesign/cli docs authoring/building-blocks/codemods.
Know what breaks apps
A breaking change stops code, commands, or links that worked in an app from working after the upgrade. For an integration, these names are part of its API.
| You change or remove | What stops working in the app |
|---|---|
A component name, such as AcmeCarousel | Imports, and npx @astryxdesign/cli component AcmeCarousel |
A template id, such as acme-dashboard | npx @astryxdesign/cli template acme-dashboard |
A theme slug, such as ocean | npx @astryxdesign/cli theme add ocean |
A topic name or route, such as acme/deploying | Reads of the old name, and links to it from other docs |
A component's import path | Imports written from the old path |
An exports entry | Imports of that path, which fail with ERR_PACKAGE_PATH_NOT_EXPORTED |
| A prop | Code that passes it |
List each breaking change in your release notes, with the codemod that migrates it. Any changelog tool works. If package.json has a files allowlist, add CHANGELOG.md to it so npm packs it.
Declare peer ranges
Declare the Astryx packages that your code imports as peer dependencies, in peerDependencies, so the app installs one copy of each. Keep each range as wide as your tests prove.
| Peer | Declare it when | Range |
|---|---|---|
@astryxdesign/core | Your code imports Core, as a theme does with @astryxdesign/core/theme | The Core versions you test, such as ^0.6.0 |
@astryxdesign/theme-* | Your code imports that theme package | The versions you test |
@astryxdesign/cli | You ship a docs section, a placed guide, a template that sets replaces or keywords, a component that sets replaces, a doc section with an id, or a theme | >=0.6.6 when you ship a template that sets keywords, >=0.6.7 when a component sets replaces, otherwise >=0.6.4. Optional in peerDependenciesMeta |
json{"peerDependencies": {"@astryxdesign/core": "^0.6.0","@astryxdesign/cli": ">=0.6.4"},"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}}
integration verify fails a package that needs the CLI peer and lacks it, or whose range admits a stable CLI too old for what it ships. integration add doc --parent and integration add theme write the peer for you.
When the CLI is outside that range, doctor integration validate and integration check list a cli_peer_version note naming your package. A note fails nothing, --strict included, and integration verify does not report it. Below the range the note gives the commands that install a newer Core and CLI together, as dev dependencies in your own package folder, and then run upgrade --apply. Above it, in an app the note asks for a release of your package that supports that CLI, and in your own package folder it asks you to test your package with that CLI and widen the range.
Support older CLIs
An app may run an older CLI than the one you build with. A CLI reads what it knows and skips the rest, but some newer files make an older CLI hide your docs.
- An unknown field in
astryx.integration.mjsis ignored with anunknown_manifest_keywarning, and the rest of the manifest still loads. - A named export that the CLI does not know is ignored with no warning, so
debugandgapReportare safe to add. - Published 0.6.3 and earlier print each
{@link ...}as written. - Published 0.6.3 and earlier reject a template that sets
replacesorkeywords, drop that template, and hide every doc topic your package ships. Published 0.6.4 and 0.6.5 still drop a template that setskeywords. - Published 0.6.3 and earlier cannot read a docs section, a section
id, or a theme folder thatintegration add themewrites. They can then hide every doc topic your package ships. - Published 0.6.2 and 0.6.3 still load your components, but published 0.6.0 and 0.6.1 cannot read the component docs that
integration add componentwrites:component AcmeCarouselfails there. - An app on an older CLI lists a component whose doc names another export of its file under the file name, and lists no family members, so
component AcmeCarouselfails there.
integration verify requires the CLI peer for a docs section, a placed guide, a template replaces or keywords, a doc section with an id, and a theme. A CLI too old for one of them cannot read it, and when it hides your topics, docs gives no warning.
Name codemod folders after Core versions
Name each codemod folder after the Core version whose upgrade should run it, not after your package's version. Which folders an app runs, and when, is in "Choose when a codemod runs" in npx @astryxdesign/cli docs authoring/building-blocks/codemods.
Check before you publish
Publish only a package that passes every check, then preview the upload with npm publish --dry-run. The dry run lists the files and the tag without uploading.
bashnpx @astryxdesign/cli doctor integration validate && npx @astryxdesign/cli doctor integration components && npx @astryxdesign/cli doctor integration templates && npx @astryxdesign/cli doctor integration docs && npx @astryxdesign/cli integration verifynpm publish --dry-run --access public
textnpm notice Publishing to https://registry.npmjs.org/ with tag latest and public access (dry-run)+ @acme/astryx-widgets@1.0.0
Set a new version for each release; npx @astryxdesign/cli docs authoring/ship/versioning covers what counts as breaking. What each check proves is in npx @astryxdesign/cli docs authoring/ship/checks.
Keep private files out
With no files list, npm packs every file your ignore files do not exclude, including an old .tgz file or an .env file. List what ships instead.
bashnpm pkg set 'files[]=components' 'files[]=templates' 'files[]=themes' 'files[]=docs' 'files[]=codemods' 'files[]=astryx.integration.mjs'npm pack --dry-run
Listonly the roots you ship, and alwaysastryx.integration.mjs. npm adds package.json, and a README and LICENSE when you have them.- Each later
integration addappends its root to the list. integration verifyfails when a root is missing from the list, and names the root to add tofiles(the message is innpx @astryxdesign/cli docs authoring/help/troubleshooting).
Publish to npm
A scoped package is private by default, so publish it with --access public. Without --tag, npm tags the release latest, which is what npm install @acme/astryx-widgets installs.
bash# Publish a releasenpm publish --access public# Or set the access once in package.jsonnpm pkg set publishConfig.access=public
To let apps try a release first, publish it under next, then point latest at it once ready.
bash# Only apps that ask for next get it: npm install @acme/astryx-widgets@nextnpm publish --tag next# Make that version the defaultnpm dist-tag add @acme/astryx-widgets@1.1.0 latestnpm dist-tag ls @acme/astryx-widgets
Check the published package
Install the version you published in a clean app, and run the same commands you ran against the .tgz file.
bashmkdir check-release && cd check-releasenpm init -ynpm install @astryxdesign/core @astryxdesign/cli @acme/astryx-widgets@1.0.0npx @astryxdesign/cli component AcmeCarouselnpx @astryxdesign/cli doctor integration validate @acme/astryx-widgets
The full list of commands to run in an app is in npx @astryxdesign/cli docs authoring/ship/test-in-an-app. To help apps move to the new version, see npx @astryxdesign/cli docs authoring/ship/upgrading.
Update for a new Astryx release
When Astryx ships a release, bump your devDependencies, rerun the checks, widen your peer ranges, and ship a codemod for each change that breaks apps.
bash# 1. Build and test against the new releasenpm install -D @astryxdesign/cli@latest @astryxdesign/core@latest# 2. Rerun the checksnpx @astryxdesign/cli doctor integration validatenpx @astryxdesign/cli doctor integration docsnpx @astryxdesign/cli integration verify# 3. Admit the new Core once the checks passnpm pkg set 'peerDependencies.@astryxdesign/core=^0.6.0 || ^0.7.0'# 4. Migrate apps across a change that breaks themnpx @astryxdesign/cli integration add codemod rename-delay --to 0.7.0
- Run every check in
npx @astryxdesign/cli docs authoring/ship/checks, not only the ones shown. - Codemod folders are named for Core versions:
npx @astryxdesign/cli docs authoring/building-blocks/codemods. - Release the result as a new version of your package; see
npx @astryxdesign/cli docs authoring/ship/publishing.
Upgrade an app
After an app installs a new Core, npx @astryxdesign/cli upgrade runs the codemods for the versions it crossed. It is a dry run by default; --apply writes the changes.
bash# Preview what would change; nothing is writtennpx @astryxdesign/cli upgrade --from 0.6.3 --integration @acme/astryx-widgets# Write the changesnpx @astryxdesign/cli upgrade --from 0.6.3 --integration @acme/astryx-widgets --apply
textIntegrations: @acme/astryx-widgets<n> codemods to runApplying integration codemods...Rename AcmeCarousel delay to interval (v0.7.0, @acme/astryx-widgets)[ok] [ok] src/Hero.tsx
--fromis the@astryxdesign/coreversion the app had before, and the target is the installed Core.--integrationnames your package. Without it, or anintegrationsentry inastryx.config, the upgrade skips your codemods, even when the app has your package installed.<n>includes Core's codemods for the same versions, which run first.- Put this command in your release notes.
Know which files upgrades skip
Upgrades never write files that the app marks as generated, vendored, or ignored. When such a file still needs a codemod change, the upgrade stops with ERR_CODEMOD_PROTECTED and exit code 1.
- Generated: an
@generatedcomment at the top of the file, orlinguist-generatedin.gitattributes. - Vendored:
linguist-vendoredin.gitattributes. - Ignored: a match in
.gitignoreor.hgignore.
text! ! src/gen/Gen.tsx - protected by @generated in the leading comment blockERR_CODEMOD_PROTECTED: 1 protected file still requires a codemod change:src/gen/Gen.tsx — @generated in the leading comment blockUpgrade incomplete: protected changes remain
The upgrade still writes every other file. Its options and exit codes are in npx @astryxdesign/cli docs cli/commands/upgrade.
Fix setup errors
These appear while you start the package or run the CLI in it. Each row quotes what the CLI prints.
| Message | Fix |
|---|---|
No package.json found. Run this command inside an integration package. | Run npm init -y and npm pkg set name=@acme/astryx-widgets first, in the package folder. |
Error: Could not find @astryxdesign/core package | Component commands read Core. Run npm install -D @astryxdesign/core in the package. |
core_not_found: Could not resolve @astryxdesign/core, so component names could not be checked. | Same fix. The check exits 1. |
Error: unknown subcommand 'integration verify', or Error: Pass --check to verify the integration tarball. | Your CLI is older than integration verify. Run npm install -D @astryxdesign/cli@latest. On the older CLI, the same check is integration pack --check. |
Note: integration pack --check is deprecated. | The old name still runs the same check. Switch to npx @astryxdesign/cli integration verify; the old name will be removed in a later release. |
Fix integration verify failures
npx @astryxdesign/cli integration verify prints each failure as a [fail] line. Add --json to see its issue code, such as component_export_missing.
| Message | Fix |
|---|---|
component_export_missing: Component "AcmeCarousel" advertises import "@acme/astryx-widgets/components/AcmeCarousel", but that packed module does not export "AcmeCarousel". | package.json has no exports map, or the module does not export the name. Keep export function AcmeCarousel, and add the entry: npm pkg set 'exports[./components/AcmeCarousel]=./components/AcmeCarousel.tsx'. When the quoted name is a file name, such as acme-carousel, set the doc's name to the component the file exports. A type-only export does not count. |
template_export_missing: Template "acme-dashboard" public import "@acme/astryx-widgets/templates/acme-dashboard" does not have a default export. | Run npm pkg set 'exports[./templates/acme-dashboard]=./templates/acme-dashboard.tsx', and keep export default in the source. Start new packages with "exports": {} so each add writes these. |
component_import_unresolvable or template_import_unresolvable: …Package subpath './components/AcmeCarousel' is not defined by "exports"… | The exports map has no entry for it. Run npm pkg set 'exports[./components/AcmeCarousel]=./components/AcmeCarousel.tsx', or 'exports[./templates/acme-dashboard]=./templates/acme-dashboard.tsx' for a template. |
component_import_unresolvable: …but a consumer cannot resolve it: Cannot find package '@acme/old-name' | You renamed the package after the add wrote each doc's import. Change import in every component doc to the new name. |
Declared templates root "templates" has 0 of 2 expected files in the pack list. Add "templates" to "files" in package.json. | Run npm pkg set 'files[]=templates'. |
docs_tree_needs_cli: The package ships a namespace doc or a placed guide but declares no @astryxdesign/cli peer. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.4' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json. |
replaces_needs_cli: The package has a template that sets replaces but declares no @astryxdesign/cli peer. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.4' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json. A stable CLI before 0.6.4 rejects replaces, drops that template, and hides your doc topics. |
component_replaces_needs_cli (a warning): The package has a component that sets replaces but declares no @astryxdesign/cli peer, or a peer range that admits an earlier CLI. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.7' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json to turn the replacement on. Until then apps see no change (npx @astryxdesign/cli docs authoring/building-blocks/components/replace-a-core-component). |
keywords_needs_cli: The package has a template that sets keywords but declares no @astryxdesign/cli peer. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.6' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json. What an older CLI does with the field is in npx @astryxdesign/cli docs authoring/building-blocks/templates/document-the-template/template-doc-overview. |
themes_need_cli: The package ships a theme but declares no @astryxdesign/cli peer. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.4' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json. A CLI older than published 0.6.4 cannot read typed theme descriptors, so it drops your themes and can hide your doc topics. |
section_ids_need_cli: The package has a doc section that sets id but declares no @astryxdesign/cli peer. | The same fix as themes_need_cli, or drop the section ids: a CLI older than published 0.6.4 rejects them and hides your doc topics. |
Fix problems in an app
These appear in an app that installs your package, or in the doctor checks that predict them.
| Symptom | Fix |
|---|---|
Error: No component named "AcmeCarousel" for a component your package documents. | Its doc still names its file. Set name to the export, such as name: 'AcmeCarousel' in acme-carousel.doc.mjs. A family member is listed only when the module its family doc imports exports it (npx @astryxdesign/cli docs authoring/building-blocks/components/describe-the-component/component-family). |
npx @astryxdesign/cli component Dialog opens Core's Dialog, not yours. | A name that Core, a legacy docs package, or another package's file already has stays theirs. Run npx @astryxdesign/cli component Dialog --package @acme/astryx-widgets, or give your component a name of its own. |
Error: Template "dashboard" is ambiguous — narrow it with --type and/or --package. | Your template id matches a Core id. Rename it, such as acme-dashboard, or set replaces: 'dashboard' to take its place. npx @astryxdesign/cli doctor integration templates warns about this before you publish. |
invalid_doc_graph: placement.parent "namespace:nope" names no namespace; @acme/astryx-widgets declares "acme". | The guide stays hidden until its placement names a namespace and slot your package declares. For "…" names no doc, fix the link target. |
Error: Unknown topic "acme". The docs of @acme/astryx-widgets did not load; run npx @astryxdesign/cli doctor integration docs in that package to see why. | One invalid doc hides all of your docs. Run npx @astryxdesign/cli doctor integration docs in the package and fix the invalid_doc it names. |
cli-command warns: Loaded integrations declare different CLI commands | Two packages the app loads set different cliCommand values, so the CLI prints the package manager's command. Agree on one command, or remove the package that does not belong. See npx @astryxdesign/cli docs authoring/building-blocks/configuration/cli-command. |
invalid_cli_command | Your cliCommand is not one line of plain text of at most 120 characters, so it is ignored. Fix the value; the rest of the manifest still loads. |
npx @astryxdesign/cli upgrade runs none of your codemods | The app must list your package in integrations in astryx.config, or pass --integration @acme/astryx-widgets. It runs a codemod folder only when its version is after --from and at most the installed @astryxdesign/core. |
| Your docs section, templates, or themes are missing only in some apps | Those apps run a CLI too old to read them (npx @astryxdesign/cli docs authoring/ship/checks lists which field needs which CLI). Update @astryxdesign/cli there, and keep your optional @astryxdesign/cli peer so npm checks the app's CLI against it when the app installs your package. |
What each check catches is in npx @astryxdesign/cli docs authoring/ship/checks.
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, issue tracker, and the command its users run the CLI with. 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} under it shows up in npx @astryxdesign/cli docs: a flat topic next to the built-in topics, where it can set replaces or extends to take over a built-in topic or add to it, and a guide with placement at its own route in the docs tree, under a namespace doc of the same package. 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 npx @astryxdesign/cli theme list; theme add 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 people file issues for this package: an absolute URL, such as its GitHub issues page. A value that is not a URL fails the whole manifest; the scheme is not checked. Nothing warns when it is missing, but without it npx @astryxdesign/cli gap-report about this package fails unless the project or a loaded integration has a gapReport handler, and npx @astryxdesign/cli swizzle prints no feedback link for this package's components. When no handler is loaded, a github.com issues URL is filed with the GitHub CLI once the reporter passes --confirm-public; any other URL is returned for the reporter to open. Example: 'https://github.com/acme/widgets/issues'. |
| docsiteUrl | string | no | Link to this integration's docs site, such as its home page. Catalogs that list the package may show it. Example: 'https://widgets.acme.dev'. |
| cliCommand | string | no | The command your users run the Astryx CLI with, when it is not their package manager, such as a wrapper your company ships. Every command the CLI prints in an app that loads your package starts with it, in place of pnpm exec astryx or yarn astryx: hints, errors, Doctor fixes, and doc text. One line, without a subcommand. If two loaded packages set different commands, neither is used and npx @astryxdesign/cli doctor warns. An invalid value is ignored with an invalid_cli_command warning, and the rest of the manifest still loads. JSON output keeps commands bare. Example: 'acme astryx'. |
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',};
In global mode: a manifest loads the same way when users install your package for their computer, beside the global CLI, and then applies in folders outside any project; inside a project, only if the project installs it. Peer ranges for @astryxdesign/core and @astryxdesign/cli are checked against the global CLI's release. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
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. Write each command in a line as a bare npx @astryxdesign/cli command; the managed block says once, before your lines, how the app starts the CLI.
Set cliCommand only when your users run the CLI with a command other than their package manager's, such as acme astryx. The CLI picks the command it prints in this order: the cliCommand of the loaded integrations when they name one command, then the command a host tool names, then the package manager's. Two different cliCommand values cancel out, and npx @astryxdesign/cli doctor warns in its cli-command check. See npx @astryxdesign/cli docs authoring/building-blocks/configuration/cli-command.
A themes root is version-gated: an older CLI lists and adds none of its themes. Depending on its age, that CLI ignores themes with an unknown_manifest_key warning and loads every other contribution kind, reports the root as invalid because it expects the earlier manifest.json theme catalog, or, if it is old enough to treat an unknown field as an error, rejects the whole manifest, so the package contributes nothing there.
Validate the manifest with npx @astryxdesign/cli doctor integration validate. At the load boundary, a known field of the wrong type is an error, issuesUrl and docsiteUrl must be valid URLs, 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, a discover source, 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 | Where reports about Core go: npx @astryxdesign/cli gap-report routes a Core report here when no handler is loaded, gapReport handlers receive it as report.target.issuesUrl, and npx @astryxdesign/cli swizzle prints it as the feedback link for a Core component. A report or swizzle for an integration's component uses that integration's own issuesUrl, never this one. An absolute URL; a value that is not a URL makes the config invalid. Default: 'https://github.com/facebook/astryx/issues/new'. |
| hooks | { postCodemod?: PostCodemodHook[] } | no | Lifecycle hooks. |
| hooks.postCodemod | PostCodemodHook[] | no | Commands to run after npx @astryxdesign/cli upgrade changes files (e.g. re-run your formatter). Each hook is an object {name?, buildCommand}: buildCommand gets {packageDir, files} and returns {command, args?, options?} to run, or null to skip. A dry run prints each command instead of running it. |
| 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 npx @astryxdesign/cli 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 for the removed layout expander, keyed by component name. The CLI still validates the field, so an entry without a string from makes astryx.config invalid, but nothing reads it. |
jsexport default {integrations: ['@acme/astryx-widgets'],};
jsimport {appendFileSync} from 'node:fs';export default {debug: event => appendFileSync("runs.ndjson", JSON.stringify(event) + "\n"),};
The config is validated at load with a strict schema: an unknown key, a misspelled one included, or a value of the wrong type makes the whole file invalid. npx @astryxdesign/cli gap-report, npx @astryxdesign/cli discover, and npx @astryxdesign/cli upgrade stop with the error, and npx @astryxdesign/cli doctor fails its config check (exit 1). Reads such as component, docs, search, template, and theme carry on silently as if there were no config and no integrations, and no debug handler runs, so run npx @astryxdesign/cli doctor after editing the file.
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 npx @astryxdesign/cli 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. Handlers add up rather than replace each other: the project handler first, then each loaded integration's in load order (the integrations list, then autolinked ones), whichever package the report is about.
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 integration's version from its package.json. Null for Core, and when the package.json has none. |
| 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 npx @astryxdesign/cli 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 then reads the official list the CLI carries; another source's entry for an official package is kept. A source that throws, runs past 30 seconds, or returns an invalid catalog never hides the others; discover then uses the last good answer that source gave for the same request, or reports the source as failed when it saved none. 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();},};
In global mode: the discover source of an integration installed for the computer is called in folders outside any project; inside a project, only the sources of integrations the project installs are. See npx @astryxdesign/cli docs global and npx @astryxdesign/cli docs authoring/global-integrations.
Astryx Codemod
A codemod module the CLI runs during npx @astryxdesign/cli 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: <codemods>/<version>/<id>.{ts,mjs,js} in an integration package: <codemods> is the codemods root of astryx.integration.*, and <version> is the exact @astryxdesign/core version (such as 0.7.0) whose upgrade runs the codemod. A file directly in the root never runs.
| 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.*. Default: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs']. 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 | yes | 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 | yes | 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 | Accepted for jscodeshift parity. A call does nothing, and upgrade shows nothing for it. |
| api.report | (...args: unknown[]) => void | yes | Accepted for jscodeshift parity. A call does nothing, and upgrade shows nothing for it. |
| 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 docs and each integration's. Artifact IDs for kinds outside the docs tree, provider instances, and compiler-input entries are defined for every provider-owned artifact; no command builds 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. It carries no installed, configured, loaded, selected, or healthy state. |
| AuthoredDocEntry | { id; provider; kind; stableName; source; authored } | yes | Normalized compiler input. stableName, the key in its doc ID, is given separately 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 and namespace doc, the CLI's and each integration's. Nothing reads aliases or audience today: a reference topic that sets one fails to load, placed in the docs tree or not, and other doc kinds accept them and ignore them.
Applies to: Every authored doc kind's .doc.mjs object. Theme descriptors take none of these fields.
| 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, and for every namespace doc: a guide with placement gets a route in the tree instead of a flat topic name, and cannot also set replaces or extends; a namespace doc with placement nests under that parent. 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. Docs without an order come after the ordered ones, sorted by title. The same order sets each doc's Previous and Next links. |
| 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 without the Astryx prefix, PascalCase. e.g. 'Button', 'TextInput', 'AppShell'. For an integration component it is the export apps import: the doc sits beside the file that exports it, and when that file exports nothing under its own name, name may name the export it does have. 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; with a lower range nothing changes and doctor integration components warns (npx @astryxdesign/cli docs authoring/building-blocks/components/replace-a-core-component). 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. npx @astryxdesign/cli search matches them; npx @astryxdesign/cli component <term> also suggests Core components by keyword. |
| 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. In an integration it hides only full entries in this doc's components, which the CLI then neither lists nor opens; hide a sub-component that has its own doc with hidden in that doc. |
| hidden | boolean | no | Hide this entire component from human-facing UI. It stays public and importable. Use for a shared primitive that only makes sense inside its 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 (astryx-* classes + data-attribute reflections) that themes target via @scope selectors in defineTheme. |
| theming.container | boolean | no | Marks a container component. Nothing reads it today: the theme pipeline maps padding to container tokens from a theming.derived entry {property: 'padding', expand: 'container'}. |
| 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. The pipeline reads only Core components' entries, so an integration's have no effect. |
| 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?, stability?}. 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. In an integration, each full entry the module named by import exports is a component under its own name. |
| subComponentOf | string | no | SubComponentDoc variant (required there): the parent component's name (e.g. 'Chat'). Marks this file as a sub-component doc. The CLI does not copy family fields (group, category, keywords, theming, playground) from the parent to an integration's sub-component: its detail, list group and search use only what its own doc sets. Set any the child needs in its own doc. |
| 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 default {type: 'component',name: 'AcmeSwitch',displayName: 'Acme 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 default {type: 'component',name: 'AcmeTable',displayName: 'Acme Table',category: 'Table & List',usage: {description: 'Displays rows and columns of data. Compose the sub-components to build headers, rows, and cells.'},components: [{name: 'AcmeTable', displayName: 'Acme Table', description: 'The table container.', props: []},{name: 'AcmeTableRow', displayName: 'Acme Table Row', description: 'A row within the table body.', props: [{name: 'isSelected', type: 'boolean', description: 'Highlights the row as selected.'},]},{name: 'useAcmeTableSelection', displayName: 'useAcmeTableSelection', 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 about as loosely as an unstamped one, so adding type: 'component' to a doc that follows the type never breaks it: displayName may be missing, category may be any string, usage, theming and playground are not checked, and examples need only be an array. Unlike an unstamped doc, each entry in a group doc's components must have a name, and import and replaces, when set, must be non-empty strings. 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 does not take the parent's family fields, and it may omitusage.
js/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeChatComposer',displayName: 'Acme Chat Composer',subComponentOf: 'AcmeChat',description: 'The message input row within an Acme 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', ... }, which npx @astryxdesign/cli integration add component writes; legacy docs use export const docs = {...} and omit type (the parser shape-sniffs). A hook that is part of a component API is listed in a MultiComponentDoc components array: inline as a ComponentEntry (with params/returns), or by name with its own SubComponentDoc file, as Core's useTableSelection is.
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. npx @astryxdesign/cli hook reads Core's hook docs only; an integration lists its hooks in a component doc's components array.
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. npx @astryxdesign/cli 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: ['useImageMode'],};
parseDoc accepts a stamped hook doc without displayName and does not check its usage, and npx @astryxdesign/cli hook prints a hook doc without checking it at all. 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.
A hook that is part of a component API is listed in that component's MultiComponentDoc components array: inline with params/returns, or by name with its own SubComponentDoc file, as Core's useTableSelection is.
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/<dir>/<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. Nothing reads it today: no reader sections docs by it or infers it from importPath. The CLI's API function docs set 'api'. |
| 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>, the name in kebab case (themeUse is theme-use). Every function doc the CLI ships declares one, and npx @astryxdesign/cli doctor fails on one that is missing or that nothing reads. |
| aliases | string[] | no | Reserved: other names for this doc. Nothing reads it today, so they do not resolve to it. |
| 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, templates, and themes 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' | 'theme'", 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',};
parseDoc accepts a stamped function doc without displayName and does not check its usage, and the CLI reads its own function docs without checking them. Write to the type anyway; it is the contract.
The type discriminant is 'function' for both flavors. kind: 'hook' or kind: 'api' names the flavor; nothing reads it today.
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 body / when-to-use. npx @astryxdesign/cli docs prints it after the summary; --help shows only the summary. |
| 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>, spaces as hyphens (theme build is cli/commands/theme-build). Every command doc the CLI ships declares one, and npx @astryxdesign/cli doctor fails on one that is missing or that nothing reads. |
| aliases | string[] | no | Reserved: other names for this doc. Nothing reads it today, so they do not resolve to it. |
| 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; npx @astryxdesign/cli docs shows that param's description when the arg has none. |
| args[].description | string | no | Override description. --help shows an argument's description only when it sets this. |
| 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. Nothing requires it: a flag with neither this nor param shows no description. |
| 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. --verbose). Nothing reads it. |
| 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. |
| outsideProject | 'same' | 'refuse' | 'own' | no | What the command does outside a project. same: the answer it gives in a new project. refuse: it writes nothing and fails, naming the app project or integration package it needs. own: something else. Every refuse and own command also carries an In global mode: note in its notes that tells readers exactly what it does. Every command and subcommand declares one, and npx @astryxdesign/cli manifest publishes it; a command that only groups subcommands declares none. The CLI tests run every command in an empty folder and check what it does against this value; npx @astryxdesign/cli doctor does not check it. |
| 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, templates, and themes.',namespace: 'cli/commands',fn: 'search',args: [{name: 'query', param: 'query', required: true}],options: [{flag: '--type <domain>', param: 'options.type', choices: ['component', 'hook', 'doc', 'template', 'theme']},{flag: '--verbose', cliOnly: true, description: "Also print each result's score and match reason."},],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: an option's in --help and npx @astryxdesign/cli docs, an arg's in npx @astryxdesign/cli docs only. Mark a flag with no function param (e.g. --verbose) cliOnly: true and give it its own description; nothing checks either. --json is a global flag, so a command doc does not declare it.
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. Only the CLI reads enum docs, from its own source: one in an integration's docs directory fails to load, and so do that package's other docs.
Applies to: <enum>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'enum' | no | Doc-kind discriminant. Optional in the type, but the CLI reads a doc as an enum doc only when it is set; without it the doc drops out of the docs tree. |
| 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 npx @astryxdesign/cli doctor fails on one that is missing or that nothing reads. |
| aliases | string[] | no | Reserved: alternate slugs for this doc. Nothing reads it today, so npx @astryxdesign/cli docs <alias> does not open it. |
| 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/api',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. npx @astryxdesign/cli 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 npx @astryxdesign/cli 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 | Reserved: bundle audience. Nothing reads it yet: a namespace that sets 'internal' still shows in npx @astryxdesign/cli docs. 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 the doc kinds it accepts. accepts.providers is not read yet: only docs of the namespace's own package can be placed in a 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. npx @astryxdesign/cli docs <route> prints the prose, heading, code, table, and list blocks above the slots, and --json returns every block with its links resolved. A collection block is not rendered yet, and npx @astryxdesign/cli doctor warns on one. |
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 without a placement 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 npx @astryxdesign/cli doctor names the loser.
Child docs request one canonical home with placement. A collection block refers to those docs by stable reference and never creates a second identity or parent. It is not rendered yet: npx @astryxdesign/cli doctor warns on one.
| 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 npx @astryxdesign/cli docs. Content is built from ordered sections of mixed content blocks.
Applies to: <topic>.doc.{ts,mjs,js} in the folder the integration manifest's docs names
| 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. npx @astryxdesign/cli 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 npx @astryxdesign/cli 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 replaces the base section with the same id, or else the one with exactly its title when either of the two has no id. A key derived from a title counts only against an id: a section without an id also matches a base section whose id is the key its title derives, and a section with an id also matches a base section without one whose title derives that id. Two different ids never match, two sections without an id whose titles differ only in case or punctuation stay separate, and a section that matches none 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 npx @astryxdesign/cli docs <topic> <section>. |
| sections[].id | string | no | Stable section anchor. New docs should set this instead of relying on a mutable title. Published @astryxdesign/cli 0.6.3 and earlier reject the field and hide every doc topic the package ships, so npx @astryxdesign/cli integration verify fails an integration that sets one until it declares an @astryxdesign/cli peer of >=0.6.4. |
| 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 npx @astryxdesign/cli 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 npx @astryxdesign/cli doctor warns on one that names no doc. |
| sections[].previewType | ReferenceTokenPreviewType | no | Preview type for token tables in this section. The CLI rejects a value not in the list below and returns the value in npx @astryxdesign/cli docs --json, but draws no preview from it. Omit for non-token sections. |
| tokenCategory | string | no | Token category for foundational docs that map to a token section (e.g. 'color'). npx @astryxdesign/cli docs --json returns it as authored; no tokens page links to the doc through it. |
js/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */export default {type: 'generic',name: 'acme-spacing',title: 'Acme 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 npx @astryxdesign/cli 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 npx @astryxdesign/cli 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 npx @astryxdesign/cli doctor warns on one that names no doc. Write a CLI command in text, shell code, or a table cell as a bare npx @astryxdesign/cli command: a text read adds the runner the reader uses in front of it, and --json returns it as written.
A reference block includes content, where a link only points at it: npx @astryxdesign/cli 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'; title?: string; 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, a hint for previewing its token table, to one of 'swatch' | 'shadow-box' | 'radius-box' | 'spacing-bar' | 'size-bar' | 'border-line' | 'duration-bar' | 'easing-curve' | 'font-sample'; the CLI rejects any other value.
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 npx @astryxdesign/cli docs authoring/field-reference), 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 npx @astryxdesign/cli doctor fails on one that is missing or that nothing reads. |
| aliases | string[] | no | Reserved: alternate slugs for this doc. Nothing reads it yet: npx @astryxdesign/cli docs finds a schema doc by its name only. |
| 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 and examples. The stable ReferenceContentBlock union (prose, heading, code, table, list, token-ref), without the reference block a ReferenceDoc section also takes; a token-ref note is accepted but not rendered. |
js/** @type {import('@astryxdesign/cli/authoring').SchemaDoc} */export const doc = {type: 'schema',name: 'integration',displayName: 'Astryx Integration',namespace: 'authoring',description: 'The astryx.integration.* manifest that points the CLI at what a package contributes.',appliesTo: 'astryx.integration.{ts,mjs,js}',fields: [{name: 'docs', type: 'string', description: 'The folder that holds your doc topics, relative to package.json.'},{name: 'agentDocs',type: '{ append?: readonly string[] }',description: 'Guidance your package adds to the agent instructions the CLI manages.',fields: [{name: 'agentDocs.append', type: 'readonly string[]', description: 'Lines added at the end, at most eight.'},],},],};
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 in the gallery. For page templates it is a human-readable label, while the existing template-directory/CLI slug owns the default registry path. npx @astryxdesign/cli template --list and npx @astryxdesign/cli search print name as the label of both, and npx @astryxdesign/cli template <id> finds a template by its id, never by name: an integration template's id is its path under the templates folder without the .doc.* suffix. |
| displayName | string | yes | Human-readable label for the gallery. The CLI prints name instead; npx @astryxdesign/cli template --list --json also returns an integration template's displayName. 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. npx @astryxdesign/cli search matches them as it matches the description and npx @astryxdesign/cli 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 npx @astryxdesign/cli --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, and npx @astryxdesign/cli doctor integration validate warns about an integration template that leaves out displayName or aspectRatio.
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.
IntegrationTemplateDoc
The doc-type for an integration package's template metadata. It has every TemplateDoc field and rule, except that category is the package's own string instead of a TemplateCategory value.
Applies to: <Name>.doc.mjs
| Field | Type | Required | Description |
|---|---|---|---|
| category | string | no | The package's own category. Bare npx @astryxdesign/cli template groups the package's templates by it. |
js/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */export default {type: 'block',name: 'acme-pricing-table',displayName: 'Pricing Table',description: 'Plans side by side, with the chosen plan highlighted.',aspectRatio: 16 / 9,category: 'Acme - Commerce',};
Every other field, and the page and block variants, are TemplateDoc's. Core's own templates use TemplateDoc, whose category is a TemplateCategory value. npx @astryxdesign/cli doctor integration validate checks an integration's template docs against IntegrationTemplateDoc.
ThemeDoc
The strongly typed descriptor for one source theme: bundled with the CLI, shipped by an integration, or kept in the project's src/themes, where theme eject writes one.
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. The descriptor holds one default-exported object of literal string and boolean fields, directly under a /** @type {import('@astryxdesign/cli/authoring').ThemeDoc} */ comment, and nothing else: without that comment, or with any other statement, it fails to load. 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.