@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
@@ -7,7 +7,11 @@
7
7
  import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import {CLI_ROOT, discoverExternalPackages} from '../utils/paths.mjs';
10
- import {assertWithin, isFilePathArg, PathSafetyError} from '../utils/path-safety.mjs';
10
+ import {
11
+ assertWithin,
12
+ isFilePathArg,
13
+ PathSafetyError,
14
+ } from '../utils/path-safety.mjs';
11
15
  import {AstryxError} from './error.mjs';
12
16
  import {ERROR_CODES} from '../lib/error-codes.mjs';
13
17
  import {loadConfig} from '../lib/config.mjs';
@@ -23,7 +27,7 @@ const BLOCKS_DIR = path.join(TEMPLATES_DIR, 'blocks');
23
27
  * or not. Mirrors apps/docsite/public/template-assets/placeholder.svg.
24
28
  */
25
29
  const PLACEHOLDER_IMAGE =
26
- "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20400%20300%22%20preserveAspectRatio%3D%22xMidYMid%20slice%22%3E%3Crect%20width%3D%22400%22%20height%3D%22300%22%20fill%3D%22%23f5f6f8%22%2F%3E%3Cg%20transform%3D%22translate%28200%20150%29%22%20fill%3D%22none%22%20stroke%3D%22%23c2cad6%22%20stroke-width%3D%225%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%22-44%22%20y%3D%22-44%22%20width%3D%2288%22%20height%3D%2288%22%20rx%3D%2216%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%22-18%22%20r%3D%222.5%22%20fill%3D%22%23c2cad6%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M-34%2030%20L-8%200%20L10%2018%20L20%208%20L34%2024%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E";
30
+ 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20400%20300%22%20preserveAspectRatio%3D%22xMidYMid%20slice%22%3E%3Crect%20width%3D%22400%22%20height%3D%22300%22%20fill%3D%22%23f5f6f8%22%2F%3E%3Cg%20transform%3D%22translate%28200%20150%29%22%20fill%3D%22none%22%20stroke%3D%22%23c2cad6%22%20stroke-width%3D%225%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%22-44%22%20y%3D%22-44%22%20width%3D%2288%22%20height%3D%2288%22%20rx%3D%2216%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%22-18%22%20r%3D%222.5%22%20fill%3D%22%23c2cad6%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M-34%2030%20L-8%200%20L10%2018%20L20%208%20L34%2024%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E';
27
31
 
28
32
  /**
29
33
  * Demo-image sources to strip from scaffolded projects — Meta's lookaside CDN
@@ -60,9 +64,12 @@ export {discoverAll as discoverTemplates};
60
64
  export function listTemplates() {
61
65
  const all = [];
62
66
  if (fs.existsSync(PAGES_DIR)) {
63
- all.push(...fs.readdirSync(PAGES_DIR, {withFileTypes: true})
64
- .filter(e => e.isDirectory())
65
- .map(e => e.name));
67
+ all.push(
68
+ ...fs
69
+ .readdirSync(PAGES_DIR, {withFileTypes: true})
70
+ .filter(e => e.isDirectory())
71
+ .map(e => e.name),
72
+ );
66
73
  }
67
74
  return all.sort();
68
75
  }
@@ -83,7 +90,8 @@ function findDocFiles(dir, pattern) {
83
90
 
84
91
  async function discoverPages() {
85
92
  if (!fs.existsSync(PAGES_DIR)) return [];
86
- const dirs = fs.readdirSync(PAGES_DIR, {withFileTypes: true})
93
+ const dirs = fs
94
+ .readdirSync(PAGES_DIR, {withFileTypes: true})
87
95
  .filter(e => e.isDirectory());
88
96
 
89
97
  const templates = [];
@@ -95,6 +103,7 @@ async function discoverPages() {
95
103
  dirName: dir.name,
96
104
  name: doc?.name || dir.name,
97
105
  description: doc?.description || '',
106
+ category: doc?.category || '',
98
107
  isReady: doc?.isReady ?? true,
99
108
  scaffold: doc?.scaffold ?? false,
100
109
  filePath: path.join(dirPath, 'page.tsx'),
@@ -137,7 +146,11 @@ async function discoverBlocks() {
137
146
  * @param {string} [cwd]
138
147
  */
139
148
  async function discoverExternalBlocks(cwd = process.cwd()) {
140
- const externals = discoverExternalPackages(cwd);
149
+ const config = await loadConfig(cwd);
150
+ const integrationPackages = (config.loadedIntegrations ?? [])
151
+ .map(integration => integration.package)
152
+ .filter(Boolean);
153
+ const externals = [...discoverExternalPackages(cwd), ...integrationPackages];
141
154
  const blocks = [];
142
155
 
143
156
  for (const ext of externals) {
@@ -192,9 +205,7 @@ async function discoverAll() {
192
205
  export async function findRelatedBlocks(componentName, cwd) {
193
206
  const blocks = await discoverAllBlocks(cwd);
194
207
  return blocks.filter(b =>
195
- b.componentsUsed.some(c =>
196
- c.toLowerCase() === componentName.toLowerCase(),
197
- ),
208
+ b.componentsUsed.some(c => c.toLowerCase() === componentName.toLowerCase()),
198
209
  );
199
210
  }
200
211
 
@@ -217,7 +228,7 @@ export async function findShowcase(componentName, cwd, options) {
217
228
  return true;
218
229
  });
219
230
 
220
- const toResult = (b) => ({
231
+ const toResult = b => ({
221
232
  name: b.name,
222
233
  aspectRatio: b.aspectRatio,
223
234
  filePath: b.filePath,
@@ -241,11 +252,17 @@ export async function findShowcase(componentName, cwd, options) {
241
252
  }
242
253
 
243
254
  const UBIQUITOUS = new Set([
244
- 'Text', 'Heading', 'Button', 'HStack', 'VStack', 'Link',
245
- 'StackItem', 'Icon',
255
+ 'Text',
256
+ 'Heading',
257
+ 'Button',
258
+ 'HStack',
259
+ 'VStack',
260
+ 'Link',
261
+ 'StackItem',
262
+ 'Icon',
246
263
  ]);
247
264
 
248
- function extractComponents(pagePath) {
265
+ export function extractComponents(pagePath) {
249
266
  const src = fs.readFileSync(pagePath, 'utf-8');
250
267
  // Match JSX opening tags, e.g. `<Section` or the legacy `<XDSSection`.
251
268
  // Templates author bare component names post un-prefix migration
@@ -258,22 +275,115 @@ function extractComponents(pagePath) {
258
275
  while ((m = tagRegex.exec(src)) !== null) {
259
276
  matches.push(m[2]);
260
277
  }
261
- return [...new Set(
262
- matches
263
- .filter(n => !['Theme', 'ThemeProvider'].includes(n))
264
- .filter(n => !UBIQUITOUS.has(n))
265
- .map(n => n.replace(/(Item|Section|Header|Content|Footer|Panel|Heading|CollapseButton|Column|Sortable|Selection|Group|Source)$/, ''))
266
- .filter(Boolean),
267
- )].sort();
278
+ return [
279
+ ...new Set(
280
+ matches
281
+ .filter(n => !['Theme', 'ThemeProvider'].includes(n))
282
+ .filter(n => !UBIQUITOUS.has(n))
283
+ .map(n =>
284
+ n.replace(
285
+ /(Item|Section|Header|Content|Footer|Panel|Heading|CollapseButton|Column|Sortable|Selection|Group|Source)$/,
286
+ '',
287
+ ),
288
+ )
289
+ .filter(Boolean),
290
+ ),
291
+ ].sort();
268
292
  }
269
293
 
270
294
  const STRUCTURAL = new Set([
271
- 'AppShell', 'Layout', 'LayoutHeader', 'LayoutContent', 'LayoutPanel',
272
- 'LayoutFooter', 'Card', 'Section', 'Grid', 'GridSpan', 'List',
273
- 'Table', 'TabList', 'Toolbar', 'SideNav', 'TopNav', 'Dialog',
274
- 'FormLayout', 'Center',
295
+ 'AppShell',
296
+ 'Layout',
297
+ 'LayoutHeader',
298
+ 'LayoutContent',
299
+ 'LayoutPanel',
300
+ 'LayoutFooter',
301
+ 'Card',
302
+ 'Section',
303
+ 'Grid',
304
+ 'GridSpan',
305
+ 'List',
306
+ 'Table',
307
+ 'TabList',
308
+ 'Toolbar',
309
+ 'SideNav',
310
+ 'TopNav',
311
+ 'Dialog',
312
+ 'FormLayout',
313
+ 'Center',
275
314
  ]);
276
315
 
316
+ const SPATIAL_PROPS = [
317
+ 'padding',
318
+ 'contentPadding',
319
+ 'gap',
320
+ 'rowGap',
321
+ 'columnGap',
322
+ 'columns',
323
+ 'minChildWidth',
324
+ 'hasDivider',
325
+ 'defaultHasDividers',
326
+ 'variant',
327
+ 'density',
328
+ 'role',
329
+ 'height',
330
+ 'width',
331
+ 'maxWidth',
332
+ ];
333
+
334
+ /**
335
+ * Copy allowlisted layout props verbatim from a JSX opening-tag fragment.
336
+ * Uses quote/brace matching so object literals and spaced strings stay intact.
337
+ *
338
+ * @param {string} tagText
339
+ * @returns {string[]}
340
+ */
341
+ function extractSpatialAttrs(tagText) {
342
+ const attrs = [];
343
+ for (const name of SPATIAL_PROPS) {
344
+ const eqMatch = tagText.match(new RegExp(`\\b${name}\\s*=\\s*`));
345
+ if (eqMatch) {
346
+ const start = eqMatch.index;
347
+ let i = eqMatch.index + eqMatch[0].length;
348
+ const rest = tagText.slice(i);
349
+
350
+ if (rest[0] === '"' || rest[0] === "'") {
351
+ const q = rest[0];
352
+ i += 1;
353
+ while (i < tagText.length && tagText[i] !== q) {
354
+ if (tagText[i] === '\\') i += 1;
355
+ i += 1;
356
+ }
357
+ if (i < tagText.length) i += 1;
358
+ } else if (rest[0] === '{') {
359
+ let depth = 0;
360
+ while (i < tagText.length) {
361
+ if (tagText[i] === '{') depth += 1;
362
+ else if (tagText[i] === '}') {
363
+ depth -= 1;
364
+ if (depth === 0) {
365
+ i += 1;
366
+ break;
367
+ }
368
+ }
369
+ i += 1;
370
+ }
371
+ } else {
372
+ const bare = rest.match(/^[^\s/>]+/);
373
+ i += bare ? bare[0].length : 0;
374
+ }
375
+
376
+ attrs.push(tagText.slice(start, i).trim());
377
+ continue;
378
+ }
379
+
380
+ if (new RegExp(`\\b${name}(?=[\\s/>])`).test(tagText)) {
381
+ attrs.push(name);
382
+ }
383
+ }
384
+ return attrs;
385
+ }
386
+
277
387
  function extractSkeleton(source) {
278
388
  const lines = source.split('\n');
279
389
  const out = [];
@@ -286,11 +396,18 @@ function extractSkeleton(source) {
286
396
  for (let i = 0; i < lines.length; i++) {
287
397
  const t = lines[i].trim();
288
398
 
289
- if (t.match(/^export\s+default\s+function/)) { inDefaultExport = true; continue; }
290
- if (inDefaultExport && t.match(/^return\s*\(/)) { capturing = true; continue; }
399
+ if (t.match(/^export\s+default\s+function/)) {
400
+ inDefaultExport = true;
401
+ continue;
402
+ }
403
+ if (inDefaultExport && t.match(/^return\s*\(/)) {
404
+ capturing = true;
405
+ continue;
406
+ }
291
407
  if (!capturing) continue;
292
408
  if (out.length >= MAX_LINES) {
293
- if (!out[out.length - 1]?.includes('...')) out.push(' '.repeat(depth) + '...');
409
+ if (!out[out.length - 1]?.includes('...'))
410
+ out.push(' '.repeat(depth) + '...');
294
411
  continue;
295
412
  }
296
413
 
@@ -308,20 +425,13 @@ function extractSkeleton(source) {
308
425
  if (lines[j].includes('>')) break;
309
426
  }
310
427
 
311
- const props = [];
312
- const propRegex = /\b(padding|contentPadding|gap|rowGap|columnGap|columns|minChildWidth|hasDivider|defaultHasDividers|variant|density|role|height|width|maxWidth)\s*[=]\s*\{?\s*['"]?([^}'"\s,/>]+)/g;
313
- let m;
314
- while ((m = propRegex.exec(tagText)) !== null) {
315
- const val = m[2];
316
- if (val === 'true') props.push(m[1]);
317
- else if (/^\d+$/.test(val)) props.push(`${m[1]}={${val}}`);
318
- else props.push(`${m[1]}="${val}"`);
319
- }
320
-
428
+ const props = extractSpatialAttrs(tagText);
321
429
  const hasSpatialProps = props.length > 0;
322
430
  const propStr = hasSpatialProps ? ' ' + props.join(' ') : '';
323
431
  const isVStack = comp === 'VStack' || comp === 'HStack';
324
- const isSelfClosing = tagText.match(new RegExp('<' + tagName + '[^>]*/>', 's'));
432
+ const isSelfClosing = tagText.match(
433
+ new RegExp('<' + tagName + '[^>]*/>', 's'),
434
+ );
325
435
 
326
436
  if (isVStack && !hasSpatialProps) continue;
327
437
 
@@ -348,13 +458,18 @@ function extractSkeleton(source) {
348
458
  continue;
349
459
  }
350
460
 
351
- const slotMatch = t.match(/^(header|content|footer|start|end|sideNav|topNav)\s*=\s*\{/);
461
+ const slotMatch = t.match(
462
+ /^(header|content|footer|start|end|sideNav|topNav)\s*=\s*\{/,
463
+ );
352
464
  if (slotMatch) {
353
465
  out.push(' '.repeat(depth) + `/* ${slotMatch[1]}: */`);
354
466
  continue;
355
467
  }
356
468
 
357
- if (t.startsWith('<div') && (t.includes('padding') || t.includes('maxWidth') || t.includes('gap:'))) {
469
+ if (
470
+ t.startsWith('<div') &&
471
+ (t.includes('padding') || t.includes('maxWidth') || t.includes('gap:'))
472
+ ) {
358
473
  const styleProps = [];
359
474
  const divText = lines.slice(i, Math.min(i + 5, lines.length)).join(' ');
360
475
  const pp = divText.match(/padding[^:]*:\s*['"]?([^'"},)]+)/);
@@ -394,7 +509,7 @@ export async function getTemplateById(id, options = {}) {
394
509
  'Add a template.get function to astryx.config.mjs:\n\n' +
395
510
  ' export default {\n' +
396
511
  ' template: {\n' +
397
- " get: async (id) => { /* return template source string */ },\n" +
512
+ ' get: async (id) => { /* return template source string */ },\n' +
398
513
  ' },\n' +
399
514
  ' };',
400
515
  undefined,
@@ -407,7 +522,11 @@ export async function getTemplateById(id, options = {}) {
407
522
  source = await getter(id);
408
523
  } catch (err) {
409
524
  const detail = err instanceof Error ? err.message : String(err);
410
- throw new AstryxError(`template.get("${id}") threw an error: ${detail}`, undefined, ERROR_CODES.ERR_TEMPLATE_GET);
525
+ throw new AstryxError(
526
+ `template.get("${id}") threw an error: ${detail}`,
527
+ undefined,
528
+ ERROR_CODES.ERR_TEMPLATE_GET,
529
+ );
411
530
  }
412
531
 
413
532
  if (source == null) {
@@ -449,7 +568,14 @@ export async function getTemplateById(id, options = {}) {
449
568
  * @returns {Promise<{type: string, data: unknown}>}
450
569
  */
451
570
  export async function template(name, options = {}) {
452
- const {list = false, skeleton = false, show = false, targetPath, type, cwd = process.cwd()} = options;
571
+ const {
572
+ list = false,
573
+ skeleton = false,
574
+ show = false,
575
+ targetPath,
576
+ type,
577
+ cwd = process.cwd(),
578
+ } = options;
453
579
  const templates = await discoverAll();
454
580
 
455
581
  if (list || (!name && !skeleton)) {
@@ -486,7 +612,11 @@ export async function template(name, options = {}) {
486
612
  );
487
613
  }
488
614
  if (!fs.existsSync(match.filePath)) {
489
- throw new AstryxError(`No source file found for template "${name}"`, undefined, ERROR_CODES.ERR_NO_SOURCE);
615
+ throw new AstryxError(
616
+ `No source file found for template "${name}"`,
617
+ undefined,
618
+ ERROR_CODES.ERR_NO_SOURCE,
619
+ );
490
620
  }
491
621
  const src = fs.readFileSync(match.filePath, 'utf-8');
492
622
  return {
@@ -501,7 +631,11 @@ export async function template(name, options = {}) {
501
631
  }
502
632
 
503
633
  if (!fs.existsSync(match.filePath)) {
504
- throw new AstryxError(`No source file found for template "${name}"`, undefined, ERROR_CODES.ERR_NO_SOURCE);
634
+ throw new AstryxError(
635
+ `No source file found for template "${name}"`,
636
+ undefined,
637
+ ERROR_CODES.ERR_NO_SOURCE,
638
+ );
505
639
  }
506
640
 
507
641
  if (show || !targetPath) {
@@ -528,7 +662,11 @@ export async function template(name, options = {}) {
528
662
  });
529
663
  } catch (err) {
530
664
  if (err instanceof PathSafetyError) {
531
- throw new AstryxError(err.message, undefined, ERROR_CODES.ERR_PATH_TRAVERSAL);
665
+ throw new AstryxError(
666
+ err.message,
667
+ undefined,
668
+ ERROR_CODES.ERR_PATH_TRAVERSAL,
669
+ );
532
670
  }
533
671
  throw err;
534
672
  }
@@ -545,9 +683,8 @@ export async function template(name, options = {}) {
545
683
  outputFilePath = resolvedTarget;
546
684
  } else {
547
685
  outputDir = resolvedTarget;
548
- outputFileName = match.type === 'block'
549
- ? path.basename(match.filePath)
550
- : 'page.tsx';
686
+ outputFileName =
687
+ match.type === 'block' ? path.basename(match.filePath) : 'page.tsx';
551
688
  outputFilePath = path.join(outputDir, outputFileName);
552
689
  }
553
690
 
@@ -562,6 +699,11 @@ export async function template(name, options = {}) {
562
699
  const relOutput = path.relative(cwd, outputDir) || '.';
563
700
  return {
564
701
  type: 'template.copy',
565
- data: {template: name, outputDir: relOutput, fileName: outputFileName, filesCopied: 1},
702
+ data: {
703
+ template: name,
704
+ outputDir: relOutput,
705
+ fileName: outputFileName,
706
+ filesCopied: 1,
707
+ },
566
708
  };
567
709
  }
@@ -74,5 +74,7 @@ describe('template --skeleton component extraction (prefix-agnostic)', () => {
74
74
  expect(result.data.skeleton.trim().length).toBeGreaterThan(0);
75
75
  expect(result.data.skeleton).toMatch(/<[A-Z]\w+/);
76
76
  expect(result.data.skeleton).not.toContain('<XDS');
77
+
78
+ expect(result.data.skeleton).toContain('columns={{minWidth: 200}}');
77
79
  });
78
80
  });
@@ -16,6 +16,7 @@ describe('registry', () => {
16
16
  '0.0.13',
17
17
  '0.0.14',
18
18
  '0.0.15',
19
+ '0.1.0',
19
20
  ]);
20
21
  });
21
22
  });
@@ -17,6 +17,7 @@ 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')],
20
21
  ]);
21
22
 
22
23
  // Re-export from the shared utility so registry callers and other consumers
@@ -11,7 +11,6 @@
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';
15
14
  import {humanLog} from '../lib/json.mjs';
16
15
 
17
16
  // Known corruption patterns that indicate a broken transform.
@@ -87,39 +86,6 @@ function findSourceFiles(dir) {
87
86
  return results.sort();
88
87
  }
89
88
 
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
-
123
89
  /**
124
90
  * Validate transform output before writing to disk.
125
91
  *
@@ -162,6 +128,86 @@ export function validateOutput(result, source, j, {parse = true} = {}) {
162
128
  return {valid: true};
163
129
  }
164
130
 
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
+
165
211
  /**
166
212
  * Run codemods against source files.
167
213
  *
@@ -197,18 +243,12 @@ export async function runCodemods(
197
243
 
198
244
  if (files.length === 0) {
199
245
  log.warn('No source files found.');
200
- return {
201
- ok: true,
202
- totalFilesChanged: 0,
203
- totalTransformsApplied: 0,
204
- errors: [],
205
- writtenFiles: [],
206
- skippedOptional: [],
207
- };
246
+ } else {
247
+ log.info(
248
+ `Found ${files.length} source file${files.length === 1 ? '' : 's'}`,
249
+ );
208
250
  }
209
251
 
210
- log.info(`Found ${files.length} source file${files.length === 1 ? '' : 's'}`);
211
-
212
252
  // Dynamically import jscodeshift
213
253
  const jscodeshift = (await import('jscodeshift')).default;
214
254
 
@@ -239,6 +279,24 @@ export async function runCodemods(
239
279
 
240
280
  log.info(` ${meta.title}`);
241
281
 
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
+
242
300
  let filesChanged = 0;
243
301
 
244
302
  for (const filePath of files) {
@@ -323,12 +381,6 @@ export async function runCodemods(
323
381
  }
324
382
  }
325
383
 
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
-
332
384
  if (totalValidationBlocked > 0) {
333
385
  log.warn(
334
386
  `${totalValidationBlocked} file${totalValidationBlocked === 1 ? ' was' : 's were'} blocked by validation — no changes written to ${totalValidationBlocked === 1 ? 'that file' : 'those files'}.`,
@@ -366,7 +418,9 @@ export async function runCodemods(
366
418
  if (meta.description) {
367
419
  log.info(` ${meta.description}`);
368
420
  }
369
- log.info(` Run: astryx upgrade --codemod ${name} --path <dir> --apply`);
421
+ log.info(
422
+ ` Run: astryx upgrade --codemod ${name} --path <dir> --apply`,
423
+ );
370
424
  }
371
425
  }
372
426