@flowscripter/dynamic-cli-framework 1.1.6 → 1.2.1

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 (160) hide show
  1. package/.github/workflows/release-bun-library.yml +1 -0
  2. package/README/core-commands.md +40 -0
  3. package/README/core-service-providers.md +198 -0
  4. package/README/development.md +60 -0
  5. package/README/implementation-details.md +474 -0
  6. package/README/key-concepts.md +751 -0
  7. package/README.md +12 -1413
  8. package/bun.lock +22 -18
  9. package/index.ts +97 -1
  10. package/package.json +8 -7
  11. package/src/api/BaseCLIFeatureOptions.ts +10 -0
  12. package/src/api/CLIConfig.ts +5 -0
  13. package/src/api/RunResult.ts +16 -0
  14. package/src/api/argument/Argument.ts +24 -0
  15. package/src/api/service/ServiceProvider.ts +3 -3
  16. package/src/api/service/core/ArgumentPrompterService.ts +10 -0
  17. package/src/api/service/core/AsciiBannerGeneratorService.ts +44 -7
  18. package/src/api/service/core/CompletionService.ts +36 -0
  19. package/src/api/service/core/DataDumpGeneratorService.ts +27 -0
  20. package/src/api/service/core/ImagePrinterService.ts +26 -0
  21. package/src/api/service/core/KeyValueService.ts +22 -5
  22. package/src/api/service/core/PrinterService.ts +127 -1
  23. package/src/api/service/core/PrompterService.ts +49 -0
  24. package/src/api/service/core/SecretService.ts +27 -0
  25. package/src/api/service/core/ShutdownService.ts +17 -1
  26. package/src/api/service/core/Table.ts +85 -0
  27. package/src/api/service/core/TableGeneratorService.ts +38 -0
  28. package/src/api/service/core/TreePrinterService.ts +11 -0
  29. package/src/cli/BaseCLI.ts +116 -44
  30. package/src/cli/DefaultRuntimeCLI.ts +10 -18
  31. package/src/command/CompletionCommand.ts +179 -0
  32. package/src/command/MultiCommandCliHelpCommand.ts +18 -2
  33. package/src/command/SingleCommandCliHelpCommand.ts +10 -1
  34. package/src/launcher.ts +16 -58
  35. package/src/runtime/command/CommandValidator.ts +3 -3
  36. package/src/runtime/runner.ts +102 -30
  37. package/src/runtime/scanner.ts +18 -15
  38. package/src/runtime/values/argumentValueValidation.ts +49 -5
  39. package/src/runtime/values/globalCommandValuePopulation.ts +1 -1
  40. package/src/runtime/values/subCommandValuePopulation.ts +9 -8
  41. package/src/service/argumentPrompter/ArgumentPrompterServiceProvider.ts +34 -0
  42. package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +330 -0
  43. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +1 -1
  44. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +254 -17
  45. package/src/service/banner/BannerServiceProvider.ts +2 -2
  46. package/src/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.ts +85 -0
  47. package/src/service/chiselAsciiBannerGenerator/chisel.flf.json +3 -0
  48. package/src/service/completion/CompletionServiceProvider.ts +212 -0
  49. package/src/service/completion/DefaultCompletionService.ts +364 -0
  50. package/src/service/completion/shell/ShellHandler.ts +16 -0
  51. package/src/service/completion/shell/bash.ts +47 -0
  52. package/src/service/completion/shell/fish.ts +43 -0
  53. package/src/service/completion/shell/powershell.ts +66 -0
  54. package/src/service/completion/shell/zsh.ts +50 -0
  55. package/src/service/configuration/ConfigurationServiceProvider.ts +138 -42
  56. package/src/service/configuration/DefaultKeyValueService.ts +53 -7
  57. package/src/service/configuration/DefaultSecretService.ts +85 -0
  58. package/src/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.ts +41 -0
  59. package/src/service/dataDumpGenerator/DefaultDataDumpGeneratorService.ts +80 -0
  60. package/src/service/imagePrinter/DefaultImagePrinterService.ts +23 -0
  61. package/src/service/imagePrinter/ImagePrinterServiceProvider.ts +32 -0
  62. package/src/service/prettyPrinter/PrettyPrinterServiceProvider.ts +1 -1
  63. package/src/service/printer/DefaultPrinterService.ts +147 -27
  64. package/src/service/printer/PrinterServiceProvider.ts +7 -7
  65. package/src/service/printer/command/NoHyperlinksCommand.ts +40 -0
  66. package/src/service/printer/terminal/Progress.ts +21 -4
  67. package/src/service/printer/terminal/Spinner.ts +32 -8
  68. package/src/service/printer/terminal/Theme.ts +1 -1
  69. package/src/service/prompter/DefaultPrompterService.ts +87 -0
  70. package/src/service/prompter/PrompterServiceProvider.ts +36 -0
  71. package/src/service/prompter/command/NoPromptCommand.ts +40 -0
  72. package/src/service/prompter/prompt/PromptContext.ts +47 -0
  73. package/src/service/prompter/prompt/promptAcknowledge.ts +28 -0
  74. package/src/service/prompter/prompt/promptMultiSelect.ts +99 -0
  75. package/src/service/prompter/prompt/promptOpenUrl.ts +97 -0
  76. package/src/service/prompter/prompt/promptSingleSelect.ts +102 -0
  77. package/src/service/prompter/prompt/promptText.ts +107 -0
  78. package/src/service/prompter/prompt/promptToggle.ts +28 -0
  79. package/src/service/shutdown/DefaultShutdownService.ts +13 -0
  80. package/src/service/shutdown/ShutdownServiceProvider.ts +28 -9
  81. package/src/service/shutdown/ShutdownState.ts +5 -0
  82. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +1 -1
  83. package/src/service/tableGenerator/DefaultTableGeneratorService.ts +415 -0
  84. package/src/service/tableGenerator/TableGeneratorServiceProvider.ts +43 -0
  85. package/src/service/treePrinter/DefaultTreePrinterService.ts +49 -0
  86. package/src/service/treePrinter/TreePrinterServiceProvider.ts +40 -0
  87. package/src/terminal/Ansi.ts +27 -0
  88. package/src/terminal/ImageRenderer.ts +199 -0
  89. package/src/terminal/KeyReader.ts +23 -0
  90. package/src/terminal/PngDecoder.ts +143 -0
  91. package/src/{service/printer/terminal → terminal}/Styler.ts +3 -0
  92. package/src/{service/printer/terminal → terminal}/Terminal.ts +1 -0
  93. package/src/terminal/TtyKeyReader.ts +83 -0
  94. package/src/terminal/TtyStyler.ts +78 -0
  95. package/src/{service/printer/terminal → terminal}/TtyTerminal.ts +9 -1
  96. package/src/terminal/supportsHyperlinks.ts +110 -0
  97. package/src/util/configHelper.ts +2 -2
  98. package/src/util/envVarHelper.ts +2 -2
  99. package/src/util/helpHelper.ts +66 -122
  100. package/src/util/logger.ts +16 -14
  101. package/src/util/runnerHelper.ts +1 -1
  102. package/tests/cli/BaseCLI.test.ts +50 -54
  103. package/tests/command/CompletionCommand.test.ts +68 -0
  104. package/tests/command/UsageCommand.test.ts +3 -2
  105. package/tests/command/VersionCommand.test.ts +3 -2
  106. package/tests/fixtures/Context.ts +9 -2
  107. package/tests/fixtures/util.ts +1 -1
  108. package/tests/runtime/parser.test.ts +9 -7
  109. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
  110. package/tests/runtime/values/argumentValueValidation.test.ts +193 -0
  111. package/tests/service/argumentPrompter/ArgumentPrompterServiceProvider.test.ts +47 -0
  112. package/tests/service/argumentPrompter/DefaultArgumentPrompterService.test.ts +754 -0
  113. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +5 -4
  114. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +163 -3
  115. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +1 -1
  116. package/tests/service/banner/BannerServiceProvider.test.ts +8 -4
  117. package/tests/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.test.ts +52 -0
  118. package/tests/service/completion/CompletionServiceProvider.test.ts +404 -0
  119. package/tests/service/completion/DefaultCompletionService.test.ts +278 -0
  120. package/tests/service/completion/shell/bash.test.ts +73 -0
  121. package/tests/service/completion/shell/fish.test.ts +62 -0
  122. package/tests/service/completion/shell/powershell.test.ts +59 -0
  123. package/tests/service/completion/shell/zsh.test.ts +60 -0
  124. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +24 -12
  125. package/tests/service/configuration/DefaultKeyValueService.test.ts +118 -16
  126. package/tests/service/configuration/DefaultSecretService.test.ts +148 -0
  127. package/tests/service/configuration/command/ConfigCommand.test.ts +29 -0
  128. package/tests/service/configuration/command/DumpConfigCommand.test.ts +60 -0
  129. package/tests/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.test.ts +57 -0
  130. package/tests/service/dataDumpGenerator/DefaultDataDumpGeneratorService.test.ts +217 -0
  131. package/tests/service/imagePrinter/DefaultImagePrinterService.test.ts +96 -0
  132. package/tests/service/imagePrinter/ImagePrinterServiceProvider.test.ts +17 -0
  133. package/tests/service/prettyPrinter/DefaultPrettyPrinterService.test.ts +2 -2
  134. package/tests/service/prettyPrinter/PrettyPrinterServiceProvider.test.ts +5 -4
  135. package/tests/service/printer/DefaultPrinterService.test.ts +233 -2
  136. package/tests/service/printer/PrinterServiceProvider.test.ts +6 -5
  137. package/tests/service/printer/command/DarkModeCommand.test.ts +83 -0
  138. package/tests/service/printer/command/LogLevelCommand.test.ts +7 -5
  139. package/tests/service/printer/command/NoColorCommand.test.ts +83 -0
  140. package/tests/service/printer/command/NoHyperlinksCommand.test.ts +89 -0
  141. package/tests/service/printer/terminal/Progress.test.ts +64 -4
  142. package/tests/service/printer/terminal/Spinner.test.ts +44 -3
  143. package/tests/service/prompter/DefaultPrompterService.test.ts +615 -0
  144. package/tests/service/prompter/PrompterServiceProvider.test.ts +47 -0
  145. package/tests/service/prompter/command/NoPromptCommand.test.ts +58 -0
  146. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +4 -2
  147. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +3 -2
  148. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +5 -4
  149. package/tests/service/tableGenerator/DefaultTableGeneratorService.test.ts +363 -0
  150. package/tests/service/tableGenerator/TableGeneratorServiceProvider.test.ts +47 -0
  151. package/tests/service/treePrinter/DefaultTreePrinterService.test.ts +144 -0
  152. package/tests/service/treePrinter/TreePrinterServiceProvider.test.ts +55 -0
  153. package/tests/terminal/ImageRenderer.test.ts +161 -0
  154. package/tests/terminal/PngDecoder.test.ts +223 -0
  155. package/tests/terminal/supportsHyperlinks.test.ts +149 -0
  156. package/tests/util/configHelper.test.ts +4 -4
  157. package/tests/util/logger.test.ts +23 -1
  158. package/tsconfig.json +31 -0
  159. package/src/service/printer/terminal/Ansi.ts +0 -10
  160. package/src/service/printer/terminal/TtyStyler.ts +0 -39
@@ -0,0 +1,199 @@
1
+ import process from "node:process";
2
+ import supportsTerminalGraphics from "supports-terminal-graphics";
3
+ import { decodePngToRGBA } from "./PngDecoder.ts";
4
+ import {
5
+ BACKGROUND_COLOR_END,
6
+ backgroundColorStart,
7
+ FOREGROUND_COLOR_END,
8
+ foregroundColorStart,
9
+ } from "./Ansi.ts";
10
+ import type Terminal from "./Terminal.ts";
11
+ import { Buffer } from "node:buffer";
12
+
13
+ export default class ImageRenderer {
14
+ #terminal: Terminal;
15
+
16
+ constructor(terminal: Terminal) {
17
+ this.#terminal = terminal;
18
+ }
19
+
20
+ renderImage(
21
+ imageBuffer: Uint8Array,
22
+ widthPercentage: number = 100,
23
+ hexFormattedBackgroundColor?: string,
24
+ ): Promise<string> {
25
+ if (!this.#isMultiplexer()) {
26
+ if (supportsTerminalGraphics.stdout.kitty) {
27
+ return this.#renderKitty(imageBuffer, widthPercentage);
28
+ }
29
+ if (supportsTerminalGraphics.stdout.iterm2) {
30
+ return this.#renderITerm2(imageBuffer, widthPercentage);
31
+ }
32
+ }
33
+ return this.#renderAnsiBlocks(
34
+ imageBuffer,
35
+ widthPercentage,
36
+ hexFormattedBackgroundColor,
37
+ );
38
+ }
39
+
40
+ #isMultiplexer(): boolean {
41
+ const term = (process.env.TERM || "").toLowerCase();
42
+ const termProgram = (process.env.TERM_PROGRAM || "").toLowerCase();
43
+ return termProgram === "tmux" || termProgram === "screen" ||
44
+ term.startsWith("tmux") || term.startsWith("screen");
45
+ }
46
+
47
+ // https://sw.kovidgoyal.net/kitty/graphics-protocol/
48
+ async #renderKitty(
49
+ imageBuffer: Uint8Array,
50
+ widthPercentage: number,
51
+ ): Promise<string> {
52
+ const pngBytes = await new Bun.Image(imageBuffer).png().bytes();
53
+ const columns = Math.floor(
54
+ this.#terminal.columns() * widthPercentage / 100,
55
+ );
56
+ const base64 = Buffer.from(pngBytes).toString("base64");
57
+ const chunks: string[] = [];
58
+ for (let i = 0; i < base64.length; i += 4096) {
59
+ chunks.push(base64.slice(i, i + 4096));
60
+ }
61
+ let result = "";
62
+ for (let i = 0; i < chunks.length; i++) {
63
+ const isLast = i === chunks.length - 1;
64
+ if (i === 0) {
65
+ result += `\x1b_Gf=100,a=T,c=${columns},m=${isLast ? 0 : 1};${
66
+ chunks[i]
67
+ }\x1b\\`;
68
+ } else {
69
+ result += `\x1b_Gm=${isLast ? 0 : 1};${chunks[i]}\x1b\\`;
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+
75
+ // https://iterm2.com/documentation-images.html
76
+ async #renderITerm2(
77
+ imageBuffer: Uint8Array,
78
+ widthPercentage: number,
79
+ ): Promise<string> {
80
+ const pngBytes = await new Bun.Image(imageBuffer).png().bytes();
81
+ const base64 = Buffer.from(pngBytes).toString("base64");
82
+ return `\x1b]1337;File=inline=1;width=${widthPercentage}%:${base64}\x07`;
83
+ }
84
+
85
+ static #parseHexColor(
86
+ hex: string,
87
+ ): { r: number; g: number; b: number } | undefined {
88
+ const match = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
89
+ if (!match) return undefined;
90
+ return {
91
+ r: Number.parseInt(match[1]!, 16),
92
+ g: Number.parseInt(match[2]!, 16),
93
+ b: Number.parseInt(match[3]!, 16),
94
+ };
95
+ }
96
+
97
+ async #renderAnsiBlocks(
98
+ imageBuffer: Uint8Array,
99
+ widthPercentage: number,
100
+ hexFormattedBackgroundColor?: string,
101
+ ): Promise<string> {
102
+ const bg = hexFormattedBackgroundColor
103
+ ? ImageRenderer.#parseHexColor(hexFormattedBackgroundColor)
104
+ : undefined;
105
+ const columns = this.#terminal.columns();
106
+ const rows = this.#terminal.rows();
107
+ const targetWidth = Math.floor(columns * widthPercentage / 100);
108
+
109
+ const meta = await new Bun.Image(imageBuffer).metadata();
110
+ const aspectRatio = meta.height! / meta.width!;
111
+ let targetHeight = Math.round(targetWidth * aspectRatio);
112
+ if (targetHeight % 2 !== 0) targetHeight++;
113
+ const maxHeight = (rows - 2) * 2;
114
+ if (targetHeight > maxHeight) targetHeight = maxHeight;
115
+
116
+ const resizedPng = await new Bun.Image(imageBuffer)
117
+ .resize(targetWidth, targetHeight, { fit: "inside" })
118
+ .png()
119
+ .bytes();
120
+ const { width, height, pixels } = decodePngToRGBA(resizedPng);
121
+
122
+ const lines: string[] = [];
123
+ for (let y = 0; y < height; y += 2) {
124
+ let line = "";
125
+ const hasBottom = y + 1 < height;
126
+ for (let x = 0; x < width; x++) {
127
+ const topIdx = (y * width + x) * 4;
128
+ const r = pixels[topIdx]!;
129
+ const g = pixels[topIdx + 1]!;
130
+ const b = pixels[topIdx + 2]!;
131
+ const a = pixels[topIdx + 3]!;
132
+
133
+ if (hasBottom) {
134
+ const botIdx = ((y + 1) * width + x) * 4;
135
+ const r2 = pixels[botIdx]!;
136
+ const g2 = pixels[botIdx + 1]!;
137
+ const b2 = pixels[botIdx + 2]!;
138
+ const a2 = pixels[botIdx + 3]!;
139
+
140
+ if (a === 0 && a2 === 0) {
141
+ if (bg) {
142
+ line += backgroundColorStart(
143
+ bg.r,
144
+ bg.g,
145
+ bg.b,
146
+ ) + " " + BACKGROUND_COLOR_END;
147
+ } else {
148
+ line += " ";
149
+ }
150
+ } else if (a === 0) {
151
+ if (bg) {
152
+ line += backgroundColorStart(
153
+ bg.r,
154
+ bg.g,
155
+ bg.b,
156
+ ) + foregroundColorStart(r2, g2, b2) + "▄" +
157
+ FOREGROUND_COLOR_END + BACKGROUND_COLOR_END;
158
+ } else {
159
+ line += foregroundColorStart(r2, g2, b2) + "▄" +
160
+ FOREGROUND_COLOR_END;
161
+ }
162
+ } else if (a2 === 0) {
163
+ if (bg) {
164
+ line += backgroundColorStart(
165
+ bg.r,
166
+ bg.g,
167
+ bg.b,
168
+ ) + foregroundColorStart(r, g, b) + "▀" +
169
+ FOREGROUND_COLOR_END + BACKGROUND_COLOR_END;
170
+ } else {
171
+ line += foregroundColorStart(r, g, b) + "▀" +
172
+ FOREGROUND_COLOR_END;
173
+ }
174
+ } else {
175
+ line += backgroundColorStart(r, g, b) +
176
+ foregroundColorStart(r2, g2, b2) + "▄" +
177
+ FOREGROUND_COLOR_END + BACKGROUND_COLOR_END;
178
+ }
179
+ } else {
180
+ if (a === 0) {
181
+ if (bg) {
182
+ line += backgroundColorStart(
183
+ bg.r,
184
+ bg.g,
185
+ bg.b,
186
+ ) + " " + BACKGROUND_COLOR_END;
187
+ } else {
188
+ line += " ";
189
+ }
190
+ } else {
191
+ line += backgroundColorStart(r, g, b) + " " + BACKGROUND_COLOR_END;
192
+ }
193
+ }
194
+ }
195
+ lines.push(line);
196
+ }
197
+ return lines.join("\n");
198
+ }
199
+ }
@@ -0,0 +1,23 @@
1
+ export enum SpecialKey {
2
+ UP = 0,
3
+ DOWN = 1,
4
+ LEFT = 2,
5
+ RIGHT = 3,
6
+ ENTER = 4,
7
+ SPACE = 5,
8
+ BACKSPACE = 6,
9
+ ESCAPE = 7,
10
+ TAB = 8,
11
+ INTERRUPT = 9,
12
+ }
13
+
14
+ export interface KeyEvent {
15
+ readonly key?: string;
16
+ readonly specialKey?: SpecialKey;
17
+ }
18
+
19
+ export default interface KeyReader {
20
+ enableRawMode(): void;
21
+ disableRawMode(): void;
22
+ readKey(): Promise<KeyEvent>;
23
+ }
@@ -0,0 +1,143 @@
1
+ // Minimal PNG-to-RGBA pixel extractor. Supports 8-bit RGB and RGBA only.
2
+ import { inflateSync } from "node:zlib";
3
+
4
+ function paethPredictor(a: number, b: number, c: number): number {
5
+ const p = a + b - c;
6
+ const pa = Math.abs(p - a);
7
+ const pb = Math.abs(p - b);
8
+ const pc = Math.abs(p - c);
9
+ if (pa <= pb && pa <= pc) return a;
10
+ if (pb <= pc) return b;
11
+ return c;
12
+ }
13
+
14
+ export function decodePngToRGBA(
15
+ pngBytes: Uint8Array,
16
+ ): { width: number; height: number; pixels: Uint8Array } {
17
+ const sig = [137, 80, 78, 71, 13, 10, 26, 10];
18
+ for (let i = 0; i < 8; i++) {
19
+ if (pngBytes[i] !== sig[i]) throw new Error("Invalid PNG signature");
20
+ }
21
+
22
+ const view = new DataView(
23
+ pngBytes.buffer,
24
+ pngBytes.byteOffset,
25
+ pngBytes.byteLength,
26
+ );
27
+ let offset = 8;
28
+
29
+ let width = 0;
30
+ let height = 0;
31
+ let bitDepth = 0;
32
+ let colorType = 0;
33
+ const idatChunks: Uint8Array[] = [];
34
+
35
+ while (offset < pngBytes.length) {
36
+ const length = view.getUint32(offset);
37
+ const type = String.fromCharCode(
38
+ pngBytes[offset + 4]!,
39
+ pngBytes[offset + 5]!,
40
+ pngBytes[offset + 6]!,
41
+ pngBytes[offset + 7]!,
42
+ );
43
+ const dataStart = offset + 8;
44
+
45
+ if (type === "IHDR") {
46
+ width = view.getUint32(dataStart);
47
+ height = view.getUint32(dataStart + 4);
48
+ bitDepth = pngBytes[dataStart + 8]!;
49
+ colorType = pngBytes[dataStart + 9]!;
50
+ const interlace = pngBytes[dataStart + 12]!;
51
+ if (bitDepth !== 8) {
52
+ throw new Error(
53
+ `Unsupported bit depth: ${bitDepth} (only 8 supported)`,
54
+ );
55
+ }
56
+ if (colorType !== 2 && colorType !== 6) {
57
+ throw new Error(
58
+ `Unsupported color type: ${colorType} (only RGB=2 and RGBA=6 supported)`,
59
+ );
60
+ }
61
+ if (interlace !== 0) {
62
+ throw new Error("Interlaced PNGs are not supported");
63
+ }
64
+ } else if (type === "IDAT") {
65
+ idatChunks.push(pngBytes.subarray(dataStart, dataStart + length));
66
+ } else if (type === "IEND") {
67
+ break;
68
+ }
69
+
70
+ offset = dataStart + length + 4; // +4 for CRC
71
+ }
72
+
73
+ if (width === 0 || height === 0) throw new Error("Missing IHDR chunk");
74
+ if (idatChunks.length === 0) throw new Error("Missing IDAT chunks");
75
+
76
+ const bpp = colorType === 6 ? 4 : 3;
77
+ const compressed = new Uint8Array(
78
+ idatChunks.reduce((sum, c) => sum + c.length, 0),
79
+ );
80
+ let pos = 0;
81
+ for (const chunk of idatChunks) {
82
+ compressed.set(chunk, pos);
83
+ pos += chunk.length;
84
+ }
85
+
86
+ const raw = inflateSync(compressed);
87
+ const stride = bpp * width;
88
+ const pixels = new Uint8Array(width * height * 4);
89
+ const prevRow = new Uint8Array(stride);
90
+ const currRow = new Uint8Array(stride);
91
+
92
+ let rawOffset = 0;
93
+
94
+ for (let y = 0; y < height; y++) {
95
+ const filterType = raw[rawOffset++]!;
96
+
97
+ for (let x = 0; x < stride; x++) {
98
+ currRow[x] = raw[rawOffset++]!;
99
+ }
100
+
101
+ for (let x = 0; x < stride; x++) {
102
+ const left = x >= bpp ? currRow[x - bpp]! : 0;
103
+ const up = prevRow[x]!;
104
+ const upperLeft = x >= bpp ? prevRow[x - bpp]! : 0;
105
+
106
+ switch (filterType) {
107
+ case 0:
108
+ break;
109
+ case 1:
110
+ currRow[x] = (currRow[x]! + left) & 0xff;
111
+ break;
112
+ case 2:
113
+ currRow[x] = (currRow[x]! + up) & 0xff;
114
+ break;
115
+ case 3:
116
+ currRow[x] = (currRow[x]! + ((left + up) >> 1)) & 0xff;
117
+ break;
118
+ case 4:
119
+ currRow[x] = (currRow[x]! + paethPredictor(left, up, upperLeft)) &
120
+ 0xff;
121
+ break;
122
+ default:
123
+ throw new Error(`Unknown filter type: ${filterType}`);
124
+ }
125
+ }
126
+
127
+ const rowStart = y * width * 4;
128
+ if (bpp === 4) {
129
+ pixels.set(currRow, rowStart);
130
+ } else {
131
+ for (let x = 0; x < width; x++) {
132
+ pixels[rowStart + x * 4] = currRow[x * 3]!;
133
+ pixels[rowStart + x * 4 + 1] = currRow[x * 3 + 1]!;
134
+ pixels[rowStart + x * 4 + 2] = currRow[x * 3 + 2]!;
135
+ pixels[rowStart + x * 4 + 3] = 255;
136
+ }
137
+ }
138
+
139
+ prevRow.set(currRow);
140
+ }
141
+
142
+ return { width, height, pixels };
143
+ }
@@ -1,5 +1,8 @@
1
1
  export default interface Styler {
2
2
  colorEnabled: boolean;
3
+ hyperlinksEnabled: boolean;
3
4
  colorText(text: string, colorValue: number): string;
5
+ backgroundColorText(text: string, colorValue: number): string;
4
6
  italicText(text: string): string;
7
+ hyperlink(text: string, url: string): string;
5
8
  }
@@ -5,4 +5,5 @@ export default interface Terminal {
5
5
  showCursor(): Promise<void>;
6
6
  write(text: string): Promise<void>;
7
7
  columns(): number;
8
+ rows(): number;
8
9
  }
@@ -0,0 +1,83 @@
1
+ import { ReadStream } from "node:tty";
2
+ import { Buffer } from "node:buffer";
3
+ import type KeyReader from "./KeyReader.ts";
4
+ import { type KeyEvent, SpecialKey } from "./KeyReader.ts";
5
+
6
+ export default class TtyKeyReader implements KeyReader {
7
+ readonly #ttyReadStream: ReadStream;
8
+
9
+ constructor(ttyReadStream: ReadStream) {
10
+ if (ttyReadStream.isTTY === false) {
11
+ throw new Error("The provided ReadStream is not a TTY");
12
+ }
13
+ this.#ttyReadStream = ttyReadStream;
14
+ }
15
+
16
+ enableRawMode(): void {
17
+ this.#ttyReadStream.setRawMode(true);
18
+ this.#ttyReadStream.resume();
19
+ }
20
+
21
+ disableRawMode(): void {
22
+ this.#ttyReadStream.setRawMode(false);
23
+ this.#ttyReadStream.pause();
24
+ }
25
+
26
+ readKey(): Promise<KeyEvent> {
27
+ return new Promise((resolve) => {
28
+ const onData = (data: Buffer): void => {
29
+ this.#ttyReadStream.removeListener("data", onData);
30
+ resolve(TtyKeyReader.#decodeKeyEvent(data));
31
+ };
32
+ this.#ttyReadStream.on("data", onData);
33
+ });
34
+ }
35
+
36
+ static #decodeKeyEvent(data: Buffer): KeyEvent {
37
+ if (data.length === 0) {
38
+ return {};
39
+ }
40
+
41
+ // escape sequences
42
+ if (data[0] === 0x1b) {
43
+ if (data.length === 1) {
44
+ return { specialKey: SpecialKey.ESCAPE };
45
+ }
46
+ if (data.length >= 3 && data[1] === 0x5b) {
47
+ switch (data[2]) {
48
+ case 0x41:
49
+ return { specialKey: SpecialKey.UP };
50
+ case 0x42:
51
+ return { specialKey: SpecialKey.DOWN };
52
+ case 0x43:
53
+ return { specialKey: SpecialKey.RIGHT };
54
+ case 0x44:
55
+ return { specialKey: SpecialKey.LEFT };
56
+ }
57
+ }
58
+ return { specialKey: SpecialKey.ESCAPE };
59
+ }
60
+
61
+ // control characters
62
+ switch (data[0]) {
63
+ case 0x0d:
64
+ return { specialKey: SpecialKey.ENTER };
65
+ case 0x20:
66
+ return { specialKey: SpecialKey.SPACE };
67
+ case 0x7f:
68
+ return { specialKey: SpecialKey.BACKSPACE };
69
+ case 0x09:
70
+ return { specialKey: SpecialKey.TAB };
71
+ case 0x03:
72
+ return { specialKey: SpecialKey.INTERRUPT };
73
+ }
74
+
75
+ // printable characters
76
+ const str = data.toString("utf8");
77
+ if (str.length > 0) {
78
+ return { key: str };
79
+ }
80
+
81
+ return {};
82
+ }
83
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ BACKGROUND_COLOR_END,
3
+ FOREGROUND_COLOR_END,
4
+ HYPERLINK_END,
5
+ hyperlinkStart,
6
+ ITALIC_END,
7
+ ITALIC_START,
8
+ } from "./Ansi.ts";
9
+ import type Styler from "./Styler.ts";
10
+
11
+ export default class TtyStyler implements Styler {
12
+ colorEnabled: boolean = true;
13
+ hyperlinksEnabled: boolean;
14
+
15
+ readonly colorLevel: number;
16
+
17
+ /**
18
+ * The color level in use.
19
+ *
20
+ * 1 = 16 color
21
+ * 2 = 256 color
22
+ * 3 = 16 million color
23
+ */
24
+ constructor(colorLevel: number = 1, hyperlinksEnabled: boolean = true) {
25
+ this.colorLevel = colorLevel;
26
+ this.hyperlinksEnabled = hyperlinksEnabled;
27
+ }
28
+
29
+ colorText(text: string, colorValue: number): string {
30
+ if (!this.colorEnabled) {
31
+ return text;
32
+ }
33
+
34
+ if (this.colorLevel === 3) {
35
+ return Bun.color(colorValue, "ansi-16m") + text + FOREGROUND_COLOR_END;
36
+ }
37
+
38
+ if (this.colorLevel === 2) {
39
+ return Bun.color(colorValue, "ansi-256") + text + FOREGROUND_COLOR_END;
40
+ }
41
+
42
+ return Bun.color(colorValue, "ansi-16") + text + FOREGROUND_COLOR_END;
43
+ }
44
+
45
+ backgroundColorText(text: string, colorValue: number): string {
46
+ if (!this.colorEnabled) {
47
+ return text;
48
+ }
49
+
50
+ if (this.colorLevel === 3) {
51
+ const r = (colorValue >> 16) & 0xFF;
52
+ const g = (colorValue >> 8) & 0xFF;
53
+ const b = colorValue & 0xFF;
54
+ return `\x1b[48;2;${r};${g};${b}m${text}${BACKGROUND_COLOR_END}`;
55
+ }
56
+
57
+ if (this.colorLevel === 2) {
58
+ return Bun.color(colorValue, "ansi-256")!.replace("[38;", "[48;") + text +
59
+ BACKGROUND_COLOR_END;
60
+ }
61
+
62
+ return Bun.color(colorValue, "ansi-16")!.replace(
63
+ /\[(\d+)m/,
64
+ (_, n) => `[${Number(n) + 10}m`,
65
+ ) + text + BACKGROUND_COLOR_END;
66
+ }
67
+
68
+ italicText(text: string): string {
69
+ return ITALIC_START + text + ITALIC_END;
70
+ }
71
+
72
+ hyperlink(text: string, url: string): string {
73
+ if (!this.hyperlinksEnabled) {
74
+ return `${text}: ${url}`;
75
+ }
76
+ return hyperlinkStart(url) + text + HYPERLINK_END;
77
+ }
78
+ }
@@ -57,9 +57,17 @@ export default class TtyTerminal implements Terminal {
57
57
 
58
58
  columns(): number {
59
59
  try {
60
- return this.#ttyWriteStream.columns;
60
+ return this.#ttyWriteStream.columns || 80;
61
61
  } catch {
62
62
  return 80;
63
63
  }
64
64
  }
65
+
66
+ rows(): number {
67
+ try {
68
+ return this.#ttyWriteStream.rows || 24;
69
+ } catch {
70
+ return 24;
71
+ }
72
+ }
65
73
  }
@@ -0,0 +1,110 @@
1
+ import process from "node:process";
2
+ import type { WriteStream } from "node:tty";
3
+
4
+ function parseVersion(versionString = ""): {
5
+ major: number;
6
+ minor: number;
7
+ patch: number;
8
+ } {
9
+ if (/^\d{3,4}$/.test(versionString)) {
10
+ const match = /(\d{1,2})(\d{2})/.exec(versionString) ?? [];
11
+ return {
12
+ major: 0,
13
+ minor: Number.parseInt(match[1] ?? "0", 10),
14
+ patch: Number.parseInt(match[2] ?? "0", 10),
15
+ };
16
+ }
17
+
18
+ const versions = (versionString ?? "").split(".").map((n) =>
19
+ Number.parseInt(n, 10)
20
+ );
21
+ return {
22
+ major: versions[0] ?? 0,
23
+ minor: versions[1] ?? 0,
24
+ patch: versions[2] ?? 0,
25
+ };
26
+ }
27
+
28
+ export default function supportsHyperlinks(
29
+ stream: WriteStream | undefined,
30
+ ): boolean {
31
+ const {
32
+ CI,
33
+ CURSOR_TRACE_ID,
34
+ FORCE_HYPERLINK,
35
+ TEAMCITY_VERSION,
36
+ TERM_PROGRAM,
37
+ TERM_PROGRAM_VERSION,
38
+ VTE_VERSION,
39
+ TERM,
40
+ } = process.env;
41
+
42
+ if (FORCE_HYPERLINK) {
43
+ return !(FORCE_HYPERLINK.length > 0 &&
44
+ Number.parseInt(FORCE_HYPERLINK, 10) === 0);
45
+ }
46
+
47
+ if (stream && !stream.isTTY) {
48
+ return false;
49
+ }
50
+
51
+ if ("WT_SESSION" in process.env) {
52
+ return true;
53
+ }
54
+
55
+ if (CI) {
56
+ return false;
57
+ }
58
+
59
+ if (TEAMCITY_VERSION) {
60
+ return false;
61
+ }
62
+
63
+ if (TERM_PROGRAM) {
64
+ const version = parseVersion(TERM_PROGRAM_VERSION);
65
+
66
+ switch (TERM_PROGRAM) {
67
+ case "iTerm.app": {
68
+ if (version.major === 3) {
69
+ return version.minor >= 1;
70
+ }
71
+ return version.major > 3;
72
+ }
73
+ case "WezTerm": {
74
+ if (/^0-unstable-\d{4}-\d{2}-\d{2}$/.test(TERM_PROGRAM_VERSION!)) {
75
+ const date = TERM_PROGRAM_VERSION!.slice("0-unstable-".length);
76
+ return date >= "2020-06-20";
77
+ }
78
+ return version.major >= 20200620;
79
+ }
80
+ case "vscode": {
81
+ if (CURSOR_TRACE_ID) {
82
+ return true;
83
+ }
84
+ return version.major > 1 ||
85
+ (version.major === 1 && version.minor >= 72);
86
+ }
87
+ case "ghostty":
88
+ return true;
89
+ case "zed":
90
+ return true;
91
+ }
92
+ }
93
+
94
+ if (VTE_VERSION) {
95
+ if (VTE_VERSION === "0.50.0") {
96
+ return false;
97
+ }
98
+ const version = parseVersion(VTE_VERSION);
99
+ return version.major > 0 || version.minor >= 50;
100
+ }
101
+
102
+ switch (TERM) {
103
+ case "alacritty":
104
+ return true;
105
+ case "xterm-kitty":
106
+ return true;
107
+ }
108
+
109
+ return false;
110
+ }
@@ -9,7 +9,7 @@ function getKeySegment(segment: string, isCustom = false, isArray = false) {
9
9
  let keySegment = segment;
10
10
  if (!isCustom) {
11
11
  keySegment = keySegment.replace("-", "_").toUpperCase();
12
- if (keySegment[0].match(/[0-9]/)) {
12
+ if (keySegment[0]!.match(/[0-9]/)) {
13
13
  keySegment = `_${keySegment}`;
14
14
  }
15
15
  }
@@ -43,7 +43,7 @@ export function getSubCommandArgumentConfigurationKey(
43
43
  }
44
44
 
45
45
  const keySegments: Array<string> = [];
46
- const firstArgument = argumentAncestry[0];
46
+ const firstArgument = argumentAncestry[0]!;
47
47
  if (firstArgument.configurationKey === undefined) {
48
48
  keySegments.push(getKeySegment(cliConfig.name));
49
49
  keySegments.push(getKeySegment(command.name));