@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,87 @@
1
+ import type PrompterService from "../../api/service/core/PrompterService.ts";
2
+ import {
3
+ type Prompt,
4
+ type PromptResult,
5
+ PromptType,
6
+ } from "../../api/service/core/PrompterService.ts";
7
+ import type Terminal from "../../terminal/Terminal.ts";
8
+ import type KeyReader from "../../terminal/KeyReader.ts";
9
+ import type PrinterService from "../../api/service/core/PrinterService.ts";
10
+ import type { PromptContext } from "./prompt/PromptContext.ts";
11
+ import promptSingleSelect from "./prompt/promptSingleSelect.ts";
12
+ import promptMultiSelect from "./prompt/promptMultiSelect.ts";
13
+ import promptAcknowledge from "./prompt/promptAcknowledge.ts";
14
+ import promptToggle from "./prompt/promptToggle.ts";
15
+ import promptText from "./prompt/promptText.ts";
16
+ import promptOpenUrl from "./prompt/promptOpenUrl.ts";
17
+
18
+ export interface PrompterServiceConfig {
19
+ readonly inputPromptCharacters: string;
20
+ readonly checkboxCheckedChars: string;
21
+ readonly checkboxUncheckedChars: string;
22
+ readonly scrollVisibleOptions: number;
23
+ readonly openUrl?: (url: string) => Promise<void>;
24
+ }
25
+
26
+ export const DEFAULT_PROMPTER_CONFIG: PrompterServiceConfig = {
27
+ inputPromptCharacters: "> ",
28
+ checkboxCheckedChars: "[x]",
29
+ checkboxUncheckedChars: "[ ]",
30
+ scrollVisibleOptions: 8,
31
+ };
32
+
33
+ export default class DefaultPrompterService implements PrompterService {
34
+ promptEnabled = true;
35
+
36
+ readonly #ctx: PromptContext;
37
+
38
+ public constructor(
39
+ config: PrompterServiceConfig,
40
+ terminal: Terminal,
41
+ keyReader: KeyReader,
42
+ printerService: PrinterService,
43
+ ) {
44
+ this.#ctx = { config, terminal, keyReader, printerService };
45
+ }
46
+
47
+ prompt(promptDef: Prompt): Promise<PromptResult> {
48
+ if (!this.promptEnabled) {
49
+ if (promptDef.defaultOption) {
50
+ return Promise.resolve({
51
+ name: promptDef.name,
52
+ value: promptDef.defaultOption.returnedValue,
53
+ });
54
+ }
55
+ return Promise.reject(
56
+ new Error(
57
+ `Prompting is disabled and no default for prompt: ${promptDef.name}`,
58
+ ),
59
+ );
60
+ }
61
+
62
+ switch (promptDef.type) {
63
+ case PromptType.SINGLE_SELECT:
64
+ return promptSingleSelect(this.#ctx, promptDef);
65
+ case PromptType.MULTI_SELECT:
66
+ return promptMultiSelect(this.#ctx, promptDef);
67
+ case PromptType.ACKNOWLEDGE:
68
+ return promptAcknowledge(this.#ctx, promptDef);
69
+ case PromptType.TOGGLE:
70
+ return promptToggle(this.#ctx, promptDef);
71
+ case PromptType.TEXT:
72
+ return promptText(this.#ctx, promptDef);
73
+ case PromptType.OPEN_URL:
74
+ return promptOpenUrl(this.#ctx, promptDef);
75
+ }
76
+ }
77
+
78
+ async promptAll(
79
+ prompts: ReadonlyArray<Prompt>,
80
+ ): Promise<ReadonlyArray<PromptResult>> {
81
+ const results: PromptResult[] = [];
82
+ for (const p of prompts) {
83
+ results.push(await this.prompt(p));
84
+ }
85
+ return results;
86
+ }
87
+ }
@@ -0,0 +1,36 @@
1
+ import type Context from "../../api/Context.ts";
2
+ import type {
3
+ ServiceInfo,
4
+ ServiceProvider,
5
+ } from "../../api/service/ServiceProvider.ts";
6
+ import type PrompterService from "../../api/service/core/PrompterService.ts";
7
+ import { PROMPTER_SERVICE_ID } from "../../api/service/core/PrompterService.ts";
8
+ import NoPromptCommand from "./command/NoPromptCommand.ts";
9
+ import type CLIConfig from "../../api/CLIConfig.ts";
10
+
11
+ export default class PrompterServiceProvider implements ServiceProvider {
12
+ readonly serviceId: string = PROMPTER_SERVICE_ID;
13
+ readonly servicePriority: number;
14
+ readonly prompterService: PrompterService;
15
+
16
+ public constructor(
17
+ servicePriority: number,
18
+ prompterService: PrompterService,
19
+ ) {
20
+ this.servicePriority = servicePriority;
21
+ this.prompterService = prompterService;
22
+ }
23
+
24
+ public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
25
+ return Promise.resolve({
26
+ service: this.prompterService,
27
+ commands: [
28
+ new NoPromptCommand(this, this.servicePriority),
29
+ ],
30
+ });
31
+ }
32
+
33
+ initService(_context: Context): Promise<void> {
34
+ return Promise.resolve();
35
+ }
36
+ }
@@ -0,0 +1,40 @@
1
+ import type GlobalModifierCommand from "../../../api/command/GlobalModifierCommand.ts";
2
+ import type GlobalCommandArgument from "../../../api/argument/GlobalCommandArgument.ts";
3
+ import {
4
+ type ArgumentSingleValueType,
5
+ ArgumentValueTypeName,
6
+ } from "../../../api/argument/ArgumentValueTypes.ts";
7
+ import type Context from "../../../api/Context.ts";
8
+ import type PrompterServiceProvider from "../PrompterServiceProvider.ts";
9
+
10
+ export default class NoPromptCommand implements GlobalModifierCommand {
11
+ readonly name = "no-prompt";
12
+ readonly description = "Disable interactive prompting";
13
+ readonly enableConfiguration = true;
14
+ readonly argument: GlobalCommandArgument = {
15
+ type: ArgumentValueTypeName.BOOLEAN,
16
+ defaultValue: false,
17
+ configurationKey: "NO_PROMPT",
18
+ };
19
+ readonly executePriority: number;
20
+
21
+ readonly #prompterServiceProvider: PrompterServiceProvider;
22
+
23
+ public constructor(
24
+ prompterServiceProvider: PrompterServiceProvider,
25
+ executePriority: number,
26
+ ) {
27
+ this.#prompterServiceProvider = prompterServiceProvider;
28
+ this.executePriority = executePriority;
29
+ }
30
+
31
+ public execute(
32
+ _context: Context,
33
+ argumentValue: ArgumentSingleValueType,
34
+ ): Promise<void> {
35
+ this.#prompterServiceProvider.prompterService.promptEnabled =
36
+ !(argumentValue as boolean);
37
+
38
+ return Promise.resolve();
39
+ }
40
+ }
@@ -0,0 +1,47 @@
1
+ import type { Prompt } from "../../../api/service/core/PrompterService.ts";
2
+ import type Terminal from "../../../terminal/Terminal.ts";
3
+ import type KeyReader from "../../../terminal/KeyReader.ts";
4
+ import type PrinterService from "../../../api/service/core/PrinterService.ts";
5
+ import type { PrompterServiceConfig } from "../DefaultPrompterService.ts";
6
+
7
+ export interface PromptContext {
8
+ readonly config: PrompterServiceConfig;
9
+ readonly terminal: Terminal;
10
+ readonly keyReader: KeyReader;
11
+ readonly printerService: PrinterService;
12
+ }
13
+
14
+ export function physicalLineCount(
15
+ terminal: Terminal,
16
+ text: string,
17
+ ): number {
18
+ const cols = terminal.columns();
19
+ if (cols <= 0) return 1;
20
+ const width = Bun.stringWidth(text);
21
+ return Math.max(1, Math.ceil(width / cols));
22
+ }
23
+
24
+ export function headerLineCount(
25
+ terminal: Terminal,
26
+ promptDef: Prompt,
27
+ ): number {
28
+ let count = physicalLineCount(terminal, promptDef.promptText);
29
+ if (promptDef.description) {
30
+ count += physicalLineCount(terminal, promptDef.description);
31
+ }
32
+ return count;
33
+ }
34
+
35
+ export async function renderPromptHeader(
36
+ ctx: PromptContext,
37
+ promptDef: Prompt,
38
+ ): Promise<void> {
39
+ await ctx.terminal.write(
40
+ `${ctx.printerService.emphasised(promptDef.promptText)}\n`,
41
+ );
42
+ if (promptDef.description) {
43
+ await ctx.terminal.write(
44
+ `${ctx.printerService.secondary(promptDef.description)}\n`,
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ type Prompt,
3
+ type PromptResult,
4
+ PromptType,
5
+ } from "../../../api/service/core/PrompterService.ts";
6
+ import type { PromptContext } from "./PromptContext.ts";
7
+ import promptSingleSelect from "./promptSingleSelect.ts";
8
+
9
+ export default function promptAcknowledge(
10
+ ctx: PromptContext,
11
+ promptDef: Prompt,
12
+ ): Promise<PromptResult> {
13
+ const yesNo: Prompt = {
14
+ name: promptDef.name,
15
+ promptText: promptDef.promptText,
16
+ description: promptDef.description,
17
+ type: PromptType.SINGLE_SELECT,
18
+ defaultOption: promptDef.defaultOption ?? {
19
+ displayValue: "No",
20
+ returnedValue: false,
21
+ },
22
+ options: [
23
+ { displayValue: "Yes", returnedValue: true },
24
+ { displayValue: "No", returnedValue: false },
25
+ ],
26
+ };
27
+ return promptSingleSelect(ctx, yesNo);
28
+ }
@@ -0,0 +1,99 @@
1
+ import type {
2
+ Prompt,
3
+ PromptResult,
4
+ } from "../../../api/service/core/PrompterService.ts";
5
+ import { SpecialKey } from "../../../terminal/KeyReader.ts";
6
+ import ShutdownServiceProvider from "../../shutdown/ShutdownServiceProvider.ts";
7
+ import { headerLineCount, renderPromptHeader } from "./PromptContext.ts";
8
+ import type { PromptContext } from "./PromptContext.ts";
9
+
10
+ export default async function promptMultiSelect(
11
+ ctx: PromptContext,
12
+ promptDef: Prompt,
13
+ ): Promise<PromptResult> {
14
+ const options = promptDef.options;
15
+ if (options.length === 0) {
16
+ throw new Error(`No options for prompt: ${promptDef.name}`);
17
+ }
18
+
19
+ let focusIndex = 0;
20
+ const checked = new Set<number>();
21
+ let scrollOffset = 0;
22
+ const visibleCount = Math.min(
23
+ ctx.config.scrollVisibleOptions,
24
+ options.length,
25
+ );
26
+
27
+ ctx.keyReader.enableRawMode();
28
+ try {
29
+ await ctx.terminal.hideCursor();
30
+ let firstRender = true;
31
+
32
+ while (true) {
33
+ if (scrollOffset > focusIndex) {
34
+ scrollOffset = focusIndex;
35
+ } else if (focusIndex >= scrollOffset + visibleCount) {
36
+ scrollOffset = focusIndex - visibleCount + 1;
37
+ }
38
+
39
+ const headerLines = headerLineCount(ctx.terminal, promptDef);
40
+
41
+ if (!firstRender) {
42
+ await ctx.terminal.clearUpLines(visibleCount + headerLines);
43
+ }
44
+ firstRender = false;
45
+
46
+ await renderPromptHeader(ctx, promptDef);
47
+
48
+ for (let i = scrollOffset; i < scrollOffset + visibleCount; i++) {
49
+ const option = options[i]!;
50
+ const isFocused = i === focusIndex;
51
+ const isChecked = checked.has(i);
52
+ const checkbox = isChecked
53
+ ? ctx.printerService.green(ctx.config.checkboxCheckedChars)
54
+ : ctx.printerService.secondary(
55
+ ctx.config.checkboxUncheckedChars,
56
+ );
57
+ const text = option.displayValue;
58
+ await ctx.terminal.write(
59
+ `${checkbox} ${
60
+ isFocused
61
+ ? ctx.printerService.cyan(text)
62
+ : ctx.printerService.secondary(text)
63
+ }\n`,
64
+ );
65
+ }
66
+
67
+ const keyEvent = await ctx.keyReader.readKey();
68
+ if (keyEvent.specialKey === SpecialKey.UP && focusIndex > 0) {
69
+ focusIndex--;
70
+ } else if (
71
+ keyEvent.specialKey === SpecialKey.DOWN &&
72
+ focusIndex < options.length - 1
73
+ ) {
74
+ focusIndex++;
75
+ } else if (keyEvent.specialKey === SpecialKey.SPACE) {
76
+ if (checked.has(focusIndex)) {
77
+ checked.delete(focusIndex);
78
+ } else {
79
+ checked.add(focusIndex);
80
+ }
81
+ } else if (keyEvent.specialKey === SpecialKey.ENTER) {
82
+ await ctx.terminal.showCursor();
83
+ const values = [...checked].sort().map(
84
+ (i) => options[i]!.returnedValue,
85
+ );
86
+ return { name: promptDef.name, value: values };
87
+ } else if (keyEvent.specialKey === SpecialKey.ESCAPE) {
88
+ await ctx.terminal.showCursor();
89
+ throw new Error("Prompt cancelled");
90
+ } else if (keyEvent.specialKey === SpecialKey.INTERRUPT) {
91
+ await ctx.terminal.showCursor();
92
+ ShutdownServiceProvider.onInterrupt();
93
+ throw new Error("Interrupted");
94
+ }
95
+ }
96
+ } finally {
97
+ ctx.keyReader.disableRawMode();
98
+ }
99
+ }
@@ -0,0 +1,97 @@
1
+ import type {
2
+ Prompt,
3
+ PromptResult,
4
+ } from "../../../api/service/core/PrompterService.ts";
5
+ import { SpecialKey } from "../../../terminal/KeyReader.ts";
6
+ import ShutdownServiceProvider from "../../shutdown/ShutdownServiceProvider.ts";
7
+ import { renderPromptHeader } from "./PromptContext.ts";
8
+ import type { PromptContext } from "./PromptContext.ts";
9
+
10
+ function isRemoteSession(): boolean {
11
+ return !!(
12
+ process.env.SSH_CONNECTION ||
13
+ process.env.SSH_CLIENT ||
14
+ process.env.SSH_TTY
15
+ );
16
+ }
17
+
18
+ async function defaultOpenUrl(url: string): Promise<void> {
19
+ const parsed = new URL(url);
20
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
21
+ throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
22
+ }
23
+ const platform = process.platform;
24
+ let cmd: string[];
25
+ if (platform === "darwin") {
26
+ cmd = ["open", url];
27
+ } else if (platform === "win32") {
28
+ cmd = ["cmd", "/c", "start", "", url];
29
+ } else {
30
+ cmd = ["xdg-open", url];
31
+ }
32
+ const proc = Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" });
33
+ const exitCode = await proc.exited;
34
+ if (exitCode !== 0) {
35
+ throw new Error(`Failed to open URL (exit code ${exitCode})`);
36
+ }
37
+ }
38
+
39
+ export default async function promptOpenUrl(
40
+ ctx: PromptContext,
41
+ promptDef: Prompt,
42
+ ): Promise<PromptResult> {
43
+ if (promptDef.options.length === 0) {
44
+ throw new Error(`No URL option for prompt: ${promptDef.name}`);
45
+ }
46
+
47
+ const url = String(promptDef.options[0]!.returnedValue);
48
+ const displayLabel = promptDef.options[0]!.displayValue;
49
+ const openUrlFn = ctx.config.openUrl ?? defaultOpenUrl;
50
+ const canOpenBrowser = !isRemoteSession();
51
+
52
+ ctx.keyReader.enableRawMode();
53
+ try {
54
+ await renderPromptHeader(ctx, promptDef);
55
+
56
+ await ctx.terminal.write(
57
+ `${displayLabel}: ${ctx.printerService.cyan(url)}\n`,
58
+ );
59
+
60
+ const instruction = canOpenBrowser
61
+ ? "Press ENTER to open in the browser..."
62
+ : "Copy the URL above and open it in your local browser, then press ENTER to continue...";
63
+ await ctx.terminal.write(
64
+ `${ctx.printerService.secondary(instruction)}\n`,
65
+ );
66
+
67
+ while (true) {
68
+ const keyEvent = await ctx.keyReader.readKey();
69
+
70
+ if (keyEvent.specialKey === SpecialKey.ENTER) {
71
+ if (canOpenBrowser) {
72
+ try {
73
+ await openUrlFn(url);
74
+ } catch (e) {
75
+ await ctx.terminal.write(
76
+ `${
77
+ ctx.printerService.red(
78
+ `Failed to open URL: ${
79
+ e instanceof Error ? e.message : String(e)
80
+ }`,
81
+ )
82
+ }\n`,
83
+ );
84
+ }
85
+ }
86
+ return { name: promptDef.name, value: url };
87
+ } else if (keyEvent.specialKey === SpecialKey.ESCAPE) {
88
+ throw new Error("Prompt cancelled");
89
+ } else if (keyEvent.specialKey === SpecialKey.INTERRUPT) {
90
+ ShutdownServiceProvider.onInterrupt();
91
+ throw new Error("Interrupted");
92
+ }
93
+ }
94
+ } finally {
95
+ ctx.keyReader.disableRawMode();
96
+ }
97
+ }
@@ -0,0 +1,102 @@
1
+ import type {
2
+ Prompt,
3
+ PromptResult,
4
+ } from "../../../api/service/core/PrompterService.ts";
5
+ import { SpecialKey } from "../../../terminal/KeyReader.ts";
6
+ import ShutdownServiceProvider from "../../shutdown/ShutdownServiceProvider.ts";
7
+ import { headerLineCount, renderPromptHeader } from "./PromptContext.ts";
8
+ import type { PromptContext } from "./PromptContext.ts";
9
+
10
+ export default async function promptSingleSelect(
11
+ ctx: PromptContext,
12
+ promptDef: Prompt,
13
+ ): Promise<PromptResult> {
14
+ const options = promptDef.options;
15
+ if (options.length === 0) {
16
+ throw new Error(`No options for prompt: ${promptDef.name}`);
17
+ }
18
+
19
+ let selectedIndex = 0;
20
+ if (promptDef.defaultOption) {
21
+ const idx = options.findIndex(
22
+ (o) => o.returnedValue === promptDef.defaultOption!.returnedValue,
23
+ );
24
+ if (idx >= 0) selectedIndex = idx;
25
+ }
26
+
27
+ let scrollOffset = 0;
28
+ const visibleCount = Math.min(
29
+ ctx.config.scrollVisibleOptions,
30
+ options.length,
31
+ );
32
+
33
+ ctx.keyReader.enableRawMode();
34
+ try {
35
+ await ctx.terminal.hideCursor();
36
+ let firstRender = true;
37
+
38
+ while (true) {
39
+ if (scrollOffset > selectedIndex) {
40
+ scrollOffset = selectedIndex;
41
+ } else if (selectedIndex >= scrollOffset + visibleCount) {
42
+ scrollOffset = selectedIndex - visibleCount + 1;
43
+ }
44
+
45
+ const headerLines = headerLineCount(ctx.terminal, promptDef);
46
+
47
+ if (!firstRender) {
48
+ await ctx.terminal.clearUpLines(visibleCount + headerLines);
49
+ }
50
+ firstRender = false;
51
+
52
+ await renderPromptHeader(ctx, promptDef);
53
+
54
+ for (let i = scrollOffset; i < scrollOffset + visibleCount; i++) {
55
+ const option = options[i]!;
56
+ const isSelected = i === selectedIndex;
57
+ const prefix = isSelected ? ctx.config.inputPromptCharacters : " ";
58
+ const text = prefix + option.displayValue;
59
+ await ctx.terminal.write(
60
+ `${
61
+ isSelected
62
+ ? ctx.printerService.cyan(text)
63
+ : ctx.printerService.secondary(text)
64
+ }\n`,
65
+ );
66
+ }
67
+
68
+ const keyEvent = await ctx.keyReader.readKey();
69
+ if (keyEvent.specialKey === SpecialKey.UP && selectedIndex > 0) {
70
+ selectedIndex--;
71
+ } else if (
72
+ keyEvent.specialKey === SpecialKey.DOWN &&
73
+ selectedIndex < options.length - 1
74
+ ) {
75
+ selectedIndex++;
76
+ } else if (keyEvent.specialKey === SpecialKey.ENTER) {
77
+ await ctx.terminal.showCursor();
78
+ const option = options[selectedIndex]!;
79
+ const validationError = option.validate
80
+ ? option.validate(option.returnedValue)
81
+ : undefined;
82
+ if (validationError) {
83
+ await ctx.terminal.write(
84
+ `${ctx.printerService.red(validationError)}\n`,
85
+ );
86
+ firstRender = true;
87
+ continue;
88
+ }
89
+ return { name: promptDef.name, value: option.returnedValue };
90
+ } else if (keyEvent.specialKey === SpecialKey.ESCAPE) {
91
+ await ctx.terminal.showCursor();
92
+ throw new Error("Prompt cancelled");
93
+ } else if (keyEvent.specialKey === SpecialKey.INTERRUPT) {
94
+ await ctx.terminal.showCursor();
95
+ ShutdownServiceProvider.onInterrupt();
96
+ throw new Error("Interrupted");
97
+ }
98
+ }
99
+ } finally {
100
+ ctx.keyReader.disableRawMode();
101
+ }
102
+ }
@@ -0,0 +1,107 @@
1
+ import type {
2
+ Prompt,
3
+ PromptResult,
4
+ } from "../../../api/service/core/PrompterService.ts";
5
+ import type { ArgumentSingleValueType } from "../../../api/argument/ArgumentValueTypes.ts";
6
+ import { SpecialKey } from "../../../terminal/KeyReader.ts";
7
+ import ShutdownServiceProvider from "../../shutdown/ShutdownServiceProvider.ts";
8
+ import { renderPromptHeader } from "./PromptContext.ts";
9
+ import type { PromptContext } from "./PromptContext.ts";
10
+
11
+ export default async function promptText(
12
+ ctx: PromptContext,
13
+ promptDef: Prompt,
14
+ ): Promise<PromptResult> {
15
+ ctx.keyReader.enableRawMode();
16
+ try {
17
+ while (true) {
18
+ await renderPromptHeader(ctx, promptDef);
19
+ const promptMarker = ctx.printerService.cyan(
20
+ ctx.config.inputPromptCharacters,
21
+ );
22
+ await ctx.terminal.write(promptMarker);
23
+
24
+ let buffer = promptDef.defaultOption
25
+ ? String(promptDef.defaultOption.returnedValue)
26
+ : "";
27
+ if (buffer.length > 0) {
28
+ await ctx.terminal.write(buffer);
29
+ }
30
+
31
+ while (true) {
32
+ const keyEvent = await ctx.keyReader.readKey();
33
+
34
+ if (keyEvent.specialKey === SpecialKey.ENTER) {
35
+ await ctx.terminal.write("\n");
36
+ break;
37
+ } else if (keyEvent.specialKey === SpecialKey.ESCAPE) {
38
+ await ctx.terminal.write("\n");
39
+ throw new Error("Prompt cancelled");
40
+ } else if (keyEvent.specialKey === SpecialKey.INTERRUPT) {
41
+ await ctx.terminal.write("\n");
42
+ ShutdownServiceProvider.onInterrupt();
43
+ throw new Error("Interrupted");
44
+ } else if (keyEvent.specialKey === SpecialKey.BACKSPACE) {
45
+ if (buffer.length > 0) {
46
+ buffer = buffer.slice(0, -1);
47
+ await ctx.terminal.write("\b \b");
48
+ }
49
+ } else if (keyEvent.key) {
50
+ buffer += keyEvent.key;
51
+ const isSecret = promptDef.options.length > 0 &&
52
+ promptDef.options[0]!.displayValue === "__SECRET__";
53
+ await ctx.terminal.write(isSecret ? "*" : keyEvent.key);
54
+ }
55
+ }
56
+
57
+ if (buffer.length === 0 && !promptDef.defaultOption) {
58
+ await ctx.terminal.write(
59
+ ctx.printerService.red("Value required\n"),
60
+ );
61
+ continue;
62
+ }
63
+
64
+ const value: ArgumentSingleValueType = buffer;
65
+
66
+ const validationOption = promptDef.options.length > 0
67
+ ? promptDef.options[0]
68
+ : undefined;
69
+ if (validationOption?.validate) {
70
+ const error = validationOption.validate(value);
71
+ if (error) {
72
+ await ctx.terminal.write(
73
+ `${ctx.printerService.red(error)}\n`,
74
+ );
75
+ continue;
76
+ }
77
+ }
78
+
79
+ if (validationOption?.min !== undefined && typeof value === "string") {
80
+ const num = Number(value);
81
+ if (!isNaN(num) && num < validationOption.min) {
82
+ await ctx.terminal.write(
83
+ ctx.printerService.red(
84
+ `Value must be at least ${validationOption.min}\n`,
85
+ ),
86
+ );
87
+ continue;
88
+ }
89
+ }
90
+ if (validationOption?.max !== undefined && typeof value === "string") {
91
+ const num = Number(value);
92
+ if (!isNaN(num) && num > validationOption.max) {
93
+ await ctx.terminal.write(
94
+ ctx.printerService.red(
95
+ `Value must be at most ${validationOption.max}\n`,
96
+ ),
97
+ );
98
+ continue;
99
+ }
100
+ }
101
+
102
+ return { name: promptDef.name, value };
103
+ }
104
+ } finally {
105
+ ctx.keyReader.disableRawMode();
106
+ }
107
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ type Prompt,
3
+ type PromptResult,
4
+ PromptType,
5
+ } from "../../../api/service/core/PrompterService.ts";
6
+ import type { PromptContext } from "./PromptContext.ts";
7
+ import promptSingleSelect from "./promptSingleSelect.ts";
8
+
9
+ export default function promptToggle(
10
+ ctx: PromptContext,
11
+ promptDef: Prompt,
12
+ ): Promise<PromptResult> {
13
+ const trueFalse: Prompt = {
14
+ name: promptDef.name,
15
+ promptText: promptDef.promptText,
16
+ description: promptDef.description,
17
+ type: PromptType.SINGLE_SELECT,
18
+ defaultOption: promptDef.defaultOption ?? {
19
+ displayValue: "False",
20
+ returnedValue: false,
21
+ },
22
+ options: promptDef.options.length > 0 ? promptDef.options : [
23
+ { displayValue: "True", returnedValue: true },
24
+ { displayValue: "False", returnedValue: false },
25
+ ],
26
+ };
27
+ return promptSingleSelect(ctx, trueFalse);
28
+ }