@jskit-ai/jskit-cli 0.2.39 → 0.2.40

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,9 +1,11 @@
1
+ import path from "node:path";
1
2
  import {
2
3
  isHelpToken,
3
4
  renderGenerateCatalogHelp,
4
5
  renderGeneratePackageHelp,
5
6
  renderGenerateSubcommandHelp
6
7
  } from "./discoverabilityHelp.js";
8
+ import { interpolateOptionValue } from "../../shared/optionInterpolation.js";
7
9
 
8
10
  function resolveGeneratorSubcommandDefinitionMetadata(packageEntry = {}, subcommandName = "") {
9
11
  const descriptor = packageEntry?.descriptor && typeof packageEntry.descriptor === "object"
@@ -25,6 +27,51 @@ function resolveGeneratorSubcommandDefinitionMetadata(packageEntry = {}, subcomm
25
27
  return definition && typeof definition === "object" ? definition : {};
26
28
  }
27
29
 
30
+ function mapDescriptorBackedSubcommandArgsToInlineOptions(
31
+ packageEntry = {},
32
+ subcommandName = "",
33
+ subcommandArgs = [],
34
+ inlineOptions = {},
35
+ createCliError
36
+ ) {
37
+ const definition = resolveGeneratorSubcommandDefinitionMetadata(packageEntry, subcommandName);
38
+ const positionalArgs = Array.isArray(definition?.positionalArgs)
39
+ ? definition.positionalArgs
40
+ : [];
41
+ const providedArgs = Array.isArray(subcommandArgs) ? subcommandArgs : [];
42
+ if (providedArgs.length > positionalArgs.length) {
43
+ throw createCliError(
44
+ `Generator command "${subcommandName}" for ${String(packageEntry?.packageId || "unknown-package")} accepts at most ${positionalArgs.length} positional argument${positionalArgs.length === 1 ? "" : "s"}.`
45
+ );
46
+ }
47
+
48
+ const mappedInlineOptions = {
49
+ ...(inlineOptions && typeof inlineOptions === "object" ? inlineOptions : {})
50
+ };
51
+ for (const [index, rawValue] of providedArgs.entries()) {
52
+ const positionalArg = positionalArgs[index];
53
+ const optionName = String(positionalArg?.name || "").trim();
54
+ if (!optionName) {
55
+ throw createCliError(
56
+ `Generator command "${subcommandName}" for ${String(packageEntry?.packageId || "unknown-package")} defines positional arg ${index + 1} without a name.`
57
+ );
58
+ }
59
+
60
+ const value = String(rawValue || "").trim();
61
+ const existingValue = Object.prototype.hasOwnProperty.call(mappedInlineOptions, optionName)
62
+ ? String(mappedInlineOptions[optionName] || "").trim()
63
+ : null;
64
+ if (existingValue != null && existingValue !== value) {
65
+ throw createCliError(
66
+ `Generator command "${subcommandName}" for ${String(packageEntry?.packageId || "unknown-package")} received both positional "${optionName}" and --${optionName} with different values.`
67
+ );
68
+ }
69
+ mappedInlineOptions[optionName] = value;
70
+ }
71
+
72
+ return mappedInlineOptions;
73
+ }
74
+
28
75
  function resolveSubcommandRequiresInput(packageEntry = {}, subcommandName = "") {
29
76
  const descriptor = packageEntry?.descriptor && typeof packageEntry.descriptor === "object"
30
77
  ? packageEntry.descriptor
@@ -62,6 +109,108 @@ function resolveSubcommandRequiresInput(packageEntry = {}, subcommandName = "")
62
109
  return false;
63
110
  }
64
111
 
112
+ function collectUnexpectedGeneratorSubcommandOptionNames(packageEntry = {}, subcommandName = "", inlineOptions = {}) {
113
+ const subcommandDefinition = resolveGeneratorSubcommandDefinitionMetadata(packageEntry, subcommandName);
114
+ if (!Array.isArray(subcommandDefinition?.optionNames)) {
115
+ return [];
116
+ }
117
+
118
+ const allowedOptionNameSet = new Set(
119
+ subcommandDefinition.optionNames
120
+ .map((optionName) => String(optionName || "").trim())
121
+ .filter(Boolean)
122
+ );
123
+ return Object.keys(inlineOptions || {})
124
+ .map((optionName) => String(optionName || "").trim())
125
+ .filter(Boolean)
126
+ .filter((optionName) => !allowedOptionNameSet.has(optionName))
127
+ .sort((left, right) => left.localeCompare(right));
128
+ }
129
+
130
+ function resolveCreateTargetPolicy(packageEntry = {}, subcommandName = "") {
131
+ const definition = resolveGeneratorSubcommandDefinitionMetadata(packageEntry, subcommandName);
132
+ const createTarget = definition?.createTarget;
133
+ return createTarget && typeof createTarget === "object" ? createTarget : {};
134
+ }
135
+
136
+ function normalizeRelativePathWithinApp(appRoot = "", targetPath = "", createCliError) {
137
+ const normalizedTargetPath = String(targetPath || "").trim().replace(/\\/g, "/").replace(/^\.\/+/, "");
138
+ if (!normalizedTargetPath) {
139
+ throw createCliError("Generator create target path cannot be empty.");
140
+ }
141
+
142
+ const absolutePath = path.resolve(appRoot, normalizedTargetPath);
143
+ const relativePath = path.relative(appRoot, absolutePath);
144
+ if (
145
+ !relativePath ||
146
+ relativePath === ".." ||
147
+ relativePath.startsWith(`..${path.sep}`) ||
148
+ path.isAbsolute(relativePath)
149
+ ) {
150
+ throw createCliError(`Generator create target must stay within app root: ${normalizedTargetPath}`);
151
+ }
152
+
153
+ return {
154
+ absolutePath,
155
+ relativePath: relativePath.split(path.sep).join("/")
156
+ };
157
+ }
158
+
159
+ async function enforceDescriptorBackedCreateTargetPolicy({
160
+ packageEntry,
161
+ subcommandName,
162
+ inlineOptions = {},
163
+ appRoot = "",
164
+ packageIdInput = "",
165
+ createCliError,
166
+ readdir
167
+ } = {}) {
168
+ const policy = resolveCreateTargetPolicy(packageEntry, subcommandName);
169
+ const pathTemplate = String(policy.pathTemplate || "").trim();
170
+ if (!pathTemplate) {
171
+ return;
172
+ }
173
+
174
+ const forceOptionName = String(policy.forceOptionName || "force").trim() || "force";
175
+ const forceOverwrite = String(inlineOptions?.[forceOptionName] || "").trim().toLowerCase() === "true";
176
+ if (forceOverwrite) {
177
+ return;
178
+ }
179
+
180
+ const interpolatedTargetPath = interpolateOptionValue(
181
+ pathTemplate,
182
+ inlineOptions,
183
+ String(packageEntry?.packageId || "unknown-package"),
184
+ `${String(subcommandName || "generator")}.createTarget.pathTemplate`
185
+ );
186
+ const resolvedTargetPath = normalizeRelativePathWithinApp(appRoot, interpolatedTargetPath, createCliError);
187
+
188
+ try {
189
+ const entries = await readdir(resolvedTargetPath.absolutePath);
190
+ if (policy.allowExistingEmptyDirectory === true && entries.length < 1) {
191
+ return;
192
+ }
193
+
194
+ const commandLabel = `${String(packageIdInput || packageEntry?.packageId || "generator").trim()} ${String(subcommandName || "").trim()}`.trim();
195
+ const targetLabel = String(policy.label || "target").trim() || "target";
196
+ throw createCliError(
197
+ `${commandLabel} will not overwrite existing ${targetLabel} ${resolvedTargetPath.relativePath}. Re-run with --force to overwrite it.`
198
+ );
199
+ } catch (error) {
200
+ if (error?.code === "ENOENT") {
201
+ return;
202
+ }
203
+ if (error?.code === "ENOTDIR") {
204
+ const commandLabel = `${String(packageIdInput || packageEntry?.packageId || "generator").trim()} ${String(subcommandName || "").trim()}`.trim();
205
+ const targetLabel = String(policy.label || "target").trim() || "target";
206
+ throw createCliError(
207
+ `${commandLabel} will not overwrite existing ${targetLabel} ${resolvedTargetPath.relativePath}. Re-run with --force to overwrite it.`
208
+ );
209
+ }
210
+ throw error;
211
+ }
212
+ }
213
+
65
214
  async function runPackageGenerateCommand(
66
215
  ctx = {},
67
216
  { positional, options, cwd, io },
@@ -79,6 +228,8 @@ async function runPackageGenerateCommand(
79
228
  resolvePackageKind,
80
229
  resolveGeneratorPrimarySubcommand,
81
230
  hasGeneratorSubcommandDefinition,
231
+ readdir,
232
+ validateInlineOptionValuesForPackage,
82
233
  runGeneratorSubcommand
83
234
  } = ctx;
84
235
 
@@ -145,27 +296,12 @@ async function runPackageGenerateCommand(
145
296
  }
146
297
 
147
298
  if (isHelpToken(subcommandName)) {
148
- const helpSubcommandName = String(subcommandArgs[0] || "").trim();
149
- if (subcommandArgs.length > 1) {
150
- throw createCliError("generate help accepts at most one subcommand name.");
299
+ if (subcommandArgs.length > 0) {
300
+ throw createCliError(
301
+ `Unknown generator usage: jskit generate ${targetId} help ${subcommandArgs.join(" ")}. Use: jskit generate ${targetId} <subcommand> help`
302
+ );
151
303
  }
152
304
  const { packageEntry } = await resolveGeneratorPackageEntry(targetId);
153
- if (helpSubcommandName) {
154
- const rendered = renderGenerateSubcommandHelp({
155
- io,
156
- packageEntry,
157
- packageIdInput: targetId,
158
- subcommandName: helpSubcommandName,
159
- json: options.json
160
- });
161
- if (!rendered) {
162
- throw createCliError(
163
- `Unknown generator subcommand "${helpSubcommandName}" for ${String(packageEntry?.packageId || targetId)}.`
164
- );
165
- }
166
- return 0;
167
- }
168
-
169
305
  renderGeneratePackageHelp({
170
306
  io,
171
307
  packageEntry,
@@ -175,56 +311,111 @@ async function runPackageGenerateCommand(
175
311
  return 0;
176
312
  }
177
313
 
178
- if (subcommandName) {
179
- const {
180
- appRoot,
181
- packageEntry,
182
- resolvedPackageId
183
- } = await resolveGeneratorPackageEntry(targetId);
314
+ async function runResolvedGeneratorSubcommand({
315
+ appRoot,
316
+ packageEntry,
317
+ resolvedPackageId,
318
+ subcommandName: rawSubcommandName = "",
319
+ subcommandArgs: rawSubcommandArgs = []
320
+ } = {}) {
321
+ const normalizedSubcommandName = String(rawSubcommandName || "").trim().toLowerCase();
322
+ const normalizedSubcommandArgs = Array.isArray(rawSubcommandArgs)
323
+ ? rawSubcommandArgs
324
+ : [];
184
325
  const hasInlineOptions = Object.keys(options?.inlineOptions || {}).length > 0;
185
- const hasSubcommandArgs = subcommandArgs.length > 0;
186
- if (!hasInlineOptions && !hasSubcommandArgs && resolveSubcommandRequiresInput(packageEntry, subcommandName)) {
326
+ const hasSubcommandArgs = normalizedSubcommandArgs.length > 0;
327
+ if (!hasInlineOptions && !hasSubcommandArgs && resolveSubcommandRequiresInput(packageEntry, normalizedSubcommandName)) {
187
328
  const rendered = renderGenerateSubcommandHelp({
188
329
  io,
189
330
  packageEntry,
190
331
  packageIdInput: targetId,
191
- subcommandName,
332
+ subcommandName: normalizedSubcommandName,
192
333
  json: options.json
193
334
  });
194
335
  if (rendered) {
195
336
  return 0;
196
337
  }
197
338
  }
198
- if (subcommandArgs.length === 1 && isHelpToken(subcommandArgs[0])) {
339
+ if (normalizedSubcommandArgs.length === 1 && isHelpToken(normalizedSubcommandArgs[0])) {
199
340
  const rendered = renderGenerateSubcommandHelp({
200
341
  io,
201
342
  packageEntry,
202
343
  packageIdInput: targetId,
203
- subcommandName,
344
+ subcommandName: normalizedSubcommandName,
204
345
  json: options.json
205
346
  });
206
347
  if (!rendered) {
207
- throw createCliError(`Unknown generator subcommand "${subcommandName}" for ${resolvedPackageId}.`);
348
+ throw createCliError(`Unknown generator subcommand "${normalizedSubcommandName}" for ${resolvedPackageId}.`);
208
349
  }
209
350
  return 0;
210
351
  }
211
352
 
212
- const normalizedSubcommandName = String(subcommandName || "").trim().toLowerCase();
353
+ const subcommandDefinition = resolveGeneratorSubcommandDefinitionMetadata(packageEntry, normalizedSubcommandName);
354
+ const unexpectedOptionNames = collectUnexpectedGeneratorSubcommandOptionNames(
355
+ packageEntry,
356
+ normalizedSubcommandName,
357
+ options.inlineOptions
358
+ );
359
+ if (unexpectedOptionNames.length > 0) {
360
+ const commandLabel = String(targetId || resolvedPackageId || "").trim() || resolvedPackageId;
361
+ throw createCliError(
362
+ `Unknown option${unexpectedOptionNames.length === 1 ? "" : "s"} for generator command ${commandLabel} ${normalizedSubcommandName}: ${unexpectedOptionNames.map((optionName) => `--${optionName}`).join(", ")}.`,
363
+ {
364
+ renderUsage: options.json
365
+ ? null
366
+ : () => {
367
+ renderGenerateSubcommandHelp({
368
+ io: {
369
+ ...io,
370
+ stdout: io.stderr || io.stdout
371
+ },
372
+ packageEntry,
373
+ packageIdInput: targetId,
374
+ subcommandName: normalizedSubcommandName,
375
+ json: false
376
+ });
377
+ }
378
+ }
379
+ );
380
+ }
381
+ const validatedOptionNames = Array.isArray(subcommandDefinition?.optionNames)
382
+ ? subcommandDefinition.optionNames
383
+ : [];
384
+ await validateInlineOptionValuesForPackage(packageEntry, options.inlineOptions, {
385
+ appRoot,
386
+ optionNames: validatedOptionNames
387
+ });
388
+
213
389
  const primarySubcommand = resolveGeneratorPrimarySubcommand(packageEntry);
214
390
  if (
215
391
  normalizedSubcommandName &&
216
392
  normalizedSubcommandName === primarySubcommand &&
217
393
  !hasGeneratorSubcommandDefinition(packageEntry, normalizedSubcommandName)
218
394
  ) {
219
- if (subcommandArgs.length > 0) {
220
- throw createCliError(
221
- `Generator command "${primarySubcommand}" for ${resolvedPackageId} does not accept positional arguments.`
222
- );
223
- }
395
+ const inlineOptionsForPrimarySubcommand = mapDescriptorBackedSubcommandArgsToInlineOptions(
396
+ packageEntry,
397
+ normalizedSubcommandName,
398
+ normalizedSubcommandArgs,
399
+ options.inlineOptions,
400
+ createCliError
401
+ );
402
+ await validateInlineOptionValuesForPackage(packageEntry, inlineOptionsForPrimarySubcommand, {
403
+ appRoot
404
+ });
405
+ await enforceDescriptorBackedCreateTargetPolicy({
406
+ packageEntry,
407
+ subcommandName: normalizedSubcommandName,
408
+ inlineOptions: inlineOptionsForPrimarySubcommand,
409
+ appRoot,
410
+ packageIdInput: targetId,
411
+ createCliError,
412
+ readdir
413
+ });
224
414
  return runCommandAdd({
225
415
  positional: ["package", resolvedPackageId],
226
416
  options: {
227
417
  ...options,
418
+ inlineOptions: inlineOptionsForPrimarySubcommand,
228
419
  commandMode: "generate"
229
420
  },
230
421
  cwd,
@@ -246,8 +437,8 @@ async function runPackageGenerateCommand(
246
437
 
247
438
  return runGeneratorSubcommand({
248
439
  packageEntry: executablePackageEntry,
249
- subcommandName,
250
- subcommandArgs,
440
+ subcommandName: normalizedSubcommandName,
441
+ subcommandArgs: normalizedSubcommandArgs,
251
442
  inlineOptions: options.inlineOptions,
252
443
  appRoot,
253
444
  io,
@@ -256,15 +447,23 @@ async function runPackageGenerateCommand(
256
447
  });
257
448
  }
258
449
 
259
- return runCommandAdd({
260
- positional: ["package", targetId],
261
- options: {
262
- ...options,
263
- commandMode: "generate"
264
- },
265
- cwd,
266
- io
450
+ if (subcommandName) {
451
+ const resolvedGeneratorPackage = await resolveGeneratorPackageEntry(targetId);
452
+ return runResolvedGeneratorSubcommand({
453
+ ...resolvedGeneratorPackage,
454
+ subcommandName,
455
+ subcommandArgs
456
+ });
457
+ }
458
+
459
+ const { packageEntry } = await resolveGeneratorPackageEntry(targetId);
460
+ renderGeneratePackageHelp({
461
+ io,
462
+ packageEntry,
463
+ packageIdInput: targetId,
464
+ json: options.json
267
465
  });
466
+ return 0;
268
467
  }
269
468
 
270
469
  export { runPackageGenerateCommand };
@@ -51,6 +51,7 @@ function parseArgs(argv, { createCliError } = {}) {
51
51
  inlineOptions: {}
52
52
  };
53
53
  const positional = [];
54
+ const allowLooseInlineOptionParsing = command === "generate";
54
55
 
55
56
  while (args.length > 0) {
56
57
  const token = String(args.shift() || "");
@@ -99,27 +100,45 @@ function parseArgs(argv, { createCliError } = {}) {
99
100
  options.help = true;
100
101
  continue;
101
102
  }
103
+ if (token === "--force") {
104
+ options.inlineOptions.force = "true";
105
+ continue;
106
+ }
102
107
 
103
108
  if (token.startsWith("--")) {
104
109
  const withoutPrefix = token.slice(2);
105
110
  const hasInlineValue = withoutPrefix.includes("=");
106
111
  const optionName = hasInlineValue ? withoutPrefix.slice(0, withoutPrefix.indexOf("=")) : withoutPrefix;
107
- const optionValueRaw = hasInlineValue
108
- ? withoutPrefix.slice(withoutPrefix.indexOf("=") + 1)
109
- : args.shift();
110
-
111
- if (!/^[a-z][a-z0-9-]*$/.test(optionName)) {
112
+ const optionNamePattern = allowLooseInlineOptionParsing
113
+ ? /^[A-Za-z][A-Za-z0-9_-]*$/
114
+ : /^[a-z][a-z0-9-]*$/;
115
+ if (!optionNamePattern.test(optionName)) {
112
116
  throw createCliError(`Unknown option: ${token}`, { showUsage: true });
113
117
  }
114
- if (typeof optionValueRaw !== "string") {
115
- throw createCliError(`--${optionName} requires a value.`, { showUsage: true });
118
+
119
+ let optionValueRaw;
120
+ if (hasInlineValue) {
121
+ optionValueRaw = withoutPrefix.slice(withoutPrefix.indexOf("=") + 1);
122
+ } else {
123
+ const nextToken = typeof args[0] === "string" ? String(args[0]) : "";
124
+ if (nextToken && !nextToken.startsWith("-")) {
125
+ optionValueRaw = args.shift();
126
+ }
116
127
  }
117
- const optionValue = optionValueRaw.trim();
118
- if (!hasInlineValue && optionValue.startsWith("-")) {
128
+
129
+ if (!allowLooseInlineOptionParsing && typeof optionValueRaw !== "string") {
119
130
  throw createCliError(`--${optionName} requires a value.`, { showUsage: true });
120
131
  }
132
+ if (typeof optionValueRaw === "string") {
133
+ const optionValue = optionValueRaw.trim();
134
+ if (!hasInlineValue && optionValue.startsWith("-")) {
135
+ throw createCliError(`--${optionName} requires a value.`, { showUsage: true });
136
+ }
137
+ options.inlineOptions[optionName] = optionValue;
138
+ continue;
139
+ }
121
140
 
122
- options.inlineOptions[optionName] = optionValue;
141
+ options.inlineOptions[optionName] = undefined;
123
142
  continue;
124
143
  }
125
144
 
@@ -18,6 +18,7 @@ function createCommandHandlerDeps(deps = {}) {
18
18
  hydratePackageRegistryFromInstalledNodeModules: deps.hydratePackageRegistryFromInstalledNodeModules,
19
19
  resolvePackageTemplateRoot: deps.resolvePackageTemplateRoot,
20
20
  validateInlineOptionsForPackage: deps.validateInlineOptionsForPackage,
21
+ validateInlineOptionValuesForPackage: deps.validateInlineOptionValuesForPackage,
21
22
  resolveLocalDependencyOrder: deps.resolveLocalDependencyOrder,
22
23
  validatePlannedCapabilityClosure: deps.validatePlannedCapabilityClosure,
23
24
  resolvePackageOptions: deps.resolvePackageOptions,
@@ -34,6 +35,7 @@ function createCommandHandlerDeps(deps = {}) {
34
35
  writeJsonFile: deps.writeJsonFile,
35
36
  writeFile: deps.writeFile,
36
37
  mkdir: deps.mkdir,
38
+ readdir: deps.readdir,
37
39
  path: deps.path,
38
40
  inspectPackageOfferings: deps.inspectPackageOfferings,
39
41
  buildFileWriteGroups: deps.buildFileWriteGroups,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  mkdir,
3
+ readdir,
3
4
  rm,
4
5
  writeFile
5
6
  } from "node:fs/promises";
@@ -67,7 +68,8 @@ import {
67
68
  } from "../cliRuntime/packageIntrospection.js";
68
69
  import {
69
70
  resolvePackageOptions,
70
- validateInlineOptionsForPackage
71
+ validateInlineOptionsForPackage,
72
+ validateInlineOptionValuesForPackage
71
73
  } from "../cliRuntime/packageOptions.js";
72
74
  import {
73
75
  resolvePackageTemplateRoot,
@@ -103,6 +105,7 @@ const commandHandlers = createCommandHandlers(
103
105
  hydratePackageRegistryFromInstalledNodeModules,
104
106
  resolvePackageTemplateRoot,
105
107
  validateInlineOptionsForPackage,
108
+ validateInlineOptionValuesForPackage,
106
109
  resolveLocalDependencyOrder,
107
110
  validatePlannedCapabilityClosure,
108
111
  resolvePackageOptions,
@@ -119,6 +122,7 @@ const commandHandlers = createCommandHandlers(
119
122
  writeJsonFile,
120
123
  writeFile,
121
124
  mkdir,
125
+ readdir,
122
126
  path,
123
127
  inspectPackageOfferings,
124
128
  buildFileWriteGroups,
@@ -122,7 +122,9 @@ function createRunCli({
122
122
  throw createCliError(`Unhandled command: ${command}`, { showUsage: true });
123
123
  } catch (error) {
124
124
  stderr.write(`jskit: ${error?.message || String(error)}\n`);
125
- if (error?.showUsage) {
125
+ if (typeof error?.renderUsage === "function") {
126
+ error.renderUsage();
127
+ } else if (error?.showUsage) {
126
128
  printUsage(stderr);
127
129
  }
128
130
  return 1;
@@ -1,4 +1,8 @@
1
1
  import { resolveCommandAlias } from "./commandCatalog.js";
2
+ import {
3
+ createColorFormatter,
4
+ writeWrappedLines
5
+ } from "../shared/outputFormatting.js";
2
6
 
3
7
  const COMMAND_OVERVIEW = Object.freeze([
4
8
  Object.freeze({
@@ -114,9 +118,26 @@ const COMMAND_HELP = Object.freeze({
114
118
  "No npm install runs unless --run-npm-install is passed.",
115
119
  "Short ids resolve to @jskit-ai/<id> when available.",
116
120
  "Running without args lists available generators.",
117
- "If no subcommand is provided, the generator primary command runs.",
121
+ "Running with only <generatorId> shows generator help.",
118
122
  "Use jskit generate <generatorId> <subcommand> help for subcommand-specific usage."
119
123
  ]),
124
+ examples: Object.freeze([
125
+ Object.freeze({
126
+ label: "Common usage",
127
+ lines: Object.freeze([
128
+ "npx jskit generate ui-generator page \\",
129
+ " admin/reports/index.vue"
130
+ ])
131
+ }),
132
+ Object.freeze({
133
+ label: "More advanced usage",
134
+ lines: Object.freeze([
135
+ "npx jskit generate crud-ui-generator crud \\",
136
+ " admin/catalog/index/products \\",
137
+ " --resource-file packages/products/src/shared/productResource.js"
138
+ ])
139
+ })
140
+ ]),
120
141
  fullUse: "jskit generate <generatorId> [subcommand] [subcommand args...] [--<option> <value>...] [--dry-run] [--run-npm-install] [--json] [--verbose]"
121
142
  }),
122
143
  list: Object.freeze({
@@ -297,22 +318,41 @@ const BARE_COMMAND_HELP = new Set([
297
318
  "remove"
298
319
  ]);
299
320
 
300
- function writeLine(stream, line = "") {
301
- stream.write(`${line}\n`);
321
+ function appendSeparatedBlocks(lines = [], blocks = []) {
322
+ const normalizedBlocks = Array.isArray(blocks) ? blocks : [];
323
+ for (const [index, block] of normalizedBlocks.entries()) {
324
+ if (index > 0) {
325
+ lines.push("");
326
+ }
327
+ const lineList = Array.isArray(block) ? block : [block];
328
+ for (const line of lineList) {
329
+ lines.push(line);
330
+ }
331
+ }
332
+ }
333
+
334
+ function writeHelpLines(stream, lines = []) {
335
+ writeWrappedLines({
336
+ stdout: stream,
337
+ lines
338
+ });
302
339
  }
303
340
 
304
341
  function printTopLevelHelp(stream = process.stderr) {
305
- writeLine(stream, "JSKit CLI");
306
- writeLine(stream, "");
307
- writeLine(stream, "Use: jskit help <command> for command-specific usage.");
308
- writeLine(stream, "");
309
- writeLine(stream, "Available commands:");
342
+ const color = createColorFormatter(stream);
343
+ const lines = [];
344
+ lines.push(color.heading("JSKit CLI"));
345
+ lines.push("");
346
+ lines.push("Use: jskit help <command> for command-specific usage.");
347
+ lines.push("");
348
+ lines.push(color.heading("Available commands:"));
310
349
  for (const entry of COMMAND_OVERVIEW) {
311
- writeLine(stream, ` ${entry.command.padEnd(16, " ")} ${entry.summary}`);
350
+ lines.push(` ${color.item(entry.command.padEnd(16, " "))} ${entry.summary}`);
312
351
  }
313
- writeLine(stream, "");
314
- writeLine(stream, "Global flags:");
315
- writeLine(stream, " --dry-run --run-npm-install --json --verbose --help");
352
+ lines.push("");
353
+ lines.push(color.heading("Global flags:"));
354
+ lines.push(" --dry-run --run-npm-install --json --verbose --help");
355
+ writeHelpLines(stream, lines);
316
356
  }
317
357
 
318
358
  function printCommandHelp(stream = process.stderr, command = "") {
@@ -323,27 +363,54 @@ function printCommandHelp(stream = process.stderr, command = "") {
323
363
  return;
324
364
  }
325
365
 
326
- writeLine(stream, `Command: ${entry.title}`);
327
- writeLine(stream, "");
366
+ const color = createColorFormatter(stream);
367
+ const lines = [];
368
+ lines.push(`Command: ${color.emphasis(entry.title)}`);
369
+ lines.push("");
370
+
371
+ let sectionNumber = 1;
328
372
 
329
- writeLine(stream, "1) Minimal use");
330
- writeLine(stream, ` ${entry.minimalUse}`);
373
+ lines.push(color.heading(`${sectionNumber++}) Minimal use`));
374
+ lines.push(` ${entry.minimalUse}`);
331
375
  if (entry.parameters.length > 0) {
332
- writeLine(stream, " Parameters:");
333
- for (const parameter of entry.parameters) {
334
- writeLine(stream, ` - ${parameter.name}: ${parameter.description}`);
335
- }
376
+ lines.push(color.heading(" Parameters:"));
377
+ appendSeparatedBlocks(
378
+ lines,
379
+ entry.parameters.map((parameter) => ` - ${parameter.name}: ${parameter.description}`)
380
+ );
336
381
  }
337
- writeLine(stream, "");
382
+ lines.push("");
383
+
384
+ lines.push(color.heading(`${sectionNumber++}) Defaults`));
385
+ appendSeparatedBlocks(
386
+ lines,
387
+ entry.defaults.map((defaultLine) => ` - ${defaultLine}`)
388
+ );
389
+ lines.push("");
338
390
 
339
- writeLine(stream, "2) Defaults");
340
- for (const defaultLine of entry.defaults) {
341
- writeLine(stream, ` - ${defaultLine}`);
391
+ const examples = Array.isArray(entry.examples) ? entry.examples : [];
392
+ if (examples.length > 0) {
393
+ lines.push(color.heading(`${sectionNumber++}) Examples`));
394
+ appendSeparatedBlocks(
395
+ lines,
396
+ examples.map((example) => {
397
+ const block = [];
398
+ const label = String(example?.label || "").trim();
399
+ if (label) {
400
+ block.push(` - ${color.item(label)}`);
401
+ }
402
+ for (const line of Array.isArray(example?.lines) ? example.lines : []) {
403
+ block.push(` ${line}`);
404
+ }
405
+ return block;
406
+ })
407
+ );
408
+ lines.push("");
342
409
  }
343
- writeLine(stream, "");
344
410
 
345
- writeLine(stream, "3) Full use");
346
- writeLine(stream, ` ${entry.fullUse}`);
411
+ lines.push(color.heading(`${sectionNumber++}) Full use`));
412
+ lines.push(` ${entry.fullUse}`);
413
+ writeHelpLines(stream, lines);
347
414
  }
348
415
 
349
416
  function printUsage(stream = process.stderr, { command = "" } = {}) {
@@ -1,8 +1,9 @@
1
- function createCliError(message, { showUsage = false, exitCode = 1 } = {}) {
1
+ function createCliError(message, { showUsage = false, exitCode = 1, renderUsage = null } = {}) {
2
2
  const error = new Error(String(message || "Unknown CLI error"));
3
3
  error.name = "CliError";
4
4
  error.showUsage = Boolean(showUsage);
5
5
  error.exitCode = Number.isInteger(exitCode) ? exitCode : 1;
6
+ error.renderUsage = typeof renderUsage === "function" ? renderUsage : null;
6
7
  return error;
7
8
  }
8
9