@astryxdesign/cli 0.1.0-canary.f94dd07 → 0.1.1-canary.a514b99

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 (137) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +117 -75
  3. package/bin/astryx.mjs +22 -7
  4. package/docs/icons.doc.mjs +1 -1
  5. package/docs/migration.doc.mjs +2 -2
  6. package/docs/shape.doc.mjs +1 -1
  7. package/docs/styling.doc.mjs +2 -2
  8. package/docs/theme.doc.dense.mjs +2 -2
  9. package/docs/theme.doc.mjs +14 -0
  10. package/docs/theme.doc.zh.mjs +2 -2
  11. package/docs/working-with-ai.doc.mjs +4 -4
  12. package/package.json +8 -8
  13. package/src/api/search.mjs +207 -13
  14. package/src/api/template.mjs +2 -1
  15. package/src/codemods/__tests__/registry.test.mjs +1 -0
  16. package/src/codemods/registry.mjs +1 -0
  17. package/src/codemods/runner.mjs +105 -51
  18. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
  19. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
  20. package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
  21. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
  22. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
  23. package/src/commands/agent-docs.mjs +92 -56
  24. package/src/commands/agent-docs.path-safety.test.mjs +1 -1
  25. package/src/commands/agent-docs.test.mjs +66 -10
  26. package/src/commands/build-theme.import-path.test.mjs +1 -1
  27. package/src/commands/build-theme.path-safety.test.mjs +1 -1
  28. package/src/commands/build-theme.prose.test.mjs +1 -1
  29. package/src/commands/build.mjs +196 -0
  30. package/src/commands/component-package.test.mjs +1 -1
  31. package/src/commands/component.test.mjs +1 -1
  32. package/src/commands/docs.test.mjs +1 -1
  33. package/src/commands/doctor.test.mjs +1 -1
  34. package/src/commands/external-showcase.test.mjs +1 -1
  35. package/src/commands/init.mjs +9 -1
  36. package/src/commands/interactive-guard.test.mjs +1 -1
  37. package/src/commands/json-contract.test.mjs +10 -3
  38. package/src/commands/swizzle-gap-safety.test.mjs +1 -1
  39. package/src/commands/swizzle.path-safety.test.mjs +1 -1
  40. package/src/commands/template.path-safety.test.mjs +1 -1
  41. package/src/commands/template.test.mjs +1 -1
  42. package/src/commands/upgrade.mjs +353 -169
  43. package/src/commands/upgrade.test.mjs +41 -27
  44. package/src/index.mjs +1 -0
  45. package/src/lib/config.mjs +12 -0
  46. package/src/lib/config.test.mjs +42 -0
  47. package/src/lib/error-codes.mjs +3 -0
  48. package/src/types/error-codes.d.ts +1 -0
  49. package/src/utils/interactive.mjs +1 -1
  50. package/src/utils/interactive.test.mjs +2 -0
  51. package/src/utils/package-manager.test.mjs +1 -1
  52. package/src/utils/path-safety.test.mjs +1 -1
  53. package/src/utils/paths.test.mjs +8 -8
  54. package/src/utils/update-check.mjs +4 -26
  55. package/src/utils/update-check.test.mjs +2 -64
  56. package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
  57. package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
  58. package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
  59. package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
  60. package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
  61. package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
  62. package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
  63. package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
  64. package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
  65. package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
  66. package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
  67. package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
  68. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
  69. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
  70. package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
  71. package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
  72. package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
  73. package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
  74. package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
  75. package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
  76. package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
  77. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
  78. package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
  79. package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
  80. package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
  81. package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
  82. package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
  83. package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
  84. package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
  85. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
  86. package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
  87. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
  88. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +2 -12
  89. package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
  90. package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
  91. package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
  92. package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
  93. package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
  94. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
  95. package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
  96. package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
  97. package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
  98. package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
  99. package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
  100. package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
  101. package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
  102. package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
  103. package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
  104. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
  105. package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
  106. package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
  107. package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
  108. package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
  109. package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
  110. package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
  111. package/templates/pages/ai-chat/page.tsx +71 -64
  112. package/templates/pages/ai-chat-landing/page.tsx +8 -12
  113. package/templates/pages/centered-hero/page.tsx +13 -15
  114. package/templates/pages/classic-gallery/page.tsx +27 -34
  115. package/templates/pages/detail-page/page.tsx +18 -18
  116. package/templates/pages/documentation/page.tsx +11 -14
  117. package/templates/pages/documentation-design/page.tsx +10 -13
  118. package/templates/pages/documentation-technical/page.tsx +15 -16
  119. package/templates/pages/editor/page.tsx +42 -54
  120. package/templates/pages/file-explorer/page.tsx +13 -16
  121. package/templates/pages/form-two-column/page.tsx +13 -17
  122. package/templates/pages/gallery-hero/page.tsx +13 -15
  123. package/templates/pages/ide/page.tsx +32 -39
  124. package/templates/pages/library/page.tsx +16 -23
  125. package/templates/pages/login/page.tsx +14 -18
  126. package/templates/pages/login-card/page.tsx +14 -18
  127. package/templates/pages/login-split/page.tsx +50 -48
  128. package/templates/pages/login-sso/page.tsx +9 -13
  129. package/templates/pages/mixed-gallery/page.tsx +51 -45
  130. package/templates/pages/payment-form/page.tsx +56 -70
  131. package/templates/pages/product-detail/page.tsx +27 -33
  132. package/templates/pages/product-gallery/page.tsx +7 -13
  133. package/templates/pages/settings-dialog/page.tsx +35 -43
  134. package/templates/pages/settings-sidebar/page.tsx +39 -47
  135. package/templates/pages/side-gallery/page.tsx +6 -9
  136. package/templates/pages/table-grouped/page.tsx +11 -15
  137. package/templates/pages/theme-showcase/page.tsx +33 -37
@@ -3,122 +3,220 @@
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 {pathToFileURL} from 'node:url';
30
+ import {execFile} from 'node:child_process';
31
+ import {promisify} from 'node:util';
31
32
  import * as p from '@clack/prompts';
32
33
  import {ensureJscodeshift} from '../codemods/ensure-jscodeshift.mjs';
33
- import {
34
- getTransformsBetween,
35
- latestVersion,
36
- } from '../codemods/registry.mjs';
34
+ import {getTransformsBetween, latestVersion} from '../codemods/registry.mjs';
37
35
  import {runCodemods} from '../codemods/runner.mjs';
38
36
  import {installAgentDocs, discoverAgentDocs} from './agent-docs.mjs';
39
- import {detectPackageManager, getRunPrefix} from '../utils/package-manager.mjs';
40
- import {isValidSemver, semverGte} from '../utils/semver.mjs';
37
+ import {getRunPrefix} from '../utils/package-manager.mjs';
38
+ import {isValidSemver, semverGte, semverGt} from '../utils/semver.mjs';
41
39
  import {jsonOut, jsonError} from '../lib/json.mjs';
40
+ import {loadConfig} from '../lib/config.mjs';
42
41
  import {ERROR_CODES} from '../lib/error-codes.mjs';
43
42
 
43
+ const execFileAsync = promisify(execFile);
44
+
44
45
  /**
45
- * Detect the installed @astryxdesign/core version from the consumer's package.json.
46
- * @returns {string|null}
46
+ * Detect the installed target version from node_modules.
47
+ * @returns {{version: string, packageName: string}|null}
47
48
  */
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;
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.
62
+ }
63
63
  }
64
+ return null;
64
65
  }
65
66
 
66
- /**
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
72
- */
73
- function bumpXdsDeps(targetVersion) {
74
- const pkgPath = path.resolve(process.cwd(), 'package.json');
75
- if (!fs.existsSync(pkgPath)) return null;
67
+ function isPathSpec(spec) {
68
+ return (
69
+ spec.startsWith('.') ||
70
+ spec.startsWith('/') ||
71
+ spec.endsWith('.mjs') ||
72
+ spec.endsWith('.js')
73
+ );
74
+ }
76
75
 
77
- const raw = fs.readFileSync(pkgPath, 'utf-8');
78
- const pkg = JSON.parse(raw);
79
- const bumped = [];
76
+ function resolvePackageDir(packageName) {
77
+ const parts = packageName.split('/');
78
+ return path.resolve(process.cwd(), 'node_modules', ...parts);
79
+ }
80
80
 
81
- for (const depField of ['dependencies', 'devDependencies']) {
82
- const deps = pkg[depField];
83
- if (!deps) continue;
81
+ function resolveIntegrationFile(spec) {
82
+ if (isPathSpec(spec)) {
83
+ return path.resolve(process.cwd(), spec);
84
+ }
84
85
 
85
- for (const name of Object.keys(deps)) {
86
- if (!name.startsWith('@astryxdesign/')) continue;
86
+ const packageDir = resolvePackageDir(spec);
87
+ const pkgPath = path.join(packageDir, 'package.json');
88
+ let pkg;
89
+ try {
90
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
91
+ } catch {
92
+ throw new Error(
93
+ `Could not find installed integration package "${spec}" at ${pkgPath}. Install it first or pass a direct integration file path.`,
94
+ );
95
+ }
87
96
 
88
- const current = deps[name];
89
- // Preserve range prefix (^, ~, >=, etc.)
90
- const prefix = current.match(/^([^\d]*)/)?.[1] ?? '^';
91
- const newRange = `${prefix}${targetVersion}`;
97
+ const manifestPath = pkg.astryx?.integration ?? pkg.xds?.integration;
98
+ if (!manifestPath) {
99
+ throw new Error(
100
+ `Package "${spec}" does not declare astryx.integration (or legacy xds.integration) in package.json.`,
101
+ );
102
+ }
103
+ return path.resolve(packageDir, manifestPath);
104
+ }
92
105
 
93
- if (current !== newRange) {
94
- deps[name] = newRange;
95
- bumped.push(name);
106
+ async function loadIntegrations(specs) {
107
+ const integrations = [];
108
+ for (const spec of specs) {
109
+ const file = resolveIntegrationFile(spec);
110
+ const mod = await import(pathToFileURL(file).href);
111
+ const integration = mod.default ?? mod.integration ?? mod;
112
+ if (!integration || typeof integration !== 'object') {
113
+ throw new Error(`Integration ${spec} did not export an object.`);
114
+ }
115
+ const integrationDir = path.dirname(file);
116
+ if (Array.isArray(integration.codemods)) {
117
+ for (const codemod of integration.codemods) {
118
+ if (typeof codemod.transform === 'string') {
119
+ const transformPath = path.resolve(integrationDir, codemod.transform);
120
+ const transformMod = await import(pathToFileURL(transformPath).href);
121
+ codemod.transform =
122
+ transformMod.default ?? transformMod.transform ?? transformMod;
123
+ }
96
124
  }
97
125
  }
126
+ integrations.push({
127
+ ...integration,
128
+ __file: file,
129
+ __dir: integrationDir,
130
+ __spec: spec,
131
+ });
98
132
  }
133
+ return integrations;
134
+ }
99
135
 
100
- if (bumped.length === 0) return {bumped: [], pkgPath};
136
+ function normalizeIntegrationTransforms(integration, from, to) {
137
+ const transforms = [];
138
+ for (const entry of integration.codemods ?? []) {
139
+ const entryFrom = entry.from ?? '0.0.0';
140
+ const entryTo = entry.to ?? to;
141
+ if (semverGte(from, entryTo) || semverGt(entryFrom, to)) continue;
142
+ if (!entry.name)
143
+ throw new Error(
144
+ `Integration ${integration.name ?? integration.__spec} has a codemod without a name.`,
145
+ );
146
+ if (!entry.transform)
147
+ throw new Error(
148
+ `Integration codemod ${entry.name} is missing transform.`,
149
+ );
150
+ const directTransform =
151
+ typeof entry.transform === 'function' ? entry.transform : null;
152
+ if (!directTransform)
153
+ throw new Error(
154
+ `Integration codemod ${entry.name} did not resolve to a function.`,
155
+ );
156
+ transforms.push({
157
+ name: entry.name,
158
+ meta: {
159
+ title:
160
+ entry.title ??
161
+ `${integration.name ?? integration.__spec}: ${entry.name}`,
162
+ description: entry.description ?? '',
163
+ pr: entry.pr,
164
+ fileExtensions: entry.fileExtensions,
165
+ },
166
+ optional: !!entry.optional,
167
+ transform: directTransform,
168
+ });
169
+ }
170
+ return transforms.length ? [{version: to, transforms}] : [];
171
+ }
101
172
 
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};
173
+ function uniqueFiles(files) {
174
+ return [...new Set((files ?? []).filter(Boolean))];
106
175
  }
107
176
 
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' : ''}`;
177
+ async function runPostCodemodHooks(integrations, context, silent) {
178
+ const hooks = integrations.flatMap(integration =>
179
+ (integration.postCodemod ?? []).map(hook => ({integration, hook})),
180
+ );
181
+ if (hooks.length === 0) return;
182
+
183
+ const log = silent ? {info() {}, warn() {}, success() {}, error() {}} : p.log;
184
+
185
+ const run = async (command, args, options = {}) => {
186
+ await execFileAsync(command, args, {
187
+ cwd: options.cwd ?? context.packageDir,
188
+ timeout: options.timeoutMs ?? 300_000,
189
+ stdio: 'pipe',
190
+ encoding: 'utf-8',
191
+ env: {...process.env, ...(options.env ?? {})},
192
+ });
193
+ };
194
+
195
+ const ctx = {...context, run};
196
+ for (const {integration, hook} of hooks) {
197
+ const label = `${integration.name ?? integration.__spec}:${hook.name ?? 'postCodemod'}`;
198
+ try {
199
+ if (typeof hook.run === 'function') {
200
+ await hook.run(ctx);
201
+ } else if (typeof hook.command === 'function') {
202
+ const cmd = await hook.command(ctx);
203
+ if (cmd) {
204
+ await run(cmd.command, cmd.args ?? [], {
205
+ cwd: cmd.cwd,
206
+ timeoutMs: cmd.timeoutMs,
207
+ env: cmd.env,
208
+ });
209
+ }
210
+ } else {
211
+ log.warn(
212
+ `Integration hook ${label} has no run() or command() function; skipping.`,
213
+ );
214
+ continue;
215
+ }
216
+ log.success(`Post-codemod hook ${label} completed.`);
217
+ } catch (err) {
218
+ log.warn(`Post-codemod hook ${label} failed: ${err.message}`);
219
+ }
122
220
  }
123
221
  }
124
222
 
@@ -129,35 +227,50 @@ export function registerUpgrade(program) {
129
227
  program
130
228
  .command('upgrade')
131
229
  .description('Run codemods to migrate between versions')
230
+ .option(
231
+ '--from <version>',
232
+ 'Previous version before the dependency upgrade',
233
+ )
132
234
  .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)
235
+ .option(
236
+ '--force',
237
+ 'Run codemods even if --from is newer than the installed version',
238
+ false,
239
+ )
136
240
  .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)
241
+ .option(
242
+ '--integration <package-or-file>',
243
+ 'Explicit integration package name or integration file path (repeatable)',
244
+ (value, previous) => [...(previous ?? []), value],
245
+ [],
246
+ )
140
247
  .option('--path <dir>', 'Source directory to scan', './src')
141
- .option('--install-deps', 'Auto-install jscodeshift without prompting', false)
248
+ .option(
249
+ '--install-deps',
250
+ 'Auto-install jscodeshift without prompting',
251
+ false,
252
+ )
142
253
  .option('--list', 'List available codemods', false)
143
- .action(async (options) => {
254
+ .action(async options => {
144
255
  const json = program.opts().json || false;
145
256
  if (!json) p.intro('Upgrade');
146
257
 
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);
258
+ if (!options.list && !options.from) {
259
+ const msg =
260
+ 'Missing required --from. Install the target version first, then run `astryx upgrade --from <old-version>`.';
261
+ if (json)
262
+ return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_ARGUMENT);
153
263
  p.log.error(msg);
154
264
  p.outro('Aborted');
155
265
  process.exitCode = 1;
156
266
  return;
157
267
  }
158
- if (options.from !== undefined && !isValidSemver(options.from)) {
268
+
269
+ // Validate --from upfront so callers don't silently accept typos.
270
+ if (!options.list && !isValidSemver(options.from)) {
159
271
  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);
272
+ if (json)
273
+ return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_VERSION);
161
274
  p.log.error(msg);
162
275
  p.outro('Aborted');
163
276
  process.exitCode = 1;
@@ -172,72 +285,113 @@ export function registerUpgrade(program) {
172
285
  const manifests = await getTransformsBetween('0.0.0', latestVersion);
173
286
  for (const {version, transforms} of manifests) {
174
287
  for (const {name, meta, optional} of transforms) {
175
- codemods.push({name, title: meta.title, version, pr: meta.pr, optional: !!optional});
288
+ codemods.push({
289
+ name,
290
+ title: meta.title,
291
+ version,
292
+ pr: meta.pr,
293
+ optional: !!optional,
294
+ });
176
295
  }
177
296
  }
178
- if (json) return jsonOut('upgrade.list', codemods.map(({name, title, version, optional}) => ({name, title, version, optional})));
297
+ if (json)
298
+ return jsonOut(
299
+ 'upgrade.list',
300
+ codemods.map(({name, title, version, optional}) => ({
301
+ name,
302
+ title,
303
+ version,
304
+ optional,
305
+ })),
306
+ );
179
307
  p.log.step('Available codemods:');
180
308
  for (const {name, title, pr, optional} of codemods) {
181
- p.log.info(` ${name} — ${title}${optional ? ' (optional)' : ''} (${pr})`);
309
+ p.log.info(
310
+ ` ${name} — ${title}${optional ? ' (optional)' : ''} (${pr})`,
311
+ );
182
312
  }
183
313
  p.outro('Done');
184
314
  return;
185
315
  }
186
316
 
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);
317
+ const currentVersion = options.from;
318
+ const installed = detectInstalledTargetVersion();
319
+ if (!installed) {
320
+ const msg =
321
+ 'Could not find installed @astryxdesign/core (or legacy @xds/core). Install the target version first, then rerun `astryx upgrade --from <old-version>`.';
322
+ if (json)
323
+ return jsonError(msg, undefined, ERROR_CODES.ERR_VERSION_DETECT);
201
324
  p.log.error(msg);
202
325
  p.outro('Aborted');
203
326
  process.exitCode = 1;
204
327
  return;
205
328
  }
329
+ const targetVersion = installed.version;
206
330
 
207
- const targetVersion = options.to;
331
+ if (!json) {
332
+ p.log.info(`From version: ${currentVersion}`);
333
+ p.log.info(
334
+ `Installed target: ${targetVersion} (${installed.packageName})`,
335
+ );
336
+ }
208
337
 
209
- if (!skipVersionCheck) {
210
- if (!json) {
211
- p.log.info(`Current version: ${currentVersion}`);
212
- p.log.info(`Target version: ${targetVersion}`);
213
- }
338
+ let integrations;
339
+ try {
340
+ const config = await loadConfig(process.cwd());
341
+ const integrationSpecs = uniqueFiles([
342
+ ...(config.integrations ?? []),
343
+ ...(options.integration ?? []),
344
+ ]);
345
+ integrations = await loadIntegrations(integrationSpecs);
346
+ } catch (err) {
347
+ if (json)
348
+ return jsonError(
349
+ err.message,
350
+ undefined,
351
+ ERROR_CODES.ERR_INVALID_ARGUMENT,
352
+ );
353
+ p.log.error(err.message);
354
+ p.outro('Aborted');
355
+ process.exitCode = 1;
356
+ return;
357
+ }
358
+ if (!json && integrations.length > 0) {
359
+ p.log.info(
360
+ `Integrations: ${integrations.map(i => i.name ?? i.__spec).join(', ')}`,
361
+ );
362
+ }
214
363
 
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;
364
+ if (!options.force && semverGte(currentVersion, targetVersion)) {
365
+ if (json) {
366
+ return jsonOut('upgrade.status', {
367
+ status: 'up_to_date',
368
+ from: currentVersion,
369
+ to: targetVersion,
370
+ });
227
371
  }
372
+ p.log.success('Already up to date — no codemods to run.');
373
+ p.log.info('Use --force to run codemods anyway.');
374
+ p.outro('Done');
375
+ return;
228
376
  }
229
377
 
230
378
  // Resolve transforms
231
- const versionManifests = await getTransformsBetween(
232
- skipVersionCheck ? '0.0.0' : currentVersion,
233
- targetVersion,
234
- );
379
+ const versionManifests = [
380
+ ...(await getTransformsBetween(currentVersion, targetVersion)),
381
+ ...integrations.flatMap(integration =>
382
+ normalizeIntegrationTransforms(
383
+ integration,
384
+ currentVersion,
385
+ targetVersion,
386
+ ),
387
+ ),
388
+ ];
235
389
 
236
390
  if (versionManifests.length === 0) {
237
391
  if (json) {
238
392
  return jsonOut('upgrade.status', {
239
393
  status: 'no_codemods',
240
- from: skipVersionCheck ? null : currentVersion,
394
+ from: currentVersion,
241
395
  to: targetVersion,
242
396
  });
243
397
  }
@@ -262,7 +416,8 @@ export function registerUpgrade(program) {
262
416
 
263
417
  if (totalTransforms === 0 && totalOptional === 0) {
264
418
  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);
419
+ if (json)
420
+ return jsonError(msg, undefined, ERROR_CODES.ERR_UNKNOWN_CODEMOD);
266
421
  p.log.error(msg);
267
422
  p.outro('Aborted');
268
423
  process.exitCode = 1;
@@ -279,34 +434,26 @@ export function registerUpgrade(program) {
279
434
  }
280
435
  }
281
436
 
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
- }
437
+ const receipt = {
438
+ from: currentVersion,
439
+ to: targetVersion,
440
+ codemods: totalTransforms,
441
+ integrations: integrations.map(i => i.name ?? i.__spec),
442
+ agentDocsRefreshed: false,
443
+ };
305
444
 
306
445
  // Ensure jscodeshift is available
307
- const ready = await ensureJscodeshift({installDeps: options.installDeps, silent: json});
446
+ const ready = await ensureJscodeshift({
447
+ installDeps: options.installDeps,
448
+ silent: json,
449
+ });
308
450
  if (!ready) {
309
- if (json) return jsonError('jscodeshift is required but could not be installed.', undefined, ERROR_CODES.ERR_DEP_MISSING);
451
+ if (json)
452
+ return jsonError(
453
+ 'jscodeshift is required but could not be installed.',
454
+ undefined,
455
+ ERROR_CODES.ERR_DEP_MISSING,
456
+ );
310
457
  p.outro('Aborted');
311
458
  process.exitCode = 1;
312
459
  return;
@@ -320,6 +467,31 @@ export function registerUpgrade(program) {
320
467
  silent: json,
321
468
  });
322
469
 
470
+ if (options.apply && integrations.length > 0) {
471
+ const codemodDir = path.resolve(options.path);
472
+ const absoluteChangedFiles = uniqueFiles(
473
+ codemodResult?.writtenFiles ?? [],
474
+ );
475
+ const changedFiles = absoluteChangedFiles.map(file =>
476
+ path.relative(process.cwd(), file),
477
+ );
478
+ const packageChangedFiles = absoluteChangedFiles
479
+ .filter(file => file.startsWith(process.cwd() + path.sep))
480
+ .map(file => path.relative(process.cwd(), file));
481
+ await runPostCodemodHooks(
482
+ integrations,
483
+ {
484
+ packageDir: process.cwd(),
485
+ codemodDir,
486
+ changedFiles,
487
+ absoluteChangedFiles,
488
+ packageChangedFiles,
489
+ apply: options.apply,
490
+ },
491
+ json,
492
+ );
493
+ }
494
+
323
495
  // Refresh agent docs if any exist (AGENTS.md, CLAUDE.md, .claude/CLAUDE.md, etc.)
324
496
  // Always update after --apply; also update during dry-run if files exist,
325
497
  // since the index reflects the installed CLI version, not the codemods.
@@ -330,7 +502,8 @@ export function registerUpgrade(program) {
330
502
  // Don't inject into files that never had Astryx content.
331
503
  const written = installAgentDocs(process.cwd(), {onlyReplace: true});
332
504
  receipt.agentDocsRefreshed = written.length > 0;
333
- if (!json && written.length > 0) p.log.success(`Agent docs updated: ${written.join(', ')}`);
505
+ if (!json && written.length > 0)
506
+ p.log.success(`Agent docs updated: ${written.join(', ')}`);
334
507
  } catch {
335
508
  if (!json) {
336
509
  p.log.warn(
@@ -340,12 +513,23 @@ export function registerUpgrade(program) {
340
513
  }
341
514
  }
342
515
 
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 ?? [];
516
+ if (codemodResult && typeof codemodResult === 'object') {
517
+ receipt.filesChanged = codemodResult.totalFilesChanged ?? 0;
518
+ receipt.transformsApplied = codemodResult.totalTransformsApplied ?? 0;
519
+ receipt.errors = codemodResult.errors ?? [];
520
+ }
521
+
522
+ if (receipt.errors?.length > 0) {
523
+ const msg = `Upgrade completed with ${receipt.errors.length} codemod error${receipt.errors.length === 1 ? '' : 's'}.`;
524
+ if (json) {
525
+ return jsonError(msg, {receipt}, ERROR_CODES.ERR_CODEMOD_FAILED);
348
526
  }
527
+ p.outro('Upgrade failed');
528
+ process.exitCode = 1;
529
+ return;
530
+ }
531
+
532
+ if (json) {
349
533
  return jsonOut('upgrade.run', receipt);
350
534
  }
351
535
  p.outro(options.apply ? 'Upgrade complete' : 'Dry run complete');