@flowscripter/dynamic-cli-framework 2.0.0 → 2.1.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.
package/README.md CHANGED
@@ -40,6 +40,9 @@
40
40
  configuration management, and user input prompting.
41
41
  - Graceful signal handling (SIGINT/SIGTERM) with shutdown hooks, cooperative
42
42
  cancellation for long-running commands, and triple-interrupt force exit.
43
+ - Graceful degradation when piped or run non-interactively: color,
44
+ hyperlinks, prompting, spinners/progress bars and image rendering are
45
+ automatically disabled based on which of `stdin`/`stdout`/`stderr` are TTYs.
43
46
  - Core (but optional) support for dynamic discovery and installation of commands
44
47
  and services using
45
48
  [dynamic-plugin-framework](https://github.com/flowscripter/dynamic-plugin-framework)
@@ -25,6 +25,12 @@ import type BaseCLIFeatureOptions from "./BaseCLIFeatureOptions.ts";
25
25
  *
26
26
  * * {@link ImagePrinterServiceProvider}
27
27
  *
28
+ * `keyReader` is optional. If omitted, or if the stderr {@link Terminal} is not a TTY, prompting is
29
+ * unavailable: {@link PrompterServiceProvider} (and {@link ArgumentPrompterServiceProvider}, if
30
+ * enabled) are simply not registered. If `promptingEnabled` is `true` in that situation, {@link run}
31
+ * throws rather than failing later inside a prompt. Similarly {@link ImagePrinterServiceProvider} is
32
+ * only registered if the stdout {@link Terminal} is a TTY.
33
+ *
28
34
  * By default the following commands are added:
29
35
  *
30
36
  * * {@link SingleCommandCliHelpSubCommand} or {@link MultiCommandCliHelpSubCommand}
@@ -34,7 +40,7 @@ import type BaseCLIFeatureOptions from "./BaseCLIFeatureOptions.ts";
34
40
  */
35
41
  export default class BaseCLI implements CLI {
36
42
  #private;
37
- constructor(cliConfig: CLIConfig, stdoutWritableStream: WritableStream, stderrWritableStream: WritableStream, stdoutIsColor: boolean, stderrIsColor: boolean, stdoutTerminal: Terminal, stderrTerminal: Terminal, styler: Styler, keyReader: KeyReader, options?: BaseCLIFeatureOptions);
43
+ constructor(cliConfig: CLIConfig, stdoutWritableStream: WritableStream, stderrWritableStream: WritableStream, stdoutIsColor: boolean, stderrIsColor: boolean, stdoutTerminal: Terminal, stderrTerminal: Terminal, styler: Styler, keyReader: KeyReader | undefined, options?: BaseCLIFeatureOptions);
38
44
  /**
39
45
  * Add a {@link ServiceProvider} to the CLI's {@link ServiceProviderRegistry}.
40
46
  *
@@ -1 +1 @@
1
- {"version":3,"file":"BaseCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/BaseCLI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4BzE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAC;AAYpE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,OAAO,OAAO,OAAQ,YAAW,GAAG;;gBAavC,SAAS,EAAE,SAAS,EACpB,oBAAoB,EAAE,cAAc,EACpC,oBAAoB,EAAE,cAAc,EACpC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,OAAO,EACtB,cAAc,EAAE,QAAQ,EACxB,cAAc,EAAE,QAAQ,EACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,OAAO,CAAC,EAAE,qBAAqB;IA2DjC;;;;;;;OAOG;IACI,kBAAkB,CAAC,eAAe,EAAE,eAAe;IAI1D;;;;;;;;OAQG;IACI,UAAU,CAAC,OAAO,EAAE,OAAO;IAU5B,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;CAuJ3D"}
1
+ {"version":3,"file":"BaseCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/BaseCLI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4BzE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAC;AAYpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,OAAO,OAAO,OAAQ,YAAW,GAAG;;gBAavC,SAAS,EAAE,SAAS,EACpB,oBAAoB,EAAE,cAAc,EACpC,oBAAoB,EAAE,cAAc,EACpC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,OAAO,EACtB,cAAc,EAAE,QAAQ,EACxB,cAAc,EAAE,QAAQ,EACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,qBAAqB;IA2DjC;;;;;;;OAOG;IACI,kBAAkB,CAAC,eAAe,EAAE,eAAe;IAI1D;;;;;;;;OAQG;IACI,UAAU,CAAC,OAAO,EAAE,OAAO;IAU5B,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;CAmL3D"}
@@ -43,6 +43,12 @@ const logger = getLogger("BaseCLI");
43
43
  *
44
44
  * * {@link ImagePrinterServiceProvider}
45
45
  *
46
+ * `keyReader` is optional. If omitted, or if the stderr {@link Terminal} is not a TTY, prompting is
47
+ * unavailable: {@link PrompterServiceProvider} (and {@link ArgumentPrompterServiceProvider}, if
48
+ * enabled) are simply not registered. If `promptingEnabled` is `true` in that situation, {@link run}
49
+ * throws rather than failing later inside a prompt. Similarly {@link ImagePrinterServiceProvider} is
50
+ * only registered if the stdout {@link Terminal} is a TTY.
51
+ *
46
52
  * By default the following commands are added:
47
53
  *
48
54
  * * {@link SingleCommandCliHelpSubCommand} or {@link MultiCommandCliHelpSubCommand}
@@ -145,15 +151,35 @@ export default class BaseCLI {
145
151
  this.addServiceProvider(new ShutdownServiceProvider(100));
146
152
  this.addServiceProvider(new PrinterServiceProvider(80, this.#printerService));
147
153
  this.addServiceProvider(new TableGeneratorServiceProvider(70));
148
- const prompterService = new DefaultPrompterService(DEFAULT_PROMPTER_CONFIG, this.#stderrTerminal, this.#keyReader, this.#printerService);
149
- prompterService.promptEnabled = this.#options.promptingEnabled;
150
- this.addServiceProvider(new PrompterServiceProvider(75, prompterService));
154
+ const canPrompt = this.#keyReader !== undefined && this.#stderrTerminal.isTty();
155
+ if (this.#options.promptingEnabled && !canPrompt) {
156
+ throw new Error("promptingEnabled requires a keyReader and a TTY stderr terminal");
157
+ }
158
+ let prompterService;
159
+ if (canPrompt) {
160
+ prompterService = new DefaultPrompterService(DEFAULT_PROMPTER_CONFIG, this.#stderrTerminal, this.#keyReader, this.#printerService);
161
+ prompterService.promptEnabled = this.#options.promptingEnabled;
162
+ this.addServiceProvider(new PrompterServiceProvider(75, prompterService));
163
+ }
164
+ else {
165
+ logger.debug(() => `Skipping PrompterServiceProvider: keyReader present: ${this.#keyReader !== undefined}, stderr isTty: ${this.#stderrTerminal.isTty()}`);
166
+ }
151
167
  if (this.#options.argumentPrompterServiceEnabled) {
152
- const argumentPrompterService = new DefaultArgumentPrompterService(prompterService);
153
- this.addServiceProvider(new ArgumentPrompterServiceProvider(65, argumentPrompterService));
168
+ if (prompterService !== undefined) {
169
+ const argumentPrompterService = new DefaultArgumentPrompterService(prompterService);
170
+ this.addServiceProvider(new ArgumentPrompterServiceProvider(65, argumentPrompterService));
171
+ }
172
+ else {
173
+ logger.debug("Skipping ArgumentPrompterServiceProvider: argumentPrompterServiceEnabled is true but prompting is unavailable");
174
+ }
154
175
  }
155
176
  if (this.#options.imagePrinterServiceEnabled) {
156
- this.addServiceProvider(new ImagePrinterServiceProvider(55, this.#stdoutTerminal));
177
+ if (this.#stdoutTerminal.isTty()) {
178
+ this.addServiceProvider(new ImagePrinterServiceProvider(55, this.#stdoutTerminal));
179
+ }
180
+ else {
181
+ logger.debug("Skipping ImagePrinterServiceProvider: imagePrinterServiceEnabled is true but stdout is not a TTY");
182
+ }
157
183
  }
158
184
  if (this.#options.completionServiceEnabled) {
159
185
  const completionService = new DefaultCompletionService();
@@ -4,6 +4,10 @@ import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type BaseCLIFeatureOptions from "./BaseCLIFeatureOptions.ts";
5
5
  /**
6
6
  * Default Bun implementation of a {@link CLI} using `process.stdout`, `process.stderr` and `process.argv`.
7
+ *
8
+ * Degrades gracefully when `stdin`, `stdout` or `stderr` are not TTYs (e.g. piped or redirected):
9
+ * color and hyperlinks are disabled, prompting is disabled, and non-TTY streams are wrapped in a
10
+ * {@link NonTtyTerminal} rather than the usual {@link TtyTerminal}.
7
11
  */
8
12
  export default class DefaultRuntimeCLI extends BaseCLI {
9
13
  constructor(cliConfig: CLIConfig, options?: BaseCLIFeatureOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultRuntimeCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/DefaultRuntimeCLI.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAC;AAQpE;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,OAAO;gBACxC,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,qBAAqB;IAkBlD,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC;CAIzC"}
1
+ {"version":3,"file":"DefaultRuntimeCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/DefaultRuntimeCLI.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAC;AAYpE;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,OAAO;gBACxC,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,qBAAqB;IAsDlD,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC;CAIzC"}
@@ -2,16 +2,44 @@ import process from "node:process";
2
2
  import BaseCLI from "./BaseCLI.js";
3
3
  import { Writable } from "node:stream";
4
4
  import TtyTerminal from "../terminal/TtyTerminal.js";
5
+ import NonTtyTerminal from "../terminal/NonTtyTerminal.js";
5
6
  import TtyKeyReader from "../terminal/TtyKeyReader.js";
6
7
  import TtyStyler from "../terminal/TtyStyler.js";
7
8
  import supportsColor from "supports-color";
8
9
  import supportsHyperlinks from "../terminal/supportsHyperlinks.js";
10
+ import getLogger from "../util/logger.js";
11
+ const logger = getLogger("DefaultRuntimeCLI");
9
12
  /**
10
13
  * Default Bun implementation of a {@link CLI} using `process.stdout`, `process.stderr` and `process.argv`.
14
+ *
15
+ * Degrades gracefully when `stdin`, `stdout` or `stderr` are not TTYs (e.g. piped or redirected):
16
+ * color and hyperlinks are disabled, prompting is disabled, and non-TTY streams are wrapped in a
17
+ * {@link NonTtyTerminal} rather than the usual {@link TtyTerminal}.
11
18
  */
12
19
  export default class DefaultRuntimeCLI extends BaseCLI {
13
20
  constructor(cliConfig, options) {
14
- super(cliConfig, Writable.toWeb(process.stdout), Writable.toWeb(process.stderr), supportsColor.stdout !== false, supportsColor.stderr !== false, new TtyTerminal(process.stdout), new TtyTerminal(process.stderr), new TtyStyler(supportsColor.stderr === false ? 1 : supportsColor.stderr.level, supportsHyperlinks(process.stderr)), new TtyKeyReader(process.stdin), { promptingEnabled: process.stdin.isTTY === true, ...options });
21
+ const stdinIsTty = process.stdin.isTTY === true;
22
+ const stdoutIsTty = process.stdout.isTTY === true;
23
+ const stderrIsTty = process.stderr.isTTY === true;
24
+ logger.debug(() => `stdin isTTY: ${stdinIsTty}, stdout isTTY: ${stdoutIsTty}, stderr isTTY: ${stderrIsTty}`);
25
+ const stdoutTerminal = stdoutIsTty
26
+ ? new TtyTerminal(process.stdout)
27
+ : new NonTtyTerminal(process.stdout);
28
+ const stderrTerminal = stderrIsTty
29
+ ? new TtyTerminal(process.stderr)
30
+ : new NonTtyTerminal(process.stderr);
31
+ logger.debug(() => `stdout terminal: ${stdoutIsTty ? "TtyTerminal" : "NonTtyTerminal"}, stderr terminal: ${stderrIsTty ? "TtyTerminal" : "NonTtyTerminal"}`);
32
+ const colorLevel = stderrIsTty
33
+ ? supportsColor.stderr === false
34
+ ? 1
35
+ : supportsColor.stderr.level
36
+ : 0;
37
+ logger.debug(() => `color level: ${colorLevel}`);
38
+ const keyReader = stdinIsTty ? new TtyKeyReader(process.stdin) : undefined;
39
+ logger.debug(() => `keyReader constructed: ${keyReader !== undefined}`);
40
+ const promptingEnabled = stdinIsTty && stderrIsTty;
41
+ logger.debug(() => `promptingEnabled default: ${promptingEnabled}`);
42
+ super(cliConfig, Writable.toWeb(process.stdout), Writable.toWeb(process.stderr), supportsColor.stdout !== false, supportsColor.stderr !== false, stdoutTerminal, stderrTerminal, new TtyStyler(colorLevel, supportsHyperlinks(process.stderr)), keyReader, { promptingEnabled, ...options });
15
43
  }
16
44
  async run() {
17
45
  const runResult = await super.run(process.argv.slice(2));
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultPrinterService.d.ts","sourceRoot":"","sources":["../../../../src/service/printer/DefaultPrinterService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DefaultPrinterService.d.ts","sourceRoot":"","sources":["../../../../src/service/printer/DefaultPrinterService.ts"],"names":[],"mappings":"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"}
@@ -22,16 +22,14 @@ export default class DefaultPrinterService {
22
22
  #quote;
23
23
  #markedLineCount;
24
24
  #markEndedAt;
25
- #markedTerminal;
26
25
  #countLines(message) {
27
26
  return message.split("\n").length - (message.endsWith("\n") ? 1 : 0);
28
27
  }
29
- #recordMarkedWrite(terminal, message) {
28
+ #recordMarkedWrite(message) {
30
29
  if (this.#markedLineCount === undefined) {
31
30
  return;
32
31
  }
33
32
  this.#markedLineCount += this.#countLines(message);
34
- this.#markedTerminal = terminal;
35
33
  }
36
34
  async #log(level, message, icon) {
37
35
  if (this.#threshold > level) {
@@ -40,7 +38,7 @@ export default class DefaultPrinterService {
40
38
  await this.#spinner.pause();
41
39
  await this.#progress.pause();
42
40
  const composed = `${icon !== undefined ? `${this.#iconDefinitions[icon]} ` : ""}${message}`;
43
- this.#recordMarkedWrite(this.#stderrTerminal, composed);
41
+ this.#recordMarkedWrite(composed);
44
42
  await this.#stderrTerminal.write(this.#quote.prefixLines(composed));
45
43
  this.#spinner.resume();
46
44
  this.#progress.resume();
@@ -214,8 +212,7 @@ export default class DefaultPrinterService {
214
212
  const writer = this.stdoutWritable.getWriter();
215
213
  const colorMessage = this.#stdoutIsColor ? this.primary(message) : message;
216
214
  const composed = `${icon !== undefined ? `${this.#iconDefinitions[icon]} ` : ""}${colorMessage}`;
217
- this.#recordMarkedWrite(this.#stdoutTerminal, composed);
218
- const encoded = this.#encoder.encode(this.#quote.prefixLines(composed));
215
+ const encoded = this.#encoder.encode(composed);
219
216
  await writer.ready;
220
217
  await writer.write(encoded);
221
218
  writer.releaseLock();
@@ -248,16 +245,18 @@ export default class DefaultPrinterService {
248
245
  if (this.#markedLineCount === undefined || this.#markEndedAt === undefined) {
249
246
  throw new Error("clearMarked() called without a preceding endMark()");
250
247
  }
248
+ if (!this.#stderrTerminal.isTty()) {
249
+ this.#markedLineCount = undefined;
250
+ this.#markEndedAt = undefined;
251
+ return;
252
+ }
251
253
  const remaining = minimumDisplayTimeMs - (Date.now() - this.#markEndedAt);
252
254
  if (remaining > 0) {
253
255
  await new Promise((resolve) => setTimeout(resolve, remaining));
254
256
  }
255
- if (this.#markedTerminal !== undefined) {
256
- await this.#markedTerminal.clearUpLines(this.#markedLineCount);
257
- }
257
+ await this.#stderrTerminal.clearUpLines(this.#markedLineCount);
258
258
  this.#markedLineCount = undefined;
259
259
  this.#markEndedAt = undefined;
260
- this.#markedTerminal = undefined;
261
260
  }
262
261
  async setLevel(level) {
263
262
  if (this.#threshold > Level.INFO) {
@@ -270,6 +269,9 @@ export default class DefaultPrinterService {
270
269
  return this.#threshold;
271
270
  }
272
271
  async showSpinner(message, style) {
272
+ if (!this.#stderrTerminal.isTty()) {
273
+ return;
274
+ }
273
275
  await this.#progress.hideAll();
274
276
  if (this.#threshold > Level.INFO) {
275
277
  return;
@@ -283,6 +285,9 @@ export default class DefaultPrinterService {
283
285
  await this.#spinner.hide();
284
286
  }
285
287
  async showProgressBar(units, message = "", total = 100, current = 0, style) {
288
+ if (!this.#stderrTerminal.isTty()) {
289
+ return -1;
290
+ }
286
291
  await this.#spinner.hide();
287
292
  if (this.#threshold > Level.INFO) {
288
293
  return -1;
@@ -0,0 +1,14 @@
1
+ import type Terminal from "./Terminal.ts";
2
+ export default class NonTtyTerminal implements Terminal {
3
+ #private;
4
+ constructor(writeStream: NodeJS.WritableStream);
5
+ write(text: string): Promise<void>;
6
+ columns(): number;
7
+ rows(): number;
8
+ clearLine(): Promise<void>;
9
+ clearUpLines(_count: number): Promise<void>;
10
+ hideCursor(): Promise<void>;
11
+ showCursor(): Promise<void>;
12
+ isTty(): boolean;
13
+ }
14
+ //# sourceMappingURL=NonTtyTerminal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NonTtyTerminal.d.ts","sourceRoot":"","sources":["../../../src/terminal/NonTtyTerminal.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAE1C,MAAM,CAAC,OAAO,OAAO,cAAe,YAAW,QAAQ;;gBAIzC,WAAW,EAAE,MAAM,CAAC,cAAc;IAQ9C,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,OAAO,IAAI,MAAM;IAIjB,IAAI,IAAI,MAAM;IAId,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,KAAK,IAAI,OAAO;CAGjB"}
@@ -0,0 +1,36 @@
1
+ import { WriteStream } from "node:tty";
2
+ export default class NonTtyTerminal {
3
+ #writeStream;
4
+ #encoder = new TextEncoder();
5
+ constructor(writeStream) {
6
+ if (writeStream.isTTY === true) {
7
+ throw new Error("The provided WriteStream is a TTY");
8
+ }
9
+ this.#writeStream = writeStream;
10
+ }
11
+ write(text) {
12
+ this.#writeStream.write(this.#encoder.encode(text));
13
+ return Promise.resolve();
14
+ }
15
+ columns() {
16
+ return 80;
17
+ }
18
+ rows() {
19
+ return 24;
20
+ }
21
+ clearLine() {
22
+ return Promise.reject(new Error("clearLine() is not supported on a non-TTY terminal"));
23
+ }
24
+ clearUpLines(_count) {
25
+ return Promise.reject(new Error("clearUpLines() is not supported on a non-TTY terminal"));
26
+ }
27
+ hideCursor() {
28
+ return Promise.reject(new Error("hideCursor() is not supported on a non-TTY terminal"));
29
+ }
30
+ showCursor() {
31
+ return Promise.reject(new Error("showCursor() is not supported on a non-TTY terminal"));
32
+ }
33
+ isTty() {
34
+ return false;
35
+ }
36
+ }
@@ -6,5 +6,6 @@ export default interface Terminal {
6
6
  write(text: string): Promise<void>;
7
7
  columns(): number;
8
8
  rows(): number;
9
+ isTty(): boolean;
9
10
  }
10
11
  //# sourceMappingURL=Terminal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Terminal.d.ts","sourceRoot":"","sources":["../../../src/terminal/Terminal.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,WAAW,QAAQ;IAC/B,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,OAAO,IAAI,MAAM,CAAC;IAClB,IAAI,IAAI,MAAM,CAAC;CAChB"}
1
+ {"version":3,"file":"Terminal.d.ts","sourceRoot":"","sources":["../../../src/terminal/Terminal.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,WAAW,QAAQ;IAC/B,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,OAAO,IAAI,MAAM,CAAC;IAClB,IAAI,IAAI,MAAM,CAAC;IACf,KAAK,IAAI,OAAO,CAAC;CAClB"}
@@ -6,6 +6,7 @@ export default class TtyStyler implements Styler {
6
6
  /**
7
7
  * The color level in use.
8
8
  *
9
+ * 0 = no color
9
10
  * 1 = 16 color
10
11
  * 2 = 256 color
11
12
  * 3 = 16 million color
@@ -1 +1 @@
1
- {"version":3,"file":"TtyStyler.d.ts","sourceRoot":"","sources":["../../../src/terminal/TtyStyler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,MAAM,CAAC,OAAO,OAAO,SAAU,YAAW,MAAM;IAC9C,YAAY,EAAE,OAAO,CAAQ;IAC7B,iBAAiB,EAAE,OAAO,CAAC;IAE3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B;;;;;;OAMG;gBACS,UAAU,GAAE,MAAU,EAAE,iBAAiB,GAAE,OAAc;IAKrE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAgBnD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAyB7D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIhC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;CAM7C"}
1
+ {"version":3,"file":"TtyStyler.d.ts","sourceRoot":"","sources":["../../../src/terminal/TtyStyler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,MAAM,CAAC,OAAO,OAAO,SAAU,YAAW,MAAM;IAC9C,YAAY,EAAE,OAAO,CAAQ;IAC7B,iBAAiB,EAAE,OAAO,CAAC;IAE3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B;;;;;;;OAOG;gBACS,UAAU,GAAE,MAAU,EAAE,iBAAiB,GAAE,OAAc;IAKrE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAgBnD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAyB7D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAOhC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;CAM7C"}
@@ -6,6 +6,7 @@ export default class TtyStyler {
6
6
  /**
7
7
  * The color level in use.
8
8
  *
9
+ * 0 = no color
9
10
  * 1 = 16 color
10
11
  * 2 = 256 color
11
12
  * 3 = 16 million color
@@ -15,7 +16,7 @@ export default class TtyStyler {
15
16
  this.hyperlinksEnabled = hyperlinksEnabled;
16
17
  }
17
18
  colorText(text, colorValue) {
18
- if (!this.colorEnabled) {
19
+ if (!this.colorEnabled || this.colorLevel === 0) {
19
20
  return text;
20
21
  }
21
22
  if (this.colorLevel === 3) {
@@ -27,7 +28,7 @@ export default class TtyStyler {
27
28
  return Bun.color(colorValue, "ansi-16") + text + FOREGROUND_COLOR_END;
28
29
  }
29
30
  backgroundColorText(text, colorValue) {
30
- if (!this.colorEnabled) {
31
+ if (!this.colorEnabled || this.colorLevel === 0) {
31
32
  return text;
32
33
  }
33
34
  if (this.colorLevel === 3) {
@@ -44,6 +45,9 @@ export default class TtyStyler {
44
45
  BACKGROUND_COLOR_END);
45
46
  }
46
47
  italicText(text) {
48
+ if (!this.colorEnabled || this.colorLevel === 0) {
49
+ return text;
50
+ }
47
51
  return ITALIC_START + text + ITALIC_END;
48
52
  }
49
53
  hyperlink(text, url) {
@@ -10,5 +10,6 @@ export default class TtyTerminal implements Terminal {
10
10
  write(text: string): Promise<void>;
11
11
  columns(): number;
12
12
  rows(): number;
13
+ isTty(): boolean;
13
14
  }
14
15
  //# sourceMappingURL=TtyTerminal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TtyTerminal.d.ts","sourceRoot":"","sources":["../../../src/terminal/TtyTerminal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAI1C,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,QAAQ;;gBAItC,cAAc,EAAE,WAAW;IAcvC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAM1B,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,OAAO,IAAI,MAAM;IAQjB,IAAI,IAAI,MAAM;CAOf"}
1
+ {"version":3,"file":"TtyTerminal.d.ts","sourceRoot":"","sources":["../../../src/terminal/TtyTerminal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAI1C,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,QAAQ;;gBAItC,cAAc,EAAE,WAAW;IAcvC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAM1B,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,OAAO,IAAI,MAAM;IAQjB,IAAI,IAAI,MAAM;IAQd,KAAK,IAAI,OAAO;CAGjB"}
@@ -49,4 +49,7 @@ export default class TtyTerminal {
49
49
  return 24;
50
50
  }
51
51
  }
52
+ isTty() {
53
+ return true;
54
+ }
52
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "2.0.0",
3
+ "version": "2.1.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",
@@ -70,6 +70,12 @@ const logger = getLogger("BaseCLI");
70
70
  *
71
71
  * * {@link ImagePrinterServiceProvider}
72
72
  *
73
+ * `keyReader` is optional. If omitted, or if the stderr {@link Terminal} is not a TTY, prompting is
74
+ * unavailable: {@link PrompterServiceProvider} (and {@link ArgumentPrompterServiceProvider}, if
75
+ * enabled) are simply not registered. If `promptingEnabled` is `true` in that situation, {@link run}
76
+ * throws rather than failing later inside a prompt. Similarly {@link ImagePrinterServiceProvider} is
77
+ * only registered if the stdout {@link Terminal} is a TTY.
78
+ *
73
79
  * By default the following commands are added:
74
80
  *
75
81
  * * {@link SingleCommandCliHelpSubCommand} or {@link MultiCommandCliHelpSubCommand}
@@ -80,7 +86,7 @@ const logger = getLogger("BaseCLI");
80
86
  export default class BaseCLI implements CLI {
81
87
  readonly #cliConfig: CLIConfig;
82
88
  readonly #options: Required<BaseCLIFeatureOptions>;
83
- readonly #keyReader: KeyReader;
89
+ readonly #keyReader: KeyReader | undefined;
84
90
  readonly #stderrTerminal: Terminal;
85
91
  readonly #addedNonModifierCommands: Array<Command>;
86
92
  readonly #serviceProviderRegistry: DefaultServiceProviderRegistry;
@@ -98,7 +104,7 @@ export default class BaseCLI implements CLI {
98
104
  stdoutTerminal: Terminal,
99
105
  stderrTerminal: Terminal,
100
106
  styler: Styler,
101
- keyReader: KeyReader,
107
+ keyReader: KeyReader | undefined,
102
108
  options?: BaseCLIFeatureOptions,
103
109
  ) {
104
110
  if (cliConfig.name.length === 0) {
@@ -210,22 +216,50 @@ export default class BaseCLI implements CLI {
210
216
  this.addServiceProvider(new PrinterServiceProvider(80, this.#printerService));
211
217
  this.addServiceProvider(new TableGeneratorServiceProvider(70));
212
218
 
213
- const prompterService = new DefaultPrompterService(
214
- DEFAULT_PROMPTER_CONFIG,
215
- this.#stderrTerminal,
216
- this.#keyReader,
217
- this.#printerService,
218
- );
219
- prompterService.promptEnabled = this.#options.promptingEnabled;
220
- this.addServiceProvider(new PrompterServiceProvider(75, prompterService));
219
+ const canPrompt = this.#keyReader !== undefined && this.#stderrTerminal.isTty();
220
+
221
+ if (this.#options.promptingEnabled && !canPrompt) {
222
+ throw new Error("promptingEnabled requires a keyReader and a TTY stderr terminal");
223
+ }
224
+
225
+ let prompterService: DefaultPrompterService | undefined;
226
+ if (canPrompt) {
227
+ prompterService = new DefaultPrompterService(
228
+ DEFAULT_PROMPTER_CONFIG,
229
+ this.#stderrTerminal,
230
+ this.#keyReader!,
231
+ this.#printerService,
232
+ );
233
+ prompterService.promptEnabled = this.#options.promptingEnabled;
234
+ this.addServiceProvider(new PrompterServiceProvider(75, prompterService));
235
+ } else {
236
+ logger.debug(
237
+ () =>
238
+ `Skipping PrompterServiceProvider: keyReader present: ${
239
+ this.#keyReader !== undefined
240
+ }, stderr isTty: ${this.#stderrTerminal.isTty()}`,
241
+ );
242
+ }
221
243
 
222
244
  if (this.#options.argumentPrompterServiceEnabled) {
223
- const argumentPrompterService = new DefaultArgumentPrompterService(prompterService);
224
- this.addServiceProvider(new ArgumentPrompterServiceProvider(65, argumentPrompterService));
245
+ if (prompterService !== undefined) {
246
+ const argumentPrompterService = new DefaultArgumentPrompterService(prompterService);
247
+ this.addServiceProvider(new ArgumentPrompterServiceProvider(65, argumentPrompterService));
248
+ } else {
249
+ logger.debug(
250
+ "Skipping ArgumentPrompterServiceProvider: argumentPrompterServiceEnabled is true but prompting is unavailable",
251
+ );
252
+ }
225
253
  }
226
254
 
227
255
  if (this.#options.imagePrinterServiceEnabled) {
228
- this.addServiceProvider(new ImagePrinterServiceProvider(55, this.#stdoutTerminal));
256
+ if (this.#stdoutTerminal.isTty()) {
257
+ this.addServiceProvider(new ImagePrinterServiceProvider(55, this.#stdoutTerminal));
258
+ } else {
259
+ logger.debug(
260
+ "Skipping ImagePrinterServiceProvider: imagePrinterServiceEnabled is true but stdout is not a TTY",
261
+ );
262
+ }
229
263
  }
230
264
 
231
265
  if (this.#options.completionServiceEnabled) {
@@ -5,30 +5,74 @@ import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
5
5
  import type BaseCLIFeatureOptions from "./BaseCLIFeatureOptions.ts";
6
6
  import { Writable } from "node:stream";
7
7
  import TtyTerminal from "../terminal/TtyTerminal.ts";
8
+ import NonTtyTerminal from "../terminal/NonTtyTerminal.ts";
8
9
  import TtyKeyReader from "../terminal/TtyKeyReader.ts";
9
10
  import TtyStyler from "../terminal/TtyStyler.ts";
10
11
  import supportsColor from "supports-color";
11
12
  import supportsHyperlinks from "../terminal/supportsHyperlinks.ts";
13
+ import getLogger from "../util/logger.ts";
14
+
15
+ const logger = getLogger("DefaultRuntimeCLI");
12
16
 
13
17
  /**
14
18
  * Default Bun implementation of a {@link CLI} using `process.stdout`, `process.stderr` and `process.argv`.
19
+ *
20
+ * Degrades gracefully when `stdin`, `stdout` or `stderr` are not TTYs (e.g. piped or redirected):
21
+ * color and hyperlinks are disabled, prompting is disabled, and non-TTY streams are wrapped in a
22
+ * {@link NonTtyTerminal} rather than the usual {@link TtyTerminal}.
15
23
  */
16
24
  export default class DefaultRuntimeCLI extends BaseCLI {
17
25
  constructor(cliConfig: CLIConfig, options?: BaseCLIFeatureOptions) {
26
+ const stdinIsTty = process.stdin.isTTY === true;
27
+ const stdoutIsTty = process.stdout.isTTY === true;
28
+ const stderrIsTty = process.stderr.isTTY === true;
29
+
30
+ logger.debug(
31
+ () =>
32
+ `stdin isTTY: ${stdinIsTty}, stdout isTTY: ${stdoutIsTty}, stderr isTTY: ${stderrIsTty}`,
33
+ );
34
+
35
+ const stdoutTerminal = stdoutIsTty
36
+ ? new TtyTerminal(process.stdout)
37
+ : new NonTtyTerminal(process.stdout);
38
+ const stderrTerminal = stderrIsTty
39
+ ? new TtyTerminal(process.stderr)
40
+ : new NonTtyTerminal(process.stderr);
41
+
42
+ logger.debug(
43
+ () =>
44
+ `stdout terminal: ${
45
+ stdoutIsTty ? "TtyTerminal" : "NonTtyTerminal"
46
+ }, stderr terminal: ${stderrIsTty ? "TtyTerminal" : "NonTtyTerminal"}`,
47
+ );
48
+
49
+ const colorLevel = stderrIsTty
50
+ ? supportsColor.stderr === false
51
+ ? 1
52
+ : supportsColor.stderr.level
53
+ : 0;
54
+
55
+ logger.debug(() => `color level: ${colorLevel}`);
56
+
57
+ const keyReader = stdinIsTty ? new TtyKeyReader(process.stdin) : undefined;
58
+
59
+ logger.debug(() => `keyReader constructed: ${keyReader !== undefined}`);
60
+
61
+ const promptingEnabled = stdinIsTty && stderrIsTty;
62
+
63
+ logger.debug(() => `promptingEnabled default: ${promptingEnabled}`);
64
+
18
65
  super(
19
66
  cliConfig,
20
67
  Writable.toWeb(process.stdout),
21
68
  Writable.toWeb(process.stderr),
22
69
  supportsColor.stdout !== false,
23
70
  supportsColor.stderr !== false,
24
- new TtyTerminal(process.stdout),
25
- new TtyTerminal(process.stderr),
26
- new TtyStyler(
27
- supportsColor.stderr === false ? 1 : supportsColor.stderr.level,
28
- supportsHyperlinks(process.stderr),
29
- ),
30
- new TtyKeyReader(process.stdin),
31
- { promptingEnabled: process.stdin.isTTY === true, ...options },
71
+ stdoutTerminal,
72
+ stderrTerminal,
73
+ new TtyStyler(colorLevel, supportsHyperlinks(process.stderr)),
74
+ keyReader,
75
+ { promptingEnabled, ...options },
32
76
  );
33
77
  }
34
78
 
@@ -13,6 +13,7 @@ import { Color } from "./terminal/Color.ts";
13
13
  import { getDarkModeTheme, getLightModeTheme } from "./terminal/Theme.ts";
14
14
  import { WritableStream } from "node:stream/web";
15
15
  import type Styler from "../../terminal/Styler.ts";
16
+
16
17
  export default class DefaultPrinterService implements PrinterService {
17
18
  readonly stdoutWritable: WritableStream;
18
19
  readonly stderrWritable: WritableStream;
@@ -30,18 +31,16 @@ export default class DefaultPrinterService implements PrinterService {
30
31
  #quote: Quote;
31
32
  #markedLineCount: number | undefined;
32
33
  #markEndedAt: number | undefined;
33
- #markedTerminal: Terminal | undefined;
34
34
 
35
35
  #countLines(message: string): number {
36
36
  return message.split("\n").length - (message.endsWith("\n") ? 1 : 0);
37
37
  }
38
38
 
39
- #recordMarkedWrite(terminal: Terminal, message: string): void {
39
+ #recordMarkedWrite(message: string): void {
40
40
  if (this.#markedLineCount === undefined) {
41
41
  return;
42
42
  }
43
43
  this.#markedLineCount += this.#countLines(message);
44
- this.#markedTerminal = terminal;
45
44
  }
46
45
 
47
46
  async #log(level: number, message: string, icon?: Icon): Promise<void> {
@@ -51,7 +50,7 @@ export default class DefaultPrinterService implements PrinterService {
51
50
  await this.#spinner.pause();
52
51
  await this.#progress.pause();
53
52
  const composed = `${icon !== undefined ? `${this.#iconDefinitions[icon]} ` : ""}${message}`;
54
- this.#recordMarkedWrite(this.#stderrTerminal, composed);
53
+ this.#recordMarkedWrite(composed);
55
54
  await this.#stderrTerminal.write(this.#quote.prefixLines(composed));
56
55
  this.#spinner.resume();
57
56
  this.#progress.resume();
@@ -277,8 +276,7 @@ export default class DefaultPrinterService implements PrinterService {
277
276
  const composed = `${
278
277
  icon !== undefined ? `${this.#iconDefinitions[icon]} ` : ""
279
278
  }${colorMessage}`;
280
- this.#recordMarkedWrite(this.#stdoutTerminal, composed);
281
- const encoded = this.#encoder.encode(this.#quote.prefixLines(composed));
279
+ const encoded = this.#encoder.encode(composed);
282
280
 
283
281
  await writer.ready;
284
282
  await writer.write(encoded);
@@ -318,16 +316,18 @@ export default class DefaultPrinterService implements PrinterService {
318
316
  if (this.#markedLineCount === undefined || this.#markEndedAt === undefined) {
319
317
  throw new Error("clearMarked() called without a preceding endMark()");
320
318
  }
319
+ if (!this.#stderrTerminal.isTty()) {
320
+ this.#markedLineCount = undefined;
321
+ this.#markEndedAt = undefined;
322
+ return;
323
+ }
321
324
  const remaining = minimumDisplayTimeMs - (Date.now() - this.#markEndedAt);
322
325
  if (remaining > 0) {
323
326
  await new Promise((resolve) => setTimeout(resolve, remaining));
324
327
  }
325
- if (this.#markedTerminal !== undefined) {
326
- await this.#markedTerminal.clearUpLines(this.#markedLineCount);
327
- }
328
+ await this.#stderrTerminal.clearUpLines(this.#markedLineCount);
328
329
  this.#markedLineCount = undefined;
329
330
  this.#markEndedAt = undefined;
330
- this.#markedTerminal = undefined;
331
331
  }
332
332
 
333
333
  public async setLevel(level: Level): Promise<void> {
@@ -343,6 +343,9 @@ export default class DefaultPrinterService implements PrinterService {
343
343
  }
344
344
 
345
345
  public async showSpinner(message?: string, style?: SpinnerStyle): Promise<void> {
346
+ if (!this.#stderrTerminal.isTty()) {
347
+ return;
348
+ }
346
349
  await this.#progress.hideAll();
347
350
  if (this.#threshold > Level.INFO) {
348
351
  return;
@@ -364,6 +367,9 @@ export default class DefaultPrinterService implements PrinterService {
364
367
  current = 0,
365
368
  style?: ProgressStyle,
366
369
  ): Promise<number> {
370
+ if (!this.#stderrTerminal.isTty()) {
371
+ return -1;
372
+ }
367
373
  await this.#spinner.hide();
368
374
  if (this.#threshold > Level.INFO) {
369
375
  return -1;
@@ -0,0 +1,48 @@
1
+ import { WriteStream } from "node:tty";
2
+ import type Terminal from "./Terminal.ts";
3
+
4
+ export default class NonTtyTerminal implements Terminal {
5
+ readonly #writeStream: NodeJS.WritableStream;
6
+ readonly #encoder = new TextEncoder();
7
+
8
+ constructor(writeStream: NodeJS.WritableStream) {
9
+ if ((writeStream as WriteStream).isTTY === true) {
10
+ throw new Error("The provided WriteStream is a TTY");
11
+ }
12
+
13
+ this.#writeStream = writeStream;
14
+ }
15
+
16
+ write(text: string): Promise<void> {
17
+ this.#writeStream.write(this.#encoder.encode(text));
18
+ return Promise.resolve();
19
+ }
20
+
21
+ columns(): number {
22
+ return 80;
23
+ }
24
+
25
+ rows(): number {
26
+ return 24;
27
+ }
28
+
29
+ clearLine(): Promise<void> {
30
+ return Promise.reject(new Error("clearLine() is not supported on a non-TTY terminal"));
31
+ }
32
+
33
+ clearUpLines(_count: number): Promise<void> {
34
+ return Promise.reject(new Error("clearUpLines() is not supported on a non-TTY terminal"));
35
+ }
36
+
37
+ hideCursor(): Promise<void> {
38
+ return Promise.reject(new Error("hideCursor() is not supported on a non-TTY terminal"));
39
+ }
40
+
41
+ showCursor(): Promise<void> {
42
+ return Promise.reject(new Error("showCursor() is not supported on a non-TTY terminal"));
43
+ }
44
+
45
+ isTty(): boolean {
46
+ return false;
47
+ }
48
+ }
@@ -6,4 +6,5 @@ export default interface Terminal {
6
6
  write(text: string): Promise<void>;
7
7
  columns(): number;
8
8
  rows(): number;
9
+ isTty(): boolean;
9
10
  }
@@ -17,6 +17,7 @@ export default class TtyStyler implements Styler {
17
17
  /**
18
18
  * The color level in use.
19
19
  *
20
+ * 0 = no color
20
21
  * 1 = 16 color
21
22
  * 2 = 256 color
22
23
  * 3 = 16 million color
@@ -27,7 +28,7 @@ export default class TtyStyler implements Styler {
27
28
  }
28
29
 
29
30
  colorText(text: string, colorValue: number): string {
30
- if (!this.colorEnabled) {
31
+ if (!this.colorEnabled || this.colorLevel === 0) {
31
32
  return text;
32
33
  }
33
34
 
@@ -43,7 +44,7 @@ export default class TtyStyler implements Styler {
43
44
  }
44
45
 
45
46
  backgroundColorText(text: string, colorValue: number): string {
46
- if (!this.colorEnabled) {
47
+ if (!this.colorEnabled || this.colorLevel === 0) {
47
48
  return text;
48
49
  }
49
50
 
@@ -68,6 +69,9 @@ export default class TtyStyler implements Styler {
68
69
  }
69
70
 
70
71
  italicText(text: string): string {
72
+ if (!this.colorEnabled || this.colorLevel === 0) {
73
+ return text;
74
+ }
71
75
  return ITALIC_START + text + ITALIC_END;
72
76
  }
73
77
 
@@ -62,4 +62,8 @@ export default class TtyTerminal implements Terminal {
62
62
  return 24;
63
63
  }
64
64
  }
65
+
66
+ isTty(): boolean {
67
+ return true;
68
+ }
65
69
  }