@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +117 -75
  3. package/bin/astryx.mjs +22 -7
  4. package/docs/getting-started.doc.mjs +11 -11
  5. package/docs/icons.doc.mjs +1 -1
  6. package/docs/migration.doc.mjs +2 -2
  7. package/docs/shape.doc.mjs +1 -1
  8. package/docs/styling.doc.mjs +3 -4
  9. package/docs/theme.doc.dense.mjs +2 -2
  10. package/docs/theme.doc.mjs +14 -0
  11. package/docs/theme.doc.zh.mjs +2 -2
  12. package/docs/working-with-ai.doc.mjs +4 -4
  13. package/package.json +8 -8
  14. package/src/api/doctor.mjs +3 -3
  15. package/src/api/search.mjs +207 -13
  16. package/src/api/template.mjs +62 -11
  17. package/src/api/template.test.mjs +2 -0
  18. package/src/codemods/__tests__/registry.test.mjs +1 -0
  19. package/src/codemods/registry.mjs +1 -0
  20. package/src/codemods/runner.mjs +105 -51
  21. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
  22. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
  23. package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
  24. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
  25. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
  26. package/src/commands/agent-docs.mjs +119 -66
  27. package/src/commands/agent-docs.path-safety.test.mjs +1 -1
  28. package/src/commands/agent-docs.test.mjs +87 -31
  29. package/src/commands/build-theme.import-path.test.mjs +1 -1
  30. package/src/commands/build-theme.path-safety.test.mjs +1 -1
  31. package/src/commands/build-theme.prose.test.mjs +1 -1
  32. package/src/commands/build.mjs +196 -0
  33. package/src/commands/component-package.test.mjs +1 -1
  34. package/src/commands/component.test.mjs +1 -1
  35. package/src/commands/docs.test.mjs +1 -1
  36. package/src/commands/doctor.test.mjs +1 -1
  37. package/src/commands/external-showcase.test.mjs +1 -1
  38. package/src/commands/init.mjs +43 -9
  39. package/src/commands/init.next-steps.test.mjs +46 -0
  40. package/src/commands/interactive-guard.test.mjs +1 -1
  41. package/src/commands/json-contract.test.mjs +10 -3
  42. package/src/commands/swizzle-gap-safety.test.mjs +1 -1
  43. package/src/commands/swizzle.path-safety.test.mjs +1 -1
  44. package/src/commands/template.path-safety.test.mjs +1 -1
  45. package/src/commands/template.test.mjs +1 -1
  46. package/src/commands/upgrade.mjs +353 -169
  47. package/src/commands/upgrade.test.mjs +41 -27
  48. package/src/index.mjs +1 -0
  49. package/src/lib/config.mjs +12 -0
  50. package/src/lib/config.test.mjs +42 -0
  51. package/src/lib/error-codes.mjs +3 -0
  52. package/src/types/error-codes.d.ts +1 -0
  53. package/src/utils/interactive.mjs +1 -1
  54. package/src/utils/interactive.test.mjs +2 -0
  55. package/src/utils/package-manager.mjs +1 -1
  56. package/src/utils/package-manager.test.mjs +1 -1
  57. package/src/utils/path-safety.test.mjs +1 -1
  58. package/src/utils/paths.test.mjs +8 -8
  59. package/src/utils/update-check.mjs +4 -26
  60. package/src/utils/update-check.test.mjs +2 -64
  61. package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
  62. package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
  63. package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
  64. package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
  65. package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
  66. package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
  67. package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
  68. package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
  69. package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
  70. package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
  71. package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
  72. package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
  73. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
  74. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
  75. package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
  76. package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
  77. package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
  78. package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
  79. package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
  80. package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
  81. package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
  82. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
  83. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
  84. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
  85. package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
  86. package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
  87. package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
  88. package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
  89. package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
  90. package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
  91. package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
  92. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
  93. package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
  94. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
  95. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +2 -12
  96. package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
  97. package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
  98. package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
  99. package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
  100. package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
  101. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
  102. package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
  103. package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
  104. package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
  105. package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
  106. package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
  107. package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
  108. package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
  109. package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
  110. package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
  111. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
  112. package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
  113. package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
  114. package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
  115. package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
  116. package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
  117. package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
  118. package/templates/pages/ai-chat/page.tsx +71 -64
  119. package/templates/pages/ai-chat-landing/page.tsx +8 -12
  120. package/templates/pages/centered-hero/page.tsx +13 -15
  121. package/templates/pages/classic-gallery/page.tsx +27 -34
  122. package/templates/pages/detail-page/page.tsx +18 -18
  123. package/templates/pages/documentation/page.tsx +42 -58
  124. package/templates/pages/documentation-design/page.tsx +82 -60
  125. package/templates/pages/documentation-technical/page.tsx +101 -60
  126. package/templates/pages/editor/page.tsx +42 -54
  127. package/templates/pages/file-explorer/page.tsx +13 -16
  128. package/templates/pages/form-two-column/page.tsx +13 -17
  129. package/templates/pages/gallery-hero/page.tsx +13 -15
  130. package/templates/pages/ide/page.tsx +188 -264
  131. package/templates/pages/library/page.tsx +16 -23
  132. package/templates/pages/login/page.tsx +14 -18
  133. package/templates/pages/login-card/page.tsx +14 -18
  134. package/templates/pages/login-split/page.tsx +50 -48
  135. package/templates/pages/login-sso/page.tsx +9 -13
  136. package/templates/pages/mixed-gallery/page.tsx +51 -45
  137. package/templates/pages/payment-form/page.tsx +56 -70
  138. package/templates/pages/product-detail/page.tsx +27 -33
  139. package/templates/pages/product-gallery/page.tsx +7 -13
  140. package/templates/pages/settings-dialog/page.tsx +35 -43
  141. package/templates/pages/settings-sidebar/page.tsx +39 -47
  142. package/templates/pages/side-gallery/page.tsx +6 -9
  143. package/templates/pages/table-grouped/page.tsx +11 -15
  144. 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 XDS_MARKER_START = '<!-- XDS:START -->';
35
- const XDS_MARKER_END = '<!-- XDS:END -->';
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 = [XDS_MARKER_START];
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} ${componentCount} components`);
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
- // Behavioral workflowtemplates first, then component lookup
122
- lines.push('Before writing any UI code:');
123
- lines.push(`1. \`${run} template --list\`find a related page pattern`);
124
- lines.push(`2. \`${run} template <name> --skeleton\` — study layout structure (gap, padding, nesting)`);
125
- lines.push(`3. \`${run} component <Name>\` — read props + examples for EVERY component you use`);
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 setupcomponents 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
- // Rulesinline, compact, prevents the top error categories
132
- lines.push('No <div> anywhere not for layout, not for wrappers, not for spacing. Use components.');
133
- lines.push('Full-page shells AppShell (not Layout). Sidebar nav SideNav (not List).');
134
- lines.push('No style={{}}use the xstyle prop on components for custom styling.');
135
- lines.push('If a component prop does what you need, use it never replicate with CSS/stylex.');
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("WORKFLOWdiscover, 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
- // CLI quick reference
141
- lines.push(`${run} component --list ${componentCount} components by category`);
142
- lines.push(`${run} component <Name> props, types, examples`);
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
- // Doc topics from live discovery
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
- for (const file of fs.readdirSync(docsDir).sort()) {
148
- const match = file.match(/^(\w+)\.doc\.mjs$/);
149
- if (!match) continue;
150
- const topic = match[1];
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 (templates.length > 0) {
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(`${run} swizzle <Name> eject source (--gap to report why)`);
178
- lines.push(`${run} upgrade --apply codemods after version bump`);
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
- const startIdx = content.indexOf(XDS_MARKER_START);
222
- const endIdx = content.indexOf(XDS_MARKER_END);
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 + XDS_MARKER_END.length);
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, {coreDir: findCoreDir(targetDir)});
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, {coreDir: findCoreDir(targetDir)});
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
- const startIdx = content.indexOf(XDS_MARKER_START);
281
- const endIdx = content.indexOf(XDS_MARKER_END);
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 + XDS_MARKER_END.length).trimStart();
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 compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, runPrefix});
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(), 'xds-agent-docs-paths-'));
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(), 'xds-agent-docs-test-'));
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('<!-- XDS:START -->');
36
- expect(result).toContain('<!-- XDS:END -->');
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 --astryx-color/);
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('astryx upgrade');
48
- expect(result).toContain('astryx upgrade --apply');
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('uses custom runPrefix when provided', () => {
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 component <Name>');
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 component <Name>');
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, '<!-- XDS:START -->\nnew\n<!-- XDS:END -->');
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('<!-- XDS:START -->');
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, '<!-- XDS:START -->\nnew\n<!-- XDS:END -->');
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, '<!-- XDS:START -->\ncontent\n<!-- XDS:END -->');
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, '<!-- XDS:START -->\nnew\n<!-- XDS:END -->', {onlyReplace: true});
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('<!-- XDS:START -->');
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, '<!-- XDS:START -->\nnew\n<!-- XDS:END -->', {onlyReplace: true});
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, '<!-- XDS:START -->\ncontent\n<!-- XDS:END -->', {
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('<!-- XDS:START -->');
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('<!-- XDS:START -->');
216
+ expect(content).toContain('<!-- ASTRYX:START -->');
161
217
  expect(content).toContain('Astryx v1.0.0');
162
- expect(content).toContain('<!-- XDS:END -->');
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('<!-- XDS:START -->');
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('<!-- XDS:START -->');
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('<!-- XDS:START -->');
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('<!-- XDS:START -->');
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('<!-- XDS:START -->');
353
- expect(claudeContent).toContain('<!-- XDS:START -->');
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('<!-- XDS:START -->');
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('<!-- XDS:START -->');
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(), 'xds-build-theme-import-path-'));
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(), 'xds-build-theme-paths-'));
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(), 'xds-build-theme-prose-'));
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});