@astryxdesign/cli 0.1.0 → 0.1.1-canary.080d887

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 (176) 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 +20 -9
  14. package/src/api/discover.mjs +78 -26
  15. package/src/api/doctor.mjs +3 -3
  16. package/src/api/layout.mjs +301 -0
  17. package/src/api/layout.test.mjs +238 -0
  18. package/src/api/search.mjs +207 -13
  19. package/src/api/template.mjs +193 -51
  20. package/src/api/template.test.mjs +2 -0
  21. package/src/codemods/__tests__/registry.test.mjs +1 -0
  22. package/src/codemods/registry.mjs +1 -0
  23. package/src/codemods/runner.mjs +105 -51
  24. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
  25. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
  26. package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
  27. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
  28. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
  29. package/src/commands/agent-docs.mjs +119 -66
  30. package/src/commands/agent-docs.path-safety.test.mjs +1 -1
  31. package/src/commands/agent-docs.test.mjs +87 -31
  32. package/src/commands/build-theme.import-path.test.mjs +1 -1
  33. package/src/commands/build-theme.path-safety.test.mjs +1 -1
  34. package/src/commands/build-theme.prose.test.mjs +1 -1
  35. package/src/commands/build.mjs +196 -0
  36. package/src/commands/component-package.test.mjs +1 -1
  37. package/src/commands/component.test.mjs +1 -1
  38. package/src/commands/docs.test.mjs +1 -1
  39. package/src/commands/doctor.test.mjs +1 -1
  40. package/src/commands/external-showcase.test.mjs +1 -1
  41. package/src/commands/gap-report.mjs +17 -9
  42. package/src/commands/gap-report.test.mjs +21 -16
  43. package/src/commands/init.mjs +43 -9
  44. package/src/commands/init.next-steps.test.mjs +46 -0
  45. package/src/commands/interactive-guard.test.mjs +1 -1
  46. package/src/commands/json-contract.test.mjs +10 -3
  47. package/src/commands/layout.mjs +139 -0
  48. package/src/commands/swizzle-gap-safety.test.mjs +1 -1
  49. package/src/commands/swizzle.mjs +51 -23
  50. package/src/commands/swizzle.path-safety.test.mjs +1 -1
  51. package/src/commands/template.path-safety.test.mjs +1 -1
  52. package/src/commands/template.test.mjs +1 -1
  53. package/src/commands/upgrade.mjs +292 -177
  54. package/src/commands/upgrade.test.mjs +41 -27
  55. package/src/config.mjs +31 -0
  56. package/src/config.test.mjs +24 -0
  57. package/src/index.mjs +5 -0
  58. package/src/lib/config-schema.mjs +119 -0
  59. package/src/lib/config.mjs +45 -6
  60. package/src/lib/config.test.mjs +91 -0
  61. package/src/lib/error-codes.mjs +11 -0
  62. package/src/lib/integrations.mjs +155 -0
  63. package/src/lib/integrations.test.mjs +154 -0
  64. package/src/lib/levenshtein.mjs +29 -0
  65. package/src/lib/manifest.mjs +6 -0
  66. package/src/lib/package-scanner.mjs +31 -7
  67. package/src/lib/string-utils.mjs +5 -14
  68. package/src/lib/xle/browser.d.ts +91 -0
  69. package/src/lib/xle/browser.mjs +120 -0
  70. package/src/lib/xle/expand.mjs +622 -0
  71. package/src/lib/xle/parse.mjs +581 -0
  72. package/src/lib/xle/print.mjs +174 -0
  73. package/src/lib/xle/registry-core.mjs +170 -0
  74. package/src/lib/xle/registry.mjs +237 -0
  75. package/src/lib/xle/splice.mjs +137 -0
  76. package/src/lib/xle/validate.mjs +356 -0
  77. package/src/lib/xle/xle.test.mjs +333 -0
  78. package/src/types/config.d.ts +99 -0
  79. package/src/types/error-codes.d.ts +1 -0
  80. package/src/utils/github.mjs +12 -27
  81. package/src/utils/interactive.mjs +1 -1
  82. package/src/utils/interactive.test.mjs +2 -0
  83. package/src/utils/package-manager.mjs +1 -1
  84. package/src/utils/package-manager.test.mjs +1 -1
  85. package/src/utils/path-safety.test.mjs +1 -1
  86. package/src/utils/paths.test.mjs +8 -8
  87. package/src/utils/update-check.mjs +4 -26
  88. package/src/utils/update-check.test.mjs +2 -64
  89. package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
  90. package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
  91. package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
  92. package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
  93. package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
  94. package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
  95. package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
  96. package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
  97. package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
  98. package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
  99. package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
  100. package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
  101. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
  102. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
  103. package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
  104. package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
  105. package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
  106. package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
  107. package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
  108. package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
  109. package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
  110. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
  111. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
  112. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
  113. package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
  114. package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
  115. package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
  116. package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
  117. package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
  118. package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
  119. package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
  120. package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
  121. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
  122. package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
  123. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
  124. package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
  125. package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
  126. package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
  127. package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
  128. package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
  129. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
  130. package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
  131. package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
  132. package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
  133. package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
  134. package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
  135. package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
  136. package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
  137. package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
  138. package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
  139. package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
  140. package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
  141. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
  142. package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
  143. package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
  144. package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
  145. package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
  146. package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
  147. package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
  148. package/templates/pages/ai-chat/page.tsx +71 -64
  149. package/templates/pages/ai-chat-landing/page.tsx +8 -12
  150. package/templates/pages/centered-hero/page.tsx +13 -15
  151. package/templates/pages/classic-gallery/page.tsx +27 -34
  152. package/templates/pages/detail-page/page.tsx +18 -18
  153. package/templates/pages/documentation/page.tsx +42 -58
  154. package/templates/pages/documentation-design/page.tsx +82 -60
  155. package/templates/pages/documentation-technical/page.tsx +101 -60
  156. package/templates/pages/editor/page.tsx +42 -54
  157. package/templates/pages/file-explorer/page.tsx +13 -16
  158. package/templates/pages/form-two-column/page.tsx +13 -17
  159. package/templates/pages/gallery-hero/page.tsx +13 -15
  160. package/templates/pages/ide/page.tsx +188 -264
  161. package/templates/pages/library/page.tsx +16 -23
  162. package/templates/pages/login/page.tsx +14 -18
  163. package/templates/pages/login-card/page.tsx +14 -18
  164. package/templates/pages/login-split/page.tsx +50 -48
  165. package/templates/pages/login-sso/page.tsx +9 -13
  166. package/templates/pages/mixed-gallery/page.tsx +51 -45
  167. package/templates/pages/payment-form/page.tsx +56 -70
  168. package/templates/pages/product-detail/page.tsx +27 -33
  169. package/templates/pages/product-gallery/page.tsx +7 -13
  170. package/templates/pages/settings-dialog/page.tsx +35 -43
  171. package/templates/pages/settings-sidebar/page.tsx +39 -47
  172. package/templates/pages/side-gallery/page.tsx +6 -9
  173. package/templates/pages/table-grouped/page.tsx +11 -15
  174. package/templates/pages/theme-showcase/page.tsx +33 -37
  175. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
  176. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -67
@@ -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
 
@@ -132,7 +132,7 @@ export function registerGapReport(program) {
132
132
  .action(async () => {
133
133
  p.intro('Gap report setup');
134
134
 
135
- const config = loadGapReportConfig();
135
+ const config = await loadGapReportConfig();
136
136
  const currentMode = !config.enabled
137
137
  ? 'disabled'
138
138
  : config.command
@@ -252,9 +252,14 @@ export function registerGapReport(program) {
252
252
  return;
253
253
  }
254
254
 
255
- const config = loadGapReportConfig();
255
+ const config = await loadGapReportConfig();
256
256
  if (!config.enabled) {
257
- if (json) return jsonError('Gap reporting is disabled', undefined, ERROR_CODES.ERR_GAP_REPORT_FAILED);
257
+ if (json)
258
+ return jsonError(
259
+ 'Gap reporting is disabled',
260
+ undefined,
261
+ ERROR_CODES.ERR_GAP_REPORT_FAILED,
262
+ );
258
263
  humanLog(
259
264
  `Gap reporting is disabled (ASTRYX_GAP_REPORT=off or astryx.config.mjs).\n` +
260
265
  `Run \`${getRunPrefix()} astryx gap-report setup\` to configure.`,
@@ -295,7 +300,7 @@ export function registerGapReport(program) {
295
300
  return;
296
301
  }
297
302
 
298
- const preview = buildGapReportPreview({
303
+ const preview = await buildGapReportPreview({
299
304
  component: options.component,
300
305
  category: options.category,
301
306
  intention: options.reason,
@@ -326,7 +331,7 @@ export function registerGapReport(program) {
326
331
  }
327
332
 
328
333
  try {
329
- const url = createGapReport({
334
+ const url = await createGapReport({
330
335
  component: options.component,
331
336
  category: options.category,
332
337
  intention: options.reason,
@@ -343,7 +348,9 @@ export function registerGapReport(program) {
343
348
  humanLog('\nGap reporting is disabled via configuration.\n');
344
349
  }
345
350
  } catch (err) {
346
- cliError(`Filing gap report failed: ${err.message}`, {code: ERROR_CODES.ERR_GAP_REPORT_FAILED});
351
+ cliError(`Filing gap report failed: ${err.message}`, {
352
+ code: ERROR_CODES.ERR_GAP_REPORT_FAILED,
353
+ });
347
354
  return;
348
355
  }
349
356
  return;
@@ -386,7 +393,8 @@ export function registerGapReport(program) {
386
393
  placeholder:
387
394
  'e.g. "Need a compact variant for use in dense data tables"',
388
395
  validate: val => {
389
- if (!val.trim()) return 'Please describe what you were trying to do';
396
+ if (!val.trim())
397
+ return 'Please describe what you were trying to do';
390
398
  },
391
399
  }),
392
400
  );
@@ -406,7 +414,7 @@ export function registerGapReport(program) {
406
414
  source: 'interactive',
407
415
  };
408
416
 
409
- const preview = buildGapReportPreview(previewArgs);
417
+ const preview = await buildGapReportPreview(previewArgs);
410
418
 
411
419
  // Always show the user exactly what would be filed before sending.
412
420
  p.note(
@@ -440,7 +448,7 @@ export function registerGapReport(program) {
440
448
  s.start('Filing gap report');
441
449
 
442
450
  try {
443
- const url = createGapReport(previewArgs);
451
+ const url = await createGapReport(previewArgs);
444
452
  s.stop(url ? 'Gap report filed' : 'Gap reporting is disabled');
445
453
  if (url) {
446
454
  p.log.success(url);
@@ -40,9 +40,9 @@ describe('shouldActuallyFile', () => {
40
40
  });
41
41
 
42
42
  it('--dry-run wins over --commit (safety)', () => {
43
- expect(
44
- shouldActuallyFile({isTTY: true, commit: true, dryRun: true}),
45
- ).toBe(false);
43
+ expect(shouldActuallyFile({isTTY: true, commit: true, dryRun: true})).toBe(
44
+ false,
45
+ );
46
46
  });
47
47
  });
48
48
 
@@ -54,7 +54,9 @@ describe('formatPreview', () => {
54
54
  title: '[gap] Button: missing compact variant',
55
55
  body: '## User Intention\n\nNeed a compact variant',
56
56
  });
57
- expect(out).toContain('Would file GitHub issue on facebookexperimental/xds');
57
+ expect(out).toContain(
58
+ 'Would file GitHub issue on facebookexperimental/xds',
59
+ );
58
60
  expect(out).toContain('[gap] Button');
59
61
  expect(out).toContain('Need a compact variant');
60
62
  expect(out).toContain('Labels: gap-report');
@@ -91,13 +93,14 @@ describe('ASTRYX_GAP_REPORT=off env var', () => {
91
93
  it('disables gap reporting when set to "off"', async () => {
92
94
  process.env.ASTRYX_GAP_REPORT = 'off';
93
95
  vi.resetModules();
94
- const {loadGapReportConfig, createGapReport} = await import(
95
- '../utils/github.mjs'
96
- );
97
- expect(loadGapReportConfig().enabled).toBe(false);
96
+ const {loadGapReportConfig, createGapReport} =
97
+ await import('../utils/github.mjs');
98
+ await expect(loadGapReportConfig()).resolves.toMatchObject({
99
+ enabled: false,
100
+ });
98
101
 
99
102
  // createGapReport must return null and never invoke gh.
100
- const result = createGapReport({
103
+ const result = await createGapReport({
101
104
  component: 'Button',
102
105
  category: 'other',
103
106
  intention: 'test',
@@ -109,12 +112,16 @@ describe('ASTRYX_GAP_REPORT=off env var', () => {
109
112
  process.env.ASTRYX_GAP_REPORT = 'false';
110
113
  vi.resetModules();
111
114
  let mod = await import('../utils/github.mjs');
112
- expect(mod.loadGapReportConfig().enabled).toBe(false);
115
+ await expect(mod.loadGapReportConfig()).resolves.toMatchObject({
116
+ enabled: false,
117
+ });
113
118
 
114
119
  process.env.ASTRYX_GAP_REPORT = '0';
115
120
  vi.resetModules();
116
121
  mod = await import('../utils/github.mjs');
117
- expect(mod.loadGapReportConfig().enabled).toBe(false);
122
+ await expect(mod.loadGapReportConfig()).resolves.toMatchObject({
123
+ enabled: false,
124
+ });
118
125
  });
119
126
  });
120
127
 
@@ -133,16 +140,14 @@ describe('buildGapReportPreview', () => {
133
140
  it('renders title and body without invoking gh', async () => {
134
141
  vi.resetModules();
135
142
  const {buildGapReportPreview} = await import('../utils/github.mjs');
136
- const preview = buildGapReportPreview({
143
+ const preview = await buildGapReportPreview({
137
144
  component: 'Button',
138
145
  category: 'missing_variant',
139
146
  intention: 'Need compact variant for tables',
140
147
  });
141
148
  expect(preview.mode).toBe('github');
142
149
  expect(preview.enabled).toBe(true);
143
- expect(preview.title).toBe(
144
- '[gap] Button: Need compact variant for tables',
145
- );
150
+ expect(preview.title).toBe('[gap] Button: Need compact variant for tables');
146
151
  expect(preview.body).toContain('| **Component** | Button |');
147
152
  expect(preview.body).toContain('Need compact variant for tables');
148
153
  expect(preview.repo).toBe('facebookexperimental/xds');
@@ -152,7 +157,7 @@ describe('buildGapReportPreview', () => {
152
157
  process.env.ASTRYX_GAP_REPORT = 'off';
153
158
  vi.resetModules();
154
159
  const {buildGapReportPreview} = await import('../utils/github.mjs');
155
- const preview = buildGapReportPreview({
160
+ const preview = await buildGapReportPreview({
156
161
  component: 'Button',
157
162
  category: 'other',
158
163
  intention: 'x',
@@ -29,6 +29,37 @@ import {requireInteractive} from '../utils/interactive.mjs';
29
29
  const VALID_FEATURES = ['agents', 'theme', 'template'];
30
30
  const run = getRunPrefix();
31
31
 
32
+ /**
33
+ * Build the "Next steps" lines printed at the end of `astryx init`.
34
+ *
35
+ * Theme guidance must match the runtime recommendation emitted by core's
36
+ * <Theme> component (packages/core/src/theme/Theme.tsx): the pre-built theme
37
+ * path (`/built` import + `theme.css`) plus the base CSS import, so users
38
+ * don't end up with an unstyled app or the slower runtime style-injection
39
+ * path. See https://github.com/facebook/astryx/issues/3080.
40
+ *
41
+ * Exported for testing.
42
+ *
43
+ * @param {string} runPrefix package-manager run prefix (e.g. `npx`)
44
+ * @returns {string[]} ordered list of human-facing lines
45
+ */
46
+ export function getNextSteps(runPrefix) {
47
+ return [
48
+ '',
49
+ ' Next steps:',
50
+ " 1. Import base styles: import '@astryxdesign/core/reset.css'",
51
+ " and import '@astryxdesign/core/astryx.css'",
52
+ " 2. Import components: import { Button } from '@astryxdesign/core'",
53
+ ' 3. Optionally add a theme (use the pre-built path for performance):',
54
+ " import { neutralTheme } from '@astryxdesign/theme-neutral/built'",
55
+ " import '@astryxdesign/theme-neutral/theme.css'",
56
+ ' <Theme theme={neutralTheme}>...</Theme>',
57
+ ` For custom themes, run \`${runPrefix} astryx theme build <file>\` to generate the built artifacts.`,
58
+ ` 4. ${runPrefix} astryx --help for all commands`,
59
+ '',
60
+ ];
61
+ }
62
+
32
63
  function isCancel(value) {
33
64
  if (p.isCancel(value)) {
34
65
  p.cancel('Setup cancelled.');
@@ -100,7 +131,15 @@ async function runTemplate(targetDir, {interactive = true, templateName} = {}) {
100
131
 
101
132
  if (!interactive) {
102
133
  if (!templateName) {
103
- humanLog(`✓ Available templates: ${templates.join(', ')}. Use ${run} astryx template <name> [path].`);
134
+ // Point agents at the build workflow rather than dumping page-template
135
+ // names — `build` surfaces pages AND blocks AND components for an idea,
136
+ // and `build` with no args is the full how-to-build playbook.
137
+ humanLog('✓ To build UI, use these commands:');
138
+ humanLog('');
139
+ humanLog(` ${run} astryx build "<what you're building>" build a page — kit: closest template + blocks + components`);
140
+ humanLog(` ${run} astryx build the how-to-build workflow (read this first)`);
141
+ humanLog(` ${run} astryx search <query> find anything — components, docs, templates, blocks`);
142
+ humanLog('');
104
143
  return;
105
144
  }
106
145
 
@@ -242,13 +281,8 @@ export function registerInit(program) {
242
281
  // Outro
243
282
  p.outro('Design system initialized!');
244
283
 
245
- humanLog('');
246
- humanLog(' Next steps:');
247
- humanLog(" 1. Import components: import { Button } from '@astryxdesign/core'");
248
- humanLog(' 2. Optionally add a theme:');
249
- humanLog(" import { neutralTheme } from '@astryxdesign/theme-neutral'");
250
- humanLog(' <Theme theme={neutralTheme}>...</Theme>');
251
- humanLog(` 3. ${run} xds --help for all commands`);
252
- humanLog('');
284
+ for (const line of getNextSteps(run)) {
285
+ humanLog(line);
286
+ }
253
287
  });
254
288
  }
@@ -0,0 +1,46 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Regression test for `astryx init` "Next steps" theme guidance.
5
+ *
6
+ * The init command previously steered users toward the slower runtime
7
+ * style-injection path:
8
+ *
9
+ * import { neutralTheme } from '@astryxdesign/theme-neutral'
10
+ * <Theme theme={neutralTheme}>...</Theme>
11
+ *
12
+ * ...which contradicted the runtime console warning emitted by core's
13
+ * <Theme> component (packages/core/src/theme/Theme.tsx) recommending the
14
+ * pre-built path, and left users with an unstyled app because the base CSS
15
+ * imports were never mentioned (facebook/astryx#3080).
16
+ *
17
+ * These assertions lock in the corrected guidance: base CSS imports, the
18
+ * pre-built (`/built` + `theme.css`) theme path, and the custom-theme build
19
+ * command.
20
+ */
21
+
22
+ import {describe, it, expect} from 'vitest';
23
+ import {getNextSteps} from './init.mjs';
24
+
25
+ describe('init Next steps theme guidance', () => {
26
+ const text = getNextSteps('npx').join('\n');
27
+
28
+ it('mentions the base CSS imports so the app is not left unstyled', () => {
29
+ expect(text).toContain("'@astryxdesign/core/reset.css'");
30
+ expect(text).toContain("'@astryxdesign/core/astryx.css'");
31
+ });
32
+
33
+ it('uses the pre-built theme path matching the runtime recommendation', () => {
34
+ expect(text).toContain("'@astryxdesign/theme-neutral/built'");
35
+ expect(text).toContain("'@astryxdesign/theme-neutral/theme.css'");
36
+ });
37
+
38
+ it('mentions building custom themes via `astryx theme build`', () => {
39
+ expect(text).toContain('astryx theme build <file>');
40
+ });
41
+
42
+ it('does not steer users to the runtime style-injection import', () => {
43
+ // The bare source import (no `/built`) is the slow runtime-injection path.
44
+ expect(text).not.toContain("from '@astryxdesign/theme-neutral'");
45
+ });
46
+ });
@@ -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);