@flowscripter/dynamic-cli-framework 1.0.0

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 (133) hide show
  1. package/.github/workflows/check-bun-dependencies.yml +9 -0
  2. package/.github/workflows/lint-pr-message.yml +11 -0
  3. package/.github/workflows/release-bun-library.yml +8 -0
  4. package/.github/workflows/validate-bun-library-pr.yml +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +1475 -0
  7. package/bun.lock +63 -0
  8. package/index.ts +89 -0
  9. package/package.json +23 -0
  10. package/src/api/CLI.ts +17 -0
  11. package/src/api/CLIConfig.ts +19 -0
  12. package/src/api/Context.ts +25 -0
  13. package/src/api/RunResult.ts +142 -0
  14. package/src/api/argument/Argument.ts +36 -0
  15. package/src/api/argument/ArgumentValueTypes.ts +87 -0
  16. package/src/api/argument/ComplexOption.ts +31 -0
  17. package/src/api/argument/GlobalCommandArgument.ts +17 -0
  18. package/src/api/argument/Option.ts +32 -0
  19. package/src/api/argument/Positional.ts +27 -0
  20. package/src/api/argument/SubCommandArgument.ts +20 -0
  21. package/src/api/command/Command.ts +22 -0
  22. package/src/api/command/GlobalCommand.ts +33 -0
  23. package/src/api/command/GlobalModifierCommand.ts +12 -0
  24. package/src/api/command/GroupCommand.ts +20 -0
  25. package/src/api/command/SubCommand.ts +41 -0
  26. package/src/api/command/UsageExample.ts +21 -0
  27. package/src/api/service/ServiceProvider.ts +52 -0
  28. package/src/api/service/core/AsciiBannerGeneratorService.ts +35 -0
  29. package/src/api/service/core/KeyValueService.ts +28 -0
  30. package/src/api/service/core/PrinterService.ts +266 -0
  31. package/src/api/service/core/ShutdownService.ts +12 -0
  32. package/src/api/service/core/SyntaxHighlighterService.ts +42 -0
  33. package/src/cli/BaseCLI.ts +312 -0
  34. package/src/cli/DefaultRuntimeCLI.ts +54 -0
  35. package/src/command/MultiCommandCliHelpCommand.ts +394 -0
  36. package/src/command/SingleCommandCliHelpCommand.ts +183 -0
  37. package/src/command/UsageCommand.ts +35 -0
  38. package/src/command/VersionCommand.ts +22 -0
  39. package/src/launcher.ts +127 -0
  40. package/src/runtime/DefaultContext.ts +32 -0
  41. package/src/runtime/argument/ArgumentTypeGuards.ts +8 -0
  42. package/src/runtime/command/CommandTypeGuards.ts +30 -0
  43. package/src/runtime/command/CommandValidator.ts +549 -0
  44. package/src/runtime/parser.ts +219 -0
  45. package/src/runtime/registry/CommandRegistry.ts +121 -0
  46. package/src/runtime/registry/DefaultCommandRegistry.ts +318 -0
  47. package/src/runtime/registry/DefaultServiceProviderRegistry.ts +26 -0
  48. package/src/runtime/registry/ServiceProviderRegistry.ts +11 -0
  49. package/src/runtime/runner.ts +697 -0
  50. package/src/runtime/scanner.ts +417 -0
  51. package/src/runtime/values/ValuePopulationResult.ts +41 -0
  52. package/src/runtime/values/argumentValueMerge.ts +201 -0
  53. package/src/runtime/values/argumentValueValidation.ts +610 -0
  54. package/src/runtime/values/globalCommandValuePopulation.ts +96 -0
  55. package/src/runtime/values/subCommandValuePopulation.ts +680 -0
  56. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +45 -0
  57. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +38 -0
  58. package/src/service/asciiBannerGenerator/standard.flf.json +3 -0
  59. package/src/service/banner/BannerServiceProvider.ts +99 -0
  60. package/src/service/banner/command/NoBannerCommand.ts +42 -0
  61. package/src/service/configuration/ConfigurationServiceProvider.ts +571 -0
  62. package/src/service/configuration/DefaultKeyValueService.ts +60 -0
  63. package/src/service/configuration/command/ConfigCommand.ts +43 -0
  64. package/src/service/configuration/command/DumpConfigCommand.ts +47 -0
  65. package/src/service/printer/DefaultPrinterService.ts +256 -0
  66. package/src/service/printer/PrinterServiceProvider.ts +61 -0
  67. package/src/service/printer/command/DarkModeCommand.ts +43 -0
  68. package/src/service/printer/command/LogLevelCommand.ts +68 -0
  69. package/src/service/printer/command/NoColorCommand.ts +43 -0
  70. package/src/service/printer/terminal/Ansi.ts +10 -0
  71. package/src/service/printer/terminal/Color.ts +14 -0
  72. package/src/service/printer/terminal/Progress.ts +373 -0
  73. package/src/service/printer/terminal/Spinner.ts +100 -0
  74. package/src/service/printer/terminal/Styler.ts +5 -0
  75. package/src/service/printer/terminal/Terminal.ts +8 -0
  76. package/src/service/printer/terminal/Theme.ts +34 -0
  77. package/src/service/printer/terminal/TtyStyler.ts +39 -0
  78. package/src/service/printer/terminal/TtyTerminal.ts +65 -0
  79. package/src/service/shutdown/DefaultShutdownService.ts +9 -0
  80. package/src/service/shutdown/ShutdownServiceProvider.ts +55 -0
  81. package/src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts +52 -0
  82. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +51 -0
  83. package/src/util/configHelper.ts +95 -0
  84. package/src/util/envVarHelper.ts +338 -0
  85. package/src/util/helpHelper.ts +751 -0
  86. package/src/util/logger.ts +84 -0
  87. package/src/util/runnerHelper.ts +112 -0
  88. package/tests/cli/BaseCLI.test.ts +344 -0
  89. package/tests/command/MultiCommandCliHelpCommand.test.ts +650 -0
  90. package/tests/command/SingleCommandCliHelpCommand.test.ts +384 -0
  91. package/tests/command/UsageCommand.test.ts +53 -0
  92. package/tests/command/VersionCommand.test.ts +36 -0
  93. package/tests/fixtures/CLIConfig.ts +9 -0
  94. package/tests/fixtures/Command.ts +229 -0
  95. package/tests/fixtures/CommandRegistry.ts +8 -0
  96. package/tests/fixtures/ConfigurationServiceProvider.ts +28 -0
  97. package/tests/fixtures/Context.ts +33 -0
  98. package/tests/fixtures/ServiceProviderRegistry.ts +16 -0
  99. package/tests/fixtures/StreamString.ts +37 -0
  100. package/tests/fixtures/util.ts +41 -0
  101. package/tests/runtime/DefaultContext.test.ts +20 -0
  102. package/tests/runtime/argument/ArgumentTypeGuards.test.ts +49 -0
  103. package/tests/runtime/command/CommandTypeGuards.test.ts +43 -0
  104. package/tests/runtime/command/CommandValidator.test.ts +816 -0
  105. package/tests/runtime/parser.test.ts +766 -0
  106. package/tests/runtime/registry/DefaultCommandRegistry.test.ts +166 -0
  107. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +42 -0
  108. package/tests/runtime/runner.test.ts +1298 -0
  109. package/tests/runtime/scanner.test.ts +1113 -0
  110. package/tests/runtime/values/argumentValueMerge.test.ts +314 -0
  111. package/tests/runtime/values/argumentValueValidation.test.ts +1020 -0
  112. package/tests/runtime/values/globalCommandValuePopulation.test.ts +662 -0
  113. package/tests/runtime/values/subCommandValuePopulation.test.ts +2658 -0
  114. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +22 -0
  115. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +70 -0
  116. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +22 -0
  117. package/tests/service/asciiBannerGenerator/small.flf.json +3 -0
  118. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +249 -0
  119. package/tests/service/configuration/DefaultKeyValueService.test.ts +65 -0
  120. package/tests/service/launchBanner/BannerServiceProvider.test.ts +49 -0
  121. package/tests/service/launchBanner/__snapshots__/BannerServiceProvider.test.ts.snap +3 -0
  122. package/tests/service/launchBanner/command/NoBannerCommand.test.ts +21 -0
  123. package/tests/service/printer/DefaultPrinterService.test.ts +409 -0
  124. package/tests/service/printer/PrinterServiceProvider.test.ts +29 -0
  125. package/tests/service/printer/command/LogLevelCommand.test.ts +58 -0
  126. package/tests/service/printer/terminal/Progress.test.ts +40 -0
  127. package/tests/service/printer/terminal/Spinner.test.ts +57 -0
  128. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +13 -0
  129. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +123 -0
  130. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +41 -0
  131. package/tests/util/configHelper.test.ts +275 -0
  132. package/tests/util/envVarHelper.test.ts +318 -0
  133. package/tests/util/logger.test.ts +10 -0
@@ -0,0 +1,219 @@
1
+ import type Command from "../api/command/Command.ts";
2
+ import type GroupCommand from "../api/command/GroupCommand.ts";
3
+ import type {
4
+ ArgumentValueType,
5
+ PopulatedArgumentSingleValueType,
6
+ PopulatedArgumentValues,
7
+ } from "../api/argument/ArgumentValueTypes.ts";
8
+ import type { InvalidArgument } from "../api/RunResult.ts";
9
+ import type { CommandClause } from "./scanner.ts";
10
+ import populateGlobalCommandValue from "./values/globalCommandValuePopulation.ts";
11
+ import {
12
+ validateGlobalCommandArgumentValue,
13
+ validateOptionValue,
14
+ validatePositionalValue,
15
+ } from "./values/argumentValueValidation.ts";
16
+ import getLogger from "../util/logger.ts";
17
+ import populateSubCommandValues from "./values/subCommandValuePopulation.ts";
18
+ import type GlobalCommand from "../api/command/GlobalCommand.ts";
19
+ import type SubCommand from "../api/command/SubCommand.ts";
20
+
21
+ const logger = getLogger("parser");
22
+
23
+ /**
24
+ * A container holding the result of a {@link CommandClause} parsing operation.
25
+ */
26
+ export interface ParseResult {
27
+ /**
28
+ * The {@link Command} to execute if the parse result is valid (i.e. {@link invalidArguments} is empty).
29
+ */
30
+ readonly command: Command;
31
+
32
+ /**
33
+ * Optional parent {@link GroupCommand} if the parsed {@link command} was a member {@link SubCommand},
34
+ */
35
+ readonly groupCommand?: GroupCommand;
36
+
37
+ /**
38
+ * The argument values populated from the command line args. If the parse result is valid
39
+ * (i.e. {@link invalidArguments} is empty), this can be used to execute the specified {@link command}.
40
+ * {@link PopulatedArgumentSingleValueType} is for {@link GlobalCommand} values, {@link PopulatedArgumentValues} is for
41
+ * {@link SubCommand} values.
42
+ */
43
+ readonly populatedArgumentValues:
44
+ | PopulatedArgumentValues
45
+ | PopulatedArgumentSingleValueType;
46
+
47
+ /**
48
+ * Any arguments which were invalid.
49
+ */
50
+ readonly invalidArguments: ReadonlyArray<InvalidArgument>;
51
+
52
+ /**
53
+ * Any arguments which were unused in the parsing operation.
54
+ */
55
+ readonly unusedArgs: ReadonlyArray<string>;
56
+ }
57
+
58
+ /**
59
+ * Parse the arguments for the specified {@link CommandClause} assuming it contains a {@link SubCommand}.
60
+ *
61
+ * @param commandClause the clause to parse.
62
+ * @param defaultValues optional default {@link PopulatedArgumentValues} to use for population before parsing the provided arguments.
63
+ */
64
+ export function parseSubCommandClause(
65
+ commandClause: CommandClause,
66
+ defaultValues?: PopulatedArgumentValues,
67
+ ): ParseResult {
68
+ const command = commandClause.command as SubCommand;
69
+ const potentialArgs = commandClause.potentialArgs;
70
+ const groupCommand = commandClause.groupCommand;
71
+
72
+ const invalidArguments: Array<InvalidArgument> = [];
73
+
74
+ // check if we need to process any arguments at all
75
+ if ((command.options.length === 0) && (command.positionals.length === 0)) {
76
+ return {
77
+ command,
78
+ groupCommand,
79
+ populatedArgumentValues: {},
80
+ invalidArguments,
81
+ unusedArgs: potentialArgs,
82
+ };
83
+ }
84
+
85
+ const { populatedArgumentValues, unusedArgs, invalidArgument } =
86
+ populateSubCommandValues(
87
+ command,
88
+ potentialArgs,
89
+ defaultValues,
90
+ );
91
+
92
+ if (invalidArgument) {
93
+ invalidArguments.push(invalidArgument);
94
+ }
95
+
96
+ if (invalidArguments.length === 0) {
97
+ // validate the option values
98
+ command.options.forEach((option) => {
99
+ const validatedValue = validateOptionValue(
100
+ option,
101
+ populatedArgumentValues[option.name] || option.defaultValue,
102
+ invalidArguments,
103
+ );
104
+ if (validatedValue !== undefined) {
105
+ populatedArgumentValues[option.name] = validatedValue;
106
+ }
107
+ });
108
+
109
+ logger.debug(
110
+ "Command arguments after options validated: %j with invalid args: %j",
111
+ populatedArgumentValues,
112
+ invalidArguments,
113
+ );
114
+
115
+ if (invalidArguments.length === 0) {
116
+ // validate the positional values
117
+ command.positionals.forEach((positional) => {
118
+ const validatedValue = validatePositionalValue(
119
+ positional,
120
+ populatedArgumentValues[positional.name] as ArgumentValueType,
121
+ invalidArguments,
122
+ );
123
+ if (validatedValue !== undefined) {
124
+ populatedArgumentValues[positional.name] = validatedValue;
125
+ }
126
+ });
127
+ logger.debug(
128
+ "Command arguments after positionals validated: %j with invalid args: %j",
129
+ populatedArgumentValues,
130
+ invalidArguments,
131
+ );
132
+ }
133
+ }
134
+ if (groupCommand) {
135
+ return {
136
+ command,
137
+ populatedArgumentValues,
138
+ groupCommand,
139
+ invalidArguments,
140
+ unusedArgs,
141
+ };
142
+ }
143
+ return {
144
+ command,
145
+ populatedArgumentValues,
146
+ invalidArguments,
147
+ unusedArgs: unusedArgs,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Parse the arguments for the specified {@link CommandClause} assuming it contains a {@link GlobalCommand} or {@link GlobalModifierCommand}.
153
+ *
154
+ * @param commandClause the clause to parse.
155
+ * @param defaultValue optional default {@link PopulatedArgumentSingleValueType} to use for population before parsing the provided arguments.
156
+ */
157
+ export function parseGlobalCommandClause(
158
+ commandClause: CommandClause,
159
+ defaultValue?: PopulatedArgumentSingleValueType,
160
+ ): ParseResult {
161
+ const command = commandClause.command as GlobalCommand;
162
+ const potentialArgs = commandClause.potentialArgs;
163
+ const invalidArguments: Array<InvalidArgument> = [];
164
+
165
+ // check if we need to process an argument at all
166
+ if (!command.argument) {
167
+ return {
168
+ command,
169
+ populatedArgumentValues: undefined,
170
+ invalidArguments,
171
+ unusedArgs: potentialArgs,
172
+ };
173
+ }
174
+
175
+ let { populatedArgumentValue, unusedArgs, invalidArgument } =
176
+ populateGlobalCommandValue(
177
+ command,
178
+ potentialArgs,
179
+ defaultValue,
180
+ );
181
+
182
+ if (invalidArgument) {
183
+ invalidArguments.push(invalidArgument);
184
+ }
185
+
186
+ logger.debug(
187
+ "Command arguments for command: %s after value population: %j with invalid args: %j and unused args: %j",
188
+ command.name,
189
+ populatedArgumentValue,
190
+ invalidArguments,
191
+ unusedArgs,
192
+ );
193
+
194
+ // don't validate the value if the argument is already invalid
195
+ if (invalidArguments.length === 0) {
196
+ const validatedValue = validateGlobalCommandArgumentValue(
197
+ command,
198
+ populatedArgumentValue,
199
+ invalidArguments,
200
+ );
201
+ if (validatedValue !== undefined) {
202
+ populatedArgumentValue = validatedValue;
203
+ }
204
+ }
205
+
206
+ logger.debug(
207
+ "Command arguments for command: %s after value validation: %j with invalid args: %j",
208
+ command.name,
209
+ populatedArgumentValue,
210
+ invalidArguments,
211
+ );
212
+
213
+ return {
214
+ command,
215
+ populatedArgumentValues: populatedArgumentValue,
216
+ invalidArguments,
217
+ unusedArgs,
218
+ };
219
+ }
@@ -0,0 +1,121 @@
1
+ import type GroupCommand from "../../api/command/GroupCommand.ts";
2
+ import type SubCommand from "../../api/command/SubCommand.ts";
3
+ import type GlobalCommand from "../../api/command/GlobalCommand.ts";
4
+ import type GlobalModifierCommand from "../../api/command/GlobalModifierCommand.ts";
5
+ import type Command from "../../api/command/Command.ts";
6
+
7
+ /**
8
+ * Interface used by a {@link CLI} to register {@link Command} instances.
9
+ */
10
+ export default interface CommandRegistry {
11
+ /**
12
+ * Get all registered {@link SubCommand} instances.
13
+ */
14
+ getSubCommands(): ReadonlyArray<SubCommand>;
15
+
16
+ /**
17
+ * Get all registered {@link GroupCommand} instances.
18
+ */
19
+ getGroupCommands(): ReadonlyArray<GroupCommand>;
20
+
21
+ /**
22
+ * Get all registered {@link GlobalCommand} instances.
23
+ */
24
+ getGlobalCommands(): ReadonlyArray<GlobalCommand>;
25
+
26
+ /**
27
+ * Get all registered {@link GlobalModifierCommand} instances.
28
+ */
29
+ getGlobalModifierCommands(): ReadonlyArray<GlobalModifierCommand>;
30
+
31
+ /**
32
+ * Get a registered {@link SubCommand} by name.
33
+ */
34
+ getSubCommandByName(name: string): SubCommand | undefined;
35
+
36
+ /**
37
+ * Get a registered {@link GroupCommand} by name.
38
+ */
39
+ getGroupCommandByName(name: string): GroupCommand | undefined;
40
+
41
+ /**
42
+ * Get a registered {@link GroupCommand} and member {@link SubCommand} by
43
+ * combined name e.g. `<group-command-name>:<member-sub-command-name>`.
44
+ */
45
+ getGroupCommandAndMemberSubCommandByJoinedName(
46
+ getGroupCommandAndMemberSubCommandByName: string,
47
+ ): { groupCommand: GroupCommand; command: SubCommand } | undefined;
48
+
49
+ /**
50
+ * Get a registered {@link GlobalCommand} by name.
51
+ */
52
+ getGlobalCommandByName(name: string): GlobalCommand | undefined;
53
+
54
+ /**
55
+ * Get a registered {@link GlobalCommand} by short alias.
56
+ */
57
+ // getGlobalCommandByShortAlias(shortAlias: string): GlobalCommand | undefined;
58
+
59
+ /**
60
+ * Get a registered {@link GlobalModifierCommand} by name.
61
+ */
62
+ getGlobalModifierCommandByName(
63
+ name: string,
64
+ ): GlobalModifierCommand | undefined;
65
+
66
+ /**
67
+ * Get a registered {@link GlobalModifierCommand} by short alias.
68
+ */
69
+ // getGlobalModifierCommandByShortAlias(
70
+ // shortAlias: string,
71
+ // ): GlobalModifierCommand | undefined;
72
+
73
+ /**
74
+ * Get a map of all registered {@link GroupCommand} and member {@link SubCommand} instance combinations by
75
+ * combined name e.g. `<group-command-name>:<member-sub-command-name>`.
76
+ */
77
+ getGroupAndMemberCommandsByJoinedName(): ReadonlyMap<
78
+ string,
79
+ { groupCommand: GroupCommand; command: SubCommand }
80
+ >;
81
+
82
+ /**
83
+ * Get a map of all registered {@link GlobalModifierCommand} instances by name provided by the specified service.
84
+ */
85
+ getGlobalModifierCommandsByNameProvidedByService(
86
+ serviceId: string,
87
+ ): ReadonlyMap<string, GlobalModifierCommand>;
88
+
89
+ /**
90
+ * Get a map of all registered {@link GlobalModifierCommand} instances by short alias provided by the specified service.
91
+ */
92
+ getGlobalModifierCommandsByShortAliasProvidedByService(
93
+ serviceId: string,
94
+ ): ReadonlyMap<string, GlobalModifierCommand>;
95
+
96
+ /**
97
+ * Get a map of all registered {@link GlobalModifierCommand} instances by name not provided by a service.
98
+ */
99
+ getGlobalModifierCommandsByNameNotProvidedByService(): ReadonlyMap<
100
+ string,
101
+ GlobalModifierCommand
102
+ >;
103
+
104
+ /**
105
+ * Get a map of all registered {@link GlobalModifierCommand} instances by short alias not provided by a service.
106
+ */
107
+ getGlobalModifierCommandsByShortAliasNotProvidedByService(): ReadonlyMap<
108
+ string,
109
+ GlobalModifierCommand
110
+ >;
111
+
112
+ /**
113
+ * Get a map of all registered non-modifier {@link Command} instances by name.
114
+ */
115
+ getNonModifierCommandsByName(): ReadonlyMap<string, Command>;
116
+
117
+ /**
118
+ * Get a map of all registered {@link GlobalCommand} instances by short alias.
119
+ */
120
+ getGlobalCommandsByShortAlias(): ReadonlyMap<string, GlobalCommand>;
121
+ }
@@ -0,0 +1,318 @@
1
+ import type CommandRegistry from "./CommandRegistry.ts";
2
+ import type GlobalCommand from "../../api/command/GlobalCommand.ts";
3
+ import type GlobalModifierCommand from "../../api/command/GlobalModifierCommand.ts";
4
+ import type GroupCommand from "../../api/command/GroupCommand.ts";
5
+ import type SubCommand from "../../api/command/SubCommand.ts";
6
+ import type Command from "../../api/command/Command.ts";
7
+ import {
8
+ isGlobalCommand,
9
+ isGlobalModifierCommand,
10
+ isGroupCommand,
11
+ isSubCommand,
12
+ } from "../command/CommandTypeGuards.ts";
13
+ import getLogger from "../../util/logger.ts";
14
+ import type CommandValidator from "../command/CommandValidator.ts";
15
+
16
+ const logger = getLogger("DefaultCommandRegistry");
17
+
18
+ /**
19
+ * Default implementation of a {@link CommandRegistry}.
20
+ */
21
+ export default class DefaultCommandRegistry implements CommandRegistry {
22
+ readonly #commandValidator: CommandValidator | undefined;
23
+ readonly #subCommandsByName: Map<string, SubCommand> = new Map();
24
+ readonly #groupCommandsByName: Map<string, GroupCommand> = new Map();
25
+ readonly #groupCommandsAndMemberSubCommandsByName: Map<
26
+ string,
27
+ { groupCommand: GroupCommand; command: SubCommand }
28
+ > = new Map();
29
+ readonly #globalCommandsByName: Map<string, GlobalCommand> = new Map();
30
+ readonly #globalCommandsByShortAlias: Map<string, GlobalCommand> = new Map();
31
+ readonly #globalModifierCommandsByName: Map<
32
+ string,
33
+ GlobalModifierCommand
34
+ > = new Map();
35
+ readonly #globalModifierCommandsByShortAlias: Map<
36
+ string,
37
+ GlobalModifierCommand
38
+ > = new Map();
39
+ readonly #globalModifierCommandsByNameByServiceId: Map<
40
+ string,
41
+ Map<string, GlobalModifierCommand>
42
+ > = new Map();
43
+ readonly #globalModifierCommandsByShortAliasByServiceId: Map<
44
+ string,
45
+ Map<string, GlobalModifierCommand>
46
+ > = new Map();
47
+ readonly #globalModifierCommandsByNameWithoutServiceId: Map<
48
+ string,
49
+ GlobalModifierCommand
50
+ > = new Map();
51
+ readonly #globalModifierCommandsByShortAliasWithoutServiceId: Map<
52
+ string,
53
+ GlobalModifierCommand
54
+ > = new Map();
55
+ readonly #nonModifierCommandsByName: Map<string, Command> = new Map();
56
+
57
+ /**
58
+ * Constructor taking an optional initial list of {@link Command} instances to register.
59
+ *
60
+ * @param commands optional list of {@link Command} instances.
61
+ * @param commandValidator optional {@link CommandValidator} to use for all registered {@link Command} instances.
62
+ */
63
+ public constructor(
64
+ commands?: ReadonlyArray<Command>,
65
+ commandValidator?: CommandValidator,
66
+ ) {
67
+ this.#commandValidator = commandValidator;
68
+ commands?.forEach((command) => this.addCommand(command));
69
+ }
70
+
71
+ /**
72
+ * Validate a {@link GlobalCommand} or by extension a {@link GlobalModifierCommand} against those registered so far.
73
+ *
74
+ * @throws {Error} if the name of the provided {@link GlobalCommand} is already registered.
75
+ */
76
+ #validateGlobalCommandWithOtherGlobalCommands(
77
+ globalCommand: GlobalCommand,
78
+ ): void {
79
+ if (this.#globalCommandsByName.has(globalCommand.name)) {
80
+ throw new Error(
81
+ `Command name: ${globalCommand.name} duplicates the name of an existing command`,
82
+ );
83
+ }
84
+ if (this.#globalModifierCommandsByName.has(globalCommand.name)) {
85
+ throw new Error(
86
+ `Command name: ${globalCommand.name} duplicates the name of an existing command`,
87
+ );
88
+ }
89
+ if (globalCommand.shortAlias !== undefined) {
90
+ if (this.#globalCommandsByShortAlias.has(globalCommand.shortAlias)) {
91
+ throw new Error(
92
+ `Command name: ${globalCommand.shortAlias} duplicates the short alias of an existing command`,
93
+ );
94
+ }
95
+ if (
96
+ this.#globalModifierCommandsByShortAlias.has(globalCommand.shortAlias)
97
+ ) {
98
+ throw new Error(
99
+ `Command name: ${globalCommand.shortAlias} duplicates the short alias of an existing command`,
100
+ );
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Validate a non-global {@link Command} against those registered so far.
107
+ *
108
+ * @throws {Error} if the name of the provided {@link Command} is already registered.
109
+ */
110
+ #validateNonGlobalCommandWithOtherNonGlobalCommands(
111
+ command: Command,
112
+ ): void {
113
+ if (this.#subCommandsByName.has(command.name)) {
114
+ throw new Error(
115
+ `Command name: ${command.name} duplicates the name of an existing command`,
116
+ );
117
+ }
118
+ if (this.#groupCommandsByName.has(command.name)) {
119
+ throw new Error(
120
+ `Command name: ${command.name} duplicates the name of an existing command`,
121
+ );
122
+ }
123
+ }
124
+
125
+ public getSubCommands(): ReadonlyArray<SubCommand> {
126
+ return Array.from(this.#subCommandsByName.values());
127
+ }
128
+
129
+ public getGroupCommands(): ReadonlyArray<GroupCommand> {
130
+ return Array.from(this.#groupCommandsByName.values());
131
+ }
132
+
133
+ public getGlobalCommands(): ReadonlyArray<GlobalCommand> {
134
+ return Array.from(this.#globalCommandsByName.values());
135
+ }
136
+
137
+ public getGlobalModifierCommands(): ReadonlyArray<GlobalModifierCommand> {
138
+ return Array.from(this.#globalModifierCommandsByName.values());
139
+ }
140
+
141
+ /**
142
+ * Add a {@link Command} to the registry optionally specifying a service ID if the command
143
+ * was provided by a {@link ServiceProvider}.
144
+ *
145
+ * @param command the {@link Command} to register.
146
+ * @param serviceId the service ID if the command was provided by a {@link ServiceProvider}.
147
+ */
148
+ public addCommand(command: Command, serviceId?: string): void {
149
+ logger.debug("Adding command: %s", command.name);
150
+
151
+ if (this.#commandValidator !== undefined) {
152
+ this.#commandValidator.validate(command);
153
+ }
154
+
155
+ if (isSubCommand(command)) {
156
+ this.#validateNonGlobalCommandWithOtherNonGlobalCommands(command);
157
+ this.#subCommandsByName.set(command.name, command);
158
+ this.#nonModifierCommandsByName.set(command.name, command);
159
+ return;
160
+ }
161
+
162
+ if (isGroupCommand(command)) {
163
+ this.#validateNonGlobalCommandWithOtherNonGlobalCommands(command);
164
+ this.#groupCommandsByName.set(command.name, command);
165
+ this.#nonModifierCommandsByName.set(command.name, command);
166
+ const groupCommand = command as GroupCommand;
167
+ groupCommand.memberSubCommands.forEach((command) => {
168
+ this.#groupCommandsAndMemberSubCommandsByName.set(
169
+ `${groupCommand.name}:${command.name}`,
170
+ {
171
+ groupCommand,
172
+ command,
173
+ },
174
+ );
175
+ });
176
+ return;
177
+ }
178
+
179
+ if (isGlobalCommand(command)) {
180
+ this.#validateGlobalCommandWithOtherGlobalCommands(command);
181
+ this.#globalCommandsByName.set(command.name, command);
182
+ if (command.shortAlias !== undefined) {
183
+ this.#globalCommandsByShortAlias.set(command.shortAlias, command);
184
+ }
185
+ this.#nonModifierCommandsByName.set(command.name, command);
186
+ return;
187
+ }
188
+
189
+ if (isGlobalModifierCommand(command)) {
190
+ this.#validateGlobalCommandWithOtherGlobalCommands(command);
191
+ this.#globalModifierCommandsByName.set(command.name, command);
192
+ if (command.shortAlias !== undefined) {
193
+ this.#globalModifierCommandsByShortAlias.set(
194
+ command.shortAlias,
195
+ command,
196
+ );
197
+ }
198
+ if (serviceId === undefined) {
199
+ this.#globalModifierCommandsByNameWithoutServiceId.set(
200
+ command.name,
201
+ command,
202
+ );
203
+ if (command.shortAlias !== undefined) {
204
+ this.#globalModifierCommandsByShortAliasWithoutServiceId.set(
205
+ command.shortAlias,
206
+ command,
207
+ );
208
+ }
209
+ } else {
210
+ if (!this.#globalModifierCommandsByNameByServiceId.has(serviceId)) {
211
+ this.#globalModifierCommandsByNameByServiceId.set(
212
+ serviceId,
213
+ new Map(),
214
+ );
215
+ }
216
+ this.#globalModifierCommandsByNameByServiceId.get(serviceId)!.set(
217
+ command.name,
218
+ command,
219
+ );
220
+ if (command.shortAlias !== undefined) {
221
+ if (
222
+ !this.#globalModifierCommandsByShortAliasByServiceId.has(serviceId)
223
+ ) {
224
+ this.#globalModifierCommandsByShortAliasByServiceId.set(
225
+ serviceId,
226
+ new Map(),
227
+ );
228
+ }
229
+ this.#globalModifierCommandsByShortAliasByServiceId.get(serviceId)!
230
+ .set(command.shortAlias, command);
231
+ }
232
+ }
233
+ return;
234
+ }
235
+ }
236
+
237
+ public getSubCommandByName(name: string): SubCommand | undefined {
238
+ return this.#subCommandsByName.get(name);
239
+ }
240
+
241
+ public getGroupCommandByName(name: string): GroupCommand | undefined {
242
+ return this.#groupCommandsByName.get(name);
243
+ }
244
+
245
+ public getGroupCommandAndMemberSubCommandByJoinedName(
246
+ groupAndMemberSubCommandName: string,
247
+ ): { groupCommand: GroupCommand; command: SubCommand } | undefined {
248
+ return this.#groupCommandsAndMemberSubCommandsByName.get(
249
+ groupAndMemberSubCommandName,
250
+ );
251
+ }
252
+
253
+ public getGlobalCommandByName(name: string): GlobalCommand | undefined {
254
+ return this.#globalCommandsByName.get(name);
255
+ }
256
+
257
+ public getGlobalModifierCommandByName(
258
+ name: string,
259
+ ): GlobalModifierCommand | undefined {
260
+ return this.#globalModifierCommandsByName.get(name);
261
+ }
262
+
263
+ public getGroupAndMemberCommandsByJoinedName(): ReadonlyMap<
264
+ string,
265
+ { groupCommand: GroupCommand; command: SubCommand }
266
+ > {
267
+ return this.#groupCommandsAndMemberSubCommandsByName;
268
+ }
269
+
270
+ public getGlobalModifierCommandsByNameProvidedByService(
271
+ serviceId: string,
272
+ ): ReadonlyMap<string, GlobalModifierCommand> {
273
+ const getGlobalModifierCommandByName = this
274
+ .#globalModifierCommandsByNameByServiceId.get(serviceId);
275
+
276
+ if (getGlobalModifierCommandByName !== undefined) {
277
+ return getGlobalModifierCommandByName;
278
+ }
279
+ return new Map();
280
+ }
281
+
282
+ public getGlobalModifierCommandsByShortAliasProvidedByService(
283
+ serviceId: string,
284
+ ): ReadonlyMap<string, GlobalModifierCommand> {
285
+ const getGlobalModifierCommandByShortAlias = this
286
+ .#globalModifierCommandsByShortAliasByServiceId.get(serviceId);
287
+
288
+ if (getGlobalModifierCommandByShortAlias !== undefined) {
289
+ return getGlobalModifierCommandByShortAlias;
290
+ }
291
+ return new Map();
292
+ }
293
+
294
+ public getGlobalModifierCommandsByNameNotProvidedByService(): ReadonlyMap<
295
+ string,
296
+ GlobalModifierCommand
297
+ > {
298
+ return this.#globalModifierCommandsByNameWithoutServiceId;
299
+ }
300
+
301
+ public getGlobalModifierCommandsByShortAliasNotProvidedByService(): ReadonlyMap<
302
+ string,
303
+ GlobalModifierCommand
304
+ > {
305
+ return this.#globalModifierCommandsByShortAliasWithoutServiceId;
306
+ }
307
+
308
+ public getNonModifierCommandsByName(): ReadonlyMap<
309
+ string,
310
+ Command
311
+ > {
312
+ return this.#nonModifierCommandsByName;
313
+ }
314
+
315
+ public getGlobalCommandsByShortAlias(): ReadonlyMap<string, GlobalCommand> {
316
+ return this.#globalCommandsByShortAlias;
317
+ }
318
+ }
@@ -0,0 +1,26 @@
1
+ import type ServiceProviderRegistry from "./ServiceProviderRegistry.ts";
2
+ import type { ServiceProvider } from "../../api/service/ServiceProvider.ts";
3
+
4
+ /**
5
+ * Default implementation of a {@link ServiceProviderRegistry}.
6
+ */
7
+ export default class DefaultServiceProviderRegistry
8
+ implements ServiceProviderRegistry {
9
+ readonly #serviceProviders: Array<ServiceProvider> = [];
10
+
11
+ public constructor(serviceProviders?: ReadonlyArray<ServiceProvider>) {
12
+ if (serviceProviders !== undefined) {
13
+ this.#serviceProviders.push(...serviceProviders);
14
+ }
15
+ }
16
+
17
+ public addServiceProvider(serviceProvider: ServiceProvider): void {
18
+ this.#serviceProviders.push(serviceProvider);
19
+ }
20
+
21
+ public getServiceProviders(): ReadonlyArray<ServiceProvider> {
22
+ return this.#serviceProviders.sort((a, b) =>
23
+ b.servicePriority - a.servicePriority
24
+ );
25
+ }
26
+ }
@@ -0,0 +1,11 @@
1
+ import type { ServiceProvider } from "../../api/service/ServiceProvider.ts";
2
+
3
+ /**
4
+ * Interface used by a {@link CLI} to register {@link ServiceProvider} instances.
5
+ */
6
+ export default interface ServiceProviderRegistry {
7
+ /**
8
+ * Return all {@link ServiceProvider} instances registered in order of descending {@link ServiceProvider.servicePriority}
9
+ */
10
+ getServiceProviders(): ReadonlyArray<ServiceProvider>;
11
+ }