@astryxdesign/cli 0.6.3-canary.ddb63c3 → 0.6.3-canary.df1837b
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 +2 -0
- package/api/upgrade/_adapter.d.mts +22 -4
- package/api/upgrade/_adapter.mjs +19 -6
- package/api/upgrade/run/run.mjs +355 -59
- package/api/upgrade/upgrade.doc.mjs +5 -1
- package/api/upgrade/upgrade.type.d.mts +34 -0
- package/api/upgrade/upgrade.type.mjs +15 -0
- package/assets/codemods/__tests__/runner.test.mjs +330 -8
- package/assets/codemods/integration-runner.mjs +56 -4
- package/assets/codemods/integration-runner.protection.test.mjs +153 -0
- package/assets/codemods/run-codemod.mjs +177 -34
- package/assets/codemods/runner.mjs +248 -110
- package/assets/codemods/transforms/next/__tests__/migrate-native-picker-to-presentation.test.mjs +63 -0
- package/assets/codemods/transforms/next/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +4 -0
- package/assets/codemods/transforms/next/index.mjs +8 -0
- package/assets/codemods/transforms/next/migrate-native-picker-to-presentation.mjs +148 -0
- package/assets/docs/cli-integrations.doc.mjs +13 -0
- package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +1 -1
- package/assets/templates/blocks/components/Item/ItemDocumentTabs.doc.mjs +14 -0
- package/assets/templates/blocks/components/Item/ItemDocumentTabs.tsx +100 -0
- package/assets/templates/blocks/components/TimeInput/TimeInputConstrained.tsx +1 -0
- package/authoring/codemod/type.ts +12 -0
- package/clients/cli/commands/upgrade.doc.mjs +2 -1
- package/clients/cli/commands/upgrade.file-protection.test.mjs +228 -0
- package/clients/cli/commands/upgrade.mjs +3 -0
- package/foundation/fs/file-protection.d.mts +33 -0
- package/foundation/fs/file-protection.mjs +825 -0
- package/foundation/fs/file-protection.test.mjs +250 -0
- package/foundation/response/error-codes.d.mts +3 -1
- package/foundation/response/error-codes.d.ts +2 -0
- package/foundation/response/error-codes.doc.mjs +10 -0
- package/foundation/response/error-codes.mjs +6 -0
- package/foundation/response/error-codes.test.mjs +55 -11
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -189,6 +189,8 @@ if (isError(result)) {
|
|
|
189
189
|
| `ERR_UNKNOWN_FEATURE` | An unrecognized `--features` value was passed to init. |
|
|
190
190
|
| `ERR_UNKNOWN_CODEMOD` | A `--codemod` value did not match any registered codemod (upgrade). |
|
|
191
191
|
| `ERR_CODEMOD_FAILED` | One or more codemods failed during an upgrade run. |
|
|
192
|
+
| `ERR_CODEMOD_PROTECTED` | A required codemod change remains blocked by a protected consumer file. |
|
|
193
|
+
| `ERR_CODEMOD_PROTECTION_SOURCE` | A working-tree protection declaration could not be read or parsed. |
|
|
192
194
|
| `ERR_NOT_FOUND` | A generic discover/lookup query matched nothing in any package. |
|
|
193
195
|
| `ERR_NO_DOC` | A component exists but has no typed `.doc.mjs` file. |
|
|
194
196
|
| `ERR_NO_SHOWCASE` | No showcase exists for the requested component. |
|
|
@@ -104,19 +104,27 @@ export function ensureCodemodDeps({ installDeps }?: {
|
|
|
104
104
|
* Run the CORE registry codemods. Runs BEFORE the config is loaded so a core
|
|
105
105
|
* CONFIG codemod can repair a config the strict loader would otherwise reject.
|
|
106
106
|
* @param {CoreVersionManifest[]} versionManifests
|
|
107
|
-
* @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>, root?: string}} options
|
|
107
|
+
* @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>, root?: string, protection?: {root: string, classify: (file: string) => import('../../foundation/fs/file-protection.mjs').FileProtection[]}, silent?: boolean}} options
|
|
108
108
|
*/
|
|
109
|
-
export function runCoreCodemods(versionManifests: CoreVersionManifest[], { apply, path: srcPath, codemod, skipCodemods, root }: {
|
|
109
|
+
export function runCoreCodemods(versionManifests: CoreVersionManifest[], { apply, path: srcPath, codemod, skipCodemods, root, protection, silent }: {
|
|
110
110
|
apply: boolean;
|
|
111
111
|
path: string;
|
|
112
112
|
codemod?: string;
|
|
113
113
|
skipCodemods: Set<string>;
|
|
114
114
|
root?: string;
|
|
115
|
+
protection?: {
|
|
116
|
+
root: string;
|
|
117
|
+
classify: (file: string) => import("../../foundation/fs/file-protection.mjs").FileProtection[];
|
|
118
|
+
};
|
|
119
|
+
silent?: boolean;
|
|
115
120
|
}): Promise<{
|
|
116
121
|
totalFilesChanged: number;
|
|
117
122
|
totalTransformsApplied: number;
|
|
118
123
|
totalValidationBlocked: number;
|
|
124
|
+
changedFiles: string[];
|
|
119
125
|
writtenFiles: string[];
|
|
126
|
+
stagedContents: Map<string, string>;
|
|
127
|
+
protectedFiles: import("../../authoring/codemod/type").CodemodRunResult["protectedFiles"];
|
|
120
128
|
errors: Array<{
|
|
121
129
|
file: string;
|
|
122
130
|
codemod: string;
|
|
@@ -180,20 +188,30 @@ export function selectIntegrationCodemodsFor(integrations: Array<import("../../f
|
|
|
180
188
|
/**
|
|
181
189
|
* Run the file-based INTEGRATION codemods (config codemods first, then code).
|
|
182
190
|
* @param {Array<{version: string, codemods: import('../../authoring/codemod/type').CodemodEntry[]}>} versionGroups
|
|
183
|
-
* @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string
|
|
191
|
+
* @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>, root?: string, protection?: {root: string, classify: (file: string) => import('../../foundation/fs/file-protection.mjs').FileProtection[]}, contents?: Map<string, string>, silent?: boolean}} options
|
|
184
192
|
*/
|
|
185
193
|
export function runIntegrationCodemodsStep(versionGroups: Array<{
|
|
186
194
|
version: string;
|
|
187
195
|
codemods: import("../../authoring/codemod/type").CodemodEntry[];
|
|
188
|
-
}>, { apply, path: srcPath, codemod, skipCodemods }: {
|
|
196
|
+
}>, { apply, path: srcPath, codemod, skipCodemods, root, protection, contents, silent, }: {
|
|
189
197
|
apply: boolean;
|
|
190
198
|
path: string;
|
|
191
199
|
codemod?: string;
|
|
192
200
|
skipCodemods: Set<string>;
|
|
201
|
+
root?: string;
|
|
202
|
+
protection?: {
|
|
203
|
+
root: string;
|
|
204
|
+
classify: (file: string) => import("../../foundation/fs/file-protection.mjs").FileProtection[];
|
|
205
|
+
};
|
|
206
|
+
contents?: Map<string, string>;
|
|
207
|
+
silent?: boolean;
|
|
193
208
|
}): Promise<{
|
|
194
209
|
totalFilesChanged: number;
|
|
195
210
|
totalTransformsApplied: number;
|
|
211
|
+
changedFiles: string[];
|
|
196
212
|
writtenFiles: string[];
|
|
213
|
+
stagedContents: Map<string, string>;
|
|
214
|
+
protectedFiles: import("../../authoring/codemod/type").CodemodRunResult["protectedFiles"];
|
|
197
215
|
errors: Array<{
|
|
198
216
|
file: string;
|
|
199
217
|
codemod: string;
|
package/api/upgrade/_adapter.mjs
CHANGED
|
@@ -329,19 +329,20 @@ export async function ensureCodemodDeps({installDeps} = {}) {
|
|
|
329
329
|
* Run the CORE registry codemods. Runs BEFORE the config is loaded so a core
|
|
330
330
|
* CONFIG codemod can repair a config the strict loader would otherwise reject.
|
|
331
331
|
* @param {CoreVersionManifest[]} versionManifests
|
|
332
|
-
* @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>, root?: string}} options
|
|
332
|
+
* @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>, root?: string, protection?: {root: string, classify: (file: string) => import('../../foundation/fs/file-protection.mjs').FileProtection[]}, silent?: boolean}} options
|
|
333
333
|
*/
|
|
334
334
|
export async function runCoreCodemods(
|
|
335
335
|
versionManifests,
|
|
336
|
-
{apply, path: srcPath, codemod, skipCodemods, root},
|
|
336
|
+
{apply, path: srcPath, codemod, skipCodemods, root, protection, silent},
|
|
337
337
|
) {
|
|
338
338
|
return runCodemods(versionManifests, {
|
|
339
339
|
apply,
|
|
340
340
|
path: srcPath,
|
|
341
341
|
codemod,
|
|
342
342
|
skipCodemods,
|
|
343
|
-
silent: logger.silent,
|
|
343
|
+
silent: silent ?? logger.silent,
|
|
344
344
|
root,
|
|
345
|
+
protection,
|
|
345
346
|
});
|
|
346
347
|
}
|
|
347
348
|
|
|
@@ -479,11 +480,20 @@ export async function selectIntegrationCodemodsFor(integrations, from, to) {
|
|
|
479
480
|
/**
|
|
480
481
|
* Run the file-based INTEGRATION codemods (config codemods first, then code).
|
|
481
482
|
* @param {Array<{version: string, codemods: import('../../authoring/codemod/type').CodemodEntry[]}>} versionGroups
|
|
482
|
-
* @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string
|
|
483
|
+
* @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>, root?: string, protection?: {root: string, classify: (file: string) => import('../../foundation/fs/file-protection.mjs').FileProtection[]}, contents?: Map<string, string>, silent?: boolean}} options
|
|
483
484
|
*/
|
|
484
485
|
export async function runIntegrationCodemodsStep(
|
|
485
486
|
versionGroups,
|
|
486
|
-
{
|
|
487
|
+
{
|
|
488
|
+
apply,
|
|
489
|
+
path: srcPath,
|
|
490
|
+
codemod,
|
|
491
|
+
skipCodemods,
|
|
492
|
+
root,
|
|
493
|
+
protection,
|
|
494
|
+
contents,
|
|
495
|
+
silent,
|
|
496
|
+
},
|
|
487
497
|
) {
|
|
488
498
|
const jscodeshift = (await import('jscodeshift')).default;
|
|
489
499
|
return runIntegrationCodemods(versionGroups, {
|
|
@@ -492,6 +502,9 @@ export async function runIntegrationCodemodsStep(
|
|
|
492
502
|
codemod,
|
|
493
503
|
skipCodemods,
|
|
494
504
|
jscodeshift,
|
|
495
|
-
silent: logger.silent,
|
|
505
|
+
silent: silent ?? logger.silent,
|
|
506
|
+
root,
|
|
507
|
+
protection,
|
|
508
|
+
contents,
|
|
496
509
|
});
|
|
497
510
|
}
|
package/api/upgrade/run/run.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* integration; execution errors abort before the agent-doc write.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import * as fs from 'node:fs';
|
|
20
21
|
import * as path from 'node:path';
|
|
21
22
|
import {
|
|
22
23
|
detectInstalledTargetVersion,
|
|
@@ -47,7 +48,58 @@ import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
|
47
48
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
48
49
|
import {AstryxError} from '../../error.mjs';
|
|
49
50
|
import {logger} from '../../logger.mjs';
|
|
50
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
assertWithin,
|
|
53
|
+
PathSafetyError,
|
|
54
|
+
} from '../../../foundation/fs/path-safety.mjs';
|
|
55
|
+
import {createFileProtectionResolver} from '../../../foundation/fs/file-protection.mjs';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collapse per-codemod protection hits into one stable row per file.
|
|
59
|
+
* @param {Array<{file: string, codemod: string, reason: string, declaration: string, generated: boolean, command?: string}>} rows
|
|
60
|
+
*/
|
|
61
|
+
function summarizeProtectedFiles(rows) {
|
|
62
|
+
const byFile = new Map();
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const current = byFile.get(row.file) ?? {
|
|
65
|
+
file: row.file,
|
|
66
|
+
codemods: new Set(),
|
|
67
|
+
reasons: new Set(),
|
|
68
|
+
declarations: new Set(),
|
|
69
|
+
commands: new Set(),
|
|
70
|
+
};
|
|
71
|
+
current.codemods.add(row.codemod);
|
|
72
|
+
current.reasons.add(row.reason);
|
|
73
|
+
current.declarations.add(row.declaration);
|
|
74
|
+
if (row.command) current.commands.add(row.command);
|
|
75
|
+
byFile.set(row.file, current);
|
|
76
|
+
}
|
|
77
|
+
return [...byFile.values()]
|
|
78
|
+
.map(item => ({
|
|
79
|
+
file: item.file,
|
|
80
|
+
codemods: [...item.codemods].sort(),
|
|
81
|
+
reasons: [...item.reasons].sort(),
|
|
82
|
+
declarations: [...item.declarations].sort(),
|
|
83
|
+
commands: [...item.commands].sort(),
|
|
84
|
+
}))
|
|
85
|
+
.sort((a, b) => a.file.localeCompare(b.file));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @param {string} cwd */
|
|
89
|
+
function loadFileProtection(cwd) {
|
|
90
|
+
try {
|
|
91
|
+
return createFileProtectionResolver(cwd);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
logger.error(message);
|
|
95
|
+
logger.log('Upgrade failed\n');
|
|
96
|
+
throw new AstryxError(
|
|
97
|
+
message,
|
|
98
|
+
undefined,
|
|
99
|
+
ERROR_CODES.ERR_CODEMOD_PROTECTION_SOURCE,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
51
103
|
|
|
52
104
|
/**
|
|
53
105
|
* Run the upgrade pipeline for a validated, non-list invocation. Returns the
|
|
@@ -75,7 +127,11 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
75
127
|
if (err instanceof PathSafetyError) {
|
|
76
128
|
logger.error(err.message);
|
|
77
129
|
logger.log('Aborted\n');
|
|
78
|
-
throw new AstryxError(
|
|
130
|
+
throw new AstryxError(
|
|
131
|
+
err.message,
|
|
132
|
+
undefined,
|
|
133
|
+
ERROR_CODES.ERR_PATH_TRAVERSAL,
|
|
134
|
+
);
|
|
79
135
|
}
|
|
80
136
|
throw err;
|
|
81
137
|
}
|
|
@@ -133,7 +189,10 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
133
189
|
});
|
|
134
190
|
}
|
|
135
191
|
|
|
136
|
-
const versionManifests = await getCoreVersionManifests(
|
|
192
|
+
const versionManifests = await getCoreVersionManifests(
|
|
193
|
+
currentVersion,
|
|
194
|
+
targetVersion,
|
|
195
|
+
);
|
|
137
196
|
|
|
138
197
|
const coreConfigCodemodNames = [];
|
|
139
198
|
for (const {transforms} of versionManifests) {
|
|
@@ -157,6 +216,11 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
157
216
|
}
|
|
158
217
|
}
|
|
159
218
|
|
|
219
|
+
// Read and parse all working-tree protection declarations before any
|
|
220
|
+
// dependency installation or codemod write. Core and integration runners
|
|
221
|
+
// share this exact snapshot.
|
|
222
|
+
let protection = loadFileProtection(cwd);
|
|
223
|
+
|
|
160
224
|
const ready = await ensureCodemodDeps({installDeps: options.installDeps});
|
|
161
225
|
if (!ready) {
|
|
162
226
|
const msg = 'jscodeshift is required but could not be installed.';
|
|
@@ -172,34 +236,81 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
172
236
|
codemod: options.codemod,
|
|
173
237
|
skipCodemods,
|
|
174
238
|
root: cwd,
|
|
239
|
+
protection,
|
|
175
240
|
});
|
|
176
|
-
const coreResult =
|
|
241
|
+
const coreResult =
|
|
242
|
+
codemodResult && 'totalFilesChanged' in codemodResult
|
|
243
|
+
? codemodResult
|
|
244
|
+
: null;
|
|
177
245
|
|
|
178
246
|
/** @type {Array<import('../../../foundation/integrations/integrations.mjs').LoadedIntegration>} */
|
|
179
247
|
let integrations;
|
|
180
248
|
/** @type {import('../../../authoring/config/type').PostCodemodHook[]} */
|
|
181
249
|
let postCodemodHooks;
|
|
182
250
|
try {
|
|
183
|
-
const projectContext = await loadProjectContext(
|
|
251
|
+
const projectContext = await loadProjectContext(
|
|
252
|
+
cwd,
|
|
253
|
+
options.integration ?? [],
|
|
254
|
+
);
|
|
184
255
|
postCodemodHooks = projectContext.postCodemodHooks;
|
|
185
256
|
integrations = projectContext.integrations;
|
|
186
257
|
} catch (err) {
|
|
187
258
|
const configErr = /** @type {Error} */ (err);
|
|
259
|
+
const allProtected = coreResult?.protectedFiles ?? [];
|
|
260
|
+
if (allProtected.length > 0) {
|
|
261
|
+
const protectedFiles = summarizeProtectedFiles(allProtected);
|
|
262
|
+
logger.error(
|
|
263
|
+
`${ERROR_CODES.ERR_CODEMOD_PROTECTED}: protected codemod changes remain while loading the Astryx config.`,
|
|
264
|
+
);
|
|
265
|
+
for (const item of protectedFiles) {
|
|
266
|
+
logger.error(` ${item.file} — ${item.declarations.join('; ')}`);
|
|
267
|
+
}
|
|
268
|
+
logger.log('Upgrade incomplete: protected changes remain\n');
|
|
269
|
+
return {
|
|
270
|
+
type: 'upgrade.run',
|
|
271
|
+
data: {
|
|
272
|
+
from: currentVersion,
|
|
273
|
+
to: targetVersion,
|
|
274
|
+
codemods: totalTransforms,
|
|
275
|
+
integrations: [],
|
|
276
|
+
agentDocsRefreshed: false,
|
|
277
|
+
agentDocs: {
|
|
278
|
+
status: 'current',
|
|
279
|
+
installedVersion: targetVersion,
|
|
280
|
+
fromVersions: [],
|
|
281
|
+
files: [],
|
|
282
|
+
refreshed: false,
|
|
283
|
+
action: 'none',
|
|
284
|
+
},
|
|
285
|
+
filesChanged: coreResult?.totalFilesChanged ?? 0,
|
|
286
|
+
transformsApplied: coreResult?.totalTransformsApplied ?? 0,
|
|
287
|
+
modifiedFiles: uniqueFiles(coreResult?.changedFiles).map(file =>
|
|
288
|
+
path.relative(cwd, file).split(path.sep).join('/'),
|
|
289
|
+
),
|
|
290
|
+
protectedFiles,
|
|
291
|
+
declinedCandidates: [],
|
|
292
|
+
complete: false,
|
|
293
|
+
errorCode: 'ERR_CODEMOD_PROTECTED',
|
|
294
|
+
errors: coreResult?.errors ?? [],
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
188
298
|
// Graceful dry-run catch: a config that fails strict validation is expected
|
|
189
299
|
// & fixable ONLY when dry-run AND a pending core config codemod previewed a
|
|
190
300
|
// change (the codemod that would repair it).
|
|
191
|
-
const codemodWouldFixConfig =
|
|
301
|
+
const codemodWouldFixConfig =
|
|
302
|
+
hasCoreConfigCodemod && (coreResult?.totalFilesChanged ?? 0) > 0;
|
|
192
303
|
if (!apply && codemodWouldFixConfig) {
|
|
193
304
|
// Lightweight inspection — no config/Project load (config is still broken
|
|
194
305
|
// in dry-run; the codemod previewed a fix but did not write it).
|
|
195
306
|
const inspection = inspectAgentDocs(cwd, targetVersion);
|
|
196
|
-
return statusConfigFixable(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
307
|
+
return statusConfigFixable({
|
|
308
|
+
from: currentVersion,
|
|
309
|
+
to: targetVersion,
|
|
310
|
+
configError: configErr.message,
|
|
311
|
+
configCodemods: coreConfigCodemodNames,
|
|
312
|
+
agentDocs:
|
|
313
|
+
/** @type {import('../upgrade.type.mjs').AgentDocsSummary} */ ({
|
|
203
314
|
status: inspection.status,
|
|
204
315
|
installedVersion: targetVersion,
|
|
205
316
|
fromVersions: inspection.blockVersions,
|
|
@@ -207,17 +318,22 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
207
318
|
refreshed: false,
|
|
208
319
|
action: inspection.status === 'missing' ? 'nudge-init' : 'none',
|
|
209
320
|
}),
|
|
210
|
-
|
|
211
|
-
);
|
|
321
|
+
});
|
|
212
322
|
}
|
|
213
323
|
// Genuine config error: abort.
|
|
214
324
|
logger.error(configErr.message);
|
|
215
325
|
logger.log('Aborted\n');
|
|
216
|
-
throw new AstryxError(
|
|
326
|
+
throw new AstryxError(
|
|
327
|
+
configErr.message,
|
|
328
|
+
undefined,
|
|
329
|
+
ERROR_CODES.ERR_INVALID_ARGUMENT,
|
|
330
|
+
);
|
|
217
331
|
}
|
|
218
332
|
|
|
219
333
|
if (integrations.length > 0) {
|
|
220
|
-
logger.log(
|
|
334
|
+
logger.log(
|
|
335
|
+
`Integrations: ${integrations.map(i => i.name ?? i.__spec).join(', ')}`,
|
|
336
|
+
);
|
|
221
337
|
}
|
|
222
338
|
|
|
223
339
|
// Non-blocking nudge for integration validation issues (suppressed for
|
|
@@ -229,7 +345,9 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
229
345
|
currentVersion,
|
|
230
346
|
targetVersion,
|
|
231
347
|
);
|
|
232
|
-
const hasIntegrationCodemods = integrationVersionGroups.some(
|
|
348
|
+
const hasIntegrationCodemods = integrationVersionGroups.some(
|
|
349
|
+
g => g.codemods.length > 0,
|
|
350
|
+
);
|
|
233
351
|
|
|
234
352
|
for (const {codemods} of integrationVersionGroups) {
|
|
235
353
|
for (const c of codemods) {
|
|
@@ -267,14 +385,14 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
267
385
|
}
|
|
268
386
|
|
|
269
387
|
if (totalTransforms > 0) {
|
|
270
|
-
logger.log(
|
|
388
|
+
logger.log(
|
|
389
|
+
`${totalTransforms} codemod${totalTransforms === 1 ? '' : 's'} to run${apply ? '' : ' (dry run)'}`,
|
|
390
|
+
);
|
|
271
391
|
} else {
|
|
272
392
|
logger.log('No automatic codemods to run for this version range.');
|
|
273
393
|
}
|
|
274
394
|
|
|
275
|
-
/**
|
|
276
|
-
* @type {{from: string, to: string, codemods: number, integrations: string[], agentDocsRefreshed: boolean, agentDocs: import('../upgrade.type.mjs').AgentDocsSummary, registryCompositions?: import('../upgrade.type.mjs').RegistryCompositionSummary, filesChanged?: number, transformsApplied?: number, errors?: Array<{file: string, codemod: string, error: string}>}}
|
|
277
|
-
*/
|
|
395
|
+
/** @type {import('../upgrade.type.mjs').UpgradeRunResponse['data']} */
|
|
278
396
|
const receipt = {
|
|
279
397
|
from: currentVersion,
|
|
280
398
|
to: targetVersion,
|
|
@@ -282,79 +400,257 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
282
400
|
integrations: integrations.map(i => i.name ?? i.__spec),
|
|
283
401
|
agentDocsRefreshed: false,
|
|
284
402
|
agentDocs: /** @type {import('../upgrade.type.mjs').AgentDocsSummary} */ ({
|
|
285
|
-
status: 'current',
|
|
286
|
-
|
|
403
|
+
status: 'current',
|
|
404
|
+
installedVersion: targetVersion,
|
|
405
|
+
fromVersions: [],
|
|
406
|
+
files: [],
|
|
407
|
+
refreshed: false,
|
|
408
|
+
action: 'none',
|
|
287
409
|
}),
|
|
288
410
|
};
|
|
289
411
|
|
|
290
412
|
let integrationResult = null;
|
|
291
413
|
if (hasIntegrationCodemods) {
|
|
414
|
+
const coreStagedContents = coreResult?.stagedContents;
|
|
415
|
+
if (
|
|
416
|
+
(coreResult?.writtenFiles.length ?? 0) > 0 ||
|
|
417
|
+
(coreStagedContents?.size ?? 0) > 0
|
|
418
|
+
) {
|
|
419
|
+
protection = createFileProtectionResolver(cwd, {
|
|
420
|
+
overrides: coreStagedContents,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
292
423
|
logger.log('Applying integration codemods...');
|
|
293
|
-
integrationResult = await runIntegrationCodemodsStep(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
424
|
+
integrationResult = await runIntegrationCodemodsStep(
|
|
425
|
+
integrationVersionGroups,
|
|
426
|
+
{
|
|
427
|
+
apply,
|
|
428
|
+
path: path_,
|
|
429
|
+
codemod: options.codemod,
|
|
430
|
+
skipCodemods,
|
|
431
|
+
root: cwd,
|
|
432
|
+
protection,
|
|
433
|
+
contents: coreStagedContents,
|
|
434
|
+
},
|
|
435
|
+
);
|
|
299
436
|
}
|
|
300
437
|
|
|
301
438
|
const registryResult = await reconcileCompositions();
|
|
302
439
|
|
|
303
|
-
const mergedFilesChanged =
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
440
|
+
const mergedFilesChanged =
|
|
441
|
+
(coreResult?.totalFilesChanged ?? 0) +
|
|
442
|
+
(integrationResult?.totalFilesChanged ?? 0);
|
|
443
|
+
const mergedTransformsApplied =
|
|
444
|
+
(coreResult?.totalTransformsApplied ?? 0) +
|
|
445
|
+
(integrationResult?.totalTransformsApplied ?? 0);
|
|
446
|
+
const mergedChangedFiles = [
|
|
447
|
+
...(coreResult?.changedFiles ?? []),
|
|
448
|
+
...(integrationResult?.changedFiles ?? []),
|
|
308
449
|
...(registryResult?.writtenFiles ?? []),
|
|
309
450
|
];
|
|
310
|
-
const mergedErrors = [
|
|
451
|
+
const mergedErrors = [
|
|
452
|
+
...(coreResult?.errors ?? []),
|
|
453
|
+
...(integrationResult?.errors ?? []),
|
|
454
|
+
];
|
|
455
|
+
let finalErrors = mergedErrors;
|
|
456
|
+
/** @type {string|undefined} */
|
|
457
|
+
let hookFailure;
|
|
458
|
+
const initialProtected = [
|
|
459
|
+
...(coreResult?.protectedFiles ?? []),
|
|
460
|
+
...(integrationResult?.protectedFiles ?? []),
|
|
461
|
+
];
|
|
311
462
|
const registryFilesChanged = registryResult?.writtenFiles.length ?? 0;
|
|
463
|
+
const generatedChangeBlocked = initialProtected.some(item => item.generated);
|
|
464
|
+
const shouldRunHooks =
|
|
465
|
+
postCodemodHooks.length > 0 &&
|
|
466
|
+
(mergedFilesChanged > 0 ||
|
|
467
|
+
registryFilesChanged > 0 ||
|
|
468
|
+
generatedChangeBlocked);
|
|
469
|
+
/** @type {Map<string, Buffer>} */
|
|
470
|
+
const protectedBeforeHooks = new Map();
|
|
471
|
+
if (apply && shouldRunHooks) {
|
|
472
|
+
for (const item of initialProtected) {
|
|
473
|
+
const absolute = path.resolve(cwd, item.file);
|
|
474
|
+
const relative = path.relative(cwd, absolute);
|
|
475
|
+
if (
|
|
476
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
477
|
+
relative === '..' ||
|
|
478
|
+
path.isAbsolute(relative)
|
|
479
|
+
)
|
|
480
|
+
continue;
|
|
481
|
+
try {
|
|
482
|
+
if (fs.lstatSync(absolute).isFile()) {
|
|
483
|
+
protectedBeforeHooks.set(absolute, fs.readFileSync(absolute));
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
// A candidate can disappear between planning and regeneration.
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/** @type {string[]} */
|
|
491
|
+
const hookModifiedFiles = [];
|
|
312
492
|
|
|
313
|
-
if (
|
|
314
|
-
const files = uniqueFiles(
|
|
493
|
+
if (shouldRunHooks) {
|
|
494
|
+
const files = uniqueFiles(mergedChangedFiles).map(file =>
|
|
495
|
+
path.relative(cwd, file),
|
|
496
|
+
);
|
|
315
497
|
try {
|
|
316
|
-
await runPostCodemodHooks(postCodemodHooks, {
|
|
498
|
+
await runPostCodemodHooks(postCodemodHooks, {
|
|
499
|
+
packageDir: cwd,
|
|
500
|
+
files,
|
|
501
|
+
apply: apply || false,
|
|
502
|
+
});
|
|
317
503
|
} catch (err) {
|
|
318
504
|
const hookErr = /** @type {Error} */ (err);
|
|
319
505
|
const msg = `Post-codemod hook failed: ${hookErr.message}`;
|
|
320
506
|
logger.error(msg);
|
|
321
|
-
|
|
322
|
-
|
|
507
|
+
if (initialProtected.length === 0) {
|
|
508
|
+
logger.log('Upgrade failed\n');
|
|
509
|
+
throw new AstryxError(msg, undefined, ERROR_CODES.ERR_CODEMOD_FAILED);
|
|
510
|
+
}
|
|
511
|
+
hookFailure = msg;
|
|
512
|
+
finalErrors = [
|
|
513
|
+
...mergedErrors,
|
|
514
|
+
{file: '.', codemod: 'post-codemod-hook', error: msg},
|
|
515
|
+
];
|
|
516
|
+
logger.log('Regeneration failed; protected changes remain\n');
|
|
517
|
+
}
|
|
518
|
+
if (apply) {
|
|
519
|
+
for (const [file, before] of protectedBeforeHooks) {
|
|
520
|
+
try {
|
|
521
|
+
if (!before.equals(fs.readFileSync(file)))
|
|
522
|
+
hookModifiedFiles.push(file);
|
|
523
|
+
} catch {
|
|
524
|
+
hookModifiedFiles.push(file);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
323
527
|
}
|
|
324
528
|
}
|
|
325
529
|
|
|
530
|
+
// A successful apply hook may have regenerated protected outputs. Rerun the
|
|
531
|
+
// selected codemods in preview mode against fresh bytes and fresh declarations
|
|
532
|
+
// to distinguish resolved outputs from changes that remain blocked.
|
|
533
|
+
let remainingProtected = initialProtected;
|
|
534
|
+
if (apply && shouldRunHooks && !hookFailure) {
|
|
535
|
+
const refreshedProtection = loadFileProtection(cwd);
|
|
536
|
+
const coreCheck = await runCoreCodemods(versionManifests, {
|
|
537
|
+
apply: false,
|
|
538
|
+
path: path_,
|
|
539
|
+
codemod: options.codemod,
|
|
540
|
+
skipCodemods,
|
|
541
|
+
root: cwd,
|
|
542
|
+
protection: refreshedProtection,
|
|
543
|
+
silent: true,
|
|
544
|
+
});
|
|
545
|
+
const checkedCore =
|
|
546
|
+
coreCheck && 'totalFilesChanged' in coreCheck ? coreCheck : null;
|
|
547
|
+
const recheckProtection =
|
|
548
|
+
(checkedCore?.stagedContents.size ?? 0) > 0
|
|
549
|
+
? createFileProtectionResolver(cwd, {
|
|
550
|
+
overrides: checkedCore?.stagedContents,
|
|
551
|
+
})
|
|
552
|
+
: refreshedProtection;
|
|
553
|
+
const integrationCheck = hasIntegrationCodemods
|
|
554
|
+
? await runIntegrationCodemodsStep(integrationVersionGroups, {
|
|
555
|
+
apply: false,
|
|
556
|
+
path: path_,
|
|
557
|
+
codemod: options.codemod,
|
|
558
|
+
skipCodemods,
|
|
559
|
+
root: cwd,
|
|
560
|
+
protection: recheckProtection,
|
|
561
|
+
contents: checkedCore?.stagedContents,
|
|
562
|
+
silent: true,
|
|
563
|
+
})
|
|
564
|
+
: null;
|
|
565
|
+
const recheckedProtected = [
|
|
566
|
+
...(checkedCore?.protectedFiles ?? []),
|
|
567
|
+
...(integrationCheck?.protectedFiles ?? []),
|
|
568
|
+
];
|
|
569
|
+
const recheckedChangedFiles = [
|
|
570
|
+
...(checkedCore?.changedFiles ?? []),
|
|
571
|
+
...(integrationCheck?.changedFiles ?? []),
|
|
572
|
+
].map(file => path.relative(cwd, file).split(path.sep).join('/'));
|
|
573
|
+
const initialByFile = new Map();
|
|
574
|
+
for (const item of initialProtected) {
|
|
575
|
+
const rows = initialByFile.get(item.file) ?? [];
|
|
576
|
+
rows.push(item);
|
|
577
|
+
initialByFile.set(item.file, rows);
|
|
578
|
+
}
|
|
579
|
+
// A hook that removes a protection marker without regenerating the bytes
|
|
580
|
+
// does not make the required change disappear. Retain the original
|
|
581
|
+
// declaration for that still-pending file.
|
|
582
|
+
for (const file of recheckedChangedFiles) {
|
|
583
|
+
recheckedProtected.push(...(initialByFile.get(file) ?? []));
|
|
584
|
+
}
|
|
585
|
+
remainingProtected = recheckedProtected;
|
|
586
|
+
finalErrors = [
|
|
587
|
+
...mergedErrors,
|
|
588
|
+
...(checkedCore?.errors ?? []),
|
|
589
|
+
...(integrationCheck?.errors ?? []),
|
|
590
|
+
];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const protectedFiles = summarizeProtectedFiles(remainingProtected);
|
|
326
594
|
receipt.filesChanged = mergedFilesChanged;
|
|
327
595
|
receipt.transformsApplied = mergedTransformsApplied;
|
|
328
|
-
receipt.
|
|
596
|
+
receipt.modifiedFiles = uniqueFiles([
|
|
597
|
+
...mergedChangedFiles,
|
|
598
|
+
...hookModifiedFiles,
|
|
599
|
+
]).map(file => path.relative(cwd, file).split(path.sep).join('/'));
|
|
600
|
+
receipt.protectedFiles = protectedFiles;
|
|
601
|
+
receipt.declinedCandidates = [];
|
|
602
|
+
receipt.complete = protectedFiles.length === 0;
|
|
603
|
+
if (!receipt.complete) {
|
|
604
|
+
receipt.errorCode = 'ERR_CODEMOD_PROTECTED';
|
|
605
|
+
}
|
|
606
|
+
receipt.errors = finalErrors;
|
|
329
607
|
if (registryResult) receipt.registryCompositions = registryResult.summary;
|
|
330
608
|
|
|
331
|
-
if (
|
|
609
|
+
if (protectedFiles.length > 0) {
|
|
610
|
+
logger.error(
|
|
611
|
+
`${ERROR_CODES.ERR_CODEMOD_PROTECTED}: ${protectedFiles.length} protected file${protectedFiles.length === 1 ? '' : 's'} still require${protectedFiles.length === 1 ? 's' : ''} a codemod change:`,
|
|
612
|
+
);
|
|
613
|
+
for (const item of protectedFiles) {
|
|
614
|
+
logger.error(` ${item.file} — ${item.declarations.join('; ')}`);
|
|
615
|
+
for (const command of item.commands) {
|
|
616
|
+
logger.log(` Regenerate with: ${command}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (receipt.errors?.length > 0 && protectedFiles.length === 0) {
|
|
332
622
|
const msg = `Upgrade completed with ${receipt.errors.length} codemod error${receipt.errors.length === 1 ? '' : 's'}.`;
|
|
333
623
|
logger.log('Upgrade failed\n');
|
|
334
624
|
throw new AstryxError(msg, undefined, ERROR_CODES.ERR_CODEMOD_FAILED);
|
|
335
625
|
}
|
|
336
626
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
627
|
+
// Only refresh managed docs after all required codemod changes are complete.
|
|
628
|
+
if (protectedFiles.length === 0) {
|
|
629
|
+
const agentDocsPlan = await prepareAgentDocsRefresh({
|
|
630
|
+
cwd,
|
|
631
|
+
installedVersion: targetVersion,
|
|
632
|
+
apply,
|
|
633
|
+
fresh: true,
|
|
634
|
+
});
|
|
635
|
+
const completedAgentDocs = apply
|
|
636
|
+
? applyAgentDocsRefresh(agentDocsPlan)
|
|
637
|
+
: agentDocsPlan.summary;
|
|
638
|
+
receipt.agentDocs = completedAgentDocs;
|
|
639
|
+
receipt.agentDocsRefreshed = completedAgentDocs.refreshed;
|
|
640
|
+
}
|
|
349
641
|
|
|
350
642
|
const registryOk = receipt.registryCompositions?.ok ?? true;
|
|
351
643
|
logger.log(
|
|
352
|
-
|
|
353
|
-
?
|
|
354
|
-
:
|
|
644
|
+
protectedFiles.length > 0
|
|
645
|
+
? 'Upgrade incomplete: protected changes remain\n'
|
|
646
|
+
: registryOk
|
|
647
|
+
? (apply ? 'Upgrade complete' : 'Dry run complete') + '\n'
|
|
648
|
+
: 'Upgrade finished with unresolved registry items\n',
|
|
355
649
|
);
|
|
356
650
|
return {
|
|
357
651
|
type: 'upgrade.run',
|
|
358
|
-
data: /** @type {import('../upgrade.type.mjs').UpgradeRunResponse['data']} */ (
|
|
652
|
+
data: /** @type {import('../upgrade.type.mjs').UpgradeRunResponse['data']} */ (
|
|
653
|
+
/** @type {unknown} */ (receipt)
|
|
654
|
+
),
|
|
359
655
|
};
|
|
360
656
|
}
|