@jskit-ai/jskit-cli 0.2.41 → 0.2.42

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.41",
3
+ "version": "0.2.42",
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.40",
24
- "@jskit-ai/kernel": "0.1.32"
23
+ "@jskit-ai/jskit-catalog": "0.1.41",
24
+ "@jskit-ai/kernel": "0.1.33"
25
25
  },
26
26
  "engines": {
27
27
  "node": "20.x"
@@ -118,25 +118,30 @@ async function applyTextMutations(packageEntry, appRoot, textMutations, options,
118
118
  const operation = String(mutation?.op || "").trim();
119
119
  if (operation === "upsert-env") {
120
120
  const relativeFile = String(mutation?.file || "").trim();
121
- const key = String(mutation?.key || "").trim();
122
- if (!relativeFile || !key) {
121
+ const rawKey = String(mutation?.key || "").trim();
122
+ if (!relativeFile || !rawKey) {
123
123
  throw createCliError(`Invalid upsert-env mutation in ${packageEntry.packageId}: "file" and "key" are required.`);
124
124
  }
125
125
 
126
+ const resolvedKey = interpolateOptionValue(rawKey, options, packageEntry.packageId, `${rawKey}.key`).trim();
127
+ if (!resolvedKey) {
128
+ throw createCliError(`Invalid upsert-env mutation in ${packageEntry.packageId}: resolved key is empty.`);
129
+ }
130
+
126
131
  const absoluteFile = path.join(appRoot, relativeFile);
127
132
  const previous = await readFileBufferIfExists(absoluteFile);
128
133
  const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
129
- const resolvedValue = interpolateOptionValue(mutation?.value || "", options, packageEntry.packageId, key);
130
- const upserted = upsertEnvValue(previousContent, key, resolvedValue);
134
+ const resolvedValue = interpolateOptionValue(mutation?.value || "", options, packageEntry.packageId, resolvedKey);
135
+ const upserted = upsertEnvValue(previousContent, resolvedKey, resolvedValue);
131
136
 
132
137
  await mkdir(path.dirname(absoluteFile), { recursive: true });
133
138
  await writeFile(absoluteFile, upserted.content, "utf8");
134
139
 
135
- const recordKey = `${relativeFile}::${String(mutation?.id || key)}`;
140
+ const recordKey = `${relativeFile}::${String(mutation?.id || resolvedKey)}`;
136
141
  managedText[recordKey] = {
137
142
  file: relativeFile,
138
143
  op: "upsert-env",
139
- key,
144
+ key: resolvedKey,
140
145
  value: resolvedValue,
141
146
  hadPrevious: upserted.hadPrevious,
142
147
  previousValue: upserted.previousValue,
@@ -91,6 +91,7 @@ function resolveSurfaceDefinitionsForOptionPolicy(configContext = {}) {
91
91
 
92
92
  normalizedDefinitions[definitionId] = Object.freeze({
93
93
  id: definitionId,
94
+ label: String(definition.label || "").trim(),
94
95
  enabled: definition.enabled !== false,
95
96
  requiresWorkspace: definition.requiresWorkspace === true
96
97
  });
@@ -251,6 +252,23 @@ async function validateResolvedOptionPolicies({
251
252
  });
252
253
  }
253
254
 
255
+ function resolvePromptChoicesForOption({ schema = {}, configContext = {} } = {}) {
256
+ const validationType = normalizeResolvedOptionValue(schema.validationType);
257
+ if (validationType !== OPTION_VALIDATION_ENABLED_SURFACE_ID) {
258
+ return [];
259
+ }
260
+
261
+ const surfaceDefinitions = resolveSurfaceDefinitionsForOptionPolicy(configContext);
262
+ return Object.values(surfaceDefinitions)
263
+ .filter((entry) => entry.enabled === true)
264
+ .map((entry) => Object.freeze({
265
+ value: entry.id,
266
+ label: entry.label && entry.label.toLowerCase() !== entry.id.toLowerCase()
267
+ ? `${entry.id} (${entry.label})`
268
+ : entry.id
269
+ }));
270
+ }
271
+
254
272
  async function validateOptionValuesForPackage({
255
273
  packageEntry,
256
274
  resolvedOptions = {},
@@ -379,11 +397,13 @@ async function resolvePackageOptions(packageEntry, inlineOptions, io, { appRoot
379
397
  }
380
398
 
381
399
  if (schema.required) {
400
+ const promptConfigContext = appRoot ? await loadConfigContext() : {};
382
401
  assignResolvedOption(await promptForRequiredOption({
383
402
  ownerType: "package",
384
403
  ownerId: packageEntry.packageId,
385
404
  optionName,
386
405
  optionSchema: schema,
406
+ promptChoices: resolvePromptChoicesForOption({ schema, configContext: promptConfigContext }),
387
407
  stdin: io.stdin,
388
408
  stdout: io.stdout
389
409
  }));
@@ -73,40 +73,11 @@ function mapDescriptorBackedSubcommandArgsToInlineOptions(
73
73
  }
74
74
 
75
75
  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
76
  const subcommandDefinition = resolveGeneratorSubcommandDefinitionMetadata(packageEntry, subcommandName);
83
77
  const positionalArgs = Array.isArray(subcommandDefinition?.positionalArgs)
84
78
  ? subcommandDefinition.positionalArgs
85
79
  : [];
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;
80
+ return positionalArgs.some((arg) => arg && typeof arg === "object" && arg.required !== false);
110
81
  }
111
82
 
112
83
  function collectUnexpectedGeneratorSubcommandOptionNames(packageEntry = {}, subcommandName = "", inlineOptions = {}) {
@@ -120,8 +120,9 @@ function parseArgs(argv, { createCliError } = {}) {
120
120
  if (hasInlineValue) {
121
121
  optionValueRaw = withoutPrefix.slice(withoutPrefix.indexOf("=") + 1);
122
122
  } else {
123
- const nextToken = typeof args[0] === "string" ? String(args[0]) : "";
124
- if (nextToken && !nextToken.startsWith("-")) {
123
+ const hasNextStringToken = typeof args[0] === "string";
124
+ const nextToken = hasNextStringToken ? String(args[0]) : "";
125
+ if (hasNextStringToken && !nextToken.startsWith("-")) {
125
126
  optionValueRaw = args.shift();
126
127
  }
127
128
  }
@@ -209,6 +209,80 @@ function normalizePathValue(value) {
209
209
  .join("/");
210
210
  }
211
211
 
212
+ function normalizePromptChoices(rawChoices = []) {
213
+ return ensureArray(rawChoices)
214
+ .map((entry) => {
215
+ if (!entry || typeof entry !== "object") {
216
+ return null;
217
+ }
218
+ const value = String(entry.value || "").trim();
219
+ const label = String(entry.label || value).trim();
220
+ if (!value || !label) {
221
+ return null;
222
+ }
223
+ return Object.freeze({ value, label });
224
+ })
225
+ .filter(Boolean);
226
+ }
227
+
228
+ function findPromptChoice(promptChoices = [], answer = "") {
229
+ const normalizedAnswer = String(answer || "").trim();
230
+ if (!normalizedAnswer) {
231
+ return null;
232
+ }
233
+
234
+ if (/^\d+$/.test(normalizedAnswer)) {
235
+ const index = Number.parseInt(normalizedAnswer, 10) - 1;
236
+ if (index >= 0 && index < promptChoices.length) {
237
+ return promptChoices[index];
238
+ }
239
+ }
240
+
241
+ return promptChoices.find((entry) => {
242
+ return entry.value === normalizedAnswer || entry.label === normalizedAnswer;
243
+ }) || null;
244
+ }
245
+
246
+ async function promptForChoice({
247
+ promptText = "",
248
+ promptChoices = [],
249
+ stdin,
250
+ stdout,
251
+ required = false,
252
+ defaultValue = ""
253
+ }) {
254
+ const rl = createInterface({
255
+ input: stdin,
256
+ output: stdout
257
+ });
258
+
259
+ try {
260
+ while (true) {
261
+ const answer = String(await rl.question(promptText)).trim();
262
+ if (!answer && defaultValue) {
263
+ return defaultValue;
264
+ }
265
+ if (!answer && !required) {
266
+ return "";
267
+ }
268
+ if (!answer && required) {
269
+ throw createCliError("A selection is required.");
270
+ }
271
+
272
+ const matchedChoice = findPromptChoice(promptChoices, answer);
273
+ if (matchedChoice) {
274
+ return matchedChoice.value;
275
+ }
276
+
277
+ const values = promptChoices.map((entry) => entry.value).join(", ");
278
+ stdout.write(`Invalid selection. Enter a number or one of: ${values}.
279
+ `);
280
+ }
281
+ } finally {
282
+ rl.close();
283
+ }
284
+ }
285
+
212
286
  function parseTransformSpec(transform) {
213
287
  const normalized = String(transform || "").trim();
214
288
  if (!normalized) {
@@ -359,6 +433,7 @@ async function promptForRequiredOption({
359
433
  ownerId,
360
434
  optionName,
361
435
  optionSchema,
436
+ promptChoices = [],
362
437
  stdin = process.stdin,
363
438
  stdout = process.stdout
364
439
  }) {
@@ -387,6 +462,24 @@ async function promptForRequiredOption({
387
462
  const defaultHint = defaultValue ? ` [default: ${defaultValue}]` : "";
388
463
  const hintSuffix = promptHint ? ` ${promptHint}` : "";
389
464
  const promptText = `${label}${defaultHint}${hintSuffix}: `;
465
+ const normalizedPromptChoices = normalizePromptChoices(promptChoices);
466
+
467
+ if (normalizedPromptChoices.length > 0) {
468
+ stdout.write(`${label}${defaultHint}${hintSuffix}
469
+ `);
470
+ normalizedPromptChoices.forEach((entry, index) => {
471
+ stdout.write(` ${index + 1}) ${entry.label}
472
+ `);
473
+ });
474
+ return promptForChoice({
475
+ promptText: "Select a surface by number or id: ",
476
+ promptChoices: normalizedPromptChoices,
477
+ stdin,
478
+ stdout,
479
+ required,
480
+ defaultValue
481
+ });
482
+ }
390
483
 
391
484
  let answer = "";
392
485