@jskit-ai/jskit-cli 0.2.13 → 0.2.16
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/package.json +2 -2
- package/src/server/cliRuntime.js +332 -91
- package/src/server/commandHandlers.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/jskit-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Bundle and package orchestration CLI for JSKIT apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test": "node --test"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@jskit-ai/jskit-catalog": "0.1.
|
|
23
|
+
"@jskit-ai/jskit-catalog": "0.1.16"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": "20.x"
|
package/src/server/cliRuntime.js
CHANGED
|
@@ -101,7 +101,6 @@ function normalizeFileMutationRecord(value) {
|
|
|
101
101
|
toSurfacePath: String(record.toSurfacePath || "").trim(),
|
|
102
102
|
toSurfaceRoot: record.toSurfaceRoot === true,
|
|
103
103
|
toDir: String(record.toDir || "").trim(),
|
|
104
|
-
slug: String(record.slug || "").trim(),
|
|
105
104
|
extension: normalizeMutationExtension(record.extension),
|
|
106
105
|
preserveOnRemove: record.preserveOnRemove === true,
|
|
107
106
|
id: String(record.id || "").trim(),
|
|
@@ -262,14 +261,14 @@ function shouldApplyMutationWhen(
|
|
|
262
261
|
return true;
|
|
263
262
|
}
|
|
264
263
|
|
|
265
|
-
function buildFileWriteGroups(fileMutations) {
|
|
264
|
+
function buildFileWriteGroups(fileMutations, { packageId = "" } = {}) {
|
|
266
265
|
const groups = [];
|
|
267
266
|
const groupsByKey = new Map();
|
|
268
267
|
|
|
269
268
|
for (const mutation of ensureArray(fileMutations)) {
|
|
270
269
|
const normalized = normalizeFileMutationRecord(mutation);
|
|
271
270
|
if (normalized.op === "install-migration") {
|
|
272
|
-
if (!normalized.from || !normalized.
|
|
271
|
+
if (!normalized.from || !normalized.id) {
|
|
273
272
|
continue;
|
|
274
273
|
}
|
|
275
274
|
} else if (!normalized.from || (!normalized.to && !normalized.toSurface)) {
|
|
@@ -312,7 +311,12 @@ function buildFileWriteGroups(fileMutations) {
|
|
|
312
311
|
const extension = normalized.extension || ".cjs";
|
|
313
312
|
group.files.push({
|
|
314
313
|
from: normalized.from,
|
|
315
|
-
to:
|
|
314
|
+
to: buildManagedMigrationRelativePathLabel({
|
|
315
|
+
toDir,
|
|
316
|
+
packageId,
|
|
317
|
+
migrationId: normalized.id,
|
|
318
|
+
extension
|
|
319
|
+
})
|
|
316
320
|
});
|
|
317
321
|
continue;
|
|
318
322
|
}
|
|
@@ -334,29 +338,6 @@ function hashBuffer(buffer) {
|
|
|
334
338
|
return createHash("sha256").update(buffer).digest("hex");
|
|
335
339
|
}
|
|
336
340
|
|
|
337
|
-
function formatMigrationTimestamp(date = new Date()) {
|
|
338
|
-
const source = date instanceof Date && !Number.isNaN(date.getTime()) ? date : new Date();
|
|
339
|
-
const year = source.getUTCFullYear();
|
|
340
|
-
const month = String(source.getUTCMonth() + 1).padStart(2, "0");
|
|
341
|
-
const day = String(source.getUTCDate()).padStart(2, "0");
|
|
342
|
-
const hours = String(source.getUTCHours()).padStart(2, "0");
|
|
343
|
-
const minutes = String(source.getUTCMinutes()).padStart(2, "0");
|
|
344
|
-
const seconds = String(source.getUTCSeconds()).padStart(2, "0");
|
|
345
|
-
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function normalizeMigrationSlug(value, packageId) {
|
|
349
|
-
const normalized = String(value || "")
|
|
350
|
-
.trim()
|
|
351
|
-
.toLowerCase()
|
|
352
|
-
.replace(/[^a-z0-9_]+/g, "_")
|
|
353
|
-
.replace(/^_+|_+$/g, "");
|
|
354
|
-
if (!normalized) {
|
|
355
|
-
throw createCliError(`Invalid install-migration mutation in ${packageId}: \"slug\" is required.`);
|
|
356
|
-
}
|
|
357
|
-
return normalized;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
341
|
function normalizeMigrationExtension(value = "", fallback = ".cjs") {
|
|
361
342
|
const normalizedFallback = String(fallback || ".cjs").trim() || ".cjs";
|
|
362
343
|
const raw = String(value || "").trim();
|
|
@@ -367,46 +348,174 @@ function normalizeMigrationExtension(value = "", fallback = ".cjs") {
|
|
|
367
348
|
return candidate.toLowerCase();
|
|
368
349
|
}
|
|
369
350
|
|
|
370
|
-
const
|
|
351
|
+
const MIGRATION_ID_PATTERN = /^[a-z0-9._-]+$/;
|
|
371
352
|
|
|
372
|
-
function
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return "";
|
|
353
|
+
function normalizeMigrationId(value, packageId) {
|
|
354
|
+
const normalized = String(value || "").trim();
|
|
355
|
+
if (!normalized) {
|
|
356
|
+
throw createCliError(`Invalid install-migration mutation in ${packageId}: \"id\" is required.`);
|
|
377
357
|
}
|
|
378
|
-
|
|
358
|
+
if (!MIGRATION_ID_PATTERN.test(normalized)) {
|
|
359
|
+
throw createCliError(
|
|
360
|
+
`Invalid install-migration mutation in ${packageId}: "id" must match ${MIGRATION_ID_PATTERN.source}.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return normalized;
|
|
379
364
|
}
|
|
380
365
|
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
if (!
|
|
384
|
-
|
|
366
|
+
function resolveAppRelativePathWithinRoot(appRoot, relativePath, contextLabel = "path") {
|
|
367
|
+
const normalized = normalizeRelativePosixPath(String(relativePath || "").trim());
|
|
368
|
+
if (!normalized) {
|
|
369
|
+
throw createCliError(`Invalid ${contextLabel}: path is required.`);
|
|
370
|
+
}
|
|
371
|
+
const segments = normalized.split("/");
|
|
372
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
373
|
+
throw createCliError(`Invalid ${contextLabel}: path must be a safe relative path.`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const appRootAbsolute = path.resolve(appRoot);
|
|
377
|
+
const absolutePath = path.resolve(appRootAbsolute, normalized);
|
|
378
|
+
const relativeFromRoot = path.relative(appRootAbsolute, absolutePath);
|
|
379
|
+
if (
|
|
380
|
+
relativeFromRoot === ".." ||
|
|
381
|
+
relativeFromRoot.startsWith(`..${path.sep}`) ||
|
|
382
|
+
path.isAbsolute(relativeFromRoot)
|
|
383
|
+
) {
|
|
384
|
+
throw createCliError(`Invalid ${contextLabel}: path must stay within app root.`);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
|
|
388
|
-
|
|
387
|
+
return {
|
|
388
|
+
relativePath: normalized,
|
|
389
|
+
absolutePath
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function normalizeMigrationDirectory(value, packageId) {
|
|
394
|
+
const normalized = normalizeRelativePosixPath(String(value || "").trim() || "migrations");
|
|
395
|
+
if (!normalized) {
|
|
396
|
+
throw createCliError(`Invalid install-migration mutation in ${packageId}: "toDir" cannot be empty.`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const segments = normalized.split("/");
|
|
400
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
401
|
+
throw createCliError(`Invalid install-migration mutation in ${packageId}: "toDir" must be a safe relative path.`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return segments.join("/");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function formatMigrationTimestamp(date = new Date()) {
|
|
408
|
+
const source = date instanceof Date && !Number.isNaN(date.getTime()) ? date : new Date();
|
|
409
|
+
const year = String(source.getUTCFullYear()).padStart(4, "0");
|
|
410
|
+
const month = String(source.getUTCMonth() + 1).padStart(2, "0");
|
|
411
|
+
const day = String(source.getUTCDate()).padStart(2, "0");
|
|
412
|
+
const hour = String(source.getUTCHours()).padStart(2, "0");
|
|
413
|
+
const minute = String(source.getUTCMinutes()).padStart(2, "0");
|
|
414
|
+
const second = String(source.getUTCSeconds()).padStart(2, "0");
|
|
415
|
+
return `${year}${month}${day}${hour}${minute}${second}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function buildManagedMigrationFileName({ packageId = "", migrationId = "", extension = ".cjs", timestamp = "" } = {}) {
|
|
419
|
+
const normalizedMigrationId = normalizeMigrationId(migrationId, packageId);
|
|
420
|
+
const normalizedExtension = normalizeMigrationExtension(extension, ".cjs");
|
|
421
|
+
const normalizedTimestamp = String(timestamp || "").trim();
|
|
422
|
+
if (!/^\d{14}$/.test(normalizedTimestamp)) {
|
|
423
|
+
throw createCliError(
|
|
424
|
+
`Invalid install-migration mutation in ${packageId}: timestamp must be a 14-digit UTC string (YYYYMMDDHHmmss).`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return `${normalizedTimestamp}_${normalizedMigrationId}${normalizedExtension}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function buildManagedMigrationRelativePath({ toDir = "migrations", packageId = "", migrationId = "", extension = ".cjs", timestamp = "" } = {}) {
|
|
431
|
+
const normalizedDirectory = normalizeMigrationDirectory(toDir, packageId);
|
|
432
|
+
const fileName = buildManagedMigrationFileName({
|
|
433
|
+
packageId,
|
|
434
|
+
migrationId,
|
|
435
|
+
extension,
|
|
436
|
+
timestamp
|
|
437
|
+
});
|
|
438
|
+
return path.posix.join(normalizedDirectory, fileName);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function buildManagedMigrationRelativePathLabel({ toDir = "migrations", migrationId = "", extension = ".cjs" } = {}) {
|
|
442
|
+
const directory = normalizeRelativePosixPath(String(toDir || "").trim() || "migrations") || "migrations";
|
|
443
|
+
const id = String(migrationId || "<migration-id>").trim() || "<migration-id>";
|
|
444
|
+
const rawExtension = String(extension || ".cjs").trim() || ".cjs";
|
|
445
|
+
const ext = rawExtension.startsWith(".") ? rawExtension : `.${rawExtension}`;
|
|
446
|
+
return `${directory}/<timestamp>_${id}${ext}`.replace(/\/{2,}/g, "/");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function findExistingManagedMigrationPathById({
|
|
450
|
+
appRoot,
|
|
451
|
+
toDir = "migrations",
|
|
452
|
+
packageId = "",
|
|
453
|
+
migrationId = "",
|
|
454
|
+
extension = ".cjs"
|
|
455
|
+
} = {}) {
|
|
456
|
+
const normalizedDirectory = normalizeMigrationDirectory(toDir, packageId);
|
|
457
|
+
const resolvedDirectory = resolveAppRelativePathWithinRoot(
|
|
458
|
+
appRoot,
|
|
459
|
+
normalizedDirectory,
|
|
460
|
+
`${packageId} migration directory for ${migrationId}`
|
|
461
|
+
);
|
|
462
|
+
if (!(await fileExists(resolvedDirectory.absolutePath))) {
|
|
389
463
|
return null;
|
|
390
464
|
}
|
|
391
465
|
|
|
392
|
-
const
|
|
466
|
+
const normalizedMigrationId = normalizeMigrationId(migrationId, packageId);
|
|
467
|
+
const normalizedExtension = normalizeMigrationExtension(extension, ".cjs");
|
|
468
|
+
const suffix = `_${normalizedMigrationId}${normalizedExtension}`;
|
|
469
|
+
const entries = await readdir(resolvedDirectory.absolutePath, { withFileTypes: true }).catch(() => []);
|
|
470
|
+
const matches = [];
|
|
471
|
+
|
|
393
472
|
for (const entry of entries) {
|
|
394
473
|
if (!entry.isFile()) {
|
|
395
474
|
continue;
|
|
396
475
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
const fileMigrationId = extractMigrationIdFromSource(fileContent);
|
|
400
|
-
if (!fileMigrationId || fileMigrationId !== normalizedMigrationId) {
|
|
476
|
+
const fileName = String(entry.name || "").trim();
|
|
477
|
+
if (!fileName.endsWith(suffix)) {
|
|
401
478
|
continue;
|
|
402
479
|
}
|
|
480
|
+
const timestamp = fileName.slice(0, fileName.length - suffix.length);
|
|
481
|
+
if (!/^\d{14}$/.test(timestamp)) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
matches.push({
|
|
485
|
+
relativePath: path.posix.join(resolvedDirectory.relativePath, fileName),
|
|
486
|
+
absolutePath: path.join(resolvedDirectory.absolutePath, fileName)
|
|
487
|
+
});
|
|
488
|
+
}
|
|
403
489
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
490
|
+
matches.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
491
|
+
if (matches.length > 1) {
|
|
492
|
+
throw createCliError(
|
|
493
|
+
`${packageId}: found multiple migration files for ${normalizedMigrationId} in ${resolvedDirectory.relativePath}. Keep one file for this migration id.`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return matches[0] || null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function upsertManagedMigrationRecord(managedMigrations, record) {
|
|
500
|
+
const records = ensureArray(managedMigrations);
|
|
501
|
+
const normalizedId = String(ensureObject(record).id || "").trim();
|
|
502
|
+
if (!normalizedId) {
|
|
503
|
+
return;
|
|
407
504
|
}
|
|
408
505
|
|
|
409
|
-
|
|
506
|
+
const nextRecord = {
|
|
507
|
+
...ensureObject(record),
|
|
508
|
+
id: normalizedId
|
|
509
|
+
};
|
|
510
|
+
const existingIndex = records.findIndex(
|
|
511
|
+
(entry) => String(ensureObject(entry).id || "").trim() === normalizedId
|
|
512
|
+
);
|
|
513
|
+
if (existingIndex >= 0) {
|
|
514
|
+
records[existingIndex] = nextRecord;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
records.push(nextRecord);
|
|
410
519
|
}
|
|
411
520
|
|
|
412
521
|
function toScopedPackageId(input) {
|
|
@@ -1055,6 +1164,28 @@ function normalizePackageInstallationMode(rawValue, descriptorPath) {
|
|
|
1055
1164
|
return normalized;
|
|
1056
1165
|
}
|
|
1057
1166
|
|
|
1167
|
+
function validateInstallMigrationMutationShape(descriptor, descriptorPath) {
|
|
1168
|
+
const packageId = String(ensureObject(descriptor).packageId || "").trim() || "unknown-package";
|
|
1169
|
+
const mutations = ensureObject(ensureObject(descriptor).mutations);
|
|
1170
|
+
const files = ensureArray(mutations.files);
|
|
1171
|
+
for (const fileMutation of files) {
|
|
1172
|
+
const normalized = normalizeFileMutationRecord(fileMutation);
|
|
1173
|
+
if (normalized.op !== "install-migration") {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (!normalized.from) {
|
|
1177
|
+
throw createCliError(
|
|
1178
|
+
`Invalid package descriptor at ${descriptorPath}: install-migration in ${packageId} requires "from".`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
if (!normalized.id) {
|
|
1182
|
+
throw createCliError(
|
|
1183
|
+
`Invalid package descriptor at ${descriptorPath}: install-migration in ${packageId} requires "id".`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1058
1189
|
function validatePackageDescriptorShape(descriptor, descriptorPath) {
|
|
1059
1190
|
const normalized = ensureObject(descriptor);
|
|
1060
1191
|
const packageId = String(normalized.packageId || "").trim();
|
|
@@ -1078,6 +1209,8 @@ function validatePackageDescriptorShape(descriptor, descriptorPath) {
|
|
|
1078
1209
|
);
|
|
1079
1210
|
}
|
|
1080
1211
|
|
|
1212
|
+
validateInstallMigrationMutationShape(normalized, descriptorPath);
|
|
1213
|
+
|
|
1081
1214
|
return {
|
|
1082
1215
|
...normalized,
|
|
1083
1216
|
installationMode: normalizePackageInstallationMode(normalized.installationMode, descriptorPath)
|
|
@@ -1106,6 +1239,8 @@ function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { ex
|
|
|
1106
1239
|
throw createCliError(`Invalid app-local package descriptor at ${descriptorPath}: missing version.`);
|
|
1107
1240
|
}
|
|
1108
1241
|
|
|
1242
|
+
validateInstallMigrationMutationShape(normalized, descriptorPath);
|
|
1243
|
+
|
|
1109
1244
|
return {
|
|
1110
1245
|
...normalized,
|
|
1111
1246
|
packageId,
|
|
@@ -1545,15 +1680,30 @@ function resolvePackageDependencySpecifier(packageEntry, { existingValue = "" }
|
|
|
1545
1680
|
|
|
1546
1681
|
const descriptorVersion = String(packageEntry?.version || "").trim();
|
|
1547
1682
|
if (descriptorVersion) {
|
|
1548
|
-
return descriptorVersion;
|
|
1683
|
+
return normalizeJskitDependencySpecifier(packageEntry?.packageId, descriptorVersion);
|
|
1549
1684
|
}
|
|
1550
1685
|
const packageJsonVersion = String(packageEntry?.packageJson?.version || "").trim();
|
|
1551
1686
|
if (packageJsonVersion) {
|
|
1552
|
-
return packageJsonVersion;
|
|
1687
|
+
return normalizeJskitDependencySpecifier(packageEntry?.packageId, packageJsonVersion);
|
|
1553
1688
|
}
|
|
1554
1689
|
throw createCliError(`Unable to resolve dependency specifier for ${String(packageEntry?.packageId || "unknown package")}.`);
|
|
1555
1690
|
}
|
|
1556
1691
|
|
|
1692
|
+
function normalizeJskitDependencySpecifier(packageId, dependencySpecifier) {
|
|
1693
|
+
const normalizedPackageId = String(packageId || "").trim();
|
|
1694
|
+
const normalizedSpecifier = String(dependencySpecifier || "").trim();
|
|
1695
|
+
if (!normalizedSpecifier || !normalizedPackageId.startsWith("@jskit-ai/")) {
|
|
1696
|
+
return normalizedSpecifier;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const semverMatch = /^(\d+)\.\d+\.\d+(?:[.+-][0-9A-Za-z.-]+)?$/.exec(normalizedSpecifier);
|
|
1700
|
+
if (!semverMatch) {
|
|
1701
|
+
return normalizedSpecifier;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
return `${semverMatch[1]}.x`;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1557
1707
|
function normalizePackageNameSegment(rawValue, { label = "package name" } = {}) {
|
|
1558
1708
|
const lowered = String(rawValue || "")
|
|
1559
1709
|
.trim()
|
|
@@ -3415,7 +3565,7 @@ async function cleanupMaterializedPackageRoots() {
|
|
|
3415
3565
|
|
|
3416
3566
|
function interpolateFileMutationRecord(mutation, options, packageId) {
|
|
3417
3567
|
const mutationKey = String(
|
|
3418
|
-
mutation?.id || mutation?.
|
|
3568
|
+
mutation?.id || mutation?.to || mutation?.toSurface || mutation?.from || "files"
|
|
3419
3569
|
).trim();
|
|
3420
3570
|
const interpolate = (value, field) =>
|
|
3421
3571
|
interpolateOptionValue(String(value || ""), options, packageId, `${mutationKey}.${field}`);
|
|
@@ -3427,7 +3577,6 @@ function interpolateFileMutationRecord(mutation, options, packageId) {
|
|
|
3427
3577
|
toSurface: interpolate(mutation.toSurface, "toSurface"),
|
|
3428
3578
|
toSurfacePath: interpolate(mutation.toSurfacePath, "toSurfacePath"),
|
|
3429
3579
|
toDir: interpolate(mutation.toDir, "toDir"),
|
|
3430
|
-
slug: interpolate(mutation.slug, "slug"),
|
|
3431
3580
|
extension: interpolate(mutation.extension, "extension"),
|
|
3432
3581
|
id: interpolate(mutation.id, "id"),
|
|
3433
3582
|
category: interpolate(mutation.category, "category"),
|
|
@@ -3627,6 +3776,16 @@ async function applyFileMutations(
|
|
|
3627
3776
|
touchedFiles,
|
|
3628
3777
|
warnings = []
|
|
3629
3778
|
) {
|
|
3779
|
+
const managedMigrationById = new Map();
|
|
3780
|
+
for (const managedMigrationValue of ensureArray(managedMigrations)) {
|
|
3781
|
+
const managedMigration = ensureObject(managedMigrationValue);
|
|
3782
|
+
const migrationId = String(managedMigration.id || "").trim();
|
|
3783
|
+
if (!migrationId) {
|
|
3784
|
+
continue;
|
|
3785
|
+
}
|
|
3786
|
+
managedMigrationById.set(migrationId, managedMigration);
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3630
3789
|
for (const mutationValue of fileMutations) {
|
|
3631
3790
|
const normalizedMutation = normalizeFileMutationRecord(mutationValue);
|
|
3632
3791
|
const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
|
|
@@ -3658,8 +3817,8 @@ async function applyFileMutations(
|
|
|
3658
3817
|
if (!from) {
|
|
3659
3818
|
throw createCliError(`Invalid install-migration mutation in ${packageEntry.packageId}: \"from\" is required.`);
|
|
3660
3819
|
}
|
|
3820
|
+
const migrationId = normalizeMigrationId(mutation.id, packageEntry.packageId);
|
|
3661
3821
|
|
|
3662
|
-
const slug = normalizeMigrationSlug(mutation.slug, packageEntry.packageId);
|
|
3663
3822
|
const sourcePath = path.join(packageEntry.rootDir, from);
|
|
3664
3823
|
if (!(await fileExists(sourcePath))) {
|
|
3665
3824
|
throw createCliError(`Missing migration template source ${sourcePath} for ${packageEntry.packageId}.`);
|
|
@@ -3667,65 +3826,138 @@ async function applyFileMutations(
|
|
|
3667
3826
|
|
|
3668
3827
|
const sourceContent = await readFile(sourcePath, "utf8");
|
|
3669
3828
|
const renderedSourceContent = sourceContent.includes("${")
|
|
3670
|
-
? interpolateOptionValue(sourceContent, options, packageEntry.packageId, `${mutation.id ||
|
|
3829
|
+
? interpolateOptionValue(sourceContent, options, packageEntry.packageId, `${mutation.id || from}.source`)
|
|
3671
3830
|
: sourceContent;
|
|
3672
3831
|
const sourceExtension = normalizeMigrationExtension(path.extname(from), ".cjs");
|
|
3673
3832
|
const extension = normalizeMigrationExtension(mutation.extension, sourceExtension);
|
|
3674
|
-
const
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3833
|
+
const sourceHash = hashBuffer(Buffer.from(renderedSourceContent, "utf8"));
|
|
3834
|
+
|
|
3835
|
+
const existingManagedRecord = managedMigrationById.get(migrationId);
|
|
3836
|
+
if (existingManagedRecord) {
|
|
3837
|
+
const existingManagedPath = normalizeRelativePosixPath(String(existingManagedRecord.path || "").trim());
|
|
3838
|
+
if (!existingManagedPath) {
|
|
3839
|
+
throw createCliError(
|
|
3840
|
+
`${packageEntry.packageId}: managed migration ${migrationId} is missing path in lock.`
|
|
3841
|
+
);
|
|
3842
|
+
}
|
|
3843
|
+
const resolvedManagedPath = resolveAppRelativePathWithinRoot(
|
|
3844
|
+
appRoot,
|
|
3845
|
+
existingManagedPath,
|
|
3846
|
+
`${packageEntry.packageId} managed migration path for ${migrationId}`
|
|
3847
|
+
);
|
|
3848
|
+
const relativePath = resolvedManagedPath.relativePath;
|
|
3849
|
+
const absolutePath = resolvedManagedPath.absolutePath;
|
|
3850
|
+
let existingSourceHash = String(existingManagedRecord.hash || "").trim();
|
|
3851
|
+
if (!existingSourceHash && existingManagedPath && (await fileExists(absolutePath))) {
|
|
3852
|
+
const existingSource = await readFile(absolutePath);
|
|
3853
|
+
existingSourceHash = hashBuffer(existingSource);
|
|
3854
|
+
}
|
|
3683
3855
|
|
|
3684
|
-
|
|
3856
|
+
if (existingSourceHash && existingSourceHash !== sourceHash) {
|
|
3857
|
+
throw createCliError(
|
|
3858
|
+
`${packageEntry.packageId}: migration ${migrationId} changed after install. Keep migrations immutable and create a new migration id.`
|
|
3859
|
+
);
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
if (!(await fileExists(absolutePath))) {
|
|
3863
|
+
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
3864
|
+
await writeFile(absolutePath, renderedSourceContent, "utf8");
|
|
3865
|
+
touchedFiles.add(relativePath);
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
const nextManagedRecord = {
|
|
3869
|
+
...existingManagedRecord,
|
|
3870
|
+
id: migrationId,
|
|
3871
|
+
path: relativePath,
|
|
3872
|
+
hash: sourceHash,
|
|
3873
|
+
skipped: true,
|
|
3874
|
+
reason: mutation.reason || String(existingManagedRecord.reason || ""),
|
|
3875
|
+
category: mutation.category || String(existingManagedRecord.category || "")
|
|
3876
|
+
};
|
|
3877
|
+
managedMigrationById.set(migrationId, nextManagedRecord);
|
|
3878
|
+
upsertManagedMigrationRecord(managedMigrations, nextManagedRecord);
|
|
3685
3879
|
warnings.push(
|
|
3686
|
-
`${packageEntry.packageId}: skipped migration ${migrationId} (already
|
|
3880
|
+
`${packageEntry.packageId}: skipped migration ${migrationId} (already managed at ${nextManagedRecord.path}).`
|
|
3687
3881
|
);
|
|
3688
|
-
|
|
3882
|
+
continue;
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
const existingPathById = await findExistingManagedMigrationPathById({
|
|
3886
|
+
appRoot,
|
|
3887
|
+
toDir,
|
|
3888
|
+
packageId: packageEntry.packageId,
|
|
3889
|
+
migrationId,
|
|
3890
|
+
extension
|
|
3891
|
+
});
|
|
3892
|
+
if (existingPathById) {
|
|
3893
|
+
const existingSource = await readFile(existingPathById.absolutePath);
|
|
3894
|
+
const existingSourceHash = hashBuffer(existingSource);
|
|
3895
|
+
if (existingSourceHash !== sourceHash) {
|
|
3896
|
+
throw createCliError(
|
|
3897
|
+
`${packageEntry.packageId}: migration ${migrationId} changed after install. Keep migrations immutable and create a new migration id.`
|
|
3898
|
+
);
|
|
3899
|
+
}
|
|
3900
|
+
const nextManagedRecord = {
|
|
3689
3901
|
id: migrationId,
|
|
3690
|
-
path:
|
|
3902
|
+
path: existingPathById.relativePath,
|
|
3903
|
+
hash: sourceHash,
|
|
3691
3904
|
skipped: true,
|
|
3692
3905
|
reason: mutation.reason,
|
|
3693
3906
|
category: mutation.category
|
|
3694
|
-
}
|
|
3907
|
+
};
|
|
3908
|
+
managedMigrationById.set(migrationId, nextManagedRecord);
|
|
3909
|
+
upsertManagedMigrationRecord(managedMigrations, nextManagedRecord);
|
|
3910
|
+
warnings.push(
|
|
3911
|
+
`${packageEntry.packageId}: skipped migration ${migrationId} (already exists at ${nextManagedRecord.path}).`
|
|
3912
|
+
);
|
|
3695
3913
|
continue;
|
|
3696
3914
|
}
|
|
3697
3915
|
|
|
3698
|
-
const
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3916
|
+
const baseNowMs = Date.now();
|
|
3917
|
+
let targetPath = null;
|
|
3918
|
+
for (let secondOffset = 0; secondOffset < 86400; secondOffset += 1) {
|
|
3919
|
+
const timestamp = formatMigrationTimestamp(new Date(baseNowMs + secondOffset * 1000));
|
|
3920
|
+
const candidateRelativePath = buildManagedMigrationRelativePath({
|
|
3921
|
+
toDir,
|
|
3922
|
+
packageId: packageEntry.packageId,
|
|
3923
|
+
migrationId,
|
|
3924
|
+
extension,
|
|
3925
|
+
timestamp
|
|
3926
|
+
});
|
|
3927
|
+
const candidatePath = resolveAppRelativePathWithinRoot(
|
|
3928
|
+
appRoot,
|
|
3929
|
+
candidateRelativePath,
|
|
3930
|
+
`${packageEntry.packageId} migration path for ${migrationId}`
|
|
3931
|
+
);
|
|
3932
|
+
if (await fileExists(candidatePath.absolutePath)) {
|
|
3933
|
+
continue;
|
|
3711
3934
|
}
|
|
3712
|
-
|
|
3935
|
+
targetPath = candidatePath;
|
|
3936
|
+
break;
|
|
3713
3937
|
}
|
|
3714
3938
|
|
|
3715
3939
|
if (!targetPath) {
|
|
3716
|
-
throw createCliError(
|
|
3940
|
+
throw createCliError(
|
|
3941
|
+
`${packageEntry.packageId}: unable to allocate migration filename for ${migrationId} in ${toDir}.`
|
|
3942
|
+
);
|
|
3717
3943
|
}
|
|
3718
3944
|
|
|
3719
|
-
|
|
3720
|
-
const
|
|
3945
|
+
const relativePath = targetPath.relativePath;
|
|
3946
|
+
const absolutePath = targetPath.absolutePath;
|
|
3947
|
+
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
3948
|
+
await writeFile(absolutePath, renderedSourceContent, "utf8");
|
|
3721
3949
|
touchedFiles.add(relativePath);
|
|
3722
|
-
|
|
3950
|
+
|
|
3951
|
+
const nextManagedRecord = {
|
|
3723
3952
|
id: migrationId,
|
|
3724
3953
|
path: relativePath,
|
|
3954
|
+
hash: sourceHash,
|
|
3725
3955
|
skipped: false,
|
|
3726
3956
|
reason: mutation.reason,
|
|
3727
3957
|
category: mutation.category
|
|
3728
|
-
}
|
|
3958
|
+
};
|
|
3959
|
+
managedMigrationById.set(migrationId, nextManagedRecord);
|
|
3960
|
+
upsertManagedMigrationRecord(managedMigrations, nextManagedRecord);
|
|
3729
3961
|
continue;
|
|
3730
3962
|
}
|
|
3731
3963
|
|
|
@@ -4121,6 +4353,7 @@ async function applyPackageInstall({
|
|
|
4121
4353
|
});
|
|
4122
4354
|
|
|
4123
4355
|
const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
|
|
4356
|
+
managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
|
|
4124
4357
|
const cloneOnlyPackage = isCloneOnlyPackageEntry(packageEntry);
|
|
4125
4358
|
const mutationWarnings = [];
|
|
4126
4359
|
const mutations = ensureObject(packageEntry.descriptor.mutations);
|
|
@@ -4161,7 +4394,8 @@ async function applyPackageInstall({
|
|
|
4161
4394
|
const resolvedValue = localPackage
|
|
4162
4395
|
? resolvePackageDependencySpecifier(localPackage, { existingValue: existingRuntimeDependencyValue })
|
|
4163
4396
|
: String(dependencyVersion);
|
|
4164
|
-
const
|
|
4397
|
+
const normalizedResolvedValue = normalizeJskitDependencySpecifier(dependencyId, resolvedValue);
|
|
4398
|
+
const applied = applyPackageJsonField(appPackageJson, "dependencies", dependencyId, normalizedResolvedValue);
|
|
4165
4399
|
if (applied.changed) {
|
|
4166
4400
|
managedRecord.managed.packageJson.dependencies[dependencyId] = applied.managed;
|
|
4167
4401
|
touchedFiles.add("package.json");
|
|
@@ -4192,7 +4426,8 @@ async function applyPackageInstall({
|
|
|
4192
4426
|
const resolvedValue = localPackage
|
|
4193
4427
|
? resolvePackageDependencySpecifier(localPackage, { existingValue: existingDevDependencyValue })
|
|
4194
4428
|
: String(dependencyVersion);
|
|
4195
|
-
const
|
|
4429
|
+
const normalizedResolvedValue = normalizeJskitDependencySpecifier(dependencyId, resolvedValue);
|
|
4430
|
+
const applied = applyPackageJsonField(appPackageJson, "devDependencies", dependencyId, normalizedResolvedValue);
|
|
4196
4431
|
if (applied.changed) {
|
|
4197
4432
|
managedRecord.managed.packageJson.devDependencies[dependencyId] = applied.managed;
|
|
4198
4433
|
touchedFiles.add("package.json");
|
|
@@ -4210,7 +4445,13 @@ async function applyPackageInstall({
|
|
|
4210
4445
|
const selfDependencyValue = resolvePackageDependencySpecifier(packageEntry, {
|
|
4211
4446
|
existingValue: existingSelfDependencyValue
|
|
4212
4447
|
});
|
|
4213
|
-
const
|
|
4448
|
+
const normalizedSelfDependencyValue = normalizeJskitDependencySpecifier(packageEntry.packageId, selfDependencyValue);
|
|
4449
|
+
const selfApplied = applyPackageJsonField(
|
|
4450
|
+
appPackageJson,
|
|
4451
|
+
"dependencies",
|
|
4452
|
+
packageEntry.packageId,
|
|
4453
|
+
normalizedSelfDependencyValue
|
|
4454
|
+
);
|
|
4214
4455
|
if (selfApplied.changed) {
|
|
4215
4456
|
managedRecord.managed.packageJson.dependencies[packageEntry.packageId] = selfApplied.managed;
|
|
4216
4457
|
touchedFiles.add("package.json");
|
|
@@ -417,7 +417,10 @@ function createCommandHandlers(deps) {
|
|
|
417
417
|
if (resolvedPackageId) {
|
|
418
418
|
const packageEntry = packageRegistry.get(resolvedPackageId);
|
|
419
419
|
const descriptor = packageEntry.descriptor;
|
|
420
|
-
const fileWriteGroups = buildFileWriteGroups(
|
|
420
|
+
const fileWriteGroups = buildFileWriteGroups(
|
|
421
|
+
ensureArray(ensureObject(descriptor.mutations).files),
|
|
422
|
+
{ packageId: descriptor.packageId }
|
|
423
|
+
);
|
|
421
424
|
const fileWriteCount = fileWriteGroups.reduce((total, group) => total + ensureArray(group.files).length, 0);
|
|
422
425
|
const capabilities = ensureObject(descriptor.capabilities);
|
|
423
426
|
const runtime = ensureObject(descriptor.runtime);
|