@jskit-ai/jskit-cli 0.2.42 → 0.2.44

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 (25) 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 +2 -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 +32 -0
  11. package/src/server/cliRuntime/packageInstallFlow.js +61 -22
  12. package/src/server/cliRuntime/packageIntrospection/placementNormalization.js +13 -22
  13. package/src/server/cliRuntime/packageOptions.js +129 -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 +28 -1
  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 +9 -0
  24. package/src/server/core/commandCatalog.js +36 -13
  25. package/src/server/core/createCommandHandlers.js +3 -0
@@ -32,7 +32,8 @@ import {
32
32
  import {
33
33
  applyFileMutations,
34
34
  applyTextMutations,
35
- preflightFileMutationTemplateContexts,
35
+ partitionPreFileConfigTextMutations,
36
+ prepareFileMutations,
36
37
  resolvePositioningMutations
37
38
  } from "./mutationApplication.js";
38
39
  function createManagedRecordBase(packageEntry, options) {
@@ -138,15 +139,23 @@ async function applyPackagePositioning({
138
139
  const positioningMutations = resolvePositioningMutations(mutations);
139
140
  const appliedManagedFiles = [];
140
141
  const appliedManagedText = {};
142
+ const preparedFileMutations = await prepareFileMutations(
143
+ packageEntryForMutations,
144
+ packageOptions,
145
+ appRoot,
146
+ positioningMutations.files,
147
+ nextManaged.files
148
+ );
141
149
  if (positioningMutations.files.length > 0) {
142
150
  await applyFileMutations(
143
151
  packageEntryForMutations,
144
- packageOptions,
145
152
  appRoot,
146
- positioningMutations.files,
153
+ preparedFileMutations,
147
154
  appliedManagedFiles,
148
155
  [],
149
- touchedFiles
156
+ touchedFiles,
157
+ [],
158
+ nextManaged.files
150
159
  );
151
160
  }
152
161
  if (positioningMutations.text.length > 0) {
@@ -236,17 +245,24 @@ async function applyPackageMigrationsOnly({
236
245
  return operation === "install-migration";
237
246
  });
238
247
  const mutationWarnings = [];
248
+ const preparedFileMutations = await prepareFileMutations(
249
+ packageEntryForMutations,
250
+ packageOptions,
251
+ appRoot,
252
+ migrationFileMutations,
253
+ nextManaged.files
254
+ );
239
255
 
240
256
  if (migrationFileMutations.length > 0) {
241
257
  await applyFileMutations(
242
258
  packageEntryForMutations,
243
- packageOptions,
244
259
  appRoot,
245
- migrationFileMutations,
260
+ preparedFileMutations,
246
261
  [],
247
262
  nextManaged.migrations,
248
263
  touchedFiles,
249
- mutationWarnings
264
+ mutationWarnings,
265
+ nextManaged.files
250
266
  );
251
267
  }
252
268
 
@@ -280,19 +296,23 @@ async function applyPackageInstall({
280
296
  }) {
281
297
  const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
282
298
  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
299
  const generatorPackage = isGeneratorPackageEntry(packageEntry);
293
300
  const mutationWarnings = [];
294
301
  const mutations = ensureObject(packageEntry.descriptor.mutations);
295
302
  const fileMutations = ensureArray(mutations.files);
303
+ const textMutations = ensureArray(mutations.text);
304
+ const hasSurfaceTargetedFileMutations = fileMutations.some((mutationValue) =>
305
+ Boolean(normalizeFileMutationRecord(mutationValue).toSurface)
306
+ );
307
+ const {
308
+ preFileTextMutations,
309
+ postFileTextMutations
310
+ } = hasSurfaceTargetedFileMutations
311
+ ? partitionPreFileConfigTextMutations(textMutations)
312
+ : {
313
+ preFileTextMutations: [],
314
+ postFileTextMutations: textMutations
315
+ };
296
316
  const templateRoot = await resolvePackageTemplateRoot({
297
317
  packageEntry,
298
318
  appRoot,
@@ -305,13 +325,33 @@ async function applyPackageInstall({
305
325
  ...packageEntry,
306
326
  rootDir: templateRoot
307
327
  };
328
+ const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
329
+ managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
308
330
 
309
- const precomputedTemplateContextByMutationIndex = await preflightFileMutationTemplateContexts(
331
+ if (preFileTextMutations.length > 0) {
332
+ await applyTextMutations(
333
+ packageEntryForMutations,
334
+ appRoot,
335
+ preFileTextMutations,
336
+ packageOptions,
337
+ managedRecord.managed.text,
338
+ touchedFiles
339
+ );
340
+ }
341
+
342
+ const preparedFileMutations = await prepareFileMutations(
310
343
  packageEntryForMutations,
311
344
  packageOptions,
312
345
  appRoot,
313
- fileMutations
346
+ fileMutations,
347
+ ensureArray(existingManaged.files)
314
348
  );
349
+ await removeManagedViteProxyEntries({
350
+ appRoot,
351
+ packageId: packageEntry.packageId,
352
+ managedViteChanges: ensureObject(existingManaged.vite),
353
+ touchedFiles
354
+ });
315
355
 
316
356
  const mutationDependencies = ensureObject(mutations.dependencies);
317
357
  const runtimeDependencies = ensureObject(mutationDependencies.runtime);
@@ -416,20 +456,19 @@ async function applyPackageInstall({
416
456
 
417
457
  await applyFileMutations(
418
458
  packageEntryForMutations,
419
- packageOptions,
420
459
  appRoot,
421
- fileMutations,
460
+ preparedFileMutations,
422
461
  managedRecord.managed.files,
423
462
  managedRecord.managed.migrations,
424
463
  touchedFiles,
425
464
  mutationWarnings,
426
- precomputedTemplateContextByMutationIndex
465
+ ensureArray(existingManaged.files)
427
466
  );
428
467
 
429
468
  await applyTextMutations(
430
469
  packageEntryForMutations,
431
470
  appRoot,
432
- ensureArray(mutations.text),
471
+ postFileTextMutations,
433
472
  packageOptions,
434
473
  managedRecord.managed.text,
435
474
  touchedFiles
@@ -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 = "") {
@@ -153,6 +155,114 @@ function resolveSchemaValidatedOptionNames(packageEntry = {}, validationType = "
153
155
  ];
154
156
  }
155
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
+
156
266
  function validateEnabledSurfaceOptionValues({
157
267
  packageEntry,
158
268
  resolvedOptions = {},
@@ -254,6 +364,12 @@ async function validateResolvedOptionPolicies({
254
364
 
255
365
  function resolvePromptChoicesForOption({ schema = {}, configContext = {} } = {}) {
256
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
+ }
257
373
  if (validationType !== OPTION_VALIDATION_ENABLED_SURFACE_ID) {
258
374
  return [];
259
375
  }
@@ -275,9 +391,16 @@ async function validateOptionValuesForPackage({
275
391
  appRoot = "",
276
392
  optionNames = null
277
393
  } = {}) {
278
- if (!appRoot) {
279
- return;
280
- }
394
+ validateEnumOptionValues({
395
+ packageEntry,
396
+ resolvedOptions,
397
+ optionNames
398
+ });
399
+ validateCsvEnumOptionValues({
400
+ packageEntry,
401
+ resolvedOptions,
402
+ optionNames
403
+ });
281
404
 
282
405
  const validatedOptionNames = resolveSchemaValidatedOptionNames(
283
406
  packageEntry,
@@ -287,6 +410,9 @@ async function validateOptionValuesForPackage({
287
410
  if (validatedOptionNames.length < 1) {
288
411
  return;
289
412
  }
413
+ if (!appRoot) {
414
+ return;
415
+ }
290
416
 
291
417
  const configContext = await loadMutationWhenConfigContext(appRoot);
292
418
  validateEnabledSurfaceOptionValues({
@@ -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