@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 +3 -0
- package/dist/src/cli/BaseCLI.d.ts +7 -1
- package/dist/src/cli/BaseCLI.d.ts.map +1 -1
- package/dist/src/cli/BaseCLI.js +32 -6
- package/dist/src/cli/DefaultRuntimeCLI.d.ts +4 -0
- package/dist/src/cli/DefaultRuntimeCLI.d.ts.map +1 -1
- package/dist/src/cli/DefaultRuntimeCLI.js +29 -1
- package/dist/src/service/printer/DefaultPrinterService.d.ts.map +1 -1
- package/dist/src/service/printer/DefaultPrinterService.js +15 -10
- package/dist/src/terminal/NonTtyTerminal.d.ts +14 -0
- package/dist/src/terminal/NonTtyTerminal.d.ts.map +1 -0
- package/dist/src/terminal/NonTtyTerminal.js +36 -0
- package/dist/src/terminal/Terminal.d.ts +1 -0
- package/dist/src/terminal/Terminal.d.ts.map +1 -1
- package/dist/src/terminal/TtyStyler.d.ts +1 -0
- package/dist/src/terminal/TtyStyler.d.ts.map +1 -1
- package/dist/src/terminal/TtyStyler.js +6 -2
- package/dist/src/terminal/TtyTerminal.d.ts +1 -0
- package/dist/src/terminal/TtyTerminal.d.ts.map +1 -1
- package/dist/src/terminal/TtyTerminal.js +3 -0
- package/package.json +1 -1
- package/src/cli/BaseCLI.ts +47 -13
- package/src/cli/DefaultRuntimeCLI.ts +52 -8
- package/src/service/printer/DefaultPrinterService.ts +16 -10
- package/src/terminal/NonTtyTerminal.ts +48 -0
- package/src/terminal/Terminal.ts +1 -0
- package/src/terminal/TtyStyler.ts +6 -2
- package/src/terminal/TtyTerminal.ts +4 -0
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
|
|
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"}
|
package/dist/src/cli/BaseCLI.js
CHANGED
|
@@ -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
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAC9E,OAAO,EACL,KAAK,IAAI,EACT,KAAK,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,4BAA4B,CAAC;AAGvD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,MAAM,MAAM,0BAA0B,CAAC;
|
|
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(
|
|
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(
|
|
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.#
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -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
|
|
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) {
|
|
@@ -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;
|
|
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"}
|
package/package.json
CHANGED
package/src/cli/BaseCLI.ts
CHANGED
|
@@ -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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
prompterService
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
new TtyStyler(
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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(
|
|
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.#
|
|
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
|
-
|
|
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
|
+
}
|
package/src/terminal/Terminal.ts
CHANGED
|
@@ -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
|
|