@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
@@ -3,122 +3,151 @@
3
3
  /**
4
4
  * @file upgrade command — Full version-to-version upgrade pipeline
5
5
  *
6
- * `astryx upgrade` detects the consumer's @astryxdesign/core version, bumps all
7
- * @astryxdesign/* dependencies, installs them, and runs codemods to migrate
8
- * breaking API changes.
6
+ * `astryx upgrade` runs codemods that migrate source code from a previous
7
+ * Astryx version to the currently installed version.
8
+ *
9
+ * Consumers should bump/install their Astryx packages first, then run:
10
+ * astryx upgrade --from <old-version> --path <source-dir> --apply
9
11
  *
10
12
  * Pipeline (--apply):
11
- * 1. Detect current version from package.json (or --from)
12
- * 2. Bump all @astryxdesign/* deps in package.json to --to version
13
- * 3. Run package manager install (yarn/npm/pnpm/bun)
14
- * 4. Run codemods for the version range
15
- * 5. Refresh agent docs (AGENTS.md / CLAUDE.md) if present
13
+ * 1. Read installed @astryxdesign/core (or legacy @xds/core) version
14
+ * 2. Run codemods for --from installed version
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
18
19
  * --apply Write changes to disk (default: dry-run)
19
- * --from <version> Previous version (overrides package.json detection)
20
- * --to <version> Target version (default: latest in registry)
21
- * --force Run codemods even if versions appear up to date
22
- * --codemod <name> Run a specific transform only (skips version check)
23
- * --codemod-only Skip version bump + install, run codemods only
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
24
23
  * --path <dir> Source directory (default: ./src)
25
24
  * --install-deps Auto-install jscodeshift without prompting (for CI/LLM)
26
25
  */
27
26
 
28
27
  import * as fs from 'node:fs';
29
28
  import * as path from 'node:path';
30
- import {execSync} from 'node:child_process';
29
+ import {execFile} from 'node:child_process';
30
+ import {promisify} from 'node:util';
31
31
  import * as p from '@clack/prompts';
32
32
  import {ensureJscodeshift} from '../codemods/ensure-jscodeshift.mjs';
33
- import {
34
- getTransformsBetween,
35
- latestVersion,
36
- } from '../codemods/registry.mjs';
33
+ import {getTransformsBetween, latestVersion} from '../codemods/registry.mjs';
37
34
  import {runCodemods} from '../codemods/runner.mjs';
38
35
  import {installAgentDocs, discoverAgentDocs} from './agent-docs.mjs';
39
- import {detectPackageManager, getRunPrefix} from '../utils/package-manager.mjs';
40
- import {isValidSemver, semverGte} from '../utils/semver.mjs';
36
+ import {getRunPrefix} from '../utils/package-manager.mjs';
37
+ import {isValidSemver, semverGte, semverGt} from '../utils/semver.mjs';
41
38
  import {jsonOut, jsonError} from '../lib/json.mjs';
39
+ import {loadConfig} from '../lib/config.mjs';
40
+ import {loadIntegrations} from '../lib/integrations.mjs';
42
41
  import {ERROR_CODES} from '../lib/error-codes.mjs';
43
42
 
44
- /**
45
- * Detect the installed @astryxdesign/core version from the consumer's package.json.
46
- * @returns {string|null}
47
- */
48
- function detectCurrentVersion() {
49
- const pkgPath = path.resolve(process.cwd(), 'package.json');
50
- try {
51
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
52
- const deps = {
53
- ...pkg.peerDependencies,
54
- ...pkg.dependencies,
55
- ...pkg.devDependencies,
56
- };
57
- const version = deps['@astryxdesign/core'];
58
- if (!version) return null;
59
- // Strip semver range chars (^, ~, >=, etc.)
60
- return version.replace(/^[^\d]*/, '');
61
- } catch {
62
- return null;
63
- }
64
- }
43
+ const execFileAsync = promisify(execFile);
65
44
 
66
45
  /**
67
- * Bump all @astryxdesign/* dependencies in the consumer's package.json to the target version.
68
- * Preserves the existing semver range prefix (^, ~, etc.).
69
- *
70
- * @param {string} targetVersion - Version to bump to (e.g. '0.0.5')
71
- * @returns {{bumped: string[], pkgPath: string}|null} List of bumped package names, or null if no package.json
46
+ * Detect the installed target version from node_modules.
47
+ * @returns {{version: string, packageName: string}|null}
72
48
  */
73
- function bumpXdsDeps(targetVersion) {
74
- const pkgPath = path.resolve(process.cwd(), 'package.json');
75
- if (!fs.existsSync(pkgPath)) return null;
76
-
77
- const raw = fs.readFileSync(pkgPath, 'utf-8');
78
- const pkg = JSON.parse(raw);
79
- const bumped = [];
80
-
81
- for (const depField of ['dependencies', 'devDependencies']) {
82
- const deps = pkg[depField];
83
- if (!deps) continue;
84
-
85
- for (const name of Object.keys(deps)) {
86
- if (!name.startsWith('@astryxdesign/')) continue;
87
-
88
- const current = deps[name];
89
- // Preserve range prefix (^, ~, >=, etc.)
90
- const prefix = current.match(/^([^\d]*)/)?.[1] ?? '^';
91
- const newRange = `${prefix}${targetVersion}`;
92
-
93
- if (current !== newRange) {
94
- deps[name] = newRange;
95
- bumped.push(name);
96
- }
49
+ function detectInstalledTargetVersion() {
50
+ for (const packageName of ['@astryxdesign/core', '@xds/core']) {
51
+ const pkgPath = path.resolve(
52
+ process.cwd(),
53
+ 'node_modules',
54
+ ...packageName.split('/'),
55
+ 'package.json',
56
+ );
57
+ try {
58
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
59
+ if (pkg.version) return {version: pkg.version, packageName};
60
+ } catch {
61
+ // Missing or unreadable package.json — try the next supported package name.
97
62
  }
98
63
  }
64
+ return null;
65
+ }
99
66
 
100
- if (bumped.length === 0) return {bumped: [], pkgPath};
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
+ }
101
103
 
102
- // Write back with same formatting (detect indent from original)
103
- const indent = raw.match(/^(\s+)"/m)?.[1] ?? ' ';
104
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + '\n');
105
- return {bumped, pkgPath};
104
+ function uniqueFiles(files) {
105
+ return [...new Set((files ?? []).filter(Boolean))];
106
106
  }
107
107
 
108
- /**
109
- * Get the install command for the detected package manager.
110
- * @param {boolean} force — pass --force to bust stale lockfile resolutions
111
- * @returns {string}
112
- */
113
- function getInstallCommand(force = false) {
114
- const pm = detectPackageManager();
115
- const forceFlag = force ? ' --force' : '';
116
- switch (pm) {
117
- case 'yarn': return `yarn install${forceFlag}`;
118
- case 'pnpm': return `pnpm install${force ? ' --force' : ''}`;
119
- case 'bun': return `bun install${force ? ' --force' : ''}`;
120
- case 'npm':
121
- default: return `npm install${force ? ' --force' : ''}`;
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;
113
+
114
+ const log = silent ? {info() {}, warn() {}, success() {}, error() {}} : p.log;
115
+
116
+ const run = async (command, args, options = {}) => {
117
+ await execFileAsync(command, args, {
118
+ cwd: options.cwd ?? context.packageDir,
119
+ timeout: options.timeoutMs ?? 300_000,
120
+ stdio: 'pipe',
121
+ encoding: 'utf-8',
122
+ env: {...process.env, ...(options.env ?? {})},
123
+ });
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
+ }
122
151
  }
123
152
  }
124
153
 
@@ -129,35 +158,50 @@ export function registerUpgrade(program) {
129
158
  program
130
159
  .command('upgrade')
131
160
  .description('Run codemods to migrate between versions')
161
+ .option(
162
+ '--from <version>',
163
+ 'Previous version before the dependency upgrade',
164
+ )
132
165
  .option('--apply', 'Write changes to disk (default: dry-run)', false)
133
- .option('--from <version>', 'Previous version (overrides package.json detection)')
134
- .option('--to <version>', 'Target version', latestVersion)
135
- .option('--force', 'Run codemods even if versions appear up to date', false)
166
+ .option(
167
+ '--force',
168
+ 'Run codemods even if --from is newer than the installed version',
169
+ false,
170
+ )
136
171
  .option('--codemod <name>', 'Run a specific transform only')
137
- .option('--codemod-only', 'Skip version bump and install, run codemods only', false)
138
- .option('--skip-install', 'Skip package manager install after bumping deps', false)
139
- .option('--force-install', 'Pass --force to package manager install (busts stale lockfile resolutions)', false)
172
+ .option(
173
+ '--integration <package-or-file>',
174
+ 'Explicit integration package name or integration file path (repeatable)',
175
+ (value, previous) => [...(previous ?? []), value],
176
+ [],
177
+ )
140
178
  .option('--path <dir>', 'Source directory to scan', './src')
141
- .option('--install-deps', 'Auto-install jscodeshift without prompting', false)
179
+ .option(
180
+ '--install-deps',
181
+ 'Auto-install jscodeshift without prompting',
182
+ false,
183
+ )
142
184
  .option('--list', 'List available codemods', false)
143
- .action(async (options) => {
185
+ .action(async options => {
144
186
  const json = program.opts().json || false;
145
187
  if (!json) p.intro('Upgrade');
146
188
 
147
- // Validate --to / --from upfront so callers don't silently accept
148
- // typos like `--to bogus` (which used to flow through getTransformsBetween
149
- // and just emit "no codemods available").
150
- if (options.to !== undefined && !isValidSemver(options.to)) {
151
- const msg = `Invalid --to value: "${options.to}". Expected a semver string like 0.0.10.`;
152
- if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_VERSION);
189
+ if (!options.list && !options.from) {
190
+ const msg =
191
+ 'Missing required --from. Install the target version first, then run `astryx upgrade --from <old-version>`.';
192
+ if (json)
193
+ return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_ARGUMENT);
153
194
  p.log.error(msg);
154
195
  p.outro('Aborted');
155
196
  process.exitCode = 1;
156
197
  return;
157
198
  }
158
- if (options.from !== undefined && !isValidSemver(options.from)) {
199
+
200
+ // Validate --from upfront so callers don't silently accept typos.
201
+ if (!options.list && !isValidSemver(options.from)) {
159
202
  const msg = `Invalid --from value: "${options.from}". Expected a semver string like 0.0.5.`;
160
- if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_VERSION);
203
+ if (json)
204
+ return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_VERSION);
161
205
  p.log.error(msg);
162
206
  p.outro('Aborted');
163
207
  process.exitCode = 1;
@@ -172,72 +216,113 @@ export function registerUpgrade(program) {
172
216
  const manifests = await getTransformsBetween('0.0.0', latestVersion);
173
217
  for (const {version, transforms} of manifests) {
174
218
  for (const {name, meta, optional} of transforms) {
175
- codemods.push({name, title: meta.title, version, pr: meta.pr, optional: !!optional});
219
+ codemods.push({
220
+ name,
221
+ title: meta.title,
222
+ version,
223
+ pr: meta.pr,
224
+ optional: !!optional,
225
+ });
176
226
  }
177
227
  }
178
- if (json) return jsonOut('upgrade.list', codemods.map(({name, title, version, optional}) => ({name, title, version, optional})));
228
+ if (json)
229
+ return jsonOut(
230
+ 'upgrade.list',
231
+ codemods.map(({name, title, version, optional}) => ({
232
+ name,
233
+ title,
234
+ version,
235
+ optional,
236
+ })),
237
+ );
179
238
  p.log.step('Available codemods:');
180
239
  for (const {name, title, pr, optional} of codemods) {
181
- p.log.info(` ${name} — ${title}${optional ? ' (optional)' : ''} (${pr})`);
240
+ p.log.info(
241
+ ` ${name} — ${title}${optional ? ' (optional)' : ''} (${pr})`,
242
+ );
182
243
  }
183
244
  p.outro('Done');
184
245
  return;
185
246
  }
186
247
 
187
- // When --codemod is specified, skip version detection entirely —
188
- // the user asked for a specific transform, just run it.
189
- const skipVersionCheck = !!options.codemod;
190
-
191
- // --codemod-only skips version bump + install but still uses --from/--to
192
- // for codemod resolution. Useful for canary testing or running codemods
193
- // independently of dependency changes.
194
- const skipBump = options.codemodOnly || skipVersionCheck;
195
-
196
- // Detect current version (--from overrides package.json)
197
- const currentVersion = options.from ?? detectCurrentVersion();
198
- if (!currentVersion && !skipVersionCheck) {
199
- const msg = 'Could not detect @astryxdesign/core version. Make sure package.json is in the current directory, or use --from <version>.';
200
- if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_VERSION_DETECT);
248
+ const currentVersion = options.from;
249
+ const installed = detectInstalledTargetVersion();
250
+ if (!installed) {
251
+ const msg =
252
+ 'Could not find installed @astryxdesign/core (or legacy @xds/core). Install the target version first, then rerun `astryx upgrade --from <old-version>`.';
253
+ if (json)
254
+ return jsonError(msg, undefined, ERROR_CODES.ERR_VERSION_DETECT);
201
255
  p.log.error(msg);
202
256
  p.outro('Aborted');
203
257
  process.exitCode = 1;
204
258
  return;
205
259
  }
260
+ const targetVersion = installed.version;
206
261
 
207
- const targetVersion = options.to;
262
+ if (!json) {
263
+ p.log.info(`From version: ${currentVersion}`);
264
+ p.log.info(
265
+ `Installed target: ${targetVersion} (${installed.packageName})`,
266
+ );
267
+ }
208
268
 
209
- if (!skipVersionCheck) {
210
- if (!json) {
211
- p.log.info(`Current version: ${currentVersion}`);
212
- p.log.info(`Target version: ${targetVersion}`);
213
- }
269
+ let integrations;
270
+ try {
271
+ const config = await loadConfig(process.cwd());
272
+ const integrationSpecs = uniqueFiles([
273
+ ...(config.integrations ?? []),
274
+ ...(options.integration ?? []),
275
+ ]);
276
+ integrations = await loadIntegrations(integrationSpecs);
277
+ } catch (err) {
278
+ if (json)
279
+ return jsonError(
280
+ err.message,
281
+ undefined,
282
+ ERROR_CODES.ERR_INVALID_ARGUMENT,
283
+ );
284
+ p.log.error(err.message);
285
+ p.outro('Aborted');
286
+ process.exitCode = 1;
287
+ return;
288
+ }
289
+ if (!json && integrations.length > 0) {
290
+ p.log.info(
291
+ `Integrations: ${integrations.map(i => i.name ?? i.__spec).join(', ')}`,
292
+ );
293
+ }
214
294
 
215
- if (!options.force && semverGte(currentVersion, targetVersion)) {
216
- if (json) {
217
- return jsonOut('upgrade.status', {
218
- status: 'up_to_date',
219
- from: currentVersion,
220
- to: targetVersion,
221
- });
222
- }
223
- p.log.success('Already up to date — no codemods to run.');
224
- p.log.info('Use --force to run codemods anyway, or --from <version> to specify the previous version.');
225
- p.outro('Done');
226
- return;
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
+ });
227
302
  }
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;
228
307
  }
229
308
 
230
309
  // Resolve transforms
231
- const versionManifests = await getTransformsBetween(
232
- skipVersionCheck ? '0.0.0' : currentVersion,
233
- targetVersion,
234
- );
310
+ const versionManifests = [
311
+ ...(await getTransformsBetween(currentVersion, targetVersion)),
312
+ ...integrations.flatMap(integration =>
313
+ normalizeIntegrationTransforms(
314
+ integration,
315
+ currentVersion,
316
+ targetVersion,
317
+ ),
318
+ ),
319
+ ];
235
320
 
236
321
  if (versionManifests.length === 0) {
237
322
  if (json) {
238
323
  return jsonOut('upgrade.status', {
239
324
  status: 'no_codemods',
240
- from: skipVersionCheck ? null : currentVersion,
325
+ from: currentVersion,
241
326
  to: targetVersion,
242
327
  });
243
328
  }
@@ -262,7 +347,8 @@ export function registerUpgrade(program) {
262
347
 
263
348
  if (totalTransforms === 0 && totalOptional === 0) {
264
349
  const msg = `Codemod "${options.codemod}" not found. Use --list to see available codemods.`;
265
- if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_UNKNOWN_CODEMOD);
350
+ if (json)
351
+ return jsonError(msg, undefined, ERROR_CODES.ERR_UNKNOWN_CODEMOD);
266
352
  p.log.error(msg);
267
353
  p.outro('Aborted');
268
354
  process.exitCode = 1;
@@ -279,34 +365,26 @@ export function registerUpgrade(program) {
279
365
  }
280
366
  }
281
367
 
282
- const receipt = {from: currentVersion, to: targetVersion, codemods: totalTransforms, depsUpdated: [], agentDocsRefreshed: false};
283
-
284
- // Bump @astryxdesign/* deps and install before running codemods
285
- if (options.apply && !skipBump) {
286
- const result = bumpXdsDeps(targetVersion);
287
- if (result && result.bumped.length > 0) {
288
- receipt.depsUpdated = result.bumped;
289
- if (!json) p.log.info(`Bumped ${result.bumped.join(', ')} → ${targetVersion}`);
290
-
291
- const installCmd = getInstallCommand(options.forceInstall);
292
- if (options.skipInstall) {
293
- if (!json) p.log.info('Skipping install (--skip-install). Run your package manager manually.');
294
- } else {
295
- if (!json) p.log.step(`Running ${installCmd}...`);
296
- try {
297
- execSync(installCmd, {stdio: 'inherit', cwd: process.cwd()});
298
- if (!json) p.log.success('Dependencies installed.');
299
- } catch {
300
- if (!json) p.log.warn('Install failed — codemods will still run against existing code.');
301
- }
302
- }
303
- }
304
- }
368
+ const receipt = {
369
+ from: currentVersion,
370
+ to: targetVersion,
371
+ codemods: totalTransforms,
372
+ integrations: integrations.map(i => i.name ?? i.__spec),
373
+ agentDocsRefreshed: false,
374
+ };
305
375
 
306
376
  // Ensure jscodeshift is available
307
- const ready = await ensureJscodeshift({installDeps: options.installDeps, silent: json});
377
+ const ready = await ensureJscodeshift({
378
+ installDeps: options.installDeps,
379
+ silent: json,
380
+ });
308
381
  if (!ready) {
309
- if (json) return jsonError('jscodeshift is required but could not be installed.', undefined, ERROR_CODES.ERR_DEP_MISSING);
382
+ if (json)
383
+ return jsonError(
384
+ 'jscodeshift is required but could not be installed.',
385
+ undefined,
386
+ ERROR_CODES.ERR_DEP_MISSING,
387
+ );
310
388
  p.outro('Aborted');
311
389
  process.exitCode = 1;
312
390
  return;
@@ -320,6 +398,31 @@ export function registerUpgrade(program) {
320
398
  silent: json,
321
399
  });
322
400
 
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 =>
407
+ path.relative(process.cwd(), file),
408
+ );
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
+ );
424
+ }
425
+
323
426
  // Refresh agent docs if any exist (AGENTS.md, CLAUDE.md, .claude/CLAUDE.md, etc.)
324
427
  // Always update after --apply; also update during dry-run if files exist,
325
428
  // since the index reflects the installed CLI version, not the codemods.
@@ -330,7 +433,8 @@ export function registerUpgrade(program) {
330
433
  // Don't inject into files that never had Astryx content.
331
434
  const written = installAgentDocs(process.cwd(), {onlyReplace: true});
332
435
  receipt.agentDocsRefreshed = written.length > 0;
333
- if (!json && written.length > 0) p.log.success(`Agent docs updated: ${written.join(', ')}`);
436
+ if (!json && written.length > 0)
437
+ p.log.success(`Agent docs updated: ${written.join(', ')}`);
334
438
  } catch {
335
439
  if (!json) {
336
440
  p.log.warn(
@@ -340,12 +444,23 @@ export function registerUpgrade(program) {
340
444
  }
341
445
  }
342
446
 
343
- if (json) {
344
- if (codemodResult && typeof codemodResult === 'object') {
345
- receipt.filesChanged = codemodResult.totalFilesChanged ?? 0;
346
- receipt.transformsApplied = codemodResult.totalTransformsApplied ?? 0;
347
- receipt.errors = codemodResult.errors ?? [];
447
+ if (codemodResult && typeof codemodResult === 'object') {
448
+ receipt.filesChanged = codemodResult.totalFilesChanged ?? 0;
449
+ receipt.transformsApplied = codemodResult.totalTransformsApplied ?? 0;
450
+ receipt.errors = codemodResult.errors ?? [];
451
+ }
452
+
453
+ if (receipt.errors?.length > 0) {
454
+ const msg = `Upgrade completed with ${receipt.errors.length} codemod error${receipt.errors.length === 1 ? '' : 's'}.`;
455
+ if (json) {
456
+ return jsonError(msg, {receipt}, ERROR_CODES.ERR_CODEMOD_FAILED);
348
457
  }
458
+ p.outro('Upgrade failed');
459
+ process.exitCode = 1;
460
+ return;
461
+ }
462
+
463
+ if (json) {
349
464
  return jsonOut('upgrade.run', receipt);
350
465
  }
351
466
  p.outro(options.apply ? 'Upgrade complete' : 'Dry run complete');