@flowscripter/dynamic-cli-framework 1.4.0 → 1.4.2

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.
@@ -18,7 +18,7 @@ Lint:
18
18
 
19
19
  Generate HTML API Documentation:
20
20
 
21
- `bunx typedoc --readme none index.ts`
21
+ `bunx typedoc index.ts`
22
22
 
23
23
  ### Debug Logging
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -39,5 +39,11 @@
39
39
  },
40
40
  "peerDependencies": {
41
41
  "typescript": "^6.0.3"
42
+ },
43
+ "typedocOptions": {
44
+ "readme": "none",
45
+ "exclude": [
46
+ "tests/**"
47
+ ]
42
48
  }
43
49
  }
@@ -7,4 +7,5 @@ export default interface BaseCLIFeatureOptions {
7
7
  readonly completionServiceEnabled?: boolean;
8
8
  readonly imagePrinterServiceEnabled?: boolean;
9
9
  readonly validateAllCommands?: boolean;
10
+ readonly promptingEnabled?: boolean;
10
11
  }
@@ -129,6 +129,7 @@ export default class BaseCLI implements CLI {
129
129
  completionServiceEnabled: false,
130
130
  imagePrinterServiceEnabled: false,
131
131
  validateAllCommands: false,
132
+ promptingEnabled: true,
132
133
  ...options,
133
134
  };
134
135
  this.#keyReader = keyReader;
@@ -219,6 +220,7 @@ export default class BaseCLI implements CLI {
219
220
  this.#keyReader,
220
221
  this.#printerService,
221
222
  );
223
+ prompterService.promptEnabled = this.#options.promptingEnabled;
222
224
  this.addServiceProvider(new PrompterServiceProvider(75, prompterService));
223
225
 
224
226
  if (this.#options.argumentPrompterServiceEnabled) {
@@ -233,7 +235,7 @@ export default class BaseCLI implements CLI {
233
235
  let completionService: DefaultCompletionService | undefined;
234
236
  if (this.#options.completionServiceEnabled) {
235
237
  completionService = new DefaultCompletionService();
236
- this.addServiceProvider(new CompletionServiceProvider(60, completionService));
238
+ this.addServiceProvider(new CompletionServiceProvider(5, completionService));
237
239
  }
238
240
 
239
241
  const configurationServiceProvider = new ConfigurationServiceProvider(
@@ -29,7 +29,7 @@ export default class DefaultRuntimeCLI extends BaseCLI {
29
29
  supportsHyperlinks(process.stderr),
30
30
  ),
31
31
  new TtyKeyReader(process.stdin),
32
- options,
32
+ { promptingEnabled: process.stdin.isTTY === true, ...options },
33
33
  );
34
34
  }
35
35
 
@@ -14,8 +14,10 @@ export interface PromptContext {
14
14
  export function physicalLineCount(terminal: Terminal, text: string): number {
15
15
  const cols = terminal.columns();
16
16
  if (cols <= 0) return 1;
17
- const width = Bun.stringWidth(text);
18
- return Math.max(1, Math.ceil(width / cols));
17
+ return text.split("\n").reduce((sum, segment) => {
18
+ const width = Bun.stringWidth(segment);
19
+ return sum + Math.max(1, Math.ceil(width / cols));
20
+ }, 0);
19
21
  }
20
22
 
21
23
  export function headerLineCount(terminal: Terminal, promptDef: Prompt): number {
@@ -27,6 +27,25 @@ export default class TtyKeyReader implements KeyReader {
27
27
  return new Promise((resolve) => {
28
28
  const onData = (data: Buffer): void => {
29
29
  this.#ttyReadStream.removeListener("data", onData);
30
+
31
+ // A lone 0x1b may be the start of a multi-byte escape sequence (e.g. arrow keys).
32
+ // Wait briefly for the rest of the sequence before decoding.
33
+ if (data.length === 1 && data[0] === 0x1b) {
34
+ const timeout = setTimeout(() => {
35
+ this.#ttyReadStream.removeListener("data", onMore);
36
+ resolve(TtyKeyReader.#decodeKeyEvent(data));
37
+ }, 20);
38
+
39
+ const onMore = (more: Buffer): void => {
40
+ clearTimeout(timeout);
41
+ this.#ttyReadStream.removeListener("data", onMore);
42
+ resolve(TtyKeyReader.#decodeKeyEvent(Buffer.concat([data, more])));
43
+ };
44
+
45
+ this.#ttyReadStream.once("data", onMore);
46
+ return;
47
+ }
48
+
30
49
  resolve(TtyKeyReader.#decodeKeyEvent(data));
31
50
  };
32
51
  this.#ttyReadStream.on("data", onData);
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { physicalLineCount } from "../../../../src/service/prompter/prompt/PromptContext.ts";
3
+ import type Terminal from "../../../../src/terminal/Terminal.ts";
4
+
5
+ class MockTerminal implements Terminal {
6
+ readonly #cols: number;
7
+ constructor(cols: number) {
8
+ this.#cols = cols;
9
+ }
10
+ columns(): number {
11
+ return this.#cols;
12
+ }
13
+ rows(): number {
14
+ return 24;
15
+ }
16
+ clearLine(): Promise<void> {
17
+ return Promise.resolve();
18
+ }
19
+ clearUpLines(): Promise<void> {
20
+ return Promise.resolve();
21
+ }
22
+ hideCursor(): Promise<void> {
23
+ return Promise.resolve();
24
+ }
25
+ showCursor(): Promise<void> {
26
+ return Promise.resolve();
27
+ }
28
+ write(): Promise<void> {
29
+ return Promise.resolve();
30
+ }
31
+ }
32
+
33
+ describe("physicalLineCount", () => {
34
+ test("single short text fits on one line", () => {
35
+ expect(physicalLineCount(new MockTerminal(80), "hello")).toBe(1);
36
+ });
37
+
38
+ test("text wider than terminal wraps", () => {
39
+ // 11 chars on a 10-col terminal => 2 lines
40
+ expect(physicalLineCount(new MockTerminal(10), "hello world")).toBe(2);
41
+ });
42
+
43
+ test("embedded newline on wide terminal counts as two lines", () => {
44
+ // Both parts fit on one line (terminal 300 cols > 127 and > 81),
45
+ // but the embedded \n always forces 2 rendered lines
46
+ const text =
47
+ "This will set up your terminal so that pressing TAB while typing commands will show possible options and autocomplete arguments.\n(Enabling autocompletion will modify configuration files in your home directory.)";
48
+ expect(physicalLineCount(new MockTerminal(300), text)).toBe(2);
49
+ });
50
+
51
+ test("embedded newline combined with wrapping", () => {
52
+ // "hello world" (11 chars) => ceil(11/10) = 2 lines
53
+ // "foo" (3 chars) => ceil(3/10) = 1 line
54
+ // total = 3
55
+ expect(physicalLineCount(new MockTerminal(10), "hello world\nfoo")).toBe(3);
56
+ });
57
+
58
+ test("zero columns returns 1", () => {
59
+ expect(physicalLineCount(new MockTerminal(0), "hello")).toBe(1);
60
+ });
61
+ });