@jskit-ai/jskit-cli 0.2.41 → 0.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/package.json +4 -3
  2. package/src/server/cliRuntime/completion.js +1177 -0
  3. package/src/server/cliRuntime/descriptorValidation.js +18 -3
  4. package/src/server/cliRuntime/ioAndMigrations.js +2 -2
  5. package/src/server/cliRuntime/mutationApplication.js +1 -1
  6. package/src/server/cliRuntime/mutationWhen.js +2 -0
  7. package/src/server/cliRuntime/mutations/fileMutations.js +188 -143
  8. package/src/server/cliRuntime/mutations/installMigrationMutation.js +11 -38
  9. package/src/server/cliRuntime/mutations/templateContext.js +8 -14
  10. package/src/server/cliRuntime/mutations/textMutations.js +11 -6
  11. package/src/server/cliRuntime/packageInstallFlow.js +36 -21
  12. package/src/server/cliRuntime/packageIntrospection/placementNormalization.js +13 -22
  13. package/src/server/cliRuntime/packageOptions.js +149 -3
  14. package/src/server/cliRuntime/packageRegistries.js +3 -2
  15. package/src/server/commandHandlers/completion.js +129 -0
  16. package/src/server/commandHandlers/list.js +4 -6
  17. package/src/server/commandHandlers/packageCommands/add.js +31 -11
  18. package/src/server/commandHandlers/packageCommands/discoverabilityHelp.js +10 -2
  19. package/src/server/commandHandlers/packageCommands/generate.js +29 -31
  20. package/src/server/commandHandlers/packageCommands/tabLinkItemProvisioning.js +123 -164
  21. package/src/server/commandHandlers/shared.js +23 -3
  22. package/src/server/commandHandlers/show/renderPackageText.js +3 -3
  23. package/src/server/core/argParser.js +12 -2
  24. package/src/server/core/commandCatalog.js +36 -13
  25. package/src/server/core/createCommandHandlers.js +3 -0
  26. package/src/server/shared/optionInterpolation.js +93 -0
@@ -54,10 +54,15 @@ function buildPackageOptionRows(packageEntry = {}) {
54
54
 
55
55
  for (const optionName of sortStrings(Object.keys(optionSchemas))) {
56
56
  const schema = ensureObject(optionSchemas[optionName]);
57
+ const allowedValues = ensureArray(schema.allowedValues)
58
+ .map((value) => String(typeof value === "string" ? value : ensureObject(value).value || "").trim())
59
+ .filter(Boolean);
57
60
  rows.push(Object.freeze({
58
61
  name: optionName,
59
62
  required: schema.required === true,
60
63
  inputType: String(schema.inputType || "text").trim() || "text",
64
+ validationType: String(schema.validationType || "").trim(),
65
+ allowedValues,
61
66
  defaultValue: String(schema.defaultValue || "").trim(),
62
67
  allowEmpty: schema.allowEmpty === true,
63
68
  promptLabel: String(schema.promptLabel || "").trim(),
@@ -326,9 +331,12 @@ function formatOptionSummary(optionRow = {}, { color = null } = {}) {
326
331
  ? `; default: ${optionRow.defaultValue}`
327
332
  : optionalDefaultSuffix;
328
333
  const allowEmptySuffix = optionRow.allowEmpty ? "; allow-empty" : "";
334
+ const allowedValuesHint = Array.isArray(optionRow.allowedValues) && optionRow.allowedValues.length > 0
335
+ ? `Allowed values: ${optionRow.allowedValues.join(", ")}.`
336
+ : "";
329
337
  const labelParts = [
330
338
  String(optionRow.helpLabel || "").trim() || String(optionRow.promptLabel || "").trim(),
331
- String(optionRow.helpHint || "").trim() || String(optionRow.promptHint || "").trim()
339
+ String(optionRow.helpHint || "").trim() || String(optionRow.promptHint || "").trim() || allowedValuesHint
332
340
  ].filter(Boolean);
333
341
  const label = labelParts.join(". ");
334
342
  const normalizedInputType = String(optionRow.inputType || "").trim().toLowerCase();
@@ -337,7 +345,7 @@ function formatOptionSummary(optionRow = {}, { color = null } = {}) {
337
345
  : "";
338
346
  const baseDescription = label || "No description provided.";
339
347
  const description = optionRow.name === "placement-component-token"
340
- ? `${baseDescription} Use \`jskit list-link-items\` to discover link-item tokens.`
348
+ ? `${baseDescription} Use \`jskit list-component-tokens\` to discover link-item tokens.`
341
349
  : baseDescription;
342
350
  const optionName = `--${optionRow.name}`;
343
351
  const renderedOptionName = color ? color.item(optionName) : optionName;
@@ -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 = "",
@@ -73,40 +79,11 @@ function mapDescriptorBackedSubcommandArgsToInlineOptions(
73
79
  }
74
80
 
75
81
  function resolveSubcommandRequiresInput(packageEntry = {}, subcommandName = "") {
76
- const descriptor = packageEntry?.descriptor && typeof packageEntry.descriptor === "object"
77
- ? packageEntry.descriptor
78
- : {};
79
- const optionSchemas = descriptor?.options && typeof descriptor.options === "object"
80
- ? descriptor.options
81
- : {};
82
82
  const subcommandDefinition = resolveGeneratorSubcommandDefinitionMetadata(packageEntry, subcommandName);
83
83
  const positionalArgs = Array.isArray(subcommandDefinition?.positionalArgs)
84
84
  ? subcommandDefinition.positionalArgs
85
85
  : [];
86
- if (positionalArgs.length > 0) {
87
- return true;
88
- }
89
- const requiredOptionNames = Array.isArray(subcommandDefinition?.requiredOptionNames)
90
- ? subcommandDefinition.requiredOptionNames
91
- : [];
92
- if (requiredOptionNames.some((optionName) => String(optionName || "").trim().length > 0)) {
93
- return true;
94
- }
95
-
96
- const optionNames = Array.isArray(subcommandDefinition?.optionNames) && subcommandDefinition.optionNames.length > 0
97
- ? subcommandDefinition.optionNames
98
- : Object.keys(optionSchemas);
99
- for (const optionName of optionNames) {
100
- const normalizedOptionName = String(optionName || "").trim();
101
- if (!normalizedOptionName) {
102
- continue;
103
- }
104
- const schema = optionSchemas[normalizedOptionName];
105
- if (schema && typeof schema === "object" && schema.required === true) {
106
- return true;
107
- }
108
- }
109
- return false;
86
+ return positionalArgs.some((arg) => arg && typeof arg === "object" && arg.required !== false);
110
87
  }
111
88
 
112
89
  function collectUnexpectedGeneratorSubcommandOptionNames(packageEntry = {}, subcommandName = "", inlineOptions = {}) {
@@ -228,6 +205,7 @@ async function runPackageGenerateCommand(
228
205
  resolvePackageKind,
229
206
  resolveGeneratorPrimarySubcommand,
230
207
  hasGeneratorSubcommandDefinition,
208
+ loadLockFile,
231
209
  readdir,
232
210
  validateInlineOptionValuesForPackage,
233
211
  runGeneratorSubcommand,
@@ -389,6 +367,15 @@ async function runPackageGenerateCommand(
389
367
  appRoot,
390
368
  optionNames: validatedOptionNames
391
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
+ }
392
379
 
393
380
  const primarySubcommand = resolveGeneratorPrimarySubcommand(packageEntry);
394
381
  if (
@@ -461,7 +448,18 @@ async function runPackageGenerateCommand(
461
448
  });
462
449
  }
463
450
 
464
- 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
+
465
463
  renderGeneratePackageHelp({
466
464
  io,
467
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);
@@ -120,8 +129,9 @@ function parseArgs(argv, { createCliError } = {}) {
120
129
  if (hasInlineValue) {
121
130
  optionValueRaw = withoutPrefix.slice(withoutPrefix.indexOf("=") + 1);
122
131
  } else {
123
- const nextToken = typeof args[0] === "string" ? String(args[0]) : "";
124
- if (nextToken && !nextToken.startsWith("-")) {
132
+ const hasNextStringToken = typeof args[0] === "string";
133
+ const nextToken = hasNextStringToken ? String(args[0]) : "";
134
+ if (hasNextStringToken && !nextToken.startsWith("-")) {
125
135
  optionValueRaw = args.shift();
126
136
  }
127
137
  }