@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
@@ -2,6 +2,11 @@ import { PromptType, } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import { InvalidArgumentReason } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import { ValueTypeName, ComplexValueTypeName, } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import { isGlobalCommand, isSubCommand } from "../../runtime/command/CommandTypeGuards.js";
5
+ /**
6
+ * Used as {@link ValidationContext.command} when prompting via {@link ArgumentPrompterService.promptForArguments},
7
+ * which is not associated with a specific command.
8
+ */
9
+ const NO_COMMAND = { name: "" };
5
10
  export default class DefaultArgumentPrompterService {
6
11
  #prompterService;
7
12
  constructor(prompterService) {
@@ -34,12 +39,40 @@ export default class DefaultArgumentPrompterService {
34
39
  }
35
40
  return parseResult;
36
41
  }
42
+ async promptForArguments(args, existing = {}) {
43
+ if (!this.#prompterService.promptEnabled) {
44
+ return undefined;
45
+ }
46
+ const prompted = {};
47
+ const merged = { ...existing };
48
+ const scope = { command: NO_COMMAND, values: merged };
49
+ for (const arg of args) {
50
+ if (arg.name in existing) {
51
+ continue;
52
+ }
53
+ const value = (await this.#promptForArgument(arg.name, arg, scope));
54
+ prompted[arg.name] = value;
55
+ merged[arg.name] = value;
56
+ }
57
+ return prompted;
58
+ }
59
+ async #promptForArgument(name, arg, scope) {
60
+ if ("type" in arg && arg.type === ComplexValueTypeName.COMPLEX) {
61
+ return await this.#promptForComplexOption(name, arg, scope);
62
+ }
63
+ if (DefaultArgumentPrompterService.#isArrayArgument(arg)) {
64
+ return await this.#promptForArrayArgument(name, arg, scope);
65
+ }
66
+ const prompt = DefaultArgumentPrompterService.#argumentToPrompt(name, arg.description ?? name, arg, name, scope);
67
+ const result = await this.#prompterService.prompt(prompt);
68
+ return DefaultArgumentPrompterService.#coerceValue(result.value, arg.type);
69
+ }
37
70
  async #promptForGlobalCommand(parseResult) {
38
71
  const command = parseResult.command;
39
72
  if (!command.argument) {
40
73
  return parseResult;
41
74
  }
42
- const prompt = DefaultArgumentPrompterService.#argumentToPrompt(command.name, command.name, command.argument);
75
+ const prompt = DefaultArgumentPrompterService.#argumentToPrompt(command.name, command.name, command.argument, command.name, { command, values: undefined });
43
76
  const result = await this.#prompterService.prompt(prompt);
44
77
  const value = DefaultArgumentPrompterService.#coerceValue(result.value, command.argument.type);
45
78
  return {
@@ -57,6 +90,7 @@ export default class DefaultArgumentPrompterService {
57
90
  ...existingValues,
58
91
  };
59
92
  const remainingInvalid = [];
93
+ const scope = { command, values: newValues };
60
94
  for (const invalid of parseResult.invalidArguments) {
61
95
  if (invalid.reason !== InvalidArgumentReason.MISSING_VALUE || !invalid.name) {
62
96
  remainingInvalid.push(invalid);
@@ -67,19 +101,7 @@ export default class DefaultArgumentPrompterService {
67
101
  remainingInvalid.push(invalid);
68
102
  continue;
69
103
  }
70
- if ("type" in arg && arg.type === ComplexValueTypeName.COMPLEX) {
71
- const complexValues = await this.#promptForComplexOption(invalid.name, arg);
72
- newValues[invalid.name] = complexValues;
73
- }
74
- else if (DefaultArgumentPrompterService.#isArrayArgument(arg)) {
75
- const arrayValues = await this.#promptForArrayArgument(invalid.name, arg);
76
- newValues[invalid.name] = arrayValues;
77
- }
78
- else {
79
- const prompt = DefaultArgumentPrompterService.#argumentToPrompt(invalid.name, arg.description ?? invalid.name, arg);
80
- const result = await this.#prompterService.prompt(prompt);
81
- newValues[invalid.name] = DefaultArgumentPrompterService.#coerceValue(result.value, arg.type);
82
- }
104
+ newValues[invalid.name] = await this.#promptForArgument(invalid.name, arg, scope);
83
105
  }
84
106
  return {
85
107
  command: parseResult.command,
@@ -89,28 +111,28 @@ export default class DefaultArgumentPrompterService {
89
111
  unusedArgs: parseResult.unusedArgs,
90
112
  };
91
113
  }
92
- async #promptForComplexOption(name, complexOption) {
114
+ async #promptForComplexOption(name, complexOption, scope) {
93
115
  const values = {};
94
116
  for (const prop of complexOption.properties) {
95
117
  if ("type" in prop && prop.type === ComplexValueTypeName.COMPLEX) {
96
- values[prop.name] = await this.#promptForComplexOption(`${name}.${prop.name}`, prop);
118
+ values[prop.name] = await this.#promptForComplexOption(`${name}.${prop.name}`, prop, scope);
97
119
  }
98
120
  else {
99
121
  const option = prop;
100
122
  if (option.isOptional && option.defaultValue !== undefined) {
101
123
  continue;
102
124
  }
103
- const prompt = DefaultArgumentPrompterService.#argumentToPrompt(`${name}.${option.name}`, option.description ?? `${name}.${option.name}`, option);
125
+ const prompt = DefaultArgumentPrompterService.#argumentToPrompt(`${name}.${option.name}`, option.description ?? `${name}.${option.name}`, option, `${name}.${option.name}`, scope);
104
126
  const result = await this.#prompterService.prompt(prompt);
105
127
  values[option.name] = DefaultArgumentPrompterService.#coerceValue(result.value, option.type);
106
128
  }
107
129
  }
108
130
  return values;
109
131
  }
110
- async #promptForArrayArgument(name, arg) {
132
+ async #promptForArrayArgument(name, arg, scope) {
111
133
  const values = [];
112
134
  while (true) {
113
- const prompt = DefaultArgumentPrompterService.#argumentToPrompt(name, `${arg.description ?? name}${values.length > 0 ? ` [${values.length + 1}]` : ""}`, arg);
135
+ const prompt = DefaultArgumentPrompterService.#argumentToPrompt(name, `${arg.description ?? name}${values.length > 0 ? ` [${values.length + 1}]` : ""}`, arg, name, scope);
114
136
  const result = await this.#prompterService.prompt(prompt);
115
137
  values.push(DefaultArgumentPrompterService.#coerceValue(result.value, arg.type));
116
138
  const addAnother = {
@@ -126,7 +148,7 @@ export default class DefaultArgumentPrompterService {
126
148
  }
127
149
  return values;
128
150
  }
129
- static #argumentToPrompt(name, description, arg) {
151
+ static #argumentToPrompt(name, description, arg, path, scope) {
130
152
  if (arg.type === ValueTypeName.BOOLEAN) {
131
153
  return {
132
154
  name,
@@ -152,7 +174,17 @@ export default class DefaultArgumentPrompterService {
152
174
  returnedValue: "",
153
175
  min: arg.minValueInclusive,
154
176
  max: arg.maxValueInclusive,
155
- validate: arg.validate ? (v) => arg.validate(v) : undefined,
177
+ validate: arg.validate
178
+ ? (v) => {
179
+ const value = DefaultArgumentPrompterService.#coerceValue(v, arg.type);
180
+ return arg.validate(value, {
181
+ argument: arg,
182
+ path,
183
+ values: scope.values === undefined ? value : scope.values,
184
+ command: scope.command,
185
+ });
186
+ }
187
+ : undefined,
156
188
  };
157
189
  return {
158
190
  name,
@@ -1 +1 @@
1
- {"version":3,"file":"KeyValueServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/KeyValueServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,yCAAyC,CAAC;AAIjD,OAAO,KAAK,4BAA4B,MAAM,mCAAmC,CAAC;AAClF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAoClF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,CAAC,OAAO,OAAO,uBAAwB,YAAW,eAAe;;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAwB;IAClD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAIjC,SAAgB,sBAAsB,EAAE,OAAO,CAAC;IAChD,SAAgB,oBAAoB,EAAE,OAAO,CAAC;IAW9C;;;;;;;;OAQG;IACH,YACE,eAAe,EAAE,MAAM,EACvB,4BAA4B,EAAE,4BAA4B,EAC1D,sBAAsB,UAAQ,EAC9B,oBAAoB,UAAQ,EAY7B;IAEM,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAUhE;IAED;;;;;;;;;;OAUG;IACI,wBAAwB,CAC7B,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,eAAe,CAwBjB;IAED;;;;;;;;;;;;;;OAcG;IACI,kBAAkB,CACvB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAaT;IAqBY,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CASxD;CACF"}
1
+ {"version":3,"file":"KeyValueServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/KeyValueServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,yCAAyC,CAAC;AAIjD,OAAO,KAAK,4BAA4B,MAAM,mCAAmC,CAAC;AAClF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAoClF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,MAAM,CAAC,OAAO,OAAO,uBAAwB,YAAW,eAAe;;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAwB;IAClD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAIjC,SAAgB,sBAAsB,EAAE,OAAO,CAAC;IAChD,SAAgB,oBAAoB,EAAE,OAAO,CAAC;IAW9C;;;;;;;;OAQG;IACH,YACE,eAAe,EAAE,MAAM,EACvB,4BAA4B,EAAE,4BAA4B,EAC1D,sBAAsB,UAAQ,EAC9B,oBAAoB,UAAQ,EAY7B;IAEM,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAUhE;IAED;;;;;;;;;;OAUG;IACI,wBAAwB,CAC7B,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,eAAe,CAwBjB;IAED;;;;;;;;;;;;;;OAcG;IACI,kBAAkB,CACvB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAcT;IAqBY,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CASxD;CACF"}
@@ -180,6 +180,7 @@ export default class KeyValueServiceProvider {
180
180
  const scopedKeyValueService = this.getScopedKeyValueService(scopeType, scopeKey);
181
181
  return {
182
182
  cliConfig: context.cliConfig,
183
+ isTty: context.isTty,
183
184
  getServiceById: (id) => id === KEY_VALUE_SERVICE_ID ? scopedKeyValueService : context.getServiceById(id),
184
185
  doesServiceExist: (id) => context.doesServiceExist(id),
185
186
  addServiceInstance: (id, serviceInstance) => context.addServiceInstance(id, serviceInstance),
@@ -9,13 +9,15 @@ export default class PrinterServiceProvider implements ServiceProvider {
9
9
  readonly serviceId: string;
10
10
  readonly servicePriority: number;
11
11
  readonly printerService: PrinterService;
12
+ readonly detectDarkBackground: (() => Promise<boolean | undefined>) | undefined;
12
13
  /**
13
14
  * Create an instance of the service provider with the specified details.
14
15
  *
15
16
  * @param servicePriority the priority of the service.
16
17
  * @param printerService the PrinterService implementation to use.
18
+ * @param detectDarkBackground optional function detecting whether the terminal background is dark.
17
19
  */
18
- constructor(servicePriority: number, printerService: PrinterService);
20
+ constructor(servicePriority: number, printerService: PrinterService, detectDarkBackground?: () => Promise<boolean | undefined>);
19
21
  getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo>;
20
22
  initService(context: Context): Promise<void>;
21
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PrinterServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/printer/PrinterServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAK9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKzE;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IAExC;;;;;OAKG;IACH,YAAmB,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAGzE;IAEM,cAAc,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAUjE;IAED,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3C;CACF"}
1
+ {"version":3,"file":"PrinterServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/printer/PrinterServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAK9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKzE;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,oBAAoB,EAAE,CAAC,MAAM,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;IAEhF;;;;;;OAMG;IACH,YACE,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,cAAc,EAC9B,oBAAoB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,EAK1D;IAEM,cAAc,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAUjE;IAED,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3C;CACF"}
@@ -1,6 +1,6 @@
1
1
  import LogLevelCommand from "./command/LogLevelCommand.js";
2
2
  import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
3
- import DarkModeCommand from "./command/DarkModeCommand.js";
3
+ import ThemeCommand from "./command/ThemeCommand.js";
4
4
  import NoColorCommand from "./command/NoColorCommand.js";
5
5
  import NoHyperlinksCommand from "./command/NoHyperlinksCommand.js";
6
6
  import { SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
@@ -12,23 +12,26 @@ export default class PrinterServiceProvider {
12
12
  serviceId = PRINTER_SERVICE_ID;
13
13
  servicePriority;
14
14
  printerService;
15
+ detectDarkBackground;
15
16
  /**
16
17
  * Create an instance of the service provider with the specified details.
17
18
  *
18
19
  * @param servicePriority the priority of the service.
19
20
  * @param printerService the PrinterService implementation to use.
21
+ * @param detectDarkBackground optional function detecting whether the terminal background is dark.
20
22
  */
21
- constructor(servicePriority, printerService) {
23
+ constructor(servicePriority, printerService, detectDarkBackground) {
22
24
  this.servicePriority = servicePriority;
23
25
  this.printerService = printerService;
26
+ this.detectDarkBackground = detectDarkBackground;
24
27
  }
25
28
  getServiceInfo(_cliConfig) {
26
29
  return Promise.resolve({
27
30
  service: this.printerService,
28
31
  commands: [
29
32
  new NoHyperlinksCommand(this.servicePriority + 3),
30
- new DarkModeCommand(this.servicePriority + 2),
31
- new NoColorCommand(this.servicePriority + 1),
33
+ new ThemeCommand(this.servicePriority + 1, this.detectDarkBackground),
34
+ new NoColorCommand(this.servicePriority + 2),
32
35
  new LogLevelCommand(this.servicePriority),
33
36
  ],
34
37
  });
@@ -0,0 +1,24 @@
1
+ import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-api";
2
+ import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
+ import { type SingleValueType } from "@flowscripter/dynamic-cli-framework-api";
4
+ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
+ /**
6
+ * Command allowing the specification of the dark/light theme for the {@link PrinterService}.
7
+ *
8
+ * With the `auto` value the terminal background is detected and light is used if it cannot be determined.
9
+ */
10
+ export default class ThemeCommand implements GlobalModifierCommand {
11
+ #private;
12
+ readonly name = "theme";
13
+ readonly description = "Theme for output";
14
+ readonly enableConfiguration = true;
15
+ readonly argument: GlobalCommandArgument;
16
+ readonly executePriority: number;
17
+ /**
18
+ * @param executePriority the priority of the command.
19
+ * @param detect optional function detecting whether the terminal background is dark.
20
+ */
21
+ constructor(executePriority: number, detect?: () => Promise<boolean | undefined>);
22
+ execute(context: Context, argumentValue: SingleValueType): Promise<void>;
23
+ }
24
+ //# sourceMappingURL=ThemeCommand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/printer/command/ThemeCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,EAAE,KAAK,eAAe,EAAiB,MAAM,yCAAyC,CAAC;AAC9F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAIvE;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,YAAa,YAAW,qBAAqB;;IAChE,QAAQ,CAAC,IAAI,WAAW;IACxB,QAAQ,CAAC,WAAW,sBAAsB;IAC1C,QAAQ,CAAC,mBAAmB,QAAQ;IACpC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAKtC;IACF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAGjC;;;OAGG;IACH,YAAmB,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,EAGtF;IAEY,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAWpF;CACF"}
@@ -0,0 +1,40 @@
1
+ import { ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
3
+ /**
4
+ * Command allowing the specification of the dark/light theme for the {@link PrinterService}.
5
+ *
6
+ * With the `auto` value the terminal background is detected and light is used if it cannot be determined.
7
+ */
8
+ export default class ThemeCommand {
9
+ name = "theme";
10
+ description = "Theme for output";
11
+ enableConfiguration = true;
12
+ argument = {
13
+ type: ValueTypeName.STRING,
14
+ defaultValue: "auto",
15
+ allowableValues: ["auto", "dark", "light"],
16
+ configurationKey: "THEME",
17
+ };
18
+ executePriority;
19
+ #detect;
20
+ /**
21
+ * @param executePriority the priority of the command.
22
+ * @param detect optional function detecting whether the terminal background is dark.
23
+ */
24
+ constructor(executePriority, detect) {
25
+ this.executePriority = executePriority;
26
+ this.#detect = detect;
27
+ }
28
+ async execute(context, argumentValue) {
29
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID);
30
+ if (argumentValue === "auto") {
31
+ printerService.darkMode =
32
+ printerService.colorEnabled && this.#detect !== undefined
33
+ ? ((await this.#detect()) ?? false)
34
+ : false;
35
+ }
36
+ else {
37
+ printerService.darkMode = argumentValue === "dark";
38
+ }
39
+ }
40
+ }
@@ -1,3 +1,5 @@
1
+ export declare const ESC = "\u001B";
2
+ export declare const BEL = "\u0007";
1
3
  export declare const CLEAR_LINE: string;
2
4
  export declare const CURSOR_LEFT: string;
3
5
  export declare const CURSOR_UP: string;
@@ -7,6 +9,8 @@ export declare const ITALIC_START: string;
7
9
  export declare const ITALIC_END: string;
8
10
  export declare const FOREGROUND_COLOR_END: string;
9
11
  export declare const BACKGROUND_COLOR_END: string;
12
+ export declare const QUERY_BACKGROUND_COLOR = "\u001B]11;?\u001B\\";
13
+ export declare const QUERY_DEVICE_ATTRIBUTES: string;
10
14
  export declare const HYPERLINK_END = "\u001B]8;;\u0007";
11
15
  export declare function foregroundColorStart(r: number, g: number, b: number): string;
12
16
  export declare function backgroundColorStart(r: number, g: number, b: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"Ansi.d.ts","sourceRoot":"","sources":["../../../src/terminal/Ansi.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,QAAa,CAAC;AACrC,eAAO,MAAM,WAAW,QAAY,CAAC;AACrC,eAAO,MAAM,SAAS,QAAa,CAAC;AACpC,eAAO,MAAM,WAAW,QAAe,CAAC;AACxC,eAAO,MAAM,WAAW,QAAe,CAAC;AACxC,eAAO,MAAM,YAAY,QAAa,CAAC;AACvC,eAAO,MAAM,UAAU,QAAc,CAAC;AACtC,eAAO,MAAM,oBAAoB,QAAc,CAAC;AAChD,eAAO,MAAM,oBAAoB,QAAc,CAAC;AAChD,eAAO,MAAM,aAAa,qBAA8B,CAAC;AAEzD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}
1
+ {"version":3,"file":"Ansi.d.ts","sourceRoot":"","sources":["../../../src/terminal/Ansi.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,GAAG,WAAS,CAAC;AAC1B,eAAO,MAAM,GAAG,WAAS,CAAC;AAM1B,eAAO,MAAM,UAAU,QAAa,CAAC;AACrC,eAAO,MAAM,WAAW,QAAY,CAAC;AACrC,eAAO,MAAM,SAAS,QAAa,CAAC;AACpC,eAAO,MAAM,WAAW,QAAe,CAAC;AACxC,eAAO,MAAM,WAAW,QAAe,CAAC;AACxC,eAAO,MAAM,YAAY,QAAa,CAAC;AACvC,eAAO,MAAM,UAAU,QAAc,CAAC;AACtC,eAAO,MAAM,oBAAoB,QAAc,CAAC;AAChD,eAAO,MAAM,oBAAoB,QAAc,CAAC;AAChD,eAAO,MAAM,sBAAsB,wBAAoB,CAAC;AACxD,eAAO,MAAM,uBAAuB,QAAY,CAAC;AACjD,eAAO,MAAM,aAAa,qBAA8B,CAAC;AAEzD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}
@@ -1,6 +1,8 @@
1
- const CSI = "\x1b[";
2
- const OSC = "\x1b]";
3
- const BEL = "\x07";
1
+ export const ESC = "\x1b";
2
+ export const BEL = "\x07";
3
+ const CSI = ESC + "[";
4
+ const OSC = ESC + "]";
5
+ const ST = ESC + "\\";
4
6
  const SEP = ";";
5
7
  export const CLEAR_LINE = CSI + "2K";
6
8
  export const CURSOR_LEFT = CSI + "G";
@@ -11,6 +13,8 @@ export const ITALIC_START = CSI + "3m";
11
13
  export const ITALIC_END = CSI + "23m";
12
14
  export const FOREGROUND_COLOR_END = CSI + "39m";
13
15
  export const BACKGROUND_COLOR_END = CSI + "49m";
16
+ export const QUERY_BACKGROUND_COLOR = `${OSC}11;?${ST}`;
17
+ export const QUERY_DEVICE_ATTRIBUTES = CSI + "c";
14
18
  export const HYPERLINK_END = `${OSC}8${SEP}${SEP}${BEL}`;
15
19
  export function foregroundColorStart(r, g, b) {
16
20
  return `\x1b[38;2;${r};${g};${b}m`;
@@ -0,0 +1,25 @@
1
+ import type KeyReader from "./KeyReader.ts";
2
+ import type Terminal from "./Terminal.ts";
3
+ export declare const DEFAULT_DETECTION_TIMEOUT_MS = 150;
4
+ /**
5
+ * Parse the COLORFGBG environment variable value into a dark background flag.
6
+ *
7
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
8
+ */
9
+ export declare function parseColorFgBg(value: string | undefined): boolean | undefined;
10
+ /**
11
+ * Parse an OSC 11 background colour reply into a dark background flag.
12
+ *
13
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if no reply is present.
14
+ */
15
+ export declare function parseOsc11Reply(data: string): boolean | undefined;
16
+ /**
17
+ * Detect whether the terminal background is dark.
18
+ *
19
+ * Checks COLORFGBG first and then queries the terminal for its background colour (OSC 11).
20
+ * The query is skipped unless the terminal is a TTY and is bounded by a timeout.
21
+ *
22
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
23
+ */
24
+ export declare function detectDarkBackground(keyReader: KeyReader, terminal: Terminal, env?: Record<string, string | undefined>, timeoutMs?: number): Promise<boolean | undefined>;
25
+ //# sourceMappingURL=BackgroundDetector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BackgroundDetector.d.ts","sourceRoot":"","sources":["../../../src/terminal/BackgroundDetector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAQ1C,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAc7E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CASjE;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,QAAQ,EAClB,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,EACrD,SAAS,GAAE,MAAqC,GAC/C,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAe9B"}
@@ -0,0 +1,63 @@
1
+ import process from "node:process";
2
+ import { BEL, ESC, QUERY_BACKGROUND_COLOR, QUERY_DEVICE_ATTRIBUTES } from "./Ansi.js";
3
+ const OSC_11_REPLY = new RegExp(`${ESC}\\]11;rgb:([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})(?:${BEL}|${ESC}\\\\)`);
4
+ const DA1_REPLY = new RegExp(`${ESC}\\[\\?[0-9;]*c`);
5
+ export const DEFAULT_DETECTION_TIMEOUT_MS = 150;
6
+ /**
7
+ * Parse the COLORFGBG environment variable value into a dark background flag.
8
+ *
9
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
10
+ */
11
+ export function parseColorFgBg(value) {
12
+ if (value === undefined) {
13
+ return undefined;
14
+ }
15
+ const parts = value.split(";");
16
+ const background = parts[parts.length - 1];
17
+ if (!/^\d{1,2}$/.test(background)) {
18
+ return undefined;
19
+ }
20
+ const index = Number(background);
21
+ if (index > 15) {
22
+ return undefined;
23
+ }
24
+ return index <= 6 || index === 8;
25
+ }
26
+ /**
27
+ * Parse an OSC 11 background colour reply into a dark background flag.
28
+ *
29
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if no reply is present.
30
+ */
31
+ export function parseOsc11Reply(data) {
32
+ const match = OSC_11_REPLY.exec(data);
33
+ if (match === null) {
34
+ return undefined;
35
+ }
36
+ const [r, g, b] = [match[1], match[2], match[3]].map((channel) => parseInt(channel, 16) / (16 ** channel.length - 1));
37
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.5;
38
+ }
39
+ /**
40
+ * Detect whether the terminal background is dark.
41
+ *
42
+ * Checks COLORFGBG first and then queries the terminal for its background colour (OSC 11).
43
+ * The query is skipped unless the terminal is a TTY and is bounded by a timeout.
44
+ *
45
+ * @returns `true` for a dark background, `false` for a light background or `undefined` if not determinable.
46
+ */
47
+ export async function detectDarkBackground(keyReader, terminal, env = process.env, timeoutMs = DEFAULT_DETECTION_TIMEOUT_MS) {
48
+ if (!terminal.isTty() || env.TERM === "dumb") {
49
+ return undefined;
50
+ }
51
+ const fromEnv = parseColorFgBg(env.COLORFGBG);
52
+ if (fromEnv !== undefined) {
53
+ return fromEnv;
54
+ }
55
+ try {
56
+ const reply = keyReader.readRaw((received) => DA1_REPLY.test(received), timeoutMs);
57
+ await terminal.write(QUERY_BACKGROUND_COLOR + QUERY_DEVICE_ATTRIBUTES);
58
+ return parseOsc11Reply(await reply);
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
@@ -18,5 +18,12 @@ export default interface KeyReader {
18
18
  enableRawMode(): void;
19
19
  disableRawMode(): void;
20
20
  readKey(): Promise<KeyEvent>;
21
+ /**
22
+ * Read raw input until `isComplete` returns `true` for the accumulated input or `timeoutMs` elapses.
23
+ * Raw mode and paused state are restored afterwards.
24
+ *
25
+ * @returns the accumulated input.
26
+ */
27
+ readRaw(isComplete: (received: string) => boolean, timeoutMs: number): Promise<string>;
21
28
  }
22
29
  //# sourceMappingURL=KeyReader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"KeyReader.d.ts","sourceRoot":"","sources":["../../../src/terminal/KeyReader.ts"],"names":[],"mappings":"AAAA,oBAAY,UAAU;IACpB,EAAE,IAAI;IACN,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;IACT,KAAK,IAAI;IACT,KAAK,IAAI;IACT,SAAS,IAAI;IACb,MAAM,IAAI;IACV,GAAG,IAAI;IACP,SAAS,IAAI;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAClC;AAED,MAAM,CAAC,OAAO,WAAW,SAAS;IAChC,aAAa,IAAI,IAAI,CAAC;IACtB,cAAc,IAAI,IAAI,CAAC;IACvB,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9B"}
1
+ {"version":3,"file":"KeyReader.d.ts","sourceRoot":"","sources":["../../../src/terminal/KeyReader.ts"],"names":[],"mappings":"AAAA,oBAAY,UAAU;IACpB,EAAE,IAAI;IACN,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;IACT,KAAK,IAAI;IACT,KAAK,IAAI;IACT,SAAS,IAAI;IACb,MAAM,IAAI;IACV,GAAG,IAAI;IACP,SAAS,IAAI;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAClC;AAED,MAAM,CAAC,OAAO,WAAW,SAAS;IAChC,aAAa,IAAI,IAAI,CAAC;IACtB,cAAc,IAAI,IAAI,CAAC;IACvB,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B;;;;;OAKG;IACH,OAAO,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACxF"}
@@ -7,5 +7,6 @@ export default class TtyKeyReader implements KeyReader {
7
7
  enableRawMode(): void;
8
8
  disableRawMode(): void;
9
9
  readKey(): Promise<KeyEvent>;
10
+ readRaw(isComplete: (received: string) => boolean, timeoutMs: number): Promise<string>;
10
11
  }
11
12
  //# sourceMappingURL=TtyKeyReader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TtyKeyReader.d.ts","sourceRoot":"","sources":["../../../src/terminal/TtyKeyReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,KAAK,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAE3D,MAAM,CAAC,OAAO,OAAO,YAAa,YAAW,SAAS;;IAGpD,YAAY,aAAa,EAAE,UAAU,EAKpC;IAED,aAAa,IAAI,IAAI,CAGpB;IAED,cAAc,IAAI,IAAI,CAGrB;IAED,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CA2B3B;CAiDF"}
1
+ {"version":3,"file":"TtyKeyReader.d.ts","sourceRoot":"","sources":["../../../src/terminal/TtyKeyReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,KAAK,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAE3D,MAAM,CAAC,OAAO,OAAO,YAAa,YAAW,SAAS;;IAGpD,YAAY,aAAa,EAAE,UAAU,EAKpC;IAED,aAAa,IAAI,IAAI,CAGpB;IAED,cAAc,IAAI,IAAI,CAGrB;IAED,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CA2B3B;IAED,OAAO,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCrF;CAiDF"}
@@ -41,6 +41,36 @@ export default class TtyKeyReader {
41
41
  this.#ttyReadStream.on("data", onData);
42
42
  });
43
43
  }
44
+ readRaw(isComplete, timeoutMs) {
45
+ const stream = this.#ttyReadStream;
46
+ const wasRaw = stream.isRaw === true;
47
+ const wasPaused = stream.isPaused();
48
+ return new Promise((resolve) => {
49
+ let received = "";
50
+ let timer;
51
+ const finish = () => {
52
+ clearTimeout(timer);
53
+ stream.removeListener("data", onData);
54
+ if (!wasRaw) {
55
+ stream.setRawMode(false);
56
+ }
57
+ if (wasPaused) {
58
+ stream.pause();
59
+ }
60
+ resolve(received);
61
+ };
62
+ const onData = (data) => {
63
+ received += data.toString();
64
+ if (isComplete(received)) {
65
+ finish();
66
+ }
67
+ };
68
+ stream.setRawMode(true);
69
+ stream.on("data", onData);
70
+ stream.resume();
71
+ timer = setTimeout(finish, timeoutMs);
72
+ });
73
+ }
44
74
  static #decodeKeyEvent(data) {
45
75
  if (data.length === 0) {
46
76
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "11.0.0",
3
+ "version": "12.0.0",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -49,7 +49,7 @@
49
49
  "build": "tsc -p tsconfig.build.json"
50
50
  },
51
51
  "dependencies": {
52
- "@flowscripter/dynamic-cli-framework-api": "^5.0.2",
52
+ "@flowscripter/dynamic-cli-framework-api": "^5.1.0",
53
53
  "@flowscripter/dynamic-plugin-framework": "^2.2.22",
54
54
  "emphasize": "^7.0.0",
55
55
  "fastest-levenshtein": "^1.0.16",
@@ -44,6 +44,7 @@ import type Terminal from "../terminal/Terminal.ts";
44
44
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
45
45
  import DefaultPrinterService from "../service/printer/DefaultPrinterService.ts";
46
46
  import type Styler from "../terminal/Styler.ts";
47
+ import { detectDarkBackground } from "../terminal/BackgroundDetector.ts";
47
48
  import type KeyReader from "../terminal/KeyReader.ts";
48
49
  import type BaseCLIFeatureOptions from "./BaseCLIFeatureOptions.ts";
49
50
  import DefaultPrompterService, {
@@ -196,7 +197,11 @@ export default class BaseCLI implements CLI {
196
197
  this.#commandRegistry = new DefaultCommandRegistry([], commandValidator);
197
198
 
198
199
  // create a context
199
- this.#context = new DefaultContext(this.#cliConfig);
200
+ this.#context = new DefaultContext(this.#cliConfig, {
201
+ stdin: keyReader !== undefined,
202
+ stdout: stdoutTerminal.isTty(),
203
+ stderr: stderrTerminal.isTty(),
204
+ });
200
205
 
201
206
  this.#startupServiceProvider = new StartupServiceProvider(STARTUP_SERVICE_PRIORITY);
202
207
 
@@ -272,13 +277,19 @@ export default class BaseCLI implements CLI {
272
277
  // create and add core services
273
278
  this.addServiceProvider(new ShutdownServiceProvider(SHUTDOWN_SERVICE_PRIORITY));
274
279
  this.addServiceProvider(this.#startupServiceProvider);
280
+ const canPrompt = this.#context.isTty.stdin && this.#context.isTty.stderr;
281
+
275
282
  this.addServiceProvider(
276
- new PrinterServiceProvider(PRINTER_SERVICE_PRIORITY, this.#printerService),
283
+ new PrinterServiceProvider(
284
+ PRINTER_SERVICE_PRIORITY,
285
+ this.#printerService,
286
+ canPrompt && this.#keyReader !== undefined
287
+ ? () => detectDarkBackground(this.#keyReader!, this.#stderrTerminal)
288
+ : undefined,
289
+ ),
277
290
  );
278
291
  this.addServiceProvider(new TableGeneratorServiceProvider(TABLE_GENERATOR_SERVICE_PRIORITY));
279
292
 
280
- const canPrompt = this.#keyReader !== undefined && this.#stderrTerminal.isTty();
281
-
282
293
  if (this.#options.promptingEnabled && !canPrompt) {
283
294
  throw new Error("promptingEnabled requires a keyReader and a TTY stderr terminal");
284
295
  }
@@ -7,7 +7,10 @@ import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
7
7
  export default class DefaultContext implements Context {
8
8
  readonly #serviceInstancesById: Map<string, unknown> = new Map();
9
9
 
10
- public constructor(readonly cliConfig: CLIConfig) {}
10
+ public constructor(
11
+ readonly cliConfig: CLIConfig,
12
+ readonly isTty: Context["isTty"] = { stdin: false, stdout: false, stderr: false },
13
+ ) {}
11
14
 
12
15
  public addServiceInstance(id: string, serviceInstance: unknown) {
13
16
  if (this.#serviceInstancesById.has(id)) {