@astryxdesign/cli 0.1.0-canary.e2d38fb → 0.1.0-canary.eb78210
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/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +0 -14
- package/docs/theme.doc.zh.mjs +2 -2
- package/package.json +8 -8
- package/src/codemods/__tests__/registry.test.mjs +0 -1
- package/src/codemods/registry.mjs +0 -1
- package/src/codemods/runner.mjs +51 -105
- package/src/commands/agent-docs.mjs +56 -92
- package/src/commands/agent-docs.test.mjs +9 -65
- package/src/commands/build.mjs +29 -52
- package/src/commands/init.mjs +1 -9
- package/src/commands/upgrade.mjs +38 -117
- package/src/lib/config.mjs +0 -12
- package/src/lib/error-codes.mjs +0 -3
- package/src/types/error-codes.d.ts +0 -1
- package/src/utils/update-check.mjs +26 -4
- package/src/utils/update-check.test.mjs +63 -1
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +0 -51
- package/src/codemods/transforms/v0.1.0/index.mjs +0 -28
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +0 -84
- package/src/lib/config.test.mjs +0 -42
package/src/commands/build.mjs
CHANGED
|
@@ -24,21 +24,6 @@ import {search as searchApi} from '../api/search.mjs';
|
|
|
24
24
|
const PAGE_DIRECT = 95;
|
|
25
25
|
/** Below this a page is too weak to even offer as a layout reference. */
|
|
26
26
|
const PAGE_FLOOR = 50;
|
|
27
|
-
/** Below this a block/domain-component match is incidental noise, not surfaced. */
|
|
28
|
-
const DOMAIN_FLOOR = 55;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Always-surfaced primitives. Every page needs a shell + layout/typography/
|
|
32
|
-
* action atoms, but these never keyword-match an idea ("dashboard" != "Stack"),
|
|
33
|
-
* so search alone never returns them. We list them unconditionally so an agent
|
|
34
|
-
* composing from scratch has the whole kit (esp. off-template).
|
|
35
|
-
*/
|
|
36
|
-
const FRAME = ['AppShell', 'TopNav', 'SideNav', 'Layout'];
|
|
37
|
-
const FOUNDATION = [
|
|
38
|
-
'VStack', 'HStack', 'Grid', 'StackItem', 'Card', 'Section',
|
|
39
|
-
'Text', 'Heading', 'Button', 'Icon', 'Badge', 'Divider',
|
|
40
|
-
];
|
|
41
|
-
const ALWAYS = new Set([...FRAME, ...FOUNDATION]);
|
|
42
27
|
|
|
43
28
|
/** Print the build playbook (shown when `build` is run with no query). */
|
|
44
29
|
function printPlaybook(run) {
|
|
@@ -120,17 +105,14 @@ export function registerBuild(program) {
|
|
|
120
105
|
return;
|
|
121
106
|
}
|
|
122
107
|
|
|
123
|
-
//
|
|
108
|
+
// Group by role so an agent can assemble a UI from the pieces.
|
|
124
109
|
const pages = results
|
|
125
110
|
.filter(r => r.domain === 'template' && r.kind !== 'block' && r.score >= PAGE_FLOOR)
|
|
126
111
|
.slice(0, 3);
|
|
127
|
-
const blocks = results
|
|
128
|
-
|
|
112
|
+
const blocks = results.filter(r => r.domain === 'template' && r.kind === 'block').slice(0, 5);
|
|
113
|
+
const atoms = results
|
|
114
|
+
.filter(r => r.domain === 'component' || r.domain === 'hook')
|
|
129
115
|
.slice(0, 5);
|
|
130
|
-
// Idea-specific atoms = matched components/hooks MINUS the always-on kit.
|
|
131
|
-
const domain = results
|
|
132
|
-
.filter(r => (r.domain === 'component' || r.domain === 'hook') && r.score >= DOMAIN_FLOOR && !ALWAYS.has(r.name))
|
|
133
|
-
.slice(0, 6);
|
|
134
116
|
const directMatch = pages.length > 0 && pages[0].score >= PAGE_DIRECT;
|
|
135
117
|
|
|
136
118
|
const printItem = (r, label) => {
|
|
@@ -148,49 +130,44 @@ export function registerBuild(program) {
|
|
|
148
130
|
humanLog('');
|
|
149
131
|
humanLog(`Building "${q}":`);
|
|
150
132
|
|
|
151
|
-
|
|
152
|
-
humanLog('');
|
|
153
|
-
if (directMatch) {
|
|
154
|
-
humanLog(`START → Scaffold the \`${pages[0].name}\` page template, then adapt: ${run} astryx template ${pages[0].name} ./src/App.tsx`);
|
|
155
|
-
} else if (pages.length) {
|
|
156
|
-
humanLog(`START → No exact page template. Use \`${pages[0].name}\` as a layout reference (${run} astryx template ${pages[0].name} --skeleton) and compose the pieces below.`);
|
|
157
|
-
} else {
|
|
158
|
-
humanLog(`START → No page template fits. Frame with AppShell and compose the blocks + components below.`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// PAGE
|
|
133
|
+
let frame = null;
|
|
162
134
|
if (pages.length) {
|
|
163
135
|
humanLog('');
|
|
164
|
-
humanLog(
|
|
136
|
+
humanLog(
|
|
137
|
+
directMatch
|
|
138
|
+
? 'PAGE TEMPLATE — looks like a direct match (scaffold this):'
|
|
139
|
+
: 'CLOSEST PAGE TEMPLATES — scaffold if one fits, or use as a layout reference:',
|
|
140
|
+
);
|
|
165
141
|
pages.forEach(p => printItem(p, directMatch ? 'page' : 'closest'));
|
|
142
|
+
humanLog(` (study structure: ${run} astryx template ${pages[0].name} --skeleton)`);
|
|
143
|
+
frame = pages[0];
|
|
144
|
+
} else {
|
|
145
|
+
humanLog('');
|
|
146
|
+
humanLog('NO MATCHING PAGE TEMPLATE — compose from the blocks + components below:');
|
|
166
147
|
}
|
|
167
148
|
|
|
168
|
-
// FRAME — always (the page shell).
|
|
169
|
-
humanLog('');
|
|
170
|
-
humanLog(`FRAME — page shell (always): ${FRAME.join(', ')}`);
|
|
171
|
-
humanLog(` full-page → AppShell; or Layout + SideNav/TopNav. ${run} astryx component AppShell`);
|
|
172
|
-
|
|
173
|
-
// BLOCKS — idea-specific composed patterns.
|
|
174
149
|
if (blocks.length) {
|
|
175
150
|
humanLog('');
|
|
176
|
-
humanLog('BLOCKS — drop-in patterns that cover parts of it:');
|
|
151
|
+
humanLog('BLOCKS — drop-in patterns that likely cover parts of it:');
|
|
177
152
|
blocks.forEach(b => printItem(b, 'block'));
|
|
178
153
|
}
|
|
179
154
|
|
|
180
|
-
|
|
181
|
-
if (domain.length) {
|
|
155
|
+
if (atoms.length) {
|
|
182
156
|
humanLog('');
|
|
183
|
-
humanLog('
|
|
184
|
-
|
|
157
|
+
humanLog('COMPONENTS — building blocks to fill the gaps:');
|
|
158
|
+
atoms.forEach(c => printItem(c, c.domain === 'hook' ? 'hook' : 'component'));
|
|
185
159
|
}
|
|
186
160
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
161
|
+
const parts = [];
|
|
162
|
+
if (frame) {
|
|
163
|
+
parts.push(directMatch ? `scaffold \`${frame.name}\`` : `frame with \`${frame.name}\` (--skeleton)`);
|
|
164
|
+
}
|
|
165
|
+
if (blocks.length) parts.push(`drop in ${blocks.slice(0, 2).map(b => '`' + b.name + '`').join(', ')}`);
|
|
166
|
+
if (atoms.length) parts.push(`fill with ${atoms.slice(0, 2).map(c => '`' + c.name + '`').join(', ')}`);
|
|
167
|
+
if (parts.length) {
|
|
168
|
+
humanLog('');
|
|
169
|
+
humanLog('Compose: ' + parts.join(' → '));
|
|
170
|
+
}
|
|
194
171
|
humanLog('');
|
|
195
172
|
});
|
|
196
173
|
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -100,15 +100,7 @@ async function runTemplate(targetDir, {interactive = true, templateName} = {}) {
|
|
|
100
100
|
|
|
101
101
|
if (!interactive) {
|
|
102
102
|
if (!templateName) {
|
|
103
|
-
|
|
104
|
-
// names — `build` surfaces pages AND blocks AND components for an idea,
|
|
105
|
-
// and `build` with no args is the full how-to-build playbook.
|
|
106
|
-
humanLog('✓ To build UI, use these commands:');
|
|
107
|
-
humanLog('');
|
|
108
|
-
humanLog(` ${run} astryx build "<what you're building>" build a page — kit: closest template + blocks + components`);
|
|
109
|
-
humanLog(` ${run} astryx build the how-to-build workflow (read this first)`);
|
|
110
|
-
humanLog(` ${run} astryx search <query> find anything — components, docs, templates, blocks`);
|
|
111
|
-
humanLog('');
|
|
103
|
+
humanLog(`✓ Available templates: ${templates.join(', ')}. Use ${run} astryx template <name> [path].`);
|
|
112
104
|
return;
|
|
113
105
|
}
|
|
114
106
|
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -31,13 +31,15 @@ import {execFile} from 'node:child_process';
|
|
|
31
31
|
import {promisify} from 'node:util';
|
|
32
32
|
import * as p from '@clack/prompts';
|
|
33
33
|
import {ensureJscodeshift} from '../codemods/ensure-jscodeshift.mjs';
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
getTransformsBetween,
|
|
36
|
+
latestVersion,
|
|
37
|
+
} from '../codemods/registry.mjs';
|
|
35
38
|
import {runCodemods} from '../codemods/runner.mjs';
|
|
36
39
|
import {installAgentDocs, discoverAgentDocs} from './agent-docs.mjs';
|
|
37
40
|
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
38
41
|
import {isValidSemver, semverGte, semverGt} from '../utils/semver.mjs';
|
|
39
42
|
import {jsonOut, jsonError} from '../lib/json.mjs';
|
|
40
|
-
import {loadConfig} from '../lib/config.mjs';
|
|
41
43
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
42
44
|
|
|
43
45
|
const execFileAsync = promisify(execFile);
|
|
@@ -64,6 +66,7 @@ function detectInstalledTargetVersion() {
|
|
|
64
66
|
return null;
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
|
|
67
70
|
function isPathSpec(spec) {
|
|
68
71
|
return (
|
|
69
72
|
spec.startsWith('.') ||
|
|
@@ -144,9 +147,7 @@ function normalizeIntegrationTransforms(integration, from, to) {
|
|
|
144
147
|
`Integration ${integration.name ?? integration.__spec} has a codemod without a name.`,
|
|
145
148
|
);
|
|
146
149
|
if (!entry.transform)
|
|
147
|
-
throw new Error(
|
|
148
|
-
`Integration codemod ${entry.name} is missing transform.`,
|
|
149
|
-
);
|
|
150
|
+
throw new Error(`Integration codemod ${entry.name} is missing transform.`);
|
|
150
151
|
const directTransform =
|
|
151
152
|
typeof entry.transform === 'function' ? entry.transform : null;
|
|
152
153
|
if (!directTransform)
|
|
@@ -156,9 +157,7 @@ function normalizeIntegrationTransforms(integration, from, to) {
|
|
|
156
157
|
transforms.push({
|
|
157
158
|
name: entry.name,
|
|
158
159
|
meta: {
|
|
159
|
-
title:
|
|
160
|
-
entry.title ??
|
|
161
|
-
`${integration.name ?? integration.__spec}: ${entry.name}`,
|
|
160
|
+
title: entry.title ?? `${integration.name ?? integration.__spec}: ${entry.name}`,
|
|
162
161
|
description: entry.description ?? '',
|
|
163
162
|
pr: entry.pr,
|
|
164
163
|
fileExtensions: entry.fileExtensions,
|
|
@@ -180,7 +179,9 @@ async function runPostCodemodHooks(integrations, context, silent) {
|
|
|
180
179
|
);
|
|
181
180
|
if (hooks.length === 0) return;
|
|
182
181
|
|
|
183
|
-
const log = silent
|
|
182
|
+
const log = silent
|
|
183
|
+
? {info() {}, warn() {}, success() {}, error() {}}
|
|
184
|
+
: p.log;
|
|
184
185
|
|
|
185
186
|
const run = async (command, args, options = {}) => {
|
|
186
187
|
await execFileAsync(command, args, {
|
|
@@ -208,9 +209,7 @@ async function runPostCodemodHooks(integrations, context, silent) {
|
|
|
208
209
|
});
|
|
209
210
|
}
|
|
210
211
|
} else {
|
|
211
|
-
log.warn(
|
|
212
|
-
`Integration hook ${label} has no run() or command() function; skipping.`,
|
|
213
|
-
);
|
|
212
|
+
log.warn(`Integration hook ${label} has no run() or command() function; skipping.`);
|
|
214
213
|
continue;
|
|
215
214
|
}
|
|
216
215
|
log.success(`Post-codemod hook ${label} completed.`);
|
|
@@ -227,16 +226,9 @@ export function registerUpgrade(program) {
|
|
|
227
226
|
program
|
|
228
227
|
.command('upgrade')
|
|
229
228
|
.description('Run codemods to migrate between versions')
|
|
230
|
-
.option(
|
|
231
|
-
'--from <version>',
|
|
232
|
-
'Previous version before the dependency upgrade',
|
|
233
|
-
)
|
|
229
|
+
.option('--from <version>', 'Previous version before the dependency upgrade')
|
|
234
230
|
.option('--apply', 'Write changes to disk (default: dry-run)', false)
|
|
235
|
-
.option(
|
|
236
|
-
'--force',
|
|
237
|
-
'Run codemods even if --from is newer than the installed version',
|
|
238
|
-
false,
|
|
239
|
-
)
|
|
231
|
+
.option('--force', 'Run codemods even if --from is newer than the installed version', false)
|
|
240
232
|
.option('--codemod <name>', 'Run a specific transform only')
|
|
241
233
|
.option(
|
|
242
234
|
'--integration <package-or-file>',
|
|
@@ -245,21 +237,15 @@ export function registerUpgrade(program) {
|
|
|
245
237
|
[],
|
|
246
238
|
)
|
|
247
239
|
.option('--path <dir>', 'Source directory to scan', './src')
|
|
248
|
-
.option(
|
|
249
|
-
'--install-deps',
|
|
250
|
-
'Auto-install jscodeshift without prompting',
|
|
251
|
-
false,
|
|
252
|
-
)
|
|
240
|
+
.option('--install-deps', 'Auto-install jscodeshift without prompting', false)
|
|
253
241
|
.option('--list', 'List available codemods', false)
|
|
254
|
-
.action(async options => {
|
|
242
|
+
.action(async (options) => {
|
|
255
243
|
const json = program.opts().json || false;
|
|
256
244
|
if (!json) p.intro('Upgrade');
|
|
257
245
|
|
|
258
246
|
if (!options.list && !options.from) {
|
|
259
|
-
const msg =
|
|
260
|
-
|
|
261
|
-
if (json)
|
|
262
|
-
return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_ARGUMENT);
|
|
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);
|
|
263
249
|
p.log.error(msg);
|
|
264
250
|
p.outro('Aborted');
|
|
265
251
|
process.exitCode = 1;
|
|
@@ -269,8 +255,7 @@ export function registerUpgrade(program) {
|
|
|
269
255
|
// Validate --from upfront so callers don't silently accept typos.
|
|
270
256
|
if (!options.list && !isValidSemver(options.from)) {
|
|
271
257
|
const msg = `Invalid --from value: "${options.from}". Expected a semver string like 0.0.5.`;
|
|
272
|
-
if (json)
|
|
273
|
-
return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_VERSION);
|
|
258
|
+
if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_VERSION);
|
|
274
259
|
p.log.error(msg);
|
|
275
260
|
p.outro('Aborted');
|
|
276
261
|
process.exitCode = 1;
|
|
@@ -285,30 +270,13 @@ export function registerUpgrade(program) {
|
|
|
285
270
|
const manifests = await getTransformsBetween('0.0.0', latestVersion);
|
|
286
271
|
for (const {version, transforms} of manifests) {
|
|
287
272
|
for (const {name, meta, optional} of transforms) {
|
|
288
|
-
codemods.push({
|
|
289
|
-
name,
|
|
290
|
-
title: meta.title,
|
|
291
|
-
version,
|
|
292
|
-
pr: meta.pr,
|
|
293
|
-
optional: !!optional,
|
|
294
|
-
});
|
|
273
|
+
codemods.push({name, title: meta.title, version, pr: meta.pr, optional: !!optional});
|
|
295
274
|
}
|
|
296
275
|
}
|
|
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
|
-
);
|
|
276
|
+
if (json) return jsonOut('upgrade.list', codemods.map(({name, title, version, optional}) => ({name, title, version, optional})));
|
|
307
277
|
p.log.step('Available codemods:');
|
|
308
278
|
for (const {name, title, pr, optional} of codemods) {
|
|
309
|
-
p.log.info(
|
|
310
|
-
` ${name} — ${title}${optional ? ' (optional)' : ''} (${pr})`,
|
|
311
|
-
);
|
|
279
|
+
p.log.info(` ${name} — ${title}${optional ? ' (optional)' : ''} (${pr})`);
|
|
312
280
|
}
|
|
313
281
|
p.outro('Done');
|
|
314
282
|
return;
|
|
@@ -317,10 +285,8 @@ export function registerUpgrade(program) {
|
|
|
317
285
|
const currentVersion = options.from;
|
|
318
286
|
const installed = detectInstalledTargetVersion();
|
|
319
287
|
if (!installed) {
|
|
320
|
-
const msg =
|
|
321
|
-
|
|
322
|
-
if (json)
|
|
323
|
-
return jsonError(msg, undefined, ERROR_CODES.ERR_VERSION_DETECT);
|
|
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>`.';
|
|
289
|
+
if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_VERSION_DETECT);
|
|
324
290
|
p.log.error(msg);
|
|
325
291
|
p.outro('Aborted');
|
|
326
292
|
process.exitCode = 1;
|
|
@@ -330,26 +296,14 @@ export function registerUpgrade(program) {
|
|
|
330
296
|
|
|
331
297
|
if (!json) {
|
|
332
298
|
p.log.info(`From version: ${currentVersion}`);
|
|
333
|
-
p.log.info(
|
|
334
|
-
`Installed target: ${targetVersion} (${installed.packageName})`,
|
|
335
|
-
);
|
|
299
|
+
p.log.info(`Installed target: ${targetVersion} (${installed.packageName})`);
|
|
336
300
|
}
|
|
337
301
|
|
|
338
302
|
let integrations;
|
|
339
303
|
try {
|
|
340
|
-
|
|
341
|
-
const integrationSpecs = uniqueFiles([
|
|
342
|
-
...(config.integrations ?? []),
|
|
343
|
-
...(options.integration ?? []),
|
|
344
|
-
]);
|
|
345
|
-
integrations = await loadIntegrations(integrationSpecs);
|
|
304
|
+
integrations = await loadIntegrations(options.integration ?? []);
|
|
346
305
|
} catch (err) {
|
|
347
|
-
if (json)
|
|
348
|
-
return jsonError(
|
|
349
|
-
err.message,
|
|
350
|
-
undefined,
|
|
351
|
-
ERROR_CODES.ERR_INVALID_ARGUMENT,
|
|
352
|
-
);
|
|
306
|
+
if (json) return jsonError(err.message, undefined, ERROR_CODES.ERR_INVALID_ARGUMENT);
|
|
353
307
|
p.log.error(err.message);
|
|
354
308
|
p.outro('Aborted');
|
|
355
309
|
process.exitCode = 1;
|
|
@@ -379,11 +333,7 @@ export function registerUpgrade(program) {
|
|
|
379
333
|
const versionManifests = [
|
|
380
334
|
...(await getTransformsBetween(currentVersion, targetVersion)),
|
|
381
335
|
...integrations.flatMap(integration =>
|
|
382
|
-
normalizeIntegrationTransforms(
|
|
383
|
-
integration,
|
|
384
|
-
currentVersion,
|
|
385
|
-
targetVersion,
|
|
386
|
-
),
|
|
336
|
+
normalizeIntegrationTransforms(integration, currentVersion, targetVersion),
|
|
387
337
|
),
|
|
388
338
|
];
|
|
389
339
|
|
|
@@ -416,8 +366,7 @@ export function registerUpgrade(program) {
|
|
|
416
366
|
|
|
417
367
|
if (totalTransforms === 0 && totalOptional === 0) {
|
|
418
368
|
const msg = `Codemod "${options.codemod}" not found. Use --list to see available codemods.`;
|
|
419
|
-
if (json)
|
|
420
|
-
return jsonError(msg, undefined, ERROR_CODES.ERR_UNKNOWN_CODEMOD);
|
|
369
|
+
if (json) return jsonError(msg, undefined, ERROR_CODES.ERR_UNKNOWN_CODEMOD);
|
|
421
370
|
p.log.error(msg);
|
|
422
371
|
p.outro('Aborted');
|
|
423
372
|
process.exitCode = 1;
|
|
@@ -434,26 +383,12 @@ export function registerUpgrade(program) {
|
|
|
434
383
|
}
|
|
435
384
|
}
|
|
436
385
|
|
|
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
|
-
};
|
|
386
|
+
const receipt = {from: currentVersion, to: targetVersion, codemods: totalTransforms, integrations: integrations.map(i => i.name ?? i.__spec), agentDocsRefreshed: false};
|
|
444
387
|
|
|
445
388
|
// Ensure jscodeshift is available
|
|
446
|
-
const ready = await ensureJscodeshift({
|
|
447
|
-
installDeps: options.installDeps,
|
|
448
|
-
silent: json,
|
|
449
|
-
});
|
|
389
|
+
const ready = await ensureJscodeshift({installDeps: options.installDeps, silent: json});
|
|
450
390
|
if (!ready) {
|
|
451
|
-
if (json)
|
|
452
|
-
return jsonError(
|
|
453
|
-
'jscodeshift is required but could not be installed.',
|
|
454
|
-
undefined,
|
|
455
|
-
ERROR_CODES.ERR_DEP_MISSING,
|
|
456
|
-
);
|
|
391
|
+
if (json) return jsonError('jscodeshift is required but could not be installed.', undefined, ERROR_CODES.ERR_DEP_MISSING);
|
|
457
392
|
p.outro('Aborted');
|
|
458
393
|
process.exitCode = 1;
|
|
459
394
|
return;
|
|
@@ -469,9 +404,7 @@ export function registerUpgrade(program) {
|
|
|
469
404
|
|
|
470
405
|
if (options.apply && integrations.length > 0) {
|
|
471
406
|
const codemodDir = path.resolve(options.path);
|
|
472
|
-
const absoluteChangedFiles = uniqueFiles(
|
|
473
|
-
codemodResult?.writtenFiles ?? [],
|
|
474
|
-
);
|
|
407
|
+
const absoluteChangedFiles = uniqueFiles(codemodResult?.writtenFiles ?? []);
|
|
475
408
|
const changedFiles = absoluteChangedFiles.map(file =>
|
|
476
409
|
path.relative(process.cwd(), file),
|
|
477
410
|
);
|
|
@@ -502,8 +435,7 @@ export function registerUpgrade(program) {
|
|
|
502
435
|
// Don't inject into files that never had Astryx content.
|
|
503
436
|
const written = installAgentDocs(process.cwd(), {onlyReplace: true});
|
|
504
437
|
receipt.agentDocsRefreshed = written.length > 0;
|
|
505
|
-
if (!json && written.length > 0)
|
|
506
|
-
p.log.success(`Agent docs updated: ${written.join(', ')}`);
|
|
438
|
+
if (!json && written.length > 0) p.log.success(`Agent docs updated: ${written.join(', ')}`);
|
|
507
439
|
} catch {
|
|
508
440
|
if (!json) {
|
|
509
441
|
p.log.warn(
|
|
@@ -513,23 +445,12 @@ export function registerUpgrade(program) {
|
|
|
513
445
|
}
|
|
514
446
|
}
|
|
515
447
|
|
|
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);
|
|
526
|
-
}
|
|
527
|
-
p.outro('Upgrade failed');
|
|
528
|
-
process.exitCode = 1;
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
448
|
if (json) {
|
|
449
|
+
if (codemodResult && typeof codemodResult === 'object') {
|
|
450
|
+
receipt.filesChanged = codemodResult.totalFilesChanged ?? 0;
|
|
451
|
+
receipt.transformsApplied = codemodResult.totalTransformsApplied ?? 0;
|
|
452
|
+
receipt.errors = codemodResult.errors ?? [];
|
|
453
|
+
}
|
|
533
454
|
return jsonOut('upgrade.run', receipt);
|
|
534
455
|
}
|
|
535
456
|
p.outro(options.apply ? 'Upgrade complete' : 'Dry run complete');
|
package/src/lib/config.mjs
CHANGED
|
@@ -13,7 +13,6 @@ import {pathToFileURL} from 'node:url';
|
|
|
13
13
|
|
|
14
14
|
const DEFAULTS = {
|
|
15
15
|
packages: [],
|
|
16
|
-
integrations: [],
|
|
17
16
|
};
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -47,7 +46,6 @@ export async function loadConfig(startDir = process.cwd()) {
|
|
|
47
46
|
...DEFAULTS,
|
|
48
47
|
...config,
|
|
49
48
|
packages: normalizePackages(config.packages, path.dirname(configPath)),
|
|
50
|
-
integrations: normalizeIntegrations(config.integrations),
|
|
51
49
|
};
|
|
52
50
|
} catch {
|
|
53
51
|
return {...DEFAULTS};
|
|
@@ -74,13 +72,3 @@ function normalizePackages(packages, configDir) {
|
|
|
74
72
|
}
|
|
75
73
|
return result;
|
|
76
74
|
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Normalize integration specs to a string array. Integrations are package names
|
|
80
|
-
* or manifest paths consumed by `astryx upgrade --integration`.
|
|
81
|
-
*/
|
|
82
|
-
function normalizeIntegrations(integrations) {
|
|
83
|
-
if (!integrations) return [];
|
|
84
|
-
const arr = Array.isArray(integrations) ? integrations : [integrations];
|
|
85
|
-
return arr.filter(value => typeof value === 'string' && value !== '');
|
|
86
|
-
}
|
package/src/lib/error-codes.mjs
CHANGED
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
* | 'ERR_UNKNOWN_AGENT'
|
|
57
57
|
* | 'ERR_UNKNOWN_FEATURE'
|
|
58
58
|
* | 'ERR_UNKNOWN_CODEMOD'
|
|
59
|
-
| 'ERR_CODEMOD_FAILED'
|
|
60
59
|
* | 'ERR_NOT_FOUND'
|
|
61
60
|
* | 'ERR_NO_DOC'
|
|
62
61
|
* | 'ERR_NO_SHOWCASE'
|
|
@@ -130,8 +129,6 @@ export const ERROR_CODES = Object.freeze({
|
|
|
130
129
|
ERR_UNKNOWN_FEATURE: 'ERR_UNKNOWN_FEATURE',
|
|
131
130
|
/** A `--codemod` value did not match any registered codemod (upgrade). */
|
|
132
131
|
ERR_UNKNOWN_CODEMOD: 'ERR_UNKNOWN_CODEMOD',
|
|
133
|
-
/** One or more codemods failed during an upgrade run. */
|
|
134
|
-
ERR_CODEMOD_FAILED: 'ERR_CODEMOD_FAILED',
|
|
135
132
|
/** A generic discover/lookup query matched nothing in any package. */
|
|
136
133
|
ERR_NOT_FOUND: 'ERR_NOT_FOUND',
|
|
137
134
|
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* @file Check for newer @astryxdesign/core versions via local signals.
|
|
5
5
|
*
|
|
6
6
|
* Resolution order:
|
|
7
|
-
* 1. $ASTRYX_LATEST_VERSION env var
|
|
8
|
-
* 2.
|
|
7
|
+
* 1. $ASTRYX_LATEST_VERSION env var (set by previous CLI invocation)
|
|
8
|
+
* 2. xds.versionFile in consumer's package.json (e.g. ../../libs/xds-common/LATEST_VERSION)
|
|
9
|
+
* 3. If neither: no hint (no network calls from this module)
|
|
9
10
|
*
|
|
10
11
|
* When a newer version is detected, returns a hint string for CLI output.
|
|
11
12
|
* Also sets $ASTRYX_LATEST_VERSION for subsequent commands in the same shell.
|
|
@@ -22,13 +23,34 @@ import {semverGt} from './semver.mjs';
|
|
|
22
23
|
* @param {string} [cwd] - Project directory (default: process.cwd())
|
|
23
24
|
* @returns {string|null} Latest version string, or null if unknown
|
|
24
25
|
*/
|
|
25
|
-
export function getLatestVersion() {
|
|
26
|
+
export function getLatestVersion(cwd = process.cwd()) {
|
|
26
27
|
// 1. Check env var (fastest — set by previous CLI run)
|
|
27
28
|
const envVersion = process.env.ASTRYX_LATEST_VERSION;
|
|
28
29
|
if (envVersion && /^\d+\.\d+\.\d+/.test(envVersion)) {
|
|
29
30
|
return envVersion;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// 2. Check versionFile from package.json config
|
|
34
|
+
try {
|
|
35
|
+
const pkgPath = path.resolve(cwd, 'package.json');
|
|
36
|
+
if (fs.existsSync(pkgPath)) {
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
38
|
+
const versionFile = pkg.astryx?.versionFile;
|
|
39
|
+
|
|
40
|
+
if (versionFile) {
|
|
41
|
+
const filePath = path.resolve(cwd, versionFile);
|
|
42
|
+
if (fs.existsSync(filePath)) {
|
|
43
|
+
const version = fs.readFileSync(filePath, 'utf-8').trim();
|
|
44
|
+
if (/^\d+\.\d+\.\d+/.test(version)) {
|
|
45
|
+
return version;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Silently fail — version check should never break the CLI
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
return null;
|
|
33
55
|
}
|
|
34
56
|
|
|
@@ -75,7 +97,7 @@ export function checkForUpdate(cwd = process.cwd()) {
|
|
|
75
97
|
// Use semver-aware comparison so '0.0.20' is correctly treated as greater
|
|
76
98
|
// than '0.0.5' (lexicographic compare gets that backwards).
|
|
77
99
|
if (semverGt(latest, installed)) {
|
|
78
|
-
return `FYI: A newer version of @astryxdesign/core (${latest}) is available.
|
|
100
|
+
return `FYI: A newer version of @astryxdesign/core (${latest}) is available. You can upgrade with: astryx upgrade --apply --to ${latest}`;
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
return null;
|
|
@@ -41,6 +41,17 @@ describe('getLatestVersion', () => {
|
|
|
41
41
|
expect(getLatestVersion(tmpDir)).toBeNull();
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('reads from versionFile in package.json', () => {
|
|
45
|
+
const versionFilePath = path.join(tmpDir, 'LATEST_VERSION');
|
|
46
|
+
fs.writeFileSync(versionFilePath, '0.0.9\n');
|
|
47
|
+
fs.writeFileSync(
|
|
48
|
+
path.join(tmpDir, 'package.json'),
|
|
49
|
+
JSON.stringify({astryx: {versionFile: './LATEST_VERSION'}}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(getLatestVersion(tmpDir)).toBe('0.0.9');
|
|
53
|
+
});
|
|
54
|
+
|
|
44
55
|
it('returns null when no signals exist', () => {
|
|
45
56
|
fs.writeFileSync(
|
|
46
57
|
path.join(tmpDir, 'package.json'),
|
|
@@ -48,6 +59,26 @@ describe('getLatestVersion', () => {
|
|
|
48
59
|
);
|
|
49
60
|
expect(getLatestVersion(tmpDir)).toBeNull();
|
|
50
61
|
});
|
|
62
|
+
|
|
63
|
+
it('returns null when versionFile path does not exist', () => {
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
path.join(tmpDir, 'package.json'),
|
|
66
|
+
JSON.stringify({astryx: {versionFile: './missing/LATEST_VERSION'}}),
|
|
67
|
+
);
|
|
68
|
+
expect(getLatestVersion(tmpDir)).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('env var takes priority over versionFile', () => {
|
|
72
|
+
process.env.ASTRYX_LATEST_VERSION = '1.0.0';
|
|
73
|
+
const versionFilePath = path.join(tmpDir, 'LATEST_VERSION');
|
|
74
|
+
fs.writeFileSync(versionFilePath, '0.0.9\n');
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(tmpDir, 'package.json'),
|
|
77
|
+
JSON.stringify({astryx: {versionFile: './LATEST_VERSION'}}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(getLatestVersion(tmpDir)).toBe('1.0.0');
|
|
81
|
+
});
|
|
51
82
|
});
|
|
52
83
|
|
|
53
84
|
describe('getInstalledVersion', () => {
|
|
@@ -86,7 +117,7 @@ describe('checkForUpdate', () => {
|
|
|
86
117
|
|
|
87
118
|
const hint = checkForUpdate(tmpDir);
|
|
88
119
|
expect(hint).toContain('0.0.8');
|
|
89
|
-
expect(hint).toContain('astryx upgrade --
|
|
120
|
+
expect(hint).toContain('astryx upgrade --apply --to 0.0.8');
|
|
90
121
|
expect(hint).toContain('FYI');
|
|
91
122
|
});
|
|
92
123
|
|
|
@@ -109,6 +140,37 @@ describe('checkForUpdate', () => {
|
|
|
109
140
|
expect(checkForUpdate(tmpDir)).toBeNull();
|
|
110
141
|
});
|
|
111
142
|
|
|
143
|
+
it('sets env var for subsequent calls', () => {
|
|
144
|
+
const versionFilePath = path.join(tmpDir, 'LATEST_VERSION');
|
|
145
|
+
fs.writeFileSync(versionFilePath, '0.0.8\n');
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
path.join(tmpDir, 'package.json'),
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
dependencies: {'@astryxdesign/core': '^0.0.7'},
|
|
150
|
+
astryx: {versionFile: './LATEST_VERSION'},
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
checkForUpdate(tmpDir);
|
|
155
|
+
expect(process.env.ASTRYX_LATEST_VERSION).toBe('0.0.8');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('reads from versionFile and produces hint', () => {
|
|
159
|
+
const versionFilePath = path.join(tmpDir, 'LATEST_VERSION');
|
|
160
|
+
fs.writeFileSync(versionFilePath, '0.0.9\n');
|
|
161
|
+
fs.writeFileSync(
|
|
162
|
+
path.join(tmpDir, 'package.json'),
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
dependencies: {'@astryxdesign/core': '^0.0.7'},
|
|
165
|
+
astryx: {versionFile: './LATEST_VERSION'},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const hint = checkForUpdate(tmpDir);
|
|
170
|
+
expect(hint).toContain('0.0.9');
|
|
171
|
+
expect(hint).toContain('FYI');
|
|
172
|
+
});
|
|
173
|
+
|
|
112
174
|
it('uses semver (not lexicographic) comparison for double-digit patches', () => {
|
|
113
175
|
// Regression: with string compare, '0.0.20' > '0.0.5' is false because
|
|
114
176
|
// '2' < '5' lexicographically. Users on 0.0.5 would never be told that
|