@astryxdesign/cli 0.1.0 → 0.1.1-canary.129bf0e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +117 -75
- package/bin/astryx.mjs +22 -7
- package/docs/getting-started.doc.mjs +11 -11
- package/docs/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +2 -2
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling.doc.mjs +3 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +14 -0
- package/docs/theme.doc.zh.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +4 -4
- package/package.json +8 -8
- package/src/api/doctor.mjs +3 -3
- package/src/api/search.mjs +207 -13
- package/src/api/template.mjs +62 -11
- package/src/api/template.test.mjs +2 -0
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/runner.mjs +105 -51
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
- package/src/commands/agent-docs.mjs +119 -66
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +87 -31
- package/src/commands/build-theme.import-path.test.mjs +1 -1
- package/src/commands/build-theme.path-safety.test.mjs +1 -1
- package/src/commands/build-theme.prose.test.mjs +1 -1
- package/src/commands/build.mjs +196 -0
- package/src/commands/component-package.test.mjs +1 -1
- package/src/commands/component.test.mjs +1 -1
- package/src/commands/docs.test.mjs +1 -1
- package/src/commands/doctor.test.mjs +1 -1
- package/src/commands/external-showcase.test.mjs +1 -1
- package/src/commands/init.mjs +43 -9
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +10 -3
- package/src/commands/swizzle-gap-safety.test.mjs +1 -1
- package/src/commands/swizzle.path-safety.test.mjs +1 -1
- package/src/commands/template.path-safety.test.mjs +1 -1
- package/src/commands/template.test.mjs +1 -1
- package/src/commands/upgrade.mjs +353 -169
- package/src/commands/upgrade.test.mjs +41 -27
- package/src/index.mjs +1 -0
- package/src/lib/config.mjs +12 -0
- package/src/lib/config.test.mjs +42 -0
- package/src/lib/error-codes.mjs +3 -0
- package/src/types/error-codes.d.ts +1 -0
- package/src/utils/interactive.mjs +1 -1
- package/src/utils/interactive.test.mjs +2 -0
- package/src/utils/package-manager.mjs +1 -1
- package/src/utils/package-manager.test.mjs +1 -1
- package/src/utils/path-safety.test.mjs +1 -1
- package/src/utils/paths.test.mjs +8 -8
- package/src/utils/update-check.mjs +4 -26
- package/src/utils/update-check.test.mjs +2 -64
- package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
- package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
- package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
- package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
- package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
- package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
- package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
- package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
- package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
- package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
- package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
- package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
- package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
- package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
- package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
- package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
- package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
- package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
- package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
- package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +2 -12
- package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
- package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
- package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
- package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
- package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
- package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
- package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
- package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
- package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
- package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
- package/templates/pages/ai-chat/page.tsx +71 -64
- package/templates/pages/ai-chat-landing/page.tsx +8 -12
- package/templates/pages/centered-hero/page.tsx +13 -15
- package/templates/pages/classic-gallery/page.tsx +27 -34
- package/templates/pages/detail-page/page.tsx +18 -18
- package/templates/pages/documentation/page.tsx +42 -58
- package/templates/pages/documentation-design/page.tsx +82 -60
- package/templates/pages/documentation-technical/page.tsx +101 -60
- package/templates/pages/editor/page.tsx +42 -54
- package/templates/pages/file-explorer/page.tsx +13 -16
- package/templates/pages/form-two-column/page.tsx +13 -17
- package/templates/pages/gallery-hero/page.tsx +13 -15
- package/templates/pages/ide/page.tsx +188 -264
- package/templates/pages/library/page.tsx +16 -23
- package/templates/pages/login/page.tsx +14 -18
- package/templates/pages/login-card/page.tsx +14 -18
- package/templates/pages/login-split/page.tsx +50 -48
- package/templates/pages/login-sso/page.tsx +9 -13
- package/templates/pages/mixed-gallery/page.tsx +51 -45
- package/templates/pages/payment-form/page.tsx +56 -70
- package/templates/pages/product-detail/page.tsx +27 -33
- package/templates/pages/product-gallery/page.tsx +7 -13
- package/templates/pages/settings-dialog/page.tsx +35 -43
- package/templates/pages/settings-sidebar/page.tsx +39 -47
- package/templates/pages/side-gallery/page.tsx +6 -9
- package/templates/pages/table-grouped/page.tsx +11 -15
- package/templates/pages/theme-showcase/page.tsx +33 -37
|
@@ -31,8 +31,11 @@ const AGENTS_MD = 'AGENTS.md';
|
|
|
31
31
|
const CLAUDE_MD = 'CLAUDE.md';
|
|
32
32
|
const CLAUDE_DIR_MD = path.join('.claude', 'CLAUDE.md');
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
const
|
|
34
|
+
const MARKER_START = '<!-- ASTRYX:START -->';
|
|
35
|
+
const MARKER_END = '<!-- ASTRYX:END -->';
|
|
36
|
+
// Legacy markers — read during migration so the script finds existing XDS blocks
|
|
37
|
+
const LEGACY_MARKER_START = '<!-- XDS:START -->';
|
|
38
|
+
const LEGACY_MARKER_END = '<!-- XDS:END -->';
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* Agent tool presets — maps tool names to their file search paths.
|
|
@@ -90,16 +93,57 @@ export function resolveAgentPaths(targetDir, agent) {
|
|
|
90
93
|
return {inject: [], create: [searchPaths[searchPaths.length - 1]]};
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Detect which styling system the consumer project has wired up, so the agent
|
|
98
|
+
* docs recommend a path that actually compiles in THIS project.
|
|
99
|
+
*
|
|
100
|
+
* `xstyle`/StyleX needs the StyleX compiler (the `@stylexjs/stylex` runtime
|
|
101
|
+
* alone throws at runtime → blank page); Tailwind utilities need Tailwind.
|
|
102
|
+
* Recommending either when it isn't configured yields unstyled or blank output.
|
|
103
|
+
* Plain CSS variables (via `style`/`className`) always work, so they're the
|
|
104
|
+
* safe default. Precedence: stylex (compiler wired) → tailwind → css.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} targetDir
|
|
107
|
+
* @returns {'stylex' | 'tailwind' | 'css'}
|
|
108
|
+
*/
|
|
109
|
+
export function detectStylingSystem(targetDir) {
|
|
110
|
+
try {
|
|
111
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
112
|
+
if (!fs.existsSync(pkgPath)) return 'css';
|
|
113
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
114
|
+
const deps = {...pkg.dependencies, ...pkg.devDependencies};
|
|
115
|
+
// Key off a StyleX *compiler* plugin — the runtime alone won't render.
|
|
116
|
+
const stylexCompilers = [
|
|
117
|
+
'@stylexjs/babel-plugin',
|
|
118
|
+
'vite-plugin-stylex',
|
|
119
|
+
'unplugin-stylex',
|
|
120
|
+
'@stylexswc/unplugin',
|
|
121
|
+
'@stylexswc/nextjs-plugin',
|
|
122
|
+
'stylex-webpack',
|
|
123
|
+
];
|
|
124
|
+
if (stylexCompilers.some(d => d in deps)) return 'stylex';
|
|
125
|
+
if ('tailwindcss' in deps) return 'tailwind';
|
|
126
|
+
return 'css';
|
|
127
|
+
} catch {
|
|
128
|
+
// Best-effort: default to the universally-safe CSS-variable path.
|
|
129
|
+
return 'css';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
93
133
|
/**
|
|
94
134
|
* Generate the agent cheat sheet from live CLI metadata.
|
|
95
135
|
*
|
|
96
136
|
* Structured as: workflow (behavioral) → rules (error prevention) → CLI reference.
|
|
97
137
|
* Templates are positioned first in the workflow to teach agents the
|
|
98
138
|
* "look at reference code" reflex before writing any UI.
|
|
139
|
+
*
|
|
140
|
+
* `stylingSystem` tailors the custom-styling guidance to what the project has
|
|
141
|
+
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
142
|
+
* styling path that isn't compiled here.
|
|
99
143
|
*/
|
|
100
|
-
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix()} = {}) {
|
|
144
|
+
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix(), stylingSystem = 'css'} = {}) {
|
|
101
145
|
const run = `${runPrefix} astryx`;
|
|
102
|
-
const lines = [
|
|
146
|
+
const lines = [MARKER_START];
|
|
103
147
|
|
|
104
148
|
// Component count from live discovery
|
|
105
149
|
let componentCount = '90+';
|
|
@@ -114,70 +158,58 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
|
|
|
114
158
|
}
|
|
115
159
|
}
|
|
116
160
|
|
|
117
|
-
// Header
|
|
118
|
-
lines.push(`Astryx v${version}
|
|
161
|
+
// Header — state the CLI prefix once; commands below are shown as `astryx <cmd>`.
|
|
162
|
+
lines.push(`Astryx v${version} · ${componentCount} components`);
|
|
163
|
+
lines.push(`CLI: run every command as \`${run} <cmd>\` (shown below as \`astryx ...\`).`);
|
|
119
164
|
lines.push('');
|
|
120
165
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
lines.push(
|
|
124
|
-
lines.push(
|
|
125
|
-
lines.push(
|
|
126
|
-
lines.push('');
|
|
127
|
-
lines.push('Templates are reference code — read them for composition patterns, not just scaffolding.');
|
|
128
|
-
lines.push('Full pages → dashboard (uses AppShell). Forms → contact-form. Tables → data-table. Settings → settings-sidebar.');
|
|
166
|
+
// Required setup — components ship precompiled CSS; without these imports
|
|
167
|
+
// everything renders unstyled. Theme is optional (a default ships in astryx.css).
|
|
168
|
+
lines.push('SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled:');
|
|
169
|
+
lines.push(' import "@astryxdesign/core/reset.css";');
|
|
170
|
+
lines.push(' import "@astryxdesign/core/astryx.css";');
|
|
129
171
|
lines.push('');
|
|
130
172
|
|
|
131
|
-
//
|
|
132
|
-
lines.push(
|
|
133
|
-
lines.push('
|
|
134
|
-
lines.push('
|
|
135
|
-
lines.push('
|
|
136
|
-
lines.push(`No magic values — run \`${run} docs tokens\` for spacing/color/radius.`);
|
|
137
|
-
lines.push(`To change accent/brand colors: \`${run} theme\` — never override --astryx-color-* in :root.`);
|
|
173
|
+
// Workflow — `build` is the front door; discover before writing UI.
|
|
174
|
+
lines.push("WORKFLOW — discover, don't guess. Before writing UI:");
|
|
175
|
+
lines.push('1. `astryx build "<idea>"` — START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.');
|
|
176
|
+
lines.push('2. `astryx template <name> [--skeleton]` — scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.');
|
|
177
|
+
lines.push('3. `astryx component <Name>` — props + examples for every component you use.');
|
|
138
178
|
lines.push('');
|
|
139
179
|
|
|
140
|
-
//
|
|
141
|
-
lines.push(
|
|
142
|
-
lines.push(
|
|
180
|
+
// Rules — the top error-preventers.
|
|
181
|
+
lines.push('RULES:');
|
|
182
|
+
lines.push('- No <div> — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.');
|
|
183
|
+
// Styling guidance tailored to the project's configured system — never
|
|
184
|
+
// recommend a path that isn't compiled here (xstyle needs the StyleX compiler;
|
|
185
|
+
// utilities need Tailwind). Tokens are always the source of truth.
|
|
186
|
+
if (stylingSystem === 'stylex') {
|
|
187
|
+
lines.push('- Custom styling: component props first; else the xstyle prop / StyleX tokens (@astryxdesign/core/theme/tokens.stylex). No raw hex/px.');
|
|
188
|
+
} else if (stylingSystem === 'tailwind') {
|
|
189
|
+
lines.push('- Custom styling: component props first; else Tailwind utilities backed by tokens (bg-surface, text-primary, rounded-lg) via tailwind-theme.css. No raw hex/px.');
|
|
190
|
+
} else {
|
|
191
|
+
lines.push("- Custom styling: component props first; else style/className with tokens — var(--color-*|--spacing-*|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)");
|
|
192
|
+
}
|
|
193
|
+
lines.push('- Tokens for every value (`astryx docs tokens`). Brand/accent via `astryx theme` — never override --color-* in :root.');
|
|
194
|
+
lines.push('');
|
|
143
195
|
|
|
144
|
-
//
|
|
196
|
+
// Command reference — build/template/component are covered in WORKFLOW above.
|
|
197
|
+
lines.push('MORE CLI:');
|
|
198
|
+
lines.push(' search "<query>" find any component / hook / doc / template / block');
|
|
199
|
+
lines.push(` component --list ${componentCount} components by category`);
|
|
200
|
+
lines.push(' template --list page + block recipes');
|
|
145
201
|
const docsDir = path.join(CLI_ROOT, 'docs');
|
|
146
202
|
if (fs.existsSync(docsDir)) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let desc = topic;
|
|
152
|
-
try {
|
|
153
|
-
const fileContent = fs.readFileSync(path.join(docsDir, file), 'utf-8');
|
|
154
|
-
const descMatch = fileContent.match(/description:\s*['"](.+?)['"]/);
|
|
155
|
-
if (descMatch) desc = descMatch[1];
|
|
156
|
-
} catch {
|
|
157
|
-
// Best-effort: fall back to the topic name if the file is unreadable.
|
|
158
|
-
}
|
|
159
|
-
if (desc.length > 50) desc = desc.slice(0, 47) + '...';
|
|
160
|
-
lines.push(`${run} docs ${topic.padEnd(20)} ${desc}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Templates
|
|
165
|
-
const templatesDir = path.join(CLI_ROOT, 'templates');
|
|
166
|
-
if (fs.existsSync(templatesDir)) {
|
|
167
|
-
const templates = fs.readdirSync(templatesDir, {withFileTypes: true})
|
|
168
|
-
.filter(e => e.isDirectory())
|
|
169
|
-
.map(e => e.name)
|
|
203
|
+
const topics = fs.readdirSync(docsDir)
|
|
204
|
+
.map(f => f.match(/^(\w+)\.doc\.mjs$/))
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.map(m => m[1])
|
|
170
207
|
.sort();
|
|
171
|
-
if (
|
|
172
|
-
lines.push(`${run} template --list page recipes with component lists`);
|
|
173
|
-
lines.push(`${run} template <name> [path] scaffold from template`);
|
|
174
|
-
}
|
|
208
|
+
if (topics.length > 0) lines.push(` docs <topic> ${topics.join(', ')}`);
|
|
175
209
|
}
|
|
176
|
-
|
|
177
|
-
lines.push(
|
|
178
|
-
lines.push(
|
|
179
|
-
lines.push(`after @astryxdesign/core bump, always run ${run} upgrade --apply`);
|
|
180
|
-
lines.push(XDS_MARKER_END);
|
|
210
|
+
lines.push(' swizzle <Name> eject component source (--gap reports why)');
|
|
211
|
+
lines.push(' upgrade --apply run after any @astryxdesign/core bump');
|
|
212
|
+
lines.push(MARKER_END);
|
|
181
213
|
|
|
182
214
|
return lines.join('\n');
|
|
183
215
|
}
|
|
@@ -218,14 +250,21 @@ export function injectXdsBlock(filePath, compressedIndex, {createIfMissing = fal
|
|
|
218
250
|
if (fs.existsSync(filePath)) {
|
|
219
251
|
content = fs.readFileSync(filePath, 'utf-8');
|
|
220
252
|
|
|
221
|
-
|
|
222
|
-
|
|
253
|
+
// Find existing section — try new markers first, fall back to legacy XDS markers
|
|
254
|
+
let startIdx = content.indexOf(MARKER_START);
|
|
255
|
+
let endIdx = content.indexOf(MARKER_END);
|
|
256
|
+
let markerEndLength = MARKER_END.length;
|
|
257
|
+
if (startIdx === -1) {
|
|
258
|
+
startIdx = content.indexOf(LEGACY_MARKER_START);
|
|
259
|
+
endIdx = content.indexOf(LEGACY_MARKER_END);
|
|
260
|
+
markerEndLength = LEGACY_MARKER_END.length;
|
|
261
|
+
}
|
|
223
262
|
|
|
224
263
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
225
264
|
content =
|
|
226
265
|
content.slice(0, startIdx) +
|
|
227
266
|
compressedIndex +
|
|
228
|
-
content.slice(endIdx +
|
|
267
|
+
content.slice(endIdx + markerEndLength);
|
|
229
268
|
} else if (onlyReplace) {
|
|
230
269
|
// File exists but has no Astryx markers — skip it
|
|
231
270
|
return false;
|
|
@@ -248,7 +287,10 @@ export function injectXdsBlock(filePath, compressedIndex, {createIfMissing = fal
|
|
|
248
287
|
*/
|
|
249
288
|
export function injectAgentsMd(targetDir, version) {
|
|
250
289
|
const agentsPath = path.join(targetDir, AGENTS_MD);
|
|
251
|
-
const compressedIndex = generateCompressedIndex(version, {
|
|
290
|
+
const compressedIndex = generateCompressedIndex(version, {
|
|
291
|
+
coreDir: findCoreDir(targetDir),
|
|
292
|
+
stylingSystem: detectStylingSystem(targetDir),
|
|
293
|
+
});
|
|
252
294
|
injectXdsBlock(agentsPath, compressedIndex, {
|
|
253
295
|
createIfMissing: true,
|
|
254
296
|
header: `# AGENTS.md\n\nProject-specific guidance for AI coding agents.`,
|
|
@@ -263,7 +305,10 @@ export function injectAgentsMd(targetDir, version) {
|
|
|
263
305
|
*/
|
|
264
306
|
export function injectClaudeMd(targetDir, version) {
|
|
265
307
|
const claudePath = path.join(targetDir, CLAUDE_MD);
|
|
266
|
-
const compressedIndex = generateCompressedIndex(version, {
|
|
308
|
+
const compressedIndex = generateCompressedIndex(version, {
|
|
309
|
+
coreDir: findCoreDir(targetDir),
|
|
310
|
+
stylingSystem: detectStylingSystem(targetDir),
|
|
311
|
+
});
|
|
267
312
|
return injectXdsBlock(claudePath, compressedIndex);
|
|
268
313
|
}
|
|
269
314
|
|
|
@@ -277,13 +322,20 @@ export function removeXdsBlock(filePath, {deleteIfEmpty = false} = {}) {
|
|
|
277
322
|
if (!fs.existsSync(filePath)) return false;
|
|
278
323
|
|
|
279
324
|
let content = fs.readFileSync(filePath, 'utf-8');
|
|
280
|
-
|
|
281
|
-
|
|
325
|
+
// Find existing section — try new markers first, fall back to legacy
|
|
326
|
+
let startIdx = content.indexOf(MARKER_START);
|
|
327
|
+
let endIdx = content.indexOf(MARKER_END);
|
|
328
|
+
let markerEndLen = MARKER_END.length;
|
|
329
|
+
if (startIdx === -1) {
|
|
330
|
+
startIdx = content.indexOf(LEGACY_MARKER_START);
|
|
331
|
+
endIdx = content.indexOf(LEGACY_MARKER_END);
|
|
332
|
+
markerEndLen = LEGACY_MARKER_END.length;
|
|
333
|
+
}
|
|
282
334
|
|
|
283
335
|
if (startIdx === -1 || endIdx === -1) return false;
|
|
284
336
|
|
|
285
337
|
const before = content.slice(0, startIdx).trimEnd();
|
|
286
|
-
const after = content.slice(endIdx +
|
|
338
|
+
const after = content.slice(endIdx + markerEndLen).trimStart();
|
|
287
339
|
content = before + (after ? '\n\n' + after : '') + '\n';
|
|
288
340
|
|
|
289
341
|
if (deleteIfEmpty) {
|
|
@@ -339,7 +391,8 @@ export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onl
|
|
|
339
391
|
const coreDir = findCoreDir(targetDir);
|
|
340
392
|
const version = getXdsVersion(coreDir);
|
|
341
393
|
const runPrefix = getRunPrefix(targetDir);
|
|
342
|
-
const
|
|
394
|
+
const stylingSystem = detectStylingSystem(targetDir);
|
|
395
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, runPrefix, stylingSystem});
|
|
343
396
|
const written = [];
|
|
344
397
|
|
|
345
398
|
// Explicit paths override everything
|
|
@@ -20,7 +20,7 @@ import {PathSafetyError} from '../utils/path-safety.mjs';
|
|
|
20
20
|
|
|
21
21
|
let tmpDir;
|
|
22
22
|
beforeEach(() => {
|
|
23
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-paths-'));
|
|
24
24
|
});
|
|
25
25
|
afterEach(() => {
|
|
26
26
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -6,6 +6,7 @@ import * as path from 'node:path';
|
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
import {
|
|
8
8
|
generateCompressedIndex,
|
|
9
|
+
detectStylingSystem,
|
|
9
10
|
getXdsVersion,
|
|
10
11
|
installAgentDocs,
|
|
11
12
|
injectAgentsMd,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
let tmpDir;
|
|
21
22
|
|
|
22
23
|
beforeEach(() => {
|
|
23
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
24
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-test-'));
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
afterEach(() => {
|
|
@@ -32,38 +33,93 @@ describe('generateCompressedIndex', () => {
|
|
|
32
33
|
it('includes the version number', () => {
|
|
33
34
|
const result = generateCompressedIndex('1.2.3');
|
|
34
35
|
expect(result).toContain('Astryx v1.2.3');
|
|
35
|
-
expect(result).toContain('<!--
|
|
36
|
-
expect(result).toContain('<!--
|
|
36
|
+
expect(result).toContain('<!-- ASTRYX:START -->');
|
|
37
|
+
expect(result).toContain('<!-- ASTRYX:END -->');
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
it('includes theme nudge rule', () => {
|
|
40
41
|
const result = generateCompressedIndex('1.0.0');
|
|
41
42
|
expect(result).toMatch(/astryx theme/);
|
|
42
|
-
expect(result).toMatch(/never override --
|
|
43
|
+
expect(result).toMatch(/never override --color-/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('defaults to the CSS-variable styling path (no compiler)', () => {
|
|
47
|
+
const result = generateCompressedIndex('1.0.0');
|
|
48
|
+
expect(result).toMatch(/style\/className with tokens/);
|
|
49
|
+
expect(result).toMatch(/var\(--color-\*/);
|
|
50
|
+
// Must NOT push xstyle when no StyleX compiler is present.
|
|
51
|
+
expect(result).not.toMatch(/xstyle prop/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('recommends xstyle when StyleX is configured', () => {
|
|
55
|
+
const result = generateCompressedIndex('1.0.0', {stylingSystem: 'stylex'});
|
|
56
|
+
expect(result).toMatch(/xstyle prop \/ StyleX tokens/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('recommends Tailwind utilities when Tailwind is configured', () => {
|
|
60
|
+
const result = generateCompressedIndex('1.0.0', {stylingSystem: 'tailwind'});
|
|
61
|
+
expect(result).toMatch(/Tailwind utilities backed by tokens/);
|
|
62
|
+
expect(result).toMatch(/tailwind-theme\.css/);
|
|
43
63
|
});
|
|
44
64
|
|
|
45
65
|
it('includes upgrade command and migration rule', () => {
|
|
46
66
|
const result = generateCompressedIndex('1.0.0');
|
|
47
|
-
expect(result).toContain('
|
|
48
|
-
expect(result).
|
|
49
|
-
expect(result).toMatch(/always run .+ astryx upgrade --apply/);
|
|
67
|
+
expect(result).toContain('upgrade --apply');
|
|
68
|
+
expect(result).toMatch(/after any @astryxdesign\/core bump/);
|
|
50
69
|
});
|
|
51
70
|
|
|
52
|
-
it('
|
|
71
|
+
it('states the runPrefix once in the CLI header', () => {
|
|
53
72
|
const result = generateCompressedIndex('1.0.0', {runPrefix: 'yarn'});
|
|
54
|
-
expect(result).toContain('yarn astryx
|
|
55
|
-
expect(result).toContain('yarn astryx upgrade --apply');
|
|
56
|
-
expect(result).toContain('after @astryxdesign/core bump, always run yarn astryx upgrade --apply');
|
|
73
|
+
expect(result).toContain('yarn astryx <cmd>');
|
|
57
74
|
expect(result).not.toContain('npx astryx');
|
|
58
75
|
});
|
|
59
76
|
|
|
60
77
|
it('uses pnpm exec prefix', () => {
|
|
61
78
|
const result = generateCompressedIndex('1.0.0', {runPrefix: 'pnpm exec'});
|
|
62
|
-
expect(result).toContain('pnpm exec astryx
|
|
79
|
+
expect(result).toContain('pnpm exec astryx <cmd>');
|
|
63
80
|
expect(result).not.toContain('npx astryx');
|
|
64
81
|
});
|
|
65
82
|
});
|
|
66
83
|
|
|
84
|
+
describe('detectStylingSystem', () => {
|
|
85
|
+
function writePkg(deps) {
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(tmpDir, 'package.json'),
|
|
88
|
+
JSON.stringify({name: 'x', devDependencies: deps}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it('defaults to css when no package.json', () => {
|
|
93
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns css for a plain project', () => {
|
|
97
|
+
writePkg({react: '19.0.0', vite: '6.0.0'});
|
|
98
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('detects stylex when the compiler plugin is present', () => {
|
|
102
|
+
writePkg({'@stylexjs/babel-plugin': '0.0.1'});
|
|
103
|
+
expect(detectStylingSystem(tmpDir)).toBe('stylex');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('detects tailwind when tailwindcss is present', () => {
|
|
107
|
+
writePkg({tailwindcss: '4.0.0'});
|
|
108
|
+
expect(detectStylingSystem(tmpDir)).toBe('tailwind');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does NOT treat the StyleX runtime alone as a compiler', () => {
|
|
112
|
+
// Only the runtime, no compiler plugin → must stay on the safe css path.
|
|
113
|
+
writePkg({'@stylexjs/stylex': '0.0.1'});
|
|
114
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('prefers stylex over tailwind when both are configured', () => {
|
|
118
|
+
writePkg({'@stylexjs/babel-plugin': '0.0.1', tailwindcss: '4.0.0'});
|
|
119
|
+
expect(detectStylingSystem(tmpDir)).toBe('stylex');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
67
123
|
describe('getXdsVersion', () => {
|
|
68
124
|
it('reads version from core package.json', () => {
|
|
69
125
|
const coreDir = path.join(tmpDir, 'core');
|
|
@@ -82,19 +138,19 @@ describe('injectXdsBlock', () => {
|
|
|
82
138
|
const filePath = path.join(tmpDir, 'test.md');
|
|
83
139
|
fs.writeFileSync(filePath, '# Existing content\n');
|
|
84
140
|
|
|
85
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
141
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->');
|
|
86
142
|
|
|
87
143
|
expect(result).toBe(true);
|
|
88
144
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
145
|
expect(content).toContain('# Existing content');
|
|
90
|
-
expect(content).toContain('<!--
|
|
146
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
91
147
|
});
|
|
92
148
|
|
|
93
149
|
it('replaces existing markers', () => {
|
|
94
150
|
const filePath = path.join(tmpDir, 'test.md');
|
|
95
151
|
fs.writeFileSync(filePath, 'before\n<!-- XDS:START -->\nold\n<!-- XDS:END -->\nafter\n');
|
|
96
152
|
|
|
97
|
-
injectXdsBlock(filePath, '<!--
|
|
153
|
+
injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->');
|
|
98
154
|
|
|
99
155
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
100
156
|
expect(content).toContain('new');
|
|
@@ -106,7 +162,7 @@ describe('injectXdsBlock', () => {
|
|
|
106
162
|
it('returns false and does not create file when createIfMissing is false', () => {
|
|
107
163
|
const filePath = path.join(tmpDir, 'nonexistent.md');
|
|
108
164
|
|
|
109
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
165
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\ncontent\n<!-- ASTRYX:END -->');
|
|
110
166
|
|
|
111
167
|
expect(result).toBe(false);
|
|
112
168
|
expect(fs.existsSync(filePath)).toBe(false);
|
|
@@ -116,11 +172,11 @@ describe('injectXdsBlock', () => {
|
|
|
116
172
|
const filePath = path.join(tmpDir, 'test.md');
|
|
117
173
|
fs.writeFileSync(filePath, '# Existing content\n\nNo XDS markers here.\n');
|
|
118
174
|
|
|
119
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
175
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->', {onlyReplace: true});
|
|
120
176
|
|
|
121
177
|
expect(result).toBe(false);
|
|
122
178
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
123
|
-
expect(content).not.toContain('<!--
|
|
179
|
+
expect(content).not.toContain('<!-- ASTRYX:START -->');
|
|
124
180
|
expect(content).toBe('# Existing content\n\nNo XDS markers here.\n');
|
|
125
181
|
});
|
|
126
182
|
|
|
@@ -128,7 +184,7 @@ describe('injectXdsBlock', () => {
|
|
|
128
184
|
const filePath = path.join(tmpDir, 'test.md');
|
|
129
185
|
fs.writeFileSync(filePath, 'before\n<!-- XDS:START -->\nold\n<!-- XDS:END -->\nafter\n');
|
|
130
186
|
|
|
131
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
187
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\nnew\n<!-- ASTRYX:END -->', {onlyReplace: true});
|
|
132
188
|
|
|
133
189
|
expect(result).toBe(true);
|
|
134
190
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -139,7 +195,7 @@ describe('injectXdsBlock', () => {
|
|
|
139
195
|
it('creates file when createIfMissing is true', () => {
|
|
140
196
|
const filePath = path.join(tmpDir, 'new.md');
|
|
141
197
|
|
|
142
|
-
const result = injectXdsBlock(filePath, '<!--
|
|
198
|
+
const result = injectXdsBlock(filePath, '<!-- ASTRYX:START -->\ncontent\n<!-- ASTRYX:END -->', {
|
|
143
199
|
createIfMissing: true,
|
|
144
200
|
header: '# Header',
|
|
145
201
|
});
|
|
@@ -147,7 +203,7 @@ describe('injectXdsBlock', () => {
|
|
|
147
203
|
expect(result).toBe(true);
|
|
148
204
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
149
205
|
expect(content).toContain('# Header');
|
|
150
|
-
expect(content).toContain('<!--
|
|
206
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
151
207
|
});
|
|
152
208
|
});
|
|
153
209
|
|
|
@@ -157,9 +213,9 @@ describe('injectAgentsMd', () => {
|
|
|
157
213
|
|
|
158
214
|
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
159
215
|
expect(content).toContain('# AGENTS.md');
|
|
160
|
-
expect(content).toContain('<!--
|
|
216
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
161
217
|
expect(content).toContain('Astryx v1.0.0');
|
|
162
|
-
expect(content).toContain('<!--
|
|
218
|
+
expect(content).toContain('<!-- ASTRYX:END -->');
|
|
163
219
|
});
|
|
164
220
|
|
|
165
221
|
it('updates existing AGENTS.md by replacing XDS markers', () => {
|
|
@@ -195,7 +251,7 @@ Existing agent docs.
|
|
|
195
251
|
|
|
196
252
|
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
197
253
|
expect(content).toContain('Existing agent docs.');
|
|
198
|
-
expect(content).toContain('<!--
|
|
254
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
199
255
|
expect(content).toContain('Astryx v1.0.0');
|
|
200
256
|
});
|
|
201
257
|
});
|
|
@@ -210,7 +266,7 @@ describe('injectClaudeMd', () => {
|
|
|
210
266
|
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
211
267
|
expect(content).toContain('# Claude Config');
|
|
212
268
|
expect(content).toContain('Existing rules.');
|
|
213
|
-
expect(content).toContain('<!--
|
|
269
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
214
270
|
expect(content).toContain('Astryx v1.0.0');
|
|
215
271
|
});
|
|
216
272
|
|
|
@@ -322,7 +378,7 @@ describe('installAgentDocs', () => {
|
|
|
322
378
|
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(true);
|
|
323
379
|
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
324
380
|
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
325
|
-
expect(content).toContain('<!--
|
|
381
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
326
382
|
});
|
|
327
383
|
|
|
328
384
|
it('injects into CLAUDE.md at root when it exists', () => {
|
|
@@ -334,7 +390,7 @@ describe('installAgentDocs', () => {
|
|
|
334
390
|
expect(written).toEqual(['CLAUDE.md']);
|
|
335
391
|
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
336
392
|
const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
337
|
-
expect(claudeContent).toContain('<!--
|
|
393
|
+
expect(claudeContent).toContain('<!-- ASTRYX:START -->');
|
|
338
394
|
expect(claudeContent).toContain('Project rules.');
|
|
339
395
|
});
|
|
340
396
|
|
|
@@ -349,8 +405,8 @@ describe('installAgentDocs', () => {
|
|
|
349
405
|
expect(written).toContain('CLAUDE.md');
|
|
350
406
|
const agentsContent = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
351
407
|
const claudeContent = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
352
|
-
expect(agentsContent).toContain('<!--
|
|
353
|
-
expect(claudeContent).toContain('<!--
|
|
408
|
+
expect(agentsContent).toContain('<!-- ASTRYX:START -->');
|
|
409
|
+
expect(claudeContent).toContain('<!-- ASTRYX:START -->');
|
|
354
410
|
});
|
|
355
411
|
|
|
356
412
|
it('updates existing .claude/CLAUDE.md', () => {
|
|
@@ -363,7 +419,7 @@ describe('installAgentDocs', () => {
|
|
|
363
419
|
expect(written).toEqual(['.claude/CLAUDE.md']);
|
|
364
420
|
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
|
|
365
421
|
expect(content).toContain('Existing content.');
|
|
366
|
-
expect(content).toContain('<!--
|
|
422
|
+
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
367
423
|
});
|
|
368
424
|
|
|
369
425
|
it('respects --agent claude preset: finds existing CLAUDE.md', () => {
|
|
@@ -410,7 +466,7 @@ describe('installAgentDocs', () => {
|
|
|
410
466
|
|
|
411
467
|
expect(written).toEqual([]);
|
|
412
468
|
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
413
|
-
expect(content).not.toContain('<!--
|
|
469
|
+
expect(content).not.toContain('<!-- ASTRYX:START -->');
|
|
414
470
|
expect(content).toBe('# Claude\n\nProject rules only.\n');
|
|
415
471
|
});
|
|
416
472
|
|
|
@@ -70,7 +70,7 @@ beforeAll(() => {
|
|
|
70
70
|
|
|
71
71
|
let tmpDir;
|
|
72
72
|
beforeEach(() => {
|
|
73
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
73
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-import-path-'));
|
|
74
74
|
});
|
|
75
75
|
afterEach(() => {
|
|
76
76
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -54,7 +54,7 @@ function writeTheme(dir, name) {
|
|
|
54
54
|
|
|
55
55
|
let tmpDir;
|
|
56
56
|
beforeEach(() => {
|
|
57
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
57
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-paths-'));
|
|
58
58
|
});
|
|
59
59
|
afterEach(() => {
|
|
60
60
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -82,7 +82,7 @@ beforeAll(() => {
|
|
|
82
82
|
|
|
83
83
|
let tmpDir;
|
|
84
84
|
beforeEach(() => {
|
|
85
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
85
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-prose-'));
|
|
86
86
|
});
|
|
87
87
|
afterEach(() => {
|
|
88
88
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|