@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
@@ -21,7 +21,7 @@ import type ComplexOption from "../api/argument/ComplexOption.ts";
21
21
 
22
22
  function getKeySegment(segment: string) {
23
23
  let keySegment = segment.replace("-", "_").toUpperCase();
24
- if (keySegment[0].match(/[0-9]/)) {
24
+ if (keySegment[0]!.match(/[0-9]/)) {
25
25
  keySegment = `_${keySegment}`;
26
26
  }
27
27
  return keySegment;
@@ -152,7 +152,7 @@ function getOptionValuesFromEnvVars(
152
152
  getOptionValuesFromEnvVars(
153
153
  (envVarValues[option.name] as Array<PopulatedArgumentValues>)[
154
154
  index
155
- ],
155
+ ]!,
156
156
  matches,
157
157
  env,
158
158
  property,
@@ -21,12 +21,12 @@ import type CLIConfig from "../api/CLIConfig.ts";
21
21
  import type PrinterService from "../api/service/core/PrinterService.ts";
22
22
  import type SubCommandArgument from "../api/argument/SubCommandArgument.ts";
23
23
  import type ComplexOption from "../api/argument/ComplexOption.ts";
24
+ import type TableGeneratorService from "../api/service/core/TableGeneratorService.ts";
25
+ import Table from "../api/service/core/Table.ts";
24
26
 
25
27
  const SYNTAX_INDENT_WIDTH = 2;
26
- const SYNTAX_MIN_PADDING_WIDTH = 2;
27
28
  const MINIMUM_SYNTAX_COLUMN_WIDTH = 15;
28
29
  const HELP_SYNTAX_INDENT = " ".repeat(SYNTAX_INDENT_WIDTH);
29
- const HELP_SYNTAX_PADDING = " ".repeat(SYNTAX_MIN_PADDING_WIDTH);
30
30
 
31
31
  export interface HelpEntry {
32
32
  readonly syntax: string;
@@ -40,169 +40,113 @@ export interface HelpSection {
40
40
  readonly helpEntries: Array<HelpEntry>;
41
41
  }
42
42
 
43
- async function printSingleHelpEntry(
43
+ interface FlattenedRow {
44
+ syntax: string;
45
+ description?: string;
46
+ }
47
+
48
+ function flattenHelpEntry(
44
49
  printerService: PrinterService,
45
50
  helpEntry: HelpEntry,
46
- syntaxWidth: number,
47
51
  indentText: string,
48
52
  isRoot: boolean,
49
53
  isLastSibling: boolean,
50
- ): Promise<void> {
51
- let text = "";
52
- const notesIndent = syntaxWidth;
53
- let syntaxTreeChars = "";
54
- let totalSyntaxLength = 0;
55
-
54
+ rows: FlattenedRow[],
55
+ ): void {
56
+ let syntax = "";
56
57
  if (helpEntry.syntax) {
57
58
  if (isRoot) {
58
- text += HELP_SYNTAX_INDENT + printerService.primary(helpEntry.syntax);
59
- totalSyntaxLength = SYNTAX_INDENT_WIDTH + helpEntry.syntax.length;
59
+ syntax = HELP_SYNTAX_INDENT + printerService.primary(helpEntry.syntax);
60
60
  } else {
61
+ let syntaxTreeChars = "";
61
62
  if (indentText.length > 0) {
62
63
  syntaxTreeChars = isLastSibling ? "└╴" : "├╴";
63
64
  }
64
- text += HELP_SYNTAX_INDENT +
65
+ syntax = HELP_SYNTAX_INDENT +
65
66
  printerService.secondary(indentText + syntaxTreeChars) +
66
67
  printerService.primary(helpEntry.syntax);
67
- totalSyntaxLength = SYNTAX_INDENT_WIDTH + indentText.length +
68
- syntaxTreeChars.length + helpEntry.syntax.length;
69
- }
70
- }
71
- if (helpEntry.description) {
72
- if (helpEntry.syntax) {
73
- if (totalSyntaxLength > notesIndent) {
74
- text += HELP_SYNTAX_PADDING +
75
- printerService.secondary(helpEntry.description);
76
- } else {
77
- text += " ".repeat(syntaxWidth - totalSyntaxLength) +
78
- printerService.secondary(helpEntry.description);
79
- }
80
- } else {
81
- text += HELP_SYNTAX_PADDING +
82
- printerService.secondary(helpEntry.description);
83
68
  }
84
69
  }
85
- text += "\n";
86
- await printerService.print(text);
87
- }
88
70
 
89
- async function printHelpEntry(
90
- printerService: PrinterService,
91
- helpEntry: HelpEntry,
92
- syntaxWidth: number,
93
- indentText: string,
94
- isRoot: boolean,
95
- hasChildren: boolean,
96
- isLastSibling: boolean,
97
- ): Promise<void> {
98
- if (hasChildren) {
99
- await printSingleHelpEntry(
100
- printerService,
101
- helpEntry,
102
- syntaxWidth,
103
- indentText,
104
- isRoot,
105
- isLastSibling,
106
- );
107
- indentText += (isLastSibling ? " " : "│ ") +
71
+ const description = helpEntry.description
72
+ ? printerService.secondary(helpEntry.description)
73
+ : undefined;
74
+
75
+ rows.push({ syntax, description });
76
+
77
+ if (helpEntry.helpSubEntries !== undefined) {
78
+ const nextIndent = indentText +
79
+ (isLastSibling ? " " : "│ ") +
108
80
  " ".repeat(helpEntry.subEntryIndent || 0);
109
- for (let i = 0; i < helpEntry.helpSubEntries!.length; i++) {
110
- const helpSubEntry = helpEntry.helpSubEntries![i];
111
- const helpSubEntryHasChildren = helpSubEntry.helpSubEntries !== undefined;
112
- const helpSubEntryIsLastSibling =
113
- i === helpEntry.helpSubEntries!.length - 1;
114
- await printHelpEntry(
81
+ for (let i = 0; i < helpEntry.helpSubEntries.length; i++) {
82
+ const sub = helpEntry.helpSubEntries[i]!;
83
+ flattenHelpEntry(
115
84
  printerService,
116
- helpSubEntry,
117
- syntaxWidth,
118
- indentText,
85
+ sub,
86
+ nextIndent,
119
87
  false,
120
- helpSubEntryHasChildren,
121
- helpSubEntryIsLastSibling,
88
+ i === helpEntry.helpSubEntries.length - 1,
89
+ rows,
122
90
  );
123
91
  }
124
- } else {
125
- await printSingleHelpEntry(
126
- printerService,
127
- helpEntry,
128
- syntaxWidth,
129
- indentText,
130
- isRoot,
131
- isLastSibling,
132
- );
133
- }
134
- }
135
-
136
- function getSyntaxWidth(helpEntry: HelpEntry, indent: number): number {
137
- if (
138
- (helpEntry.helpSubEntries === undefined) ||
139
- (helpEntry.helpSubEntries.length === 0)
140
- ) {
141
- // 2 is for the tree characters
142
- let finalWidth = SYNTAX_INDENT_WIDTH + indent + 2 +
143
- SYNTAX_MIN_PADDING_WIDTH;
144
- if (helpEntry.syntax) {
145
- finalWidth += helpEntry.syntax.length;
146
- }
147
- return finalWidth;
148
92
  }
149
- let maxSubIndent = 0;
150
- helpEntry.helpSubEntries.forEach((helpSubEntry) => {
151
- // 2 is for the tree characters
152
- const subEntryWidth = getSyntaxWidth(
153
- helpSubEntry,
154
- (helpEntry.subEntryIndent ? helpEntry.subEntryIndent : 0) + 2,
155
- );
156
- if (subEntryWidth > maxSubIndent) {
157
- maxSubIndent = subEntryWidth;
158
- }
159
- });
160
- return indent + maxSubIndent;
161
93
  }
162
94
 
163
95
  export async function printHelpSections(
164
96
  printerService: PrinterService,
97
+ tableGeneratorService: TableGeneratorService,
165
98
  sections: Array<HelpSection>,
166
99
  ): Promise<void> {
167
- // calculate alignment
168
- let syntaxWidth = SYNTAX_INDENT_WIDTH + MINIMUM_SYNTAX_COLUMN_WIDTH +
169
- SYNTAX_MIN_PADDING_WIDTH;
170
- sections.forEach((section) => {
171
- section.helpEntries.forEach((helpEntry) => {
172
- const entrySyntaxWidth = getSyntaxWidth(helpEntry, 0);
173
- // don't expand out if no description as we can overwrite that space on the line
174
- if (
175
- helpEntry.description && (helpEntry.description.length > 0) &&
176
- (entrySyntaxWidth > syntaxWidth)
177
- ) {
178
- syntaxWidth = entrySyntaxWidth;
179
- }
180
- });
181
- });
100
+ const terminalWidth = printerService.stdoutColumns();
182
101
 
183
- // print the help
184
102
  for (let i = 0; i < sections.length; i++) {
185
- const section = sections[i];
103
+ const section = sections[i]!;
186
104
  if (section.title.length > 0) {
187
105
  await printerService.print(
188
106
  `${printerService.emphasised(section.title)}\n\n`,
189
107
  );
190
108
  }
191
- for (let j = 0; j < section.helpEntries.length; j++) {
192
- const helpEntry = section.helpEntries[j];
193
- await printHelpEntry(
109
+
110
+ if (section.helpEntries.length === 0) continue;
111
+
112
+ const rows: FlattenedRow[] = [];
113
+ for (const helpEntry of section.helpEntries) {
114
+ flattenHelpEntry(
194
115
  printerService,
195
116
  helpEntry,
196
- syntaxWidth,
197
117
  "",
198
118
  true,
199
- helpEntry.helpSubEntries !== undefined,
200
119
  true,
120
+ rows,
201
121
  );
202
122
  }
203
- if (section.helpEntries.length > 0) {
204
- await printerService.print("\n");
123
+
124
+ const hasDescriptions = rows.some((r) => r.description !== undefined);
125
+
126
+ if (hasDescriptions) {
127
+ const table = new Table(rows.length, 2, {
128
+ border: false,
129
+ padding: 1,
130
+ maxWidth: terminalWidth,
131
+ });
132
+ table.column(0, { minWidth: MINIMUM_SYNTAX_COLUMN_WIDTH });
133
+ table.column(1, { flexWeight: 1 });
134
+
135
+ for (let r = 0; r < rows.length; r++) {
136
+ const row = rows[r]!;
137
+ table.cell(r, 0, row.syntax);
138
+ table.cell(r, 1, row.description ?? "");
139
+ }
140
+
141
+ const rendered = tableGeneratorService.render(table);
142
+ await printerService.print(rendered + "\n");
143
+ } else {
144
+ for (const row of rows) {
145
+ await printerService.print(row.syntax + "\n");
146
+ }
205
147
  }
148
+
149
+ await printerService.print("\n");
206
150
  }
207
151
  }
208
152
 
@@ -86,9 +86,11 @@ function wrapDefaultLogger(
86
86
  * @returns A logger instance with the specified name.
87
87
  */
88
88
  export default function getLogger(loggerName: string): Logger {
89
- if (globalThis.defaultLogger === undefined) {
90
- globalThis.defaultLogger = getDefaultLogger();
89
+ const g = globalThis as Record<string, unknown>;
90
+ if (g.defaultLogger === undefined) {
91
+ g.defaultLogger = getDefaultLogger();
91
92
  }
93
+ const defaultLogger = g.defaultLogger as Logger;
92
94
 
93
95
  if (loggerName.length > maxLoggerNameLength) {
94
96
  maxLoggerNameLength = loggerName.length;
@@ -108,28 +110,28 @@ export default function getLogger(loggerName: string): Logger {
108
110
  return {
109
111
  trace: wrapDefaultLogger(
110
112
  loggerName,
111
- LEVEL_PADDINGS.TRACE,
112
- globalThis.defaultLogger.trace,
113
+ LEVEL_PADDINGS.TRACE!,
114
+ defaultLogger.trace,
113
115
  ),
114
116
  debug: wrapDefaultLogger(
115
117
  loggerName,
116
- LEVEL_PADDINGS.DEBUG,
117
- globalThis.defaultLogger.debug,
118
+ LEVEL_PADDINGS.DEBUG!,
119
+ defaultLogger.debug,
118
120
  ),
119
121
  info: wrapDefaultLogger(
120
122
  loggerName,
121
- LEVEL_PADDINGS.INFO,
122
- globalThis.defaultLogger.info,
123
+ LEVEL_PADDINGS.INFO!,
124
+ defaultLogger.info,
123
125
  ),
124
126
  warn: wrapDefaultLogger(
125
127
  loggerName,
126
- LEVEL_PADDINGS.WARN,
127
- globalThis.defaultLogger.warn,
128
+ LEVEL_PADDINGS.WARN!,
129
+ defaultLogger.warn,
128
130
  ),
129
131
  error: wrapDefaultLogger(
130
132
  loggerName,
131
- LEVEL_PADDINGS.ERROR,
132
- globalThis.defaultLogger.error,
133
+ LEVEL_PADDINGS.ERROR!,
134
+ defaultLogger.error,
133
135
  ),
134
136
  };
135
137
  }
@@ -140,8 +142,8 @@ export default function getLogger(loggerName: string): Logger {
140
142
  warn: () => {},
141
143
  error: wrapDefaultLogger(
142
144
  loggerName,
143
- LEVEL_PADDINGS.ERROR,
144
- globalThis.defaultLogger.error,
145
+ LEVEL_PADDINGS.ERROR!,
146
+ defaultLogger.error,
145
147
  ),
146
148
  };
147
149
  }
@@ -10,7 +10,7 @@ import {
10
10
  } from "../runtime/command/CommandTypeGuards.ts";
11
11
  import type Context from "../api/Context.ts";
12
12
  import { getInvalidArgumentString } from "../runtime/values/argumentValueValidation.ts";
13
- import CommandRegistry from "../runtime/registry/CommandRegistry.ts";
13
+ import type CommandRegistry from "../runtime/registry/CommandRegistry.ts";
14
14
  import { findPossibleCommandNames } from "./helpHelper.ts";
15
15
 
16
16
  function getCommandString(
@@ -23,24 +23,31 @@ import type Context from "../../src/api/Context.ts";
23
23
  import type CLIConfig from "../../src/api/CLIConfig.ts";
24
24
  import StreamString from "../fixtures/StreamString.ts";
25
25
  import { expectStringEquals, expectStringIncludes } from "../fixtures/util.ts";
26
- import TtyTerminal from "../../src/service/printer/terminal/TtyTerminal.ts";
27
- import TtyStyler from "../../src/service/printer/terminal/TtyStyler.ts";
26
+ import TtyTerminal from "../../src/terminal/TtyTerminal.ts";
27
+ import TtyStyler from "../../src/terminal/TtyStyler.ts";
28
+ import type KeyReader from "../../src/terminal/KeyReader.ts";
29
+
30
+ const mockKeyReader: KeyReader = {
31
+ enableRawMode() {},
32
+ disableRawMode() {},
33
+ readKey: () => Promise.resolve({}),
34
+ };
28
35
 
29
36
  describe("BaseCLI tests", () => {
30
37
  test("BaseCLI no command specified works", async () => {
31
38
  const config = getCLIConfig();
32
39
  const dummyStdout = new StreamString();
33
40
  const dummyStderr = new StreamString();
34
- const terminal = new TtyTerminal(dummyStdout.writeStream);
35
41
  const baseCLI = new BaseCLI(
36
42
  config,
37
43
  dummyStdout.writableStream,
38
44
  dummyStderr.writableStream,
39
45
  false,
40
46
  false,
41
- terminal,
47
+ new TtyTerminal(dummyStdout.writeStream),
48
+ new TtyTerminal(dummyStderr.writeStream),
42
49
  new TtyStyler(3),
43
- false,
50
+ mockKeyReader,
44
51
  );
45
52
 
46
53
  baseCLI.addCommand(getSubCommandWithOptionAndPositional());
@@ -54,16 +61,16 @@ describe("BaseCLI tests", () => {
54
61
  const config = getCLIConfig();
55
62
  const dummyStdout = new StreamString();
56
63
  const dummyStderr = new StreamString();
57
- const terminal = new TtyTerminal(dummyStderr.writeStream);
58
64
  const baseCLI = new BaseCLI(
59
65
  config,
60
66
  dummyStdout.writableStream,
61
67
  dummyStderr.writableStream,
62
68
  false,
63
69
  false,
64
- terminal,
70
+ new TtyTerminal(dummyStdout.writeStream),
71
+ new TtyTerminal(dummyStderr.writeStream),
65
72
  new TtyStyler(3),
66
- false,
73
+ mockKeyReader,
67
74
  );
68
75
 
69
76
  let modifierHasRun = false;
@@ -122,11 +129,14 @@ describe("BaseCLI tests", () => {
122
129
  dummyStderr.writableStream,
123
130
  false,
124
131
  false,
132
+ new TtyTerminal(dummyStdout.writeStream),
125
133
  new TtyTerminal(dummyStderr.writeStream),
126
134
  new TtyStyler(3),
127
- false,
128
- true,
129
- true,
135
+ mockKeyReader,
136
+ {
137
+ configFileSupportEnabled: true,
138
+ keyValueServiceEnabled: true,
139
+ },
130
140
  );
131
141
 
132
142
  let serviceProvider1Initialised = false;
@@ -140,21 +150,20 @@ describe("BaseCLI tests", () => {
140
150
  const SERVICE_2 = "service2";
141
151
 
142
152
  interface ServiceInterface {
143
- serviceMethod(context: Context): void;
153
+ serviceMethod(context: Context): Promise<void>;
144
154
  }
145
155
 
146
156
  class DefaultService1 implements ServiceInterface {
147
157
  copyOfKeyValueService: KeyValueService | undefined;
148
158
 
149
- serviceMethod(context: Context): void {
150
- // should only have access to the modifierCommand KV scope
151
- expect(this.copyOfKeyValueService!.hasKey("name")).toBeTrue();
159
+ async serviceMethod(context: Context): Promise<void> {
160
+ expect(await this.copyOfKeyValueService!.hasKey("name")).toBeTrue();
152
161
  expect(
153
- this.copyOfKeyValueService!.getKey("name"),
162
+ await this.copyOfKeyValueService!.getKey("name"),
154
163
  ).toEqual(
155
164
  "modifierCommand",
156
165
  );
157
- this.copyOfKeyValueService!.setKey(
166
+ await this.copyOfKeyValueService!.setKey(
158
167
  "name",
159
168
  "defaultService1+modifierCommand",
160
169
  );
@@ -163,10 +172,9 @@ describe("BaseCLI tests", () => {
163
172
  KEY_VALUE_SERVICE_ID,
164
173
  ) as KeyValueService;
165
174
 
166
- // should only have access to the modifierCommand KV scope
167
- expect(keyValueService.hasKey("name")).toBeTrue();
175
+ expect(await keyValueService.hasKey("name")).toBeTrue();
168
176
  expect(
169
- this.copyOfKeyValueService!.getKey("name"),
177
+ await this.copyOfKeyValueService!.getKey("name"),
170
178
  ).toEqual(
171
179
  "defaultService1+modifierCommand",
172
180
  );
@@ -178,13 +186,12 @@ describe("BaseCLI tests", () => {
178
186
  class DefaultService2 implements ServiceInterface {
179
187
  copyOfKeyValueService: KeyValueService | undefined;
180
188
 
181
- serviceMethod(context: Context): void {
182
- // should only have access to the modifierCommand KV scope
183
- expect(this.copyOfKeyValueService!.hasKey("name")).toBeTrue();
184
- expect(this.copyOfKeyValueService!.getKey("name")).toEqual(
189
+ async serviceMethod(context: Context): Promise<void> {
190
+ expect(await this.copyOfKeyValueService!.hasKey("name")).toBeTrue();
191
+ expect(await this.copyOfKeyValueService!.getKey("name")).toEqual(
185
192
  "subCommand",
186
193
  );
187
- this.copyOfKeyValueService!.setKey(
194
+ await this.copyOfKeyValueService!.setKey(
188
195
  "name",
189
196
  "defaultService2+subCommand",
190
197
  );
@@ -193,10 +200,9 @@ describe("BaseCLI tests", () => {
193
200
  KEY_VALUE_SERVICE_ID,
194
201
  ) as KeyValueService;
195
202
 
196
- // should only have access to the subCommand KV scope
197
- expect(keyValueService.hasKey("name")).toBeTrue();
203
+ expect(await keyValueService.hasKey("name")).toBeTrue();
198
204
  expect(
199
- this.copyOfKeyValueService!.getKey("name"),
205
+ await this.copyOfKeyValueService!.getKey("name"),
200
206
  ).toEqual(
201
207
  "defaultService2+subCommand",
202
208
  );
@@ -211,7 +217,7 @@ describe("BaseCLI tests", () => {
211
217
 
212
218
  defaultService1: DefaultService1 | undefined;
213
219
 
214
- provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
220
+ getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
215
221
  this.defaultService1 = new DefaultService1();
216
222
  return Promise.resolve({
217
223
  service: this.defaultService1,
@@ -219,20 +225,17 @@ describe("BaseCLI tests", () => {
219
225
  });
220
226
  }
221
227
 
222
- initService(context: Context): Promise<void> {
228
+ async initService(context: Context): Promise<void> {
223
229
  const keyValueService = context.getServiceById(
224
230
  KEY_VALUE_SERVICE_ID,
225
231
  ) as KeyValueService;
226
232
 
227
- // should only have access to the ServiceProvider1 KV scope
228
- expect(keyValueService.hasKey("name")).toBeFalse();
229
- keyValueService.setKey("name", "defaultService2");
233
+ expect(await keyValueService.hasKey("name")).toBeFalse();
234
+ await keyValueService.setKey("name", "defaultService2");
230
235
 
231
- // take a copy to check later when service is accessed via a command
232
236
  this.defaultService1!.copyOfKeyValueService = keyValueService;
233
237
 
234
238
  serviceProvider1Initialised = true;
235
- return Promise.resolve(undefined);
236
239
  }
237
240
  }
238
241
 
@@ -242,7 +245,7 @@ describe("BaseCLI tests", () => {
242
245
 
243
246
  defaultService2: DefaultService2 | undefined;
244
247
 
245
- provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
248
+ getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
246
249
  this.defaultService2 = new DefaultService2();
247
250
  return Promise.resolve({
248
251
  service: this.defaultService2,
@@ -250,20 +253,17 @@ describe("BaseCLI tests", () => {
250
253
  });
251
254
  }
252
255
 
253
- initService(context: Context): Promise<void> {
256
+ async initService(context: Context): Promise<void> {
254
257
  const keyValueService = context.getServiceById(
255
258
  KEY_VALUE_SERVICE_ID,
256
259
  ) as KeyValueService;
257
260
 
258
- // should only have access to the ServiceProvider2 KV scope
259
- expect(keyValueService.hasKey("name")).toBeFalse();
260
- keyValueService.setKey("name", "defaultService2");
261
+ expect(await keyValueService.hasKey("name")).toBeFalse();
262
+ await keyValueService.setKey("name", "defaultService2");
261
263
 
262
- // take a copy to check later when service is accessed via a command
263
264
  this.defaultService2!.copyOfKeyValueService = keyValueService;
264
265
 
265
266
  serviceProvider2Initialised = true;
266
- return Promise.resolve(undefined);
267
267
  }
268
268
  }
269
269
 
@@ -282,37 +282,33 @@ describe("BaseCLI tests", () => {
282
282
  };
283
283
  const subCommand = getSubCommand("command", [option], []);
284
284
 
285
- modifierCommand.execute = (context): Promise<void> => {
285
+ modifierCommand.execute = async (context): Promise<void> => {
286
286
  const keyValueService = context.getServiceById(
287
287
  KEY_VALUE_SERVICE_ID,
288
288
  ) as KeyValueService;
289
289
 
290
- // should only have access to the modifierCommand KV scope
291
- expect(keyValueService.hasKey("name")).toBeFalse();
292
- keyValueService.setKey("name", "modifierCommand");
290
+ expect(await keyValueService.hasKey("name")).toBeFalse();
291
+ await keyValueService.setKey("name", "modifierCommand");
293
292
 
294
293
  const service1 = context.getServiceById(SERVICE_1) as ServiceInterface;
295
294
 
296
- service1.serviceMethod(context);
295
+ await service1.serviceMethod(context);
297
296
 
298
297
  modifierHasRun = true;
299
- return Promise.resolve();
300
298
  };
301
- subCommand.execute = (context): Promise<void> => {
299
+ subCommand.execute = async (context): Promise<void> => {
302
300
  const keyValueService = context.getServiceById(
303
301
  KEY_VALUE_SERVICE_ID,
304
302
  ) as KeyValueService;
305
303
 
306
- // should only have access to the subCommand KV scope
307
- expect(keyValueService.hasKey("name")).toBeFalse();
308
- keyValueService.setKey("name", "subCommand");
304
+ expect(await keyValueService.hasKey("name")).toBeFalse();
305
+ await keyValueService.setKey("name", "subCommand");
309
306
 
310
307
  const service2 = context.getServiceById(SERVICE_2) as ServiceInterface;
311
308
 
312
- service2.serviceMethod(context);
309
+ await service2.serviceMethod(context);
313
310
 
314
311
  subHasRun = true;
315
- return Promise.resolve();
316
312
  };
317
313
 
318
314
  baseCLI.addServiceProvider(new ServiceProvider1());
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ CompletionCompleteSubCommand,
4
+ CompletionGroupCommand,
5
+ CompletionIntegrationSubCommand,
6
+ } from "../../src/command/CompletionCommand.ts";
7
+
8
+ describe("CompletionIntegrationSubCommand", () => {
9
+ test("has correct name", () => {
10
+ const cmd = new CompletionIntegrationSubCommand();
11
+ expect(cmd.name).toEqual("integration");
12
+ });
13
+
14
+ test("has shell positional with allowable values", () => {
15
+ const cmd = new CompletionIntegrationSubCommand();
16
+ expect(cmd.positionals.length).toEqual(1);
17
+ expect(cmd.positionals[0]!.name).toEqual("shell");
18
+ expect(cmd.positionals[0]!.allowableValues).toEqual([
19
+ "bash",
20
+ "zsh",
21
+ "fish",
22
+ "powershell",
23
+ ]);
24
+ });
25
+
26
+ test("has optional config-path option", () => {
27
+ const cmd = new CompletionIntegrationSubCommand();
28
+ expect(cmd.options.length).toEqual(1);
29
+ expect(cmd.options[0]!.name).toEqual("config-path");
30
+ expect(cmd.options[0]!.isOptional).toEqual(true);
31
+ });
32
+ });
33
+
34
+ describe("CompletionCompleteSubCommand", () => {
35
+ test("has correct name", () => {
36
+ const cmd = new CompletionCompleteSubCommand();
37
+ expect(cmd.name).toEqual("complete");
38
+ });
39
+
40
+ test("has shell positional and vararg args positional", () => {
41
+ const cmd = new CompletionCompleteSubCommand();
42
+ expect(cmd.positionals.length).toEqual(2);
43
+ expect(cmd.positionals[0]!.name).toEqual("shell");
44
+ expect(cmd.positionals[1]!.name).toEqual("args");
45
+ expect(cmd.positionals[1]!.isVarargMultiple).toEqual(true);
46
+ expect(cmd.positionals[1]!.isVarargOptional).toEqual(true);
47
+ });
48
+ });
49
+
50
+ describe("CompletionGroupCommand", () => {
51
+ test("has correct name", () => {
52
+ const cmd = new CompletionGroupCommand(
53
+ new CompletionIntegrationSubCommand(),
54
+ new CompletionCompleteSubCommand(),
55
+ );
56
+ expect(cmd.name).toEqual("completions");
57
+ });
58
+
59
+ test("has two member sub-commands", () => {
60
+ const cmd = new CompletionGroupCommand(
61
+ new CompletionIntegrationSubCommand(),
62
+ new CompletionCompleteSubCommand(),
63
+ );
64
+ expect(cmd.memberSubCommands.length).toEqual(2);
65
+ expect(cmd.memberSubCommands[0]!.name).toEqual("integration");
66
+ expect(cmd.memberSubCommands[1]!.name).toEqual("complete");
67
+ });
68
+ });
@@ -9,8 +9,8 @@ import type Context from "../../src/api/Context.ts";
9
9
  import type Command from "../../src/api/command/Command.ts";
10
10
  import StreamString from "../fixtures/StreamString.ts";
11
11
  import { expectStringEquals } from "../fixtures/util.ts";
12
- import TtyTerminal from "../../src/service/printer/terminal/TtyTerminal.ts";
13
- import TtyStyler from "../../src/service/printer/terminal/TtyStyler.ts";
12
+ import TtyTerminal from "../../src/terminal/TtyTerminal.ts";
13
+ import TtyStyler from "../../src/terminal/TtyStyler.ts";
14
14
 
15
15
  describe("UsageCommand tests", () => {
16
16
  test("Usage works", async () => {
@@ -21,6 +21,7 @@ describe("UsageCommand tests", () => {
21
21
  dummyStderr.writableStream,
22
22
  true,
23
23
  true,
24
+ new TtyTerminal(dummyStdout.writeStream),
24
25
  new TtyTerminal(dummyStderr.writeStream),
25
26
  new TtyStyler(3),
26
27
  );