@jskit-ai/jskit-cli 0.2.24 → 0.2.26

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.
@@ -28,9 +28,7 @@ import {
28
28
  import {
29
29
  BUNDLES_ROOT,
30
30
  CATALOG_PACKAGES_PATH,
31
- CLI_PACKAGE_ROOT,
32
- MODULES_ROOT,
33
- WORKSPACE_ROOT
31
+ CLI_PACKAGE_ROOT
34
32
  } from "./pathResolution.js";
35
33
  import {
36
34
  appendTextSnippet,
@@ -55,9 +53,9 @@ const VITE_DEV_PROXY_CONFIG_RELATIVE_PATH = ".jskit/vite.dev.proxy.json";
55
53
  const VITE_DEV_PROXY_CONFIG_VERSION = 1;
56
54
  const PUBLIC_APP_CONFIG_RELATIVE_PATH = "config/public.js";
57
55
  const SERVER_APP_CONFIG_RELATIVE_PATH = "config/server.js";
58
- const PACKAGE_INSTALL_MODE_INSTALLABLE = "installable";
59
- const PACKAGE_INSTALL_MODE_CLONE_ONLY = "clone-only";
60
- const PACKAGE_INSTALL_MODES = Object.freeze([PACKAGE_INSTALL_MODE_INSTALLABLE, PACKAGE_INSTALL_MODE_CLONE_ONLY]);
56
+ const PACKAGE_KIND_RUNTIME = "runtime";
57
+ const PACKAGE_KIND_GENERATOR = "generator";
58
+ const PACKAGE_KINDS = Object.freeze([PACKAGE_KIND_RUNTIME, PACKAGE_KIND_GENERATOR]);
61
59
  const WORKSPACE_VISIBILITY_LEVELS = Object.freeze(["workspace", "workspace_user"]);
62
60
  const WORKSPACE_VISIBILITY_SET = new Set(WORKSPACE_VISIBILITY_LEVELS);
63
61
  const MATERIALIZED_PACKAGE_ROOTS = new Map();
@@ -95,6 +93,20 @@ function normalizeMutationExtension(value) {
95
93
  return `.${extension}`;
96
94
  }
97
95
 
96
+ function normalizeTemplateContextRecord(value) {
97
+ const record = ensureObject(value);
98
+ const entrypoint = String(record.entrypoint || "").trim();
99
+ const exportName = String(record.export || "").trim();
100
+ if (!entrypoint && !exportName) {
101
+ return null;
102
+ }
103
+
104
+ return Object.freeze({
105
+ entrypoint,
106
+ export: exportName || "buildTemplateContext"
107
+ });
108
+ }
109
+
98
110
  function normalizeFileMutationRecord(value) {
99
111
  const record = ensureObject(value);
100
112
  const op = String(record.op || "copy-file").trim().toLowerCase() || "copy-file";
@@ -111,6 +123,7 @@ function normalizeFileMutationRecord(value) {
111
123
  id: String(record.id || "").trim(),
112
124
  category: String(record.category || "").trim(),
113
125
  reason: String(record.reason || "").trim(),
126
+ templateContext: normalizeTemplateContextRecord(record.templateContext),
114
127
  when: normalizeMutationWhen(record.when)
115
128
  };
116
129
  }
@@ -184,6 +197,44 @@ function normalizeWhenSourceValue(value) {
184
197
  return "";
185
198
  }
186
199
 
200
+ function normalizeWhenComparisonValue(value) {
201
+ const normalizedValue = normalizeWhenSourceValue(value);
202
+ if (!normalizedValue.includes(",")) {
203
+ return normalizedValue;
204
+ }
205
+
206
+ return normalizedValue
207
+ .split(",")
208
+ .map((entry) => String(entry || "").trim())
209
+ .filter(Boolean)
210
+ .join(",");
211
+ }
212
+
213
+ function splitWhenComparisonTokens(value) {
214
+ return normalizeWhenComparisonValue(value)
215
+ .split(",")
216
+ .map((entry) => String(entry || "").trim())
217
+ .filter(Boolean);
218
+ }
219
+
220
+ function matchesWhenComparisonValue(optionValue, optionTokens, expectedValue) {
221
+ const expected = normalizeWhenComparisonValue(expectedValue);
222
+ if (!expected) {
223
+ return false;
224
+ }
225
+
226
+ const expectedTokens = splitWhenComparisonTokens(expected);
227
+ if (expectedTokens.length > 1) {
228
+ return optionValue === expected;
229
+ }
230
+
231
+ if (optionTokens.length > 1) {
232
+ return optionTokens.includes(expectedTokens[0]);
233
+ }
234
+
235
+ return optionValue === expectedTokens[0];
236
+ }
237
+
187
238
  function resolveWhenConfigValue(configContext = {}, configPath = "") {
188
239
  const normalizedPath = String(configPath || "").trim();
189
240
  if (!normalizedPath) {
@@ -244,11 +295,12 @@ function shouldApplyMutationWhen(
244
295
  const sourceValue = optionName
245
296
  ? readObjectPath(options, optionName)
246
297
  : resolveWhenConfigValue(configContext, configPath);
247
- const optionValue = normalizeWhenSourceValue(sourceValue);
248
- const equals = String(when.equals || "").trim();
249
- const notEquals = String(when.notEquals || "").trim();
250
- const includes = ensureArray(when.includes).map((entry) => String(entry || "").trim()).filter(Boolean);
251
- const excludes = ensureArray(when.excludes).map((entry) => String(entry || "").trim()).filter(Boolean);
298
+ const optionValue = normalizeWhenComparisonValue(sourceValue);
299
+ const optionTokens = splitWhenComparisonTokens(optionValue);
300
+ const equals = normalizeWhenComparisonValue(when.equals);
301
+ const notEquals = normalizeWhenComparisonValue(when.notEquals);
302
+ const includes = ensureArray(when.includes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
303
+ const excludes = ensureArray(when.excludes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
252
304
 
253
305
  if (equals && optionValue !== equals) {
254
306
  return false;
@@ -256,10 +308,10 @@ function shouldApplyMutationWhen(
256
308
  if (notEquals && optionValue === notEquals) {
257
309
  return false;
258
310
  }
259
- if (includes.length > 0 && !includes.includes(optionValue)) {
311
+ if (includes.length > 0 && !includes.some((entry) => matchesWhenComparisonValue(optionValue, optionTokens, entry))) {
260
312
  return false;
261
313
  }
262
- if (excludes.length > 0 && excludes.includes(optionValue)) {
314
+ if (excludes.length > 0 && excludes.some((entry) => matchesWhenComparisonValue(optionValue, optionTokens, entry))) {
263
315
  return false;
264
316
  }
265
317
 
@@ -1113,16 +1165,16 @@ function removeEnvValue(content, key, expectedValue, previous) {
1113
1165
  };
1114
1166
  }
1115
1167
 
1116
- function normalizePackageInstallationMode(rawValue, descriptorPath) {
1117
- const normalized = String(rawValue || "")
1118
- .trim()
1119
- .toLowerCase();
1168
+ function normalizePackageKind(rawValue, descriptorPath) {
1169
+ const normalized = String(rawValue || "").trim().toLowerCase();
1120
1170
  if (!normalized) {
1121
- return PACKAGE_INSTALL_MODE_INSTALLABLE;
1171
+ throw createCliError(
1172
+ `Invalid package descriptor at ${descriptorPath}: missing kind (expected ${PACKAGE_KINDS.join(" | ")}).`
1173
+ );
1122
1174
  }
1123
- if (!PACKAGE_INSTALL_MODES.includes(normalized)) {
1175
+ if (!PACKAGE_KINDS.includes(normalized)) {
1124
1176
  throw createCliError(
1125
- `Invalid package descriptor at ${descriptorPath}: installationMode must be one of: ${PACKAGE_INSTALL_MODES.join(", ")}.`
1177
+ `Invalid package descriptor at ${descriptorPath}: kind must be one of: ${PACKAGE_KINDS.join(", ")}.`
1126
1178
  );
1127
1179
  }
1128
1180
  return normalized;
@@ -1177,13 +1229,13 @@ function validatePackageDescriptorShape(descriptor, descriptorPath) {
1177
1229
 
1178
1230
  return {
1179
1231
  ...normalized,
1180
- installationMode: normalizePackageInstallationMode(normalized.installationMode, descriptorPath)
1232
+ kind: normalizePackageKind(normalized.kind, descriptorPath)
1181
1233
  };
1182
1234
  }
1183
1235
 
1184
- function isCloneOnlyPackageEntry(packageEntry) {
1236
+ function isGeneratorPackageEntry(packageEntry) {
1185
1237
  const descriptor = ensureObject(packageEntry?.descriptor);
1186
- return String(descriptor.installationMode || "").trim().toLowerCase() === PACKAGE_INSTALL_MODE_CLONE_ONLY;
1238
+ return String(descriptor.kind || "").trim().toLowerCase() === PACKAGE_KIND_GENERATOR;
1187
1239
  }
1188
1240
 
1189
1241
  function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { expectedPackageId = "", fallbackVersion = "" } = {}) {
@@ -1208,7 +1260,8 @@ function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { ex
1208
1260
  return {
1209
1261
  ...normalized,
1210
1262
  packageId,
1211
- version
1263
+ version,
1264
+ kind: normalizePackageKind(normalized.kind, descriptorPath)
1212
1265
  };
1213
1266
  }
1214
1267
 
@@ -1277,85 +1330,6 @@ function validateBundleDescriptorShape(descriptor, descriptorPath) {
1277
1330
  return normalized;
1278
1331
  }
1279
1332
 
1280
- async function loadWorkspacePackageRegistry() {
1281
- if (!MODULES_ROOT || !(await fileExists(MODULES_ROOT))) {
1282
- return new Map();
1283
- }
1284
-
1285
- const directories = [];
1286
- const levelOne = await readdir(MODULES_ROOT, { withFileTypes: true });
1287
-
1288
- for (const entry of levelOne) {
1289
- if (!entry.isDirectory()) {
1290
- continue;
1291
- }
1292
- if (entry.name.startsWith(".") || entry.name.endsWith(".LEGACY")) {
1293
- continue;
1294
- }
1295
-
1296
- const absolute = path.join(MODULES_ROOT, entry.name);
1297
- const descriptorPath = path.join(absolute, "package.descriptor.mjs");
1298
- if (await fileExists(descriptorPath)) {
1299
- directories.push(absolute);
1300
- continue;
1301
- }
1302
-
1303
- const nested = await readdir(absolute, { withFileTypes: true }).catch(() => []);
1304
- for (const child of nested) {
1305
- if (!child.isDirectory() || child.name.startsWith(".")) {
1306
- continue;
1307
- }
1308
- const nestedAbsolute = path.join(absolute, child.name);
1309
- const nestedDescriptor = path.join(nestedAbsolute, "package.descriptor.mjs");
1310
- if (await fileExists(nestedDescriptor)) {
1311
- directories.push(nestedAbsolute);
1312
- }
1313
- }
1314
- }
1315
-
1316
- const uniqueDirectories = sortStrings([...new Set(directories)]);
1317
- const registry = new Map();
1318
-
1319
- for (const packageRoot of uniqueDirectories) {
1320
- const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
1321
- const descriptorModule = await import(pathToFileURL(descriptorPath).href);
1322
- const descriptor = validatePackageDescriptorShape(descriptorModule?.default, descriptorPath);
1323
-
1324
- const packageJsonPath = path.join(packageRoot, "package.json");
1325
- if (!(await fileExists(packageJsonPath))) {
1326
- throw createCliError(`Missing package.json for ${descriptor.packageId} at ${packageRoot}.`);
1327
- }
1328
- const packageJson = await readJsonFile(packageJsonPath);
1329
- const packageName = String(packageJson?.name || "").trim();
1330
- if (packageName !== descriptor.packageId) {
1331
- throw createCliError(
1332
- `Descriptor/package mismatch at ${packageRoot}: package.descriptor.mjs has ${descriptor.packageId} but package.json has ${packageName || "(empty)"}.`
1333
- );
1334
- }
1335
-
1336
- const relativeDir = normalizeRelativePath(WORKSPACE_ROOT || MODULES_ROOT, packageRoot);
1337
- const descriptorRelativePath = normalizeRelativePath(WORKSPACE_ROOT || MODULES_ROOT, descriptorPath);
1338
- registry.set(
1339
- descriptor.packageId,
1340
- createPackageEntry({
1341
- packageId: descriptor.packageId,
1342
- version: descriptor.version,
1343
- descriptor,
1344
- rootDir: packageRoot,
1345
- relativeDir,
1346
- descriptorRelativePath,
1347
- packageJson,
1348
- sourceType: "packages-directory",
1349
- source: {
1350
- descriptorPath: descriptorRelativePath
1351
- }
1352
- })
1353
- );
1354
- }
1355
-
1356
- return registry;
1357
- }
1358
-
1359
1333
  async function loadAppLocalPackageRegistry(appRoot) {
1360
1334
  const localPackagesRoot = path.join(appRoot, "packages");
1361
1335
  if (!(await fileExists(localPackagesRoot))) {
@@ -1468,17 +1442,14 @@ async function loadCatalogPackageRegistry() {
1468
1442
  }
1469
1443
 
1470
1444
  async function loadPackageRegistry() {
1471
- const workspaceRegistry = await loadWorkspacePackageRegistry();
1472
1445
  const catalogRegistry = await loadCatalogPackageRegistry();
1473
- const merged = mergePackageRegistries(catalogRegistry, workspaceRegistry);
1474
-
1475
- if (merged.size === 0) {
1446
+ if (catalogRegistry.size === 0) {
1476
1447
  throw createCliError(
1477
- "Unable to load package registry. Provide JSKIT_REPO_ROOT for workspace mode or ensure @jskit-ai/jskit-catalog is installed (or set JSKIT_CATALOG_PACKAGES_PATH)."
1448
+ "Unable to load package registry from @jskit-ai/jskit-catalog. Install it alongside @jskit-ai/jskit-cli or set JSKIT_CATALOG_PACKAGES_PATH."
1478
1449
  );
1479
1450
  }
1480
1451
 
1481
- return merged;
1452
+ return catalogRegistry;
1482
1453
  }
1483
1454
 
1484
1455
  async function loadInstalledNodeModulePackageEntry({ appRoot, packageId }) {
@@ -1745,6 +1716,7 @@ function createLocalPackageDescriptorTemplate({ packageId, description }) {
1745
1716
  packageVersion: 1,
1746
1717
  packageId: "${packageId}",
1747
1718
  version: "0.1.0",
1719
+ kind: "runtime",
1748
1720
  description: ${JSON.stringify(String(description || ""))},
1749
1721
  dependsOn: [
1750
1722
  // "@jskit-ai/kernel"
@@ -3544,20 +3516,134 @@ function interpolateFileMutationRecord(mutation, options, packageId) {
3544
3516
  extension: interpolate(mutation.extension, "extension"),
3545
3517
  id: interpolate(mutation.id, "id"),
3546
3518
  category: interpolate(mutation.category, "category"),
3547
- reason: interpolate(mutation.reason, "reason")
3519
+ reason: interpolate(mutation.reason, "reason"),
3520
+ templateContext: mutation.templateContext
3521
+ ? {
3522
+ entrypoint: interpolate(mutation.templateContext.entrypoint, "templateContext.entrypoint"),
3523
+ export: interpolate(mutation.templateContext.export, "templateContext.export")
3524
+ }
3525
+ : null
3548
3526
  };
3549
3527
  }
3550
3528
 
3551
- async function copyTemplateFile(sourcePath, targetPath, options, packageId, interpolationKey) {
3529
+ function applyTemplateContextReplacements(sourceContent, replacements) {
3530
+ let output = String(sourceContent || "");
3531
+ for (const [placeholder, value] of Object.entries(ensureObject(replacements))) {
3532
+ const normalizedPlaceholder = String(placeholder || "");
3533
+ if (!normalizedPlaceholder) {
3534
+ continue;
3535
+ }
3536
+ output = output.split(normalizedPlaceholder).join(String(value == null ? "" : value));
3537
+ }
3538
+ return output;
3539
+ }
3540
+
3541
+ async function copyTemplateFile(
3542
+ sourcePath,
3543
+ targetPath,
3544
+ options,
3545
+ packageId,
3546
+ interpolationKey,
3547
+ templateContextReplacements = null
3548
+ ) {
3552
3549
  const sourceContent = await readFile(sourcePath, "utf8");
3553
- const renderedContent = sourceContent.includes("${")
3550
+ let renderedContent = sourceContent.includes("${")
3554
3551
  ? interpolateOptionValue(sourceContent, options, packageId, interpolationKey)
3555
3552
  : sourceContent;
3553
+ if (templateContextReplacements) {
3554
+ renderedContent = applyTemplateContextReplacements(renderedContent, templateContextReplacements);
3555
+ }
3556
3556
 
3557
3557
  await mkdir(path.dirname(targetPath), { recursive: true });
3558
3558
  await writeFile(targetPath, renderedContent, "utf8");
3559
3559
  }
3560
3560
 
3561
+ async function resolveTemplateContextReplacementsForMutation({
3562
+ packageEntry,
3563
+ mutation,
3564
+ options,
3565
+ appRoot,
3566
+ sourcePath,
3567
+ targetPaths
3568
+ } = {}) {
3569
+ const templateContext = ensureObject(mutation?.templateContext);
3570
+ const hasTemplateContext = Object.keys(templateContext).length > 0;
3571
+ const entrypoint = String(templateContext.entrypoint || "").trim();
3572
+ if (!hasTemplateContext) {
3573
+ return null;
3574
+ }
3575
+ if (!entrypoint) {
3576
+ throw createCliError(
3577
+ `Invalid files mutation in ${packageEntry.packageId}: templateContext.entrypoint is required when templateContext is set.`
3578
+ );
3579
+ }
3580
+ const exportName = String(templateContext.export || "").trim() || "buildTemplateContext";
3581
+ const resolvedEntrypointPath = resolveAppRelativePathWithinRoot(
3582
+ packageEntry.rootDir,
3583
+ entrypoint,
3584
+ `${packageEntry.packageId} files mutation templateContext.entrypoint`
3585
+ );
3586
+ const absoluteEntrypointPath = resolvedEntrypointPath.absolutePath;
3587
+ if (!(await fileExists(absoluteEntrypointPath))) {
3588
+ throw createCliError(
3589
+ `Invalid files mutation in ${packageEntry.packageId}: templateContext.entrypoint not found at ${entrypoint}.`
3590
+ );
3591
+ }
3592
+
3593
+ let moduleNamespace = null;
3594
+ try {
3595
+ moduleNamespace = await import(`${pathToFileURL(absoluteEntrypointPath).href}?t=${Date.now()}_${Math.random()}`);
3596
+ } catch (error) {
3597
+ throw createCliError(
3598
+ `Unable to load templateContext entrypoint ${entrypoint} for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
3599
+ );
3600
+ }
3601
+
3602
+ const resolver = moduleNamespace?.[exportName];
3603
+ if (typeof resolver !== "function") {
3604
+ throw createCliError(
3605
+ `Invalid files mutation in ${packageEntry.packageId}: templateContext export "${exportName}" is not a function.`
3606
+ );
3607
+ }
3608
+
3609
+ let replacements = null;
3610
+ try {
3611
+ replacements = await resolver({
3612
+ packageId: packageEntry.packageId,
3613
+ packageRoot: packageEntry.rootDir,
3614
+ appRoot,
3615
+ options: Object.freeze({ ...ensureObject(options) }),
3616
+ mutation: Object.freeze({ ...ensureObject(mutation) }),
3617
+ sourcePath,
3618
+ targetPaths: Object.freeze([...ensureArray(targetPaths)])
3619
+ });
3620
+ } catch (error) {
3621
+ throw createCliError(
3622
+ `templateContext export "${exportName}" failed for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
3623
+ );
3624
+ }
3625
+
3626
+ if (replacements == null) {
3627
+ return null;
3628
+ }
3629
+ if (!replacements || typeof replacements !== "object" || Array.isArray(replacements)) {
3630
+ throw createCliError(
3631
+ `Invalid files mutation in ${packageEntry.packageId}: templateContext export "${exportName}" must return an object map of placeholder replacements.`
3632
+ );
3633
+ }
3634
+
3635
+ const normalizedReplacements = {};
3636
+ for (const [placeholder, value] of Object.entries(replacements)) {
3637
+ const normalizedPlaceholder = String(placeholder || "").trim();
3638
+ if (!normalizedPlaceholder) {
3639
+ continue;
3640
+ }
3641
+ normalizedReplacements[normalizedPlaceholder] = String(value == null ? "" : value);
3642
+ }
3643
+
3644
+ return Object.freeze(normalizedReplacements);
3645
+ }
3646
+
3561
3647
  function normalizeSurfaceIdForMutation(value = "") {
3562
3648
  return String(value || "")
3563
3649
  .trim()
@@ -3738,8 +3824,10 @@ async function applyFileMutations(
3738
3824
  managedFiles,
3739
3825
  managedMigrations,
3740
3826
  touchedFiles,
3741
- warnings = []
3827
+ warnings = [],
3828
+ precomputedTemplateContextByMutationIndex = null
3742
3829
  ) {
3830
+ const mutationList = ensureArray(fileMutations);
3743
3831
  const managedMigrationById = new Map();
3744
3832
  for (const managedMigrationValue of ensureArray(managedMigrations)) {
3745
3833
  const managedMigration = ensureObject(managedMigrationValue);
@@ -3750,7 +3838,7 @@ async function applyFileMutations(
3750
3838
  managedMigrationById.set(migrationId, managedMigration);
3751
3839
  }
3752
3840
 
3753
- for (const mutationValue of fileMutations) {
3841
+ for (const [mutationIndex, mutationValue] of mutationList.entries()) {
3754
3842
  const normalizedMutation = normalizeFileMutationRecord(mutationValue);
3755
3843
  const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
3756
3844
  const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
@@ -3788,10 +3876,27 @@ async function applyFileMutations(
3788
3876
  throw createCliError(`Missing migration template source ${sourcePath} for ${packageEntry.packageId}.`);
3789
3877
  }
3790
3878
 
3879
+ const hasPrecomputedTemplateContext =
3880
+ precomputedTemplateContextByMutationIndex instanceof Map &&
3881
+ precomputedTemplateContextByMutationIndex.has(mutationIndex);
3882
+ const templateContextReplacements = hasPrecomputedTemplateContext
3883
+ ? precomputedTemplateContextByMutationIndex.get(mutationIndex)
3884
+ : await resolveTemplateContextReplacementsForMutation({
3885
+ packageEntry,
3886
+ mutation,
3887
+ options,
3888
+ appRoot,
3889
+ sourcePath,
3890
+ targetPaths: [path.join(appRoot, toDir)]
3891
+ });
3892
+
3791
3893
  const sourceContent = await readFile(sourcePath, "utf8");
3792
- const renderedSourceContent = sourceContent.includes("${")
3894
+ let renderedSourceContent = sourceContent.includes("${")
3793
3895
  ? interpolateOptionValue(sourceContent, options, packageEntry.packageId, `${mutation.id || from}.source`)
3794
3896
  : sourceContent;
3897
+ if (templateContextReplacements) {
3898
+ renderedSourceContent = applyTemplateContextReplacements(renderedSourceContent, templateContextReplacements);
3899
+ }
3795
3900
  const sourceExtension = normalizeMigrationExtension(path.extname(from), ".cjs");
3796
3901
  const extension = normalizeMigrationExtension(mutation.extension, sourceExtension);
3797
3902
  const sourceHash = hashBuffer(Buffer.from(renderedSourceContent, "utf8"));
@@ -3956,6 +4061,19 @@ async function applyFileMutations(
3956
4061
  configContext
3957
4062
  })
3958
4063
  : [path.join(appRoot, to)];
4064
+ const hasPrecomputedTemplateContext =
4065
+ precomputedTemplateContextByMutationIndex instanceof Map &&
4066
+ precomputedTemplateContextByMutationIndex.has(mutationIndex);
4067
+ const templateContextReplacements = hasPrecomputedTemplateContext
4068
+ ? precomputedTemplateContextByMutationIndex.get(mutationIndex)
4069
+ : await resolveTemplateContextReplacementsForMutation({
4070
+ packageEntry,
4071
+ mutation,
4072
+ options,
4073
+ appRoot,
4074
+ sourcePath,
4075
+ targetPaths
4076
+ });
3959
4077
  for (const targetPath of targetPaths) {
3960
4078
  const previous = await readFileBufferIfExists(targetPath);
3961
4079
  await copyTemplateFile(
@@ -3963,7 +4081,8 @@ async function applyFileMutations(
3963
4081
  targetPath,
3964
4082
  options,
3965
4083
  packageEntry.packageId,
3966
- `${mutation.id || to || from}.source`
4084
+ `${mutation.id || to || from}.source`,
4085
+ templateContextReplacements
3967
4086
  );
3968
4087
  const nextBuffer = await readFile(targetPath);
3969
4088
 
@@ -3982,6 +4101,91 @@ async function applyFileMutations(
3982
4101
  }
3983
4102
  }
3984
4103
 
4104
+ async function preflightFileMutationTemplateContexts(
4105
+ packageEntry,
4106
+ options,
4107
+ appRoot,
4108
+ fileMutations
4109
+ ) {
4110
+ const mutationList = ensureArray(fileMutations);
4111
+ const replacementsByMutationIndex = new Map();
4112
+
4113
+ for (const [mutationIndex, mutationValue] of mutationList.entries()) {
4114
+ const normalizedMutation = normalizeFileMutationRecord(mutationValue);
4115
+ const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
4116
+ const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
4117
+ if (
4118
+ !shouldApplyMutationWhen(normalizedMutation.when, {
4119
+ options,
4120
+ configContext,
4121
+ packageId: packageEntry.packageId,
4122
+ mutationContext: "files mutation"
4123
+ })
4124
+ ) {
4125
+ continue;
4126
+ }
4127
+
4128
+ const mutation = interpolateFileMutationRecord(normalizedMutation, options, packageEntry.packageId);
4129
+ const templateContext = ensureObject(mutation.templateContext);
4130
+ if (Object.keys(templateContext).length < 1) {
4131
+ continue;
4132
+ }
4133
+
4134
+ const operation = mutation.op || "copy-file";
4135
+ if (operation !== "copy-file" && operation !== "install-migration") {
4136
+ continue;
4137
+ }
4138
+
4139
+ const from = mutation.from;
4140
+ const to = mutation.to;
4141
+ const toSurface = mutation.toSurface;
4142
+ if (!from) {
4143
+ throw createCliError(
4144
+ `Invalid files mutation in ${packageEntry.packageId}: "from" is required.`
4145
+ );
4146
+ }
4147
+ if (operation === "copy-file") {
4148
+ if (to && toSurface) {
4149
+ throw createCliError(
4150
+ `Invalid files mutation in ${packageEntry.packageId}: "to" and "toSurface" cannot both be set.`
4151
+ );
4152
+ }
4153
+ if (!to && !toSurface) {
4154
+ throw createCliError(
4155
+ `Invalid files mutation in ${packageEntry.packageId}: "from" plus one destination ("to" or "toSurface") are required.`
4156
+ );
4157
+ }
4158
+ }
4159
+
4160
+ const sourcePath = path.join(packageEntry.rootDir, from);
4161
+ if (!(await fileExists(sourcePath))) {
4162
+ throw createCliError(`Missing template source ${sourcePath} for ${packageEntry.packageId}.`);
4163
+ }
4164
+
4165
+ const targetPaths = operation === "copy-file"
4166
+ ? toSurface
4167
+ ? resolveSurfaceTargetPathsForMutation({
4168
+ appRoot,
4169
+ packageId: packageEntry.packageId,
4170
+ mutation,
4171
+ configContext
4172
+ })
4173
+ : [path.join(appRoot, to)]
4174
+ : [path.join(appRoot, mutation.toDir || "migrations")];
4175
+ const replacements = await resolveTemplateContextReplacementsForMutation({
4176
+ packageEntry,
4177
+ mutation,
4178
+ options,
4179
+ appRoot,
4180
+ sourcePath,
4181
+ targetPaths
4182
+ });
4183
+ replacementsByMutationIndex.set(mutationIndex, replacements);
4184
+ }
4185
+
4186
+ return replacementsByMutationIndex;
4187
+ }
4188
+
3985
4189
  async function applyTextMutations(packageEntry, appRoot, textMutations, options, managedText, touchedFiles) {
3986
4190
  for (const mutation of textMutations) {
3987
4191
  const when = normalizeMutationWhen(mutation?.when);
@@ -4298,6 +4502,79 @@ async function applyPackagePositioning({
4298
4502
  return managedRecord;
4299
4503
  }
4300
4504
 
4505
+ async function applyPackageMigrationsOnly({
4506
+ packageEntry,
4507
+ packageOptions,
4508
+ appRoot,
4509
+ lock,
4510
+ touchedFiles
4511
+ }) {
4512
+ const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
4513
+ if (Object.keys(existingInstall).length < 1) {
4514
+ throw createCliError(`Package is not installed: ${packageEntry.packageId}`);
4515
+ }
4516
+
4517
+ const existingManaged = ensureObject(existingInstall.managed);
4518
+ const existingPackageJsonManaged = ensureObject(existingManaged.packageJson);
4519
+ const nextManaged = {
4520
+ packageJson: {
4521
+ dependencies: cloneManagedMap(existingPackageJsonManaged.dependencies),
4522
+ devDependencies: cloneManagedMap(existingPackageJsonManaged.devDependencies),
4523
+ scripts: cloneManagedMap(existingPackageJsonManaged.scripts)
4524
+ },
4525
+ text: cloneManagedMap(existingManaged.text),
4526
+ vite: cloneManagedMap(existingManaged.vite),
4527
+ files: cloneManagedArray(existingManaged.files),
4528
+ migrations: cloneManagedArray(existingManaged.migrations)
4529
+ };
4530
+
4531
+ const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
4532
+ const packageEntryForMutations =
4533
+ templateRoot === packageEntry.rootDir
4534
+ ? packageEntry
4535
+ : {
4536
+ ...packageEntry,
4537
+ rootDir: templateRoot
4538
+ };
4539
+ const mutations = ensureObject(packageEntry.descriptor.mutations);
4540
+ const migrationFileMutations = ensureArray(mutations.files).filter((mutationValue) => {
4541
+ const normalized = normalizeFileMutationRecord(mutationValue);
4542
+ const operation = String(normalized.op || "copy-file").trim();
4543
+ return operation === "install-migration";
4544
+ });
4545
+ const mutationWarnings = [];
4546
+
4547
+ if (migrationFileMutations.length > 0) {
4548
+ await applyFileMutations(
4549
+ packageEntryForMutations,
4550
+ packageOptions,
4551
+ appRoot,
4552
+ migrationFileMutations,
4553
+ [],
4554
+ nextManaged.migrations,
4555
+ touchedFiles,
4556
+ mutationWarnings
4557
+ );
4558
+ }
4559
+
4560
+ const managedRecord = {
4561
+ ...existingInstall,
4562
+ packageId: packageEntry.packageId,
4563
+ source: resolveManagedSourceRecord(packageEntry, existingInstall),
4564
+ managed: nextManaged,
4565
+ options: {
4566
+ ...ensureObject(packageOptions)
4567
+ },
4568
+ migrationSyncVersion: packageEntry.version,
4569
+ installedAt: String(existingInstall.installedAt || new Date().toISOString())
4570
+ };
4571
+ lock.installedPackages[packageEntry.packageId] = managedRecord;
4572
+ if (mutationWarnings.length > 0) {
4573
+ managedRecord.warnings = mutationWarnings;
4574
+ }
4575
+ return managedRecord;
4576
+ }
4577
+
4301
4578
  async function applyPackageInstall({
4302
4579
  packageEntry,
4303
4580
  packageOptions,
@@ -4318,9 +4595,10 @@ async function applyPackageInstall({
4318
4595
 
4319
4596
  const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
4320
4597
  managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
4321
- const cloneOnlyPackage = isCloneOnlyPackageEntry(packageEntry);
4598
+ const generatorPackage = isGeneratorPackageEntry(packageEntry);
4322
4599
  const mutationWarnings = [];
4323
4600
  const mutations = ensureObject(packageEntry.descriptor.mutations);
4601
+ const fileMutations = ensureArray(mutations.files);
4324
4602
  const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
4325
4603
  const packageEntryForMutations =
4326
4604
  templateRoot === packageEntry.rootDir
@@ -4329,6 +4607,14 @@ async function applyPackageInstall({
4329
4607
  ...packageEntry,
4330
4608
  rootDir: templateRoot
4331
4609
  };
4610
+
4611
+ const precomputedTemplateContextByMutationIndex = await preflightFileMutationTemplateContexts(
4612
+ packageEntryForMutations,
4613
+ packageOptions,
4614
+ appRoot,
4615
+ fileMutations
4616
+ );
4617
+
4332
4618
  const mutationDependencies = ensureObject(mutations.dependencies);
4333
4619
  const runtimeDependencies = ensureObject(mutationDependencies.runtime);
4334
4620
  const devDependencies = ensureObject(mutationDependencies.dev);
@@ -4398,7 +4684,7 @@ async function applyPackageInstall({
4398
4684
  }
4399
4685
  }
4400
4686
 
4401
- if (cloneOnlyPackage) {
4687
+ if (generatorPackage) {
4402
4688
  const removedRuntimeDependency = removePackageJsonField(appPackageJson, "dependencies", packageEntry.packageId);
4403
4689
  const removedDevDependency = removePackageJsonField(appPackageJson, "devDependencies", packageEntry.packageId);
4404
4690
  if (removedRuntimeDependency || removedDevDependency) {
@@ -4434,11 +4720,12 @@ async function applyPackageInstall({
4434
4720
  packageEntryForMutations,
4435
4721
  packageOptions,
4436
4722
  appRoot,
4437
- ensureArray(mutations.files),
4723
+ fileMutations,
4438
4724
  managedRecord.managed.files,
4439
4725
  managedRecord.managed.migrations,
4440
4726
  touchedFiles,
4441
- mutationWarnings
4727
+ mutationWarnings,
4728
+ precomputedTemplateContextByMutationIndex
4442
4729
  );
4443
4730
 
4444
4731
  await applyTextMutations(
@@ -4459,9 +4746,10 @@ async function applyPackageInstall({
4459
4746
  touchedFiles
4460
4747
  );
4461
4748
 
4462
- if (cloneOnlyPackage) {
4749
+ if (generatorPackage) {
4463
4750
  delete lock.installedPackages[packageEntry.packageId];
4464
4751
  } else {
4752
+ managedRecord.migrationSyncVersion = packageEntry.version;
4465
4753
  lock.installedPackages[packageEntry.packageId] = managedRecord;
4466
4754
  }
4467
4755
  if (mutationWarnings.length > 0) {
@@ -4522,6 +4810,7 @@ const commandHandlers = createCommandHandlers(
4522
4810
  validatePlannedCapabilityClosure,
4523
4811
  resolvePackageOptions,
4524
4812
  applyPackageInstall,
4813
+ applyPackageMigrationsOnly,
4525
4814
  applyPackagePositioning,
4526
4815
  adoptAppLocalPackageDependencies,
4527
4816
  loadAppPackageJson,