@astryxdesign/cli 0.1.0-canary.f54a06f → 0.1.0-canary.f718310

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 (104) hide show
  1. package/package.json +7 -7
  2. package/src/api/search.mjs +13 -207
  3. package/src/api/template.mjs +1 -2
  4. package/src/codemods/__tests__/registry.test.mjs +0 -1
  5. package/src/codemods/registry.mjs +0 -1
  6. package/src/codemods/runner.mjs +51 -105
  7. package/src/commands/agent-docs.mjs +6 -23
  8. package/src/commands/init.mjs +1 -9
  9. package/src/commands/upgrade.mjs +38 -117
  10. package/src/index.mjs +0 -1
  11. package/src/lib/config.mjs +0 -12
  12. package/src/lib/error-codes.mjs +0 -3
  13. package/src/types/error-codes.d.ts +0 -1
  14. package/src/utils/update-check.mjs +26 -4
  15. package/src/utils/update-check.test.mjs +63 -1
  16. package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +9 -1
  17. package/templates/blocks/components/AppShell/AppShellShowcase.tsx +10 -1
  18. package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +9 -1
  19. package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +9 -1
  20. package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +9 -1
  21. package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +9 -1
  22. package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +19 -12
  23. package/templates/blocks/components/Banner/BannerShowcase.tsx +8 -1
  24. package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +8 -1
  25. package/templates/blocks/components/Carousel/CarouselShowcase.tsx +12 -2
  26. package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +9 -6
  27. package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +12 -10
  28. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +9 -1
  29. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +9 -1
  30. package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +9 -1
  31. package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +8 -1
  32. package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +8 -1
  33. package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
  34. package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +8 -1
  35. package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
  36. package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
  37. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +12 -9
  38. package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +15 -13
  39. package/templates/blocks/components/Divider/DividerShowcase.tsx +8 -1
  40. package/templates/blocks/components/Divider/DividerVertical.tsx +9 -7
  41. package/templates/blocks/components/Field/FieldShowcase.tsx +8 -1
  42. package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +6 -1
  43. package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +9 -1
  44. package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +6 -4
  45. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +6 -1
  46. package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +8 -2
  47. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +8 -1
  48. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +12 -2
  49. package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +11 -8
  50. package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +12 -9
  51. package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +17 -4
  52. package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +16 -15
  53. package/templates/blocks/components/Overlay/OverlayShowcase.tsx +21 -5
  54. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +14 -2
  55. package/templates/blocks/components/Pagination/PaginationPageSize.tsx +14 -12
  56. package/templates/blocks/components/Pagination/PaginationVariants.tsx +8 -1
  57. package/templates/blocks/components/Pagination/PaginationWithTable.tsx +14 -2
  58. package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +6 -1
  59. package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +7 -2
  60. package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +6 -1
  61. package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +6 -1
  62. package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +6 -1
  63. package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +7 -2
  64. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +6 -1
  65. package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +9 -4
  66. package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +10 -1
  67. package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +8 -1
  68. package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +8 -1
  69. package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +10 -1
  70. package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +11 -8
  71. package/templates/pages/ai-chat/page.tsx +64 -71
  72. package/templates/pages/ai-chat-landing/page.tsx +12 -8
  73. package/templates/pages/centered-hero/page.tsx +15 -13
  74. package/templates/pages/classic-gallery/page.tsx +34 -27
  75. package/templates/pages/detail-page/page.tsx +18 -18
  76. package/templates/pages/documentation/page.tsx +14 -11
  77. package/templates/pages/documentation-design/page.tsx +13 -10
  78. package/templates/pages/documentation-technical/page.tsx +16 -15
  79. package/templates/pages/editor/page.tsx +54 -42
  80. package/templates/pages/file-explorer/page.tsx +16 -13
  81. package/templates/pages/form-two-column/page.tsx +17 -13
  82. package/templates/pages/gallery-hero/page.tsx +15 -13
  83. package/templates/pages/ide/page.tsx +39 -32
  84. package/templates/pages/library/page.tsx +23 -16
  85. package/templates/pages/login/page.tsx +18 -14
  86. package/templates/pages/login-card/page.tsx +18 -14
  87. package/templates/pages/login-split/page.tsx +48 -50
  88. package/templates/pages/login-sso/page.tsx +13 -9
  89. package/templates/pages/mixed-gallery/page.tsx +45 -51
  90. package/templates/pages/payment-form/page.tsx +70 -56
  91. package/templates/pages/product-detail/page.tsx +33 -27
  92. package/templates/pages/product-gallery/page.tsx +13 -7
  93. package/templates/pages/settings-dialog/page.tsx +43 -35
  94. package/templates/pages/settings-sidebar/page.tsx +47 -39
  95. package/templates/pages/side-gallery/page.tsx +9 -6
  96. package/templates/pages/table-grouped/page.tsx +15 -11
  97. package/templates/pages/theme-showcase/page.tsx +37 -33
  98. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
  99. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +0 -51
  100. package/src/codemods/transforms/v0.1.0/index.mjs +0 -28
  101. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
  102. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +0 -84
  103. package/src/commands/build.mjs +0 -196
  104. package/src/lib/config.test.mjs +0 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.0-canary.f54a06f",
3
+ "version": "0.1.0-canary.f718310",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -54,9 +54,9 @@
54
54
  "jscodeshift": "^17.3.0"
55
55
  },
56
56
  "peerDependencies": {
57
- "@astryxdesign/core": "0.1.0-canary.f54a06f",
58
- "@astryxdesign/lab": "0.1.0-canary.f54a06f",
59
- "@astryxdesign/theme-neutral": "0.1.0-canary.f54a06f"
57
+ "@astryxdesign/core": "0.1.0-canary.f718310",
58
+ "@astryxdesign/lab": "0.1.0-canary.f718310",
59
+ "@astryxdesign/theme-neutral": "0.1.0-canary.f718310"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "@astryxdesign/core": {
@@ -70,9 +70,9 @@
70
70
  }
71
71
  },
72
72
  "devDependencies": {
73
- "@astryxdesign/core": "0.1.0-canary.f54a06f",
74
- "@astryxdesign/lab": "0.1.0-canary.f54a06f",
75
- "@astryxdesign/theme-neutral": "0.1.0-canary.f54a06f"
73
+ "@astryxdesign/core": "0.1.0-canary.f718310",
74
+ "@astryxdesign/lab": "0.1.0-canary.f718310",
75
+ "@astryxdesign/theme-neutral": "0.1.0-canary.f718310"
76
76
  },
77
77
  "scripts": {
78
78
  "astryx": "node bin/astryx.mjs",
@@ -41,184 +41,14 @@ import {
41
41
  } from '../lib/component-discovery.mjs';
42
42
  import {discoverHooks, findHookDoc} from '../lib/hook-discovery.mjs';
43
43
  import {levenshteinDistance} from '../lib/string-utils.mjs';
44
- import {discoverTemplates, extractComponents} from './template.mjs';
44
+ import {discoverTemplates} from './template.mjs';
45
45
  import {AstryxError} from './error.mjs';
46
46
 
47
47
  const DOCS_DIR = path.join(CLI_ROOT, 'docs');
48
48
 
49
- /**
50
- * Synonym / intent map: product-language terms an agent is likely to type,
51
- * expanded to the catalog's vocabulary so oblique queries still rank. Keys and
52
- * values are matched bidirectionally (typing any value also pulls in the key
53
- * and its siblings). Lowercase, single words or short phrases.
54
- */
55
- const SYNONYMS = {
56
- dashboard: ['overview', 'analytics', 'kpi', 'kpis', 'metrics', 'stats', 'reporting', 'insights', 'control'],
57
- login: ['signin', 'auth', 'authentication', 'sso', 'credentials', 'account'],
58
- signup: ['register', 'registration', 'onboarding'],
59
- payment: ['checkout', 'billing', 'card', 'pay', 'purchase', 'order'],
60
- pricing: ['plans', 'plan', 'tiers', 'tier', 'subscription', 'subscriptions'],
61
- chat: ['messaging', 'message', 'messages', 'conversation', 'inbox', 'dm'],
62
- settings: ['preferences', 'config', 'configuration', 'account'],
63
- calendar: ['schedule', 'scheduling', 'events', 'event', 'month', 'agenda'],
64
- table: ['list', 'rows', 'records', 'grid', 'spreadsheet', 'datatable'],
65
- gallery: ['photos', 'photo', 'images', 'image', 'pictures'],
66
- hero: ['banner', 'splash', 'headline', 'landing'],
67
- form: ['fields', 'input', 'inputs', 'survey'],
68
- profile: ['bio', 'avatar', 'user'],
69
- documentation: ['docs', 'reference', 'guide', 'api'],
70
- navigation: ['nav', 'menu', 'sidebar'],
71
- };
72
-
73
- // Flatten into a token -> Set(expansions) lookup (bidirectional).
74
- const SYNONYM_INDEX = (() => {
75
- const idx = new Map();
76
- const add = (a, b) => {
77
- if (!idx.has(a)) idx.set(a, new Set());
78
- idx.get(a).add(b);
79
- };
80
- for (const [key, vals] of Object.entries(SYNONYMS)) {
81
- for (const v of vals) {
82
- add(key, v);
83
- add(v, key);
84
- for (const v2 of vals) if (v2 !== v) add(v, v2);
85
- }
86
- }
87
- return idx;
88
- })();
89
-
90
- /**
91
- * Light stemmer: strips common English suffixes so "charts"/"charting" and
92
- * "chart" share a root. Deliberately crude (no Porter) — good enough to bridge
93
- * plural/gerund gaps without a dependency.
94
- * @param {string} w
95
- * @returns {string}
96
- */
97
- export function stem(w) {
98
- let s = w;
99
- for (const suf of ['ing', 'ed', 'ies', 'es', 's']) {
100
- if (s.length > suf.length + 2 && s.endsWith(suf)) {
101
- s = suf === 'ies' ? s.slice(0, -3) + 'y' : s.slice(0, -suf.length);
102
- break;
103
- }
104
- }
105
- return s;
106
- }
107
-
108
-
109
49
  /** Valid domain filters for `--type`. */
110
50
  export const SEARCH_DOMAINS = ['component', 'hook', 'doc', 'template'];
111
51
 
112
- /**
113
- * Filler words stripped from multi-word queries so natural-language phrasing
114
- * ("a page where you can see business stats") ranks on its content words.
115
- */
116
- const STOPWORDS = new Set([
117
- 'a', 'an', 'the', 'of', 'for', 'to', 'with', 'and', 'or', 'in', 'on', 'at',
118
- 'by', 'that', 'this', 'my', 'your', 'our', 'their', 'is', 'are', 'be', 'it',
119
- 'its', 'as', 'from', 'page', 'screen', 'app', 'application', 'view', 'where',
120
- 'you', 'can', 'some', 'like', 'just', 'basically', 'kinda', 'want', 'wants',
121
- 'need', 'needs', 'something', 'thing', 'things', 'build', 'make', 'create',
122
- 'i', 'me', 'we', 'us', 'so', 'up', 'out', 'over', 'side', 'one', 'big',
123
- ]);
124
-
125
- /**
126
- * Split a query into meaningful content tokens (lowercased, stopwords + very
127
- * short words removed). Empty for single-word queries (callers fall back to
128
- * whole-phrase scoring).
129
- * @param {string} term - Already-lowercased query.
130
- * @returns {string[]}
131
- */
132
- export function tokenizeQuery(term) {
133
- return term
134
- .split(/\s+/)
135
- // Strip only leading/trailing punctuation; keep joined identifiers intact
136
- // (e.g. "foo_bar" stays one token) so gibberish stays gibberish.
137
- .map(t => t.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, ''))
138
- .filter(t => t.length >= 2 && !STOPWORDS.has(t));
139
- }
140
-
141
- /**
142
- * Score a candidate against a query, handling multi-word natural language.
143
- * Tries the whole phrase (so exact/near matches still win) AND a per-token
144
- * pass (so "data table with filters" matches `table-page` via table+filter),
145
- * and returns whichever is stronger.
146
- *
147
- * @param {string} term - Lowercased full query.
148
- * @param {string[]} tokens - Content tokens from tokenizeQuery(term).
149
- * @param {object} candidate
150
- * @returns {{score: number, reason: string} | null}
151
- */
152
- /**
153
- * Minimum per-token score (in the multi-word pass) to count as a real match.
154
- * 50 = a genuine name/keyword/description hit; below that is loose Levenshtein
155
- * fuzz that would otherwise turn gibberish queries into noise.
156
- */
157
- const MIN_TOKEN_SCORE = 50;
158
-
159
- /**
160
- * Best score for a token against a candidate, fanning out through synonyms
161
- * (synonym hits are discounted so a direct hit always wins).
162
- * @returns {{score: number, reason: string} | null}
163
- */
164
- function bestForToken(tok, candidate) {
165
- let best = scoreCandidate(tok, candidate);
166
- const syns = SYNONYM_INDEX.get(tok);
167
- if (syns) {
168
- for (const s of syns) {
169
- const h = scoreCandidate(s, candidate);
170
- if (h) {
171
- const score = Math.round(h.score * 0.85);
172
- if (!best || score > best.score) best = {score, reason: `${h.reason} (~${tok})`};
173
- }
174
- }
175
- }
176
- return best;
177
- }
178
-
179
- export function scoreQuery(term, tokens, candidate) {
180
- const full = scoreCandidate(term, candidate);
181
-
182
- // 0–1 content tokens: keep whole-phrase fuzzy matching (typo tolerance for
183
- // single words), but if stopwords left exactly one DIFFERENT token (e.g.
184
- // "pricing page" → "pricing"), score that token too and take the stronger.
185
- if (tokens.length <= 1) {
186
- const single = tokens.length === 1 ? bestForToken(tokens[0], candidate) : null;
187
- if (full && (!single || full.score >= single.score)) return full;
188
- return single;
189
- }
190
-
191
- // Multi-word natural language: score each content token, counting only
192
- // strong hits, then reward coverage so candidates matching more terms win.
193
- let sum = 0;
194
- let matched = 0;
195
- const hitTerms = [];
196
- for (const tok of tokens) {
197
- const h = bestForToken(tok, candidate);
198
- if (h && h.score >= MIN_TOKEN_SCORE) {
199
- sum += h.score;
200
- matched++;
201
- hitTerms.push(tok);
202
- }
203
- }
204
- if (matched === 0) return full;
205
-
206
- // Reward the AVERAGE strength of the concepts that matched (not divided by
207
- // total query length — that penalizes verbose / low-fidelity prompts), plus
208
- // a bonus per additional matched concept and a coverage term. A candidate
209
- // that matches several of the query's concepts beats one matching a single
210
- // incidental word.
211
- const avgMatched = sum / matched;
212
- const coverage = matched / tokens.length;
213
- const tokenScore = Math.round(avgMatched + Math.min(matched - 1, 3) * 12 + coverage * 15);
214
-
215
- if (full && full.score >= tokenScore) return full;
216
- return {
217
- score: tokenScore,
218
- reason: `matches ${matched}/${tokens.length} terms: ${hitTerms.join(', ')}`,
219
- };
220
- }
221
-
222
52
  /**
223
53
  * Score a single candidate against the search term across name, keywords,
224
54
  * and prose signals. Returns the best (highest) score plus a human reason,
@@ -277,13 +107,10 @@ export function scoreCandidate(term, {name, keywords = [], description = '', pro
277
107
  else if (dist === 2) consider(30, `keyword "${kw}" (distance ${dist})`);
278
108
  }
279
109
 
280
- // ── Prose / description signals (stem-tolerant whole word) ──────
281
- // Match the term's stem as a whole word, tolerating plural/gerund suffixes
282
- // so "chart" matches "charts" and "filter" matches "filtering".
110
+ // ── Prose / description signals (whole-word boundary) ───────────
283
111
  if (term.length >= 3) {
284
- const root = stem(term);
285
- const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
286
- const re = new RegExp(`\\b${escaped}(s|es|ing|ed|ies)?\\b`);
112
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
+ const re = new RegExp(`\\b${escaped}\\b`);
287
114
  if (description && re.test(description.toLowerCase())) {
288
115
  consider(50, `description mentions "${term}"`);
289
116
  } else {
@@ -413,30 +240,14 @@ async function gatherTemplates(cwd) {
413
240
  } catch {
414
241
  return [];
415
242
  }
416
- return templates.map(t => {
417
- // Blocks ship componentsUsed; page templates don't, so derive them from the
418
- // source. Category words (e.g. "Dashboard - Analytics") are strong intent
419
- // signal for pages, which otherwise only index on name + description.
420
- let keywords = Array.isArray(t.componentsUsed) ? [...t.componentsUsed] : [];
421
- if (t.type === 'page') {
422
- if (t.filePath) {
423
- try {
424
- keywords = keywords.concat(extractComponents(t.filePath));
425
- } catch {
426
- // Best-effort: skip keyword enrichment if the source can't be read.
427
- }
428
- }
429
- if (t.category) keywords = keywords.concat(t.category.split(/[^A-Za-z0-9]+/).filter(Boolean));
430
- }
431
- return {
432
- domain: 'template',
433
- name: t.dirName,
434
- keywords,
435
- description: t.description || '',
436
- _displayName: t.name,
437
- _kind: t.type, // 'page' | 'block'
438
- };
439
- });
243
+ return templates.map(t => ({
244
+ domain: 'template',
245
+ name: t.dirName,
246
+ keywords: Array.isArray(t.componentsUsed) ? t.componentsUsed : [],
247
+ description: t.description || '',
248
+ _displayName: t.name,
249
+ _kind: t.type, // 'page' | 'block'
250
+ }));
440
251
  }
441
252
 
442
253
  /**
@@ -514,7 +325,6 @@ export async function search(query, options = {}) {
514
325
  }
515
326
 
516
327
  const term = String(query).trim().toLowerCase();
517
- const tokens = tokenizeQuery(term);
518
328
 
519
329
  const coreDir = findCoreDir(cwd);
520
330
  if (!coreDir) {
@@ -532,13 +342,9 @@ export async function search(query, options = {}) {
532
342
 
533
343
  const all = [...components, ...hooks, ...docTopics, ...templates];
534
344
 
535
- // Score every candidate on its own merits. The consumer groups results by
536
- // role (page / block / component) and takes the top of each, so there's no
537
- // cross-role competition to engineer — a target page only needs to be the
538
- // strongest PAGE, not outrank every component.
539
345
  const scored = [];
540
346
  for (const candidate of all) {
541
- const hit = scoreQuery(term, tokens, candidate);
347
+ const hit = scoreCandidate(term, candidate);
542
348
  if (hit) scored.push(toResult(candidate, hit.score, hit.reason));
543
349
  }
544
350
 
@@ -95,7 +95,6 @@ async function discoverPages() {
95
95
  dirName: dir.name,
96
96
  name: doc?.name || dir.name,
97
97
  description: doc?.description || '',
98
- category: doc?.category || '',
99
98
  isReady: doc?.isReady ?? true,
100
99
  scaffold: doc?.scaffold ?? false,
101
100
  filePath: path.join(dirPath, 'page.tsx'),
@@ -246,7 +245,7 @@ const UBIQUITOUS = new Set([
246
245
  'StackItem', 'Icon',
247
246
  ]);
248
247
 
249
- export function extractComponents(pagePath) {
248
+ function extractComponents(pagePath) {
250
249
  const src = fs.readFileSync(pagePath, 'utf-8');
251
250
  // Match JSX opening tags, e.g. `<Section` or the legacy `<XDSSection`.
252
251
  // Templates author bare component names post un-prefix migration
@@ -16,7 +16,6 @@ describe('registry', () => {
16
16
  '0.0.13',
17
17
  '0.0.14',
18
18
  '0.0.15',
19
- '0.1.0',
20
19
  ]);
21
20
  });
22
21
  });
@@ -17,7 +17,6 @@ const registry = new Map([
17
17
  ['0.0.13', () => import('./transforms/v0.0.13/index.mjs')],
18
18
  ['0.0.14', () => import('./transforms/v0.0.14/index.mjs')],
19
19
  ['0.0.15', () => import('./transforms/v0.0.15/index.mjs')],
20
- ['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
21
20
  ]);
22
21
 
23
22
  // Re-export from the shared utility so registry callers and other consumers
@@ -11,6 +11,7 @@
11
11
  import * as fs from 'node:fs';
12
12
  import * as path from 'node:path';
13
13
  import * as p from '@clack/prompts';
14
+ import {getRunPrefix} from '../utils/package-manager.mjs';
14
15
  import {humanLog} from '../lib/json.mjs';
15
16
 
16
17
  // Known corruption patterns that indicate a broken transform.
@@ -86,6 +87,39 @@ function findSourceFiles(dir) {
86
87
  return results.sort();
87
88
  }
88
89
 
90
+ /**
91
+ * Detect the project's formatter and run it on changed files.
92
+ * Tries prettier, then biome. Silently skips if none found.
93
+ *
94
+ * @param {string[]} files - Absolute paths to files that were modified
95
+ * @param {boolean} [silent] - Suppress human-facing output
96
+ */
97
+ async function formatChangedFiles(files, silent = false) {
98
+ if (files.length === 0) return;
99
+
100
+ const {execSync} = await import('node:child_process');
101
+ const fileArgs = files.map(f => `"${f}"`).join(' ');
102
+ const prefix = getRunPrefix();
103
+
104
+ const formatters = [
105
+ {name: 'prettier', cmd: `${prefix} prettier --write ${fileArgs}`},
106
+ {name: 'biome', cmd: `${prefix} biome format --write ${fileArgs}`},
107
+ ];
108
+
109
+ for (const {name, cmd} of formatters) {
110
+ try {
111
+ execSync(cmd, {stdio: 'pipe', timeout: 30000});
112
+ if (!silent)
113
+ p.log.info(
114
+ `Formatted ${files.length} file${files.length === 1 ? '' : 's'} with ${name}`,
115
+ );
116
+ return;
117
+ } catch {
118
+ // Formatter not available or failed — try next
119
+ }
120
+ }
121
+ }
122
+
89
123
  /**
90
124
  * Validate transform output before writing to disk.
91
125
  *
@@ -128,86 +162,6 @@ export function validateOutput(result, source, j, {parse = true} = {}) {
128
162
  return {valid: true};
129
163
  }
130
164
 
131
- function isConfigCodemod(transformEntry) {
132
- return transformEntry.meta?.codemodType === 'config';
133
- }
134
-
135
- const CONFIG_CODEMOD_PATHS = new Set([
136
- 'package.json',
137
- 'astryx.config.mjs',
138
- 'xds.config.mjs',
139
- ]);
140
-
141
- function resolveConfigPath(relativePath) {
142
- if (!CONFIG_CODEMOD_PATHS.has(relativePath)) {
143
- throw new Error(`unsupported config codemod path: ${relativePath}`);
144
- }
145
- return path.resolve(process.cwd(), relativePath);
146
- }
147
-
148
- function readOptionalConfigFile(relativePath) {
149
- const fullPath = resolveConfigPath(relativePath);
150
- if (!fs.existsSync(fullPath)) return null;
151
- return {path: relativePath, source: fs.readFileSync(fullPath, 'utf-8')};
152
- }
153
-
154
- function getConfigCodemodContext() {
155
- return {
156
- packageJson: readOptionalConfigFile('package.json'),
157
- astryxConfig: readOptionalConfigFile('astryx.config.mjs'),
158
- xdsConfig: readOptionalConfigFile('xds.config.mjs'),
159
- };
160
- }
161
-
162
- async function runConfigCodemod(transformEntry, {apply, log}) {
163
- const {transform} = transformEntry;
164
- const api = {config: getConfigCodemodContext()};
165
- const result = await transform({path: process.cwd(), source: ''}, api);
166
- const errors = result?.errors ?? [];
167
- if (errors.length > 0) {
168
- for (const error of errors) {
169
- log.error(` ✗ ${error.file ?? 'config'} — ${error.error}`);
170
- }
171
- return {filesChanged: 0, errors};
172
- }
173
-
174
- const changes = result?.changes ?? [];
175
- if (changes.length === 0) return {filesChanged: 0, errors: []};
176
-
177
- for (const change of changes) {
178
- const fullPath = resolveConfigPath(change.path);
179
- if (change.delete) {
180
- if (apply) fs.rmSync(fullPath, {force: true});
181
- log[apply ? 'success' : 'warn'](
182
- ` ${apply ? '✓' : '~'} ${change.path} (delete${apply ? '' : ', dry run'})`,
183
- );
184
- continue;
185
- }
186
-
187
- if (apply) {
188
- fs.writeFileSync(fullPath, change.source, 'utf-8');
189
- }
190
- log[apply ? 'success' : 'warn'](
191
- ` ${apply ? '✓' : '~'} ${change.path}${apply ? '' : ' (would change)'}`,
192
- );
193
- }
194
-
195
- if (changes.length > 0) {
196
- const verb = apply ? 'Updated' : 'Would update';
197
- log.info(
198
- ` ${verb} ${changes.length} config file${changes.length === 1 ? '' : 's'}`,
199
- );
200
- }
201
-
202
- return {
203
- filesChanged: changes.length,
204
- writtenFiles: changes
205
- .filter(change => !change.delete && path.extname(change.path) !== '.json')
206
- .map(change => resolveConfigPath(change.path)),
207
- errors: [],
208
- };
209
- }
210
-
211
165
  /**
212
166
  * Run codemods against source files.
213
167
  *
@@ -243,12 +197,18 @@ export async function runCodemods(
243
197
 
244
198
  if (files.length === 0) {
245
199
  log.warn('No source files found.');
246
- } else {
247
- log.info(
248
- `Found ${files.length} source file${files.length === 1 ? '' : 's'}`,
249
- );
200
+ return {
201
+ ok: true,
202
+ totalFilesChanged: 0,
203
+ totalTransformsApplied: 0,
204
+ errors: [],
205
+ writtenFiles: [],
206
+ skippedOptional: [],
207
+ };
250
208
  }
251
209
 
210
+ log.info(`Found ${files.length} source file${files.length === 1 ? '' : 's'}`);
211
+
252
212
  // Dynamically import jscodeshift
253
213
  const jscodeshift = (await import('jscodeshift')).default;
254
214
 
@@ -279,24 +239,6 @@ export async function runCodemods(
279
239
 
280
240
  log.info(` ${meta.title}`);
281
241
 
282
- if (isConfigCodemod(transformEntry)) {
283
- const result = await runConfigCodemod(transformEntry, {apply, log});
284
- if (result.errors.length > 0) {
285
- for (const error of result.errors) {
286
- errors.push({
287
- file: error.file ?? 'config',
288
- codemod: name,
289
- error: error.error,
290
- });
291
- }
292
- } else if (result.filesChanged > 0) {
293
- totalFilesChanged += result.filesChanged;
294
- totalTransformsApplied += result.filesChanged;
295
- writtenFiles.push(...(result.writtenFiles ?? []));
296
- }
297
- continue;
298
- }
299
-
300
242
  let filesChanged = 0;
301
243
 
302
244
  for (const filePath of files) {
@@ -381,6 +323,12 @@ export async function runCodemods(
381
323
  }
382
324
  }
383
325
 
326
+ // Post-codemod formatting: run the project's formatter on changed files
327
+ // so codemods don't introduce style drift (jscodeshift may change quotes, etc.)
328
+ if (apply && writtenFiles.length > 0) {
329
+ await formatChangedFiles(writtenFiles, silent);
330
+ }
331
+
384
332
  if (totalValidationBlocked > 0) {
385
333
  log.warn(
386
334
  `${totalValidationBlocked} file${totalValidationBlocked === 1 ? ' was' : 's were'} blocked by validation — no changes written to ${totalValidationBlocked === 1 ? 'that file' : 'those files'}.`,
@@ -418,9 +366,7 @@ export async function runCodemods(
418
366
  if (meta.description) {
419
367
  log.info(` ${meta.description}`);
420
368
  }
421
- log.info(
422
- ` Run: astryx upgrade --codemod ${name} --path <dir> --apply`,
423
- );
369
+ log.info(` Run: astryx upgrade --codemod ${name} --path <dir> --apply`);
424
370
  }
425
371
  }
426
372
 
@@ -121,29 +121,14 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
121
121
  lines.push(`Astryx v${version} — ${componentCount} components`);
122
122
  lines.push('');
123
123
 
124
- // One-time project setup components ship precompiled CSS that MUST be
125
- // imported, or everything renders unstyled. Stated as text (the CLI does not
126
- // edit your files). The Theme provider is OPTIONAL (a default theme ships in
127
- // astryx.css); only the CSS imports are required.
128
- lines.push('SETUP (required, once) — in your app entry (e.g. main.tsx):');
129
- lines.push(' import "@astryxdesign/core/reset.css";');
130
- lines.push(' import "@astryxdesign/core/astryx.css";');
131
- lines.push('Without these CSS imports, components render completely unstyled.');
132
- lines.push(`Optional: apply a specific theme with <Theme> — see \`${run} docs theme\`.`);
133
- lines.push('');
134
-
135
- // Behavioral workflow — `build` is the front door for making pages: it returns
136
- // a composition kit, and `build` with no args prints the full how-to playbook.
124
+ // Behavioral workflowtemplates first, then component lookup
137
125
  lines.push('Before writing any UI code:');
138
- lines.push(
139
- `1. \`${run} build "<what you're building>"\` — START HERE. Returns a composition kit: the closest [page] recipe (scaffold it, or use as a layout reference), the [block]s that cover parts, and the [component]s to fill gaps. (Run \`${run} build\` with no args for the full how-to-build playbook.)`,
140
- );
141
- lines.push(`2. \`${run} template <name> --skeleton\` — scaffold a matched [page], or study its layout (gap, padding, nesting)`);
142
- lines.push(`3. \`${run} template <BlockName>\` — drop in each [block] the kit surfaced (ready-made multi-component patterns) instead of hand-building`);
143
- lines.push(`4. \`${run} component <Name>\` — read props + examples for EVERY component you use`);
126
+ lines.push(`1. \`${run} template --list\` — find a related page pattern`);
127
+ lines.push(`2. \`${run} template <name> --skeleton\` — study layout structure (gap, padding, nesting)`);
128
+ lines.push(`3. \`${run} component <Name>\` — read props + examples for EVERY component you use`);
144
129
  lines.push('');
145
- lines.push('Templates and blocks are reference code — read them for composition patterns, not just scaffolding.');
146
- lines.push(`(\`${run} search <query>\` is a neutral find across components/hooks/docs/templates when you just need to look something up.)`);
130
+ lines.push('Templates are reference code — read them for composition patterns, not just scaffolding.');
131
+ lines.push('Full pages dashboard (uses AppShell). Forms contact-form. Tables data-table. Settings settings-sidebar.');
147
132
  lines.push('');
148
133
 
149
134
  // Rules — inline, compact, prevents the top error categories
@@ -156,8 +141,6 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
156
141
  lines.push('');
157
142
 
158
143
  // CLI quick reference
159
- lines.push(`${run} build "<idea>" START HERE for pages — kit: closest page + blocks + components (\`build\` = how-to playbook)`);
160
- lines.push(`${run} search "<query>" find anything: components, hooks, docs, templates, blocks`);
161
144
  lines.push(`${run} component --list ${componentCount} components by category`);
162
145
  lines.push(`${run} component <Name> props, types, examples`);
163
146
 
@@ -100,15 +100,7 @@ async function runTemplate(targetDir, {interactive = true, templateName} = {}) {
100
100
 
101
101
  if (!interactive) {
102
102
  if (!templateName) {
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('');
103
+ humanLog(`✓ Available templates: ${templates.join(', ')}. Use ${run} astryx template <name> [path].`);
112
104
  return;
113
105
  }
114
106