@flowscripter/dynamic-cli-framework 2.0.0 → 2.2.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 (54) hide show
  1. package/README.md +3 -0
  2. package/dist/index.d.ts +5 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +4 -1
  5. package/dist/src/cli/BaseCLI.d.ts +8 -1
  6. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  7. package/dist/src/cli/BaseCLI.js +38 -6
  8. package/dist/src/cli/BaseCLIFeatureOptions.d.ts +1 -0
  9. package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
  10. package/dist/src/cli/DefaultRuntimeCLI.d.ts +4 -0
  11. package/dist/src/cli/DefaultRuntimeCLI.d.ts.map +1 -1
  12. package/dist/src/cli/DefaultRuntimeCLI.js +29 -1
  13. package/dist/src/service/plugin/DefaultPluginService.d.ts +1 -1
  14. package/dist/src/service/plugin/DefaultPluginService.d.ts.map +1 -1
  15. package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts +3 -2
  16. package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts.map +1 -1
  17. package/dist/src/service/plugin/DefaultPluginServiceProvider.js +22 -2
  18. package/dist/src/service/plugin/SpawnInterfaceAdapter.d.ts +19 -0
  19. package/dist/src/service/plugin/SpawnInterfaceAdapter.d.ts.map +1 -0
  20. package/dist/src/service/plugin/SpawnInterfaceAdapter.js +40 -0
  21. package/dist/src/service/printer/DefaultPrinterService.d.ts.map +1 -1
  22. package/dist/src/service/printer/DefaultPrinterService.js +15 -10
  23. package/dist/src/service/spawn/DefaultSpawnService.d.ts +7 -0
  24. package/dist/src/service/spawn/DefaultSpawnService.d.ts.map +1 -0
  25. package/dist/src/service/spawn/DefaultSpawnService.js +94 -0
  26. package/dist/src/service/spawn/SpawnServiceProvider.d.ts +12 -0
  27. package/dist/src/service/spawn/SpawnServiceProvider.d.ts.map +1 -0
  28. package/dist/src/service/spawn/SpawnServiceProvider.js +24 -0
  29. package/dist/src/terminal/NonTtyTerminal.d.ts +14 -0
  30. package/dist/src/terminal/NonTtyTerminal.d.ts.map +1 -0
  31. package/dist/src/terminal/NonTtyTerminal.js +36 -0
  32. package/dist/src/terminal/Terminal.d.ts +1 -0
  33. package/dist/src/terminal/Terminal.d.ts.map +1 -1
  34. package/dist/src/terminal/TtyStyler.d.ts +1 -0
  35. package/dist/src/terminal/TtyStyler.d.ts.map +1 -1
  36. package/dist/src/terminal/TtyStyler.js +6 -2
  37. package/dist/src/terminal/TtyTerminal.d.ts +1 -0
  38. package/dist/src/terminal/TtyTerminal.d.ts.map +1 -1
  39. package/dist/src/terminal/TtyTerminal.js +3 -0
  40. package/index.ts +10 -1
  41. package/package.json +3 -3
  42. package/src/cli/BaseCLI.ts +54 -13
  43. package/src/cli/BaseCLIFeatureOptions.ts +1 -0
  44. package/src/cli/DefaultRuntimeCLI.ts +52 -8
  45. package/src/service/plugin/DefaultPluginService.ts +1 -1
  46. package/src/service/plugin/DefaultPluginServiceProvider.ts +39 -3
  47. package/src/service/plugin/SpawnInterfaceAdapter.ts +62 -0
  48. package/src/service/printer/DefaultPrinterService.ts +16 -10
  49. package/src/service/spawn/DefaultSpawnService.ts +119 -0
  50. package/src/service/spawn/SpawnServiceProvider.ts +32 -0
  51. package/src/terminal/NonTtyTerminal.ts +48 -0
  52. package/src/terminal/Terminal.ts +1 -0
  53. package/src/terminal/TtyStyler.ts +6 -2
  54. package/src/terminal/TtyTerminal.ts +4 -0
@@ -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,119 @@
1
+ import type {
2
+ PrinterService,
3
+ ShutdownService,
4
+ SpawnOptions,
5
+ SpawnResult,
6
+ SpawnService,
7
+ } from "@flowscripter/dynamic-cli-framework-api";
8
+ import getLogger from "../../util/logger.ts";
9
+
10
+ const logger = getLogger("DefaultSpawnService");
11
+
12
+ const SHUTDOWN_GRACE_PERIOD_MS = 5000;
13
+
14
+ export default class DefaultSpawnService implements SpawnService {
15
+ #printerService: PrinterService | undefined;
16
+ #shutdownService: ShutdownService | undefined;
17
+
18
+ public setDependencies(printerService: PrinterService, shutdownService: ShutdownService): void {
19
+ this.#printerService = printerService;
20
+ this.#shutdownService = shutdownService;
21
+ }
22
+
23
+ async #pipeLines(
24
+ stream: ReadableStream<Uint8Array> | number | undefined,
25
+ streamName: "stdout" | "stderr",
26
+ onOutput: (line: string, stream: "stdout" | "stderr") => void,
27
+ ): Promise<void> {
28
+ if (typeof stream !== "object" || stream === null) {
29
+ return;
30
+ }
31
+ const decoder = new TextDecoder();
32
+ const reader = stream.getReader();
33
+ let buffer = "";
34
+ for (;;) {
35
+ const { done, value } = await reader.read();
36
+ if (done) {
37
+ break;
38
+ }
39
+ buffer += decoder.decode(value, { stream: true });
40
+ let newlineIndex = buffer.indexOf("\n");
41
+ while (newlineIndex !== -1) {
42
+ onOutput(buffer.slice(0, newlineIndex), streamName);
43
+ buffer = buffer.slice(newlineIndex + 1);
44
+ newlineIndex = buffer.indexOf("\n");
45
+ }
46
+ }
47
+ if (buffer.length > 0) {
48
+ onOutput(buffer, streamName);
49
+ }
50
+ }
51
+
52
+ public async spawn(
53
+ command: ReadonlyArray<string>,
54
+ options: SpawnOptions = {},
55
+ ): Promise<SpawnResult> {
56
+ if (this.#printerService === undefined || this.#shutdownService === undefined) {
57
+ throw new Error("DefaultSpawnService.spawn() called before setDependencies()");
58
+ }
59
+ const printerService = this.#printerService;
60
+ const shutdownService = this.#shutdownService;
61
+ const stdio = options.stdio ?? "inherit";
62
+ const longRunning = options.longRunning ?? true;
63
+
64
+ if (stdio === "inherit") {
65
+ await printerService.hideSpinner();
66
+ await printerService.hideAllProgressBars();
67
+ }
68
+
69
+ let proc;
70
+ try {
71
+ proc = Bun.spawn([...command], {
72
+ cwd: options.cwd,
73
+ stdout: stdio === "wrapped" ? "pipe" : "inherit",
74
+ stderr: stdio === "wrapped" ? "pipe" : "inherit",
75
+ });
76
+ } catch (error) {
77
+ logger.debug(() => `Failed to launch command '${command.join(" ")}': ${error}`);
78
+ return { ok: false, error: error as Error };
79
+ }
80
+
81
+ let settled = false;
82
+
83
+ if (longRunning) {
84
+ shutdownService.enterLongRunningMode();
85
+ }
86
+
87
+ shutdownService.addShutdownListener(async () => {
88
+ if (settled) {
89
+ return;
90
+ }
91
+ logger.debug(() => `Sending SIGTERM to spawned command '${command.join(" ")}'`);
92
+ proc.kill("SIGTERM");
93
+ await Promise.race([
94
+ proc.exited.then(() => {}),
95
+ new Promise<void>((resolve) => setTimeout(resolve, SHUTDOWN_GRACE_PERIOD_MS)),
96
+ ]);
97
+ if (!settled) {
98
+ logger.debug(() => `Sending SIGKILL to spawned command '${command.join(" ")}'`);
99
+ proc.kill("SIGKILL");
100
+ }
101
+ });
102
+
103
+ if (stdio === "wrapped" && options.onOutput) {
104
+ const onOutput = options.onOutput;
105
+ void this.#pipeLines(proc.stdout, "stdout", onOutput);
106
+ void this.#pipeLines(proc.stderr, "stderr", onOutput);
107
+ }
108
+
109
+ try {
110
+ const exitCode = await proc.exited;
111
+ settled = true;
112
+ return exitCode === 0 ? { ok: true, exitCode } : { ok: false, exitCode };
113
+ } finally {
114
+ if (longRunning) {
115
+ shutdownService.leaveLongRunningMode();
116
+ }
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,32 @@
1
+ import type { ServiceInfo, ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
2
+ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
3
+ import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
4
+ import type { PrinterService, ShutdownService } from "@flowscripter/dynamic-cli-framework-api";
5
+ import { PRINTER_SERVICE_ID, SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
+ import { SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
7
+ import DefaultSpawnService from "./DefaultSpawnService.ts";
8
+
9
+ export default class SpawnServiceProvider implements ServiceProvider {
10
+ readonly serviceId: string = SPAWN_SERVICE_ID;
11
+ readonly servicePriority: number;
12
+ #spawnService: DefaultSpawnService | undefined;
13
+
14
+ public constructor(servicePriority: number) {
15
+ this.servicePriority = servicePriority;
16
+ }
17
+
18
+ public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
19
+ this.#spawnService = new DefaultSpawnService();
20
+ return Promise.resolve({
21
+ service: this.#spawnService,
22
+ commands: [],
23
+ });
24
+ }
25
+
26
+ public initService(context: Context): Promise<void> {
27
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
28
+ const shutdownService = context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService;
29
+ this.#spawnService!.setDependencies(printerService, shutdownService);
30
+ return Promise.resolve();
31
+ }
32
+ }
@@ -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
  }