CLI Integrations

Build an npm package that adds components, templates, themes, docs, and codemods to Astryx apps.

Overview

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 astryx docs cli/integrations/building-blocks.

For example, ship a carousel as an integration. Any app that installs it can find it with npx astryx 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 astryx docs cli/integrations/quick-start.

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, then install the Astryx CLI and Core for development.

bash
mkdir acme-widgets && cd acme-widgets
npm init -y
npm pkg set name=@acme/astryx-widgets
npm pkg set 'exports={}' --json
npm install -D @astryxdesign/cli @astryxdesign/core
  • Start with "exports": {}: each component and template you add then writes the public import that integration verify resolves.
  • Run the CLI as npx astryx, which runs the @astryxdesign/cli you installed as a devDependency.
  • Component commands read Core, so they need @astryxdesign/core installed.

Add your first integration item

An integration can ship several kinds of items. Start with a component named AcmeCarousel.

bash
npx astryx integration add component AcmeCarousel
text
component contribution added
​
[ok] AcmeCarousel
​
Declare 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.

bash
npx astryx component AcmeCarousel

You now have an integration package with one exported component.

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.

bash
npx astryx integration verify
npm pack --pack-destination ..
text
Integration package ready
​
[ok] @acme/astryx-widgets@1.0.0
​
4 packed files; 3/3 required files present.

integration verify packs the package, unpacks it into a temporary app, and checks that the component resolves through its public import there. 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.

bash
cd ..
mkdir my-app && cd my-app
npm init -y
npm install @astryxdesign/core @astryxdesign/cli ../acme-astryx-widgets-1.0.0.tgz
npx astryx 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 astryx docs cli/integrations/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.

FieldSet it toWhy
nameYour package name, such as @acme/astryx-widgetsEach add writes it into the import of the doc it generates. Rename before you add, or update each import after.
versionThe release you publish, such as 1.0.0Apps see it; astryx.integration.mjs never repeats it.
exportsStart with {}Each component and template add writes its public import here, and integration verify resolves it.
filesOptional: the paths to publishKeeps private files out. When the list exists, each add appends its root and astryx.integration.mjs.
peerDependencies@astryxdesign/cli; add @astryxdesign/core when your code imports itThe app supplies these packages for your integration.
devDependencies@astryxdesign/cli and @astryxdesign/coreLets you run the CLI and build components while working on the integration.

Peer ranges and releases are covered in astryx docs cli/integrations/ship/versioning; files and publishing in astryx docs cli/integrations/ship/publishing.

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.json defines the npm package, including its name, version, dependencies, published files, and exports.
  • astryx.integration.mjs tells 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.tsx for its source and AcmeWidget.doc.mjs for its documentation.

Fields in the integration file

astryx.integration.mjs
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.

FieldTypeRequiredDescription
providerIdstringnoThe 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'.
componentsstringnoThe folder that holds your components and their docs, relative to package.json. Example: './src/components'.
templatesstringnoThe folder that holds your templates, relative to package.json. Example: './src/templates'.
codemodsstringnoThe folder that holds your codemods, relative to package.json. Example: './codemods'.
docsstringnoThe folder that holds your doc topics, relative to package.json. Every {topic}.doc.{ts,mjs,js} in it shows up in astryx docs next to the built-in topics; a topic can also set replaces or extends to take over a built-in topic or add to it. Example: './docs'.
themesstringnoThe folder that holds your themes, relative to package.json, with one folder per theme. Each theme folder has the theme source and a matching .doc.mjs file with the same name. Installed themes show up in astryx theme list; theme add --import imports their built package exports, and theme eject creates an editable local fork. Example: './themes'.
agentDocs{ append?: readonly string[] }noLines 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.'] }.
issuesUrlstringnoWhere to file issues/feedback for this integration. Example: 'https://github.com/acme/widgets/issues'.

From Astryx Integration: astryx 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 astryx docs cli/commands/integration-add.

astryx integration verify: Check the package the way npm will publish it, before you publish. Read it with astryx docs cli/commands/integration-verify.

astryx doctor integration: Check an integration package while authoring it. Read it with astryx docs cli/commands/doctor-integration.

Building Blocks

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.

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.

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.

bash
npx astryx integration add component AcmeCarousel
text
component contribution added
​
[ok] AcmeCarousel
​
Declare 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 astryx docs cli/integrations/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 (astryx docs cli/integrations/quick-start).

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.mjs together.
  • Update the doc whenever the public name, import, behavior, props, defaults, examples, or accessibility requirements change.
  • integration verify checks the doc shape and packed import, but it cannot 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.

bash
npx astryx 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.

components/AcmeCarousel.doc.mjs
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,
},
],
};

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. Read it with astryx 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.

javascript
usage: {
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.'},
],
},

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.

javascript
props: [
{
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: true only when every caller must pass the prop.
  • Write default exactly as the value should appear in documentation.
  • Skip styling escape hatches such as xstyle, className, and style.

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.

javascript
examples: [
{
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.

bash
npx astryx component AcmeCarousel
npx astryx 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 props for a component entry. Use params and returns for a hook entry.
  • Do not add private implementation helpers or exports that people should not use directly.
FieldTypeRequiredDescription
components(ComponentEntry | ComponentRef)[]noMultiComponentDoc variant (required there): one entry per public component/hook exported from the directory. Each entry is a full ComponentEntry (inline: name, displayName, description, props | params+returns) or a name-only ComponentRef pointing at a sibling {Name}.doc.mjs.

From ComponentDoc: astryx docs authoring component-doc

Document the family inline

components/AcmeTabs.doc.mjs
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.

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 astryx docs cli/integrations/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

components/AcmeDialog.doc.mjs
javascript
/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
export default {
type: 'component',
name: 'AcmeDialog',
displayName: 'Acme Dialog',
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

components/AcmeDialogHeader.doc.mjs
javascript
/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
export default {
type: 'component',
name: 'AcmeDialogHeader',
displayName: 'Acme Dialog Header',
subComponentOf: 'AcmeDialog',
description: 'Labels an Acme Dialog and holds its close action.',
props: [
{
name: 'title',
type: 'string',
description: 'The dialog title.',
required: true,
},
],
};
FieldTypeRequiredDescription
subComponentOfstringnoSubComponentDoc variant (required there): the parent component's name (e.g. 'Chat'). Marks this file as a sub-component doc that inherits family fields (group, category, keywords, theming, playground) from the parent.
descriptionstringnoSubComponentDoc variant (required there): one-sentence description of the sub-component's role within the parent composition. Single/Multi docs have no top-level description; they derive their summary from usage.
propsComponentPropDoc[]noSingleComponentDoc variant (required there): all public props for the one primary component. Each prop is {name, type, description, default?, required?, slotElements?}. Skip styling props like xstyle/className/style. Also present on SubComponentDoc.

From ComponentDoc: astryx docs authoring component-doc

  • subComponentOf must exactly match the parent doc's name.
  • description explains this member's role in the family. usage is optional; add it when the member needs guidance beyond that sentence.
  • The child inherits family fields such as group, category, keywords, theming, and playground unless it overrides them.

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.

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

Verify the packed import

integration verify installs the packed package in a temporary app, resolves each documented import, and checks that the module exports the documented component name.

bash
npx astryx integration verify
  • component_import_unresolvable means the exports map has no entry for the documented import.
  • component_export_missing means the module does not export the documented name, or package.json has no exports map.
  • typescript_extension_in_specifier means the public import ends in .ts or .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 astryx docs cli/commands/integration-verify.

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.

bash
npx astryx component AcmeCarousel
npx astryx component --list
npx astryx search carousel
text
# AcmeCarousel
​
Cycles 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 astryx docs cli/integrations/ship/test-in-an-app.

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

components/AcmeSideNav.doc.mjs
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: [],
};
FieldTypeRequiredDescription
replacesstringnoIntegration components only: the exact name of the Core ComponentDoc this component takes over for unqualified lookup, so every app that loads the integration gets it from component detail, component lists, search, swizzle <Name>, and issue routing; swizzle --list keeps listing Core names. The Core original stays reachable with --package @astryxdesign/core. It takes effect only when the package's peer range starts at the release that applies it, "@astryxdesign/cli": ">=0.6.7" or later; without such a range the component keeps its own name and doctor integration components warns. Set it only to intentionally own a Core identity; give an alternative or variant its own name instead.

From ComponentDoc: astryx 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:

package.json
json
{
"peerDependencies": {"@astryxdesign/cli": ">=0.6.7"},
"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}
}
  • Any @astryxdesign/cli range that starts at that release or later turns it on, including the >=0.7.0 that earlier integration add theme and integration add doc --parent wrote.
  • 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 components warns with the range to add. Apps that load such a package see no change.
  • replaces names the Core ComponentDoc identity, 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/core always selects the original Core component, and --package with your package selects your component by either name.
  • Replacing SideNav replaces only that name. SideNavItem and the other SideNav parts stay Core components; document any parts your package provides under their own names.
  • swizzle --list lists Core components, including the one you replace. swizzle SideNav copies your component, and swizzle SideNav --package @astryxdesign/core copies the original.

Check replacement resolution

bash
npx astryx doctor integration components
npx astryx component SideNav
npx astryx component AcmeSideNav
npx astryx 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 components reports 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.json wins and Doctor warns.

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 it
npx astryx template --list
npx astryx template acme-dashboard src/app/dashboard

Why share one

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

KindUse it for
PageA complete screen, such as a dashboard, settings page, or checkout flow.
BlockA 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 astryx template. To change a label, edit the doc (astryx docs cli/integrations/building-blocks/templates/document-the-template/template-doc-overview); never rename the id.

bash
# See the Core ids
npx astryx --json template --list --package @astryxdesign/core

Run the add command

Pages are the default. Pass --type block to add a block.

bash
npx astryx integration add template acme-dashboard
npx astryx integration add template acme-stat-card --type block
text
template contribution added
​
[ok] acme-dashboard
​
Declare 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 (astryx docs cli/integrations/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.

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.

FileWhat it controls
templates/acme-dashboard.tsxThe UI source that an app copies and then owns.
templates/acme-dashboard.doc.mjsHow 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.

Document the shared fields

These fields apply to every page and block. The page, block, and replacement guides add the fields unique to each.

FieldTypeRequiredDescription
type'page' | 'block'yesDiscriminant selecting the variant: 'page' for a full page template, 'block' for an editable composition that may be standalone or component-owned.
namestringyesStable identifier for block templates; change displayName, not name, to edit their visible label. For page templates it is a human-readable label, while the existing template-directory/CLI slug owns the default registry path.
displayNamestringyesHuman-readable label for the gallery/CLI. Spaces out block names that mirror a PascalCase component ('ChatMessageMetadata' → 'Chat Message Metadata').
descriptionstringnoOne-sentence description of what the template provides.
keywordsstring[]noSearch keywords for CLI discovery: the ideas, domains, and other names a builder might use for what the template serves (e.g. ['monitoring', 'uptime', 'on-call'] for a service-health dashboard). Lowercase. astryx search matches them as it matches the description and astryx build ranks page templates on them, so keep them out of description. Integration templates need @astryxdesign/cli 0.6.6 or later: earlier CLIs reject the field and drop that template, and CLIs before 0.6.4 also hide the package's doc topics.
isReadybooleannoWhether the template is ready for use. false shows as '(WIP)' in the gallery and CLI.

From TemplateDoc: astryx docs authoring template-doc

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 (astryx docs cli/integrations/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.

bash
npx astryx --json template --list --package @acme/astryx-templates
Relevant list result
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.

templates/acme-dashboard.doc.mjs
javascript
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
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

FieldTypeRequiredDescription
categoryTemplateCategorynoFunctional gallery category following a 'Group - Variant' convention (e.g. 'Dashboard - Analytics', 'Table - Basic', 'Form - Wizard'). The overview groups by the text before ' - '.
scaffoldbooleannoScaffolding-only template (e.g. blank page): available via the CLI but hidden from browsable galleries.

From TemplateDoc: astryx docs authoring template-doc

  • Set category to the most specific supported {Group} - {Variant} value. Its words become search terms for astryx search, and it appears in the JSON template list.
  • Set scaffold: true only 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.

templates/acme-stat-card.doc.mjs
javascript
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
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

FieldTypeRequiredDescription
exampleForstringnoBlock templates only: optional component ownership. Set this when the block is specifically an example of one component. Omit it for a standalone composition.
alsoExampleForstring[]noBlock templates only: additional component/hook doc pages whose Examples section should include this block.
alsoShowcaseForstring[]noBlock templates only: additional doc pages whose hero showcase should reuse this block (secondary placements; does not change the primary showcase).
componentsUsedstring[]noBlock templates only: component names this block uses, for 'See also'/'Used in' cross-references (not primary attribution).
isShowcasebooleannoBlock templates only: when true this block is the canonical hero showcase for its exampleFor component. Requires exampleFor.

From TemplateDoc: astryx docs authoring template-doc

Keep one clear primary owner. Use the also* fields only for intentional secondary placements.

templates/acme-status-card-showcase.doc.mjs
javascript
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
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 (astryx docs cli/integrations/building-blocks/templates/document-the-template/template-doc-overview) and confirm the entry carries the relationship you set. astryx component does not show integration blocks, so the list is where to check.

Choose a useful preview

FieldTypeRequiredDescription
aspectRationumbernoBlock templates only (required): width-to-height ratio for preview containers (e.g. 16/9, 1, 3/4).

From TemplateDoc: astryx docs authoring template-doc

Start 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 shapeStarting value
Wide navigation, banner, toolbar, or tabs16 / 4
Square button, badge, avatar, icon, spinner, or status1
Tall navigation, calendar, list, or tree3 / 4
Content card, dialog, table, or form group4 / 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 (astryx docs cli/integrations/building-blocks/templates/start-a-template).

  • Sharing a Core id without replaces does not replace Core. It makes the bare id ambiguous, so astryx template <id> fails until the app adds --package, and doctor integration templates reports an accidental collision.
  • A replacement can keep the Core id or use its own. replaces is what makes it the default.

Declare the replacement

Find the exact Core id and kind first.

bash
npx astryx --json template --list --package @astryxdesign/core
templates/acme-app-shell.doc.mjs
javascript
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
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',
};
FieldTypeRequiredDescription
replacesstringnoIntegration templates only: the exact id of the Core template this one replaces for unqualified lookup. Find it with astryx --json template --list --package @astryxdesign/core; the Core original stays selectable with --package @astryxdesign/core. A page replaces only a Core page and a block only a Core block. Needs @astryxdesign/cli 0.6.4 or later: earlier CLIs reject the field, drop that template, and hide the package's doc topics.

From TemplateDoc: astryx docs authoring template-doc

Declare replaces on the template doc, never in astryx.integration.mjs.

Require a compatible CLI

Declare the CLI floor from the field above as an optional peer (astryx docs cli/integrations/ship/versioning).

package.json
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, an older CLI drops that template and hides your doc topics.

What apps receive

bash
# Receives the active replacement
npx astryx template shell-side-nav src/app
​
# Receives the original Core template
npx astryx 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.mjs wins 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

bash
npx astryx doctor integration templates
IssueMeaning
missing_template_replacement_targetreplaces does not name a Core template id.
invalid_template_replacementThe replacement is unusable or its page/block kind differs from Core.
ambiguous_template_replacementOne 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

astryx 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.

TemplateDirectory targetExplicit file target
PageWrites <target>/page.tsxWrites the exact file path
BlockWrites <target>/<source-basename>.tsxWrites the exact file path
bash
# Page: src/app/account/page.tsx
npx astryx template acme-account src/app/account
​
# Block: src/features/account/acme-stat-card.tsx
npx astryx template acme-stat-card src/features/account
​
# Exact destination for either kind
npx astryx 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.

tsx
import {Button} from '@astryxdesign/core/Button';
import {Card} from '@astryxdesign/core/Card';
import {HStack, VStack} from '@astryxdesign/core/Stack';
import {Text} from '@astryxdesign/core/Text';
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>
);
}

Export one component

The source must default-export one React component. Named helpers inside the file are fine.

  • Add use client as the first statement only when hooks, event handlers, or browser APIs 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 (astryx docs cli/integrations/building-blocks/templates/build-the-template/write-the-template-file). Give every asset exactly one owner:

OwnerUse whenHow the copied source reaches it
Copied sourceThe value is small, editable, and belongs to the starting pointKeep it in the .tsx file
Integration packageThe asset should stay centrally maintained with the packageImport a stable public package path
AppThe app must provide product-specific content or brandingUse 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 Stack and Grid spacing 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.

tsx
import * 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 astryx docs cli/integrations/building-blocks/templates/write-good-templates/template-grading-rubric.

Publish shared CSS deliberately

When a stylesheet must stay package-owned, import it from the template by its public package path.

templates/acme-dashboard.tsx
tsx
import '@acme/astryx-widgets/styles/acme-dashboard.css';

Then export that path and include the file in the package (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/export-template-assets). In a TypeScript app, this import type-checks only when the app 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 (astryx docs cli/integrations/building-blocks/templates/build-the-template/template-assets/template-styles).

styles/acme-dashboard.css
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: swap unless 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 (astryx docs cli/integrations/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 (astryx docs cli/integrations/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.
  • Text stays 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.

tsx
import {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} />} />

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.

templates/acme-dashboard.tsx
tsx
import {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 (astryx docs cli/integrations/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 (astryx docs cli/integrations/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_modules is 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 referenceCopied result
/template-assets/hero.pngInline SVG data URL placeholder
/template-assets/demo.mp4Empty string
./hero.png or /product/hero.pngUnchanged
https://example.com/template-assets/hero.pngUnchanged
  • 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-assets only 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.

tsx
const HERO_IMAGE = '/acme-dashboard-hero.jpg';
// Replace HERO_IMAGE with an app-owned public URL before shipping the page.
​
<img
src={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 (astryx docs cli/integrations/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.

package.json
json
{
"exports": {
"./templates/acme-dashboard": "./templates/acme-dashboard.tsx"
}
}
  • integration add writes this entry when package.json has an exports map (astryx docs cli/integrations/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.

Relevant package.json entries
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.

package.json
json
{
"files": [
"astryx.integration.mjs",
"templates",
"components",
"icons",
"styles",
"assets/fonts"
],
"sideEffects": ["**/*.css"]
}
  • integration add appends the manifest and templates directory when a files list already exists. Add the other directories yourself.
  • If package.json sets sideEffects, include CSS in it, as above, so bundlers keep side-effect stylesheet imports.
  • Run npm pack --dry-run and 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 astryx docs cli/integrations/ship/versioning. For other packages:

FieldUse it for
peerDependenciesLibraries the copied source imports, such as an icon library. List them in devDependencies too, so the package itself builds.
dependenciesRuntime packages used only by package-owned modules, which should travel with the integration
devDependenciesEverything 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

bash
npx astryx 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.

  1. It validates the manifest, every template doc, its source file, and the templates directory.
  2. It checks that the manifest and every template file are in the .tgz file.
  3. It checks that each packed template keeps the same id, type, name, and replaces as your working copy. It does not compare template source or other doc fields.
  4. It resolves each template through its public package path in the unpacked package.
  5. It checks that the resolved template module has a default export.
  6. 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. astryx docs cli/integrations/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 replaces target exists. Run npx astryx doctor integration templates for those.
  • How the template scores on quality.

Install the packed package

Install the packed package in a clean app as described in astryx docs cli/integrations/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 metadata
npx astryx --json template --list --package @acme/astryx-widgets
​
# Inspect source without writing
npx astryx template acme-dashboard --package @acme/astryx-widgets
​
# Copy a page and a block to their real app locations
npx astryx template acme-dashboard --package @acme/astryx-widgets src/app/acme-dashboard
npx astryx template acme-stat-card --package @acme/astryx-widgets src/components
  • Confirm the copied files: src/app/acme-dashboard/page.tsx and src/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.

bash
npx tsc --noEmit
npm 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

DimensionRequired checks
WidthWide and narrow; include the smallest supported viewport
ColorEvery supported color mode and theme
InputKeyboard and pointer for the primary task
ContentRealistic, empty, long, loading, and error states that the pattern supports
AssetsSuccessful CSS, font, icon, image, poster, and video requests
RuntimeNo 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 (astryx docs cli/integrations/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.

  1. Read every scoring rule in astryx docs cli/integrations/building-blocks/templates/write-good-templates/template-grading-rubric.
  2. Fix publication blockers first, then every reasonable deduction.
  3. Repeat the full grade on the packed package in a clean app (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/test-template-in-app).
  4. To have an agent grade and improve the template, use astryx docs cli/integrations/building-blocks/templates/write-good-templates/grade-template-with-agent.

Keep each scorecard with its package revision and rubric version.

Understand the score

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.

GradeScoreMeaning
A90-100Exemplary. Copy-ready with no known quality problems.
B75-89Good. Minor issues may remain, but the template is usable.
C60-74Needs work before publication.
D40-59Poor. Significant rewrites are needed.
F0-39Failing. 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

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 whether it is necessary because Astryx has no equivalent, or unnecessary because an Astryx component can replace it.

Raw HTML elementsPoints
030
1-2, all necessary25
1-2, any unnecessary20
3-515
6-108
11-204
21 or more0
Raw HTML useAstryx replacement
div for layoutVStack, HStack, Card, Section, or Center
div for a gridGrid
span or p for textText
h1 through h6Heading level={N}
buttonButton or IconButton
aLink
In-page nav or asideLayoutPanel in the start slot
header or mainLayoutHeader or LayoutContent in Layout
ul, ol, or liList and ListItem
input, textarea, or selectThe matching Astryx form control
table, tr, or tdTable
hrDivider
dialogDialog
details or summaryCollapsible
  • Do not deduct for an img when no general Astryx image component fits and its source passes the Image handling category.
  • Do not deduct for a form that wraps FormLayout to provide native submission semantics.
  • Do not deduct for input type="hidden" when it carries native form state.

Icon purity: 15 points

Count raw icon markup in the copied file. astryx docs cli/integrations/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, or g used as an icon.
  • Count an inline SVG component defined in the template.
  • Count an icon component rendered directly instead of through Icon or an Astryx icon prop.
Raw SVG icon instancesPoints
015
1-210
3-55
6 or more0

Custom CSS: 15 points

Prefer Astryx component props and design tokens. 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 declarationsPoints
015
1-3, all justified because no Astryx alternative exists12
1-3, any unjustified because an Astryx prop exists8
4-105
11-202
21 or more0

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

  1. Use Layout or Center as the page root. A template whose category starts with Shell - uses AppShell because global chrome is its purpose.
  2. Outside a Shell - template, leave global navigation to the host app. Put in-page navigation in a LayoutPanel and page headings in LayoutHeader.
  3. Use Grid with columns={{minWidth: 280}} for responsive collections. Do not fix the column count or rebuild the grid in raw CSS.
  4. Use Center for centered content instead of custom flexbox workarounds.
  5. Render one page from one source file. Links may be inert examples, but the template does not create nested routes or router integration.
Page conditionPoints
Correct root, responsive grids, proper centering, and one page15
Valid root with a smaller issue such as fixed columns or multi-page behavior8
Wrong root for the category, a raw layout root, or no Astryx root0

Block templates

  1. Do not wrap a block in AppShell. A block renders inside a preview or page container.
  2. Keep the block focused on one pattern or component usage.
  3. Keep the composition substantial enough to teach the pattern and small enough to adapt. About 20-100 lines is the normal range.
Block conditionPoints
No AppShell, one focused pattern, and a reasonable length15
A smaller focus or length issue10
Wrapped in AppShell or deeply unfocused0

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

Field conditionPoints
All applicable fields are present and accurate6
All fields are present with one inaccuracy4
One required field is missing2
Two or more required fields are missing, or no doc exists0

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 conditionPoints
All four slots, 6 or more distinct content words, and clear separation from sibling templates3
Names and differentiates the pattern but misses one slot or leaves a synonym implicit2
Generic, repeats component names, or describes only the sample data1
Missing or restates the name0

Naming: 1 point

The id follows astryx docs cli/integrations/building-blocks/templates/start-a-template, displayName is readable, and a browsable page has a specific category (astryx docs cli/integrations/building-blocks/templates/document-the-template/page-template). Slug length is guidance, not a scored condition.

Naming conditionPoints
Id, display name, and applicable category follow the convention1
Any naming or category requirement is missed0

Image handling: 5 points

Inspect every image reference in the copied file against astryx docs cli/integrations/building-blocks/templates/build-the-template/template-assets/template-images-media.

Image conditionPoints
No image is needed, or every image still works after copy and the demo-placeholder behavior is intentional5
One optional demo image is missing in preview, or a placeholder service remains2
An essential image breaks after copy, uses a package-relative path, or depends on an inaccessible URL0

Code quality: 10 points

Award 2 points for each condition. Grade the copied file, not only the package source.

ConditionPointsHow to verify
Correct client boundary2use 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 export2The copied file has one default-exported React component.
Self-contained imports2Every 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 data2Content has realistic names, amounts, dates, lengths, and states instead of lorem ipsum or numbered placeholders.
No dead code2There 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.

Agent grading prompt
text
Grade integration template <id> from <package> at <revision>.
​
Before scoring:
1. Read `npx astryx docs cli/integrations/building-blocks/templates/write-good-templates --full`.
2. Read `npx astryx docs cli/integrations/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 astryx integration verify` in the package.
5. Follow `npx astryx docs cli/integrations/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 fixes
1. <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

  1. Fix publication blockers first.
  2. Fix every reasonable deduction without hiding it behind helper components or package CSS.
  3. Pack again, copy into a fresh app, and have the agent repeat the full grade. Keep the earlier scorecard so the improvement is visible.
  4. Stop only when no reasonable fix remains, and record every intentional tradeoff that still costs points.

Themes

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 --import, 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 astryx 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.

bash
npx astryx integration add theme ocean
text
theme contribution added
​
[ok] ocean
​
Declare 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. oceanTheme.doc.mjs describes it with type: 'theme', name, displayName, description, and maintained; theme list shows its name, description, and maintained. The command also declares ./themes/ocean and ./themes/ocean.css package exports. Every descriptor field is in astryx docs authoring.

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 --import to record and import the built module and stylesheets; plain theme add still copies source while that default is deprecated. See astryx docs cli/integrations/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.

bash
npx astryx 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 (astryx docs cli/integrations/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.

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 and keep enough contrast to read.

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 astryx docs cli/integrations/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 files
npx astryx theme palette generate themes/ocean/palette.config.json
# Write the palette and its receipt
npx astryx 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.json

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. The full list of config fields and options is in astryx docs cli/commands/theme-palette-generate.

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.ts
import {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 the theme disappears from theme list. npx astryx theme template writes a file that explains every defineTheme field. For the full token set, scope selectors, and component theming, read astryx 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. The derived theme keeps a live link to the base and inherits its later changes — unlike --from, which forks a copy (astryx docs cli/integrations/building-blocks/themes/add-a-theme).

ts
import {defineTheme} from '@astryxdesign/core/theme';
import {oceanTheme} from './oceanTheme';
​
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: a browser without light-dark() drops the declaration, and the stop form is the one that degrades safely.

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.ts
export 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 astryx 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 --import 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. If the app loads the same family outside the generated module, Doctor warns because it cannot prove the loader, 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 or one of its packed files cannot resolve.

Verify in an app

Before publishing, run integration verify. Then install the package in a clean app, run theme add --import, 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.

  • Text renders 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 --import 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. Plain theme add still copies source while that default is deprecated.

bash
npm install @acme/astryx-widgets
npx astryx theme add ocean --import --package @acme/astryx-widgets
tsx
import {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 astryx 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 astryx docs cli/integrations/building-blocks/themes/define-the-theme.

ts
import {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']},
});

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 astryx docs theme. Add it with extends: 'theme' (astryx docs cli/integrations/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 --import --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 --import, apply the generated module (astryx docs cli/integrations/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 --import imports (astryx docs cli/integrations/building-blocks/themes/fonts-and-assets). Extend theme rather than a topic another package replaces, or an app that lists that package first 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 astryx docs cli/integrations/building-blocks/configuration/agent-guidance.

Add a codemod

integration add codemod writes a codemod into a folder named after a version. Apps run it with astryx upgrade to migrate their code.

bash
npx astryx integration add codemod rename-delay --to 0.7.0
text
codemods/
0.7.0/
rename-delay.mjs # the codemod
rename-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 astryx docs authoring.

Choose when a codemod runs

Two rules decide whether an app's astryx upgrade runs your codemods: the app's Core versions, and whether the app names your package.

  1. Version folders are matched against the app's @astryxdesign/core versions, 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.3 runs 0.7.0/, and --from 0.7.0 runs nothing. A folder named after your own release, such as 1.0.0/, waits until the app has Core 1.0.0.
  2. upgrade runs your codemods only when the app lists your package in integrations in its astryx.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 astryx upgrade and writes the changes with --apply. Without --apply, nothing on disk changes.

bash
# Preview each codemod and the files it would change
npx astryx upgrade --from 0.6.3 --integration @acme/astryx-widgets
# Write the changes
npx astryx upgrade --from 0.6.3 --integration @acme/astryx-widgets --apply
text
Integrations: @acme/astryx-widgets
1 codemod to run (dry run)
Applying integration codemods...
Rename AcmeCarousel delay to interval (v0.7.0, @acme/astryx-widgets)
! ~ src/Hero.tsx (would change)

The count includes Core's codemods for the same versions, which 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 astryx 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.

Configuration

Beyond the things you ship, an integration can set how it behaves in an app: the guidance it gives the AI agents working there, 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.

bash
npx astryx integration add agent-doc 'Use AcmeCarousel for rotating content.'

The line lands in agentDocs.append in your manifest:

js
// astryx.integration.mjs
export 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.

LimitWhat happens past it
8 lines per packageintegration add fails: append may contain at most 8 lines.
240 characters per lineintegration add fails: must contain at most 240 Unicode code points.
32 lines per app, from all its packagesinit 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.

See what agents read

An app's astryx 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.

bash
npx astryx init --features agents
text
INTEGRATIONS:
- `@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.

bash
npx astryx init --features agents
# Or: --from takes the Core version the app had before
npx astryx upgrade --from 0.7.0 --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 astryx docs cli/commands/upgrade.

Record runs with debug

Export a debug function from astryx.integration.mjs, and the CLI calls it once for each command run in an app that loads your package.

js
// astryx.integration.mjs
import {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 astryx docs authoring.

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 that load the app's project, such as component and docs, and not for --version or a mistyped command. In an app whose config names neither word, each of those commands also prints a warning on stderr.

debug is a named export, not a manifest field, so a CLI that does not know it 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}}

From then on, your handler no longer runs in that app.

Handle gap reports

Export a gapReport handler, and astryx 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. The receipt status is one of:

`status`Meaning
filedYou created or queued the report. Return url or message.
routed_onlyYou point the caller to where to file it. url is required.
skippedYou chose not to act, for example on a duplicate.

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.

bash
npx astryx gap-report AcmeCarousel --category missing_variant --reason 'Need a vertical layout'
text
handlerType: integration
handler: @acme/astryx-widgets
audience: public
status: consent_required
message: 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

When the app has no gapReport handler at all, the CLI routes the report to the target package's issuesUrl from its manifest instead.

  • 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_only receipt 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 astryx docs cli/commands/gap-report.

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 package
npm pack --pack-destination ..
​
# In an app folder beside it
npm install @astryxdesign/core @astryxdesign/cli ../acme-astryx-widgets-1.0.0.tgz
  • Install the CLI in the app too: npx astryx runs the CLI from the app's node_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.

bash
npx astryx component AcmeCarousel
npx astryx template --list --package @acme/astryx-widgets
npx astryx theme list --package @acme/astryx-widgets
npx astryx docs acme
npx astryx search deploying --type doc
CommandLook for
component AcmeCarouselimport {AcmeCarousel} from '@acme/astryx-widgets/components/AcmeCarousel';
template --list --packageAn acme-dashboard entry with package: @acme/astryx-widgets
theme list --package- ocean (maintained, @acme/astryx-widgets)
docs acmeYour section, with guides such as deploying
search deploying --type docacme/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 astryx doctor checks the whole app, including every package it loaded. doctor integration validate checks your package as the app installed it.

bash
npx astryx doctor
npx astryx doctor integration validate @acme/astryx-widgets
text
id: implicit-integrations
status: [info]
label: Implicitly linked integrations
message: 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-integrations row names each package the app loads with no config. [info] is expected.
  • The docs-progressive-disclosure row warns when one of your doc sections is over 32 KB. The doctor integration checks do not measure size.
  • doctor integration validate @acme/astryx-widgets prints [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; integration verify checks the package npm would publish.

CommandProvesExits 1 whenOnly warns when
npx astryx doctor integration validateThe manifest loads, and each root holds contributions the CLI can readA 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). With no astryx.integration.mjs it prints a hint and exits 0
npx astryx doctor integration componentsNo component name clashes with a Core component, and each replaces names one Core componentCore 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, or a component sets replaces in a package without that range (inactive_component_replacement, plus any missing, invalid, or duplicate replaces reported as a warning)
npx astryx doctor integration templatesEach replaces names a Core template of the same typeA 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 astryx doctor integration docsYour docs tree, every link, and topic names against CoreA 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)
npx astryx integration verifyThe packed package holds every file, shows the same contributions, resolves every public import, and declares the CLI it needsAnything validate fails on, no manifest, a file left out of the .tgz file, an import that does not resolve, or a missing CLI peerAnything 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 astryx doctor integration validate @acme/astryx-widgets, to check an installed copy from an app instead.

Run every check in CI

Chain the five checks so the first failure stops the run. Install devDependencies first, because the components check needs Core.

bash
npx astryx doctor integration validate && npx astryx doctor integration components && npx astryx doctor integration templates && npx astryx doctor integration docs && npx astryx integration verify
  • integration verify runs validate but none of the other three: a Core name clash, a replaces that names no Core template, a topic that takes a Core name, or a hidden guide still passes it.
  • Warnings keep exit code 0, so read them before you publish.
  • Bare npx astryx doctor in 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.

  1. It runs npm pack the way npm publish would, including your prepack script.
  2. It checks that every contribution file is in the .tgz file. A root missing from files fails with Add "templates" to "files" in package.json.
  3. It lists the components, templates, themes, docs, and codemods the temporary app sees, and compares them with your source.
  4. It resolves each component's import, and each template's public import, the way Node does, and checks that the module exports the component, or a default export for a template.
  5. 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.
  6. It fails a package that needs an @astryxdesign/cli peer and lacks it: >=0.6.6 for a template that sets keywords, and >=0.6.4 for a template that sets replaces, a docs section, a placed guide, a doc section with an id, or a theme.

integration pack --check, the name this check had in 0.6, still runs it and prints a note; it will be removed in a later release. The options and exit codes are in astryx 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 change
npm version major
# 1.0.0 -> 1.1.0: a new feature
npm version minor
# 1.0.0 -> 1.0.1: a fix
npm version patch
  • Before 1.0.0, npm treats each minor version as breaking: an app that asks for ^0.6.0 never gets 0.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 astryx docs cli/integrations/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 removeWhat stops working in the app
A component name, such as AcmeCarouselImports, and npx astryx component AcmeCarousel
A template id, such as acme-dashboardnpx astryx template acme-dashboard
A theme slug, such as oceannpx astryx theme add ocean --import
A topic name or route, such as acme/deployingReads of the old name, and links to it from other docs
A component's import pathImports written from the old path
An exports entryImports of that path, which fail with ERR_PACKAGE_PATH_NOT_EXPORTED
A propCode 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.

PeerDeclare it whenRange
@astryxdesign/coreYour code imports Core, as a theme does with @astryxdesign/core/themeThe Core versions you test, such as ^0.6.0
@astryxdesign/theme-*Your code imports that theme packageThe versions you test
@astryxdesign/cliYou ship a docs section, a placed guide, a template that sets replaces or keywords, a doc section with an id, or a theme>=0.6.6 when you ship a template that sets keywords, 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.

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.mjs is ignored with an unknown_manifest_key warning, and the rest of the manifest still loads.
  • A named export that the CLI does not know is ignored with no warning, so debug and gapReport are safe to add.
  • A stable CLI before 0.7.0 prints each {@link ...} as written.
  • Published 0.6.3 and earlier reject a template that sets replaces or keywords, drop that template, and hide every doc topic your package ships. Published 0.6.4 and 0.6.5 still drop a template that sets keywords.
  • A stable CLI before 0.6.4 cannot read a docs section, a section id, or a theme folder that integration add theme writes. It can then hide every doc topic your package ships.
  • Stable 0.6.3 still loads your components, but 0.6.0 cannot read the component docs that integration add component writes: component AcmeCarousel fails 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 astryx docs cli/integrations/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.

bash
npx astryx doctor integration validate && npx astryx doctor integration components && npx astryx doctor integration templates && npx astryx doctor integration docs && npx astryx integration verify
npm publish --dry-run --access public
text
npm 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; astryx docs cli/integrations/ship/versioning covers what counts as breaking. What each check proves is in astryx docs cli/integrations/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.

bash
npm pkg set 'files[]=components' 'files[]=templates' 'files[]=themes' 'files[]=docs' 'files[]=codemods' 'files[]=astryx.integration.mjs'
npm pack --dry-run
  • List only the roots you ship, and always astryx.integration.mjs. npm adds package.json, and a README and LICENSE when you have them.
  • Each later integration add appends its root to the list.
  • integration verify fails when a root is missing from the list: Declared templates root "templates" has 0 of 2 expected files in the pack list. Add "templates" to "files" in package.json.

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 release
npm publish --access public
​
# Or set the access once in package.json
npm pkg set publishConfig.access=public

To let apps try a release first, publish it under next, then point latest at it when it is ready.

bash
# Only apps that ask for next get it: npm install @acme/astryx-widgets@next
npm publish --tag next
​
# Make that version the default
npm dist-tag add @acme/astryx-widgets@1.1.0 latest
npm 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.

bash
mkdir check-release && cd check-release
npm init -y
npm install @astryxdesign/core @astryxdesign/cli @acme/astryx-widgets@1.0.0
npx astryx component AcmeCarousel
npx astryx doctor integration validate @acme/astryx-widgets

The full list of commands to run in an app is in astryx docs cli/integrations/ship/test-in-an-app. To help apps move to the new version, see astryx docs cli/integrations/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 release
npm install -D @astryxdesign/cli@latest @astryxdesign/core@latest
# 2. Rerun the checks
npx astryx doctor integration validate
npx astryx doctor integration docs
npx astryx integration verify
# 3. Admit the new Core once the checks pass
npm pkg set 'peerDependencies.@astryxdesign/core=^0.6.0 || ^0.7.0'
# 4. Migrate apps across a change that breaks them
npx astryx integration add codemod rename-delay --to 0.7.0

Upgrade an app

After an app installs a new Core, astryx 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 written
npx astryx upgrade --from 0.6.3 --integration @acme/astryx-widgets
# Write the changes
npx astryx upgrade --from 0.6.3 --integration @acme/astryx-widgets --apply
text
Integrations: @acme/astryx-widgets
1 codemod to run
Applying integration codemods...
Rename AcmeCarousel delay to interval (v0.7.0, @acme/astryx-widgets)
[ok] [ok] src/Hero.tsx
  • --from is the @astryxdesign/core version the app had before, and the target is the installed Core.
  • --integration names your package. Without it, or an integrations entry in astryx.config, the upgrade skips your codemods, even when the app has your package installed.
  • The count 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 @generated comment at the top of the file, or linguist-generated in .gitattributes.
  • Vendored: linguist-vendored in .gitattributes.
  • Ignored: a match in .gitignore or .hgignore.
text
! ! src/gen/Gen.tsx - protected by @generated in the leading comment block
ERR_CODEMOD_PROTECTED: 1 protected file still requires a codemod change:
src/gen/Gen.tsx — @generated in the leading comment block
Upgrade incomplete: protected changes remain

The upgrade still writes every other file. Its options and exit codes are in astryx 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.

MessageFix
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 packageComponent 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 astryx integration verify; the old name will be removed in a later release.

Fix integration verify failures

npx astryx integration verify prints each failure as a [fail] line. Add --json to see its issue code, such as component_export_missing.

MessageFix
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'.
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 the component keeps its own name and the Core component stays selected; a component named like its target stays ambiguous by that bare name.
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. A stable CLI before 0.6.6 rejects keywords and drops that template, and one before 0.6.4 also hides your doc topics.
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 stable CLI before 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 stable CLI before 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.

SymptomFix
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 astryx 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 astryx doctor integration docs in that package to see why.One invalid doc hides all of your docs. Run npx astryx doctor integration docs in the package and fix the invalid_doc it names.
npx astryx upgrade runs none of your codemodsThe 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 appsThose apps run a CLI too old to read them: a stable CLI before 0.6.6 for a template that sets keywords, or before 0.6.4 for a template that sets replaces, a docs section, or a theme. Update @astryxdesign/cli there, and keep your optional @astryxdesign/cli peer so npm warns about an old CLI.

What each check catches is in astryx docs cli/integrations/ship/checks.