@jskit-ai/jskit-cli 0.2.42 → 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.
@@ -7,6 +7,8 @@ import {
7
7
  } from "./discoverabilityHelp.js";
8
8
  import { interpolateOptionValue } from "../../shared/optionInterpolation.js";
9
9
 
10
+ const SHELL_WEB_PACKAGE_ID = "@jskit-ai/shell-web";
11
+
10
12
  function resolveGeneratorSubcommandDefinitionMetadata(packageEntry = {}, subcommandName = "") {
11
13
  const descriptor = packageEntry?.descriptor && typeof packageEntry.descriptor === "object"
12
14
  ? packageEntry.descriptor
@@ -27,6 +29,10 @@ function resolveGeneratorSubcommandDefinitionMetadata(packageEntry = {}, subcomm
27
29
  return definition && typeof definition === "object" ? definition : {};
28
30
  }
29
31
 
32
+ function resolveSubcommandRequiresShellWeb(packageEntry = {}, subcommandName = "") {
33
+ return resolveGeneratorSubcommandDefinitionMetadata(packageEntry, subcommandName)?.requiresShellWeb === true;
34
+ }
35
+
30
36
  function mapDescriptorBackedSubcommandArgsToInlineOptions(
31
37
  packageEntry = {},
32
38
  subcommandName = "",
@@ -199,6 +205,7 @@ async function runPackageGenerateCommand(
199
205
  resolvePackageKind,
200
206
  resolveGeneratorPrimarySubcommand,
201
207
  hasGeneratorSubcommandDefinition,
208
+ loadLockFile,
202
209
  readdir,
203
210
  validateInlineOptionValuesForPackage,
204
211
  runGeneratorSubcommand,
@@ -360,6 +367,15 @@ async function runPackageGenerateCommand(
360
367
  appRoot,
361
368
  optionNames: validatedOptionNames
362
369
  });
370
+ if (resolveSubcommandRequiresShellWeb(packageEntry, normalizedSubcommandName)) {
371
+ const { lock } = await loadLockFile(appRoot);
372
+ if (!lock?.installedPackages?.[SHELL_WEB_PACKAGE_ID]) {
373
+ const commandLabel = `${String(targetId || resolvedPackageId || "").trim() || resolvedPackageId} ${normalizedSubcommandName}`.trim();
374
+ throw createCliError(
375
+ `Generator command ${commandLabel} requires ${SHELL_WEB_PACKAGE_ID} to be installed in this app. Run: npx jskit add package shell-web`
376
+ );
377
+ }
378
+ }
363
379
 
364
380
  const primarySubcommand = resolveGeneratorPrimarySubcommand(packageEntry);
365
381
  if (
@@ -432,7 +448,18 @@ async function runPackageGenerateCommand(
432
448
  });
433
449
  }
434
450
 
435
- const { packageEntry } = await resolveGeneratorPackageEntry(targetId);
451
+ const resolvedGeneratorPackage = await resolveGeneratorPackageEntry(targetId);
452
+ const { packageEntry } = resolvedGeneratorPackage;
453
+ const primarySubcommand = resolveGeneratorPrimarySubcommand(packageEntry);
454
+ const hasInlineOptions = Object.keys(options?.inlineOptions || {}).length > 0;
455
+ if (primarySubcommand && hasInlineOptions) {
456
+ return runResolvedGeneratorSubcommand({
457
+ ...resolvedGeneratorPackage,
458
+ subcommandName: primarySubcommand,
459
+ subcommandArgs: []
460
+ });
461
+ }
462
+
436
463
  renderGeneratePackageHelp({
437
464
  io,
438
465
  packageEntry,
@@ -1,11 +1,20 @@
1
1
  import path from "node:path";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import {
4
+ LOCAL_LINK_ITEM_COMPONENT_DEFINITIONS,
5
+ findLocalLinkItemDefinition,
6
+ readLocalLinkItemComponentSource
7
+ } from "@jskit-ai/shell-web/server/support/localLinkItemScaffolds";
3
8
 
4
9
  const MAIN_CLIENT_PROVIDER_FILE = "packages/main/src/client/providers/MainClientProvider.js";
5
- const TAB_LINK_COMPONENT_FILE = "src/components/TabLinkItem.vue";
6
- const TAB_LINK_COMPONENT_NAME = "TabLinkItem";
10
+ const PLACEMENT_FILE = "src/placement.js";
11
+ const PLACEMENT_COMPONENT_TOKEN_PATTERN = /\bcomponentToken\s*:\s*["']([^"']+)["']/g;
7
12
  const TAB_LINK_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
8
13
 
14
+ const LOCAL_LINK_ITEM_COMPONENT_TOKENS = Object.freeze(
15
+ LOCAL_LINK_ITEM_COMPONENT_DEFINITIONS.map((entry) => entry.token)
16
+ );
17
+
9
18
  function toPosixPath(value = "") {
10
19
  return String(value || "").replaceAll("\\", "/");
11
20
  }
@@ -58,7 +67,7 @@ function insertBeforeClassDeclaration(source = "", line = "", { className = "",
58
67
  const classPattern = new RegExp(`^class\\s+${String(className || "").trim()}\\b`, "m");
59
68
  const classMatch = classPattern.exec(sourceText);
60
69
  if (!classMatch) {
61
- throw new Error(`crud-ui-generator could not find ${className} class declaration in ${contextFile || "target file"}.`);
70
+ throw new Error(`placement component provisioning could not find ${className} class declaration in ${contextFile || "target file"}.`);
62
71
  }
63
72
 
64
73
  return {
@@ -67,143 +76,42 @@ function insertBeforeClassDeclaration(source = "", line = "", { className = "",
67
76
  };
68
77
  }
69
78
 
70
- function renderTabLinkItemSource() {
71
- return `<script setup>
72
- import { computed } from "vue";
73
- import { useRoute } from "vue-router";
74
- import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
75
- import { useWorkspaceRouteContext } from "@jskit-ai/users-web/client/composables/useWorkspaceRouteContext";
76
-
77
- const props = defineProps({
78
- label: {
79
- type: String,
80
- default: ""
81
- },
82
- to: {
83
- type: String,
84
- default: ""
85
- },
86
- surface: {
87
- type: String,
88
- default: ""
89
- },
90
- workspaceSuffix: {
91
- type: String,
92
- default: "/"
93
- },
94
- nonWorkspaceSuffix: {
95
- type: String,
96
- default: "/"
97
- },
98
- disabled: {
99
- type: Boolean,
100
- default: false
101
- }
102
- });
103
-
104
- const route = useRoute();
105
- const paths = usePaths();
106
- const { currentSurfaceId, workspaceSlugFromRoute } = useWorkspaceRouteContext();
107
-
108
- function normalizePathname(pathname = "") {
109
- const source = String(pathname || "").trim();
110
- if (!source) {
111
- return "";
79
+ async function collectProvisionableLocalPlacementComponentTokensFromApp({
80
+ appRoot = ""
81
+ } = {}) {
82
+ const placementAbsolutePath = path.join(appRoot, PLACEMENT_FILE);
83
+ const placementSource = await readUtf8FileIfExists(placementAbsolutePath);
84
+ if (!placementSource) {
85
+ return [];
112
86
  }
113
87
 
114
- const queryIndex = source.indexOf("?");
115
- const hashIndex = source.indexOf("#");
116
- const cutoff =
117
- queryIndex < 0
118
- ? hashIndex
119
- : hashIndex < 0
120
- ? queryIndex
121
- : Math.min(queryIndex, hashIndex);
122
- return cutoff < 0 ? source : source.slice(0, cutoff);
123
- }
124
-
125
- function interpolateBracketParams(pathTemplate = "", params = {}) {
126
- const source = String(pathTemplate || "").trim();
127
- if (!source) {
128
- return "";
88
+ const collectedTokens = new Set();
89
+ for (const match of String(placementSource).matchAll(PLACEMENT_COMPONENT_TOKEN_PATTERN)) {
90
+ const componentToken = String(match[1] || "").trim();
91
+ if (!findLocalLinkItemDefinition(componentToken)) {
92
+ continue;
93
+ }
94
+ collectedTokens.add(componentToken);
129
95
  }
130
96
 
131
- return source.replace(/\\[([^\\]]+)\\]/g, (_match, rawKey) => {
132
- const key = String(rawKey || "").trim();
133
- if (!key) {
134
- return "";
135
- }
136
- const value = params?.[key];
137
- return value == null ? "[" + key + "]" : encodeURIComponent(String(value));
138
- });
97
+ return Array.from(collectedTokens).sort((left, right) => left.localeCompare(right));
139
98
  }
140
99
 
141
- const targetSurfaceId = computed(() => {
142
- const explicitSurface = String(props.surface || "").trim().toLowerCase();
143
- if (explicitSurface && explicitSurface !== "*") {
144
- return explicitSurface;
145
- }
146
- return String(currentSurfaceId.value || paths.currentSurfaceId.value || "").trim().toLowerCase();
147
- });
148
-
149
- const resolvedTo = computed(() => {
150
- const explicitTo = String(props.to || "").trim();
151
- if (explicitTo) {
152
- if (explicitTo.startsWith("./")) {
153
- const workspaceSlug = String(workspaceSlugFromRoute.value || "").trim();
154
- const suffixTemplate = workspaceSlug ? props.workspaceSuffix : props.nonWorkspaceSuffix;
155
- const interpolatedSuffix = interpolateBracketParams(suffixTemplate, route.params || {});
156
- if (interpolatedSuffix && !interpolatedSuffix.includes("[")) {
157
- return paths.page(interpolatedSuffix, {
158
- surface: targetSurfaceId.value,
159
- mode: "auto"
160
- });
161
- }
162
- }
163
- return explicitTo;
100
+ async function resolveProvisionableLocalPlacementComponentTokens({
101
+ appRoot = "",
102
+ componentTokens = []
103
+ } = {}) {
104
+ const collectedTokens = new Set(
105
+ (Array.isArray(componentTokens) ? componentTokens : [])
106
+ .map((value) => String(value || "").trim())
107
+ .filter((value) => Boolean(findLocalLinkItemDefinition(value)))
108
+ );
109
+
110
+ for (const componentToken of await collectProvisionableLocalPlacementComponentTokensFromApp({ appRoot })) {
111
+ collectedTokens.add(componentToken);
164
112
  }
165
113
 
166
- const workspaceSlug = String(workspaceSlugFromRoute.value || "").trim();
167
- const suffix = workspaceSlug ? props.workspaceSuffix : props.nonWorkspaceSuffix;
168
- const normalizedSuffix = String(suffix || "/").trim() || "/";
169
- return paths.page(normalizedSuffix, {
170
- surface: targetSurfaceId.value,
171
- mode: "auto"
172
- });
173
- });
174
-
175
- const isActive = computed(() => {
176
- const targetPath = normalizePathname(resolvedTo.value);
177
- const currentPath = normalizePathname(route.fullPath || route.path || "");
178
- if (!targetPath || !currentPath) {
179
- return false;
180
- }
181
- return currentPath === targetPath || currentPath.startsWith(\`\${targetPath}/\`);
182
- });
183
- </script>
184
-
185
- <template>
186
- <v-btn
187
- class="tab-link-item"
188
- variant="text"
189
- size="small"
190
- :to="resolvedTo"
191
- :active="isActive"
192
- :disabled="disabled"
193
- color="primary"
194
- >
195
- {{ label || "Tab" }}
196
- </v-btn>
197
- </template>
198
-
199
- <style scoped>
200
- .tab-link-item {
201
- text-transform: none;
202
- font-weight: 600;
203
- border-radius: 999px;
204
- }
205
- </style>
206
- `;
114
+ return Array.from(collectedTokens).sort((left, right) => left.localeCompare(right));
207
115
  }
208
116
 
209
117
  async function readUtf8FileIfExists(absolutePath = "") {
@@ -217,28 +125,35 @@ async function readUtf8FileIfExists(absolutePath = "") {
217
125
  }
218
126
  }
219
127
 
220
- async function ensureTabLinkItemComponentFile({ appRoot = "", dryRun = false, touchedFiles = new Set() } = {}) {
221
- const componentRelativePath = TAB_LINK_COMPONENT_FILE;
128
+ async function ensureProvisionedComponentFile(
129
+ definition,
130
+ {
131
+ appRoot = "",
132
+ dryRun = false,
133
+ touchedFiles = new Set()
134
+ } = {}
135
+ ) {
136
+ const componentRelativePath = definition.componentFile;
222
137
  const componentAbsolutePath = path.join(appRoot, componentRelativePath);
223
138
  const existingComponentSource = await readUtf8FileIfExists(componentAbsolutePath);
224
139
  if (existingComponentSource) {
225
140
  return;
226
141
  }
227
142
 
228
- if (dryRun !== true) {
229
- await mkdir(path.dirname(componentAbsolutePath), { recursive: true });
230
- await writeFile(componentAbsolutePath, renderTabLinkItemSource(), "utf8");
231
- }
143
+ if (dryRun !== true) {
144
+ await mkdir(path.dirname(componentAbsolutePath), { recursive: true });
145
+ await writeFile(componentAbsolutePath, await readLocalLinkItemComponentSource(definition), "utf8");
146
+ }
232
147
  touchedFiles.add(toPosixPath(componentRelativePath));
233
148
  }
234
149
 
235
- function hasTabLinkItemTokenRegistration(providerSource = "") {
236
- const tokenPattern = TAB_LINK_COMPONENT_TOKEN.replaceAll(".", "\\.");
150
+ function hasProvisionedTokenRegistration(providerSource = "", componentToken = "") {
151
+ const tokenPattern = String(componentToken || "").replaceAll(".", "\\.");
237
152
  const pattern = new RegExp(`registerMainClientComponent\\(\\s*"${tokenPattern}"\\s*,`, "m");
238
153
  return pattern.test(String(providerSource || ""));
239
154
  }
240
155
 
241
- async function loadMainClientProviderSource({ appRoot = "", createCliError } = {}) {
156
+ async function loadMainClientProviderSource({ appRoot = "", createCliError, componentToken = "" } = {}) {
242
157
  const providerAbsolutePath = path.join(appRoot, MAIN_CLIENT_PROVIDER_FILE);
243
158
  let providerSource = "";
244
159
  try {
@@ -246,7 +161,7 @@ async function loadMainClientProviderSource({ appRoot = "", createCliError } = {
246
161
  } catch (error) {
247
162
  if (error && error.code === "ENOENT") {
248
163
  throw createCliError(
249
- `crud-ui-generator placement component token "${TAB_LINK_COMPONENT_TOKEN}" requires ${MAIN_CLIENT_PROVIDER_FILE}.`
164
+ `placement component token "${componentToken}" requires ${MAIN_CLIENT_PROVIDER_FILE}.`
250
165
  );
251
166
  }
252
167
  throw error;
@@ -254,31 +169,35 @@ async function loadMainClientProviderSource({ appRoot = "", createCliError } = {
254
169
 
255
170
  if (!/\bregisterMainClientComponent\s*\(/.test(providerSource)) {
256
171
  throw createCliError(
257
- `crud-ui-generator placement component token "${TAB_LINK_COMPONENT_TOKEN}" could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
172
+ `placement component token "${componentToken}" could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
258
173
  );
259
174
  }
260
175
 
261
176
  return providerSource;
262
177
  }
263
178
 
264
- async function ensureTabLinkItemProviderRegistration({
265
- appRoot = "",
266
- createCliError,
267
- dryRun = false,
268
- touchedFiles = new Set()
269
- } = {}) {
179
+ async function ensureProvisionedProviderRegistration(
180
+ definition,
181
+ {
182
+ appRoot = "",
183
+ createCliError,
184
+ dryRun = false,
185
+ touchedFiles = new Set()
186
+ } = {}
187
+ ) {
270
188
  const providerRelativePath = MAIN_CLIENT_PROVIDER_FILE;
271
189
  const providerAbsolutePath = path.join(appRoot, providerRelativePath);
272
190
  const providerSource = await loadMainClientProviderSource({
273
191
  appRoot,
274
- createCliError
192
+ createCliError,
193
+ componentToken: definition.token
275
194
  });
276
- if (hasTabLinkItemTokenRegistration(providerSource)) {
195
+ if (hasProvisionedTokenRegistration(providerSource, definition.token)) {
277
196
  return false;
278
197
  }
279
198
 
280
- const importLine = `import ${TAB_LINK_COMPONENT_NAME} from "/${toPosixPath(TAB_LINK_COMPONENT_FILE)}";`;
281
- const registerLine = `registerMainClientComponent("${TAB_LINK_COMPONENT_TOKEN}", () => ${TAB_LINK_COMPONENT_NAME});`;
199
+ const importLine = `import ${definition.componentName} from "/${toPosixPath(definition.componentFile)}";`;
200
+ const registerLine = `registerMainClientComponent("${definition.token}", () => ${definition.componentName});`;
282
201
 
283
202
  const importApplied = insertImportIfMissing(providerSource, importLine);
284
203
  const registerApplied = insertBeforeClassDeclaration(importApplied.content, registerLine, {
@@ -297,30 +216,70 @@ async function ensureTabLinkItemProviderRegistration({
297
216
  return true;
298
217
  }
299
218
 
300
- async function ensureLocalMainTabLinkItemProvisioning({
219
+ async function ensureLocalMainPlacementComponentProvisioning({
301
220
  appRoot = "",
302
221
  createCliError,
303
222
  dryRun = false,
304
- touchedFiles = new Set()
223
+ touchedFiles = new Set(),
224
+ componentTokens = []
305
225
  } = {}) {
306
- const providerSource = await loadMainClientProviderSource({
307
- appRoot,
308
- createCliError
309
- });
310
- if (hasTabLinkItemTokenRegistration(providerSource)) {
311
- return;
226
+ const uniqueComponentTokens = Array.from(
227
+ new Set(
228
+ (Array.isArray(componentTokens) ? componentTokens : [])
229
+ .map((value) => String(value || "").trim())
230
+ .filter(Boolean)
231
+ )
232
+ );
233
+
234
+ for (const componentToken of uniqueComponentTokens) {
235
+ const definition = findLocalLinkItemDefinition(componentToken);
236
+ if (!definition) {
237
+ continue;
238
+ }
239
+
240
+ const providerSource = await loadMainClientProviderSource({
241
+ appRoot,
242
+ createCliError,
243
+ componentToken: definition.token
244
+ });
245
+ if (hasProvisionedTokenRegistration(providerSource, definition.token)) {
246
+ continue;
247
+ }
248
+
249
+ await ensureProvisionedComponentFile(definition, {
250
+ appRoot,
251
+ dryRun,
252
+ touchedFiles
253
+ });
254
+ await ensureProvisionedProviderRegistration(definition, {
255
+ appRoot,
256
+ createCliError,
257
+ dryRun,
258
+ touchedFiles
259
+ });
312
260
  }
261
+ }
313
262
 
314
- await ensureTabLinkItemComponentFile({ appRoot, dryRun, touchedFiles });
315
- await ensureTabLinkItemProviderRegistration({
263
+ async function ensureLocalMainTabLinkItemProvisioning({
264
+ appRoot = "",
265
+ createCliError,
266
+ dryRun = false,
267
+ touchedFiles = new Set()
268
+ } = {}) {
269
+ return ensureLocalMainPlacementComponentProvisioning({
316
270
  appRoot,
317
271
  createCliError,
318
272
  dryRun,
319
- touchedFiles
273
+ touchedFiles,
274
+ componentTokens: [TAB_LINK_COMPONENT_TOKEN]
320
275
  });
321
276
  }
322
277
 
323
278
  export {
279
+ LOCAL_LINK_ITEM_COMPONENT_TOKENS,
324
280
  TAB_LINK_COMPONENT_TOKEN,
281
+ collectProvisionableLocalPlacementComponentTokensFromApp,
282
+ resolveProvisionableLocalPlacementComponentTokens,
283
+ ensureLocalMainPlacementComponentProvisioning,
325
284
  ensureLocalMainTabLinkItemProvisioning
326
285
  };
@@ -1,10 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
- import { pathToFileURL } from "node:url";
2
+ import { importFreshModuleFromAbsolutePath } from "@jskit-ai/kernel/server/support";
3
3
  import {
4
4
  ensureArray,
5
5
  ensureObject,
6
6
  sortStrings
7
7
  } from "../shared/collectionUtils.js";
8
+ import {
9
+ ensureLocalMainPlacementComponentProvisioning,
10
+ resolveProvisionableLocalPlacementComponentTokens
11
+ } from "./packageCommands/tabLinkItemProvisioning.js";
8
12
 
9
13
  function createCommandHandlerShared(ctx = {}) {
10
14
  const {
@@ -242,7 +246,7 @@ function createCommandHandlerShared(ctx = {}) {
242
246
 
243
247
  let moduleNamespace = null;
244
248
  try {
245
- moduleNamespace = await import(`${pathToFileURL(entrypointPath).href}?t=${Date.now()}_${Math.random()}`);
249
+ moduleNamespace = await importFreshModuleFromAbsolutePath(entrypointPath);
246
250
  } catch (error) {
247
251
  throw createCliError(
248
252
  `Unable to load generator subcommand entrypoint ${normalizeRelativePath(appRoot, entrypointPath)}: ${String(error?.message || error || "unknown error")}`
@@ -268,9 +272,25 @@ function createCommandHandlerShared(ctx = {}) {
268
272
  dryRun: dryRun === true
269
273
  });
270
274
  const payload = ensureObject(result);
271
- const touchedFiles = sortStrings(
275
+ const touchedFileSet = new Set(
272
276
  ensureArray(payload.touchedFiles).map((value) => String(value || "").trim()).filter(Boolean)
273
277
  );
278
+ const placementComponentTokens = await resolveProvisionableLocalPlacementComponentTokens({
279
+ appRoot,
280
+ componentTokens: ensureArray(payload.placementComponentTokens)
281
+ .map((value) => String(value || "").trim())
282
+ .filter(Boolean)
283
+ });
284
+ if (placementComponentTokens.length > 0) {
285
+ await ensureLocalMainPlacementComponentProvisioning({
286
+ appRoot,
287
+ createCliError,
288
+ dryRun: dryRun === true,
289
+ touchedFiles: touchedFileSet,
290
+ componentTokens: placementComponentTokens
291
+ });
292
+ }
293
+ const touchedFiles = sortStrings([...touchedFileSet]);
274
294
  const summary = String(payload.summary || "").trim();
275
295
 
276
296
  if (json) {
@@ -99,13 +99,13 @@ function renderPackagePayloadText({
99
99
  }
100
100
 
101
101
  if (placementOutlets.length > 0) {
102
- stdout.write(`${color.heading(`Placement outlets (accepted host/position pairs) (${placementOutlets.length}):`)}\n`);
102
+ stdout.write(`${color.heading(`Placement outlets (${placementOutlets.length}):`)}\n`);
103
103
  for (const outlet of placementOutlets) {
104
104
  const surfaces = ensureArray(outlet.surfaces).map((value) => String(value || "").trim()).filter(Boolean);
105
105
  const surfacesLabel = surfaces.length > 0 ? ` ${color.installed(`[surfaces:${surfaces.join(", ")}]`)}` : "";
106
106
  const description = String(outlet.description || "").trim();
107
107
  const descriptionSuffix = description ? `: ${description}` : "";
108
- stdout.write(`- ${color.item(`${outlet.host}.${outlet.position}`)}${surfacesLabel}${descriptionSuffix}\n`);
108
+ stdout.write(`- ${color.item(outlet.target)}${surfacesLabel}${descriptionSuffix}\n`);
109
109
  if (options.details) {
110
110
  const sourceLabel = String(outlet.source || "").trim();
111
111
  if (sourceLabel) {
@@ -126,7 +126,7 @@ function renderPackagePayloadText({
126
126
  const description = String(contribution.description || "").trim();
127
127
  const descriptionSuffix = description ? `: ${description}` : "";
128
128
  stdout.write(
129
- `- ${color.item(contribution.id)} ${color.dim("->")} ${color.item(`${contribution.host}.${contribution.position}`)} ${color.installed(`[surfaces:${surfacesLabel}]`)}${orderSuffix}${componentSuffix}${descriptionSuffix}\n`
129
+ `- ${color.item(contribution.id)} ${color.dim("->")} ${color.item(contribution.target)} ${color.installed(`[surfaces:${surfacesLabel}]`)}${orderSuffix}${componentSuffix}${descriptionSuffix}\n`
130
130
  );
131
131
  if (options.details) {
132
132
  const when = String(contribution.when || "").trim();
@@ -56,6 +56,11 @@ function parseArgs(argv, { createCliError } = {}) {
56
56
  while (args.length > 0) {
57
57
  const token = String(args.shift() || "");
58
58
 
59
+ if (token === "--") {
60
+ positional.push(...args.map((value) => String(value || "")));
61
+ break;
62
+ }
63
+
59
64
  if (token === "--dry-run") {
60
65
  options.dryRun = true;
61
66
  continue;
@@ -104,6 +109,10 @@ function parseArgs(argv, { createCliError } = {}) {
104
109
  options.inlineOptions.force = "true";
105
110
  continue;
106
111
  }
112
+ if (token === "--install") {
113
+ options.inlineOptions.install = "true";
114
+ continue;
115
+ }
107
116
 
108
117
  if (token.startsWith("--")) {
109
118
  const withoutPrefix = token.slice(2);
@@ -30,9 +30,8 @@ function canDelegateAddInlineOptions(positional = []) {
30
30
  function canDelegateGenerateInlineOptions(positional = []) {
31
31
  const normalizedPositionals = Array.isArray(positional) ? positional : [];
32
32
  const first = String(normalizedPositionals[0] || "").trim();
33
- const second = String(normalizedPositionals[1] || "").trim();
34
33
  const last = String(normalizedPositionals[normalizedPositionals.length - 1] || "").trim();
35
- if (!first || !second || isHelpToken(first) || isHelpToken(second) || isHelpToken(last)) {
34
+ if (!first || isHelpToken(first) || isHelpToken(last)) {
36
35
  return false;
37
36
  }
38
37
  return true;
@@ -77,6 +76,31 @@ const COMMAND_DESCRIPTORS = Object.freeze({
77
76
  inlineOptionMode: "none",
78
77
  allowedValueOptionNames: Object.freeze([])
79
78
  }),
79
+ completion: Object.freeze({
80
+ command: "completion",
81
+ aliases: Object.freeze([]),
82
+ showInOverview: true,
83
+ summary: "Print shell completion script support.",
84
+ minimalUse: "jskit completion bash",
85
+ parameters: Object.freeze([
86
+ Object.freeze({
87
+ name: "<shell>",
88
+ description: "Shell name. Currently only bash is supported."
89
+ })
90
+ ]),
91
+ defaults: Object.freeze([
92
+ "Prints a shell completion script to stdout.",
93
+ "Use --install to write a small Bash loader file and wire ~/.bashrc automatically.",
94
+ "Use source <(npx jskit completion bash) to enable completion in the current shell.",
95
+ "The internal __complete__ mode is reserved for the generated shell function."
96
+ ]),
97
+ fullUse: "jskit completion bash [--install]",
98
+ showHelpOnBareInvocation: true,
99
+ handlerName: "commandCompletion",
100
+ allowedFlagKeys: Object.freeze([]),
101
+ inlineOptionMode: "enumerated",
102
+ allowedValueOptionNames: Object.freeze(["install"])
103
+ }),
80
104
  create: Object.freeze({
81
105
  command: "create",
82
106
  aliases: Object.freeze([]),
@@ -135,7 +159,7 @@ const COMMAND_DESCRIPTORS = Object.freeze({
135
159
  }),
136
160
  generate: Object.freeze({
137
161
  command: "generate",
138
- aliases: Object.freeze(["gen"]),
162
+ aliases: Object.freeze([]),
139
163
  showInOverview: true,
140
164
  summary: "Run a generator package (or generator subcommand).",
141
165
  minimalUse: "jskit generate <generatorId>",
@@ -188,7 +212,7 @@ const COMMAND_DESCRIPTORS = Object.freeze({
188
212
  }),
189
213
  list: Object.freeze({
190
214
  command: "list",
191
- aliases: Object.freeze(["ls"]),
215
+ aliases: Object.freeze([]),
192
216
  showInOverview: true,
193
217
  summary: "List bundles, runtime packages, or generator packages.",
194
218
  minimalUse: "jskit list",
@@ -212,7 +236,7 @@ const COMMAND_DESCRIPTORS = Object.freeze({
212
236
  }),
213
237
  "list-placements": Object.freeze({
214
238
  command: "list-placements",
215
- aliases: Object.freeze(["lp"]),
239
+ aliases: Object.freeze([]),
216
240
  showInOverview: true,
217
241
  summary: "List discovered UI placement targets.",
218
242
  minimalUse: "jskit list-placements",
@@ -229,12 +253,12 @@ const COMMAND_DESCRIPTORS = Object.freeze({
229
253
  inlineOptionMode: "none",
230
254
  allowedValueOptionNames: Object.freeze([])
231
255
  }),
232
- "list-link-items": Object.freeze({
233
- command: "list-link-items",
234
- aliases: Object.freeze(["lpct", "list-placement-component-tokens"]),
256
+ "list-component-tokens": Object.freeze({
257
+ command: "list-component-tokens",
258
+ aliases: Object.freeze([]),
235
259
  showInOverview: true,
236
- summary: "List available placement link-item component tokens.",
237
- minimalUse: "jskit list-link-items",
260
+ summary: "List available placement component tokens.",
261
+ minimalUse: "jskit list-component-tokens",
238
262
  parameters: Object.freeze([
239
263
  Object.freeze({
240
264
  name: "[--prefix <value>]",
@@ -252,7 +276,7 @@ const COMMAND_DESCRIPTORS = Object.freeze({
252
276
  "Use --all when you want the full discovered token set.",
253
277
  "Shows plain text by default; use --json for structured output."
254
278
  ]),
255
- fullUse: "jskit list-link-items [--prefix <value>] [--all] [--json]",
279
+ fullUse: "jskit list-component-tokens [--prefix <value>] [--all] [--json]",
256
280
  showHelpOnBareInvocation: false,
257
281
  handlerName: "commandListLinkItems",
258
282
  allowedFlagKeys: Object.freeze(["json", "all"]),
@@ -261,7 +285,7 @@ const COMMAND_DESCRIPTORS = Object.freeze({
261
285
  }),
262
286
  show: Object.freeze({
263
287
  command: "show",
264
- aliases: Object.freeze(["view"]),
288
+ aliases: Object.freeze([]),
265
289
  showInOverview: true,
266
290
  summary: "Show detailed metadata for a bundle or package.",
267
291
  minimalUse: "jskit show <id>",
@@ -272,7 +296,6 @@ const COMMAND_DESCRIPTORS = Object.freeze({
272
296
  })
273
297
  ]),
274
298
  defaults: Object.freeze([
275
- "view is an alias of show.",
276
299
  "Basic output is compact; --details expands capability and runtime sections.",
277
300
  "--debug-exports implies --details."
278
301
  ]),