@astryxdesign/cli 0.1.2 → 0.1.3

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 (238) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +49 -55
  3. package/docs/layout.doc.dense.mjs +90 -0
  4. package/docs/layout.doc.mjs +160 -0
  5. package/docs/principles.doc.dense.mjs +2 -2
  6. package/docs/principles.doc.mjs +4 -0
  7. package/docs/principles.doc.zh.mjs +2 -2
  8. package/docs/theme.doc.mjs +2 -2
  9. package/package.json +13 -1
  10. package/src/api/blog.mjs +192 -0
  11. package/src/api/blog.test.mjs +144 -0
  12. package/src/api/component.mjs +253 -12
  13. package/src/api/discover.mjs +14 -7
  14. package/src/api/doctor.mjs +10 -25
  15. package/src/api/index.mjs +1 -0
  16. package/src/api/layout.mjs +11 -10
  17. package/src/api/layout.test.mjs +4 -1
  18. package/src/api/template-integration.test.mjs +225 -0
  19. package/src/api/template.mjs +242 -84
  20. package/src/api/validate-integration.mjs +370 -0
  21. package/src/api/validate-integration.test.mjs +222 -0
  22. package/src/codemod.mjs +93 -0
  23. package/src/codemod.test.mjs +134 -0
  24. package/src/codemods/__tests__/registry.test.mjs +1 -0
  25. package/src/codemods/__tests__/runner.test.mjs +103 -0
  26. package/src/codemods/integration-discovery.mjs +168 -0
  27. package/src/codemods/integration-discovery.test.mjs +234 -0
  28. package/src/codemods/integration-runner.mjs +109 -0
  29. package/src/codemods/registry.mjs +1 -0
  30. package/src/codemods/run-codemod.mjs +207 -0
  31. package/src/codemods/runner.mjs +73 -96
  32. package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
  33. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
  34. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
  35. package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +104 -0
  36. package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
  37. package/src/codemods/transforms/v0.1.0/index.mjs +43 -6
  38. package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
  39. package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
  40. package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
  41. package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
  42. package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
  43. package/src/commands/agent-docs.mjs +4 -1
  44. package/src/commands/blog.mjs +60 -0
  45. package/src/commands/build-theme.import-path.test.mjs +2 -12
  46. package/src/commands/build-theme.mjs +117 -0
  47. package/src/commands/build-theme.prose.test.mjs +5 -15
  48. package/src/commands/build-theme.watch.test.mjs +149 -0
  49. package/src/commands/component/index.mjs +44 -10
  50. package/src/commands/component-ownership.test.mjs +227 -0
  51. package/src/commands/discover.mjs +5 -16
  52. package/src/commands/doctor.test.mjs +3 -3
  53. package/src/commands/ensure-core-built.mjs +120 -0
  54. package/src/commands/json-contract.test.mjs +0 -32
  55. package/src/commands/swizzle.mjs +224 -235
  56. package/src/commands/swizzle.path-safety.test.mjs +3 -3
  57. package/src/commands/swizzle.routing.test.mjs +279 -0
  58. package/src/commands/template.mjs +33 -37
  59. package/src/commands/upgrade.config-ordering.test.mjs +273 -0
  60. package/src/commands/upgrade.integration-policy.test.mjs +204 -0
  61. package/src/commands/upgrade.mjs +349 -170
  62. package/src/commands/validate-integration.mjs +110 -0
  63. package/src/commands/validate-integration.test.mjs +124 -0
  64. package/src/config.mjs +7 -20
  65. package/src/config.test.mjs +89 -13
  66. package/src/index.mjs +8 -3
  67. package/src/integration.mjs +19 -0
  68. package/src/lib/component-discovery.mjs +177 -0
  69. package/src/lib/config-cache.mjs +91 -0
  70. package/src/lib/config-cache.test.mjs +83 -0
  71. package/src/lib/config-schema.mjs +41 -55
  72. package/src/lib/error-codes.mjs +15 -12
  73. package/src/lib/integration-warnings.mjs +62 -0
  74. package/src/lib/integration-warnings.test.mjs +102 -0
  75. package/src/lib/integrations.mjs +92 -115
  76. package/src/lib/integrations.test.mjs +88 -107
  77. package/src/lib/manifest.mjs +5 -2
  78. package/src/lib/module-loader.mjs +80 -0
  79. package/src/lib/module-loader.test.mjs +106 -0
  80. package/src/lib/project.mjs +502 -0
  81. package/src/lib/project.test.mjs +308 -0
  82. package/src/lib/site.mjs +20 -0
  83. package/src/template.mjs +73 -0
  84. package/src/template.test.mjs +127 -0
  85. package/src/types/api.d.ts +2 -6
  86. package/src/types/base.d.ts +3 -9
  87. package/src/types/codemod.d.ts +81 -0
  88. package/src/types/component.d.ts +27 -2
  89. package/src/types/config.d.ts +56 -85
  90. package/src/types/error-codes.d.ts +5 -3
  91. package/src/types/index.d.ts +0 -1
  92. package/src/types/integration.d.ts +29 -0
  93. package/src/types/swizzle.d.ts +9 -2
  94. package/src/types/template-api.d.ts +54 -0
  95. package/src/types/template.d.ts +10 -7
  96. package/src/types/upgrade.d.ts +29 -0
  97. package/src/types/validate-integration.d.ts +24 -0
  98. package/src/utils/github.mjs +0 -237
  99. package/src/utils/interactive.mjs +2 -2
  100. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
  101. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
  102. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
  103. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
  104. package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
  105. package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
  106. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
  107. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
  108. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
  109. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
  110. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
  111. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
  112. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
  113. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
  114. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
  115. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
  116. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
  117. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
  118. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
  119. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
  120. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
  121. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
  122. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
  123. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
  124. package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
  125. package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
  126. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
  127. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
  128. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
  129. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
  130. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
  131. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
  132. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
  133. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
  134. package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -6
  135. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
  136. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
  137. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -5
  138. package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
  139. package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
  140. package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
  141. package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
  142. package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
  143. package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
  144. package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
  145. package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
  146. package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
  147. package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
  148. package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.doc.mjs +14 -0
  149. package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx +57 -0
  150. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
  151. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
  152. package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
  153. package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
  154. package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
  155. package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
  156. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
  157. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
  158. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
  159. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
  160. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
  161. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
  162. package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
  163. package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
  164. package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
  165. package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
  166. package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
  167. package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
  168. package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
  169. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +1 -1
  170. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
  171. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
  172. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
  173. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
  174. package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -6
  175. package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
  176. package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
  177. package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
  178. package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
  179. package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
  180. package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
  181. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
  182. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
  183. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
  184. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
  185. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
  186. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
  187. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
  188. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
  189. package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
  190. package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
  191. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
  192. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
  193. package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
  194. package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
  195. package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
  196. package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
  197. package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
  198. package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
  199. package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +1 -1
  200. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
  201. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
  202. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
  203. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
  204. package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
  205. package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
  206. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
  207. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
  208. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
  209. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
  210. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
  211. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
  212. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
  213. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
  214. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
  215. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
  216. package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
  217. package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
  218. package/templates/pages/kanban-board/page.tsx +729 -0
  219. package/templates/pages/kanban-board/template.doc.mjs +12 -0
  220. package/templates/pages/shell-nav/page.tsx +321 -0
  221. package/templates/pages/shell-nav/template.doc.mjs +12 -0
  222. package/templates/pages/shell-side-nav/page.tsx +241 -0
  223. package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
  224. package/templates/pages/shell-top-nav/page.tsx +224 -0
  225. package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
  226. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
  227. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
  228. package/src/commands/gap-report.mjs +0 -464
  229. package/src/commands/gap-report.test.mjs +0 -168
  230. package/src/commands/swizzle-gap-safety.test.mjs +0 -273
  231. package/src/lib/config.mjs +0 -113
  232. package/src/lib/config.test.mjs +0 -91
  233. package/src/types/gap-report.d.ts +0 -29
  234. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
  235. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
  236. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
  237. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
  238. /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
@@ -15,13 +15,22 @@
15
15
  * 3. Refresh agent docs (AGENTS.md / CLAUDE.md) if present
16
16
  *
17
17
  * Options:
18
- * --from <version> Previous version before the dependency upgrade
19
- * --apply Write changes to disk (default: dry-run)
20
- * --force Run codemods even when from >= installed version
21
- * --codemod <name> Run a specific transform only
22
- * --integration <spec> Load an explicit integration package or file
23
- * --path <dir> Source directory (default: ./src)
24
- * --install-deps Auto-install jscodeshift without prompting (for CI/LLM)
18
+ * --from <version> Previous version before the dependency upgrade
19
+ * --apply Write changes to disk (default: dry-run)
20
+ * --force Run codemods even when from >= installed version
21
+ * --codemod <name> Run a specific transform only
22
+ * --skip-codemod <name…> Exclude named codemods (variadic). Use this to
23
+ * re-run past a codemod that failed at execution time.
24
+ * --integration <spec> Load an explicit integration package or file
25
+ * --path <dir> Source directory (default: ./src)
26
+ * --install-deps Auto-install jscodeshift without prompting (for CI/LLM)
27
+ *
28
+ * Integration error policy:
29
+ * - A DISCOVERY/definition error for an integration (bad manifest/export,
30
+ * duplicate ids, missing root) SKIPS that integration's codemods and warns
31
+ * (via the integration-issue nudge) — it does NOT hard-fail the upgrade.
32
+ * - An EXECUTION-time failure (a transform THROWS while rewriting files)
33
+ * ABORTS the upgrade (nonzero exit) so a partial write never proceeds.
25
34
  */
26
35
 
27
36
  import * as fs from 'node:fs';
@@ -32,12 +41,18 @@ import * as p from '@clack/prompts';
32
41
  import {ensureJscodeshift} from '../codemods/ensure-jscodeshift.mjs';
33
42
  import {getTransformsBetween, latestVersion} from '../codemods/registry.mjs';
34
43
  import {runCodemods} from '../codemods/runner.mjs';
44
+ import {
45
+ discoverIntegrationCodemods,
46
+ selectIntegrationCodemods,
47
+ } from '../codemods/integration-discovery.mjs';
48
+ import {runIntegrationCodemods} from '../codemods/integration-runner.mjs';
35
49
  import {installAgentDocs, discoverAgentDocs} from './agent-docs.mjs';
36
50
  import {getRunPrefix} from '../utils/package-manager.mjs';
37
- import {isValidSemver, semverGte, semverGt} from '../utils/semver.mjs';
51
+ import {isValidSemver, semverGte} from '../utils/semver.mjs';
38
52
  import {jsonOut, jsonError} from '../lib/json.mjs';
39
- import {loadConfig} from '../lib/config.mjs';
53
+ import {Project} from '../lib/project.mjs';
40
54
  import {loadIntegrations} from '../lib/integrations.mjs';
55
+ import {warnOnIntegrationIssues} from '../lib/integration-warnings.mjs';
41
56
  import {ERROR_CODES} from '../lib/error-codes.mjs';
42
57
 
43
58
  const execFileAsync = promisify(execFile);
@@ -64,90 +79,59 @@ function detectInstalledTargetVersion() {
64
79
  return null;
65
80
  }
66
81
 
67
- function normalizeIntegrationTransforms(integration, from, to) {
68
- const transforms = [];
69
- for (const entry of integration.codemods ?? []) {
70
- const entryFrom = entry.from ?? '0.0.0';
71
- const entryTo = entry.to ?? to;
72
- if (semverGte(from, entryTo) || semverGt(entryFrom, to)) continue;
73
- if (!entry.name)
74
- throw new Error(
75
- `Integration ${integration.name ?? integration.__spec} has a codemod without a name.`,
76
- );
77
- if (!entry.transform)
78
- throw new Error(
79
- `Integration codemod ${entry.name} is missing transform.`,
80
- );
81
- const directTransform =
82
- typeof entry.transform === 'function' ? entry.transform : null;
83
- if (!directTransform)
84
- throw new Error(
85
- `Integration codemod ${entry.name} did not resolve to a function.`,
86
- );
87
- transforms.push({
88
- name: entry.name,
89
- meta: {
90
- title:
91
- entry.title ??
92
- `${integration.name ?? integration.__spec}: ${entry.name}`,
93
- description: entry.description ?? '',
94
- pr: entry.pr,
95
- fileExtensions: entry.fileExtensions,
96
- },
97
- optional: !!entry.optional,
98
- transform: directTransform,
99
- });
100
- }
101
- return transforms.length ? [{version: to, transforms}] : [];
102
- }
103
-
104
82
  function uniqueFiles(files) {
105
83
  return [...new Set((files ?? []).filter(Boolean))];
106
84
  }
107
85
 
108
- async function runPostCodemodHooks(integrations, context, silent) {
109
- const hooks = integrations.flatMap(integration =>
110
- (integration.postCodemod ?? []).map(hook => ({integration, hook})),
111
- );
112
- if (hooks.length === 0) return;
86
+ /**
87
+ * Run the app config's post-codemod hooks (config.hooks.postCodemod).
88
+ *
89
+ * Each hook's `buildCommand({packageDir, files})` returns a command to run
90
+ * (or a nullish value to skip). In dry-run mode we only PREVIEW — buildCommand
91
+ * is called (so a throw still fails the run) but the command is never executed.
92
+ * In apply mode the commands run in order via execFile; a nonzero exit (or a
93
+ * buildCommand throw) fails the upgrade.
94
+ *
95
+ * @param {import('../types/config').PostCodemodHook[]} hooks
96
+ * @param {{packageDir: string, files: string[], apply: boolean}} context
97
+ * @param {boolean} silent
98
+ */
99
+ async function runPostCodemodHooks(hooks, context, silent) {
100
+ if (!hooks || hooks.length === 0) return;
113
101
 
114
102
  const log = silent ? {info() {}, warn() {}, success() {}, error() {}} : p.log;
103
+ const {packageDir, files, apply} = context;
115
104
 
116
- const run = async (command, args, options = {}) => {
117
- await execFileAsync(command, args, {
118
- cwd: options.cwd ?? context.packageDir,
119
- timeout: options.timeoutMs ?? 300_000,
105
+ for (let i = 0; i < hooks.length; i++) {
106
+ const hook = hooks[i];
107
+ const label = hook.name ?? `postCodemod[${i}]`;
108
+ if (typeof hook.buildCommand !== 'function') {
109
+ throw new Error(
110
+ `Post-codemod hook ${label} is missing a buildCommand function.`,
111
+ );
112
+ }
113
+
114
+ const cmd = await hook.buildCommand({packageDir, files});
115
+ if (!cmd) {
116
+ log.info(`Post-codemod hook ${label} produced no command; skipping.`);
117
+ continue;
118
+ }
119
+
120
+ if (!apply) {
121
+ const preview = [cmd.command, ...(cmd.args ?? [])].join(' ');
122
+ log.info(`Post-codemod hook ${label} (dry run): ${preview}`);
123
+ continue;
124
+ }
125
+
126
+ await execFileAsync(cmd.command, cmd.args ?? [], {
127
+ cwd: cmd.options?.cwd ?? packageDir,
128
+ timeout: cmd.options?.timeout ?? 300_000,
120
129
  stdio: 'pipe',
121
130
  encoding: 'utf-8',
122
- env: {...process.env, ...(options.env ?? {})},
131
+ ...cmd.options,
132
+ env: {...process.env, ...(cmd.options?.env ?? {})},
123
133
  });
124
- };
125
-
126
- const ctx = {...context, run};
127
- for (const {integration, hook} of hooks) {
128
- const label = `${integration.name ?? integration.__spec}:${hook.name ?? 'postCodemod'}`;
129
- try {
130
- if (typeof hook.run === 'function') {
131
- await hook.run(ctx);
132
- } else if (typeof hook.command === 'function') {
133
- const cmd = await hook.command(ctx);
134
- if (cmd) {
135
- await run(cmd.command, cmd.args ?? [], {
136
- cwd: cmd.cwd,
137
- timeoutMs: cmd.timeoutMs,
138
- env: cmd.env,
139
- });
140
- }
141
- } else {
142
- log.warn(
143
- `Integration hook ${label} has no run() or command() function; skipping.`,
144
- );
145
- continue;
146
- }
147
- log.success(`Post-codemod hook ${label} completed.`);
148
- } catch (err) {
149
- log.warn(`Post-codemod hook ${label} failed: ${err.message}`);
150
- }
134
+ log.success(`Post-codemod hook ${label} completed.`);
151
135
  }
152
136
  }
153
137
 
@@ -169,6 +153,10 @@ export function registerUpgrade(program) {
169
153
  false,
170
154
  )
171
155
  .option('--codemod <name>', 'Run a specific transform only')
156
+ .option(
157
+ '--skip-codemod <name...>',
158
+ 'Exclude named codemods (repeatable). Re-run past a failed codemod by skipping it.',
159
+ )
172
160
  .option(
173
161
  '--integration <package-or-file>',
174
162
  'Explicit integration package name or integration file path (repeatable)',
@@ -266,15 +254,170 @@ export function registerUpgrade(program) {
266
254
  );
267
255
  }
268
256
 
257
+ // ───────────────────────────────────────────────────────────────────
258
+ // PIPELINE ORDERING
259
+ //
260
+ // CORE codemods run BEFORE the consumer's config is loaded. `Project.load`
261
+ // STRICT-validates astryx.config.* and THROWS on unknown keys — but a core
262
+ // CONFIG codemod (e.g. v0.1.3 migrate-layout-components, signalled by
263
+ // `meta.codemodType === 'config'`) is precisely what repairs an otherwise
264
+ // -invalid config. Loading first created a chicken-and-egg: the config was
265
+ // rejected before the codemod that would fix it ever ran. Core codemods
266
+ // read files directly (config codemods via runConfigCodemod read
267
+ // astryx.config.*; code codemods scan --path), so they do NOT need the
268
+ // loaded config. We run them here, then load config, then sequence the
269
+ // integration codemods (which DO require a valid loaded config).
270
+ // ───────────────────────────────────────────────────────────────────
271
+
272
+ if (!options.force && semverGte(currentVersion, targetVersion)) {
273
+ if (json) {
274
+ return jsonOut('upgrade.status', {
275
+ status: 'up_to_date',
276
+ from: currentVersion,
277
+ to: targetVersion,
278
+ });
279
+ }
280
+ p.log.success('Already up to date — no codemods to run.');
281
+ p.log.info('Use --force to run codemods anyway.');
282
+ p.outro('Done');
283
+ return;
284
+ }
285
+
286
+ // Resolve CORE transforms from the registry. These do not need the loaded
287
+ // config. Integration codemods are discovered later, AFTER the config
288
+ // loads successfully (they require a valid config to resolve).
289
+ const versionManifests = [
290
+ ...(await getTransformsBetween(currentVersion, targetVersion)),
291
+ ];
292
+
293
+ // Does the selected core set include >=1 CONFIG codemod? A config codemod
294
+ // is the established convention `meta.codemodType === 'config'` (see
295
+ // `toUnifiedEntry` in runner.mjs). This drives the graceful dry-run catch
296
+ // around `Project.load` below: a fixable config error is only "expected"
297
+ // when a pending core config codemod would repair it.
298
+ const coreConfigCodemodNames = [];
299
+ for (const {transforms} of versionManifests) {
300
+ for (const t of transforms) {
301
+ if (options.codemod && t.name !== options.codemod) continue;
302
+ if (t.meta?.codemodType === 'config') {
303
+ coreConfigCodemodNames.push(t.name);
304
+ }
305
+ }
306
+ }
307
+ const hasCoreConfigCodemod = coreConfigCodemodNames.length > 0;
308
+
309
+ // Codemods explicitly excluded via --skip-codemod, matched by the same
310
+ // identifier the run loop uses (core transform `t.name`, integration
311
+ // codemod `c.id`). Lets a user re-run past a codemod that failed at
312
+ // execution time.
313
+ const skipCodemods = new Set(options.skipCodemod ?? []);
314
+
315
+ // Count CORE transforms (optional codemods only count when explicitly
316
+ // requested). Integration counts are added after discovery below.
317
+ let totalTransforms = 0;
318
+ let totalOptional = 0;
319
+ for (const {transforms} of versionManifests) {
320
+ for (const t of transforms) {
321
+ if (options.codemod && t.name !== options.codemod) continue;
322
+ if (skipCodemods.has(t.name)) continue;
323
+ if (t.optional && !options.codemod) {
324
+ totalOptional++;
325
+ } else {
326
+ totalTransforms++;
327
+ }
328
+ }
329
+ }
330
+
331
+ // Ensure jscodeshift is available before running any codemod.
332
+ const ready = await ensureJscodeshift({
333
+ installDeps: options.installDeps,
334
+ silent: json,
335
+ });
336
+ if (!ready) {
337
+ if (json)
338
+ return jsonError(
339
+ 'jscodeshift is required but could not be installed.',
340
+ undefined,
341
+ ERROR_CODES.ERR_DEP_MISSING,
342
+ );
343
+ p.outro('Aborted');
344
+ process.exitCode = 1;
345
+ return;
346
+ }
347
+
348
+ // STEP 3 — Run CORE codemods FIRST (before loading config). In --apply,
349
+ // core config codemods WRITE the repaired config to disk; in dry-run they
350
+ // only PREVIEW. This is what makes the v0.1.3 config codemod reachable on
351
+ // a config that the strict loader would otherwise reject.
352
+ const codemodResult = await runCodemods(versionManifests, {
353
+ apply: options.apply,
354
+ path: options.path,
355
+ codemod: options.codemod,
356
+ skipCodemods,
357
+ silent: json,
358
+ });
359
+
360
+ // STEP 4 — Load the consumer's config (STRICT validation; unchanged). On
361
+ // --apply this now sees the repaired config the core codemod just wrote.
362
+ // We need `hooks.postCodemod` and the configured integration specs from
363
+ // here. Wrap in a graceful dry-run catch (see below).
364
+ // Assigned inside the try below; every catch branch returns, so these
365
+ // are always set before any later read.
269
366
  let integrations;
367
+ let postCodemodHooks;
368
+ let integrationVersionGroups;
270
369
  try {
271
- const config = await loadConfig(process.cwd());
370
+ const project = await Project.load(process.cwd());
371
+ postCodemodHooks = project.config.hooks?.postCodemod ?? [];
272
372
  const integrationSpecs = uniqueFiles([
273
- ...(config.integrations ?? []),
373
+ ...(project.integrations ?? []),
274
374
  ...(options.integration ?? []),
275
375
  ]);
276
376
  integrations = await loadIntegrations(integrationSpecs);
277
377
  } catch (err) {
378
+ // GRACEFUL DRY-RUN CATCH. A config that fails strict validation is the
379
+ // EXPECTED, fixable case ONLY when we are in dry-run AND a pending core
380
+ // config codemod just PREVIEWED a change to the config — i.e. the very
381
+ // codemod that would repair it. (Merely having a config codemod in the
382
+ // range is not enough: it may be a no-op on this config, in which case
383
+ // the validation error is genuine and we must abort. A config codemod
384
+ // that THREW reports zero would-change files, so it also fails this
385
+ // gate and aborts below — preserving the strictness contract.) This is
386
+ // the reason this PR reorders the pipeline.
387
+ const codemodWouldFixConfig =
388
+ hasCoreConfigCodemod && (codemodResult?.totalFilesChanged ?? 0) > 0;
389
+ if (!options.apply && codemodWouldFixConfig) {
390
+ const codemodFlags = coreConfigCodemodNames
391
+ .map(name => `--codemod ${name}`)
392
+ .join(' ');
393
+ const suggestedCommand = `astryx upgrade --from ${currentVersion} ${codemodFlags} --apply`;
394
+ const guidance =
395
+ 'Your astryx.config currently fails strict validation, but a pending ' +
396
+ 'config codemod would repair it. This dry run previewed the fix without ' +
397
+ 'writing. Re-run with --apply to apply it, or run just the config codemod(s) ' +
398
+ 'now:';
399
+ if (json) {
400
+ return jsonOut('upgrade.status', {
401
+ status: 'config_fixable',
402
+ from: currentVersion,
403
+ to: targetVersion,
404
+ configError: err.message,
405
+ configCodemods: coreConfigCodemodNames,
406
+ suggestedCommand,
407
+ message: guidance,
408
+ note: 'Integrations are skipped in this preview; they will be processed on the --apply run.',
409
+ });
410
+ }
411
+ p.log.warn(guidance);
412
+ p.log.info(` ${suggestedCommand}`);
413
+ p.log.info(
414
+ 'Integrations are skipped in this preview; they will be processed on the --apply run.',
415
+ );
416
+ p.outro('Dry run complete');
417
+ return;
418
+ }
419
+ // Genuine config error (apply mode, OR dry-run with no pending core
420
+ // config codemod that would fix it): abort as before.
278
421
  if (json)
279
422
  return jsonError(
280
423
  err.message,
@@ -286,39 +429,71 @@ export function registerUpgrade(program) {
286
429
  process.exitCode = 1;
287
430
  return;
288
431
  }
432
+
289
433
  if (!json && integrations.length > 0) {
290
434
  p.log.info(
291
435
  `Integrations: ${integrations.map(i => i.name ?? i.__spec).join(', ')}`,
292
436
  );
293
437
  }
294
438
 
295
- if (!options.force && semverGte(currentVersion, targetVersion)) {
296
- if (json) {
297
- return jsonOut('upgrade.status', {
298
- status: 'up_to_date',
299
- from: currentVersion,
300
- to: targetVersion,
301
- });
439
+ // Non-blocking nudge: if any configured integration has validation
440
+ // issues, print one compact line to stderr pointing at
441
+ // validate-integration. Best-effort; suppressed in --json mode. This
442
+ // depends on integrations being loaded, so it lives after the successful
443
+ // config load (it is skipped on the graceful dry-run path above, where
444
+ // integrations were never loaded).
445
+ try {
446
+ await warnOnIntegrationIssues(integrations, {json});
447
+ } catch {
448
+ // Never let the nudge break the upgrade.
449
+ }
450
+
451
+ // STEP 5 — Discover + run INTEGRATION codemods (only reached on a
452
+ // successful config load). A broken integration (bad export, invalid
453
+ // schema, duplicate id, missing root) is a DEFINITION error: skip that
454
+ // integration's codemods and warn (via the nudge above), rather than
455
+ // hard-failing the upgrade. An EXECUTION-time failure (a transform
456
+ // throwing) is handled later by the codemod-error gate, which still
457
+ // aborts the upgrade.
458
+ const integrationCodemodsByVersion = new Map();
459
+ for (const integration of integrations) {
460
+ if (!integration?.codemods) continue;
461
+ try {
462
+ const byVersion = await discoverIntegrationCodemods([integration]);
463
+ for (const [version, list] of byVersion) {
464
+ const existing = integrationCodemodsByVersion.get(version);
465
+ if (existing) existing.push(...list);
466
+ else integrationCodemodsByVersion.set(version, [...list]);
467
+ }
468
+ } catch {
469
+ // Skip this integration's codemods; the validate-integration nudge
470
+ // above surfaces the underlying issue. Best-effort, non-blocking.
302
471
  }
303
- p.log.success('Already up to date — no codemods to run.');
304
- p.log.info('Use --force to run codemods anyway.');
305
- p.outro('Done');
306
- return;
307
472
  }
473
+ integrationVersionGroups = selectIntegrationCodemods(
474
+ integrationCodemodsByVersion,
475
+ currentVersion,
476
+ targetVersion,
477
+ );
478
+ const hasIntegrationCodemods = integrationVersionGroups.some(
479
+ g => g.codemods.length > 0,
480
+ );
308
481
 
309
- // Resolve transforms
310
- const versionManifests = [
311
- ...(await getTransformsBetween(currentVersion, targetVersion)),
312
- ...integrations.flatMap(integration =>
313
- normalizeIntegrationTransforms(
314
- integration,
315
- currentVersion,
316
- targetVersion,
317
- ),
318
- ),
319
- ];
482
+ // Add integration transforms to the run counts.
483
+ for (const {codemods} of integrationVersionGroups) {
484
+ for (const c of codemods) {
485
+ if (options.codemod && c.id !== options.codemod) continue;
486
+ if (skipCodemods.has(c.id)) continue;
487
+ if (c.codemod.isOptional && !options.codemod) {
488
+ totalOptional++;
489
+ } else {
490
+ totalTransforms++;
491
+ }
492
+ }
493
+ }
320
494
 
321
- if (versionManifests.length === 0) {
495
+ // No codemods at all for this range (neither core nor integration).
496
+ if (versionManifests.length === 0 && !hasIntegrationCodemods) {
322
497
  if (json) {
323
498
  return jsonOut('upgrade.status', {
324
499
  status: 'no_codemods',
@@ -331,20 +506,7 @@ export function registerUpgrade(program) {
331
506
  return;
332
507
  }
333
508
 
334
- // Count transforms (optional codemods only count when explicitly requested)
335
- let totalTransforms = 0;
336
- let totalOptional = 0;
337
- for (const {transforms} of versionManifests) {
338
- for (const t of transforms) {
339
- if (options.codemod && t.name !== options.codemod) continue;
340
- if (t.optional && !options.codemod) {
341
- totalOptional++;
342
- } else {
343
- totalTransforms++;
344
- }
345
- }
346
- }
347
-
509
+ // A named `--codemod` that matched nothing (across core + integration).
348
510
  if (totalTransforms === 0 && totalOptional === 0) {
349
511
  const msg = `Codemod "${options.codemod}" not found. Use --list to see available codemods.`;
350
512
  if (json)
@@ -373,54 +535,73 @@ export function registerUpgrade(program) {
373
535
  agentDocsRefreshed: false,
374
536
  };
375
537
 
376
- // Ensure jscodeshift is available
377
- const ready = await ensureJscodeshift({
378
- installDeps: options.installDeps,
379
- silent: json,
380
- });
381
- if (!ready) {
382
- if (json)
383
- return jsonError(
384
- 'jscodeshift is required but could not be installed.',
385
- undefined,
386
- ERROR_CODES.ERR_DEP_MISSING,
387
- );
388
- p.outro('Aborted');
389
- process.exitCode = 1;
390
- return;
538
+ // Run file-based integration codemods alongside the core registry
539
+ // codemods (config codemods first, then code codemods), ordered by
540
+ // version. Their results are merged into the receipt below.
541
+ let integrationResult = null;
542
+ if (hasIntegrationCodemods) {
543
+ if (!json) p.log.step('Applying integration codemods...');
544
+ const jscodeshift = (await import('jscodeshift')).default;
545
+ integrationResult = runIntegrationCodemods(integrationVersionGroups, {
546
+ apply: options.apply,
547
+ path: options.path,
548
+ codemod: options.codemod,
549
+ skipCodemods,
550
+ jscodeshift,
551
+ silent: json,
552
+ });
391
553
  }
392
554
 
393
- // Run codemods
394
- const codemodResult = await runCodemods(versionManifests, {
395
- apply: options.apply,
396
- path: options.path,
397
- codemod: options.codemod,
398
- silent: json,
399
- });
400
555
 
401
- if (options.apply && integrations.length > 0) {
402
- const codemodDir = path.resolve(options.path);
403
- const absoluteChangedFiles = uniqueFiles(
404
- codemodResult?.writtenFiles ?? [],
405
- );
406
- const changedFiles = absoluteChangedFiles.map(file =>
556
+ // Merge core + integration codemod results into a single accounting so
557
+ // hooks, receipts, and error gating see both.
558
+ const mergedFilesChanged =
559
+ (codemodResult?.totalFilesChanged ?? 0) +
560
+ (integrationResult?.totalFilesChanged ?? 0);
561
+ const mergedTransformsApplied =
562
+ (codemodResult?.totalTransformsApplied ?? 0) +
563
+ (integrationResult?.totalTransformsApplied ?? 0);
564
+ const mergedWrittenFiles = [
565
+ ...(codemodResult?.writtenFiles ?? []),
566
+ ...(integrationResult?.writtenFiles ?? []),
567
+ ];
568
+ const mergedErrors = [
569
+ ...(codemodResult?.errors ?? []),
570
+ ...(integrationResult?.errors ?? []),
571
+ ];
572
+
573
+ // Post-codemod hooks come from the app config (config.hooks.postCodemod).
574
+ // They run only when codemods actually changed files. In apply mode the
575
+ // commands execute (nonzero exit fails the upgrade); in dry-run mode we
576
+ // only preview the resolved command (a buildCommand throw still fails).
577
+ const changedFileCount = mergedFilesChanged;
578
+ if (postCodemodHooks.length > 0 && changedFileCount > 0) {
579
+ const absoluteChangedFiles = uniqueFiles(mergedWrittenFiles);
580
+ const files = absoluteChangedFiles.map(file =>
407
581
  path.relative(process.cwd(), file),
408
582
  );
409
- const packageChangedFiles = absoluteChangedFiles
410
- .filter(file => file.startsWith(process.cwd() + path.sep))
411
- .map(file => path.relative(process.cwd(), file));
412
- await runPostCodemodHooks(
413
- integrations,
414
- {
415
- packageDir: process.cwd(),
416
- codemodDir,
417
- changedFiles,
418
- absoluteChangedFiles,
419
- packageChangedFiles,
420
- apply: options.apply,
421
- },
422
- json,
423
- );
583
+ try {
584
+ await runPostCodemodHooks(
585
+ postCodemodHooks,
586
+ {
587
+ packageDir: process.cwd(),
588
+ files,
589
+ apply: options.apply,
590
+ },
591
+ json,
592
+ );
593
+ } catch (err) {
594
+ if (json)
595
+ return jsonError(
596
+ `Post-codemod hook failed: ${err.message}`,
597
+ {receipt},
598
+ ERROR_CODES.ERR_CODEMOD_FAILED,
599
+ );
600
+ p.log.error(`Post-codemod hook failed: ${err.message}`);
601
+ p.outro('Upgrade failed');
602
+ process.exitCode = 1;
603
+ return;
604
+ }
424
605
  }
425
606
 
426
607
  // Refresh agent docs if any exist (AGENTS.md, CLAUDE.md, .claude/CLAUDE.md, etc.)
@@ -444,11 +625,9 @@ export function registerUpgrade(program) {
444
625
  }
445
626
  }
446
627
 
447
- if (codemodResult && typeof codemodResult === 'object') {
448
- receipt.filesChanged = codemodResult.totalFilesChanged ?? 0;
449
- receipt.transformsApplied = codemodResult.totalTransformsApplied ?? 0;
450
- receipt.errors = codemodResult.errors ?? [];
451
- }
628
+ receipt.filesChanged = mergedFilesChanged;
629
+ receipt.transformsApplied = mergedTransformsApplied;
630
+ receipt.errors = mergedErrors;
452
631
 
453
632
  if (receipt.errors?.length > 0) {
454
633
  const msg = `Upgrade completed with ${receipt.errors.length} codemod error${receipt.errors.length === 1 ? '' : 's'}.`;