@jskit-ai/jskit-cli 0.2.41 → 0.2.43

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.
Files changed (26) hide show
  1. package/package.json +4 -3
  2. package/src/server/cliRuntime/completion.js +1177 -0
  3. package/src/server/cliRuntime/descriptorValidation.js +18 -3
  4. package/src/server/cliRuntime/ioAndMigrations.js +2 -2
  5. package/src/server/cliRuntime/mutationApplication.js +1 -1
  6. package/src/server/cliRuntime/mutationWhen.js +2 -0
  7. package/src/server/cliRuntime/mutations/fileMutations.js +188 -143
  8. package/src/server/cliRuntime/mutations/installMigrationMutation.js +11 -38
  9. package/src/server/cliRuntime/mutations/templateContext.js +8 -14
  10. package/src/server/cliRuntime/mutations/textMutations.js +11 -6
  11. package/src/server/cliRuntime/packageInstallFlow.js +36 -21
  12. package/src/server/cliRuntime/packageIntrospection/placementNormalization.js +13 -22
  13. package/src/server/cliRuntime/packageOptions.js +149 -3
  14. package/src/server/cliRuntime/packageRegistries.js +3 -2
  15. package/src/server/commandHandlers/completion.js +129 -0
  16. package/src/server/commandHandlers/list.js +4 -6
  17. package/src/server/commandHandlers/packageCommands/add.js +31 -11
  18. package/src/server/commandHandlers/packageCommands/discoverabilityHelp.js +10 -2
  19. package/src/server/commandHandlers/packageCommands/generate.js +29 -31
  20. package/src/server/commandHandlers/packageCommands/tabLinkItemProvisioning.js +123 -164
  21. package/src/server/commandHandlers/shared.js +23 -3
  22. package/src/server/commandHandlers/show/renderPackageText.js +3 -3
  23. package/src/server/core/argParser.js +12 -2
  24. package/src/server/core/commandCatalog.js +36 -13
  25. package/src/server/core/createCommandHandlers.js +3 -0
  26. package/src/server/shared/optionInterpolation.js +93 -0
@@ -32,7 +32,7 @@ import {
32
32
  import {
33
33
  applyFileMutations,
34
34
  applyTextMutations,
35
- preflightFileMutationTemplateContexts,
35
+ prepareFileMutations,
36
36
  resolvePositioningMutations
37
37
  } from "./mutationApplication.js";
38
38
  function createManagedRecordBase(packageEntry, options) {
@@ -138,15 +138,23 @@ async function applyPackagePositioning({
138
138
  const positioningMutations = resolvePositioningMutations(mutations);
139
139
  const appliedManagedFiles = [];
140
140
  const appliedManagedText = {};
141
+ const preparedFileMutations = await prepareFileMutations(
142
+ packageEntryForMutations,
143
+ packageOptions,
144
+ appRoot,
145
+ positioningMutations.files,
146
+ nextManaged.files
147
+ );
141
148
  if (positioningMutations.files.length > 0) {
142
149
  await applyFileMutations(
143
150
  packageEntryForMutations,
144
- packageOptions,
145
151
  appRoot,
146
- positioningMutations.files,
152
+ preparedFileMutations,
147
153
  appliedManagedFiles,
148
154
  [],
149
- touchedFiles
155
+ touchedFiles,
156
+ [],
157
+ nextManaged.files
150
158
  );
151
159
  }
152
160
  if (positioningMutations.text.length > 0) {
@@ -236,17 +244,24 @@ async function applyPackageMigrationsOnly({
236
244
  return operation === "install-migration";
237
245
  });
238
246
  const mutationWarnings = [];
247
+ const preparedFileMutations = await prepareFileMutations(
248
+ packageEntryForMutations,
249
+ packageOptions,
250
+ appRoot,
251
+ migrationFileMutations,
252
+ nextManaged.files
253
+ );
239
254
 
240
255
  if (migrationFileMutations.length > 0) {
241
256
  await applyFileMutations(
242
257
  packageEntryForMutations,
243
- packageOptions,
244
258
  appRoot,
245
- migrationFileMutations,
259
+ preparedFileMutations,
246
260
  [],
247
261
  nextManaged.migrations,
248
262
  touchedFiles,
249
- mutationWarnings
263
+ mutationWarnings,
264
+ nextManaged.files
250
265
  );
251
266
  }
252
267
 
@@ -280,15 +295,6 @@ async function applyPackageInstall({
280
295
  }) {
281
296
  const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
282
297
  const existingManaged = ensureObject(existingInstall.managed);
283
- await removeManagedViteProxyEntries({
284
- appRoot,
285
- packageId: packageEntry.packageId,
286
- managedViteChanges: ensureObject(existingManaged.vite),
287
- touchedFiles
288
- });
289
-
290
- const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
291
- managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
292
298
  const generatorPackage = isGeneratorPackageEntry(packageEntry);
293
299
  const mutationWarnings = [];
294
300
  const mutations = ensureObject(packageEntry.descriptor.mutations);
@@ -306,12 +312,22 @@ async function applyPackageInstall({
306
312
  rootDir: templateRoot
307
313
  };
308
314
 
309
- const precomputedTemplateContextByMutationIndex = await preflightFileMutationTemplateContexts(
315
+ const preparedFileMutations = await prepareFileMutations(
310
316
  packageEntryForMutations,
311
317
  packageOptions,
312
318
  appRoot,
313
- fileMutations
319
+ fileMutations,
320
+ ensureArray(existingManaged.files)
314
321
  );
322
+ await removeManagedViteProxyEntries({
323
+ appRoot,
324
+ packageId: packageEntry.packageId,
325
+ managedViteChanges: ensureObject(existingManaged.vite),
326
+ touchedFiles
327
+ });
328
+
329
+ const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
330
+ managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
315
331
 
316
332
  const mutationDependencies = ensureObject(mutations.dependencies);
317
333
  const runtimeDependencies = ensureObject(mutationDependencies.runtime);
@@ -416,14 +432,13 @@ async function applyPackageInstall({
416
432
 
417
433
  await applyFileMutations(
418
434
  packageEntryForMutations,
419
- packageOptions,
420
435
  appRoot,
421
- fileMutations,
436
+ preparedFileMutations,
422
437
  managedRecord.managed.files,
423
438
  managedRecord.managed.migrations,
424
439
  touchedFiles,
425
440
  mutationWarnings,
426
- precomputedTemplateContextByMutationIndex
441
+ ensureArray(existingManaged.files)
427
442
  );
428
443
 
429
444
  await applyTextMutations(
@@ -2,15 +2,17 @@ import {
2
2
  ensureArray,
3
3
  ensureObject
4
4
  } from "../../shared/collectionUtils.js";
5
+ import {
6
+ normalizeShellOutletTargetId
7
+ } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
5
8
 
6
9
  function normalizePlacementOutlets(value) {
7
10
  const outlets = [];
8
11
  const source = ensureArray(value);
9
12
  for (const entry of source) {
10
13
  const record = ensureObject(entry);
11
- const host = String(record.host || "").trim();
12
- const position = String(record.position || "").trim();
13
- if (!host || !position) {
14
+ const target = normalizeShellOutletTargetId(record.target);
15
+ if (!target) {
14
16
  continue;
15
17
  }
16
18
 
@@ -19,8 +21,7 @@ function normalizePlacementOutlets(value) {
19
21
  const sourceLabel = String(record.source || "").trim();
20
22
  outlets.push(
21
23
  Object.freeze({
22
- host,
23
- position,
24
+ target,
24
25
  surfaces: Object.freeze(surfaces),
25
26
  description,
26
27
  source: sourceLabel
@@ -30,11 +31,7 @@ function normalizePlacementOutlets(value) {
30
31
 
31
32
  return Object.freeze(
32
33
  [...outlets].sort((left, right) => {
33
- const hostCompare = left.host.localeCompare(right.host);
34
- if (hostCompare !== 0) {
35
- return hostCompare;
36
- }
37
- return left.position.localeCompare(right.position);
34
+ return left.target.localeCompare(right.target);
38
35
  })
39
36
  );
40
37
  }
@@ -44,9 +41,8 @@ function normalizePlacementContributions(value) {
44
41
  for (const entry of ensureArray(value)) {
45
42
  const record = ensureObject(entry);
46
43
  const id = String(record.id || "").trim();
47
- const host = String(record.host || "").trim();
48
- const position = String(record.position || "").trim();
49
- if (!id || !host || !position) {
44
+ const target = normalizeShellOutletTargetId(record.target);
45
+ if (!id || !target) {
50
46
  continue;
51
47
  }
52
48
 
@@ -60,8 +56,7 @@ function normalizePlacementContributions(value) {
60
56
  contributions.push(
61
57
  Object.freeze({
62
58
  id,
63
- host,
64
- position,
59
+ target,
65
60
  surfaces: Object.freeze(surfaces),
66
61
  order,
67
62
  componentToken,
@@ -74,13 +69,9 @@ function normalizePlacementContributions(value) {
74
69
 
75
70
  return Object.freeze(
76
71
  [...contributions].sort((left, right) => {
77
- const hostCompare = left.host.localeCompare(right.host);
78
- if (hostCompare !== 0) {
79
- return hostCompare;
80
- }
81
- const positionCompare = left.position.localeCompare(right.position);
82
- if (positionCompare !== 0) {
83
- return positionCompare;
72
+ const targetCompare = left.target.localeCompare(right.target);
73
+ if (targetCompare !== 0) {
74
+ return targetCompare;
84
75
  }
85
76
  const leftOrder = Number.isFinite(left.order) ? left.order : Number.POSITIVE_INFINITY;
86
77
  const rightOrder = Number.isFinite(right.order) ? right.order : Number.POSITIVE_INFINITY;
@@ -17,6 +17,8 @@ import { loadMutationWhenConfigContext } from "./ioAndMigrations.js";
17
17
  const WORKSPACE_VISIBILITY_LEVELS = Object.freeze(["workspace", "workspace_user"]);
18
18
  const WORKSPACE_VISIBILITY_SET = new Set(WORKSPACE_VISIBILITY_LEVELS);
19
19
  const OPTION_VALIDATION_ENABLED_SURFACE_ID = "enabled-surface-id";
20
+ const OPTION_VALIDATION_ENUM = "enum";
21
+ const OPTION_VALIDATION_CSV_ENUM = "csv-enum";
20
22
  const OPTION_NORMALIZATION_PAGES_RELATIVE_TARGET_ROOT = "pages-relative-target-root";
21
23
 
22
24
  function normalizeSurfaceIdForMutation(value = "") {
@@ -91,6 +93,7 @@ function resolveSurfaceDefinitionsForOptionPolicy(configContext = {}) {
91
93
 
92
94
  normalizedDefinitions[definitionId] = Object.freeze({
93
95
  id: definitionId,
96
+ label: String(definition.label || "").trim(),
94
97
  enabled: definition.enabled !== false,
95
98
  requiresWorkspace: definition.requiresWorkspace === true
96
99
  });
@@ -152,6 +155,114 @@ function resolveSchemaValidatedOptionNames(packageEntry = {}, validationType = "
152
155
  ];
153
156
  }
154
157
 
158
+ function resolveAllowedValuesForSchema(schema = {}) {
159
+ const values = [];
160
+ const seen = new Set();
161
+ for (const rawValue of Array.isArray(schema?.allowedValues) ? schema.allowedValues : []) {
162
+ const value = String(
163
+ typeof rawValue === "string"
164
+ ? rawValue
165
+ : ensureObject(rawValue).value
166
+ ).trim();
167
+ if (!value || seen.has(value.toLowerCase())) {
168
+ continue;
169
+ }
170
+ seen.add(value.toLowerCase());
171
+ values.push(value);
172
+ }
173
+ return Object.freeze(values);
174
+ }
175
+
176
+ function parseCsvEnumOptionValues(value = "") {
177
+ return String(value || "")
178
+ .split(",")
179
+ .map((entry) => String(entry || "").trim())
180
+ .filter(Boolean);
181
+ }
182
+
183
+ function validateEnumOptionValues({
184
+ packageEntry,
185
+ resolvedOptions = {},
186
+ optionNames = null
187
+ } = {}) {
188
+ const validatedOptionNames = resolveSchemaValidatedOptionNames(
189
+ packageEntry,
190
+ OPTION_VALIDATION_ENUM,
191
+ { optionNames }
192
+ );
193
+ if (validatedOptionNames.length < 1) {
194
+ return;
195
+ }
196
+
197
+ const packageId = String(packageEntry?.packageId || "").trim() || "unknown-package";
198
+ const optionSchemas = ensureObject(packageEntry?.descriptor?.options);
199
+ for (const optionName of validatedOptionNames) {
200
+ const schema = ensureObject(optionSchemas[optionName]);
201
+ const value = String(resolvedOptions?.[optionName] || "").trim();
202
+ if (!value) {
203
+ continue;
204
+ }
205
+
206
+ const allowedValues = resolveAllowedValuesForSchema(schema);
207
+ if (allowedValues.length < 1) {
208
+ continue;
209
+ }
210
+ const allowedValueSet = new Set(allowedValues.map((entry) => entry.toLowerCase()));
211
+ if (!allowedValueSet.has(value.toLowerCase())) {
212
+ throw createCliError(
213
+ `Invalid option for package ${packageId}: --${optionName} must be one of: ${allowedValues.join(", ")}.`
214
+ );
215
+ }
216
+ }
217
+ }
218
+
219
+ function validateCsvEnumOptionValues({
220
+ packageEntry,
221
+ resolvedOptions = {},
222
+ optionNames = null
223
+ } = {}) {
224
+ const validatedOptionNames = resolveSchemaValidatedOptionNames(
225
+ packageEntry,
226
+ OPTION_VALIDATION_CSV_ENUM,
227
+ { optionNames }
228
+ );
229
+ if (validatedOptionNames.length < 1) {
230
+ return;
231
+ }
232
+
233
+ const packageId = String(packageEntry?.packageId || "").trim() || "unknown-package";
234
+ const optionSchemas = ensureObject(packageEntry?.descriptor?.options);
235
+ for (const optionName of validatedOptionNames) {
236
+ const schema = ensureObject(optionSchemas[optionName]);
237
+ const value = String(resolvedOptions?.[optionName] || "").trim();
238
+ if (!value) {
239
+ continue;
240
+ }
241
+
242
+ const allowedValues = resolveAllowedValuesForSchema(schema);
243
+ if (allowedValues.length < 1) {
244
+ continue;
245
+ }
246
+ const allowedValueSet = new Set(allowedValues.map((entry) => entry.toLowerCase()));
247
+ const providedValues = parseCsvEnumOptionValues(value);
248
+ if (providedValues.length < 1) {
249
+ throw createCliError(
250
+ `Invalid option for package ${packageId}: --${optionName} must include at least one value from: ${allowedValues.join(", ")}.`
251
+ );
252
+ }
253
+ const invalidValues = [
254
+ ...new Set(
255
+ providedValues.filter((entry) => !allowedValueSet.has(entry.toLowerCase()))
256
+ )
257
+ ];
258
+ if (invalidValues.length > 0) {
259
+ throw createCliError(
260
+ `Invalid option for package ${packageId}: --${optionName} includes unsupported value(s): ${invalidValues.join(", ")}. Allowed values: ${allowedValues.join(", ")}.`
261
+ );
262
+ }
263
+ }
264
+ }
265
+
155
266
  function validateEnabledSurfaceOptionValues({
156
267
  packageEntry,
157
268
  resolvedOptions = {},
@@ -251,15 +362,45 @@ async function validateResolvedOptionPolicies({
251
362
  });
252
363
  }
253
364
 
365
+ function resolvePromptChoicesForOption({ schema = {}, configContext = {} } = {}) {
366
+ const validationType = normalizeResolvedOptionValue(schema.validationType);
367
+ if (validationType === OPTION_VALIDATION_ENUM) {
368
+ return resolveAllowedValuesForSchema(schema).map((value) => Object.freeze({
369
+ value,
370
+ label: value
371
+ }));
372
+ }
373
+ if (validationType !== OPTION_VALIDATION_ENABLED_SURFACE_ID) {
374
+ return [];
375
+ }
376
+
377
+ const surfaceDefinitions = resolveSurfaceDefinitionsForOptionPolicy(configContext);
378
+ return Object.values(surfaceDefinitions)
379
+ .filter((entry) => entry.enabled === true)
380
+ .map((entry) => Object.freeze({
381
+ value: entry.id,
382
+ label: entry.label && entry.label.toLowerCase() !== entry.id.toLowerCase()
383
+ ? `${entry.id} (${entry.label})`
384
+ : entry.id
385
+ }));
386
+ }
387
+
254
388
  async function validateOptionValuesForPackage({
255
389
  packageEntry,
256
390
  resolvedOptions = {},
257
391
  appRoot = "",
258
392
  optionNames = null
259
393
  } = {}) {
260
- if (!appRoot) {
261
- return;
262
- }
394
+ validateEnumOptionValues({
395
+ packageEntry,
396
+ resolvedOptions,
397
+ optionNames
398
+ });
399
+ validateCsvEnumOptionValues({
400
+ packageEntry,
401
+ resolvedOptions,
402
+ optionNames
403
+ });
263
404
 
264
405
  const validatedOptionNames = resolveSchemaValidatedOptionNames(
265
406
  packageEntry,
@@ -269,6 +410,9 @@ async function validateOptionValuesForPackage({
269
410
  if (validatedOptionNames.length < 1) {
270
411
  return;
271
412
  }
413
+ if (!appRoot) {
414
+ return;
415
+ }
272
416
 
273
417
  const configContext = await loadMutationWhenConfigContext(appRoot);
274
418
  validateEnabledSurfaceOptionValues({
@@ -379,11 +523,13 @@ async function resolvePackageOptions(packageEntry, inlineOptions, io, { appRoot
379
523
  }
380
524
 
381
525
  if (schema.required) {
526
+ const promptConfigContext = appRoot ? await loadConfigContext() : {};
382
527
  assignResolvedOption(await promptForRequiredOption({
383
528
  ownerType: "package",
384
529
  ownerId: packageEntry.packageId,
385
530
  optionName,
386
531
  optionSchema: schema,
532
+ promptChoices: resolvePromptChoicesForOption({ schema, configContext: promptConfigContext }),
387
533
  stdin: io.stdin,
388
534
  stdout: io.stdout
389
535
  }));
@@ -1,6 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
+ import { importFreshModuleFromAbsolutePath } from "@jskit-ai/kernel/server/support";
4
5
  import { createCliError } from "../shared/cliError.js";
5
6
  import {
6
7
  ensureArray,
@@ -92,7 +93,7 @@ async function loadAppLocalPackageRegistry(appRoot) {
92
93
  throw createCliError(`Invalid app-local package at ${normalizeRelativePath(appRoot, packageRoot)}: package.json missing name.`);
93
94
  }
94
95
 
95
- const descriptorModule = await import(pathToFileURL(descriptorPath).href + `?t=${Date.now()}_${Math.random()}`);
96
+ const descriptorModule = await importFreshModuleFromAbsolutePath(descriptorPath);
96
97
  const descriptor = validateAppLocalPackageDescriptorShape(descriptorModule?.default, descriptorPath, {
97
98
  expectedPackageId: packageId,
98
99
  fallbackVersion: String(packageJson?.version || "").trim()
@@ -207,7 +208,7 @@ async function loadInstalledNodeModulePackageEntry({ appRoot, packageId }) {
207
208
  return null;
208
209
  }
209
210
 
210
- const descriptorModule = await import(pathToFileURL(descriptorPath).href + `?t=${Date.now()}_${Math.random()}`);
211
+ const descriptorModule = await importFreshModuleFromAbsolutePath(descriptorPath);
211
212
  const descriptor = validateAppLocalPackageDescriptorShape(descriptorModule?.default, descriptorPath, {
212
213
  expectedPackageId: resolvedPackageId,
213
214
  fallbackVersion: String(packageJson?.version || "").trim()
@@ -0,0 +1,129 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import {
5
+ getCompletions,
6
+ renderBashCompletionScript
7
+ } from "../cliRuntime/completion.js";
8
+
9
+ const INSTALL_MARKER_BEGIN = "# >>> jskit completion >>>";
10
+ const INSTALL_MARKER_END = "# <<< jskit completion <<<";
11
+
12
+ function resolveInstallBlock(relativeCompletionPath = "") {
13
+ return [
14
+ INSTALL_MARKER_BEGIN,
15
+ `if [ -f "$HOME/${relativeCompletionPath}" ]; then`,
16
+ ` source "$HOME/${relativeCompletionPath}"`,
17
+ "fi",
18
+ INSTALL_MARKER_END
19
+ ].join("\n");
20
+ }
21
+
22
+ async function readTextFileIfExists(filePath = "") {
23
+ try {
24
+ return await readFile(filePath, "utf8");
25
+ } catch (error) {
26
+ if (String(error?.code || "").trim().toUpperCase() === "ENOENT") {
27
+ return "";
28
+ }
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ async function installBashCompletion() {
34
+ const homeDirectory = os.homedir();
35
+ const completionRelativePath = ".jskit/completion/bash/jskit.bash";
36
+ const completionAbsolutePath = path.join(homeDirectory, completionRelativePath);
37
+ const bashrcAbsolutePath = path.join(homeDirectory, ".bashrc");
38
+ const installBlock = resolveInstallBlock(completionRelativePath);
39
+
40
+ await mkdir(path.dirname(completionAbsolutePath), { recursive: true });
41
+ await writeFile(completionAbsolutePath, renderBashCompletionScript(), "utf8");
42
+
43
+ const existingBashrc = await readTextFileIfExists(bashrcAbsolutePath);
44
+ const markerPattern = new RegExp(
45
+ `${INSTALL_MARKER_BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${INSTALL_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
46
+ "u"
47
+ );
48
+ const normalizedExisting = String(existingBashrc || "");
49
+ const nextBashrc = markerPattern.test(normalizedExisting)
50
+ ? normalizedExisting.replace(markerPattern, installBlock)
51
+ : `${normalizedExisting.replace(/\s*$/u, "")}${normalizedExisting.trim() ? "\n\n" : ""}${installBlock}\n`;
52
+
53
+ if (nextBashrc !== normalizedExisting) {
54
+ await writeFile(bashrcAbsolutePath, nextBashrc, "utf8");
55
+ }
56
+
57
+ return {
58
+ completionAbsolutePath,
59
+ bashrcAbsolutePath
60
+ };
61
+ }
62
+
63
+ function createCompletionCommands(ctx = {}) {
64
+ const {
65
+ createCliError,
66
+ resolveAppRootFromCwd
67
+ } = ctx;
68
+
69
+ async function commandCompletion({ positional = [], options = {}, cwd = "", stdout }) {
70
+ const shell = String(positional[0] || "").trim().toLowerCase();
71
+ if (shell !== "bash") {
72
+ throw createCliError(`Unsupported completion shell: ${shell || "<empty>"}.`, { showUsage: true });
73
+ }
74
+
75
+ const installRequested = String(options?.inlineOptions?.install || "").trim().toLowerCase() === "true";
76
+ const mode = String(positional[1] || "").trim();
77
+ if (installRequested && mode) {
78
+ throw createCliError("jskit completion bash --install does not accept internal completion mode arguments.", {
79
+ showUsage: true
80
+ });
81
+ }
82
+
83
+ if (installRequested) {
84
+ const {
85
+ completionAbsolutePath,
86
+ bashrcAbsolutePath
87
+ } = await installBashCompletion();
88
+ stdout.write(`Installed Bash completion to ${completionAbsolutePath}.\n`);
89
+ stdout.write(`Updated ${bashrcAbsolutePath}.\n`);
90
+ stdout.write("Run: source ~/.bashrc\n");
91
+ return 0;
92
+ }
93
+
94
+ if (!mode) {
95
+ stdout.write(renderBashCompletionScript());
96
+ return 0;
97
+ }
98
+
99
+ if (mode !== "__complete__") {
100
+ throw createCliError(`Unknown completion mode: ${mode}.`, { showUsage: true });
101
+ }
102
+
103
+ const rawCword = Number.parseInt(String(positional[2] || "0"), 10);
104
+ const words = positional.slice(3).map((value) => String(value ?? ""));
105
+
106
+ let appRoot = String(cwd || process.cwd());
107
+ try {
108
+ appRoot = await resolveAppRootFromCwd(appRoot);
109
+ } catch {
110
+ // Fall back to cwd so top-level completion still works outside an app root.
111
+ }
112
+
113
+ const completions = await getCompletions({
114
+ appRoot,
115
+ words,
116
+ cword: Number.isInteger(rawCword) ? rawCword : 0
117
+ });
118
+ if (completions.length > 0) {
119
+ stdout.write(`${completions.join("\n")}\n`);
120
+ }
121
+ return 0;
122
+ }
123
+
124
+ return {
125
+ commandCompletion
126
+ };
127
+ }
128
+
129
+ export { createCompletionCommands };
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
- import { pathToFileURL } from "node:url";
3
+ import { importFreshModuleFromAbsolutePath } from "@jskit-ai/kernel/server/support";
4
4
  import {
5
5
  ensureArray,
6
6
  ensureObject,
@@ -72,7 +72,7 @@ async function resolveDescriptorFromLockEntry({ appRoot = "", packageId = "", in
72
72
 
73
73
  let descriptorModule = null;
74
74
  try {
75
- descriptorModule = await import(`${pathToFileURL(descriptorAbsolutePath).href}?t=${Date.now()}_${Math.random()}`);
75
+ descriptorModule = await importFreshModuleFromAbsolutePath(descriptorAbsolutePath);
76
76
  } catch {
77
77
  return null;
78
78
  }
@@ -180,7 +180,7 @@ function createListCommands(ctx = {}) {
180
180
  }
181
181
  if (mode === "placement-component-tokens") {
182
182
  throw createCliError(
183
- 'list mode "placement-component-tokens" moved to a dedicated command: jskit list-link-items.'
183
+ 'list mode "placement-component-tokens" moved to a dedicated command: jskit list-component-tokens.'
184
184
  );
185
185
  }
186
186
 
@@ -385,9 +385,7 @@ function createListCommands(ctx = {}) {
385
385
  if (options.json) {
386
386
  const payload = {
387
387
  placements: placementTargets.map((placementTarget) => ({
388
- id: String(placementTarget.id || "").trim(),
389
- host: String(placementTarget.host || "").trim(),
390
- position: String(placementTarget.position || "").trim(),
388
+ target: String(placementTarget.id || "").trim(),
391
389
  default: placementTarget.default === true,
392
390
  sourcePath: String(placementTarget.sourcePath || "").trim()
393
391
  }))
@@ -10,10 +10,31 @@ import {
10
10
  renderAddBundleHelp
11
11
  } from "./discoverabilityHelp.js";
12
12
  import {
13
- TAB_LINK_COMPONENT_TOKEN,
14
- ensureLocalMainTabLinkItemProvisioning
13
+ ensureLocalMainPlacementComponentProvisioning,
14
+ resolveProvisionableLocalPlacementComponentTokens
15
15
  } from "./tabLinkItemProvisioning.js";
16
16
 
17
+ const COMPONENT_TOKEN_PATTERN = /\bcomponentToken\s*:\s*["']([^"']+)["']/g;
18
+
19
+ function collectPlacementComponentTokensFromManagedRecords(installedPackageRecords = []) {
20
+ const collectedTokens = new Set();
21
+
22
+ for (const record of ensureArray(installedPackageRecords)) {
23
+ const managedTextMutations = ensureObject(ensureObject(ensureObject(record).managed).text);
24
+ for (const mutationRecord of Object.values(managedTextMutations)) {
25
+ const source = String(ensureObject(mutationRecord).value || "");
26
+ for (const match of source.matchAll(COMPONENT_TOKEN_PATTERN)) {
27
+ const componentToken = String(match[1] || "").trim();
28
+ if (componentToken) {
29
+ collectedTokens.add(componentToken);
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ return sortStrings([...collectedTokens]);
36
+ }
37
+
17
38
  async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io }) {
18
39
  const {
19
40
  createCliError,
@@ -325,18 +346,17 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
325
346
 
326
347
  const finalResolvedPackageIds = sortStrings([...resolvedPackageIds, ...adoptedPackageIds]);
327
348
 
328
- const requestedPlacementComponentToken = String(options?.inlineOptions?.["placement-component-token"] || "").trim();
329
- if (
330
- invocationMode === "generate" &&
331
- targetType === "package" &&
332
- resolvedTargetPackageId === "@jskit-ai/crud-ui-generator" &&
333
- requestedPlacementComponentToken === TAB_LINK_COMPONENT_TOKEN
334
- ) {
335
- await ensureLocalMainTabLinkItemProvisioning({
349
+ const generatedPlacementComponentTokens = await resolveProvisionableLocalPlacementComponentTokens({
350
+ appRoot,
351
+ componentTokens: collectPlacementComponentTokensFromManagedRecords(installedPackageRecords)
352
+ });
353
+ if (generatedPlacementComponentTokens.length > 0) {
354
+ await ensureLocalMainPlacementComponentProvisioning({
336
355
  appRoot,
337
356
  createCliError,
338
357
  dryRun: options.dryRun === true,
339
- touchedFiles
358
+ touchedFiles,
359
+ componentTokens: generatedPlacementComponentTokens
340
360
  });
341
361
  }
342
362