@jskit-ai/jskit-cli 0.2.30 → 0.2.32

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.30",
3
+ "version": "0.2.32",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,8 +20,8 @@
20
20
  "test": "node --test"
21
21
  },
22
22
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.30",
24
- "@jskit-ai/kernel": "0.1.22"
23
+ "@jskit-ai/jskit-catalog": "0.1.32",
24
+ "@jskit-ai/kernel": "0.1.24"
25
25
  },
26
26
  "engines": {
27
27
  "node": "20.x"
@@ -55,11 +55,9 @@ function normalizeMutationWhen(value) {
55
55
  const allConditions = ensureArray(source.all)
56
56
  .map((entry) => normalizeMutationWhen(entry))
57
57
  .filter(Boolean);
58
- if (allConditions.length > 0) {
59
- return {
60
- all: allConditions
61
- };
62
- }
58
+ const anyConditions = ensureArray(source.any)
59
+ .map((entry) => normalizeMutationWhen(entry))
60
+ .filter(Boolean);
63
61
 
64
62
  const option = String(source.option || "").trim();
65
63
  const config = String(source.config || "").trim();
@@ -70,11 +68,13 @@ function normalizeMutationWhen(value) {
70
68
  const includes = ensureArray(source.in).map((entry) => String(entry || "").trim()).filter(Boolean);
71
69
  const excludes = ensureArray(source.notIn).map((entry) => String(entry || "").trim()).filter(Boolean);
72
70
 
73
- if (!option && !config) {
71
+ if (!option && !config && allConditions.length < 1 && anyConditions.length < 1) {
74
72
  return null;
75
73
  }
76
74
 
77
75
  return {
76
+ all: allConditions,
77
+ any: anyConditions,
78
78
  option,
79
79
  config,
80
80
  equals,
@@ -217,7 +217,7 @@ function shouldApplyMutationWhen(
217
217
 
218
218
  const allConditions = ensureArray(when.all).filter((entry) => entry && typeof entry === "object");
219
219
  if (allConditions.length > 0) {
220
- return allConditions.every((entry) =>
220
+ const allMatch = allConditions.every((entry) =>
221
221
  shouldApplyMutationWhen(entry, {
222
222
  options,
223
223
  configContext,
@@ -225,6 +225,24 @@ function shouldApplyMutationWhen(
225
225
  mutationContext
226
226
  })
227
227
  );
228
+ if (!allMatch) {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ const anyConditions = ensureArray(when.any).filter((entry) => entry && typeof entry === "object");
234
+ if (anyConditions.length > 0) {
235
+ const anyMatch = anyConditions.some((entry) =>
236
+ shouldApplyMutationWhen(entry, {
237
+ options,
238
+ configContext,
239
+ packageId,
240
+ mutationContext
241
+ })
242
+ );
243
+ if (!anyMatch) {
244
+ return false;
245
+ }
228
246
  }
229
247
 
230
248
  const optionName = String(when.option || "").trim();
@@ -1,3 +1,7 @@
1
+ import {
2
+ readdir,
3
+ readFile
4
+ } from "node:fs/promises";
1
5
  import {
2
6
  ensureArray,
3
7
  ensureObject,
@@ -15,9 +19,42 @@ function createHealthCommands(ctx = {}) {
15
19
  hydratePackageRegistryFromInstalledNodeModules,
16
20
  inspectPackageOfferings,
17
21
  fileExists,
22
+ normalizeRelativePath,
18
23
  path
19
24
  } = ctx;
20
25
 
26
+ const MDI_SVG_MAIN_ENTRY_CANDIDATES = Object.freeze([
27
+ "src/main.js",
28
+ "src/main.mjs",
29
+ "src/main.ts"
30
+ ]);
31
+ const MDI_SVG_SCAN_ROOTS = Object.freeze([
32
+ "src",
33
+ "packages"
34
+ ]);
35
+ const MDI_SVG_IGNORED_DIRECTORY_NAMES = new Set([
36
+ ".git",
37
+ ".jskit",
38
+ ".build",
39
+ "coverage",
40
+ "dist",
41
+ "docs",
42
+ "LEGACY",
43
+ "node_modules",
44
+ "test",
45
+ "tests",
46
+ "__tests__"
47
+ ]);
48
+ const MDI_SVG_IGNORED_FILE_PATTERNS = Object.freeze([
49
+ /\.spec\./i,
50
+ /\.test\./i,
51
+ /\.vitest\./i
52
+ ]);
53
+ const DIRECT_MDI_LITERAL_ICON_PATTERN =
54
+ /<(v-[a-z0-9-]+)[^>]*?\b(icon|prepend-icon|append-icon)\s*=\s*(['"])(mdi-[^'"]+)\3/gi;
55
+ const DIRECT_MDI_BOUND_LITERAL_ICON_PATTERN =
56
+ /<(v-[a-z0-9-]+)[^>]*?(?::|v-bind:)(icon|prepend-icon|append-icon)\s*=\s*(['"])(['"])(mdi-[^'"]+)\4\3/gi;
57
+
21
58
  function collectDescriptorContainerTokens({ packageId, side, values, issues }) {
22
59
  const declaredTokens = new Set();
23
60
  const duplicateTokens = new Set();
@@ -115,6 +152,104 @@ function createHealthCommands(ctx = {}) {
115
152
  }
116
153
  }
117
154
 
155
+ async function appUsesVuetifyMdiSvg(appRoot) {
156
+ for (const relativePath of MDI_SVG_MAIN_ENTRY_CANDIDATES) {
157
+ const absolutePath = path.join(appRoot, relativePath);
158
+ if (!(await fileExists(absolutePath))) {
159
+ continue;
160
+ }
161
+ const fileContent = await readFile(absolutePath, "utf8");
162
+ if (fileContent.includes("vuetify/iconsets/mdi-svg")) {
163
+ return true;
164
+ }
165
+ }
166
+
167
+ return false;
168
+ }
169
+
170
+ function shouldSkipMdiSvgDoctorDirectory(directoryName = "") {
171
+ return MDI_SVG_IGNORED_DIRECTORY_NAMES.has(String(directoryName || "").trim());
172
+ }
173
+
174
+ function shouldSkipMdiSvgDoctorFile(fileName = "") {
175
+ const normalizedFileName = String(fileName || "").trim();
176
+ if (!normalizedFileName.endsWith(".vue")) {
177
+ return true;
178
+ }
179
+ return MDI_SVG_IGNORED_FILE_PATTERNS.some((pattern) => pattern.test(normalizedFileName));
180
+ }
181
+
182
+ async function collectVueSourceFiles(rootDirectory, collected = []) {
183
+ if (!(await fileExists(rootDirectory))) {
184
+ return collected;
185
+ }
186
+
187
+ const entries = await readdir(rootDirectory, { withFileTypes: true });
188
+ entries.sort((left, right) => left.name.localeCompare(right.name));
189
+
190
+ for (const entry of entries) {
191
+ const entryPath = path.join(rootDirectory, entry.name);
192
+ if (entry.isDirectory()) {
193
+ if (shouldSkipMdiSvgDoctorDirectory(entry.name)) {
194
+ continue;
195
+ }
196
+ await collectVueSourceFiles(entryPath, collected);
197
+ continue;
198
+ }
199
+ if (entry.isFile() && !shouldSkipMdiSvgDoctorFile(entry.name)) {
200
+ collected.push(entryPath);
201
+ }
202
+ }
203
+
204
+ return collected;
205
+ }
206
+
207
+ function resolveLineNumberFromIndex(sourceText = "", index = 0) {
208
+ return String(sourceText || "").slice(0, Math.max(0, index)).split("\n").length;
209
+ }
210
+
211
+ function collectDirectMdiSvgTemplateIconIssues({ sourceText, relativePath, issues }) {
212
+ DIRECT_MDI_LITERAL_ICON_PATTERN.lastIndex = 0;
213
+ for (const match of sourceText.matchAll(DIRECT_MDI_LITERAL_ICON_PATTERN)) {
214
+ const [, tagName = "v-component", propName = "icon", , rawIcon = ""] = match;
215
+ const lineNumber = resolveLineNumberFromIndex(sourceText, match.index || 0);
216
+ issues.push(
217
+ `${relativePath}:${lineNumber}: raw "${rawIcon}" passed to <${tagName}> ${propName} while the app uses vuetify/iconsets/mdi-svg. Use an @mdi/js path or a Vuetify alias.`
218
+ );
219
+ }
220
+
221
+ DIRECT_MDI_BOUND_LITERAL_ICON_PATTERN.lastIndex = 0;
222
+ for (const match of sourceText.matchAll(DIRECT_MDI_BOUND_LITERAL_ICON_PATTERN)) {
223
+ const [, tagName = "v-component", propName = "icon", , , rawIcon = ""] = match;
224
+ const lineNumber = resolveLineNumberFromIndex(sourceText, match.index || 0);
225
+ issues.push(
226
+ `${relativePath}:${lineNumber}: raw "${rawIcon}" passed to <${tagName}> ${propName} while the app uses vuetify/iconsets/mdi-svg. Use an @mdi/js path or a Vuetify alias.`
227
+ );
228
+ }
229
+ }
230
+
231
+ async function collectMdiSvgDoctorIssues({ appRoot, issues }) {
232
+ if (!(await appUsesVuetifyMdiSvg(appRoot))) {
233
+ return;
234
+ }
235
+
236
+ const vueFilePaths = [];
237
+ for (const relativeRoot of MDI_SVG_SCAN_ROOTS) {
238
+ await collectVueSourceFiles(path.join(appRoot, relativeRoot), vueFilePaths);
239
+ }
240
+
241
+ vueFilePaths.sort((left, right) => left.localeCompare(right));
242
+
243
+ for (const absolutePath of vueFilePaths) {
244
+ const sourceText = await readFile(absolutePath, "utf8");
245
+ collectDirectMdiSvgTemplateIconIssues({
246
+ sourceText,
247
+ relativePath: normalizeRelativePath(appRoot, absolutePath),
248
+ issues
249
+ });
250
+ }
251
+ }
252
+
118
253
  function collectDiLabelParityIssuesForPackage({ packageEntry, packageInsights }) {
119
254
  const packageId = String(packageEntry?.packageId || "").trim();
120
255
  const descriptor = ensureObject(packageEntry?.descriptor);
@@ -199,6 +334,11 @@ function createHealthCommands(ctx = {}) {
199
334
  }
200
335
  }
201
336
 
337
+ await collectMdiSvgDoctorIssues({
338
+ appRoot,
339
+ issues
340
+ });
341
+
202
342
  const payload = {
203
343
  appRoot,
204
344
  lockVersion: lock.lockVersion,
@@ -1,9 +1,134 @@
1
+ import path from "node:path";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { pathToFileURL } from "node:url";
1
4
  import {
2
5
  ensureArray,
3
6
  ensureObject,
4
7
  sortStrings
5
8
  } from "../shared/collectionUtils.js";
6
9
 
10
+ const PLACEMENT_FILE_RELATIVE_PATH = "src/placement.js";
11
+ const MAIN_CLIENT_PROVIDERS_RELATIVE_PATH = "packages/main/src/client/providers";
12
+ const COMPONENT_TOKEN_PATTERN = /\bcomponentToken\s*:\s*["']([^"']+)["']/g;
13
+ const REGISTER_MAIN_CLIENT_COMPONENT_PATTERN = /registerMainClientComponent\(\s*["']([^"']+)["']\s*,/g;
14
+ const LINK_ITEM_TOKEN_SUFFIX = "link-item";
15
+ const PROVIDER_SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx"]);
16
+ const READ_FILE_IGNORE_ERROR_CODES = new Set(["ENOENT", "ENOTDIR", "EISDIR", "EACCES", "EPERM"]);
17
+ const READ_DIRECTORY_IGNORE_ERROR_CODES = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"]);
18
+
19
+ function collectTokenMatches(source = "", pattern = COMPONENT_TOKEN_PATTERN) {
20
+ const sourceText = String(source || "");
21
+ const tokens = [];
22
+ for (const match of sourceText.matchAll(pattern)) {
23
+ const token = String(match[1] || "").trim();
24
+ if (token) {
25
+ tokens.push(token);
26
+ }
27
+ }
28
+ return tokens;
29
+ }
30
+
31
+ function appendTokenSource(map, token = "", source = "") {
32
+ const normalizedToken = String(token || "").trim();
33
+ if (!normalizedToken) {
34
+ return;
35
+ }
36
+ const normalizedSource = String(source || "").trim();
37
+ const existingSources = map.get(normalizedToken) || new Set();
38
+ if (normalizedSource) {
39
+ existingSources.add(normalizedSource);
40
+ }
41
+ map.set(normalizedToken, existingSources);
42
+ }
43
+
44
+ function isLinkItemToken(token = "") {
45
+ return String(token || "").trim().toLowerCase().endsWith(LINK_ITEM_TOKEN_SUFFIX);
46
+ }
47
+
48
+ async function readFileIfExists(filePath = "") {
49
+ try {
50
+ return await readFile(filePath, "utf8");
51
+ } catch (error) {
52
+ const errorCode = String(error?.code || "").trim().toUpperCase();
53
+ if (READ_FILE_IGNORE_ERROR_CODES.has(errorCode)) {
54
+ return "";
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ async function resolveDescriptorFromLockEntry({ appRoot = "", packageId = "", installedPackageEntry = {} } = {}) {
61
+ const source = ensureObject(installedPackageEntry.source);
62
+ const descriptorRelativePath = String(source.descriptorPath || "").trim();
63
+ if (!descriptorRelativePath) {
64
+ return null;
65
+ }
66
+
67
+ const descriptorAbsolutePath = path.resolve(appRoot, descriptorRelativePath);
68
+ const descriptorSource = await readFileIfExists(descriptorAbsolutePath);
69
+ if (!descriptorSource) {
70
+ return null;
71
+ }
72
+
73
+ let descriptorModule = null;
74
+ try {
75
+ descriptorModule = await import(`${pathToFileURL(descriptorAbsolutePath).href}?t=${Date.now()}_${Math.random()}`);
76
+ } catch {
77
+ return null;
78
+ }
79
+
80
+ const descriptor = ensureObject(descriptorModule?.default);
81
+ if (Object.keys(descriptor).length < 1) {
82
+ return null;
83
+ }
84
+
85
+ const resolvedPackageId = String(descriptor.packageId || packageId || "").trim();
86
+ if (!resolvedPackageId) {
87
+ return null;
88
+ }
89
+
90
+ return Object.freeze({
91
+ packageId: resolvedPackageId,
92
+ descriptor
93
+ });
94
+ }
95
+
96
+ async function collectProviderSourceFiles(rootPath = "") {
97
+ const files = [];
98
+ const stack = [path.resolve(String(rootPath || ""))];
99
+
100
+ while (stack.length > 0) {
101
+ const currentPath = stack.pop();
102
+ let entries = [];
103
+ try {
104
+ entries = await readdir(currentPath, { withFileTypes: true });
105
+ } catch (error) {
106
+ const errorCode = String(error?.code || "").trim().toUpperCase();
107
+ if (READ_DIRECTORY_IGNORE_ERROR_CODES.has(errorCode)) {
108
+ continue;
109
+ }
110
+ throw error;
111
+ }
112
+
113
+ for (const entry of entries) {
114
+ const entryPath = path.join(currentPath, entry.name);
115
+ if (entry.isDirectory()) {
116
+ stack.push(entryPath);
117
+ continue;
118
+ }
119
+ if (!entry.isFile()) {
120
+ continue;
121
+ }
122
+ const extension = path.extname(entry.name).toLowerCase();
123
+ if (PROVIDER_SOURCE_EXTENSIONS.has(extension)) {
124
+ files.push(entryPath);
125
+ }
126
+ }
127
+ }
128
+
129
+ return files.sort((left, right) => left.localeCompare(right));
130
+ }
131
+
7
132
  function createListCommands(ctx = {}) {
8
133
  const {
9
134
  createCliError,
@@ -14,7 +139,9 @@ function createListCommands(ctx = {}) {
14
139
  loadPackageRegistry,
15
140
  loadBundleRegistry,
16
141
  loadAppLocalPackageRegistry,
142
+ resolveInstalledNodeModulePackageEntry,
17
143
  discoverShellOutletTargetsFromApp,
144
+ normalizePlacementContributions,
18
145
  resolvePackageKind
19
146
  } = ctx;
20
147
 
@@ -51,6 +178,11 @@ function createListCommands(ctx = {}) {
51
178
  if (mode === "placements") {
52
179
  throw createCliError('list mode "placements" moved to a dedicated command: jskit list-placements.');
53
180
  }
181
+ if (mode === "placement-component-tokens") {
182
+ throw createCliError(
183
+ 'list mode "placement-component-tokens" moved to a dedicated command: jskit list-link-items.'
184
+ );
185
+ }
54
186
 
55
187
  if (!shouldListBundles && !shouldListPackages && !shouldListGenerators) {
56
188
  throw createCliError(`Unknown list mode: ${mode}`, { showUsage: true });
@@ -283,9 +415,144 @@ function createListCommands(ctx = {}) {
283
415
  return 0;
284
416
  }
285
417
 
418
+ async function commandListLinkItems({ options, cwd, stdout }) {
419
+ const appRoot = await resolveAppRootFromCwd(cwd);
420
+ const tokenPrefixFilter = String(options?.inlineOptions?.prefix || "").trim();
421
+ const includeAllClientContainerTokens = options?.all === true;
422
+ const onlyLinkItemTokens = !includeAllClientContainerTokens;
423
+ const { lock } = await loadLockFile(appRoot);
424
+ const packageRegistry = await loadPackageRegistry();
425
+ const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
426
+ const installedPackageEntries = ensureObject(lock.installedPackages);
427
+ const installedPackageIds = sortStrings(Object.keys(installedPackageEntries));
428
+
429
+ const packageEntryById = new Map();
430
+ for (const [packageId, packageEntry] of packageRegistry.entries()) {
431
+ packageEntryById.set(packageId, packageEntry);
432
+ }
433
+ for (const [packageId, packageEntry] of appLocalRegistry.entries()) {
434
+ packageEntryById.set(packageId, packageEntry);
435
+ }
436
+ for (const packageId of installedPackageIds) {
437
+ if (packageEntryById.has(packageId)) {
438
+ continue;
439
+ }
440
+ const installedPackageEntry = ensureObject(installedPackageEntries[packageId]);
441
+ const descriptorFromLockEntry = await resolveDescriptorFromLockEntry({
442
+ appRoot,
443
+ packageId,
444
+ installedPackageEntry
445
+ });
446
+ if (descriptorFromLockEntry) {
447
+ packageEntryById.set(packageId, descriptorFromLockEntry);
448
+ packageEntryById.set(descriptorFromLockEntry.packageId, descriptorFromLockEntry);
449
+ continue;
450
+ }
451
+ if (typeof resolveInstalledNodeModulePackageEntry !== "function") {
452
+ continue;
453
+ }
454
+ const resolvedNodeModuleEntry = await resolveInstalledNodeModulePackageEntry({
455
+ appRoot,
456
+ packageId
457
+ });
458
+ if (resolvedNodeModuleEntry) {
459
+ packageEntryById.set(resolvedNodeModuleEntry.packageId, resolvedNodeModuleEntry);
460
+ }
461
+ }
462
+
463
+ const tokenSourceByToken = new Map();
464
+ for (const packageId of installedPackageIds) {
465
+ const packageEntry = packageEntryById.get(packageId) || null;
466
+ if (!packageEntry) {
467
+ continue;
468
+ }
469
+ const descriptor = ensureObject(packageEntry.descriptor);
470
+ const metadata = ensureObject(descriptor.metadata);
471
+ const ui = ensureObject(metadata.ui);
472
+ const placements = ensureObject(ui.placements);
473
+ const contributions = normalizePlacementContributions(placements.contributions);
474
+ for (const contribution of contributions) {
475
+ const componentToken = String(contribution.componentToken || "").trim();
476
+ if (!componentToken) {
477
+ continue;
478
+ }
479
+ const contributionSource = String(contribution.source || "").trim();
480
+ const sourceLabel = contributionSource
481
+ ? `package:${packageId}:${contributionSource}`
482
+ : `package:${packageId}:metadata.ui.placements.contributions`;
483
+ appendTokenSource(tokenSourceByToken, componentToken, sourceLabel);
484
+ }
485
+
486
+ if (includeAllClientContainerTokens) {
487
+ const apiSummary = ensureObject(metadata.apiSummary);
488
+ const containerTokens = ensureObject(apiSummary.containerTokens);
489
+ const clientTokens = ensureArray(containerTokens.client).map((value) => String(value || "").trim()).filter(Boolean);
490
+ for (const clientToken of clientTokens) {
491
+ appendTokenSource(tokenSourceByToken, clientToken, `package:${packageId}:metadata.apiSummary.containerTokens.client`);
492
+ }
493
+ }
494
+ }
495
+
496
+ const placementSourcePath = path.join(appRoot, PLACEMENT_FILE_RELATIVE_PATH);
497
+ const placementSource = await readFileIfExists(placementSourcePath);
498
+ for (const token of collectTokenMatches(placementSource, COMPONENT_TOKEN_PATTERN)) {
499
+ appendTokenSource(tokenSourceByToken, token, `app:${normalizeRelativePosixPath(PLACEMENT_FILE_RELATIVE_PATH)}`);
500
+ }
501
+
502
+ const providersRootPath = path.join(appRoot, MAIN_CLIENT_PROVIDERS_RELATIVE_PATH);
503
+ const providerSourceFiles = await collectProviderSourceFiles(providersRootPath);
504
+ for (const providerSourceFile of providerSourceFiles) {
505
+ const providerSource = await readFileIfExists(providerSourceFile);
506
+ if (!providerSource) {
507
+ continue;
508
+ }
509
+ const providerRelativePath = normalizeRelativePosixPath(path.relative(appRoot, providerSourceFile));
510
+ for (const token of collectTokenMatches(providerSource, REGISTER_MAIN_CLIENT_COMPONENT_PATTERN)) {
511
+ appendTokenSource(tokenSourceByToken, token, `app:${providerRelativePath}`);
512
+ }
513
+ }
514
+
515
+ const tokens = sortStrings([...tokenSourceByToken.keys()])
516
+ .filter((token) => !tokenPrefixFilter || token.startsWith(tokenPrefixFilter))
517
+ .filter((token) => !onlyLinkItemTokens || isLinkItemToken(token))
518
+ .map((token) => ({
519
+ token,
520
+ sources: sortStrings([...(tokenSourceByToken.get(token) || new Set())])
521
+ }));
522
+
523
+ if (options.json) {
524
+ stdout.write(`${JSON.stringify({ placementComponentTokens: tokens }, null, 2)}\n`);
525
+ return 0;
526
+ }
527
+
528
+ const color = createColorFormatter(stdout);
529
+ const lines = [color.heading("Available placement component tokens:")];
530
+ lines.push(
531
+ color.dim(
532
+ includeAllClientContainerTokens
533
+ ? "Showing all discovered tokens (--all), including non-link-item/container/runtime tokens."
534
+ : 'Showing link-item tokens only (token must end with "link-item"). Tip: use --all for full token list.'
535
+ )
536
+ );
537
+ if (tokens.length < 1) {
538
+ lines.push("- none");
539
+ } else {
540
+ for (const entry of tokens) {
541
+ const token = String(entry.token || "").trim();
542
+ const sources = ensureArray(entry.sources).map((value) => String(value || "").trim()).filter(Boolean);
543
+ const sourceLabel = sources.length > 0 ? ` ${color.dim(`[${sources.join(", ")}]`)}` : "";
544
+ lines.push(`- ${color.item(token)}${sourceLabel}`);
545
+ }
546
+ }
547
+
548
+ stdout.write(`${lines.join("\n")}\n`);
549
+ return 0;
550
+ }
551
+
286
552
  return {
287
553
  commandList,
288
- commandListPlacements
554
+ commandListPlacements,
555
+ commandListLinkItems
289
556
  };
290
557
  }
291
558
 
@@ -3,6 +3,16 @@ import {
3
3
  ensureObject,
4
4
  sortStrings
5
5
  } from "../../shared/collectionUtils.js";
6
+ import {
7
+ isHelpToken,
8
+ renderAddCatalogHelp,
9
+ renderAddPackageHelp,
10
+ renderAddBundleHelp
11
+ } from "./discoverabilityHelp.js";
12
+ import {
13
+ TAB_LINK_COMPONENT_TOKEN,
14
+ ensureLocalMainTabLinkItemProvisioning
15
+ } from "./tabLinkItemProvisioning.js";
6
16
 
7
17
  async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io }) {
8
18
  const {
@@ -34,6 +44,94 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
34
44
  const invocationMode = options?.commandMode === "generate" ? "generate" : "add";
35
45
  const targetType = String(positional[0] || "").trim();
36
46
  const targetId = String(positional[1] || "").trim();
47
+ const thirdToken = String(positional[2] || "").trim();
48
+
49
+ if (invocationMode === "add" && !targetType) {
50
+ const packageRegistry = await loadPackageRegistry();
51
+ const bundleRegistry = await loadBundleRegistry();
52
+ renderAddCatalogHelp({
53
+ io,
54
+ packageRegistry,
55
+ bundleRegistry,
56
+ resolvePackageKind,
57
+ json: options.json
58
+ });
59
+ return 0;
60
+ }
61
+
62
+ const addShorthandHelpTargetId =
63
+ invocationMode === "add" &&
64
+ targetType &&
65
+ targetType !== "bundle" &&
66
+ targetType !== "package" &&
67
+ isHelpToken(targetId) &&
68
+ !thirdToken
69
+ ? targetType
70
+ : "";
71
+
72
+ const addPackageHelpTargetId =
73
+ invocationMode === "add" && targetType === "package" && targetId && isHelpToken(thirdToken)
74
+ ? targetId
75
+ : addShorthandHelpTargetId;
76
+ const addBundleHelpTargetId =
77
+ invocationMode === "add" && targetType === "bundle" && targetId && isHelpToken(thirdToken)
78
+ ? targetId
79
+ : "";
80
+
81
+ if (addPackageHelpTargetId) {
82
+ const appRoot = await resolveAppRootFromCwd(cwd);
83
+ const packageRegistry = await loadPackageRegistry();
84
+ const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
85
+ const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
86
+ const resolvedPackageId = await resolvePackageIdFromRegistryOrNodeModules({
87
+ appRoot,
88
+ packageRegistry: combinedPackageRegistry,
89
+ packageIdInput: addPackageHelpTargetId
90
+ });
91
+ if (!resolvedPackageId) {
92
+ throw createCliError(
93
+ `Unknown package: ${addPackageHelpTargetId}. Install an external module first (npm install ${addPackageHelpTargetId}) if you want to adopt it into lock.`
94
+ );
95
+ }
96
+
97
+ await hydratePackageRegistryFromInstalledNodeModules({
98
+ appRoot,
99
+ packageRegistry: combinedPackageRegistry,
100
+ seedPackageIds: [resolvedPackageId]
101
+ });
102
+ const packageEntry = combinedPackageRegistry.get(resolvedPackageId);
103
+ if (!packageEntry) {
104
+ throw createCliError(`Unknown package: ${addPackageHelpTargetId}`);
105
+ }
106
+ const packageKind = resolvePackageKind(packageEntry);
107
+ if (packageKind === "generator") {
108
+ throw createCliError(
109
+ `Package ${resolvedPackageId} is a generator. Use: jskit generate ${resolvedPackageId}`
110
+ );
111
+ }
112
+ renderAddPackageHelp({
113
+ io,
114
+ packageEntry,
115
+ packageIdInput: addPackageHelpTargetId,
116
+ json: options.json
117
+ });
118
+ return 0;
119
+ }
120
+
121
+ if (addBundleHelpTargetId) {
122
+ const bundleRegistry = await loadBundleRegistry();
123
+ const bundle = bundleRegistry.get(addBundleHelpTargetId);
124
+ if (!bundle) {
125
+ throw createCliError(`Unknown bundle: ${addBundleHelpTargetId}`);
126
+ }
127
+ renderAddBundleHelp({
128
+ io,
129
+ bundleId: addBundleHelpTargetId,
130
+ bundle,
131
+ json: options.json
132
+ });
133
+ return 0;
134
+ }
37
135
 
38
136
  if (!targetType || !targetId) {
39
137
  if (invocationMode === "generate") {
@@ -222,6 +320,21 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
222
320
 
223
321
  const finalResolvedPackageIds = sortStrings([...resolvedPackageIds, ...adoptedPackageIds]);
224
322
 
323
+ const requestedPlacementComponentToken = String(options?.inlineOptions?.["placement-component-token"] || "").trim();
324
+ if (
325
+ invocationMode === "generate" &&
326
+ targetType === "package" &&
327
+ resolvedTargetPackageId === "@jskit-ai/crud-ui-generator" &&
328
+ requestedPlacementComponentToken === TAB_LINK_COMPONENT_TOKEN
329
+ ) {
330
+ await ensureLocalMainTabLinkItemProvisioning({
331
+ appRoot,
332
+ createCliError,
333
+ dryRun: options.dryRun === true,
334
+ touchedFiles
335
+ });
336
+ }
337
+
225
338
  const touchedFileList = sortStrings([...touchedFiles]);
226
339
  const successLabel = invocationMode === "generate"
227
340
  ? "Generated with"