@astryxdesign/cli 0.1.0 → 0.1.1-canary.a514b99

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 (140) 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 +2 -1
  17. package/src/codemods/__tests__/registry.test.mjs +1 -0
  18. package/src/codemods/registry.mjs +1 -0
  19. package/src/codemods/runner.mjs +105 -51
  20. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
  21. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
  22. package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
  23. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
  24. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
  25. package/src/commands/agent-docs.mjs +119 -66
  26. package/src/commands/agent-docs.path-safety.test.mjs +1 -1
  27. package/src/commands/agent-docs.test.mjs +87 -31
  28. package/src/commands/build-theme.import-path.test.mjs +1 -1
  29. package/src/commands/build-theme.path-safety.test.mjs +1 -1
  30. package/src/commands/build-theme.prose.test.mjs +1 -1
  31. package/src/commands/build.mjs +196 -0
  32. package/src/commands/component-package.test.mjs +1 -1
  33. package/src/commands/component.test.mjs +1 -1
  34. package/src/commands/docs.test.mjs +1 -1
  35. package/src/commands/doctor.test.mjs +1 -1
  36. package/src/commands/external-showcase.test.mjs +1 -1
  37. package/src/commands/init.mjs +10 -2
  38. package/src/commands/interactive-guard.test.mjs +1 -1
  39. package/src/commands/json-contract.test.mjs +10 -3
  40. package/src/commands/swizzle-gap-safety.test.mjs +1 -1
  41. package/src/commands/swizzle.path-safety.test.mjs +1 -1
  42. package/src/commands/template.path-safety.test.mjs +1 -1
  43. package/src/commands/template.test.mjs +1 -1
  44. package/src/commands/upgrade.mjs +353 -169
  45. package/src/commands/upgrade.test.mjs +41 -27
  46. package/src/index.mjs +1 -0
  47. package/src/lib/config.mjs +12 -0
  48. package/src/lib/config.test.mjs +42 -0
  49. package/src/lib/error-codes.mjs +3 -0
  50. package/src/types/error-codes.d.ts +1 -0
  51. package/src/utils/interactive.mjs +1 -1
  52. package/src/utils/interactive.test.mjs +2 -0
  53. package/src/utils/package-manager.mjs +1 -1
  54. package/src/utils/package-manager.test.mjs +1 -1
  55. package/src/utils/path-safety.test.mjs +1 -1
  56. package/src/utils/paths.test.mjs +8 -8
  57. package/src/utils/update-check.mjs +4 -26
  58. package/src/utils/update-check.test.mjs +2 -64
  59. package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
  60. package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
  61. package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
  62. package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
  63. package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
  64. package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
  65. package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
  66. package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
  67. package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
  68. package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
  69. package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
  70. package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
  71. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
  72. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
  73. package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
  74. package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
  75. package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
  76. package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
  77. package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
  78. package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
  79. package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
  80. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
  81. package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
  82. package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
  83. package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
  84. package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
  85. package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
  86. package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
  87. package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
  88. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
  89. package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
  90. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
  91. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +2 -12
  92. package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
  93. package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
  94. package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
  95. package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
  96. package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
  97. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
  98. package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
  99. package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
  100. package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
  101. package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
  102. package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
  103. package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
  104. package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
  105. package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
  106. package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
  107. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
  108. package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
  109. package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
  110. package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
  111. package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
  112. package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
  113. package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
  114. package/templates/pages/ai-chat/page.tsx +71 -64
  115. package/templates/pages/ai-chat-landing/page.tsx +8 -12
  116. package/templates/pages/centered-hero/page.tsx +13 -15
  117. package/templates/pages/classic-gallery/page.tsx +27 -34
  118. package/templates/pages/detail-page/page.tsx +18 -18
  119. package/templates/pages/documentation/page.tsx +42 -58
  120. package/templates/pages/documentation-design/page.tsx +82 -60
  121. package/templates/pages/documentation-technical/page.tsx +101 -60
  122. package/templates/pages/editor/page.tsx +42 -54
  123. package/templates/pages/file-explorer/page.tsx +13 -16
  124. package/templates/pages/form-two-column/page.tsx +13 -17
  125. package/templates/pages/gallery-hero/page.tsx +13 -15
  126. package/templates/pages/ide/page.tsx +188 -264
  127. package/templates/pages/library/page.tsx +16 -23
  128. package/templates/pages/login/page.tsx +14 -18
  129. package/templates/pages/login-card/page.tsx +14 -18
  130. package/templates/pages/login-split/page.tsx +50 -48
  131. package/templates/pages/login-sso/page.tsx +9 -13
  132. package/templates/pages/mixed-gallery/page.tsx +51 -45
  133. package/templates/pages/payment-form/page.tsx +56 -70
  134. package/templates/pages/product-detail/page.tsx +27 -33
  135. package/templates/pages/product-gallery/page.tsx +7 -13
  136. package/templates/pages/settings-dialog/page.tsx +35 -43
  137. package/templates/pages/settings-sidebar/page.tsx +39 -47
  138. package/templates/pages/side-gallery/page.tsx +6 -9
  139. package/templates/pages/table-grouped/page.tsx +11 -15
  140. package/templates/pages/theme-showcase/page.tsx +33 -37
@@ -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});
@@ -0,0 +1,196 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file build command — the page-building assistant.
5
+ *
6
+ * Two modes:
7
+ * astryx build → the PLAYBOOK: how to build a page with Astryx
8
+ * astryx build "<what>" → a COMPOSITION KIT for what you're building:
9
+ * the closest page template (scaffold or layout
10
+ * reference), the blocks that cover parts, and
11
+ * the components to fill gaps, plus a one-line
12
+ * "Compose:" suggestion.
13
+ *
14
+ * `build` is the opinionated "assemble a page" verb. For a neutral lookup across
15
+ * the whole CLI, use `astryx search <query>` instead.
16
+ */
17
+
18
+ import {getRunPrefix} from '../utils/package-manager.mjs';
19
+ import {jsonOut, humanLog} from '../lib/json.mjs';
20
+ import {cliError} from '../lib/cli-error.mjs';
21
+ import {search as searchApi} from '../api/search.mjs';
22
+
23
+ /** A page scoring at/above this is confident enough to call a direct match. */
24
+ const PAGE_DIRECT = 95;
25
+ /** Below this a page is too weak to even offer as a layout reference. */
26
+ const PAGE_FLOOR = 50;
27
+ /** Below this a block/domain-component match is incidental noise, not surfaced. */
28
+ const DOMAIN_FLOOR = 55;
29
+
30
+ /**
31
+ * Always-surfaced primitives. Every page needs a shell + layout/typography/
32
+ * action atoms, but these never keyword-match an idea ("dashboard" != "Stack"),
33
+ * so search alone never returns them. We list them unconditionally so an agent
34
+ * composing from scratch has the whole kit (esp. off-template).
35
+ */
36
+ const FRAME = ['AppShell', 'TopNav', 'SideNav', 'Layout'];
37
+ const FOUNDATION = [
38
+ 'VStack', 'HStack', 'Grid', 'StackItem', 'Card', 'Section',
39
+ 'Text', 'Heading', 'Button', 'Icon', 'Badge', 'Divider',
40
+ ];
41
+ const ALWAYS = new Set([...FRAME, ...FOUNDATION]);
42
+
43
+ /** Print the build playbook (shown when `build` is run with no query). */
44
+ function printPlaybook(run) {
45
+ const lines = [
46
+ '',
47
+ 'How to build a page with Astryx',
48
+ '',
49
+ "1. Find a starting point for what you're building:",
50
+ ` ${run} astryx build "<what you're building>"`,
51
+ ' → returns the closest [page] template, the [block]s that cover parts,',
52
+ ' and the [component]s to fill the gaps, with a "Compose:" suggestion.',
53
+ '',
54
+ '2. If a [page] template matches → scaffold it and adapt:',
55
+ ` ${run} astryx template <name> [path]`,
56
+ '',
57
+ '3. If nothing matches exactly → compose:',
58
+ ` ${run} astryx template <name> --skeleton # study a close page's layout`,
59
+ ` ${run} astryx template <BlockName> # drop in each block from the kit`,
60
+ ` ${run} astryx component <Name> # fill remaining gaps (read props)`,
61
+ '',
62
+ '4. Rules (keep it on-system):',
63
+ ' - No <div>/raw HTML for layout — use VStack/HStack/Grid/Stack/Card etc.',
64
+ ' - No style={{}} — use component props; design tokens via `astryx docs tokens`.',
65
+ ' - Wrap the app in <Theme theme={...}> and import core reset.css + astryx.css.',
66
+ '',
67
+ `Tip: \`${run} astryx build "<idea>"\` is the fastest way in. For a neutral`,
68
+ `lookup of any component/doc/template, use \`${run} astryx search <query>\`.`,
69
+ '',
70
+ ];
71
+ for (const l of lines) humanLog(l);
72
+ }
73
+
74
+ export function registerBuild(program) {
75
+ program
76
+ .command('build [query]')
77
+ .description('Build a page: composition kit for an idea, or the workflow playbook (no args)')
78
+ .option('--type <domain>', 'Filter the kit to one domain (component|hook|template)')
79
+ .option('--limit <n>', 'Max candidates to draw from (default 60)')
80
+ .option('--detail', 'Verbose output (include import paths and match reason)')
81
+ .action(async (query, options) => {
82
+ const run = getRunPrefix();
83
+ const json = program.opts().json || false;
84
+
85
+ // No query → print the playbook (the "how to build" skill).
86
+ if (!query || !String(query).trim()) {
87
+ if (json) return jsonOut('build.help', {playbook: true});
88
+ printPlaybook(run);
89
+ return;
90
+ }
91
+
92
+ // Default to a deep pool so each role section has candidates after grouping.
93
+ let limit = 60;
94
+ if (options.limit != null) {
95
+ const parsed = Number.parseInt(options.limit, 10);
96
+ if (!Number.isFinite(parsed) || parsed <= 0) {
97
+ cliError(`Invalid --limit value "${options.limit}". Must be a positive integer.`);
98
+ return;
99
+ }
100
+ limit = parsed;
101
+ }
102
+
103
+ let result;
104
+ try {
105
+ result = await searchApi(query, {cwd: process.cwd(), type: options.type, limit});
106
+ } catch (e) {
107
+ cliError(e.message, {suggestions: e.suggestions});
108
+ return;
109
+ }
110
+
111
+ if (json) return jsonOut(result.type, result.data);
112
+
113
+ const {query: q, results} = result.data;
114
+
115
+ if (results.length === 0) {
116
+ humanLog('');
117
+ humanLog(`No matches for "${q}".`);
118
+ humanLog(`Try a broader term, or browse: ${run} astryx component --list`);
119
+ humanLog('');
120
+ return;
121
+ }
122
+
123
+ // ── Group results by role (the build kit) ──────────────────────
124
+ const pages = results
125
+ .filter(r => r.domain === 'template' && r.kind !== 'block' && r.score >= PAGE_FLOOR)
126
+ .slice(0, 3);
127
+ const blocks = results
128
+ .filter(r => r.domain === 'template' && r.kind === 'block' && r.score >= DOMAIN_FLOOR)
129
+ .slice(0, 5);
130
+ // Idea-specific atoms = matched components/hooks MINUS the always-on kit.
131
+ const domain = results
132
+ .filter(r => (r.domain === 'component' || r.domain === 'hook') && r.score >= DOMAIN_FLOOR && !ALWAYS.has(r.name))
133
+ .slice(0, 6);
134
+ const directMatch = pages.length > 0 && pages[0].score >= PAGE_DIRECT;
135
+
136
+ const printItem = (r, label) => {
137
+ const display = r.domain === 'template' && r.displayName ? r.displayName : r.name;
138
+ humanLog('');
139
+ humanLog(` [${label}] ${display}`);
140
+ if (r.description) humanLog(` ${r.description}`);
141
+ humanLog(` → ${run} ${r.command}`);
142
+ if (options.detail) {
143
+ if (r.import) humanLog(` import: ${r.import}`);
144
+ humanLog(` match: ${r.reason} (score ${r.score})`);
145
+ }
146
+ };
147
+
148
+ humanLog('');
149
+ humanLog(`Building "${q}":`);
150
+
151
+ // START — the single recommended path.
152
+ humanLog('');
153
+ if (directMatch) {
154
+ humanLog(`START → Scaffold the \`${pages[0].name}\` page template, then adapt: ${run} astryx template ${pages[0].name} ./src/App.tsx`);
155
+ } else if (pages.length) {
156
+ humanLog(`START → No exact page template. Use \`${pages[0].name}\` as a layout reference (${run} astryx template ${pages[0].name} --skeleton) and compose the pieces below.`);
157
+ } else {
158
+ humanLog(`START → No page template fits. Frame with AppShell and compose the blocks + components below.`);
159
+ }
160
+
161
+ // PAGE
162
+ if (pages.length) {
163
+ humanLog('');
164
+ humanLog(directMatch ? 'PAGE TEMPLATE — direct match:' : 'CLOSEST PAGE TEMPLATES — layout reference:');
165
+ pages.forEach(p => printItem(p, directMatch ? 'page' : 'closest'));
166
+ }
167
+
168
+ // FRAME — always (the page shell).
169
+ humanLog('');
170
+ humanLog(`FRAME — page shell (always): ${FRAME.join(', ')}`);
171
+ humanLog(` full-page → AppShell; or Layout + SideNav/TopNav. ${run} astryx component AppShell`);
172
+
173
+ // BLOCKS — idea-specific composed patterns.
174
+ if (blocks.length) {
175
+ humanLog('');
176
+ humanLog('BLOCKS — drop-in patterns that cover parts of it:');
177
+ blocks.forEach(b => printItem(b, 'block'));
178
+ }
179
+
180
+ // DOMAIN COMPONENTS — idea-specific atoms.
181
+ if (domain.length) {
182
+ humanLog('');
183
+ humanLog('DOMAIN COMPONENTS — specific to this idea:');
184
+ domain.forEach(c => printItem(c, c.domain === 'hook' ? 'hook' : 'component'));
185
+ }
186
+
187
+ // FOUNDATION — always (layout/typography/actions).
188
+ humanLog('');
189
+ humanLog(`FOUNDATION — always available (layout/text/actions): ${FOUNDATION.join(' ')}`);
190
+
191
+ // SETUP — so it renders / stays on-system.
192
+ humanLog('');
193
+ humanLog('SETUP — import "@astryxdesign/core/reset.css" + "astryx.css". No <div>/style for layout — use Stack/Grid + tokens.');
194
+ humanLog('');
195
+ });
196
+ }
@@ -65,7 +65,7 @@ export const docs = {
65
65
  }
66
66
 
67
67
  beforeEach(() => {
68
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-pkg-test-'));
68
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-pkg-test-'));
69
69
  createFixture();
70
70
  });
71
71
 
@@ -18,7 +18,7 @@ import {
18
18
  let tmpDir;
19
19
 
20
20
  beforeEach(() => {
21
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-component-test-'));
21
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-component-test-'));
22
22
  });
23
23
 
24
24
  afterEach(() => {
@@ -10,7 +10,7 @@ import {registerDocs} from './docs.mjs';
10
10
  let tmpDir;
11
11
 
12
12
  beforeEach(() => {
13
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-docs-test-'));
13
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-docs-test-'));
14
14
  vi.spyOn(console, 'log').mockImplementation(() => {});
15
15
  vi.spyOn(console, 'error').mockImplementation(() => {});
16
16
  });
@@ -26,7 +26,7 @@ let logCalls;
26
26
  let exitCode;
27
27
 
28
28
  beforeEach(() => {
29
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-doctor-test-'));
29
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-doctor-test-'));
30
30
  logCalls = [];
31
31
  exitCode = undefined;
32
32
  vi.spyOn(console, 'log').mockImplementation((...args) => {
@@ -78,7 +78,7 @@ export const doc = {
78
78
  }
79
79
 
80
80
  beforeEach(() => {
81
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-showcase-test-'));
81
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-showcase-test-'));
82
82
  createFixture();
83
83
  });
84
84
 
@@ -100,7 +100,15 @@ async function runTemplate(targetDir, {interactive = true, templateName} = {}) {
100
100
 
101
101
  if (!interactive) {
102
102
  if (!templateName) {
103
- humanLog(`✓ Available templates: ${templates.join(', ')}. Use ${run} astryx template <name> [path].`);
103
+ // Point agents at the build workflow rather than dumping page-template
104
+ // names — `build` surfaces pages AND blocks AND components for an idea,
105
+ // and `build` with no args is the full how-to-build playbook.
106
+ humanLog('✓ To build UI, use these commands:');
107
+ humanLog('');
108
+ humanLog(` ${run} astryx build "<what you're building>" build a page — kit: closest template + blocks + components`);
109
+ humanLog(` ${run} astryx build the how-to-build workflow (read this first)`);
110
+ humanLog(` ${run} astryx search <query> find anything — components, docs, templates, blocks`);
111
+ humanLog('');
104
112
  return;
105
113
  }
106
114
 
@@ -248,7 +256,7 @@ export function registerInit(program) {
248
256
  humanLog(' 2. Optionally add a theme:');
249
257
  humanLog(" import { neutralTheme } from '@astryxdesign/theme-neutral'");
250
258
  humanLog(' <Theme theme={neutralTheme}>...</Theme>');
251
- humanLog(` 3. ${run} xds --help for all commands`);
259
+ humanLog(` 3. ${run} astryx --help for all commands`);
252
260
  humanLog('');
253
261
  });
254
262
  }
@@ -35,7 +35,7 @@ function runCli(args) {
35
35
  }
36
36
 
37
37
  beforeEach(() => {
38
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-interactive-guard-'));
38
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-interactive-guard-'));
39
39
  });
40
40
 
41
41
  afterEach(() => {
@@ -53,7 +53,7 @@ function parseJson(stdout) {
53
53
  let tmpDir;
54
54
 
55
55
  beforeEach(() => {
56
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-json-contract-'));
56
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-json-contract-'));
57
57
  });
58
58
 
59
59
  afterEach(() => {
@@ -177,9 +177,16 @@ describe('--json contract: supported commands emit valid envelopes', () => {
177
177
  });
178
178
 
179
179
  it('astryx upgrade --json (already up to date) emits upgrade.status', () => {
180
- // Force a no-op range: from > to.
180
+ // Force a no-op range: from > installed target.
181
+ const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
182
+ fs.mkdirSync(coreDir, {recursive: true});
183
+ fs.writeFileSync(
184
+ path.join(coreDir, 'package.json'),
185
+ JSON.stringify({name: '@astryxdesign/core', version: '0.0.1'}, null, 2),
186
+ );
187
+
181
188
  const {status, stdout} = runCli(
182
- ['upgrade', '--json', '--from', '99.0.0', '--to', '0.0.1'],
189
+ ['upgrade', '--json', '--from', '99.0.0'],
183
190
  {cwd: tmpDir},
184
191
  );
185
192
  expect(status).toBe(0);
@@ -36,7 +36,7 @@ let markerFile;
36
36
  let baseEnv;
37
37
 
38
38
  beforeAll(() => {
39
- shimDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-gh-shim-'));
39
+ shimDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-gh-shim-'));
40
40
  markerFile = path.join(shimDir, 'gh-issue-create-was-called.marker');
41
41
 
42
42
  // Sabotaged `gh` shim. Records ONLY `gh issue create` invocations to the
@@ -58,7 +58,7 @@ function runCli(args, cwd) {
58
58
 
59
59
  let tmpDir;
60
60
  beforeEach(() => {
61
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-swizzle-paths-'));
61
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-swizzle-paths-'));
62
62
  });
63
63
  afterEach(() => {
64
64
  fs.rmSync(tmpDir, {recursive: true, force: true});
@@ -17,7 +17,7 @@ let tmpDir;
17
17
  let templateApi;
18
18
 
19
19
  beforeEach(async () => {
20
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-template-paths-'));
20
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-template-paths-'));
21
21
  templateApi = (await import('../api/template.mjs')).template;
22
22
  });
23
23
  afterEach(() => {
@@ -12,7 +12,7 @@ import * as os from 'node:os';
12
12
  let tmpDir;
13
13
 
14
14
  beforeEach(() => {
15
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xds-template-test-'));
15
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-template-test-'));
16
16
  });
17
17
 
18
18
  afterEach(() => {