@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,55 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import DefaultContext from "../../../src/runtime/DefaultContext.ts";
3
+ import { getCLIConfig } from "../../fixtures/CLIConfig.ts";
4
+ import TreePrinterServiceProvider from "../../../src/service/treePrinter/TreePrinterServiceProvider.ts";
5
+ import { PRINTER_SERVICE_ID } from "../../../src/api/service/core/PrinterService.ts";
6
+ import DefaultPrinterService from "../../../src/service/printer/DefaultPrinterService.ts";
7
+ import StreamString from "../../fixtures/StreamString.ts";
8
+ import TtyTerminal from "../../../src/terminal/TtyTerminal.ts";
9
+ import TtyStyler from "../../../src/terminal/TtyStyler.ts";
10
+
11
+ describe("TreePrinterServiceProvider tests", () => {
12
+ test("getServiceInfo returns service and empty commands", async () => {
13
+ const treePrinterServiceProvider = new TreePrinterServiceProvider(100);
14
+ const cliConfig = getCLIConfig();
15
+
16
+ const serviceInfo = await treePrinterServiceProvider.getServiceInfo(
17
+ cliConfig,
18
+ );
19
+ expect(serviceInfo.commands.length).toEqual(0);
20
+ expect(serviceInfo.service).toBeDefined();
21
+ });
22
+
23
+ test("initService sets colorEnabled and colorFunction from PrinterService", async () => {
24
+ const dummyStdout = new StreamString();
25
+ const dummyStderr = new StreamString();
26
+ const treePrinterServiceProvider = new TreePrinterServiceProvider(100);
27
+ const cliConfig = getCLIConfig();
28
+ const context = new DefaultContext(cliConfig);
29
+
30
+ context.addServiceInstance(
31
+ PRINTER_SERVICE_ID,
32
+ new DefaultPrinterService(
33
+ dummyStdout.writableStream,
34
+ dummyStderr.writableStream,
35
+ true,
36
+ true,
37
+ new TtyTerminal(dummyStdout.writeStream),
38
+ new TtyTerminal(dummyStderr.writeStream),
39
+ new TtyStyler(3),
40
+ ),
41
+ );
42
+
43
+ const serviceInfo = await treePrinterServiceProvider.getServiceInfo(
44
+ cliConfig,
45
+ );
46
+ expect(serviceInfo.commands.length).toEqual(0);
47
+
48
+ await treePrinterServiceProvider.initService(context);
49
+
50
+ const service = serviceInfo
51
+ .service as import("../../../src/service/treePrinter/DefaultTreePrinterService.ts").default;
52
+ expect(service.colorEnabled).toBe(true);
53
+ expect(typeof service.colorFunction).toBe("function");
54
+ });
55
+ });
@@ -0,0 +1,161 @@
1
+ import { deflateSync } from "node:zlib";
2
+ import { describe, expect, test } from "bun:test";
3
+ import supportsTerminalGraphics from "supports-terminal-graphics";
4
+ import ImageRenderer from "../../src/terminal/ImageRenderer.ts";
5
+ import type Terminal from "../../src/terminal/Terminal.ts";
6
+
7
+ // Build a proper PNG with valid CRC values
8
+ function crc32Table(): Uint32Array {
9
+ const table = new Uint32Array(256);
10
+ for (let n = 0; n < 256; n++) {
11
+ let c = n;
12
+ for (let k = 0; k < 8; k++) {
13
+ if (c & 1) {
14
+ c = 0xedb88320 ^ (c >>> 1);
15
+ } else {
16
+ c = c >>> 1;
17
+ }
18
+ }
19
+ table[n] = c;
20
+ }
21
+ return table;
22
+ }
23
+
24
+ const CRC_TABLE = crc32Table();
25
+
26
+ function crc32(data: Uint8Array): number {
27
+ let crc = 0xffffffff;
28
+ for (let i = 0; i < data.length; i++) {
29
+ crc = CRC_TABLE[(crc ^ data[i]!) & 0xff]! ^ (crc >>> 8);
30
+ }
31
+ return (crc ^ 0xffffffff) >>> 0;
32
+ }
33
+
34
+ function buildValidPng(
35
+ width: number,
36
+ height: number,
37
+ colorType: number = 6,
38
+ ): Uint8Array {
39
+ const parts: number[] = [];
40
+
41
+ // PNG signature
42
+ parts.push(137, 80, 78, 71, 13, 10, 26, 10);
43
+
44
+ function addChunk(type: string, data: Uint8Array) {
45
+ // Length (4 bytes big-endian)
46
+ const len = data.length;
47
+ parts.push(
48
+ (len >>> 24) & 0xff,
49
+ (len >>> 16) & 0xff,
50
+ (len >>> 8) & 0xff,
51
+ len & 0xff,
52
+ );
53
+
54
+ // Type (4 bytes)
55
+ const typeBytes = new TextEncoder().encode(type);
56
+ for (const b of typeBytes) parts.push(b);
57
+
58
+ // Data
59
+ for (const b of data) parts.push(b);
60
+
61
+ // CRC over type + data
62
+ const crcInput = new Uint8Array(4 + data.length);
63
+ crcInput.set(typeBytes, 0);
64
+ crcInput.set(data, 4);
65
+ const crcVal = crc32(crcInput);
66
+ parts.push(
67
+ (crcVal >>> 24) & 0xff,
68
+ (crcVal >>> 16) & 0xff,
69
+ (crcVal >>> 8) & 0xff,
70
+ crcVal & 0xff,
71
+ );
72
+ }
73
+
74
+ // IHDR
75
+ const ihdr = new Uint8Array(13);
76
+ const ihdrView = new DataView(ihdr.buffer);
77
+ ihdrView.setUint32(0, width);
78
+ ihdrView.setUint32(4, height);
79
+ ihdr[8] = 8; // bit depth
80
+ ihdr[9] = colorType;
81
+ ihdr[10] = 0; // compression
82
+ ihdr[11] = 0; // filter
83
+ ihdr[12] = 0; // interlace
84
+ addChunk("IHDR", ihdr);
85
+
86
+ // IDAT
87
+ const bpp = colorType === 6 ? 4 : 3;
88
+ const stride = width * bpp;
89
+ const raw = new Uint8Array(height * (1 + stride));
90
+ let offset = 0;
91
+ for (let y = 0; y < height; y++) {
92
+ raw[offset++] = 0; // filter: none
93
+ for (let x = 0; x < width; x++) {
94
+ raw[offset++] = (x * 80 + y * 40) & 0xff; // R
95
+ raw[offset++] = (x * 60 + y * 30) & 0xff; // G
96
+ raw[offset++] = (x * 40 + y * 20) & 0xff; // B
97
+ if (bpp === 4) {
98
+ raw[offset++] = 255; // A
99
+ }
100
+ }
101
+ }
102
+ const compressed = deflateSync(raw);
103
+ addChunk("IDAT", new Uint8Array(compressed));
104
+
105
+ // IEND
106
+ addChunk("IEND", new Uint8Array(0));
107
+
108
+ return new Uint8Array(parts);
109
+ }
110
+
111
+ function createMockTerminal(columns: number = 80, rows: number = 24): Terminal {
112
+ return {
113
+ clearLine: () => Promise.resolve(),
114
+ clearUpLines: () => Promise.resolve(),
115
+ hideCursor: () => Promise.resolve(),
116
+ showCursor: () => Promise.resolve(),
117
+ write: () => Promise.resolve(),
118
+ columns: () => columns,
119
+ rows: () => rows,
120
+ };
121
+ }
122
+
123
+ describe("ImageRenderer", () => {
124
+ test("renderImage returns a string with correct protocol markers", async () => {
125
+ const terminal = createMockTerminal(40, 20);
126
+ const renderer = new ImageRenderer(terminal);
127
+ const png = buildValidPng(4, 4, 6);
128
+
129
+ const result = await renderer.renderImage(png, 50);
130
+ expect(typeof result).toBe("string");
131
+ expect(result.length).toBeGreaterThan(0);
132
+
133
+ if (supportsTerminalGraphics.stdout.kitty) {
134
+ expect(result).toContain("\x1b_Gf=100");
135
+ } else if (supportsTerminalGraphics.stdout.iterm2) {
136
+ expect(result).toContain("\x1b]1337;File=inline=1;width=50%:");
137
+ } else {
138
+ expect(result).toContain("\n");
139
+ }
140
+ });
141
+
142
+ test("renderImage with default widthPercentage", async () => {
143
+ const terminal = createMockTerminal(80, 24);
144
+ const renderer = new ImageRenderer(terminal);
145
+ const png = buildValidPng(2, 2, 6);
146
+
147
+ const result = await renderer.renderImage(png);
148
+ expect(typeof result).toBe("string");
149
+ expect(result.length).toBeGreaterThan(0);
150
+ });
151
+
152
+ test("renderImage with RGB image (colorType=2)", async () => {
153
+ const terminal = createMockTerminal(40, 20);
154
+ const renderer = new ImageRenderer(terminal);
155
+ const png = buildValidPng(4, 4, 2);
156
+
157
+ const result = await renderer.renderImage(png, 50);
158
+ expect(typeof result).toBe("string");
159
+ expect(result.length).toBeGreaterThan(0);
160
+ });
161
+ });
@@ -0,0 +1,223 @@
1
+ import { deflateSync } from "node:zlib";
2
+ import { describe, expect, test } from "bun:test";
3
+ import { decodePngToRGBA } from "../../src/terminal/PngDecoder.ts";
4
+
5
+ // Helper to build a minimal PNG from raw components
6
+ function buildPng(
7
+ chunks: Array<{ type: string; data: Uint8Array }>,
8
+ ): Uint8Array {
9
+ const parts: Uint8Array[] = [];
10
+ // PNG signature
11
+ parts.push(new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]));
12
+
13
+ for (const chunk of chunks) {
14
+ const typeBytes = new TextEncoder().encode(chunk.type);
15
+ const lengthBuf = new ArrayBuffer(4);
16
+ new DataView(lengthBuf).setUint32(0, chunk.data.length);
17
+
18
+ // CRC placeholder (4 bytes of zeros - decoder doesn't validate CRC)
19
+ const crc = new Uint8Array(4);
20
+
21
+ parts.push(new Uint8Array(lengthBuf));
22
+ parts.push(typeBytes);
23
+ parts.push(chunk.data);
24
+ parts.push(crc);
25
+ }
26
+
27
+ const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
28
+ const result = new Uint8Array(totalLength);
29
+ let offset = 0;
30
+ for (const p of parts) {
31
+ result.set(p, offset);
32
+ offset += p.length;
33
+ }
34
+ return result;
35
+ }
36
+
37
+ function makeIHDR(
38
+ width: number,
39
+ height: number,
40
+ bitDepth: number,
41
+ colorType: number,
42
+ interlace: number = 0,
43
+ ): Uint8Array {
44
+ const buf = new ArrayBuffer(13);
45
+ const view = new DataView(buf);
46
+ view.setUint32(0, width);
47
+ view.setUint32(4, height);
48
+ const bytes = new Uint8Array(buf);
49
+ bytes[8] = bitDepth;
50
+ bytes[9] = colorType;
51
+ bytes[10] = 0; // compression
52
+ bytes[11] = 0; // filter method
53
+ bytes[12] = interlace;
54
+ return bytes;
55
+ }
56
+
57
+ // Build raw scanline data for a simple image with a given filter type
58
+ function makeRawScanlines(
59
+ width: number,
60
+ height: number,
61
+ bpp: number,
62
+ filterType: number,
63
+ ): Uint8Array {
64
+ const stride = width * bpp;
65
+ const raw = new Uint8Array(height * (1 + stride));
66
+ let offset = 0;
67
+ for (let y = 0; y < height; y++) {
68
+ raw[offset++] = filterType;
69
+ for (let x = 0; x < stride; x++) {
70
+ // Simple pixel values
71
+ raw[offset++] = ((y * stride + x) * 37 + 100) & 0xff;
72
+ }
73
+ }
74
+ return raw;
75
+ }
76
+
77
+ function buildSimplePng(
78
+ width: number,
79
+ height: number,
80
+ colorType: number,
81
+ filterType: number = 0,
82
+ ): Uint8Array {
83
+ const bpp = colorType === 6 ? 4 : 3;
84
+ const rawData = makeRawScanlines(width, height, bpp, filterType);
85
+ const compressed = deflateSync(rawData);
86
+
87
+ return buildPng([
88
+ { type: "IHDR", data: makeIHDR(width, height, 8, colorType) },
89
+ { type: "IDAT", data: new Uint8Array(compressed) },
90
+ { type: "IEND", data: new Uint8Array(0) },
91
+ ]);
92
+ }
93
+
94
+ describe("decodePngToRGBA", () => {
95
+ test("decodes a simple RGBA (colorType=6) PNG", () => {
96
+ const png = buildSimplePng(2, 2, 6, 0);
97
+ const result = decodePngToRGBA(png);
98
+ expect(result.width).toBe(2);
99
+ expect(result.height).toBe(2);
100
+ expect(result.pixels.length).toBe(2 * 2 * 4);
101
+ });
102
+
103
+ test("decodes a simple RGB (colorType=2) PNG and sets alpha to 255", () => {
104
+ const png = buildSimplePng(2, 2, 2, 0);
105
+ const result = decodePngToRGBA(png);
106
+ expect(result.width).toBe(2);
107
+ expect(result.height).toBe(2);
108
+ expect(result.pixels.length).toBe(2 * 2 * 4);
109
+ // Every 4th byte (alpha) should be 255
110
+ for (let i = 3; i < result.pixels.length; i += 4) {
111
+ expect(result.pixels[i]).toBe(255);
112
+ }
113
+ });
114
+
115
+ test("handles filter type 1 (Sub)", () => {
116
+ const png = buildSimplePng(3, 2, 6, 1);
117
+ const result = decodePngToRGBA(png);
118
+ expect(result.width).toBe(3);
119
+ expect(result.height).toBe(2);
120
+ });
121
+
122
+ test("handles filter type 2 (Up)", () => {
123
+ const png = buildSimplePng(3, 2, 6, 2);
124
+ const result = decodePngToRGBA(png);
125
+ expect(result.width).toBe(3);
126
+ expect(result.height).toBe(2);
127
+ });
128
+
129
+ test("handles filter type 3 (Average)", () => {
130
+ const png = buildSimplePng(3, 2, 6, 3);
131
+ const result = decodePngToRGBA(png);
132
+ expect(result.width).toBe(3);
133
+ expect(result.height).toBe(2);
134
+ });
135
+
136
+ test("handles filter type 4 (Paeth)", () => {
137
+ const png = buildSimplePng(3, 2, 6, 4);
138
+ const result = decodePngToRGBA(png);
139
+ expect(result.width).toBe(3);
140
+ expect(result.height).toBe(2);
141
+ });
142
+
143
+ test("handles filter type 4 (Paeth) with RGB colorType", () => {
144
+ const png = buildSimplePng(3, 3, 2, 4);
145
+ const result = decodePngToRGBA(png);
146
+ expect(result.width).toBe(3);
147
+ expect(result.height).toBe(3);
148
+ // Alpha should be 255 for RGB
149
+ for (let i = 3; i < result.pixels.length; i += 4) {
150
+ expect(result.pixels[i]).toBe(255);
151
+ }
152
+ });
153
+
154
+ test("throws on invalid PNG signature", () => {
155
+ const data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
156
+ expect(() => decodePngToRGBA(data)).toThrow("Invalid PNG signature");
157
+ });
158
+
159
+ test("throws on unsupported bit depth", () => {
160
+ const png = buildPng([
161
+ { type: "IHDR", data: makeIHDR(1, 1, 16, 2) },
162
+ { type: "IEND", data: new Uint8Array(0) },
163
+ ]);
164
+ expect(() => decodePngToRGBA(png)).toThrow("Unsupported bit depth: 16");
165
+ });
166
+
167
+ test("throws on unsupported color type", () => {
168
+ const png = buildPng([
169
+ { type: "IHDR", data: makeIHDR(1, 1, 8, 0) },
170
+ { type: "IEND", data: new Uint8Array(0) },
171
+ ]);
172
+ expect(() => decodePngToRGBA(png)).toThrow("Unsupported color type: 0");
173
+ });
174
+
175
+ test("throws on interlaced PNG", () => {
176
+ const png = buildPng([
177
+ { type: "IHDR", data: makeIHDR(1, 1, 8, 6, 1) },
178
+ { type: "IEND", data: new Uint8Array(0) },
179
+ ]);
180
+ expect(() => decodePngToRGBA(png)).toThrow(
181
+ "Interlaced PNGs are not supported",
182
+ );
183
+ });
184
+
185
+ test("throws on unknown filter type", () => {
186
+ // Build raw scanlines with invalid filter type 5
187
+ const width = 1;
188
+ const height = 1;
189
+ const bpp = 4;
190
+ const stride = width * bpp;
191
+ const raw = new Uint8Array(height * (1 + stride));
192
+ raw[0] = 5; // invalid filter
193
+ for (let i = 1; i <= stride; i++) raw[i] = 128;
194
+ const compressed = deflateSync(raw);
195
+
196
+ const png = buildPng([
197
+ { type: "IHDR", data: makeIHDR(width, height, 8, 6) },
198
+ { type: "IDAT", data: new Uint8Array(compressed) },
199
+ { type: "IEND", data: new Uint8Array(0) },
200
+ ]);
201
+ expect(() => decodePngToRGBA(png)).toThrow("Unknown filter type: 5");
202
+ });
203
+
204
+ test("handles multiple IDAT chunks", () => {
205
+ const bpp = 4;
206
+ const rawData = makeRawScanlines(2, 2, bpp, 0);
207
+ const compressed = new Uint8Array(deflateSync(rawData));
208
+ // Split compressed data into two chunks
209
+ const mid = Math.floor(compressed.length / 2);
210
+ const chunk1 = compressed.subarray(0, mid);
211
+ const chunk2 = compressed.subarray(mid);
212
+
213
+ const png = buildPng([
214
+ { type: "IHDR", data: makeIHDR(2, 2, 8, 6) },
215
+ { type: "IDAT", data: chunk1 },
216
+ { type: "IDAT", data: chunk2 },
217
+ { type: "IEND", data: new Uint8Array(0) },
218
+ ]);
219
+ const result = decodePngToRGBA(png);
220
+ expect(result.width).toBe(2);
221
+ expect(result.height).toBe(2);
222
+ });
223
+ });
@@ -0,0 +1,149 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import supportsHyperlinks from "../../src/terminal/supportsHyperlinks.ts";
3
+ import type { WriteStream } from "node:tty";
4
+
5
+ const envKeys = [
6
+ "CI",
7
+ "CURSOR_TRACE_ID",
8
+ "FORCE_HYPERLINK",
9
+ "TEAMCITY_VERSION",
10
+ "TERM_PROGRAM",
11
+ "TERM_PROGRAM_VERSION",
12
+ "VTE_VERSION",
13
+ "TERM",
14
+ "WT_SESSION",
15
+ ];
16
+
17
+ let savedEnv: Record<string, string | undefined>;
18
+
19
+ function ttyStream(): WriteStream {
20
+ return { isTTY: true } as WriteStream;
21
+ }
22
+
23
+ function nonTtyStream(): WriteStream {
24
+ return { isTTY: false } as WriteStream;
25
+ }
26
+
27
+ beforeEach(() => {
28
+ savedEnv = {};
29
+ for (const key of envKeys) {
30
+ savedEnv[key] = process.env[key];
31
+ delete process.env[key];
32
+ }
33
+ });
34
+
35
+ afterEach(() => {
36
+ for (const key of envKeys) {
37
+ if (savedEnv[key] === undefined) {
38
+ delete process.env[key];
39
+ } else {
40
+ process.env[key] = savedEnv[key];
41
+ }
42
+ }
43
+ });
44
+
45
+ describe("supportsHyperlinks", () => {
46
+ test("returns false for non-TTY stream", () => {
47
+ expect(supportsHyperlinks(nonTtyStream())).toBe(false);
48
+ });
49
+
50
+ test("returns false for unknown terminal", () => {
51
+ expect(supportsHyperlinks(ttyStream())).toBe(false);
52
+ });
53
+
54
+ test("FORCE_HYPERLINK=1 overrides to true", () => {
55
+ process.env.FORCE_HYPERLINK = "1";
56
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
57
+ });
58
+
59
+ test("FORCE_HYPERLINK=0 overrides to false", () => {
60
+ process.env.FORCE_HYPERLINK = "0";
61
+ expect(supportsHyperlinks(ttyStream())).toBe(false);
62
+ });
63
+
64
+ test("FORCE_HYPERLINK=1 works even for non-TTY", () => {
65
+ process.env.FORCE_HYPERLINK = "1";
66
+ expect(supportsHyperlinks(nonTtyStream())).toBe(true);
67
+ });
68
+
69
+ test("returns true for Windows Terminal", () => {
70
+ process.env.WT_SESSION = "some-session-id";
71
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
72
+ });
73
+
74
+ test("returns true for iTerm.app >= 3.1", () => {
75
+ process.env.TERM_PROGRAM = "iTerm.app";
76
+ process.env.TERM_PROGRAM_VERSION = "3.1.0";
77
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
78
+ });
79
+
80
+ test("returns false for iTerm.app < 3.1", () => {
81
+ process.env.TERM_PROGRAM = "iTerm.app";
82
+ process.env.TERM_PROGRAM_VERSION = "3.0.0";
83
+ expect(supportsHyperlinks(ttyStream())).toBe(false);
84
+ });
85
+
86
+ test("returns true for vscode >= 1.72", () => {
87
+ process.env.TERM_PROGRAM = "vscode";
88
+ process.env.TERM_PROGRAM_VERSION = "1.72.0";
89
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
90
+ });
91
+
92
+ test("returns false for vscode < 1.72", () => {
93
+ process.env.TERM_PROGRAM = "vscode";
94
+ process.env.TERM_PROGRAM_VERSION = "1.71.0";
95
+ expect(supportsHyperlinks(ttyStream())).toBe(false);
96
+ });
97
+
98
+ test("returns true for Cursor (vscode + CURSOR_TRACE_ID)", () => {
99
+ process.env.TERM_PROGRAM = "vscode";
100
+ process.env.CURSOR_TRACE_ID = "some-id";
101
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
102
+ });
103
+
104
+ test("returns true for ghostty", () => {
105
+ process.env.TERM_PROGRAM = "ghostty";
106
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
107
+ });
108
+
109
+ test("returns true for zed", () => {
110
+ process.env.TERM_PROGRAM = "zed";
111
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
112
+ });
113
+
114
+ test("returns true for WezTerm", () => {
115
+ process.env.TERM_PROGRAM = "WezTerm";
116
+ process.env.TERM_PROGRAM_VERSION = "20200620";
117
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
118
+ });
119
+
120
+ test("returns true for VTE >= 0.50 (excluding 0.50.0)", () => {
121
+ process.env.VTE_VERSION = "0.50.1";
122
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
123
+ });
124
+
125
+ test("returns false for VTE 0.50.0 (segfault bug)", () => {
126
+ process.env.VTE_VERSION = "0.50.0";
127
+ expect(supportsHyperlinks(ttyStream())).toBe(false);
128
+ });
129
+
130
+ test("returns true for alacritty", () => {
131
+ process.env.TERM = "alacritty";
132
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
133
+ });
134
+
135
+ test("returns true for xterm-kitty", () => {
136
+ process.env.TERM = "xterm-kitty";
137
+ expect(supportsHyperlinks(ttyStream())).toBe(true);
138
+ });
139
+
140
+ test("returns false in CI", () => {
141
+ process.env.CI = "true";
142
+ expect(supportsHyperlinks(ttyStream())).toBe(false);
143
+ });
144
+
145
+ test("returns false for TeamCity", () => {
146
+ process.env.TEAMCITY_VERSION = "2023.1";
147
+ expect(supportsHyperlinks(ttyStream())).toBe(false);
148
+ });
149
+ });
@@ -56,7 +56,7 @@ describe("configHelper tests", () => {
56
56
 
57
57
  expect(
58
58
  getSubCommandArgumentConfigurationKey(getCLIConfig(), command, [
59
- command.positionals![0],
59
+ command.positionals![0]!,
60
60
  ]),
61
61
  ).toBeUndefined();
62
62
 
@@ -70,7 +70,7 @@ describe("configHelper tests", () => {
70
70
 
71
71
  expect(
72
72
  getSubCommandArgumentConfigurationKey(getCLIConfig(), command, [
73
- command.positionals![0],
73
+ command.positionals![0]!,
74
74
  ]),
75
75
  ).toEqual(
76
76
  "FOO_BLAH_FOO",
@@ -86,7 +86,7 @@ describe("configHelper tests", () => {
86
86
 
87
87
  expect(
88
88
  getSubCommandArgumentConfigurationKey(getCLIConfig(), command, [
89
- command.positionals![0],
89
+ command.positionals![0]!,
90
90
  ]),
91
91
  ).toEqual(
92
92
  "FOO_BLAH_FOO[_<index>]",
@@ -103,7 +103,7 @@ describe("configHelper tests", () => {
103
103
 
104
104
  expect(
105
105
  getSubCommandArgumentConfigurationKey(getCLIConfig(), command, [
106
- command.positionals![0],
106
+ command.positionals![0]!,
107
107
  ]),
108
108
  ).toEqual(
109
109
  "FOO_BAR[_<index>]",
@@ -1,4 +1,4 @@
1
- import { describe, test } from "bun:test";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import getLogger from "../../src/util/logger.ts";
3
3
 
4
4
  describe("logger tests", () => {
@@ -31,4 +31,26 @@ describe("logger tests", () => {
31
31
 
32
32
  logger.error("foo: %s %O", "bar", { foo1: "bar1", func: () => "foobar" });
33
33
  });
34
+
35
+ test("Logger name padding is updated when a longer name is used", () => {
36
+ const shortLogger = getLogger("short");
37
+ const longLogger = getLogger("very-long-logger-name");
38
+ // Both should be valid logger instances
39
+ expect(shortLogger).toBeDefined();
40
+ expect(longLogger).toBeDefined();
41
+ // Calling all levels to exercise non-debug path
42
+ shortLogger.trace("trace msg");
43
+ shortLogger.debug("debug msg");
44
+ shortLogger.info("info msg");
45
+ shortLogger.warn("warn msg");
46
+ shortLogger.error("error msg");
47
+ });
48
+
49
+ test("Logger with same-length name reuses existing padding", () => {
50
+ // Create two loggers with same length names to hit the else-if branch
51
+ const logger1 = getLogger("aaa");
52
+ const logger2 = getLogger("bbb");
53
+ expect(logger1).toBeDefined();
54
+ expect(logger2).toBeDefined();
55
+ });
34
56
  });
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ "types": ["bun", "node"],
18
+
19
+ // Best practices
20
+ "strict": true,
21
+ "skipLibCheck": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedIndexedAccess": true,
24
+ "noImplicitOverride": true,
25
+
26
+ // Some stricter flags (disabled by default)
27
+ "noUnusedLocals": false,
28
+ "noUnusedParameters": false,
29
+ "noPropertyAccessFromIndexSignature": false
30
+ }
31
+ }
@@ -1,10 +0,0 @@
1
- const CSI = "\x1b[";
2
-
3
- export const CLEAR_LINE = CSI + "2K";
4
- export const CURSOR_LEFT = CSI + "G";
5
- export const CURSOR_UP = CSI + "1A";
6
- export const HIDE_CURSOR = CSI + "?25l";
7
- export const SHOW_CURSOR = CSI + "?25h";
8
- export const ITALIC_START = CSI + "3m";
9
- export const ITALIC_END = CSI + "23m";
10
- export const COLOR_END = CSI + "39m";