@flowscripter/dynamic-cli-framework 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.github/workflows/check-bun-dependencies.yml +9 -0
  2. package/.github/workflows/lint-pr-message.yml +11 -0
  3. package/.github/workflows/release-bun-library.yml +8 -0
  4. package/.github/workflows/validate-bun-library-pr.yml +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +1475 -0
  7. package/bun.lock +63 -0
  8. package/index.ts +89 -0
  9. package/package.json +23 -0
  10. package/src/api/CLI.ts +17 -0
  11. package/src/api/CLIConfig.ts +19 -0
  12. package/src/api/Context.ts +25 -0
  13. package/src/api/RunResult.ts +142 -0
  14. package/src/api/argument/Argument.ts +36 -0
  15. package/src/api/argument/ArgumentValueTypes.ts +87 -0
  16. package/src/api/argument/ComplexOption.ts +31 -0
  17. package/src/api/argument/GlobalCommandArgument.ts +17 -0
  18. package/src/api/argument/Option.ts +32 -0
  19. package/src/api/argument/Positional.ts +27 -0
  20. package/src/api/argument/SubCommandArgument.ts +20 -0
  21. package/src/api/command/Command.ts +22 -0
  22. package/src/api/command/GlobalCommand.ts +33 -0
  23. package/src/api/command/GlobalModifierCommand.ts +12 -0
  24. package/src/api/command/GroupCommand.ts +20 -0
  25. package/src/api/command/SubCommand.ts +41 -0
  26. package/src/api/command/UsageExample.ts +21 -0
  27. package/src/api/service/ServiceProvider.ts +52 -0
  28. package/src/api/service/core/AsciiBannerGeneratorService.ts +35 -0
  29. package/src/api/service/core/KeyValueService.ts +28 -0
  30. package/src/api/service/core/PrinterService.ts +266 -0
  31. package/src/api/service/core/ShutdownService.ts +12 -0
  32. package/src/api/service/core/SyntaxHighlighterService.ts +42 -0
  33. package/src/cli/BaseCLI.ts +312 -0
  34. package/src/cli/DefaultRuntimeCLI.ts +54 -0
  35. package/src/command/MultiCommandCliHelpCommand.ts +394 -0
  36. package/src/command/SingleCommandCliHelpCommand.ts +183 -0
  37. package/src/command/UsageCommand.ts +35 -0
  38. package/src/command/VersionCommand.ts +22 -0
  39. package/src/launcher.ts +127 -0
  40. package/src/runtime/DefaultContext.ts +32 -0
  41. package/src/runtime/argument/ArgumentTypeGuards.ts +8 -0
  42. package/src/runtime/command/CommandTypeGuards.ts +30 -0
  43. package/src/runtime/command/CommandValidator.ts +549 -0
  44. package/src/runtime/parser.ts +219 -0
  45. package/src/runtime/registry/CommandRegistry.ts +121 -0
  46. package/src/runtime/registry/DefaultCommandRegistry.ts +318 -0
  47. package/src/runtime/registry/DefaultServiceProviderRegistry.ts +26 -0
  48. package/src/runtime/registry/ServiceProviderRegistry.ts +11 -0
  49. package/src/runtime/runner.ts +697 -0
  50. package/src/runtime/scanner.ts +417 -0
  51. package/src/runtime/values/ValuePopulationResult.ts +41 -0
  52. package/src/runtime/values/argumentValueMerge.ts +201 -0
  53. package/src/runtime/values/argumentValueValidation.ts +610 -0
  54. package/src/runtime/values/globalCommandValuePopulation.ts +96 -0
  55. package/src/runtime/values/subCommandValuePopulation.ts +680 -0
  56. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +45 -0
  57. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +38 -0
  58. package/src/service/asciiBannerGenerator/standard.flf.json +3 -0
  59. package/src/service/banner/BannerServiceProvider.ts +99 -0
  60. package/src/service/banner/command/NoBannerCommand.ts +42 -0
  61. package/src/service/configuration/ConfigurationServiceProvider.ts +571 -0
  62. package/src/service/configuration/DefaultKeyValueService.ts +60 -0
  63. package/src/service/configuration/command/ConfigCommand.ts +43 -0
  64. package/src/service/configuration/command/DumpConfigCommand.ts +47 -0
  65. package/src/service/printer/DefaultPrinterService.ts +256 -0
  66. package/src/service/printer/PrinterServiceProvider.ts +61 -0
  67. package/src/service/printer/command/DarkModeCommand.ts +43 -0
  68. package/src/service/printer/command/LogLevelCommand.ts +68 -0
  69. package/src/service/printer/command/NoColorCommand.ts +43 -0
  70. package/src/service/printer/terminal/Ansi.ts +10 -0
  71. package/src/service/printer/terminal/Color.ts +14 -0
  72. package/src/service/printer/terminal/Progress.ts +373 -0
  73. package/src/service/printer/terminal/Spinner.ts +100 -0
  74. package/src/service/printer/terminal/Styler.ts +5 -0
  75. package/src/service/printer/terminal/Terminal.ts +8 -0
  76. package/src/service/printer/terminal/Theme.ts +34 -0
  77. package/src/service/printer/terminal/TtyStyler.ts +39 -0
  78. package/src/service/printer/terminal/TtyTerminal.ts +65 -0
  79. package/src/service/shutdown/DefaultShutdownService.ts +9 -0
  80. package/src/service/shutdown/ShutdownServiceProvider.ts +55 -0
  81. package/src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts +52 -0
  82. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +51 -0
  83. package/src/util/configHelper.ts +95 -0
  84. package/src/util/envVarHelper.ts +338 -0
  85. package/src/util/helpHelper.ts +751 -0
  86. package/src/util/logger.ts +84 -0
  87. package/src/util/runnerHelper.ts +112 -0
  88. package/tests/cli/BaseCLI.test.ts +344 -0
  89. package/tests/command/MultiCommandCliHelpCommand.test.ts +650 -0
  90. package/tests/command/SingleCommandCliHelpCommand.test.ts +384 -0
  91. package/tests/command/UsageCommand.test.ts +53 -0
  92. package/tests/command/VersionCommand.test.ts +36 -0
  93. package/tests/fixtures/CLIConfig.ts +9 -0
  94. package/tests/fixtures/Command.ts +229 -0
  95. package/tests/fixtures/CommandRegistry.ts +8 -0
  96. package/tests/fixtures/ConfigurationServiceProvider.ts +28 -0
  97. package/tests/fixtures/Context.ts +33 -0
  98. package/tests/fixtures/ServiceProviderRegistry.ts +16 -0
  99. package/tests/fixtures/StreamString.ts +37 -0
  100. package/tests/fixtures/util.ts +41 -0
  101. package/tests/runtime/DefaultContext.test.ts +20 -0
  102. package/tests/runtime/argument/ArgumentTypeGuards.test.ts +49 -0
  103. package/tests/runtime/command/CommandTypeGuards.test.ts +43 -0
  104. package/tests/runtime/command/CommandValidator.test.ts +816 -0
  105. package/tests/runtime/parser.test.ts +766 -0
  106. package/tests/runtime/registry/DefaultCommandRegistry.test.ts +166 -0
  107. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +42 -0
  108. package/tests/runtime/runner.test.ts +1298 -0
  109. package/tests/runtime/scanner.test.ts +1113 -0
  110. package/tests/runtime/values/argumentValueMerge.test.ts +314 -0
  111. package/tests/runtime/values/argumentValueValidation.test.ts +1020 -0
  112. package/tests/runtime/values/globalCommandValuePopulation.test.ts +662 -0
  113. package/tests/runtime/values/subCommandValuePopulation.test.ts +2658 -0
  114. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +22 -0
  115. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +70 -0
  116. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +22 -0
  117. package/tests/service/asciiBannerGenerator/small.flf.json +3 -0
  118. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +249 -0
  119. package/tests/service/configuration/DefaultKeyValueService.test.ts +65 -0
  120. package/tests/service/launchBanner/BannerServiceProvider.test.ts +49 -0
  121. package/tests/service/launchBanner/__snapshots__/BannerServiceProvider.test.ts.snap +3 -0
  122. package/tests/service/launchBanner/command/NoBannerCommand.test.ts +21 -0
  123. package/tests/service/printer/DefaultPrinterService.test.ts +409 -0
  124. package/tests/service/printer/PrinterServiceProvider.test.ts +29 -0
  125. package/tests/service/printer/command/LogLevelCommand.test.ts +58 -0
  126. package/tests/service/printer/terminal/Progress.test.ts +40 -0
  127. package/tests/service/printer/terminal/Spinner.test.ts +57 -0
  128. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +13 -0
  129. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +123 -0
  130. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +41 -0
  131. package/tests/util/configHelper.test.ts +275 -0
  132. package/tests/util/envVarHelper.test.ts +318 -0
  133. package/tests/util/logger.test.ts +10 -0
@@ -0,0 +1,373 @@
1
+ import Styler from "./Styler.ts";
2
+ import type Terminal from "./Terminal.ts";
3
+
4
+ const RATE_SMOOTHING_FACTOR = 0.005;
5
+
6
+ interface Bar {
7
+ name: string;
8
+ current: number;
9
+ total: number;
10
+ units: string;
11
+ lastMillis?: number;
12
+ startMillis?: number;
13
+ endMillis?: number;
14
+ rate?: number;
15
+ }
16
+
17
+ export default class Progress {
18
+ readonly #terminal: Terminal;
19
+ readonly #styler: Styler;
20
+ readonly #bars: Map<number, Bar> = new Map();
21
+ #timer: Timer | undefined;
22
+ #isDirty = false;
23
+ #currentRenderedBarCount = 0;
24
+ #lastRenderTime = 0;
25
+ #comColor = 0x5f8700;
26
+ #remColor = 0x585858;
27
+ #labColor = 0x585858;
28
+ #valColor = 0x808080;
29
+
30
+ public constructor(terminal: Terminal, styler: Styler) {
31
+ this.#terminal = terminal;
32
+ this.#styler = styler;
33
+ }
34
+
35
+ public add(
36
+ units: string,
37
+ message: string,
38
+ total: number,
39
+ current: number,
40
+ ): number {
41
+ if (total < 0) {
42
+ total = 100;
43
+ }
44
+ if (current < 0) {
45
+ current = 0;
46
+ }
47
+ if (current > total) {
48
+ current = total;
49
+ }
50
+
51
+ this.#bars.set(this.#bars.size + 1, {
52
+ name: message,
53
+ current,
54
+ total,
55
+ units,
56
+ startMillis: Date.now(),
57
+ });
58
+
59
+ // force render on next timeout
60
+ this.#isDirty = true;
61
+
62
+ // start rendering timer
63
+ if (this.#timer === undefined) {
64
+ this.#timer = setInterval(async () => {
65
+ await this.#renderBars();
66
+ }, 100);
67
+ }
68
+
69
+ return this.#bars.size;
70
+ }
71
+
72
+ #updateRate(bar: Bar, current: number): void {
73
+ const now = Date.now();
74
+ if (bar.lastMillis === undefined) {
75
+ bar.rate = ((current - bar.current) * 1000) / (now - bar.startMillis!);
76
+ } else {
77
+ let currentRate = ((current - bar.current) * 1000) /
78
+ (now - bar.lastMillis!);
79
+ if (!isFinite(currentRate)) {
80
+ currentRate = 0;
81
+ }
82
+ bar.rate = (currentRate * RATE_SMOOTHING_FACTOR) +
83
+ (bar.rate! * (1 - RATE_SMOOTHING_FACTOR));
84
+ }
85
+ bar.lastMillis = now;
86
+ }
87
+
88
+ public update(handle: number, current: number, message?: string): void {
89
+ if (!this.#bars.has(handle)) {
90
+ return;
91
+ }
92
+ const bar = this.#bars.get(handle)!;
93
+ if (current < 0) {
94
+ current = 0;
95
+ }
96
+ if (current > bar.total) {
97
+ current = bar.total;
98
+ }
99
+ if ((current === bar.total) && (bar.endMillis === undefined)) {
100
+ bar.endMillis = Date.now();
101
+ }
102
+
103
+ this.#updateRate(bar, current);
104
+
105
+ bar.current = current;
106
+
107
+ if (message !== undefined) {
108
+ bar.name = message;
109
+ }
110
+
111
+ // force render on next timeout
112
+ this.#isDirty = true;
113
+ }
114
+
115
+ public async hide(handle: number): Promise<void> {
116
+ // clear specified bar
117
+ if (this.#bars.has(handle)) {
118
+ this.#bars.delete(handle);
119
+ }
120
+
121
+ // if no more bars, effectively this is a pause
122
+ if (this.#bars.size === 0) {
123
+ await this.pause();
124
+
125
+ return;
126
+ }
127
+
128
+ // otherwise, force render on next timeout
129
+ this.#isDirty = true;
130
+ }
131
+
132
+ public async hideAll(): Promise<void> {
133
+ // clear all bars
134
+ this.#bars.clear();
135
+
136
+ await this.pause();
137
+ }
138
+
139
+ public async pause(): Promise<void> {
140
+ if (this.#timer === undefined) {
141
+ return;
142
+ }
143
+
144
+ this.#isDirty = false;
145
+
146
+ // stop rendering timer
147
+ clearInterval(this.#timer);
148
+ this.#timer = undefined;
149
+
150
+ // clear the previously rendered bars
151
+ await this.#terminal.clearUpLines(this.#currentRenderedBarCount * 2);
152
+
153
+ this.#currentRenderedBarCount = 0;
154
+
155
+ // restore the cursor
156
+ await this.#terminal.showCursor();
157
+ }
158
+
159
+ public resume(): void {
160
+ // don't resume if nothing to render
161
+ if (this.#bars.size === 0) {
162
+ return;
163
+ }
164
+
165
+ // force render on next timeout
166
+ this.#isDirty = true;
167
+
168
+ // start rendering timer
169
+ if (this.#timer === undefined) {
170
+ this.#timer = setInterval(async () => {
171
+ await this.#renderBars();
172
+ }, 100);
173
+ }
174
+ }
175
+
176
+ async #renderBars(): Promise<void> {
177
+ const now = Date.now();
178
+
179
+ // force render after one second to update newly calculated eta
180
+ if ((now - this.#lastRenderTime >= 1000) && !this.#isDirty) {
181
+ this.#bars.forEach((bar) => this.#updateRate(bar, bar.current));
182
+ this.#isDirty = true;
183
+ }
184
+
185
+ // don't render if we don't need to
186
+ if (!this.#isDirty) {
187
+ return;
188
+ }
189
+
190
+ this.#lastRenderTime = now;
191
+
192
+ const consoleWidth = this.#terminal.columns();
193
+ const lines: Array<string> = [];
194
+
195
+ const barInfo: Array<
196
+ {
197
+ name: string;
198
+ prefix: string;
199
+ available: number;
200
+ suffix: string;
201
+ bar: Bar;
202
+ }
203
+ > = [];
204
+
205
+ this.#bars.forEach((bar) => {
206
+ const name = `${this.#styler.colorText(bar.name, this.#valColor)}${
207
+ this.#styler.colorText(":", this.#labColor)
208
+ }`;
209
+ const prefix = `${this.#styler.colorText("[", this.#labColor)}`;
210
+ let visibleWidth = 1;
211
+ const percentString = ((bar.current / bar.total) * 100).toFixed(2);
212
+ const percent = this.#styler.colorText(
213
+ percentString,
214
+ this.#valColor,
215
+ ) + this.#styler.colorText("%,", this.#labColor);
216
+ visibleWidth += percentString.length + 1;
217
+
218
+ const rate = `${
219
+ this.#styler.colorText(
220
+ (bar.rate === undefined) ? "-" : bar.rate.toFixed(2) + "",
221
+ this.#valColor,
222
+ )
223
+ }`;
224
+
225
+ let suffix = `${this.#styler.colorText("]", this.#labColor)} ${percent} `;
226
+ visibleWidth += 3;
227
+ if (bar.current === bar.total) {
228
+ const taken = this.#formatTime(bar.endMillis! - bar.startMillis!);
229
+ const totalString = bar.total + "";
230
+ suffix += `${this.#styler.colorText(totalString, this.#valColor)}${
231
+ this.#styler.colorText(bar.units + ", rate:", this.#labColor)
232
+ } ${rate}${
233
+ this.#styler.colorText(
234
+ bar.units + "/s, time taken:",
235
+ this.#labColor,
236
+ )
237
+ } ${taken}`;
238
+ visibleWidth + totalString.length + bar.units.length + 7 + 1 +
239
+ rate.length + bar.units.length + 15 + 1 + taken.length;
240
+ } else {
241
+ const remaining = ((bar.rate === undefined) || (bar.rate === 0))
242
+ ? "-"
243
+ : this.#formatTime(((bar.total - bar.current) / bar.rate) * 1000);
244
+ const currentString = bar.current + "";
245
+ const totalString = bar.total + "";
246
+ suffix += `${this.#styler.colorText(currentString, this.#valColor)}${
247
+ this.#styler.colorText("/", this.#labColor)
248
+ }${this.#styler.colorText(totalString, this.#valColor)}${
249
+ this.#styler.colorText(bar.units + ", rate:", this.#labColor)
250
+ } ${rate}${
251
+ this.#styler.colorText(
252
+ bar.units + "/s, time remaining:",
253
+ this.#labColor,
254
+ )
255
+ } ${remaining}`;
256
+ visibleWidth + currentString.length + 1 + totalString.length +
257
+ bar.units.length + 7 + 1 + rate.length + bar.units.length + 19 + 1 +
258
+ remaining.length;
259
+ }
260
+ let available = consoleWidth - visibleWidth;
261
+ if (available < 0) {
262
+ available = 0;
263
+ }
264
+ barInfo.push({
265
+ name,
266
+ prefix,
267
+ available,
268
+ suffix,
269
+ bar,
270
+ });
271
+ });
272
+
273
+ let totalLength = consoleWidth;
274
+ barInfo.forEach((barInfo) => {
275
+ if (barInfo.available < totalLength) {
276
+ totalLength = barInfo.available;
277
+ }
278
+ });
279
+ totalLength = Math.min(50, totalLength);
280
+
281
+ barInfo.forEach((barInfo) => {
282
+ const completeLength = Math.floor(
283
+ totalLength * barInfo.bar.current / barInfo.bar.total,
284
+ );
285
+ const complete = this.#styler.colorText(
286
+ "=".repeat(completeLength),
287
+ this.#comColor,
288
+ );
289
+ const remaining = this.#styler.colorText(
290
+ "-".repeat(totalLength - completeLength),
291
+ this.#remColor,
292
+ );
293
+
294
+ lines.push(barInfo.name);
295
+ lines.push(barInfo.prefix + complete + remaining + barInfo.suffix);
296
+ });
297
+
298
+ // hide the cursor before rendering
299
+ await this.#terminal.hideCursor();
300
+
301
+ // clear the previously rendered bars
302
+ await this.#terminal.clearUpLines(this.#currentRenderedBarCount * 2);
303
+
304
+ // rendere the new bars
305
+ await this.#terminal.write(lines.join("\n") + "\n");
306
+
307
+ // save count rendered bars so we can clear on the next render cycle
308
+ this.#currentRenderedBarCount = barInfo.length;
309
+
310
+ // show the cursor if there are no bars or the timer is not running
311
+ if (this.#bars.size === 0 || this.#timer === undefined) {
312
+ await this.#terminal.showCursor();
313
+ }
314
+
315
+ this.#isDirty = false;
316
+ }
317
+
318
+ set completeColor(color: number) {
319
+ this.#comColor = color;
320
+ }
321
+
322
+ set remainingColor(color: number) {
323
+ this.#remColor = color;
324
+ }
325
+
326
+ set labelColor(color: number) {
327
+ this.#labColor = color;
328
+ }
329
+
330
+ set valueColor(color: number) {
331
+ this.#valColor = color;
332
+ }
333
+
334
+ #formatTime(millis: number): string {
335
+ let sec = millis / 1000;
336
+ if (sec < 60) {
337
+ return `${this.#styler.colorText(sec.toFixed(0), this.#valColor)}${
338
+ this.#styler.colorText("s", this.#labColor)
339
+ }`;
340
+ }
341
+ let min = Math.floor(sec / 60);
342
+ sec %= 60;
343
+ if (min < 60) {
344
+ return `${this.#styler.colorText(min.toFixed(0), this.#valColor)}${
345
+ this.#styler.colorText("m", this.#labColor)
346
+ } ${this.#styler.colorText(sec.toFixed(0), this.#valColor)}${
347
+ this.#styler.colorText("s", this.#labColor)
348
+ }`;
349
+ }
350
+ let hour = Math.floor(min / 60);
351
+ min %= 60;
352
+ if (hour < 24) {
353
+ return `${this.#styler.colorText(hour.toFixed(0), this.#valColor)}${
354
+ this.#styler.colorText("h", this.#labColor)
355
+ } ${this.#styler.colorText(min.toFixed(0), this.#valColor)}${
356
+ this.#styler.colorText("m", this.#labColor)
357
+ } ${this.#styler.colorText(sec.toFixed(0), this.#valColor)}${
358
+ this.#styler.colorText("s", this.#labColor)
359
+ }`;
360
+ }
361
+ const day = Math.floor(hour / 24);
362
+ hour %= 24;
363
+ return `${this.#styler.colorText(day.toFixed(0), this.#valColor)}${
364
+ this.#styler.colorText("d", this.#labColor)
365
+ } ${this.#styler.colorText(hour.toFixed(0), this.#valColor)}${
366
+ this.#styler.colorText("h", this.#labColor)
367
+ } ${this.#styler.colorText(min.toFixed(0), this.#valColor)}${
368
+ this.#styler.colorText("m", this.#labColor)
369
+ } ${this.#styler.colorText(sec.toFixed(0), this.#valColor)}${
370
+ this.#styler.colorText("s", this.#labColor)
371
+ }`;
372
+ }
373
+ }
@@ -0,0 +1,100 @@
1
+ import Styler from "./Styler.ts";
2
+ import type Terminal from "./Terminal.ts";
3
+
4
+ const FRAMES = [
5
+ "⠋",
6
+ "⠙",
7
+ "⠹",
8
+ "⠸",
9
+ "⠼",
10
+ "⠴",
11
+ "⠦",
12
+ "⠧",
13
+ "⠇",
14
+ "⠏",
15
+ ];
16
+
17
+ export default class Spinner {
18
+ #isShown = false;
19
+ #message: string | undefined;
20
+ #frameIndex = 0;
21
+ #timer: Timer | undefined;
22
+ #spinColor = 0x8a8a8a;
23
+ #msgColor = 0x808080;
24
+ readonly #terminal: Terminal;
25
+ readonly #styler: Styler;
26
+
27
+ public constructor(terminal: Terminal, styler: Styler) {
28
+ this.#terminal = terminal;
29
+ this.#styler = styler;
30
+ }
31
+
32
+ async #nextFrame(): Promise<void> {
33
+ if (!this.#isShown) {
34
+ return;
35
+ }
36
+ await this.#terminal.clearLine();
37
+ if (this.#message) {
38
+ await this.#terminal.write(
39
+ `${this.#styler.colorText(FRAMES[this.#frameIndex], this.#spinColor)} ${
40
+ this.#styler.colorText(this.#message!, this.#msgColor)
41
+ }`,
42
+ );
43
+ } else {
44
+ await this.#terminal.write(
45
+ this.#styler.colorText(FRAMES[this.#frameIndex], this.#spinColor),
46
+ );
47
+ }
48
+ this.#frameIndex = (this.#frameIndex + 1) % FRAMES.length;
49
+ }
50
+
51
+ public async show(message?: string): Promise<void> {
52
+ this.#message = message;
53
+ if (this.#isShown) {
54
+ return Promise.resolve();
55
+ }
56
+ this.#isShown = true;
57
+ this.#frameIndex = 0;
58
+ this.#timer = setInterval(async () => {
59
+ await this.#nextFrame();
60
+ }, 100);
61
+ await this.#terminal.hideCursor();
62
+ }
63
+
64
+ public async hide(): Promise<void> {
65
+ if (!this.#isShown) {
66
+ return Promise.resolve();
67
+ }
68
+ this.#isShown = false;
69
+ clearInterval(this.#timer);
70
+ await this.#terminal.clearLine();
71
+ await this.#terminal.showCursor();
72
+ this.#message = undefined;
73
+ }
74
+
75
+ public async pause(): Promise<void> {
76
+ if (!this.#isShown) {
77
+ return Promise.resolve();
78
+ }
79
+ clearInterval(this.#timer);
80
+ this.#timer = undefined;
81
+ await this.#terminal.clearLine();
82
+ }
83
+
84
+ public resume(): void {
85
+ if ((!this.#isShown) || (this.#timer !== undefined)) {
86
+ return;
87
+ }
88
+ this.#timer = setInterval(async () => {
89
+ await this.#nextFrame();
90
+ }, 100);
91
+ }
92
+
93
+ set spinnerColor(color: number) {
94
+ this.#spinColor = color;
95
+ }
96
+
97
+ set messageColor(color: number) {
98
+ this.#msgColor = color;
99
+ }
100
+ }
@@ -0,0 +1,5 @@
1
+ export default interface Styler {
2
+ colorEnabled: boolean;
3
+ colorText(text: string, colorValue: number): string;
4
+ italicText(text: string): string;
5
+ }
@@ -0,0 +1,8 @@
1
+ export default interface Terminal {
2
+ clearLine(): Promise<void>;
3
+ clearUpLines(count: number): Promise<void>;
4
+ hideCursor(): Promise<void>;
5
+ showCursor(): Promise<void>;
6
+ write(text: string): Promise<void>;
7
+ columns(): number;
8
+ }
@@ -0,0 +1,34 @@
1
+ import { Color } from "./Color";
2
+
3
+ const commonTheme: Array<number> = [];
4
+
5
+ commonTheme[Color.YELLOW] = 0xb58900;
6
+ commonTheme[Color.ORANGE] = 0xcb4b16;
7
+ commonTheme[Color.RED] = 0xdc322f;
8
+ commonTheme[Color.MAGENTA] = 0xd33682;
9
+ commonTheme[Color.VIOLET] = 0x6c71c4;
10
+ commonTheme[Color.BLUE] = 0x268bd2;
11
+ commonTheme[Color.CYAN] = 0x2aa198;
12
+ commonTheme[Color.GREEN] = 0x859900;
13
+
14
+ export function getLightModeTheme(): Array<number> {
15
+ const lightModeTheme: Array<number> = [...commonTheme];
16
+
17
+ lightModeTheme[Color.PRIMARY] = 0x657b83;
18
+ lightModeTheme[Color.SECONDARY] = 0x93a1a1;
19
+ lightModeTheme[Color.EMPHASISED] = 0x586e75;
20
+ lightModeTheme[Color.SELECTED] = 0xeee8d5;
21
+
22
+ return lightModeTheme;
23
+ }
24
+
25
+ export function getDarkModeTheme(): Array<number> {
26
+ const darkModeTheme: Array<number> = [...commonTheme];
27
+
28
+ darkModeTheme[Color.PRIMARY] = 0x839496;
29
+ darkModeTheme[Color.SECONDARY] = 0x586e75;
30
+ darkModeTheme[Color.EMPHASISED] = 0x93a1a1;
31
+ darkModeTheme[Color.SELECTED] = 0x073642;
32
+
33
+ return darkModeTheme;
34
+ }
@@ -0,0 +1,39 @@
1
+ import { COLOR_END, ITALIC_END, ITALIC_START } from "./Ansi.ts";
2
+ import Styler from "./Styler.ts";
3
+
4
+ export default class TtyStyler implements Styler {
5
+ colorEnabled: boolean = true;
6
+
7
+ readonly colorLevel: number;
8
+
9
+ /**
10
+ * The color level in use.
11
+ *
12
+ * 1 = 16 color
13
+ * 2 = 256 color
14
+ * 3 = 16 million color
15
+ */
16
+ constructor(colorLevel: number = 1) {
17
+ this.colorLevel = colorLevel;
18
+ }
19
+
20
+ colorText(text: string, colorValue: number): string {
21
+ if (!this.colorEnabled) {
22
+ return text;
23
+ }
24
+
25
+ if (this.colorLevel === 3) {
26
+ return Bun.color(colorValue, "ansi-16m") + text + COLOR_END;
27
+ }
28
+
29
+ if (this.colorLevel === 2) {
30
+ return Bun.color(colorValue, "ansi-256") + text + COLOR_END;
31
+ }
32
+
33
+ return Bun.color(colorValue, "ansi-16") + text + COLOR_END;
34
+ }
35
+
36
+ italicText(text: string): string {
37
+ return ITALIC_START + text + ITALIC_END;
38
+ }
39
+ }
@@ -0,0 +1,65 @@
1
+ import { WriteStream } from "node:tty";
2
+ import type Terminal from "./Terminal.ts";
3
+
4
+ import {
5
+ CLEAR_LINE,
6
+ CURSOR_LEFT,
7
+ CURSOR_UP,
8
+ HIDE_CURSOR,
9
+ SHOW_CURSOR,
10
+ } from "./Ansi.ts";
11
+
12
+ export default class TtyTerminal implements Terminal {
13
+ readonly #ttyWriteStream: WriteStream;
14
+ readonly #encoder = new TextEncoder();
15
+
16
+ constructor(ttyWriteStream: WriteStream) {
17
+ if (ttyWriteStream.isTTY === false) {
18
+ throw new Error("The provided WriteStream is not a TTY");
19
+ }
20
+
21
+ this.#ttyWriteStream = ttyWriteStream;
22
+ }
23
+
24
+ #writeAll(message: string): void {
25
+ const encoded = this.#encoder.encode(message);
26
+
27
+ this.#ttyWriteStream.write(encoded);
28
+ }
29
+
30
+ clearLine(): Promise<void> {
31
+ this.#writeAll(CLEAR_LINE + CURSOR_LEFT);
32
+
33
+ return Promise.resolve();
34
+ }
35
+
36
+ clearUpLines(count: number): Promise<void> {
37
+ this.#writeAll(
38
+ (CURSOR_UP + CLEAR_LINE).repeat(count) + CLEAR_LINE + CURSOR_LEFT,
39
+ );
40
+ return Promise.resolve();
41
+ }
42
+
43
+ hideCursor(): Promise<void> {
44
+ this.#writeAll(HIDE_CURSOR);
45
+ return Promise.resolve();
46
+ }
47
+
48
+ showCursor(): Promise<void> {
49
+ this.#writeAll(SHOW_CURSOR);
50
+ return Promise.resolve();
51
+ }
52
+
53
+ write(text: string): Promise<void> {
54
+ this.#writeAll(text);
55
+ return Promise.resolve();
56
+ }
57
+
58
+ columns(): number {
59
+ try {
60
+ return this.#ttyWriteStream.columns;
61
+ } catch {
62
+ return 80;
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,9 @@
1
+ import type ShutdownService from "../../api/service/core/ShutdownService.ts";
2
+
3
+ export default class DefaultShutdownService implements ShutdownService {
4
+ static readonly callbacks: Array<() => Promise<void>> = [];
5
+
6
+ addShutdownListener(callback: () => Promise<void>): void {
7
+ DefaultShutdownService.callbacks.push(callback);
8
+ }
9
+ }
@@ -0,0 +1,55 @@
1
+ import process from "node:process";
2
+ import type {
3
+ ServiceInfo,
4
+ ServiceProvider,
5
+ } from "../../api/service/ServiceProvider.ts";
6
+ import DefaultShutdownService from "./DefaultShutdownService.ts";
7
+ import type ShutdownService from "../../api/service/core/ShutdownService.ts";
8
+ import { SHUTDOWN_SERVICE_ID } from "../../api/service/core/ShutdownService.ts";
9
+ import type Context from "../../api/Context.ts";
10
+ import getLogger from "../../util/logger.ts";
11
+ import type CLIConfig from "../../api/CLIConfig.ts";
12
+
13
+ const logger = getLogger("ShutdownServiceProvider");
14
+
15
+ /**
16
+ * Provides a {@link ShutdownService}.
17
+ */
18
+ export default class ShutdownServiceProvider implements ServiceProvider {
19
+ readonly serviceId: string = SHUTDOWN_SERVICE_ID;
20
+ readonly #shutdownService: ShutdownService;
21
+
22
+ /**
23
+ * Create an instance of the service provider with the specified details.
24
+ *
25
+ * @param servicePriority the priority of the service.
26
+ */
27
+ public constructor(
28
+ readonly servicePriority: number,
29
+ ) {
30
+ this.#shutdownService = new DefaultShutdownService();
31
+ process.on("beforeExit", ShutdownServiceProvider.shutdown);
32
+ }
33
+
34
+ public provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
35
+ return Promise.resolve({
36
+ service: this.#shutdownService,
37
+ commands: [],
38
+ });
39
+ }
40
+
41
+ initService(_context: Context): Promise<void> {
42
+ return Promise.resolve(undefined);
43
+ }
44
+
45
+ static async shutdown() {
46
+ try {
47
+ process.removeListener("beforeExit", ShutdownServiceProvider.shutdown);
48
+ for await (const callback of DefaultShutdownService.callbacks) {
49
+ await callback();
50
+ }
51
+ } catch (error) {
52
+ logger.error("shutdown error: %s", (error as Error).message);
53
+ }
54
+ }
55
+ }