@astryxdesign/cli 0.1.0 → 0.1.1
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.
- package/CHANGELOG.md +68 -0
- package/README.md +117 -75
- package/bin/astryx.mjs +22 -7
- package/docs/getting-started.doc.mjs +11 -11
- package/docs/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +2 -2
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling.doc.mjs +3 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +14 -0
- package/docs/theme.doc.zh.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +4 -4
- package/package.json +2 -2
- package/src/api/doctor.mjs +3 -3
- package/src/api/search.mjs +207 -13
- package/src/api/template.mjs +2 -1
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/runner.mjs +105 -51
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
- package/src/commands/agent-docs.mjs +119 -66
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +87 -31
- package/src/commands/build-theme.import-path.test.mjs +1 -1
- package/src/commands/build-theme.path-safety.test.mjs +1 -1
- package/src/commands/build-theme.prose.test.mjs +1 -1
- package/src/commands/build.mjs +196 -0
- package/src/commands/component-package.test.mjs +1 -1
- package/src/commands/component.test.mjs +1 -1
- package/src/commands/docs.test.mjs +1 -1
- package/src/commands/doctor.test.mjs +1 -1
- package/src/commands/external-showcase.test.mjs +1 -1
- package/src/commands/init.mjs +10 -2
- package/src/commands/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +10 -3
- package/src/commands/swizzle-gap-safety.test.mjs +1 -1
- package/src/commands/swizzle.path-safety.test.mjs +1 -1
- package/src/commands/template.path-safety.test.mjs +1 -1
- package/src/commands/template.test.mjs +1 -1
- package/src/commands/upgrade.mjs +353 -169
- package/src/commands/upgrade.test.mjs +41 -27
- package/src/index.mjs +1 -0
- package/src/lib/config.mjs +12 -0
- package/src/lib/config.test.mjs +42 -0
- package/src/lib/error-codes.mjs +3 -0
- package/src/types/error-codes.d.ts +1 -0
- package/src/utils/interactive.mjs +1 -1
- package/src/utils/interactive.test.mjs +2 -0
- package/src/utils/package-manager.mjs +1 -1
- package/src/utils/package-manager.test.mjs +1 -1
- package/src/utils/path-safety.test.mjs +1 -1
- package/src/utils/paths.test.mjs +8 -8
- package/src/utils/update-check.mjs +4 -26
- package/src/utils/update-check.test.mjs +2 -64
- package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
- package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
- package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
- package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
- package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
- package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
- package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
- package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
- package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
- package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
- package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
- package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
- package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
- package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
- package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
- package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
- package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
- package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
- package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
- package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +2 -12
- package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
- package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
- package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
- package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
- package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
- package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
- package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
- package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
- package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
- package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
- package/templates/pages/ai-chat/page.tsx +71 -64
- package/templates/pages/ai-chat-landing/page.tsx +8 -12
- package/templates/pages/centered-hero/page.tsx +13 -15
- package/templates/pages/classic-gallery/page.tsx +27 -34
- package/templates/pages/detail-page/page.tsx +18 -18
- package/templates/pages/documentation/page.tsx +42 -58
- package/templates/pages/documentation-design/page.tsx +82 -60
- package/templates/pages/documentation-technical/page.tsx +101 -60
- package/templates/pages/editor/page.tsx +42 -54
- package/templates/pages/file-explorer/page.tsx +13 -16
- package/templates/pages/form-two-column/page.tsx +13 -17
- package/templates/pages/gallery-hero/page.tsx +13 -15
- package/templates/pages/ide/page.tsx +188 -264
- package/templates/pages/library/page.tsx +16 -23
- package/templates/pages/login/page.tsx +14 -18
- package/templates/pages/login-card/page.tsx +14 -18
- package/templates/pages/login-split/page.tsx +50 -48
- package/templates/pages/login-sso/page.tsx +9 -13
- package/templates/pages/mixed-gallery/page.tsx +51 -45
- package/templates/pages/payment-form/page.tsx +56 -70
- package/templates/pages/product-detail/page.tsx +27 -33
- package/templates/pages/product-gallery/page.tsx +7 -13
- package/templates/pages/settings-dialog/page.tsx +35 -43
- package/templates/pages/settings-sidebar/page.tsx +39 -47
- package/templates/pages/side-gallery/page.tsx +6 -9
- package/templates/pages/table-grouped/page.tsx +11 -15
- package/templates/pages/theme-showcase/page.tsx +33 -37
package/src/commands/upgrade.mjs
CHANGED
|
@@ -3,122 +3,220 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file upgrade command — Full version-to-version upgrade pipeline
|
|
5
5
|
*
|
|
6
|
-
* `astryx upgrade`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
12
|
-
* 2.
|
|
13
|
-
* 3.
|
|
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
|
-
* --
|
|
20
|
-
* --
|
|
21
|
-
* --
|
|
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 {
|
|
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 {
|
|
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
|
|
46
|
-
* @returns {string|null}
|
|
46
|
+
* Detect the installed target version from node_modules.
|
|
47
|
+
* @returns {{version: string, packageName: string}|null}
|
|
47
48
|
*/
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
...
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
76
|
+
function resolvePackageDir(packageName) {
|
|
77
|
+
const parts = packageName.split('/');
|
|
78
|
+
return path.resolve(process.cwd(), 'node_modules', ...parts);
|
|
79
|
+
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
function resolveIntegrationFile(spec) {
|
|
82
|
+
if (isPathSpec(spec)) {
|
|
83
|
+
return path.resolve(process.cwd(), spec);
|
|
84
|
+
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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(
|
|
134
|
-
|
|
135
|
-
|
|
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(
|
|
138
|
-
|
|
139
|
-
|
|
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(
|
|
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
|
|
254
|
+
.action(async options => {
|
|
144
255
|
const json = program.opts().json || false;
|
|
145
256
|
if (!json) p.intro('Upgrade');
|
|
146
257
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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)
|
|
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({
|
|
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)
|
|
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(
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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 =
|
|
232
|
-
|
|
233
|
-
|
|
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:
|
|
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)
|
|
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 = {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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({
|
|
446
|
+
const ready = await ensureJscodeshift({
|
|
447
|
+
installDeps: options.installDeps,
|
|
448
|
+
silent: json,
|
|
449
|
+
});
|
|
308
450
|
if (!ready) {
|
|
309
|
-
if (json)
|
|
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)
|
|
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 (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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');
|