@jskit-ai/jskit-cli 0.2.25 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
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.25"
23
+ "@jskit-ai/jskit-catalog": "0.1.26"
24
24
  },
25
25
  "engines": {
26
26
  "node": "20.x"
@@ -1,6 +1,7 @@
1
1
  const KNOWN_COMMANDS = new Set([
2
2
  "help",
3
3
  "create",
4
+ "generate",
4
5
  "list",
5
6
  "show",
6
7
  "view",
@@ -15,7 +16,8 @@ const KNOWN_COMMANDS = new Set([
15
16
 
16
17
  const COMMAND_ALIASES = Object.freeze({
17
18
  view: "show",
18
- ls: "list"
19
+ ls: "list",
20
+ gen: "generate"
19
21
  });
20
22
 
21
23
  function resolveCommandAlias(rawCommand) {
@@ -44,6 +46,8 @@ function parseArgs(argv, { createCliError } = {}) {
44
46
  expanded: false,
45
47
  details: false,
46
48
  debugExports: false,
49
+ checkDiLabels: false,
50
+ verbose: false,
47
51
  json: false,
48
52
  all: false,
49
53
  help: true,
@@ -67,6 +71,8 @@ function parseArgs(argv, { createCliError } = {}) {
67
71
  expanded: false,
68
72
  details: false,
69
73
  debugExports: false,
74
+ checkDiLabels: false,
75
+ verbose: false,
70
76
  json: false,
71
77
  all: false,
72
78
  help: false,
@@ -101,6 +107,14 @@ function parseArgs(argv, { createCliError } = {}) {
101
107
  options.debugExports = true;
102
108
  continue;
103
109
  }
110
+ if (token === "--check-di-labels") {
111
+ options.checkDiLabels = true;
112
+ continue;
113
+ }
114
+ if (token === "--verbose") {
115
+ options.verbose = true;
116
+ continue;
117
+ }
104
118
  if (token === "--json") {
105
119
  options.json = true;
106
120
  continue;
@@ -159,10 +173,11 @@ function printUsage(stream = process.stderr) {
159
173
  stream.write("Usage: jskit <command> [options]\n\n");
160
174
  stream.write("Commands:\n");
161
175
  stream.write(" create package <name> Scaffold app-local package under packages/ and install it\n");
162
- stream.write(" list [bundles [all]|packages] List available bundles/packages and installed status\n");
176
+ stream.write(" list [bundles [all]|packages|generators] List available bundles/runtime packages/generators and installed status\n");
163
177
  stream.write(" lint-descriptors Validate bundle/package descriptor files\n");
164
178
  stream.write(" add bundle <bundleId> Add one bundle (bundle is a package shortcut)\n");
165
- stream.write(" add package <packageId> Add one package to current app (catalog/app-local/installed external)\n");
179
+ stream.write(" add package <packageId> Add one runtime package to current app (catalog/app-local/installed external)\n");
180
+ stream.write(" generate <packageId> Run one generator package\n");
166
181
  stream.write(" position element <packageId> Re-apply positioning mutations for one installed package\n");
167
182
  stream.write(" show <id> Show details for bundle id or package id\n");
168
183
  stream.write(" view <id> Alias of show <id>\n");
@@ -173,7 +188,7 @@ function printUsage(stream = process.stderr) {
173
188
  stream.write("\n");
174
189
  stream.write("Options:\n");
175
190
  stream.write(" --dry-run Print planned changes only\n");
176
- stream.write(" --no-install Skip npm install during create/add/update/remove\n");
191
+ stream.write(" --no-install Skip npm install during create/add/generate/update/remove\n");
177
192
  stream.write(" --scope <scope> (create package) override generated package scope\n");
178
193
  stream.write(" --package-id <id> (create package) explicit @scope/name package id\n");
179
194
  stream.write(" --description <text> (create package) descriptor description text\n");
@@ -181,6 +196,8 @@ function printUsage(stream = process.stderr) {
181
196
  stream.write(" --expanded Show expanded/transitive package ids\n");
182
197
  stream.write(" --details Show extra capability detail in show output\n");
183
198
  stream.write(" --debug-exports Show export provenance/re-export source details in show output\n");
199
+ stream.write(" --check-di-labels (lint-descriptors) verify DI labels used by providers match descriptor container tokens\n");
200
+ stream.write(" --verbose Show verbose informational diagnostics\n");
184
201
  stream.write(" --<option> <value> Package option (for packages requiring input)\n");
185
202
  stream.write(" --json Print structured output\n");
186
203
  stream.write(" -h, --help Show help\n");
@@ -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);
@@ -4391,9 +4595,10 @@ async function applyPackageInstall({
4391
4595
 
4392
4596
  const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
4393
4597
  managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
4394
- const cloneOnlyPackage = isCloneOnlyPackageEntry(packageEntry);
4598
+ const generatorPackage = isGeneratorPackageEntry(packageEntry);
4395
4599
  const mutationWarnings = [];
4396
4600
  const mutations = ensureObject(packageEntry.descriptor.mutations);
4601
+ const fileMutations = ensureArray(mutations.files);
4397
4602
  const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
4398
4603
  const packageEntryForMutations =
4399
4604
  templateRoot === packageEntry.rootDir
@@ -4402,6 +4607,14 @@ async function applyPackageInstall({
4402
4607
  ...packageEntry,
4403
4608
  rootDir: templateRoot
4404
4609
  };
4610
+
4611
+ const precomputedTemplateContextByMutationIndex = await preflightFileMutationTemplateContexts(
4612
+ packageEntryForMutations,
4613
+ packageOptions,
4614
+ appRoot,
4615
+ fileMutations
4616
+ );
4617
+
4405
4618
  const mutationDependencies = ensureObject(mutations.dependencies);
4406
4619
  const runtimeDependencies = ensureObject(mutationDependencies.runtime);
4407
4620
  const devDependencies = ensureObject(mutationDependencies.dev);
@@ -4471,7 +4684,7 @@ async function applyPackageInstall({
4471
4684
  }
4472
4685
  }
4473
4686
 
4474
- if (cloneOnlyPackage) {
4687
+ if (generatorPackage) {
4475
4688
  const removedRuntimeDependency = removePackageJsonField(appPackageJson, "dependencies", packageEntry.packageId);
4476
4689
  const removedDevDependency = removePackageJsonField(appPackageJson, "devDependencies", packageEntry.packageId);
4477
4690
  if (removedRuntimeDependency || removedDevDependency) {
@@ -4507,11 +4720,12 @@ async function applyPackageInstall({
4507
4720
  packageEntryForMutations,
4508
4721
  packageOptions,
4509
4722
  appRoot,
4510
- ensureArray(mutations.files),
4723
+ fileMutations,
4511
4724
  managedRecord.managed.files,
4512
4725
  managedRecord.managed.migrations,
4513
4726
  touchedFiles,
4514
- mutationWarnings
4727
+ mutationWarnings,
4728
+ precomputedTemplateContextByMutationIndex
4515
4729
  );
4516
4730
 
4517
4731
  await applyTextMutations(
@@ -4532,7 +4746,7 @@ async function applyPackageInstall({
4532
4746
  touchedFiles
4533
4747
  );
4534
4748
 
4535
- if (cloneOnlyPackage) {
4749
+ if (generatorPackage) {
4536
4750
  delete lock.installedPackages[packageEntry.packageId];
4537
4751
  } else {
4538
4752
  managedRecord.migrationSyncVersion = packageEntry.version;
@@ -124,6 +124,18 @@ function createCommandHandlers(deps) {
124
124
  return sortStrings(dependents);
125
125
  }
126
126
 
127
+ function resolvePackageKind(packageEntry) {
128
+ const descriptor = ensureObject(packageEntry?.descriptor);
129
+ const normalizedKind = String(descriptor.kind || "").trim().toLowerCase();
130
+ if (normalizedKind === "runtime" || normalizedKind === "generator") {
131
+ return normalizedKind;
132
+ }
133
+ const packageId = String(packageEntry?.packageId || descriptor.packageId || "unknown-package").trim();
134
+ throw createCliError(
135
+ `Invalid package descriptor for ${packageId}: missing/invalid kind (expected runtime or generator).`
136
+ );
137
+ }
138
+
127
139
  function resolvePackageOptionNames(packageEntry) {
128
140
  const optionSchemas = ensureObject(packageEntry?.descriptor?.options);
129
141
  return Object.keys(optionSchemas);
@@ -177,6 +189,155 @@ function createCommandHandlers(deps) {
177
189
  : " This bundle does not accept inline options.";
178
190
  throw createCliError(`Unknown option(s) for bundle ${bundleId}: ${sortedUnknown.join(", ")}.${suffix}`);
179
191
  }
192
+
193
+ function collectDescriptorContainerTokens({ packageId, side, values, issues }) {
194
+ const declaredTokens = new Set();
195
+ const duplicateTokens = new Set();
196
+ let invalidCount = 0;
197
+
198
+ for (const rawValue of ensureArray(values)) {
199
+ if (typeof rawValue !== "string") {
200
+ invalidCount += 1;
201
+ continue;
202
+ }
203
+ const token = rawValue.trim();
204
+ if (!token) {
205
+ invalidCount += 1;
206
+ continue;
207
+ }
208
+ if (declaredTokens.has(token)) {
209
+ duplicateTokens.add(token);
210
+ continue;
211
+ }
212
+ declaredTokens.add(token);
213
+ }
214
+
215
+ if (invalidCount > 0) {
216
+ issues.push({
217
+ packageId,
218
+ side,
219
+ code: "descriptor-token-invalid",
220
+ message: `${packageId} (${side}): metadata.apiSummary.containerTokens includes ${invalidCount} non-string or empty token value(s).`
221
+ });
222
+ }
223
+ for (const token of sortStrings([...duplicateTokens])) {
224
+ issues.push({
225
+ packageId,
226
+ side,
227
+ code: "descriptor-token-duplicate",
228
+ token,
229
+ message: `${packageId} (${side}): descriptor token is declared more than once: ${token}.`
230
+ });
231
+ }
232
+
233
+ return declaredTokens;
234
+ }
235
+
236
+ function collectUsedContainerTokens({ packageId, side, bindings, issues }) {
237
+ const usedTokens = new Set();
238
+ for (const rawBinding of ensureArray(bindings)) {
239
+ const binding = ensureObject(rawBinding);
240
+ const tokenExpression = String(binding.tokenExpression || "").trim();
241
+ const token = String(binding.token || "").trim();
242
+ const location = String(binding.location || "").trim();
243
+ if (binding.tokenResolved !== true || !token) {
244
+ const expressionLabel = tokenExpression || "<empty>";
245
+ const locationSuffix = location ? ` at ${location}` : "";
246
+ issues.push({
247
+ packageId,
248
+ side,
249
+ code: "binding-token-unresolved",
250
+ tokenExpression: expressionLabel,
251
+ location,
252
+ message: `${packageId} (${side}): unresolved DI token expression "${expressionLabel}"${locationSuffix}.`
253
+ });
254
+ continue;
255
+ }
256
+ usedTokens.add(token);
257
+ }
258
+ return usedTokens;
259
+ }
260
+
261
+ function collectProviderIntrospectionIssues({ packageId, packageInsights, issues }) {
262
+ const introspection = ensureObject(packageInsights);
263
+ if (!introspection.available) {
264
+ issues.push({
265
+ packageId,
266
+ side: "",
267
+ code: "provider-introspection-unavailable",
268
+ message: `${packageId}: provider source introspection is unavailable, so DI token parity cannot be verified.`
269
+ });
270
+ return;
271
+ }
272
+
273
+ const notes = ensureArray(introspection.notes).map((value) => String(value || "").trim()).filter(Boolean);
274
+ for (const note of notes) {
275
+ if (
276
+ note.startsWith("Skipped wildcard provider entrypoint during introspection:") ||
277
+ note.startsWith("Provider file missing during introspection:") ||
278
+ note.startsWith("Failed reading provider ")
279
+ ) {
280
+ issues.push({
281
+ packageId,
282
+ side: "",
283
+ code: "provider-introspection-incomplete",
284
+ message: `${packageId}: ${note}`
285
+ });
286
+ }
287
+ }
288
+ }
289
+
290
+ function collectDiLabelParityIssuesForPackage({ packageEntry, packageInsights }) {
291
+ const packageId = String(packageEntry?.packageId || "").trim();
292
+ const descriptor = ensureObject(packageEntry?.descriptor);
293
+ const metadataApiSummary = ensureObject(ensureObject(descriptor.metadata).apiSummary);
294
+ const descriptorTokenSummary = ensureObject(metadataApiSummary.containerTokens);
295
+ const bindingSections = ensureObject(ensureObject(packageInsights).containerBindings);
296
+ const issues = [];
297
+ const sides = ["server", "client"];
298
+
299
+ collectProviderIntrospectionIssues({ packageId, packageInsights, issues });
300
+
301
+ for (const side of sides) {
302
+ const declaredTokens = collectDescriptorContainerTokens({
303
+ packageId,
304
+ side,
305
+ values: descriptorTokenSummary[side],
306
+ issues
307
+ });
308
+ const usedTokens = collectUsedContainerTokens({
309
+ packageId,
310
+ side,
311
+ bindings: bindingSections[side],
312
+ issues
313
+ });
314
+
315
+ for (const token of sortStrings([...usedTokens])) {
316
+ if (!declaredTokens.has(token)) {
317
+ issues.push({
318
+ packageId,
319
+ side,
320
+ code: "binding-token-undeclared",
321
+ token,
322
+ message: `${packageId} (${side}): token is used by providers but missing from metadata.apiSummary.containerTokens.${side}: ${token}.`
323
+ });
324
+ }
325
+ }
326
+ for (const token of sortStrings([...declaredTokens])) {
327
+ if (!usedTokens.has(token)) {
328
+ issues.push({
329
+ packageId,
330
+ side,
331
+ code: "descriptor-token-unused",
332
+ token,
333
+ message: `${packageId} (${side}): token is declared in metadata.apiSummary.containerTokens.${side} but never bound by providers: ${token}.`
334
+ });
335
+ }
336
+ }
337
+ }
338
+
339
+ return issues;
340
+ }
180
341
 
181
342
  async function commandList({ positional, options, cwd, stdout }) {
182
343
  const packageRegistry = await loadPackageRegistry();
@@ -207,8 +368,9 @@ function createCommandHandlers(deps) {
207
368
  const mode = String(positional[0] || "").trim();
208
369
  const shouldListBundles = !mode || mode === "bundles";
209
370
  const shouldListPackages = !mode || mode === "packages";
210
-
211
- if (!shouldListBundles && !shouldListPackages) {
371
+ const shouldListGenerators = !mode || mode === "generators";
372
+
373
+ if (!shouldListBundles && !shouldListPackages && !shouldListGenerators) {
212
374
  throw createCliError(`Unknown list mode: ${mode}`, { showUsage: true });
213
375
  }
214
376
 
@@ -238,8 +400,11 @@ function createCommandHandlers(deps) {
238
400
  if (lines.length > 0) {
239
401
  lines.push("");
240
402
  }
241
- lines.push(color.heading("Available packages:"));
242
- const packageIds = sortStrings([...packageRegistry.keys()]);
403
+ lines.push(color.heading("Available runtime packages:"));
404
+ const packageIds = sortStrings([...packageRegistry.keys()].filter((packageId) => {
405
+ const packageEntry = packageRegistry.get(packageId);
406
+ return resolvePackageKind(packageEntry) === "runtime";
407
+ }));
243
408
  for (const packageId of packageIds) {
244
409
  const packageEntry = packageRegistry.get(packageId);
245
410
  const installedLabel = installedPackages.has(packageId) ? " (installed)" : "";
@@ -281,6 +446,24 @@ function createCommandHandlers(deps) {
281
446
  }
282
447
  }
283
448
  }
449
+
450
+ if (shouldListGenerators) {
451
+ if (lines.length > 0) {
452
+ lines.push("");
453
+ }
454
+ lines.push(color.heading("Available generators:"));
455
+ const packageIds = sortStrings([...packageRegistry.keys()].filter((packageId) => {
456
+ const packageEntry = packageRegistry.get(packageId);
457
+ return resolvePackageKind(packageEntry) === "generator";
458
+ }));
459
+ for (const packageId of packageIds) {
460
+ const packageEntry = packageRegistry.get(packageId);
461
+ const installedLabel = installedPackages.has(packageId) ? " (installed)" : "";
462
+ lines.push(
463
+ `- ${color.item(packageId)} ${color.version(`(${packageEntry.version})`)}${installedLabel ? color.installed(installedLabel) : ""}`
464
+ );
465
+ }
466
+ }
284
467
 
285
468
  if (options.json) {
286
469
  const payload = {
@@ -299,14 +482,42 @@ function createCommandHandlers(deps) {
299
482
  })
300
483
  : [],
301
484
  packages: shouldListPackages
485
+ ? sortStrings([...packageRegistry.keys()])
486
+ .filter((packageId) => resolvePackageKind(packageRegistry.get(packageId)) === "runtime")
487
+ .map((packageId) => {
488
+ const packageEntry = packageRegistry.get(packageId);
489
+ return {
490
+ packageId,
491
+ version: packageEntry.version,
492
+ installed: installedPackages.has(packageId)
493
+ };
494
+ })
495
+ : [],
496
+ runtimePackages: shouldListPackages
302
497
  ? sortStrings([...packageRegistry.keys()]).map((packageId) => {
303
498
  const packageEntry = packageRegistry.get(packageId);
499
+ if (resolvePackageKind(packageEntry) !== "runtime") {
500
+ return null;
501
+ }
304
502
  return {
305
503
  packageId,
306
504
  version: packageEntry.version,
307
505
  installed: installedPackages.has(packageId)
308
506
  };
309
- })
507
+ }).filter(Boolean)
508
+ : [],
509
+ generators: shouldListGenerators
510
+ ? sortStrings([...packageRegistry.keys()]).map((packageId) => {
511
+ const packageEntry = packageRegistry.get(packageId);
512
+ if (resolvePackageKind(packageEntry) !== "generator") {
513
+ return null;
514
+ }
515
+ return {
516
+ packageId,
517
+ version: packageEntry.version,
518
+ installed: installedPackages.has(packageId)
519
+ };
520
+ }).filter(Boolean)
310
521
  : [],
311
522
  installedLocalPackages: shouldListPackages
312
523
  ? installedLocalPackageIds.map((packageId) => {
@@ -1103,10 +1314,16 @@ function createCommandHandlers(deps) {
1103
1314
  }
1104
1315
 
1105
1316
  async function commandAdd({ positional, options, cwd, io }) {
1317
+ const invocationMode = options?.commandMode === "generate" ? "generate" : "add";
1106
1318
  const targetType = String(positional[0] || "").trim();
1107
1319
  const targetId = String(positional[1] || "").trim();
1108
1320
 
1109
1321
  if (!targetType || !targetId) {
1322
+ if (invocationMode === "generate") {
1323
+ throw createCliError("generate requires a package id (generate <packageId>).", {
1324
+ showUsage: true
1325
+ });
1326
+ }
1110
1327
  throw createCliError("add requires target type and id (add bundle <id> | add package <id>).", {
1111
1328
  showUsage: true
1112
1329
  });
@@ -1114,6 +1331,11 @@ function createCommandHandlers(deps) {
1114
1331
  if (targetType !== "bundle" && targetType !== "package") {
1115
1332
  throw createCliError(`Unsupported add target type: ${targetType}`, { showUsage: true });
1116
1333
  }
1334
+ if (invocationMode === "generate" && targetType !== "package") {
1335
+ throw createCliError("generate requires a package id (generate <packageId>).", {
1336
+ showUsage: true
1337
+ });
1338
+ }
1117
1339
 
1118
1340
  const appRoot = await resolveAppRootFromCwd(cwd);
1119
1341
  const packageRegistry = await loadPackageRegistry();
@@ -1123,10 +1345,11 @@ function createCommandHandlers(deps) {
1123
1345
  const { packageJsonPath, packageJson } = await loadAppPackageJson(appRoot);
1124
1346
  const { lockPath, lock } = await loadLockFile(appRoot);
1125
1347
  let resolvedTargetPackageId = targetType === "package" ? resolvePackageIdInput(targetId, combinedPackageRegistry) : "";
1126
- if (targetType === "package" && !resolvedTargetPackageId) {
1348
+ if (targetType === "package") {
1349
+ const packageIdForNodeModulesLookup = resolvedTargetPackageId || targetId;
1127
1350
  const installedNodeModuleEntry = await resolveInstalledNodeModulePackageEntry({
1128
1351
  appRoot,
1129
- packageId: targetId
1352
+ packageId: packageIdForNodeModulesLookup
1130
1353
  });
1131
1354
  if (installedNodeModuleEntry) {
1132
1355
  combinedPackageRegistry.set(installedNodeModuleEntry.packageId, installedNodeModuleEntry);
@@ -1157,6 +1380,17 @@ function createCommandHandlers(deps) {
1157
1380
  if (!targetPackageEntry) {
1158
1381
  throw createCliError(`Unknown package: ${targetId}`);
1159
1382
  }
1383
+ const packageKind = resolvePackageKind(targetPackageEntry);
1384
+ if (invocationMode === "add" && packageKind === "generator") {
1385
+ throw createCliError(
1386
+ `Package ${resolvedTargetPackageId} is a generator. Use: jskit generate ${resolvedTargetPackageId}`
1387
+ );
1388
+ }
1389
+ if (invocationMode === "generate" && packageKind !== "generator") {
1390
+ throw createCliError(
1391
+ `Package ${resolvedTargetPackageId} is a runtime package. Use: jskit add package ${resolvedTargetPackageId}`
1392
+ );
1393
+ }
1160
1394
  validateInlineOptionsForPackage(targetPackageEntry, options.inlineOptions);
1161
1395
  }
1162
1396
 
@@ -1164,6 +1398,17 @@ function createCommandHandlers(deps) {
1164
1398
  targetPackageIds,
1165
1399
  combinedPackageRegistry
1166
1400
  );
1401
+ if (invocationMode === "add" && targetType === "bundle") {
1402
+ const bundledGenerators = resolvedPackageIds.filter((packageId) => {
1403
+ const packageEntry = combinedPackageRegistry.get(packageId);
1404
+ return resolvePackageKind(packageEntry) === "generator";
1405
+ });
1406
+ if (bundledGenerators.length > 0) {
1407
+ throw createCliError(
1408
+ `Bundle ${targetId} includes generator package(s): ${bundledGenerators.join(", ")}. Use: jskit generate <packageId>`
1409
+ );
1410
+ }
1411
+ }
1167
1412
  const plannedInstalledPackageIds = sortStrings([
1168
1413
  ...new Set([
1169
1414
  ...Object.keys(ensureObject(lock.installedPackages)).map((value) => String(value || "").trim()).filter(Boolean),
@@ -1173,7 +1418,7 @@ function createCommandHandlers(deps) {
1173
1418
  validatePlannedCapabilityClosure(
1174
1419
  plannedInstalledPackageIds,
1175
1420
  combinedPackageRegistry,
1176
- `add ${targetType} ${targetId}`
1421
+ `${invocationMode} ${targetType} ${targetId}`
1177
1422
  );
1178
1423
 
1179
1424
  if (targetType === "bundle") {
@@ -1252,14 +1497,18 @@ function createCommandHandlers(deps) {
1252
1497
  validatePlannedCapabilityClosure(
1253
1498
  postInstallPackageIds,
1254
1499
  combinedPackageRegistry,
1255
- `add ${targetType} ${targetId}`
1500
+ `${invocationMode} ${targetType} ${targetId}`
1256
1501
  );
1257
1502
  }
1258
1503
 
1259
1504
  const finalResolvedPackageIds = sortStrings([...resolvedPackageIds, ...adoptedPackageIds]);
1260
1505
 
1261
1506
  const touchedFileList = sortStrings([...touchedFiles]);
1262
- const successLabel = targetType === "bundle" ? "Added bundle" : "Added package";
1507
+ const successLabel = invocationMode === "generate"
1508
+ ? "Generated with"
1509
+ : targetType === "bundle"
1510
+ ? "Added bundle"
1511
+ : "Added package";
1263
1512
  const installWarnings = installedPackageRecords
1264
1513
  .flatMap((record) => ensureArray(ensureObject(record).warnings))
1265
1514
  .map((value) => String(value || "").trim())
@@ -1275,7 +1524,7 @@ function createCommandHandlers(deps) {
1275
1524
 
1276
1525
  if (options.json) {
1277
1526
  io.stdout.write(`${JSON.stringify({
1278
- targetType,
1527
+ targetType: invocationMode === "generate" ? "generator" : targetType,
1279
1528
  targetId,
1280
1529
  resolvedPackages: finalResolvedPackageIds,
1281
1530
  touchedFiles: touchedFileList,
@@ -1310,6 +1559,32 @@ function createCommandHandlers(deps) {
1310
1559
 
1311
1560
  return 0;
1312
1561
  }
1562
+
1563
+ async function commandGenerate({ positional, options, cwd, io }) {
1564
+ const firstToken = String(positional[0] || "").trim();
1565
+ const secondToken = String(positional[1] || "").trim();
1566
+ if (firstToken === "bundle") {
1567
+ throw createCliError("generate supports packages only (generate <packageId>).", {
1568
+ showUsage: true
1569
+ });
1570
+ }
1571
+ const targetId = firstToken === "package" ? secondToken : firstToken;
1572
+ if (!targetId) {
1573
+ throw createCliError("generate requires a package id (generate <packageId>).", {
1574
+ showUsage: true
1575
+ });
1576
+ }
1577
+
1578
+ return commandAdd({
1579
+ positional: ["package", targetId],
1580
+ options: {
1581
+ ...options,
1582
+ commandMode: "generate"
1583
+ },
1584
+ cwd,
1585
+ io
1586
+ });
1587
+ }
1313
1588
 
1314
1589
  async function commandUpdate({ positional, options, cwd, io }) {
1315
1590
  const targetType = String(positional[0] || "").trim();
@@ -1455,7 +1730,7 @@ function createCommandHandlers(deps) {
1455
1730
  io.stdout.write(`- ${touchedFile}\n`);
1456
1731
  }
1457
1732
  io.stdout.write(`Lock file: ${normalizeRelativePath(appRoot, lockPath)}\n`);
1458
- if (migrationWarnings.length > 0) {
1733
+ if (options.verbose && migrationWarnings.length > 0) {
1459
1734
  io.stdout.write(`Warnings (${migrationWarnings.length}):\n`);
1460
1735
  for (const warning of migrationWarnings) {
1461
1736
  io.stdout.write(`- ${warning}\n`);
@@ -1760,19 +2035,58 @@ function createCommandHandlers(deps) {
1760
2035
  async function commandLintDescriptors({ options, stdout }) {
1761
2036
  const packageRegistry = await loadPackageRegistry();
1762
2037
  const bundleRegistry = await loadBundleRegistry();
2038
+ const shouldCheckDiLabels = options.checkDiLabels === true;
2039
+ let diLabelIssues = [];
2040
+ if (shouldCheckDiLabels) {
2041
+ const issues = [];
2042
+ for (const packageId of sortStrings([...packageRegistry.keys()])) {
2043
+ const packageEntry = packageRegistry.get(packageId);
2044
+ if (!packageEntry) {
2045
+ continue;
2046
+ }
2047
+ const packageInsights = await inspectPackageOfferings({ packageEntry });
2048
+ issues.push(...collectDiLabelParityIssuesForPackage({ packageEntry, packageInsights }));
2049
+ }
2050
+ diLabelIssues = issues;
2051
+ }
1763
2052
  const payload = {
1764
2053
  packageCount: packageRegistry.size,
1765
2054
  bundleCount: bundleRegistry.size,
1766
2055
  packages: sortStrings([...packageRegistry.keys()]),
1767
- bundles: sortStrings([...bundleRegistry.keys()])
2056
+ bundles: sortStrings([...bundleRegistry.keys()]),
2057
+ diLabelCheck: shouldCheckDiLabels
2058
+ ? {
2059
+ enabled: true,
2060
+ issueCount: diLabelIssues.length,
2061
+ issues: diLabelIssues
2062
+ }
2063
+ : {
2064
+ enabled: false
2065
+ }
1768
2066
  };
1769
2067
 
1770
2068
  if (options.json) {
1771
2069
  stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
1772
2070
  } else {
1773
- stdout.write(`Descriptor lint passed.\n`);
2071
+ const descriptorStatus = shouldCheckDiLabels && diLabelIssues.length > 0 ? "failed" : "passed";
2072
+ stdout.write(`Descriptor lint ${descriptorStatus}.\n`);
1774
2073
  stdout.write(`Packages: ${payload.packageCount}\n`);
1775
2074
  stdout.write(`Bundles: ${payload.bundleCount}\n`);
2075
+ if (shouldCheckDiLabels) {
2076
+ if (diLabelIssues.length === 0) {
2077
+ stdout.write("DI label parity check passed.\n");
2078
+ } else {
2079
+ stdout.write(`DI label parity check failed (${diLabelIssues.length} issue(s)).\n`);
2080
+ for (const issue of diLabelIssues) {
2081
+ const code = String(issue?.code || "").trim();
2082
+ const codeLabel = code ? `[${code}] ` : "";
2083
+ stdout.write(`- ${codeLabel}${String(issue?.message || "").trim()}\n`);
2084
+ }
2085
+ }
2086
+ }
2087
+ }
2088
+ if (shouldCheckDiLabels && diLabelIssues.length > 0) {
2089
+ return 1;
1776
2090
  }
1777
2091
  return 0;
1778
2092
  }
@@ -1782,6 +2096,7 @@ function createCommandHandlers(deps) {
1782
2096
  commandShow,
1783
2097
  commandCreate,
1784
2098
  commandAdd,
2099
+ commandGenerate,
1785
2100
  commandMigrations,
1786
2101
  commandPosition,
1787
2102
  commandUpdate,
@@ -8,66 +8,6 @@ import { createCliError } from "./cliError.js";
8
8
  const CLI_PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
9
9
  const require = createRequire(import.meta.url);
10
10
 
11
- function isWorkspaceRoot(candidateRoot) {
12
- if (!candidateRoot) {
13
- return false;
14
- }
15
- return (
16
- existsSync(path.join(candidateRoot, "packages")) &&
17
- existsSync(path.join(candidateRoot, "packages", "kernel")) &&
18
- existsSync(path.join(candidateRoot, "tooling", "jskit-cli"))
19
- );
20
- }
21
-
22
- function collectAncestorDirectories(startDirectory) {
23
- const ancestors = [];
24
- let current = path.resolve(startDirectory);
25
- while (true) {
26
- ancestors.push(current);
27
- const parent = path.dirname(current);
28
- if (parent === current) {
29
- break;
30
- }
31
- current = parent;
32
- }
33
- return ancestors;
34
- }
35
-
36
- function resolveWorkspaceRoot() {
37
- const candidates = [];
38
- const seen = new Set();
39
- const appendCandidate = (candidatePath) => {
40
- const raw = String(candidatePath || "").trim();
41
- if (!raw) {
42
- return;
43
- }
44
- const absolute = path.resolve(raw);
45
- if (seen.has(absolute)) {
46
- return;
47
- }
48
- seen.add(absolute);
49
- candidates.push(absolute);
50
- };
51
-
52
- appendCandidate(process.env.JSKIT_REPO_ROOT);
53
- appendCandidate(path.resolve(CLI_PACKAGE_ROOT, "../.."));
54
- appendCandidate(CLI_PACKAGE_ROOT);
55
-
56
- const cwdAncestors = collectAncestorDirectories(process.cwd());
57
- for (const ancestor of cwdAncestors) {
58
- appendCandidate(ancestor);
59
- appendCandidate(path.join(ancestor, "jskit-ai"));
60
- }
61
-
62
- for (const candidate of candidates) {
63
- if (isWorkspaceRoot(candidate)) {
64
- return candidate;
65
- }
66
- }
67
-
68
- return "";
69
- }
70
-
71
11
  function resolveCatalogPackagesPath() {
72
12
  const explicitPath = String(process.env.JSKIT_CATALOG_PACKAGES_PATH || "").trim();
73
13
  if (explicitPath) {
@@ -92,15 +32,11 @@ function resolveCatalogPackagesPath() {
92
32
  );
93
33
  }
94
34
 
95
- const WORKSPACE_ROOT = resolveWorkspaceRoot();
96
- const MODULES_ROOT = WORKSPACE_ROOT ? path.join(WORKSPACE_ROOT, "packages") : "";
97
35
  const BUNDLES_ROOT = path.join(CLI_PACKAGE_ROOT, "bundles");
98
36
  const CATALOG_PACKAGES_PATH = resolveCatalogPackagesPath();
99
37
 
100
38
  export {
101
39
  CLI_PACKAGE_ROOT,
102
- WORKSPACE_ROOT,
103
- MODULES_ROOT,
104
40
  BUNDLES_ROOT,
105
41
  CATALOG_PACKAGES_PATH
106
42
  };
@@ -64,6 +64,14 @@ function createRunCli({
64
64
  io: { stdin, stdout, stderr }
65
65
  });
66
66
  }
67
+ if (command === "generate") {
68
+ return await commandHandlers.commandGenerate({
69
+ positional,
70
+ options,
71
+ cwd,
72
+ io: { stdin, stdout, stderr }
73
+ });
74
+ }
67
75
  if (command === "position") {
68
76
  return await commandHandlers.commandPosition({
69
77
  positional,