@astryxdesign/cli 0.1.0-canary.cfbdec3 → 0.1.0-canary.d1e1201
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/README.md +117 -75
- package/docs/working-with-ai.doc.mjs +1 -1
- package/package.json +7 -7
- package/src/commands/json-contract.test.mjs +9 -2
- package/src/commands/upgrade.mjs +254 -149
- package/src/commands/upgrade.test.mjs +40 -26
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npx astryx docs migration
|
|
|
11
11
|
npx astryx template --list
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Finding things: `astryx search`
|
|
15
15
|
|
|
16
16
|
When you don't know whether what you need is a component, a hook, a docs topic,
|
|
17
17
|
or a template, search across all of them at once. Results are ranked by
|
|
@@ -50,20 +50,20 @@ Options:
|
|
|
50
50
|
|
|
51
51
|
## Commands
|
|
52
52
|
|
|
53
|
-
| Command | Description
|
|
54
|
-
| ------------- |
|
|
53
|
+
| Command | Description |
|
|
54
|
+
| ------------- | ---------------------------------------------------------------------------------------------------- |
|
|
55
55
|
| `init` | Initialize the design system in your project: installs packages, sets up theming, adds AI agent docs |
|
|
56
|
-
| `component` | List components or print detailed docs, props, usage examples, and source
|
|
57
|
-
| `search` | Find components, hooks, docs, and templates in one ranked, cross-domain result set
|
|
58
|
-
| `docs` | Print reference documentation (tokens, theme, color, typography, spacing, etc.)
|
|
59
|
-
| `template` | Inject page or block templates into your project
|
|
60
|
-
| `hook` | List hooks and print hook documentation
|
|
61
|
-
| `swizzle` | Copy component source into your project for deep customization
|
|
62
|
-
| `upgrade` | Run codemods to migrate between versions
|
|
63
|
-
| `theme build` | Compile a defineTheme file to production CSS and JS
|
|
64
|
-
| `discover` | Discover external packages and components
|
|
65
|
-
| `gap-report` | Report a gap when a component doesn't meet your needs
|
|
66
|
-
| `doctor` | Diagnose your XDS setup and report problems with fixes (CI-friendly via exit code)
|
|
56
|
+
| `component` | List components or print detailed docs, props, usage examples, and source |
|
|
57
|
+
| `search` | Find components, hooks, docs, and templates in one ranked, cross-domain result set |
|
|
58
|
+
| `docs` | Print reference documentation (tokens, theme, color, typography, spacing, etc.) |
|
|
59
|
+
| `template` | Inject page or block templates into your project |
|
|
60
|
+
| `hook` | List hooks and print hook documentation |
|
|
61
|
+
| `swizzle` | Copy component source into your project for deep customization |
|
|
62
|
+
| `upgrade` | Run codemods to migrate between versions |
|
|
63
|
+
| `theme build` | Compile a defineTheme file to production CSS and JS |
|
|
64
|
+
| `discover` | Discover external packages and components |
|
|
65
|
+
| `gap-report` | Report a gap when a component doesn't meet your needs |
|
|
66
|
+
| `doctor` | Diagnose your XDS setup and report problems with fixes (CI-friendly via exit code) |
|
|
67
67
|
|
|
68
68
|
### Global options
|
|
69
69
|
|
|
@@ -122,46 +122,46 @@ if (isError(result)) {
|
|
|
122
122
|
|
|
123
123
|
### Error codes
|
|
124
124
|
|
|
125
|
-
| Code
|
|
126
|
-
|
|
|
127
|
-
| `ERR_UNKNOWN`
|
|
128
|
-
| `ERR_UNKNOWN_COMMAND`
|
|
129
|
-
| `ERR_UNKNOWN_SUBCOMMAND` | A subcommand under a group was not recognized (e.g. `astryx theme bogus`).
|
|
130
|
-
| `ERR_INVALID_OPTION`
|
|
131
|
-
| `ERR_INVALID_ARGUMENT`
|
|
132
|
-
| `ERR_MISSING_ARGUMENT`
|
|
133
|
-
| `ERR_INVALID_LANG`
|
|
134
|
-
| `ERR_INVALID_DETAIL`
|
|
135
|
-
| `ERR_NODE_VERSION`
|
|
136
|
-
| `ERR_CORE_NOT_FOUND`
|
|
137
|
-
| `ERR_UNKNOWN_COMPONENT`
|
|
138
|
-
| `ERR_UNKNOWN_HOOK`
|
|
139
|
-
| `ERR_UNKNOWN_TOPIC`
|
|
140
|
-
| `ERR_UNKNOWN_SECTION`
|
|
141
|
-
| `ERR_UNKNOWN_CATEGORY`
|
|
142
|
-
| `ERR_UNKNOWN_TEMPLATE`
|
|
143
|
-
| `ERR_UNKNOWN_PACKAGE`
|
|
144
|
-
| `ERR_UNKNOWN_AGENT`
|
|
145
|
-
| `ERR_UNKNOWN_FEATURE`
|
|
146
|
-
| `ERR_UNKNOWN_CODEMOD`
|
|
147
|
-
| `ERR_NOT_FOUND`
|
|
148
|
-
| `ERR_NO_DOC`
|
|
149
|
-
| `ERR_NO_SHOWCASE`
|
|
150
|
-
| `ERR_NO_SOURCE`
|
|
151
|
-
| `ERR_INVALID_DOC`
|
|
152
|
-
| `ERR_FILE_NOT_FOUND`
|
|
153
|
-
| `ERR_FILE_EXISTS`
|
|
154
|
-
| `ERR_PATH_TRAVERSAL`
|
|
155
|
-
| `ERR_WRITE_FAILED`
|
|
156
|
-
| `ERR_THEME_INVALID`
|
|
157
|
-
| `ERR_THEME_LOAD`
|
|
158
|
-
| `ERR_TEMPLATE_CONFIG`
|
|
159
|
-
| `ERR_TEMPLATE_GET`
|
|
160
|
-
| `ERR_VERSION_DETECT`
|
|
161
|
-
| `ERR_INVALID_VERSION`
|
|
162
|
-
| `ERR_DEP_MISSING`
|
|
163
|
-
| `ERR_GH_CLI`
|
|
164
|
-
| `ERR_GAP_REPORT_FAILED`
|
|
125
|
+
| Code | Meaning |
|
|
126
|
+
| ------------------------ | -------------------------------------------------------------------------------------- |
|
|
127
|
+
| `ERR_UNKNOWN` | Generic fallback for any error without a more specific code. |
|
|
128
|
+
| `ERR_UNKNOWN_COMMAND` | A top-level command name was not recognized (e.g. `astryx bogus`). |
|
|
129
|
+
| `ERR_UNKNOWN_SUBCOMMAND` | A subcommand under a group was not recognized (e.g. `astryx theme bogus`). |
|
|
130
|
+
| `ERR_INVALID_OPTION` | An unknown flag was passed, or `--json` was used on a command that doesn't support it. |
|
|
131
|
+
| `ERR_INVALID_ARGUMENT` | An option/argument value was rejected, or required flags were missing. |
|
|
132
|
+
| `ERR_MISSING_ARGUMENT` | A required positional argument was omitted (e.g. `astryx theme build` with no file). |
|
|
133
|
+
| `ERR_INVALID_LANG` | `--lang` was given a value outside its choices (`en`, `zh`, `dense`). |
|
|
134
|
+
| `ERR_INVALID_DETAIL` | `--detail` was given a value outside its choices (`full`, `compact`, `brief`). |
|
|
135
|
+
| `ERR_NODE_VERSION` | The running Node.js version is below the supported minimum. |
|
|
136
|
+
| `ERR_CORE_NOT_FOUND` | `@astryxdesign/core` could not be located (not installed / not in a monorepo). |
|
|
137
|
+
| `ERR_UNKNOWN_COMPONENT` | No component matched the requested name. |
|
|
138
|
+
| `ERR_UNKNOWN_HOOK` | No hook matched the requested name. |
|
|
139
|
+
| `ERR_UNKNOWN_TOPIC` | No docs topic matched the requested name. |
|
|
140
|
+
| `ERR_UNKNOWN_SECTION` | A docs topic exists but the requested section within it does not. |
|
|
141
|
+
| `ERR_UNKNOWN_CATEGORY` | A `--category` filter value did not match any known category. |
|
|
142
|
+
| `ERR_UNKNOWN_TEMPLATE` | No template matched the requested name. |
|
|
143
|
+
| `ERR_UNKNOWN_PACKAGE` | No package matched the requested name (discover). |
|
|
144
|
+
| `ERR_UNKNOWN_AGENT` | An unrecognized `--agent` value was passed (agent docs / init). |
|
|
145
|
+
| `ERR_UNKNOWN_FEATURE` | An unrecognized `--features` value was passed to `init`. |
|
|
146
|
+
| `ERR_UNKNOWN_CODEMOD` | A `--codemod` value did not match any registered codemod (upgrade). |
|
|
147
|
+
| `ERR_NOT_FOUND` | A discover/lookup query matched nothing in any package. |
|
|
148
|
+
| `ERR_NO_DOC` | A component exists but has no typed `.doc.mjs` file. |
|
|
149
|
+
| `ERR_NO_SHOWCASE` | No showcase exists for the requested component. |
|
|
150
|
+
| `ERR_NO_SOURCE` | No source file could be located for the component/template. |
|
|
151
|
+
| `ERR_INVALID_DOC` | A component's docs failed validation (malformed `.doc.mjs`). |
|
|
152
|
+
| `ERR_FILE_NOT_FOUND` | A required input file did not exist. |
|
|
153
|
+
| `ERR_FILE_EXISTS` | Refused to overwrite an existing file in non-interactive mode. |
|
|
154
|
+
| `ERR_PATH_TRAVERSAL` | A path escaped its allowed root, or a name contained traversal markers. |
|
|
155
|
+
| `ERR_WRITE_FAILED` | Writing output files failed (and was rolled back). |
|
|
156
|
+
| `ERR_THEME_INVALID` | A theme definition was missing a required property (e.g. `name`). |
|
|
157
|
+
| `ERR_THEME_LOAD` | A theme file could not be loaded / parsed into a `defineTheme` result. |
|
|
158
|
+
| `ERR_TEMPLATE_CONFIG` | `template.get` is not configured in `astryx.config.mjs` (fetch-by-id). |
|
|
159
|
+
| `ERR_TEMPLATE_GET` | A configured `template.get` threw or returned an invalid value. |
|
|
160
|
+
| `ERR_VERSION_DETECT` | The current `@astryxdesign/core` version could not be detected. |
|
|
161
|
+
| `ERR_INVALID_VERSION` | A `--from`/`--to` value was not a valid semver string. |
|
|
162
|
+
| `ERR_DEP_MISSING` | A required external dependency (e.g. jscodeshift) is missing. |
|
|
163
|
+
| `ERR_GH_CLI` | GitHub CLI (`gh`) is not installed or not authenticated. |
|
|
164
|
+
| `ERR_GAP_REPORT_FAILED` | Filing a gap report failed (disabled, or the integration errored). |
|
|
165
165
|
|
|
166
166
|
## Capability manifest (agent discovery)
|
|
167
167
|
|
|
@@ -186,25 +186,59 @@ Shape:
|
|
|
186
186
|
"version": "0.0.14",
|
|
187
187
|
"description": "Design system CLI — components, themes, and tooling",
|
|
188
188
|
"globalOptions": [
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
{
|
|
190
|
+
"flag": "--json",
|
|
191
|
+
"type": "boolean",
|
|
192
|
+
"description": "Output as typed JSON…",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"flag": "--lang <locale>",
|
|
196
|
+
"type": "enum",
|
|
197
|
+
"choices": ["en", "zh", "dense"],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"flag": "--detail <level>",
|
|
201
|
+
"type": "enum",
|
|
202
|
+
"choices": ["full", "compact", "brief"],
|
|
203
|
+
"default": "full",
|
|
204
|
+
},
|
|
192
205
|
],
|
|
193
206
|
"commands": [
|
|
194
207
|
{
|
|
195
208
|
"name": "component",
|
|
196
209
|
"description": "List components or print component docs",
|
|
197
|
-
"arguments": [
|
|
198
|
-
|
|
210
|
+
"arguments": [
|
|
211
|
+
{
|
|
212
|
+
"name": "name",
|
|
213
|
+
"required": false,
|
|
214
|
+
"variadic": false,
|
|
215
|
+
"description": "",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
"options": [
|
|
219
|
+
{
|
|
220
|
+
"flag": "--props",
|
|
221
|
+
"type": "boolean",
|
|
222
|
+
"description": "Print only the props table",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
199
225
|
"json": true,
|
|
200
|
-
"responseTypes": [
|
|
201
|
-
|
|
202
|
-
|
|
226
|
+
"responseTypes": [
|
|
227
|
+
"component.list",
|
|
228
|
+
"component.detail",
|
|
229
|
+
"component.detail.props",
|
|
230
|
+
"…",
|
|
231
|
+
],
|
|
232
|
+
"examples": ["astryx component Button --props --json"],
|
|
233
|
+
},
|
|
203
234
|
// …one entry per command; subcommands (e.g. `theme build`) nest under `subcommands`
|
|
204
235
|
],
|
|
205
236
|
"jsonSupported": ["component", "docs", "…"],
|
|
206
|
-
"responseTypes": {
|
|
207
|
-
|
|
237
|
+
"responseTypes": {
|
|
238
|
+
"component": ["component.list", "…"],
|
|
239
|
+
"theme build": ["theme.build"],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
208
242
|
}
|
|
209
243
|
```
|
|
210
244
|
|
|
@@ -225,7 +259,15 @@ For the standalone manifest envelope (`type: "manifest"`), use `astryx manifest
|
|
|
225
259
|
The same logic that powers `xds --json` is available as importable, type-safe functions:
|
|
226
260
|
|
|
227
261
|
```typescript
|
|
228
|
-
import {
|
|
262
|
+
import {
|
|
263
|
+
component,
|
|
264
|
+
docs,
|
|
265
|
+
discover,
|
|
266
|
+
template,
|
|
267
|
+
hook,
|
|
268
|
+
search,
|
|
269
|
+
AstryxError,
|
|
270
|
+
} from '@astryxdesign/cli/api';
|
|
229
271
|
|
|
230
272
|
// Same result as: xds --json component Button
|
|
231
273
|
const btn = await component('Button');
|
|
@@ -359,16 +401,16 @@ No failures — but review the ⚠ warnings above when you can.
|
|
|
359
401
|
|
|
360
402
|
### Checks
|
|
361
403
|
|
|
362
|
-
| Check
|
|
363
|
-
|
|
|
364
|
-
| Node.js version
|
|
365
|
-
| @astryxdesign/core installed
|
|
366
|
-
| Version alignment
|
|
367
|
-
| Theme packages
|
|
368
|
-
| astryx.config.mjs
|
|
369
|
-
| AI agent docs
|
|
370
|
-
| Peer dependencies
|
|
371
|
-
| Package manager
|
|
404
|
+
| Check | Status it can return | What it verifies |
|
|
405
|
+
| ---------------------------- | -------------------- | -------------------------------------------------------------------- |
|
|
406
|
+
| Node.js version | pass / fail | Running Node meets the CLI's minimum |
|
|
407
|
+
| @astryxdesign/core installed | pass / fail | `@astryxdesign/core` is resolvable from the project |
|
|
408
|
+
| Version alignment | pass / warn / info | Installed `@astryxdesign/core` is in step with `@astryxdesign/cli` |
|
|
409
|
+
| Theme packages | pass / warn | An `@astryxdesign/theme-*` package is installed and a theme is wired |
|
|
410
|
+
| astryx.config.mjs | pass / fail / info | Config (if present) loads cleanly with a valid shape |
|
|
411
|
+
| AI agent docs | pass / warn / info | Agent docs exist and contain the XDS section markers |
|
|
412
|
+
| Peer dependencies | pass / warn / info | `@astryxdesign/core`'s peer deps (react, …) are installed |
|
|
413
|
+
| Package manager | info | Reports the detected package manager |
|
|
372
414
|
|
|
373
415
|
### CI gate
|
|
374
416
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.0-canary.
|
|
3
|
+
"version": "0.1.0-canary.d1e1201",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"jscodeshift": "^17.3.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@astryxdesign/core": "0.1.0-canary.
|
|
58
|
-
"@astryxdesign/lab": "0.1.0-canary.
|
|
59
|
-
"@astryxdesign/theme-neutral": "0.1.0-canary.
|
|
57
|
+
"@astryxdesign/core": "0.1.0-canary.d1e1201",
|
|
58
|
+
"@astryxdesign/lab": "0.1.0-canary.d1e1201",
|
|
59
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.d1e1201"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@astryxdesign/core": {
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@astryxdesign/core": "0.1.0-canary.
|
|
74
|
-
"@astryxdesign/lab": "0.1.0-canary.
|
|
75
|
-
"@astryxdesign/theme-neutral": "0.1.0-canary.
|
|
73
|
+
"@astryxdesign/core": "0.1.0-canary.d1e1201",
|
|
74
|
+
"@astryxdesign/lab": "0.1.0-canary.d1e1201",
|
|
75
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.d1e1201"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"astryx": "node bin/astryx.mjs",
|
|
@@ -177,9 +177,16 @@ describe('--json contract: supported commands emit valid envelopes', () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
it('astryx upgrade --json (already up to date) emits upgrade.status', () => {
|
|
180
|
-
// Force a no-op range: from >
|
|
180
|
+
// Force a no-op range: from > installed target.
|
|
181
|
+
const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
182
|
+
fs.mkdirSync(coreDir, {recursive: true});
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
path.join(coreDir, 'package.json'),
|
|
185
|
+
JSON.stringify({name: '@astryxdesign/core', version: '0.0.1'}, null, 2),
|
|
186
|
+
);
|
|
187
|
+
|
|
181
188
|
const {status, stdout} = runCli(
|
|
182
|
-
['upgrade', '--json', '--from', '99.0.0'
|
|
189
|
+
['upgrade', '--json', '--from', '99.0.0'],
|
|
183
190
|
{cwd: tmpDir},
|
|
184
191
|
);
|
|
185
192
|
expect(status).toBe(0);
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -3,31 +3,32 @@
|
|
|
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
34
|
import {
|
|
@@ -36,89 +37,185 @@ import {
|
|
|
36
37
|
} from '../codemods/registry.mjs';
|
|
37
38
|
import {runCodemods} from '../codemods/runner.mjs';
|
|
38
39
|
import {installAgentDocs, discoverAgentDocs} from './agent-docs.mjs';
|
|
39
|
-
import {
|
|
40
|
-
import {isValidSemver, semverGte} from '../utils/semver.mjs';
|
|
40
|
+
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
41
|
+
import {isValidSemver, semverGte, semverGt} from '../utils/semver.mjs';
|
|
41
42
|
import {jsonOut, jsonError} from '../lib/json.mjs';
|
|
42
43
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
43
44
|
|
|
45
|
+
const execFileAsync = promisify(execFile);
|
|
46
|
+
|
|
44
47
|
/**
|
|
45
|
-
* Detect the installed
|
|
46
|
-
* @returns {string|null}
|
|
48
|
+
* Detect the installed target version from node_modules.
|
|
49
|
+
* @returns {{version: string, packageName: string}|null}
|
|
47
50
|
*/
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
...
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return null;
|
|
51
|
+
function detectInstalledTargetVersion() {
|
|
52
|
+
for (const packageName of ['@astryxdesign/core', '@xds/core']) {
|
|
53
|
+
const pkgPath = path.resolve(
|
|
54
|
+
process.cwd(),
|
|
55
|
+
'node_modules',
|
|
56
|
+
...packageName.split('/'),
|
|
57
|
+
'package.json',
|
|
58
|
+
);
|
|
59
|
+
try {
|
|
60
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
61
|
+
if (pkg.version) return {version: pkg.version, packageName};
|
|
62
|
+
} catch {
|
|
63
|
+
// Missing or unreadable package.json — try the next supported package name.
|
|
64
|
+
}
|
|
63
65
|
}
|
|
66
|
+
return null;
|
|
64
67
|
}
|
|
65
68
|
|
|
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;
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
function isPathSpec(spec) {
|
|
71
|
+
return (
|
|
72
|
+
spec.startsWith('.') ||
|
|
73
|
+
spec.startsWith('/') ||
|
|
74
|
+
spec.endsWith('.mjs') ||
|
|
75
|
+
spec.endsWith('.js')
|
|
76
|
+
);
|
|
77
|
+
}
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
function resolvePackageDir(packageName) {
|
|
80
|
+
const parts = packageName.split('/');
|
|
81
|
+
return path.resolve(process.cwd(), 'node_modules', ...parts);
|
|
82
|
+
}
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
function resolveIntegrationFile(spec) {
|
|
85
|
+
if (isPathSpec(spec)) {
|
|
86
|
+
return path.resolve(process.cwd(), spec);
|
|
87
|
+
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
const packageDir = resolvePackageDir(spec);
|
|
90
|
+
const pkgPath = path.join(packageDir, 'package.json');
|
|
91
|
+
let pkg;
|
|
92
|
+
try {
|
|
93
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Could not find installed integration package "${spec}" at ${pkgPath}. Install it first or pass a direct integration file path.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
92
99
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
const manifestPath = pkg.astryx?.integration ?? pkg.xds?.integration;
|
|
101
|
+
if (!manifestPath) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Package "${spec}" does not declare astryx.integration (or legacy xds.integration) in package.json.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return path.resolve(packageDir, manifestPath);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function loadIntegrations(specs) {
|
|
110
|
+
const integrations = [];
|
|
111
|
+
for (const spec of specs) {
|
|
112
|
+
const file = resolveIntegrationFile(spec);
|
|
113
|
+
const mod = await import(pathToFileURL(file).href);
|
|
114
|
+
const integration = mod.default ?? mod.integration ?? mod;
|
|
115
|
+
if (!integration || typeof integration !== 'object') {
|
|
116
|
+
throw new Error(`Integration ${spec} did not export an object.`);
|
|
117
|
+
}
|
|
118
|
+
const integrationDir = path.dirname(file);
|
|
119
|
+
if (Array.isArray(integration.codemods)) {
|
|
120
|
+
for (const codemod of integration.codemods) {
|
|
121
|
+
if (typeof codemod.transform === 'string') {
|
|
122
|
+
const transformPath = path.resolve(integrationDir, codemod.transform);
|
|
123
|
+
const transformMod = await import(pathToFileURL(transformPath).href);
|
|
124
|
+
codemod.transform =
|
|
125
|
+
transformMod.default ?? transformMod.transform ?? transformMod;
|
|
126
|
+
}
|
|
96
127
|
}
|
|
97
128
|
}
|
|
129
|
+
integrations.push({
|
|
130
|
+
...integration,
|
|
131
|
+
__file: file,
|
|
132
|
+
__dir: integrationDir,
|
|
133
|
+
__spec: spec,
|
|
134
|
+
});
|
|
98
135
|
}
|
|
136
|
+
return integrations;
|
|
137
|
+
}
|
|
99
138
|
|
|
100
|
-
|
|
139
|
+
function normalizeIntegrationTransforms(integration, from, to) {
|
|
140
|
+
const transforms = [];
|
|
141
|
+
for (const entry of integration.codemods ?? []) {
|
|
142
|
+
const entryFrom = entry.from ?? '0.0.0';
|
|
143
|
+
const entryTo = entry.to ?? to;
|
|
144
|
+
if (semverGte(from, entryTo) || semverGt(entryFrom, to)) continue;
|
|
145
|
+
if (!entry.name)
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Integration ${integration.name ?? integration.__spec} has a codemod without a name.`,
|
|
148
|
+
);
|
|
149
|
+
if (!entry.transform)
|
|
150
|
+
throw new Error(`Integration codemod ${entry.name} is missing transform.`);
|
|
151
|
+
const directTransform =
|
|
152
|
+
typeof entry.transform === 'function' ? entry.transform : null;
|
|
153
|
+
if (!directTransform)
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Integration codemod ${entry.name} did not resolve to a function.`,
|
|
156
|
+
);
|
|
157
|
+
transforms.push({
|
|
158
|
+
name: entry.name,
|
|
159
|
+
meta: {
|
|
160
|
+
title: entry.title ?? `${integration.name ?? integration.__spec}: ${entry.name}`,
|
|
161
|
+
description: entry.description ?? '',
|
|
162
|
+
pr: entry.pr,
|
|
163
|
+
fileExtensions: entry.fileExtensions,
|
|
164
|
+
},
|
|
165
|
+
optional: !!entry.optional,
|
|
166
|
+
transform: directTransform,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return transforms.length ? [{version: to, transforms}] : [];
|
|
170
|
+
}
|
|
101
171
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + '\n');
|
|
105
|
-
return {bumped, pkgPath};
|
|
172
|
+
function uniqueFiles(files) {
|
|
173
|
+
return [...new Set((files ?? []).filter(Boolean))];
|
|
106
174
|
}
|
|
107
175
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
176
|
+
async function runPostCodemodHooks(integrations, context, silent) {
|
|
177
|
+
const hooks = integrations.flatMap(integration =>
|
|
178
|
+
(integration.postCodemod ?? []).map(hook => ({integration, hook})),
|
|
179
|
+
);
|
|
180
|
+
if (hooks.length === 0) return;
|
|
181
|
+
|
|
182
|
+
const log = silent
|
|
183
|
+
? {info() {}, warn() {}, success() {}, error() {}}
|
|
184
|
+
: p.log;
|
|
185
|
+
|
|
186
|
+
const run = async (command, args, options = {}) => {
|
|
187
|
+
await execFileAsync(command, args, {
|
|
188
|
+
cwd: options.cwd ?? context.packageDir,
|
|
189
|
+
timeout: options.timeoutMs ?? 300_000,
|
|
190
|
+
stdio: 'pipe',
|
|
191
|
+
encoding: 'utf-8',
|
|
192
|
+
env: {...process.env, ...(options.env ?? {})},
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const ctx = {...context, run};
|
|
197
|
+
for (const {integration, hook} of hooks) {
|
|
198
|
+
const label = `${integration.name ?? integration.__spec}:${hook.name ?? 'postCodemod'}`;
|
|
199
|
+
try {
|
|
200
|
+
if (typeof hook.run === 'function') {
|
|
201
|
+
await hook.run(ctx);
|
|
202
|
+
} else if (typeof hook.command === 'function') {
|
|
203
|
+
const cmd = await hook.command(ctx);
|
|
204
|
+
if (cmd) {
|
|
205
|
+
await run(cmd.command, cmd.args ?? [], {
|
|
206
|
+
cwd: cmd.cwd,
|
|
207
|
+
timeoutMs: cmd.timeoutMs,
|
|
208
|
+
env: cmd.env,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
log.warn(`Integration hook ${label} has no run() or command() function; skipping.`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
log.success(`Post-codemod hook ${label} completed.`);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
log.warn(`Post-codemod hook ${label} failed: ${err.message}`);
|
|
218
|
+
}
|
|
122
219
|
}
|
|
123
220
|
}
|
|
124
221
|
|
|
@@ -129,14 +226,16 @@ export function registerUpgrade(program) {
|
|
|
129
226
|
program
|
|
130
227
|
.command('upgrade')
|
|
131
228
|
.description('Run codemods to migrate between versions')
|
|
229
|
+
.option('--from <version>', 'Previous version before the dependency upgrade')
|
|
132
230
|
.option('--apply', 'Write changes to disk (default: dry-run)', false)
|
|
133
|
-
.option('--
|
|
134
|
-
.option('--to <version>', 'Target version', latestVersion)
|
|
135
|
-
.option('--force', 'Run codemods even if versions appear up to date', false)
|
|
231
|
+
.option('--force', 'Run codemods even if --from is newer than the installed version', false)
|
|
136
232
|
.option('--codemod <name>', 'Run a specific transform only')
|
|
137
|
-
.option(
|
|
138
|
-
|
|
139
|
-
|
|
233
|
+
.option(
|
|
234
|
+
'--integration <package-or-file>',
|
|
235
|
+
'Explicit integration package name or integration file path (repeatable)',
|
|
236
|
+
(value, previous) => [...(previous ?? []), value],
|
|
237
|
+
[],
|
|
238
|
+
)
|
|
140
239
|
.option('--path <dir>', 'Source directory to scan', './src')
|
|
141
240
|
.option('--install-deps', 'Auto-install jscodeshift without prompting', false)
|
|
142
241
|
.option('--list', 'List available codemods', false)
|
|
@@ -144,18 +243,17 @@ export function registerUpgrade(program) {
|
|
|
144
243
|
const json = program.opts().json || false;
|
|
145
244
|
if (!json) p.intro('Upgrade');
|
|
146
245
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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);
|
|
246
|
+
if (!options.list && !options.from) {
|
|
247
|
+
const msg = 'Missing required --from. Install the target version first, then run `astryx upgrade --from <old-version>`.';
|
|
248
|
+
if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_ARGUMENT);
|
|
153
249
|
p.log.error(msg);
|
|
154
250
|
p.outro('Aborted');
|
|
155
251
|
process.exitCode = 1;
|
|
156
252
|
return;
|
|
157
253
|
}
|
|
158
|
-
|
|
254
|
+
|
|
255
|
+
// Validate --from upfront so callers don't silently accept typos.
|
|
256
|
+
if (!options.list && !isValidSemver(options.from)) {
|
|
159
257
|
const msg = `Invalid --from value: "${options.from}". Expected a semver string like 0.0.5.`;
|
|
160
258
|
if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_VERSION);
|
|
161
259
|
p.log.error(msg);
|
|
@@ -184,60 +282,66 @@ export function registerUpgrade(program) {
|
|
|
184
282
|
return;
|
|
185
283
|
}
|
|
186
284
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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>.';
|
|
285
|
+
const currentVersion = options.from;
|
|
286
|
+
const installed = detectInstalledTargetVersion();
|
|
287
|
+
if (!installed) {
|
|
288
|
+
const msg = 'Could not find installed @astryxdesign/core (or legacy @xds/core). Install the target version first, then rerun `astryx upgrade --from <old-version>`.';
|
|
200
289
|
if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_VERSION_DETECT);
|
|
201
290
|
p.log.error(msg);
|
|
202
291
|
p.outro('Aborted');
|
|
203
292
|
process.exitCode = 1;
|
|
204
293
|
return;
|
|
205
294
|
}
|
|
295
|
+
const targetVersion = installed.version;
|
|
206
296
|
|
|
207
|
-
|
|
297
|
+
if (!json) {
|
|
298
|
+
p.log.info(`From version: ${currentVersion}`);
|
|
299
|
+
p.log.info(`Installed target: ${targetVersion} (${installed.packageName})`);
|
|
300
|
+
}
|
|
208
301
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
302
|
+
let integrations;
|
|
303
|
+
try {
|
|
304
|
+
integrations = await loadIntegrations(options.integration ?? []);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (json) return jsonError(err.message, undefined, ERROR_CODES.ERR_INVALID_ARGUMENT);
|
|
307
|
+
p.log.error(err.message);
|
|
308
|
+
p.outro('Aborted');
|
|
309
|
+
process.exitCode = 1;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!json && integrations.length > 0) {
|
|
313
|
+
p.log.info(
|
|
314
|
+
`Integrations: ${integrations.map(i => i.name ?? i.__spec).join(', ')}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
214
317
|
|
|
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;
|
|
318
|
+
if (!options.force && semverGte(currentVersion, targetVersion)) {
|
|
319
|
+
if (json) {
|
|
320
|
+
return jsonOut('upgrade.status', {
|
|
321
|
+
status: 'up_to_date',
|
|
322
|
+
from: currentVersion,
|
|
323
|
+
to: targetVersion,
|
|
324
|
+
});
|
|
227
325
|
}
|
|
326
|
+
p.log.success('Already up to date — no codemods to run.');
|
|
327
|
+
p.log.info('Use --force to run codemods anyway.');
|
|
328
|
+
p.outro('Done');
|
|
329
|
+
return;
|
|
228
330
|
}
|
|
229
331
|
|
|
230
332
|
// Resolve transforms
|
|
231
|
-
const versionManifests =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
333
|
+
const versionManifests = [
|
|
334
|
+
...(await getTransformsBetween(currentVersion, targetVersion)),
|
|
335
|
+
...integrations.flatMap(integration =>
|
|
336
|
+
normalizeIntegrationTransforms(integration, currentVersion, targetVersion),
|
|
337
|
+
),
|
|
338
|
+
];
|
|
235
339
|
|
|
236
340
|
if (versionManifests.length === 0) {
|
|
237
341
|
if (json) {
|
|
238
342
|
return jsonOut('upgrade.status', {
|
|
239
343
|
status: 'no_codemods',
|
|
240
|
-
from:
|
|
344
|
+
from: currentVersion,
|
|
241
345
|
to: targetVersion,
|
|
242
346
|
});
|
|
243
347
|
}
|
|
@@ -279,29 +383,7 @@ export function registerUpgrade(program) {
|
|
|
279
383
|
}
|
|
280
384
|
}
|
|
281
385
|
|
|
282
|
-
const receipt = {from: currentVersion, to: targetVersion, codemods: totalTransforms,
|
|
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
|
-
}
|
|
386
|
+
const receipt = {from: currentVersion, to: targetVersion, codemods: totalTransforms, integrations: integrations.map(i => i.name ?? i.__spec), agentDocsRefreshed: false};
|
|
305
387
|
|
|
306
388
|
// Ensure jscodeshift is available
|
|
307
389
|
const ready = await ensureJscodeshift({installDeps: options.installDeps, silent: json});
|
|
@@ -320,6 +402,29 @@ export function registerUpgrade(program) {
|
|
|
320
402
|
silent: json,
|
|
321
403
|
});
|
|
322
404
|
|
|
405
|
+
if (options.apply && integrations.length > 0) {
|
|
406
|
+
const codemodDir = path.resolve(options.path);
|
|
407
|
+
const absoluteChangedFiles = uniqueFiles(codemodResult?.writtenFiles ?? []);
|
|
408
|
+
const changedFiles = absoluteChangedFiles.map(file =>
|
|
409
|
+
path.relative(process.cwd(), file),
|
|
410
|
+
);
|
|
411
|
+
const packageChangedFiles = absoluteChangedFiles
|
|
412
|
+
.filter(file => file.startsWith(process.cwd() + path.sep))
|
|
413
|
+
.map(file => path.relative(process.cwd(), file));
|
|
414
|
+
await runPostCodemodHooks(
|
|
415
|
+
integrations,
|
|
416
|
+
{
|
|
417
|
+
packageDir: process.cwd(),
|
|
418
|
+
codemodDir,
|
|
419
|
+
changedFiles,
|
|
420
|
+
absoluteChangedFiles,
|
|
421
|
+
packageChangedFiles,
|
|
422
|
+
apply: options.apply,
|
|
423
|
+
},
|
|
424
|
+
json,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
323
428
|
// Refresh agent docs if any exist (AGENTS.md, CLAUDE.md, .claude/CLAUDE.md, etc.)
|
|
324
429
|
// Always update after --apply; also update during dry-run if files exist,
|
|
325
430
|
// since the index reflects the installed CLI version, not the codemods.
|
|
@@ -6,7 +6,6 @@ import * as path from 'node:path';
|
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
import {Command} from 'commander';
|
|
8
8
|
import {registerUpgrade} from './upgrade.mjs';
|
|
9
|
-
import {latestVersion} from '../codemods/registry.mjs';
|
|
10
9
|
|
|
11
10
|
let tmpDir;
|
|
12
11
|
let originalCwd;
|
|
@@ -53,13 +52,28 @@ function createProgram() {
|
|
|
53
52
|
return program;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
function writePkg(deps) {
|
|
55
|
+
function writePkg(deps = {}) {
|
|
57
56
|
fs.writeFileSync(
|
|
58
57
|
path.join(tmpDir, 'package.json'),
|
|
59
58
|
JSON.stringify({name: 'fixture', dependencies: deps}, null, 2),
|
|
60
59
|
);
|
|
61
60
|
}
|
|
62
61
|
|
|
62
|
+
function writeInstalledCore(version, packageName = '@astryxdesign/core') {
|
|
63
|
+
const parts = packageName.split('/');
|
|
64
|
+
const dir = path.join(tmpDir, 'node_modules', ...parts);
|
|
65
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
path.join(dir, 'package.json'),
|
|
68
|
+
JSON.stringify({name: packageName, version}, null, 2),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeSourceFile() {
|
|
73
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
74
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'index.ts'), 'const x = 1;\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
/** Run a command and capture the parsed JSON response (last printed JSON line). */
|
|
64
78
|
async function runJson(args) {
|
|
65
79
|
const program = createProgram();
|
|
@@ -83,53 +97,53 @@ async function runJson(args) {
|
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
describe('upgrade gate (semver comparison)', () => {
|
|
86
|
-
it('does NOT block an upgrade from 0.0.9 to 0.0.10 (regression)', async () => {
|
|
100
|
+
it('does NOT block an upgrade from 0.0.9 to installed 0.0.10 (regression)', async () => {
|
|
87
101
|
// The original bug: string compare said '0.0.9' >= '0.0.10', so the
|
|
88
102
|
// gate told users "Already up to date" without --force.
|
|
89
|
-
writePkg(
|
|
103
|
+
writePkg();
|
|
104
|
+
writeInstalledCore('0.0.10');
|
|
105
|
+
writeSourceFile();
|
|
90
106
|
|
|
91
|
-
const result = await runJson(['--json', 'upgrade', '--
|
|
92
|
-
// Either a real run or "no codemods available" — but never the
|
|
93
|
-
// up-to-date short-circuit (which has no `type` field).
|
|
107
|
+
const result = await runJson(['--json', 'upgrade', '--from', '0.0.9', '--path', 'src']);
|
|
94
108
|
expect(result).not.toBeNull();
|
|
95
|
-
// The receipt or "no codemods" path should not look like the
|
|
96
|
-
// up-to-date short-circuit (which would return without printing JSON).
|
|
97
109
|
expect(result.type === 'upgrade.run' || result.error || logCalls.some(l => l.includes('No codemods'))).toBeTruthy();
|
|
98
110
|
});
|
|
99
111
|
|
|
100
|
-
it('blocks when
|
|
101
|
-
writePkg(
|
|
112
|
+
it('blocks when --from >= installed target by semver (e.g. 0.0.10 → 0.0.9)', async () => {
|
|
113
|
+
writePkg();
|
|
114
|
+
writeInstalledCore('0.0.9');
|
|
102
115
|
const program = createProgram();
|
|
103
|
-
await program.parseAsync(['node', 'astryx', 'upgrade', '--
|
|
116
|
+
await program.parseAsync(['node', 'astryx', 'upgrade', '--from', '0.0.10']);
|
|
104
117
|
const output = stdoutCalls.join('') + logCalls.join('\n');
|
|
105
118
|
expect(output).toMatch(/up to date|Already/i);
|
|
106
119
|
});
|
|
107
120
|
});
|
|
108
121
|
|
|
109
|
-
describe('upgrade
|
|
110
|
-
it('
|
|
111
|
-
writePkg(
|
|
112
|
-
|
|
122
|
+
describe('upgrade argument validation', () => {
|
|
123
|
+
it('requires --from for upgrade runs', async () => {
|
|
124
|
+
writePkg();
|
|
125
|
+
writeInstalledCore('0.0.15');
|
|
126
|
+
const result = await runJson(['--json', 'upgrade']);
|
|
113
127
|
expect(result).not.toBeNull();
|
|
114
|
-
expect(result.error).toMatch(/
|
|
128
|
+
expect(result.error).toMatch(/Missing required --from/);
|
|
115
129
|
expect(exitCode).toBe(1);
|
|
116
130
|
});
|
|
117
131
|
|
|
118
132
|
it('rejects bogus --from values', async () => {
|
|
119
|
-
writePkg(
|
|
120
|
-
|
|
133
|
+
writePkg();
|
|
134
|
+
writeInstalledCore('0.0.15');
|
|
135
|
+
const result = await runJson(['--json', 'upgrade', '--from', 'not-a-version']);
|
|
121
136
|
expect(result).not.toBeNull();
|
|
122
137
|
expect(result.error).toMatch(/Invalid --from/);
|
|
123
138
|
expect(exitCode).toBe(1);
|
|
124
139
|
});
|
|
125
140
|
|
|
126
|
-
it('
|
|
127
|
-
writePkg(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const result = await runJson(['--json', 'upgrade', '--
|
|
131
|
-
|
|
132
|
-
expect(result?.error || '').not.toMatch(/Invalid --to/);
|
|
141
|
+
it('detects the installed target version from @astryxdesign/core', async () => {
|
|
142
|
+
writePkg();
|
|
143
|
+
writeInstalledCore('0.0.15');
|
|
144
|
+
writeSourceFile();
|
|
145
|
+
const result = await runJson(['--json', 'upgrade', '--from', '0.0.14', '--path', 'src']);
|
|
146
|
+
expect(result?.error || '').not.toMatch(/Could not find installed/);
|
|
133
147
|
});
|
|
134
148
|
});
|
|
135
149
|
|