@jskit-ai/jskit-cli 0.2.30 → 0.2.31

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.
@@ -1,3 +1,67 @@
1
+ import {
2
+ isHelpToken,
3
+ renderGenerateCatalogHelp,
4
+ renderGeneratePackageHelp,
5
+ renderGenerateSubcommandHelp
6
+ } from "./discoverabilityHelp.js";
7
+
8
+ function resolveGeneratorSubcommandDefinitionMetadata(packageEntry = {}, subcommandName = "") {
9
+ const descriptor = packageEntry?.descriptor && typeof packageEntry.descriptor === "object"
10
+ ? packageEntry.descriptor
11
+ : {};
12
+ const metadata = descriptor?.metadata && typeof descriptor.metadata === "object"
13
+ ? descriptor.metadata
14
+ : {};
15
+ const subcommands = metadata?.generatorSubcommands && typeof metadata.generatorSubcommands === "object"
16
+ ? metadata.generatorSubcommands
17
+ : descriptor?.generatorSubcommands && typeof descriptor.generatorSubcommands === "object"
18
+ ? descriptor.generatorSubcommands
19
+ : {};
20
+ const normalizedSubcommandName = String(subcommandName || "").trim();
21
+ if (!normalizedSubcommandName) {
22
+ return {};
23
+ }
24
+ const definition = subcommands[normalizedSubcommandName];
25
+ return definition && typeof definition === "object" ? definition : {};
26
+ }
27
+
28
+ function resolveSubcommandRequiresInput(packageEntry = {}, subcommandName = "") {
29
+ const descriptor = packageEntry?.descriptor && typeof packageEntry.descriptor === "object"
30
+ ? packageEntry.descriptor
31
+ : {};
32
+ const optionSchemas = descriptor?.options && typeof descriptor.options === "object"
33
+ ? descriptor.options
34
+ : {};
35
+ const subcommandDefinition = resolveGeneratorSubcommandDefinitionMetadata(packageEntry, subcommandName);
36
+ const positionalArgs = Array.isArray(subcommandDefinition?.positionalArgs)
37
+ ? subcommandDefinition.positionalArgs
38
+ : [];
39
+ if (positionalArgs.length > 0) {
40
+ return true;
41
+ }
42
+ const requiredOptionNames = Array.isArray(subcommandDefinition?.requiredOptionNames)
43
+ ? subcommandDefinition.requiredOptionNames
44
+ : [];
45
+ if (requiredOptionNames.some((optionName) => String(optionName || "").trim().length > 0)) {
46
+ return true;
47
+ }
48
+
49
+ const optionNames = Array.isArray(subcommandDefinition?.optionNames) && subcommandDefinition.optionNames.length > 0
50
+ ? subcommandDefinition.optionNames
51
+ : Object.keys(optionSchemas);
52
+ for (const optionName of optionNames) {
53
+ const normalizedOptionName = String(optionName || "").trim();
54
+ if (!normalizedOptionName) {
55
+ continue;
56
+ }
57
+ const schema = optionSchemas[normalizedOptionName];
58
+ if (schema && typeof schema === "object" && schema.required === true) {
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+
1
65
  async function runPackageGenerateCommand(
2
66
  ctx = {},
3
67
  { positional, options, cwd, io },
@@ -28,13 +92,8 @@ async function runPackageGenerateCommand(
28
92
  const targetId = firstToken === "package" ? secondToken : firstToken;
29
93
  const subcommandName = firstToken === "package" ? thirdToken : secondToken;
30
94
  const subcommandArgs = firstToken === "package" ? positional.slice(3) : positional.slice(2);
31
- if (!targetId) {
32
- throw createCliError("generate requires a package id (generate <packageId>).", {
33
- showUsage: true
34
- });
35
- }
36
95
 
37
- if (subcommandName) {
96
+ async function resolveGeneratorPackageEntry(packageIdInput = "") {
38
97
  const appRoot = await resolveAppRootFromCwd(cwd);
39
98
  const packageRegistry = await loadPackageRegistry();
40
99
  const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
@@ -43,11 +102,11 @@ async function runPackageGenerateCommand(
43
102
  const resolvedPackageId = await resolvePackageIdFromRegistryOrNodeModules({
44
103
  appRoot,
45
104
  packageRegistry: combinedPackageRegistry,
46
- packageIdInput: targetId
105
+ packageIdInput
47
106
  });
48
107
  if (!resolvedPackageId) {
49
108
  throw createCliError(
50
- `Unknown package: ${targetId}. Install it first (npm install ${targetId}) if you want to run generator subcommands from node_modules.`
109
+ `Unknown package: ${packageIdInput}. Install it first (npm install ${packageIdInput}) if you want to run generator subcommands from node_modules.`
51
110
  );
52
111
  }
53
112
 
@@ -58,15 +117,97 @@ async function runPackageGenerateCommand(
58
117
  });
59
118
  const packageEntry = combinedPackageRegistry.get(resolvedPackageId);
60
119
  if (!packageEntry) {
61
- throw createCliError(`Unknown package: ${targetId}`);
120
+ throw createCliError(`Unknown package: ${packageIdInput}`);
62
121
  }
63
-
64
122
  if (resolvePackageKind(packageEntry) !== "generator") {
65
123
  throw createCliError(
66
124
  `Package ${resolvedPackageId} is a runtime package. Use: jskit add package ${resolvedPackageId}`
67
125
  );
68
126
  }
69
127
 
128
+ return Object.freeze({
129
+ appRoot,
130
+ packageEntry,
131
+ resolvedPackageId
132
+ });
133
+ }
134
+
135
+ if (!targetId) {
136
+ const packageRegistry = await loadPackageRegistry();
137
+ renderGenerateCatalogHelp({
138
+ io,
139
+ packageRegistry,
140
+ resolvePackageKind,
141
+ json: options.json
142
+ });
143
+ return 0;
144
+ }
145
+
146
+ if (isHelpToken(subcommandName)) {
147
+ const helpSubcommandName = String(subcommandArgs[0] || "").trim();
148
+ if (subcommandArgs.length > 1) {
149
+ throw createCliError("generate help accepts at most one subcommand name.");
150
+ }
151
+ const { packageEntry } = await resolveGeneratorPackageEntry(targetId);
152
+ if (helpSubcommandName) {
153
+ const rendered = renderGenerateSubcommandHelp({
154
+ io,
155
+ packageEntry,
156
+ packageIdInput: targetId,
157
+ subcommandName: helpSubcommandName,
158
+ json: options.json
159
+ });
160
+ if (!rendered) {
161
+ throw createCliError(
162
+ `Unknown generator subcommand "${helpSubcommandName}" for ${String(packageEntry?.packageId || targetId)}.`
163
+ );
164
+ }
165
+ return 0;
166
+ }
167
+
168
+ renderGeneratePackageHelp({
169
+ io,
170
+ packageEntry,
171
+ packageIdInput: targetId,
172
+ json: options.json
173
+ });
174
+ return 0;
175
+ }
176
+
177
+ if (subcommandName) {
178
+ const {
179
+ appRoot,
180
+ packageEntry,
181
+ resolvedPackageId
182
+ } = await resolveGeneratorPackageEntry(targetId);
183
+ const hasInlineOptions = Object.keys(options?.inlineOptions || {}).length > 0;
184
+ const hasSubcommandArgs = subcommandArgs.length > 0;
185
+ if (!hasInlineOptions && !hasSubcommandArgs && resolveSubcommandRequiresInput(packageEntry, subcommandName)) {
186
+ const rendered = renderGenerateSubcommandHelp({
187
+ io,
188
+ packageEntry,
189
+ packageIdInput: targetId,
190
+ subcommandName,
191
+ json: options.json
192
+ });
193
+ if (rendered) {
194
+ return 0;
195
+ }
196
+ }
197
+ if (subcommandArgs.length === 1 && isHelpToken(subcommandArgs[0])) {
198
+ const rendered = renderGenerateSubcommandHelp({
199
+ io,
200
+ packageEntry,
201
+ packageIdInput: targetId,
202
+ subcommandName,
203
+ json: options.json
204
+ });
205
+ if (!rendered) {
206
+ throw createCliError(`Unknown generator subcommand "${subcommandName}" for ${resolvedPackageId}.`);
207
+ }
208
+ return 0;
209
+ }
210
+
70
211
  const normalizedSubcommandName = String(subcommandName || "").trim().toLowerCase();
71
212
  const primarySubcommand = resolveGeneratorPrimarySubcommand(packageEntry);
72
213
  if (
@@ -0,0 +1,326 @@
1
+ import path from "node:path";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+
4
+ 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";
7
+ const TAB_LINK_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
8
+
9
+ function toPosixPath(value = "") {
10
+ return String(value || "").replaceAll("\\", "/");
11
+ }
12
+
13
+ function ensureTrailingNewline(value = "") {
14
+ const source = String(value || "");
15
+ return source.endsWith("\n") ? source : `${source}\n`;
16
+ }
17
+
18
+ function insertImportIfMissing(source = "", importLine = "") {
19
+ const normalizedImportLine = String(importLine || "").trim();
20
+ const sourceText = String(source || "");
21
+ if (!normalizedImportLine || sourceText.includes(normalizedImportLine)) {
22
+ return {
23
+ changed: false,
24
+ content: sourceText
25
+ };
26
+ }
27
+
28
+ const importPattern = /^import\s+[^;]+;\s*$/gm;
29
+ let match = null;
30
+ let insertionIndex = 0;
31
+ while ((match = importPattern.exec(sourceText)) !== null) {
32
+ insertionIndex = match.index + match[0].length;
33
+ }
34
+
35
+ if (insertionIndex > 0) {
36
+ return {
37
+ changed: true,
38
+ content: `${sourceText.slice(0, insertionIndex)}\n${normalizedImportLine}${sourceText.slice(insertionIndex)}`
39
+ };
40
+ }
41
+
42
+ return {
43
+ changed: true,
44
+ content: `${normalizedImportLine}\n${sourceText}`
45
+ };
46
+ }
47
+
48
+ function insertBeforeClassDeclaration(source = "", line = "", { className = "", contextFile = "" } = {}) {
49
+ const normalizedLine = String(line || "").trim();
50
+ const sourceText = String(source || "");
51
+ if (!normalizedLine || sourceText.includes(normalizedLine)) {
52
+ return {
53
+ changed: false,
54
+ content: sourceText
55
+ };
56
+ }
57
+
58
+ const classPattern = new RegExp(`^class\\s+${String(className || "").trim()}\\b`, "m");
59
+ const classMatch = classPattern.exec(sourceText);
60
+ if (!classMatch) {
61
+ throw new Error(`crud-ui-generator could not find ${className} class declaration in ${contextFile || "target file"}.`);
62
+ }
63
+
64
+ return {
65
+ changed: true,
66
+ content: `${sourceText.slice(0, classMatch.index)}${normalizedLine}\n\n${sourceText.slice(classMatch.index)}`
67
+ };
68
+ }
69
+
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 "";
112
+ }
113
+
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 "";
129
+ }
130
+
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
+ });
139
+ }
140
+
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;
164
+ }
165
+
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
+ `;
207
+ }
208
+
209
+ async function readUtf8FileIfExists(absolutePath = "") {
210
+ try {
211
+ return await readFile(absolutePath, "utf8");
212
+ } catch (error) {
213
+ if (error && error.code === "ENOENT") {
214
+ return "";
215
+ }
216
+ throw error;
217
+ }
218
+ }
219
+
220
+ async function ensureTabLinkItemComponentFile({ appRoot = "", dryRun = false, touchedFiles = new Set() } = {}) {
221
+ const componentRelativePath = TAB_LINK_COMPONENT_FILE;
222
+ const componentAbsolutePath = path.join(appRoot, componentRelativePath);
223
+ const existingComponentSource = await readUtf8FileIfExists(componentAbsolutePath);
224
+ if (existingComponentSource) {
225
+ return;
226
+ }
227
+
228
+ if (dryRun !== true) {
229
+ await mkdir(path.dirname(componentAbsolutePath), { recursive: true });
230
+ await writeFile(componentAbsolutePath, renderTabLinkItemSource(), "utf8");
231
+ }
232
+ touchedFiles.add(toPosixPath(componentRelativePath));
233
+ }
234
+
235
+ function hasTabLinkItemTokenRegistration(providerSource = "") {
236
+ const tokenPattern = TAB_LINK_COMPONENT_TOKEN.replaceAll(".", "\\.");
237
+ const pattern = new RegExp(`registerMainClientComponent\\(\\s*"${tokenPattern}"\\s*,`, "m");
238
+ return pattern.test(String(providerSource || ""));
239
+ }
240
+
241
+ async function loadMainClientProviderSource({ appRoot = "", createCliError } = {}) {
242
+ const providerAbsolutePath = path.join(appRoot, MAIN_CLIENT_PROVIDER_FILE);
243
+ let providerSource = "";
244
+ try {
245
+ providerSource = await readFile(providerAbsolutePath, "utf8");
246
+ } catch (error) {
247
+ if (error && error.code === "ENOENT") {
248
+ throw createCliError(
249
+ `crud-ui-generator placement component token "${TAB_LINK_COMPONENT_TOKEN}" requires ${MAIN_CLIENT_PROVIDER_FILE}.`
250
+ );
251
+ }
252
+ throw error;
253
+ }
254
+
255
+ if (!/\bregisterMainClientComponent\s*\(/.test(providerSource)) {
256
+ throw createCliError(
257
+ `crud-ui-generator placement component token "${TAB_LINK_COMPONENT_TOKEN}" could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
258
+ );
259
+ }
260
+
261
+ return providerSource;
262
+ }
263
+
264
+ async function ensureTabLinkItemProviderRegistration({
265
+ appRoot = "",
266
+ createCliError,
267
+ dryRun = false,
268
+ touchedFiles = new Set()
269
+ } = {}) {
270
+ const providerRelativePath = MAIN_CLIENT_PROVIDER_FILE;
271
+ const providerAbsolutePath = path.join(appRoot, providerRelativePath);
272
+ const providerSource = await loadMainClientProviderSource({
273
+ appRoot,
274
+ createCliError
275
+ });
276
+ if (hasTabLinkItemTokenRegistration(providerSource)) {
277
+ return false;
278
+ }
279
+
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});`;
282
+
283
+ const importApplied = insertImportIfMissing(providerSource, importLine);
284
+ const registerApplied = insertBeforeClassDeclaration(importApplied.content, registerLine, {
285
+ className: "MainClientProvider",
286
+ contextFile: MAIN_CLIENT_PROVIDER_FILE
287
+ });
288
+
289
+ if (!importApplied.changed && !registerApplied.changed) {
290
+ return false;
291
+ }
292
+
293
+ if (dryRun !== true) {
294
+ await writeFile(providerAbsolutePath, ensureTrailingNewline(registerApplied.content), "utf8");
295
+ }
296
+ touchedFiles.add(toPosixPath(providerRelativePath));
297
+ return true;
298
+ }
299
+
300
+ async function ensureLocalMainTabLinkItemProvisioning({
301
+ appRoot = "",
302
+ createCliError,
303
+ dryRun = false,
304
+ touchedFiles = new Set()
305
+ } = {}) {
306
+ const providerSource = await loadMainClientProviderSource({
307
+ appRoot,
308
+ createCliError
309
+ });
310
+ if (hasTabLinkItemTokenRegistration(providerSource)) {
311
+ return;
312
+ }
313
+
314
+ await ensureTabLinkItemComponentFile({ appRoot, dryRun, touchedFiles });
315
+ await ensureTabLinkItemProviderRegistration({
316
+ appRoot,
317
+ createCliError,
318
+ dryRun,
319
+ touchedFiles
320
+ });
321
+ }
322
+
323
+ export {
324
+ TAB_LINK_COMPONENT_TOKEN,
325
+ ensureLocalMainTabLinkItemProvisioning
326
+ };
@@ -4,6 +4,7 @@ const KNOWN_COMMAND_IDS = Object.freeze([
4
4
  "generate",
5
5
  "list",
6
6
  "list-placements",
7
+ "list-link-items",
7
8
  "show",
8
9
  "view",
9
10
  "migrations",
@@ -21,7 +22,9 @@ const COMMAND_ALIASES = Object.freeze({
21
22
  view: "show",
22
23
  ls: "list",
23
24
  gen: "generate",
24
- lp: "list-placements"
25
+ lp: "list-placements",
26
+ lpct: "list-link-items",
27
+ "list-placement-component-tokens": "list-link-items"
25
28
  });
26
29
 
27
30
  function resolveCommandAlias(rawCommand) {
@@ -11,7 +11,7 @@ function createCommandHandlers(deps = {}) {
11
11
  ...shared
12
12
  };
13
13
 
14
- const { commandList, commandListPlacements } = createListCommands(commandContext);
14
+ const { commandList, commandListPlacements, commandListLinkItems } = createListCommands(commandContext);
15
15
  const { commandShow } = createShowCommand(commandContext);
16
16
  const {
17
17
  commandCreate,
@@ -27,6 +27,7 @@ function createCommandHandlers(deps = {}) {
27
27
  return {
28
28
  commandList,
29
29
  commandListPlacements,
30
+ commandListLinkItems,
30
31
  commandShow,
31
32
  commandCreate,
32
33
  commandAdd,
@@ -58,6 +58,9 @@ function createRunCli({
58
58
  if (command === "list-placements") {
59
59
  return await commandHandlers.commandListPlacements({ options, cwd, stdout });
60
60
  }
61
+ if (command === "list-link-items") {
62
+ return await commandHandlers.commandListLinkItems({ options, cwd, stdout });
63
+ }
61
64
  if (command === "show") {
62
65
  return await commandHandlers.commandShow({ positional, options, stdout });
63
66
  }
@@ -21,6 +21,10 @@ const COMMAND_OVERVIEW = Object.freeze([
21
21
  command: "list-placements",
22
22
  summary: "List discovered UI placement targets."
23
23
  }),
24
+ Object.freeze({
25
+ command: "list-link-items",
26
+ summary: "List available placement link-item component tokens."
27
+ }),
24
28
  Object.freeze({
25
29
  command: "show",
26
30
  summary: "Show detailed metadata for a bundle or package."
@@ -84,6 +88,7 @@ const COMMAND_HELP = Object.freeze({
84
88
  defaults: Object.freeze([
85
89
  "No npm install runs unless --run-npm-install is passed.",
86
90
  "Short ids resolve to @jskit-ai/<id> when available.",
91
+ "Running without args lists bundles and runtime packages.",
87
92
  "Existing matching version is skipped unless options force reapply."
88
93
  ]),
89
94
  fullUse: "jskit add <package|bundle> <id> [--<option> <value>...] [--dry-run] [--run-npm-install] [--json] [--verbose]"
@@ -98,7 +103,7 @@ const COMMAND_HELP = Object.freeze({
98
103
  }),
99
104
  Object.freeze({
100
105
  name: "[subcommand]",
101
- description: "Optional generator subcommand (for example: add-field)."
106
+ description: "Optional generator subcommand (for example: scaffold or scaffold-field)."
102
107
  }),
103
108
  Object.freeze({
104
109
  name: "[subcommand args...]",
@@ -108,7 +113,9 @@ const COMMAND_HELP = Object.freeze({
108
113
  defaults: Object.freeze([
109
114
  "No npm install runs unless --run-npm-install is passed.",
110
115
  "Short ids resolve to @jskit-ai/<id> when available.",
111
- "If no subcommand is provided, the generator primary command runs."
116
+ "Running without args lists available generators.",
117
+ "If no subcommand is provided, the generator primary command runs.",
118
+ "Use jskit generate <generatorId> <subcommand> help for subcommand-specific usage."
112
119
  ]),
113
120
  fullUse: "jskit generate <generatorId> [subcommand] [subcommand args...] [--<option> <value>...] [--dry-run] [--run-npm-install] [--json] [--verbose]"
114
121
  }),
@@ -133,12 +140,34 @@ const COMMAND_HELP = Object.freeze({
133
140
  minimalUse: "jskit list-placements",
134
141
  parameters: Object.freeze([]),
135
142
  defaults: Object.freeze([
136
- "Discovers ShellOutlet targets from app src/ Vue files.",
143
+ "Discovers placement outlets from app Vue ShellOutlet tags and route meta.",
137
144
  "Includes placement outlets contributed by installed package metadata.",
138
145
  "Shows plain text by default; use --json for structured output."
139
146
  ]),
140
147
  fullUse: "jskit list-placements [--json]"
141
148
  }),
149
+ "list-link-items": Object.freeze({
150
+ title: "list-link-items",
151
+ minimalUse: "jskit list-link-items",
152
+ parameters: Object.freeze([
153
+ Object.freeze({
154
+ name: "[--prefix <value>]",
155
+ description: "Optional token prefix filter (example: local.main. or users.web.shell.)."
156
+ }),
157
+ Object.freeze({
158
+ name: "[--all]",
159
+ description: "Include all discovered tokens (including non-link-item and client container/runtime tokens)."
160
+ })
161
+ ]),
162
+ defaults: Object.freeze([
163
+ "Default output shows link-item tokens only (token names ending with link-item).",
164
+ "Default includes app and installed-package placement-linked token sources.",
165
+ "Use --prefix to narrow quickly (recommended: --prefix local.main.).",
166
+ "Use --all when you want the full discovered token set.",
167
+ "Shows plain text by default; use --json for structured output."
168
+ ]),
169
+ fullUse: "jskit list-link-items [--prefix <value>] [--all] [--json]"
170
+ }),
142
171
  show: Object.freeze({
143
172
  title: "show",
144
173
  minimalUse: "jskit show <id>",
@@ -261,8 +290,6 @@ const COMMAND_HELP = Object.freeze({
261
290
 
262
291
  const BARE_COMMAND_HELP = new Set([
263
292
  "create",
264
- "add",
265
- "generate",
266
293
  "show",
267
294
  "migrations",
268
295
  "position",
@@ -197,6 +197,12 @@ function normalizePathValue(value) {
197
197
  return normalizedSegment;
198
198
  }
199
199
 
200
+ const routeGroupMatch = /^\(([^()]+)\)$/.exec(normalizedSegment);
201
+ if (routeGroupMatch) {
202
+ const routeGroupName = wordsToKebab(splitTextIntoWords(routeGroupMatch[1]));
203
+ return routeGroupName ? `(${routeGroupName})` : "";
204
+ }
205
+
200
206
  return wordsToKebab(splitTextIntoWords(normalizedSegment));
201
207
  })
202
208
  .filter(Boolean)