@flowscripter/dynamic-cli-framework 11.0.0 → 12.0.0

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 (52) hide show
  1. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  2. package/dist/src/cli/BaseCLI.js +10 -3
  3. package/dist/src/runtime/DefaultContext.d.ts +2 -1
  4. package/dist/src/runtime/DefaultContext.d.ts.map +1 -1
  5. package/dist/src/runtime/DefaultContext.js +3 -1
  6. package/dist/src/runtime/command/CommandValidator.d.ts.map +1 -1
  7. package/dist/src/runtime/command/CommandValidator.js +18 -6
  8. package/dist/src/runtime/parser.d.ts.map +1 -1
  9. package/dist/src/runtime/parser.js +13 -4
  10. package/dist/src/runtime/values/argumentValueValidation.d.ts +26 -4
  11. package/dist/src/runtime/values/argumentValueValidation.d.ts.map +1 -1
  12. package/dist/src/runtime/values/argumentValueValidation.js +53 -50
  13. package/dist/src/service/argumentPrompter/DefaultArgumentPrompterService.d.ts +4 -0
  14. package/dist/src/service/argumentPrompter/DefaultArgumentPrompterService.d.ts.map +1 -1
  15. package/dist/src/service/argumentPrompter/DefaultArgumentPrompterService.js +53 -21
  16. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -1
  17. package/dist/src/service/configuration/KeyValueServiceProvider.js +1 -0
  18. package/dist/src/service/printer/PrinterServiceProvider.d.ts +3 -1
  19. package/dist/src/service/printer/PrinterServiceProvider.d.ts.map +1 -1
  20. package/dist/src/service/printer/PrinterServiceProvider.js +7 -4
  21. package/dist/src/service/printer/command/ThemeCommand.d.ts +24 -0
  22. package/dist/src/service/printer/command/ThemeCommand.d.ts.map +1 -0
  23. package/dist/src/service/printer/command/ThemeCommand.js +40 -0
  24. package/dist/src/terminal/Ansi.d.ts +4 -0
  25. package/dist/src/terminal/Ansi.d.ts.map +1 -1
  26. package/dist/src/terminal/Ansi.js +7 -3
  27. package/dist/src/terminal/BackgroundDetector.d.ts +25 -0
  28. package/dist/src/terminal/BackgroundDetector.d.ts.map +1 -0
  29. package/dist/src/terminal/BackgroundDetector.js +63 -0
  30. package/dist/src/terminal/KeyReader.d.ts +7 -0
  31. package/dist/src/terminal/KeyReader.d.ts.map +1 -1
  32. package/dist/src/terminal/TtyKeyReader.d.ts +1 -0
  33. package/dist/src/terminal/TtyKeyReader.d.ts.map +1 -1
  34. package/dist/src/terminal/TtyKeyReader.js +30 -0
  35. package/package.json +2 -2
  36. package/src/cli/BaseCLI.ts +15 -4
  37. package/src/runtime/DefaultContext.ts +4 -1
  38. package/src/runtime/command/CommandValidator.ts +35 -5
  39. package/src/runtime/parser.ts +14 -0
  40. package/src/runtime/values/argumentValueValidation.ts +82 -50
  41. package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +92 -26
  42. package/src/service/configuration/KeyValueServiceProvider.ts +1 -0
  43. package/src/service/printer/PrinterServiceProvider.ts +11 -4
  44. package/src/service/printer/command/ThemeCommand.ts +47 -0
  45. package/src/terminal/Ansi.ts +7 -3
  46. package/src/terminal/BackgroundDetector.ts +78 -0
  47. package/src/terminal/KeyReader.ts +7 -0
  48. package/src/terminal/TtyKeyReader.ts +35 -0
  49. package/dist/src/service/printer/command/DarkModeCommand.d.ts +0 -17
  50. package/dist/src/service/printer/command/DarkModeCommand.d.ts.map +0 -1
  51. package/dist/src/service/printer/command/DarkModeCommand.js +0 -24
  52. package/src/service/printer/command/DarkModeCommand.ts +0 -32
@@ -4,7 +4,7 @@ import type { GlobalCommand } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { Argument } from "@flowscripter/dynamic-cli-framework-api";
5
5
  import type { GroupCommand } from "@flowscripter/dynamic-cli-framework-api";
6
6
  import type { SubCommand } from "@flowscripter/dynamic-cli-framework-api";
7
- import type { SingleValueType } from "@flowscripter/dynamic-cli-framework-api";
7
+ import type { PopulatedValues, SingleValueType } from "@flowscripter/dynamic-cli-framework-api";
8
8
  import { ValueTypeName, ComplexValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
9
9
  import type { Option } from "@flowscripter/dynamic-cli-framework-api";
10
10
  import type { ComplexOption } from "@flowscripter/dynamic-cli-framework-api";
@@ -13,6 +13,8 @@ import {
13
13
  getInvalidArgumentString,
14
14
  validateGlobalCommandArgumentValue,
15
15
  validateOptionValue,
16
+ runCustomValidation,
17
+ type DeferredCustomValidation,
16
18
  } from "../values/argumentValueValidation.ts";
17
19
  import getLogger from "../../util/logger.ts";
18
20
  import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
@@ -219,7 +221,16 @@ export default class CommandValidator {
219
221
  validateArgument(argument, globalCommand.name);
220
222
  if (argument.defaultValue) {
221
223
  const invalidArguments: Array<InvalidArgument> = [];
222
- validateGlobalCommandArgumentValue(globalCommand, argument.defaultValue, invalidArguments);
224
+ const deferred: Array<DeferredCustomValidation> = [];
225
+ validateGlobalCommandArgumentValue(
226
+ globalCommand,
227
+ argument.defaultValue,
228
+ invalidArguments,
229
+ deferred,
230
+ );
231
+ if (invalidArguments.length === 0) {
232
+ runCustomValidation(deferred, globalCommand, argument.defaultValue, invalidArguments);
233
+ }
223
234
  if (invalidArguments.length > 0) {
224
235
  throw new Error(
225
236
  `Illegal default value for global command: '${globalCommand.name}' => ${invalidArguments
@@ -440,12 +451,13 @@ export default class CommandValidator {
440
451
  option as unknown as SubCommandArgument,
441
452
  ]);
442
453
  } else {
443
- this.#validateOption(option, currentOptionPaths, allOptionPaths);
454
+ this.#validateOption(subCommand, option, currentOptionPaths, allOptionPaths);
444
455
  this.#validateSubCommandArgumentConfigurationKey(subCommand, [...argumentAncestry, option]);
445
456
  }
446
457
  }
447
458
 
448
459
  #validateOption(
460
+ subCommand: SubCommand,
449
461
  option: Option,
450
462
  currentOptionPaths: Array<string>,
451
463
  allOptionPaths: Array<string>,
@@ -460,7 +472,16 @@ export default class CommandValidator {
460
472
  }
461
473
  if (option.defaultValue) {
462
474
  const invalidArguments: Array<InvalidArgument> = [];
463
- validateOptionValue(option, option.defaultValue, invalidArguments);
475
+ const deferred: Array<DeferredCustomValidation> = [];
476
+ validateOptionValue(option, option.defaultValue, invalidArguments, deferred);
477
+ if (invalidArguments.length === 0) {
478
+ runCustomValidation(
479
+ deferred,
480
+ subCommand,
481
+ { [option.name]: option.defaultValue } as PopulatedValues,
482
+ invalidArguments,
483
+ );
484
+ }
464
485
  if (invalidArguments.length > 0) {
465
486
  throw new Error(
466
487
  `Illegal default value for option: '${option.name}' => ${invalidArguments
@@ -515,7 +536,16 @@ export default class CommandValidator {
515
536
  }
516
537
  if (complexOption.defaultValue) {
517
538
  const invalidArguments: Array<InvalidArgument> = [];
518
- validateOptionValue(complexOption, complexOption.defaultValue, invalidArguments);
539
+ const deferred: Array<DeferredCustomValidation> = [];
540
+ validateOptionValue(complexOption, complexOption.defaultValue, invalidArguments, deferred);
541
+ if (invalidArguments.length === 0) {
542
+ runCustomValidation(
543
+ deferred,
544
+ subCommand,
545
+ { [complexOption.name]: complexOption.defaultValue } as PopulatedValues,
546
+ invalidArguments,
547
+ );
548
+ }
519
549
  if (invalidArguments.length > 0) {
520
550
  throw new Error(
521
551
  `Illegal default value for option: '${complexOption.name}' => ${invalidArguments
@@ -11,6 +11,8 @@ import {
11
11
  validateGlobalCommandArgumentValue,
12
12
  validateOptionValue,
13
13
  validatePositionalValue,
14
+ runCustomValidation,
15
+ type DeferredCustomValidation,
14
16
  } from "./values/argumentValueValidation.ts";
15
17
  import getLogger from "../util/logger.ts";
16
18
  import { redactGlobalCommandValue, redactSubCommandValues } from "../util/secretRedaction.ts";
@@ -64,12 +66,14 @@ export function parseSubCommandClause(
64
66
  }
65
67
 
66
68
  if (invalidArguments.length === 0) {
69
+ const deferred: Array<DeferredCustomValidation> = [];
67
70
  // validate the option values
68
71
  command.options.forEach((option) => {
69
72
  const validatedValue = validateOptionValue(
70
73
  option,
71
74
  populatedArgumentValues[option.name] || option.defaultValue,
72
75
  invalidArguments,
76
+ deferred,
73
77
  );
74
78
  if (validatedValue !== undefined) {
75
79
  populatedArgumentValues[option.name] = validatedValue;
@@ -89,11 +93,16 @@ export function parseSubCommandClause(
89
93
  positional,
90
94
  populatedArgumentValues[positional.name] as ValueType,
91
95
  invalidArguments,
96
+ deferred,
92
97
  );
93
98
  if (validatedValue !== undefined) {
94
99
  populatedArgumentValues[positional.name] = validatedValue;
95
100
  }
96
101
  });
102
+ if (invalidArguments.length === 0) {
103
+ // custom validators run once all built-in validation has passed and see all values
104
+ runCustomValidation(deferred, command, populatedArgumentValues, invalidArguments);
105
+ }
97
106
  logger.debug(
98
107
  "Command arguments after positionals validated: %O with invalid args: %O",
99
108
  redactSubCommandValues(command, populatedArgumentValues),
@@ -162,14 +171,19 @@ export function parseGlobalCommandClause(
162
171
 
163
172
  // don't validate the value if the argument is already invalid
164
173
  if (invalidArguments.length === 0) {
174
+ const deferred: Array<DeferredCustomValidation> = [];
165
175
  const validatedValue = validateGlobalCommandArgumentValue(
166
176
  command,
167
177
  populatedArgumentValue,
168
178
  invalidArguments,
179
+ deferred,
169
180
  );
170
181
  if (validatedValue !== undefined) {
171
182
  populatedArgumentValue = validatedValue;
172
183
  }
184
+ if (invalidArguments.length === 0) {
185
+ runCustomValidation(deferred, command, populatedArgumentValue, invalidArguments);
186
+ }
173
187
  }
174
188
 
175
189
  logger.debug(
@@ -16,6 +16,7 @@ import { InvalidArgumentReason } from "@flowscripter/dynamic-cli-framework-api";
16
16
  import type { SubCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
17
17
  import type { Argument } from "@flowscripter/dynamic-cli-framework-api";
18
18
  import type { GlobalCommand } from "@flowscripter/dynamic-cli-framework-api";
19
+ import type { Command } from "@flowscripter/dynamic-cli-framework-api";
19
20
 
20
21
  interface ValidationResult {
21
22
  invalidArgument?: InvalidArgument;
@@ -41,32 +42,61 @@ function withPrefix(invalidArgument: InvalidArgument, prefix: string): InvalidAr
41
42
  }
42
43
 
43
44
  /**
44
- * Invokes the custom validate function (if any) of the provided {@link Argument} and returns an
45
- * {@link InvalidArgument} if the custom validation failed.
46
- *
47
- * @param argument the {@link Argument} providing the custom validate function.
48
- * @param name the name to use for the {@link InvalidArgument}.
49
- * @param value the validated and type-converted value.
45
+ * A custom validation to be invoked once all built-in validation has passed.
46
+ */
47
+ export interface DeferredCustomValidation {
48
+ readonly argument: Argument | ComplexOption;
49
+ readonly path: string;
50
+ readonly value: SingleValueType | Values | Array<SingleValueType | Values>;
51
+ }
52
+
53
+ /**
54
+ * Records a custom validation of the provided {@link Argument} (if it has one) for later invocation by
55
+ * {@link runCustomValidation}. Deferred validations are recorded bottom-up and depth-first.
50
56
  */
51
- function doCustomValidation(
57
+ function deferCustomValidation(
58
+ deferred: Array<DeferredCustomValidation> | undefined,
52
59
  argument: Argument | ComplexOption,
53
- name: string,
60
+ path: string,
54
61
  value: SingleValueType | Values | Array<SingleValueType | Values>,
55
- ): InvalidArgument | undefined {
56
- if (argument.validate === undefined) {
57
- return undefined;
62
+ ): void {
63
+ if (deferred !== undefined && argument.validate !== undefined) {
64
+ deferred.push({ argument, path, value });
58
65
  }
59
- const message = argument.validate(value as ValueType | Values | Array<Values>);
60
- if (message === undefined) {
61
- return undefined;
66
+ }
67
+
68
+ /**
69
+ * Invokes the recorded custom validations in order, stopping at the first failure.
70
+ *
71
+ * @param deferred the validations recorded while validating the values of the command.
72
+ * @param command the {@link Command} the validated values belong to.
73
+ * @param values all validated and converted values of the command.
74
+ * @param invalidArguments an array of {@link InvalidArgument} which is added to if a custom validation fails.
75
+ */
76
+ export function runCustomValidation(
77
+ deferred: ReadonlyArray<DeferredCustomValidation>,
78
+ command: Command,
79
+ values: PopulatedValues | PopulatedSingleValueType,
80
+ invalidArguments: Array<InvalidArgument>,
81
+ ): void {
82
+ for (const { argument, path, value } of deferred) {
83
+ const message = argument.validate!(value as ValueType | Values | Array<Values>, {
84
+ argument: argument as Argument,
85
+ path,
86
+ values,
87
+ command,
88
+ });
89
+ if (message !== undefined) {
90
+ invalidArguments.push({
91
+ argument,
92
+ name: path,
93
+ value: value as PopulatedValueType | PopulatedValues,
94
+ reason: InvalidArgumentReason.CUSTOM_VALIDATION,
95
+ message,
96
+ });
97
+ return;
98
+ }
62
99
  }
63
- return {
64
- argument,
65
- name,
66
- value: value as PopulatedValueType | PopulatedValues,
67
- reason: InvalidArgumentReason.CUSTOM_VALIDATION,
68
- message,
69
- };
70
100
  }
71
101
 
72
102
  /**
@@ -193,6 +223,8 @@ function validatePrimitiveValue(
193
223
  function validateArrayValue(
194
224
  subCommandArgument: SubCommandArgument | ComplexOption,
195
225
  arrayValue: Array<PopulatedSingleValueType | PopulatedValues | undefined>,
226
+ path: string,
227
+ deferred: Array<DeferredCustomValidation> | undefined,
196
228
  ): ArrayValueValidationResult {
197
229
  const convertedArrayValue: Array<SingleValueType | Values> = [];
198
230
 
@@ -228,7 +260,12 @@ function validateArrayValue(
228
260
  },
229
261
  };
230
262
  }
231
- validationResult = validateObjectValue(subCommandArgument as ComplexOption, singleValue);
263
+ validationResult = validateObjectValue(
264
+ subCommandArgument as ComplexOption,
265
+ singleValue,
266
+ `${path}[${i}]`,
267
+ deferred,
268
+ );
232
269
  } // if not array and not object, then must be primitive
233
270
  else {
234
271
  if (isComplexOption(subCommandArgument)) {
@@ -267,11 +304,14 @@ function validateArrayValue(
267
304
  function validateObjectValue(
268
305
  argument: ComplexOption,
269
306
  objectValue: PopulatedValues,
307
+ path: string,
308
+ deferred: Array<DeferredCustomValidation> | undefined,
270
309
  ): ObjectValueValidationResult {
271
310
  const convertedObjectValue: Values = {};
272
311
 
273
312
  for (let i = 0; i < argument.properties.length; i++) {
274
313
  const propertyArg = argument.properties[i]!;
314
+ const propertyPath = `${path}.${propertyArg.name}`;
275
315
  let propertyValue = objectValue[propertyArg.name];
276
316
 
277
317
  if (propertyValue === undefined && propertyArg.defaultValue !== undefined) {
@@ -311,6 +351,8 @@ function validateObjectValue(
311
351
  validationResult = validateArrayValue(
312
352
  propertyArg as ComplexOption | SubCommandArgument,
313
353
  propertyValue,
354
+ propertyPath,
355
+ deferred,
314
356
  );
315
357
  } else if (typeof propertyValue === "object") {
316
358
  if (propertyArg.type !== ComplexValueTypeName.COMPLEX) {
@@ -327,6 +369,8 @@ function validateObjectValue(
327
369
  validationResult = validateObjectValue(
328
370
  propertyArg as ComplexOption,
329
371
  propertyValue as PopulatedValues,
372
+ propertyPath,
373
+ deferred,
330
374
  );
331
375
  } // if not array and not object, then must be primitive
332
376
  else {
@@ -356,18 +400,8 @@ function validateObjectValue(
356
400
  invalidArgument: withPrefix(validationResult.invalidArgument, `.${propertyArg.name}`),
357
401
  };
358
402
  }
359
- // custom validation runs only once the property (and any nested properties) passed validation
360
- const customInvalidArgument = doCustomValidation(
361
- propertyArg,
362
- `.${propertyArg.name}`,
363
- validationResult.validValue!,
364
- );
365
- if (customInvalidArgument !== undefined) {
366
- return {
367
- validValue: convertedObjectValue,
368
- invalidArgument: customInvalidArgument,
369
- };
370
- }
403
+ // custom validation is deferred until all built-in validation has passed
404
+ deferCustomValidation(deferred, propertyArg, propertyPath, validationResult.validValue!);
371
405
  }
372
406
  return {
373
407
  validValue: convertedObjectValue,
@@ -380,6 +414,7 @@ function doSubCommandArgumentValidation(
380
414
  isArray: boolean,
381
415
  isOptional: boolean,
382
416
  invalidArguments: Array<InvalidArgument>,
417
+ deferred: Array<DeferredCustomValidation> | undefined,
383
418
  ): PopulatedValueType | PopulatedValues | Array<PopulatedValues> {
384
419
  // if there is a value, check if it is valid
385
420
  if (value !== undefined) {
@@ -395,7 +430,7 @@ function doSubCommandArgumentValidation(
395
430
  });
396
431
  return undefined;
397
432
  }
398
- validationResult = validateArrayValue(argument, value);
433
+ validationResult = validateArrayValue(argument, value, argument.name, deferred);
399
434
  } else if (typeof value === "object") {
400
435
  if (!isComplexOption(argument)) {
401
436
  invalidArguments.push({
@@ -406,7 +441,7 @@ function doSubCommandArgumentValidation(
406
441
  });
407
442
  return undefined;
408
443
  }
409
- validationResult = validateObjectValue(argument, value);
444
+ validationResult = validateObjectValue(argument, value, argument.name, deferred);
410
445
  } else {
411
446
  if (isComplexOption(argument)) {
412
447
  invalidArguments.push({
@@ -424,15 +459,7 @@ function doSubCommandArgumentValidation(
424
459
  invalidArguments.push(withPrefix(validationResult.invalidArgument, argument.name));
425
460
  return undefined;
426
461
  }
427
- const customInvalidArgument = doCustomValidation(
428
- argument,
429
- argument.name,
430
- validationResult.validValue!,
431
- );
432
- if (customInvalidArgument !== undefined) {
433
- invalidArguments.push(customInvalidArgument);
434
- return undefined;
435
- }
462
+ deferCustomValidation(deferred, argument, argument.name, validationResult.validValue!);
436
463
  return validationResult.validValue as PopulatedValueType | PopulatedValues | undefined;
437
464
  }
438
465
 
@@ -453,11 +480,13 @@ function doSubCommandArgumentValidation(
453
480
  * @param option the {@link Option} to validate against.
454
481
  * @param value the value (if any) for the {@link Option}.
455
482
  * @param invalidArguments an array of {@link InvalidArgument} which may be added to if the provided value is invalid.
483
+ * @param deferred if provided, custom validations are recorded here for later invocation by {@link runCustomValidation}. If omitted, custom validations are not invoked.
456
484
  */
457
485
  export function validateOptionValue(
458
486
  option: Option | ComplexOption,
459
487
  value: PopulatedValueType | PopulatedValues | Array<PopulatedValues | undefined>,
460
488
  invalidArguments: Array<InvalidArgument>,
489
+ deferred?: Array<DeferredCustomValidation>,
461
490
  ): PopulatedValueType | PopulatedValues | Array<PopulatedValues> {
462
491
  return doSubCommandArgumentValidation(
463
492
  option,
@@ -465,6 +494,7 @@ export function validateOptionValue(
465
494
  option.isArray || false,
466
495
  option.isOptional || false,
467
496
  invalidArguments,
497
+ deferred,
468
498
  );
469
499
  }
470
500
 
@@ -474,11 +504,13 @@ export function validateOptionValue(
474
504
  * @param positional the {@link Positional} to validate against.
475
505
  * @param value the value (if any) for the {@link Positional}.
476
506
  * @param invalidArguments an array of {@link InvalidArgument} which may be added to if the provided value is invalid.
507
+ * @param deferred if provided, custom validations are recorded here for later invocation by {@link runCustomValidation}. If omitted, custom validations are not invoked.
477
508
  */
478
509
  export function validatePositionalValue(
479
510
  positional: Positional,
480
511
  value: PopulatedValueType,
481
512
  invalidArguments: Array<InvalidArgument>,
513
+ deferred?: Array<DeferredCustomValidation>,
482
514
  ): PopulatedValueType {
483
515
  return doSubCommandArgumentValidation(
484
516
  positional,
@@ -486,6 +518,7 @@ export function validatePositionalValue(
486
518
  positional.isVarargMultiple || false,
487
519
  positional.isVarargOptional || false,
488
520
  invalidArguments,
521
+ deferred,
489
522
  ) as PopulatedValueType;
490
523
  }
491
524
 
@@ -496,11 +529,13 @@ export function validatePositionalValue(
496
529
  * @param globalCommand the {@link GlobalCommand} providing the {@link GlobalCommandArgument}.
497
530
  * @param value the value (if any) for the {@link GlobalCommandArgument}.
498
531
  * @param invalidArguments an array of {@link InvalidArgument} which may be added to if the provided value is invalid.
532
+ * @param deferred if provided, custom validations are recorded here for later invocation by {@link runCustomValidation}. If omitted, custom validations are not invoked.
499
533
  */
500
534
  export function validateGlobalCommandArgumentValue(
501
535
  globalCommand: GlobalCommand,
502
536
  value: PopulatedSingleValueType,
503
537
  invalidArguments: Array<InvalidArgument>,
538
+ deferred?: Array<DeferredCustomValidation>,
504
539
  ): PopulatedSingleValueType {
505
540
  // if this function is called it is because the argument is defined
506
541
  const globalCommandArgument = globalCommand.argument!;
@@ -527,15 +562,12 @@ export function validateGlobalCommandArgumentValue(
527
562
  return undefined;
528
563
  }
529
564
 
530
- const customInvalidArgument = doCustomValidation(
565
+ deferCustomValidation(
566
+ deferred,
531
567
  globalCommandArgument,
532
568
  globalCommand.name,
533
569
  validationResult.validValue!,
534
570
  );
535
- if (customInvalidArgument !== undefined) {
536
- invalidArguments.push(customInvalidArgument);
537
- return undefined;
538
- }
539
571
  return validationResult.validValue as PopulatedSingleValueType;
540
572
  }
541
573
 
@@ -14,7 +14,9 @@ import {
14
14
  ComplexValueTypeName,
15
15
  type PopulatedSingleValueType,
16
16
  type PopulatedValues,
17
+ type Values,
17
18
  } from "@flowscripter/dynamic-cli-framework-api";
19
+ import type { Command } from "@flowscripter/dynamic-cli-framework-api";
18
20
  import type { Argument } from "@flowscripter/dynamic-cli-framework-api";
19
21
  import type { Option } from "@flowscripter/dynamic-cli-framework-api";
20
22
  import type { Positional } from "@flowscripter/dynamic-cli-framework-api";
@@ -23,6 +25,20 @@ import type { SubCommand } from "@flowscripter/dynamic-cli-framework-api";
23
25
  import type { GlobalCommand } from "@flowscripter/dynamic-cli-framework-api";
24
26
  import { isGlobalCommand, isSubCommand } from "../../runtime/command/CommandTypeGuards.ts";
25
27
 
28
+ /**
29
+ * The command and values visible to {@link Argument.validate} functions while prompting.
30
+ */
31
+ interface PromptScope {
32
+ readonly command: Command;
33
+ readonly values: PopulatedValues | PopulatedSingleValueType;
34
+ }
35
+
36
+ /**
37
+ * Used as {@link ValidationContext.command} when prompting via {@link ArgumentPrompterService.promptForArguments},
38
+ * which is not associated with a specific command.
39
+ */
40
+ const NO_COMMAND = { name: "" } as Command;
41
+
26
42
  export default class DefaultArgumentPrompterService implements ArgumentPrompterService {
27
43
  readonly #prompterService: PrompterService;
28
44
 
@@ -63,6 +79,53 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
63
79
  return parseResult;
64
80
  }
65
81
 
82
+ async promptForArguments(
83
+ args: ReadonlyArray<Option | ComplexOption>,
84
+ existing: Values = {},
85
+ ): Promise<Values | undefined> {
86
+ if (!this.#prompterService.promptEnabled) {
87
+ return undefined;
88
+ }
89
+ const prompted: Record<string, Values[string]> = {};
90
+ const merged: Record<string, Values[string]> = { ...existing };
91
+ const scope: PromptScope = { command: NO_COMMAND, values: merged as PopulatedValues };
92
+
93
+ for (const arg of args) {
94
+ if (arg.name in existing) {
95
+ continue;
96
+ }
97
+ const value = (await this.#promptForArgument(arg.name, arg, scope)) as Values[string];
98
+ prompted[arg.name] = value;
99
+ merged[arg.name] = value;
100
+ }
101
+ return prompted as Values;
102
+ }
103
+
104
+ async #promptForArgument(
105
+ name: string,
106
+ arg: Option | Positional | ComplexOption,
107
+ scope: PromptScope,
108
+ ): Promise<PopulatedValues[string]> {
109
+ if ("type" in arg && (arg as ComplexOption).type === ComplexValueTypeName.COMPLEX) {
110
+ return await this.#promptForComplexOption(name, arg as ComplexOption, scope);
111
+ }
112
+ if (DefaultArgumentPrompterService.#isArrayArgument(arg)) {
113
+ return await this.#promptForArrayArgument(name, arg as Option | Positional, scope);
114
+ }
115
+ const prompt = DefaultArgumentPrompterService.#argumentToPrompt(
116
+ name,
117
+ (arg as Option | Positional).description ?? name,
118
+ arg as Argument,
119
+ name,
120
+ scope,
121
+ );
122
+ const result = await this.#prompterService.prompt(prompt);
123
+ return DefaultArgumentPrompterService.#coerceValue(
124
+ result.value as SingleValueType,
125
+ (arg as Argument).type,
126
+ );
127
+ }
128
+
66
129
  async #promptForGlobalCommand(parseResult: ParseResult): Promise<ParseResult> {
67
130
  const command = parseResult.command as GlobalCommand;
68
131
  if (!command.argument) {
@@ -73,6 +136,8 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
73
136
  command.name,
74
137
  command.name,
75
138
  command.argument,
139
+ command.name,
140
+ { command, values: undefined },
76
141
  );
77
142
  const result = await this.#prompterService.prompt(prompt);
78
143
  const value = DefaultArgumentPrompterService.#coerceValue(
@@ -96,6 +161,7 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
96
161
  ...existingValues,
97
162
  };
98
163
  const remainingInvalid: InvalidArgument[] = [];
164
+ const scope: PromptScope = { command, values: newValues as PopulatedValues };
99
165
 
100
166
  for (const invalid of parseResult.invalidArguments) {
101
167
  if (invalid.reason !== InvalidArgumentReason.MISSING_VALUE || !invalid.name) {
@@ -109,30 +175,7 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
109
175
  continue;
110
176
  }
111
177
 
112
- if ("type" in arg && (arg as ComplexOption).type === ComplexValueTypeName.COMPLEX) {
113
- const complexValues = await this.#promptForComplexOption(
114
- invalid.name,
115
- arg as ComplexOption,
116
- );
117
- newValues[invalid.name] = complexValues;
118
- } else if (DefaultArgumentPrompterService.#isArrayArgument(arg)) {
119
- const arrayValues = await this.#promptForArrayArgument(
120
- invalid.name,
121
- arg as Option | Positional,
122
- );
123
- newValues[invalid.name] = arrayValues;
124
- } else {
125
- const prompt = DefaultArgumentPrompterService.#argumentToPrompt(
126
- invalid.name,
127
- (arg as Option | Positional).description ?? invalid.name,
128
- arg as Argument,
129
- );
130
- const result = await this.#prompterService.prompt(prompt);
131
- newValues[invalid.name] = DefaultArgumentPrompterService.#coerceValue(
132
- result.value as SingleValueType,
133
- (arg as Argument).type,
134
- );
135
- }
178
+ newValues[invalid.name] = await this.#promptForArgument(invalid.name, arg, scope);
136
179
  }
137
180
 
138
181
  return {
@@ -147,6 +190,7 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
147
190
  async #promptForComplexOption(
148
191
  name: string,
149
192
  complexOption: ComplexOption,
193
+ scope: PromptScope,
150
194
  ): Promise<PopulatedValues> {
151
195
  const values: Record<string, PopulatedValues[string]> = {};
152
196
 
@@ -155,6 +199,7 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
155
199
  values[(prop as ComplexOption).name] = await this.#promptForComplexOption(
156
200
  `${name}.${(prop as ComplexOption).name}`,
157
201
  prop as ComplexOption,
202
+ scope,
158
203
  );
159
204
  } else {
160
205
  const option = prop as Option;
@@ -165,6 +210,8 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
165
210
  `${name}.${option.name}`,
166
211
  option.description ?? `${name}.${option.name}`,
167
212
  option,
213
+ `${name}.${option.name}`,
214
+ scope,
168
215
  );
169
216
  const result = await this.#prompterService.prompt(prompt);
170
217
  values[option.name] = DefaultArgumentPrompterService.#coerceValue(
@@ -180,6 +227,7 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
180
227
  async #promptForArrayArgument(
181
228
  name: string,
182
229
  arg: Option | Positional,
230
+ scope: PromptScope,
183
231
  ): Promise<Array<SingleValueType>> {
184
232
  const values: SingleValueType[] = [];
185
233
 
@@ -188,6 +236,8 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
188
236
  name,
189
237
  `${arg.description ?? name}${values.length > 0 ? ` [${values.length + 1}]` : ""}`,
190
238
  arg as Argument,
239
+ name,
240
+ scope,
191
241
  );
192
242
  const result = await this.#prompterService.prompt(prompt);
193
243
  values.push(
@@ -212,7 +262,13 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
212
262
  return values;
213
263
  }
214
264
 
215
- static #argumentToPrompt(name: string, description: string, arg: Argument): Prompt {
265
+ static #argumentToPrompt(
266
+ name: string,
267
+ description: string,
268
+ arg: Argument,
269
+ path: string,
270
+ scope: PromptScope,
271
+ ): Prompt {
216
272
  if (arg.type === ValueTypeName.BOOLEAN) {
217
273
  return {
218
274
  name,
@@ -240,7 +296,17 @@ export default class DefaultArgumentPrompterService implements ArgumentPrompterS
240
296
  returnedValue: "",
241
297
  min: arg.minValueInclusive,
242
298
  max: arg.maxValueInclusive,
243
- validate: arg.validate ? (v) => arg.validate!(v) : undefined,
299
+ validate: arg.validate
300
+ ? (v) => {
301
+ const value = DefaultArgumentPrompterService.#coerceValue(v, arg.type);
302
+ return arg.validate!(value, {
303
+ argument: arg,
304
+ path,
305
+ values: scope.values === undefined ? value : scope.values,
306
+ command: scope.command,
307
+ });
308
+ }
309
+ : undefined,
244
310
  };
245
311
 
246
312
  return {
@@ -226,6 +226,7 @@ export default class KeyValueServiceProvider implements ServiceProvider {
226
226
  const scopedKeyValueService = this.getScopedKeyValueService(scopeType, scopeKey);
227
227
  return {
228
228
  cliConfig: context.cliConfig,
229
+ isTty: context.isTty,
229
230
  getServiceById: (id: string): unknown =>
230
231
  id === KEY_VALUE_SERVICE_ID ? scopedKeyValueService : context.getServiceById(id),
231
232
  doesServiceExist: (id: string): boolean => context.doesServiceExist(id),
@@ -3,7 +3,7 @@ import LogLevelCommand from "./command/LogLevelCommand.ts";
3
3
  import type { ServiceInfo, ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
5
5
  import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
- import DarkModeCommand from "./command/DarkModeCommand.ts";
6
+ import ThemeCommand from "./command/ThemeCommand.ts";
7
7
  import NoColorCommand from "./command/NoColorCommand.ts";
8
8
  import NoHyperlinksCommand from "./command/NoHyperlinksCommand.ts";
9
9
  import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
@@ -18,16 +18,23 @@ export default class PrinterServiceProvider implements ServiceProvider {
18
18
  readonly serviceId: string = PRINTER_SERVICE_ID;
19
19
  readonly servicePriority: number;
20
20
  readonly printerService: PrinterService;
21
+ readonly detectDarkBackground: (() => Promise<boolean | undefined>) | undefined;
21
22
 
22
23
  /**
23
24
  * Create an instance of the service provider with the specified details.
24
25
  *
25
26
  * @param servicePriority the priority of the service.
26
27
  * @param printerService the PrinterService implementation to use.
28
+ * @param detectDarkBackground optional function detecting whether the terminal background is dark.
27
29
  */
28
- public constructor(servicePriority: number, printerService: PrinterService) {
30
+ public constructor(
31
+ servicePriority: number,
32
+ printerService: PrinterService,
33
+ detectDarkBackground?: () => Promise<boolean | undefined>,
34
+ ) {
29
35
  this.servicePriority = servicePriority;
30
36
  this.printerService = printerService;
37
+ this.detectDarkBackground = detectDarkBackground;
31
38
  }
32
39
 
33
40
  public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
@@ -35,8 +42,8 @@ export default class PrinterServiceProvider implements ServiceProvider {
35
42
  service: this.printerService,
36
43
  commands: [
37
44
  new NoHyperlinksCommand(this.servicePriority + 3),
38
- new DarkModeCommand(this.servicePriority + 2),
39
- new NoColorCommand(this.servicePriority + 1),
45
+ new ThemeCommand(this.servicePriority + 1, this.detectDarkBackground),
46
+ new NoColorCommand(this.servicePriority + 2),
40
47
  new LogLevelCommand(this.servicePriority),
41
48
  ],
42
49
  });