@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.
- package/.github/workflows/check-bun-dependencies.yml +9 -0
- package/.github/workflows/lint-pr-message.yml +11 -0
- package/.github/workflows/release-bun-library.yml +8 -0
- package/.github/workflows/validate-bun-library-pr.yml +8 -0
- package/LICENSE +21 -0
- package/README.md +1475 -0
- package/bun.lock +63 -0
- package/index.ts +89 -0
- package/package.json +23 -0
- package/src/api/CLI.ts +17 -0
- package/src/api/CLIConfig.ts +19 -0
- package/src/api/Context.ts +25 -0
- package/src/api/RunResult.ts +142 -0
- package/src/api/argument/Argument.ts +36 -0
- package/src/api/argument/ArgumentValueTypes.ts +87 -0
- package/src/api/argument/ComplexOption.ts +31 -0
- package/src/api/argument/GlobalCommandArgument.ts +17 -0
- package/src/api/argument/Option.ts +32 -0
- package/src/api/argument/Positional.ts +27 -0
- package/src/api/argument/SubCommandArgument.ts +20 -0
- package/src/api/command/Command.ts +22 -0
- package/src/api/command/GlobalCommand.ts +33 -0
- package/src/api/command/GlobalModifierCommand.ts +12 -0
- package/src/api/command/GroupCommand.ts +20 -0
- package/src/api/command/SubCommand.ts +41 -0
- package/src/api/command/UsageExample.ts +21 -0
- package/src/api/service/ServiceProvider.ts +52 -0
- package/src/api/service/core/AsciiBannerGeneratorService.ts +35 -0
- package/src/api/service/core/KeyValueService.ts +28 -0
- package/src/api/service/core/PrinterService.ts +266 -0
- package/src/api/service/core/ShutdownService.ts +12 -0
- package/src/api/service/core/SyntaxHighlighterService.ts +42 -0
- package/src/cli/BaseCLI.ts +312 -0
- package/src/cli/DefaultRuntimeCLI.ts +54 -0
- package/src/command/MultiCommandCliHelpCommand.ts +394 -0
- package/src/command/SingleCommandCliHelpCommand.ts +183 -0
- package/src/command/UsageCommand.ts +35 -0
- package/src/command/VersionCommand.ts +22 -0
- package/src/launcher.ts +127 -0
- package/src/runtime/DefaultContext.ts +32 -0
- package/src/runtime/argument/ArgumentTypeGuards.ts +8 -0
- package/src/runtime/command/CommandTypeGuards.ts +30 -0
- package/src/runtime/command/CommandValidator.ts +549 -0
- package/src/runtime/parser.ts +219 -0
- package/src/runtime/registry/CommandRegistry.ts +121 -0
- package/src/runtime/registry/DefaultCommandRegistry.ts +318 -0
- package/src/runtime/registry/DefaultServiceProviderRegistry.ts +26 -0
- package/src/runtime/registry/ServiceProviderRegistry.ts +11 -0
- package/src/runtime/runner.ts +697 -0
- package/src/runtime/scanner.ts +417 -0
- package/src/runtime/values/ValuePopulationResult.ts +41 -0
- package/src/runtime/values/argumentValueMerge.ts +201 -0
- package/src/runtime/values/argumentValueValidation.ts +610 -0
- package/src/runtime/values/globalCommandValuePopulation.ts +96 -0
- package/src/runtime/values/subCommandValuePopulation.ts +680 -0
- package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +45 -0
- package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +38 -0
- package/src/service/asciiBannerGenerator/standard.flf.json +3 -0
- package/src/service/banner/BannerServiceProvider.ts +99 -0
- package/src/service/banner/command/NoBannerCommand.ts +42 -0
- package/src/service/configuration/ConfigurationServiceProvider.ts +571 -0
- package/src/service/configuration/DefaultKeyValueService.ts +60 -0
- package/src/service/configuration/command/ConfigCommand.ts +43 -0
- package/src/service/configuration/command/DumpConfigCommand.ts +47 -0
- package/src/service/printer/DefaultPrinterService.ts +256 -0
- package/src/service/printer/PrinterServiceProvider.ts +61 -0
- package/src/service/printer/command/DarkModeCommand.ts +43 -0
- package/src/service/printer/command/LogLevelCommand.ts +68 -0
- package/src/service/printer/command/NoColorCommand.ts +43 -0
- package/src/service/printer/terminal/Ansi.ts +10 -0
- package/src/service/printer/terminal/Color.ts +14 -0
- package/src/service/printer/terminal/Progress.ts +373 -0
- package/src/service/printer/terminal/Spinner.ts +100 -0
- package/src/service/printer/terminal/Styler.ts +5 -0
- package/src/service/printer/terminal/Terminal.ts +8 -0
- package/src/service/printer/terminal/Theme.ts +34 -0
- package/src/service/printer/terminal/TtyStyler.ts +39 -0
- package/src/service/printer/terminal/TtyTerminal.ts +65 -0
- package/src/service/shutdown/DefaultShutdownService.ts +9 -0
- package/src/service/shutdown/ShutdownServiceProvider.ts +55 -0
- package/src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts +52 -0
- package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +51 -0
- package/src/util/configHelper.ts +95 -0
- package/src/util/envVarHelper.ts +338 -0
- package/src/util/helpHelper.ts +751 -0
- package/src/util/logger.ts +84 -0
- package/src/util/runnerHelper.ts +112 -0
- package/tests/cli/BaseCLI.test.ts +344 -0
- package/tests/command/MultiCommandCliHelpCommand.test.ts +650 -0
- package/tests/command/SingleCommandCliHelpCommand.test.ts +384 -0
- package/tests/command/UsageCommand.test.ts +53 -0
- package/tests/command/VersionCommand.test.ts +36 -0
- package/tests/fixtures/CLIConfig.ts +9 -0
- package/tests/fixtures/Command.ts +229 -0
- package/tests/fixtures/CommandRegistry.ts +8 -0
- package/tests/fixtures/ConfigurationServiceProvider.ts +28 -0
- package/tests/fixtures/Context.ts +33 -0
- package/tests/fixtures/ServiceProviderRegistry.ts +16 -0
- package/tests/fixtures/StreamString.ts +37 -0
- package/tests/fixtures/util.ts +41 -0
- package/tests/runtime/DefaultContext.test.ts +20 -0
- package/tests/runtime/argument/ArgumentTypeGuards.test.ts +49 -0
- package/tests/runtime/command/CommandTypeGuards.test.ts +43 -0
- package/tests/runtime/command/CommandValidator.test.ts +816 -0
- package/tests/runtime/parser.test.ts +766 -0
- package/tests/runtime/registry/DefaultCommandRegistry.test.ts +166 -0
- package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +42 -0
- package/tests/runtime/runner.test.ts +1298 -0
- package/tests/runtime/scanner.test.ts +1113 -0
- package/tests/runtime/values/argumentValueMerge.test.ts +314 -0
- package/tests/runtime/values/argumentValueValidation.test.ts +1020 -0
- package/tests/runtime/values/globalCommandValuePopulation.test.ts +662 -0
- package/tests/runtime/values/subCommandValuePopulation.test.ts +2658 -0
- package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +22 -0
- package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +70 -0
- package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +22 -0
- package/tests/service/asciiBannerGenerator/small.flf.json +3 -0
- package/tests/service/configuration/ConfigurationServiceProvider.test.ts +249 -0
- package/tests/service/configuration/DefaultKeyValueService.test.ts +65 -0
- package/tests/service/launchBanner/BannerServiceProvider.test.ts +49 -0
- package/tests/service/launchBanner/__snapshots__/BannerServiceProvider.test.ts.snap +3 -0
- package/tests/service/launchBanner/command/NoBannerCommand.test.ts +21 -0
- package/tests/service/printer/DefaultPrinterService.test.ts +409 -0
- package/tests/service/printer/PrinterServiceProvider.test.ts +29 -0
- package/tests/service/printer/command/LogLevelCommand.test.ts +58 -0
- package/tests/service/printer/terminal/Progress.test.ts +40 -0
- package/tests/service/printer/terminal/Spinner.test.ts +57 -0
- package/tests/service/shutdown/ShutdownServiceProvider.test.ts +13 -0
- package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +123 -0
- package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +41 -0
- package/tests/util/configHelper.test.ts +275 -0
- package/tests/util/envVarHelper.test.ts +318 -0
- package/tests/util/logger.test.ts +10 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type SyntaxHighlighterService from "../../api/service/core/SyntaxHighlighterService.ts";
|
|
2
|
+
import type { LanguageFn as HighlightSyntax } from "highlight.js";
|
|
3
|
+
import { createEmphasize } from "emphasize";
|
|
4
|
+
import json from "highlight.js/lib/languages/json";
|
|
5
|
+
|
|
6
|
+
interface Emphasize {
|
|
7
|
+
listLanguages(): ReadonlyArray<string>;
|
|
8
|
+
registered(name: string): boolean;
|
|
9
|
+
highlight(name: string, text: string): { value: string };
|
|
10
|
+
register(name: string, syntax: HighlightSyntax): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default implementation of {@link SyntaxHighlighterService} which has a syntax definition
|
|
15
|
+
* for JSON already registered.
|
|
16
|
+
*/
|
|
17
|
+
export default class DefaultSyntaxHighlighterService
|
|
18
|
+
implements SyntaxHighlighterService {
|
|
19
|
+
colorEnabled = true;
|
|
20
|
+
emphasize: Emphasize;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this.emphasize = createEmphasize();
|
|
24
|
+
this.registerSyntax("json", json);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getRegisteredSyntaxes(): ReadonlyArray<string> {
|
|
28
|
+
return this.emphasize.listLanguages();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
highlight(text: string, syntaxName: string): string {
|
|
32
|
+
const name = syntaxName.toLowerCase();
|
|
33
|
+
if (!this.emphasize.registered(name)) {
|
|
34
|
+
throw new Error(`Syntax name is not registered: ${name}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// skip highlighting if color disabled
|
|
38
|
+
if (!this.colorEnabled) {
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return this.emphasize.highlight(name, text).value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
registerSyntax(syntaxName: string, syntaxDefinition: HighlightSyntax): void {
|
|
46
|
+
const name = syntaxName.toLowerCase();
|
|
47
|
+
if (this.emphasize.registered(name)) {
|
|
48
|
+
throw new Error(`Syntax name already registered: ${name}`);
|
|
49
|
+
}
|
|
50
|
+
this.emphasize.register(name, syntaxDefinition);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ServiceInfo,
|
|
3
|
+
ServiceProvider,
|
|
4
|
+
} from "../../api/service/ServiceProvider.ts";
|
|
5
|
+
import DefaultSyntaxHighlighterService from "./DefaultSyntaxHighlighterService.ts";
|
|
6
|
+
import {
|
|
7
|
+
SYNTAX_HIGHLIGHTER_SERVICE_ID,
|
|
8
|
+
} from "../../api/service/core/SyntaxHighlighterService.ts";
|
|
9
|
+
import type Context from "../../api/Context.ts";
|
|
10
|
+
import type PrinterService from "../../api/service/core/PrinterService.ts";
|
|
11
|
+
import { PRINTER_SERVICE_ID } from "../../api/service/core/PrinterService.ts";
|
|
12
|
+
import type CLIConfig from "../../api/CLIConfig.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Provides a {@link SyntaxHighlighterService}.
|
|
16
|
+
*/
|
|
17
|
+
export default class SyntaxHighlighterServiceProvider
|
|
18
|
+
implements ServiceProvider {
|
|
19
|
+
readonly serviceId: string = SYNTAX_HIGHLIGHTER_SERVICE_ID;
|
|
20
|
+
readonly #defaultSyntaxHighlighterService: DefaultSyntaxHighlighterService;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create an instance of the service provider with the specified details.
|
|
24
|
+
*
|
|
25
|
+
* @param servicePriority the priority of the service.
|
|
26
|
+
*/
|
|
27
|
+
public constructor(
|
|
28
|
+
readonly servicePriority: number,
|
|
29
|
+
) {
|
|
30
|
+
this.#defaultSyntaxHighlighterService =
|
|
31
|
+
new DefaultSyntaxHighlighterService();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
35
|
+
return Promise.resolve({
|
|
36
|
+
service: this.#defaultSyntaxHighlighterService,
|
|
37
|
+
commands: [],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
initService(context: Context): Promise<void> {
|
|
42
|
+
const printerService = context.getServiceById(
|
|
43
|
+
PRINTER_SERVICE_ID,
|
|
44
|
+
) as PrinterService;
|
|
45
|
+
|
|
46
|
+
this.#defaultSyntaxHighlighterService.colorEnabled =
|
|
47
|
+
printerService.colorEnabled;
|
|
48
|
+
|
|
49
|
+
return Promise.resolve(undefined);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type Command from "../api/command/Command.ts";
|
|
2
|
+
import type CLIConfig from "../api/CLIConfig.ts";
|
|
3
|
+
import type Option from "../api/argument/Option.ts";
|
|
4
|
+
import type Positional from "../api/argument/Positional.ts";
|
|
5
|
+
import type SubCommandArgument from "../api/argument/SubCommandArgument.ts";
|
|
6
|
+
import type GlobalCommandArgument from "../api/argument/GlobalCommandArgument.ts";
|
|
7
|
+
|
|
8
|
+
function getKeySegment(segment: string, isCustom = false, isArray = false) {
|
|
9
|
+
let keySegment = segment;
|
|
10
|
+
if (!isCustom) {
|
|
11
|
+
keySegment = keySegment.replace("-", "_").toUpperCase();
|
|
12
|
+
if (keySegment[0].match(/[0-9]/)) {
|
|
13
|
+
keySegment = `_${keySegment}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (isArray) {
|
|
17
|
+
return `${keySegment}[_<index>]`;
|
|
18
|
+
}
|
|
19
|
+
return keySegment;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Determine the configuration key for the specified {@link SubCommandArgument}.
|
|
24
|
+
*
|
|
25
|
+
* @param cliConfig the {@link CLIConfig} to use for determining the default configuration key.
|
|
26
|
+
* @param command the {@link Command} to use for determining the default configuration key.
|
|
27
|
+
* @param argumentAncestry a list of {@link SubCommandArgument} instances from the root command arguments to a leaf argument.
|
|
28
|
+
* Apart from {@link ComplexOption} instances this will always only have a single entry.
|
|
29
|
+
*/
|
|
30
|
+
export function getSubCommandArgumentConfigurationKey(
|
|
31
|
+
cliConfig: CLIConfig,
|
|
32
|
+
command: Command,
|
|
33
|
+
argumentAncestry: Array<SubCommandArgument>,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
if (
|
|
36
|
+
(command.enableConfiguration === undefined) ||
|
|
37
|
+
(command.enableConfiguration !== true)
|
|
38
|
+
) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (argumentAncestry.length === 0) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const keySegments: Array<string> = [];
|
|
46
|
+
const firstArgument = argumentAncestry[0];
|
|
47
|
+
if (firstArgument.configurationKey === undefined) {
|
|
48
|
+
keySegments.push(getKeySegment(cliConfig.name));
|
|
49
|
+
keySegments.push(getKeySegment(command.name));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
argumentAncestry.forEach((argument) => {
|
|
53
|
+
const isArray = (argument as Option).isArray ||
|
|
54
|
+
(argument as Positional).isVarargMultiple;
|
|
55
|
+
keySegments.push(
|
|
56
|
+
getKeySegment(
|
|
57
|
+
argument.configurationKey || argument.name,
|
|
58
|
+
argument.configurationKey !== undefined,
|
|
59
|
+
isArray,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return keySegments.join("_");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Determine the configuration key for the specified {@link GlobalCommandArgument}.
|
|
69
|
+
*
|
|
70
|
+
* @param cliConfig the {@link CLIConfig} to use for determining the default configuration key.
|
|
71
|
+
* @param command the {@link Command} to use for determining the default configuration key.
|
|
72
|
+
* @param globalCommandArgument the {@link GlobalCommandArgument}.
|
|
73
|
+
*/
|
|
74
|
+
export function getGlobalCommandArgumentConfigurationKey(
|
|
75
|
+
cliConfig: CLIConfig,
|
|
76
|
+
command: Command,
|
|
77
|
+
globalCommandArgument: GlobalCommandArgument,
|
|
78
|
+
): string | undefined {
|
|
79
|
+
if (
|
|
80
|
+
(command.enableConfiguration === undefined) ||
|
|
81
|
+
(command.enableConfiguration !== true)
|
|
82
|
+
) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const keySegments: Array<string> = [];
|
|
87
|
+
if (globalCommandArgument.configurationKey === undefined) {
|
|
88
|
+
keySegments.push(getKeySegment(cliConfig.name));
|
|
89
|
+
keySegments.push(getKeySegment(command.name));
|
|
90
|
+
} else {
|
|
91
|
+
keySegments.push(getKeySegment(globalCommandArgument.configurationKey));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return keySegments.join("_");
|
|
95
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import type Command from "../api/command/Command.ts";
|
|
3
|
+
import type CLIConfig from "../api/CLIConfig.ts";
|
|
4
|
+
import {
|
|
5
|
+
ArgumentValueTypeName,
|
|
6
|
+
ComplexValueTypeName,
|
|
7
|
+
type PopulatedArgumentSingleValueType,
|
|
8
|
+
type PopulatedArgumentValues,
|
|
9
|
+
} from "../api/argument/ArgumentValueTypes.ts";
|
|
10
|
+
type ProcessEnv = NodeJS.ProcessEnv;
|
|
11
|
+
|
|
12
|
+
import type SubCommandArgument from "../api/argument/SubCommandArgument.ts";
|
|
13
|
+
import {
|
|
14
|
+
MAXIMUM_ARGUMENT_ARRAY_SIZE,
|
|
15
|
+
} from "../api/argument/SubCommandArgument.ts";
|
|
16
|
+
import type Option from "../api/argument/Option.ts";
|
|
17
|
+
import type GlobalCommandArgument from "../api/argument/GlobalCommandArgument.ts";
|
|
18
|
+
import type GlobalCommand from "../api/command/GlobalCommand.ts";
|
|
19
|
+
import type SubCommand from "../api/command/SubCommand.ts";
|
|
20
|
+
import type ComplexOption from "../api/argument/ComplexOption.ts";
|
|
21
|
+
|
|
22
|
+
function getKeySegment(segment: string) {
|
|
23
|
+
let keySegment = segment.replace("-", "_").toUpperCase();
|
|
24
|
+
if (keySegment[0].match(/[0-9]/)) {
|
|
25
|
+
keySegment = `_${keySegment}`;
|
|
26
|
+
}
|
|
27
|
+
return keySegment;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getSubCommandArgumentKeyPrefix(
|
|
31
|
+
cliConfig: CLIConfig,
|
|
32
|
+
command: Command,
|
|
33
|
+
subCommandArgument: SubCommandArgument,
|
|
34
|
+
) {
|
|
35
|
+
if (subCommandArgument.configurationKey) {
|
|
36
|
+
return subCommandArgument.configurationKey;
|
|
37
|
+
}
|
|
38
|
+
return `${getKeySegment(cliConfig.name)}_${getKeySegment(command.name)}_${
|
|
39
|
+
getKeySegment(subCommandArgument.name)
|
|
40
|
+
}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getGlobalArgumentKeyPrefix(
|
|
44
|
+
cliConfig: CLIConfig,
|
|
45
|
+
command: Command,
|
|
46
|
+
globalCommandArgument: GlobalCommandArgument,
|
|
47
|
+
) {
|
|
48
|
+
if (globalCommandArgument.configurationKey) {
|
|
49
|
+
return globalCommandArgument.configurationKey;
|
|
50
|
+
}
|
|
51
|
+
return `${getKeySegment(cliConfig.name)}_${getKeySegment(command.name)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get configured argument value for the specified {@link GlobalCommandArgument} from environment variables.
|
|
56
|
+
*
|
|
57
|
+
* @param cliConfig the {@link CLIConfig} to use for determining the default configuration key.
|
|
58
|
+
* @param command the {@link GlobalCommand} to use for determining the default configuration key and containing a
|
|
59
|
+
* {@link GlobalCommandArgument} instance to populate a value for.
|
|
60
|
+
*/
|
|
61
|
+
export function getGlobalCommandValueFromEnvVars(
|
|
62
|
+
cliConfig: CLIConfig,
|
|
63
|
+
command: GlobalCommand,
|
|
64
|
+
): PopulatedArgumentSingleValueType {
|
|
65
|
+
const { argument } = command;
|
|
66
|
+
if (argument === undefined) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// simple check for a specific env var name
|
|
71
|
+
let envVarValue = process.env[
|
|
72
|
+
getGlobalArgumentKeyPrefix(cliConfig, command, argument)
|
|
73
|
+
];
|
|
74
|
+
if (
|
|
75
|
+
(envVarValue != undefined) &&
|
|
76
|
+
(argument.type === ArgumentValueTypeName.BOOLEAN)
|
|
77
|
+
) {
|
|
78
|
+
envVarValue = (envVarValue.length > 0) ? "true" : "false";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return envVarValue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getOptionValuesFromEnvVars(
|
|
85
|
+
envVarValues: PopulatedArgumentValues,
|
|
86
|
+
potentialEnvVarNames: Array<string>,
|
|
87
|
+
env: ProcessEnv,
|
|
88
|
+
option: Option | ComplexOption,
|
|
89
|
+
envVarNamePrefix: string,
|
|
90
|
+
) {
|
|
91
|
+
if (potentialEnvVarNames.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
option.name === "__proto__" || option.name === "constructor" ||
|
|
97
|
+
option.name === "prototype"
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Unsafe key name in use: ${option.name}, CodeQL Rule ID: js/prototype-polluting-assignment`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (option.isArray) {
|
|
105
|
+
envVarNamePrefix = `${envVarNamePrefix}_`;
|
|
106
|
+
|
|
107
|
+
const matches = potentialEnvVarNames.filter((envVarName) =>
|
|
108
|
+
envVarName.startsWith(envVarNamePrefix)
|
|
109
|
+
);
|
|
110
|
+
matches.forEach((match) => {
|
|
111
|
+
const path = match.substring(envVarNamePrefix.length);
|
|
112
|
+
|
|
113
|
+
// either '1' or '1_property...' or 'something_invalid'....
|
|
114
|
+
let indexString = path;
|
|
115
|
+
let suffix = "";
|
|
116
|
+
const indexOf = path.indexOf("_");
|
|
117
|
+
if (indexOf > -1) {
|
|
118
|
+
indexString = path.substring(0, indexOf);
|
|
119
|
+
if (indexOf + 1 < path.length) {
|
|
120
|
+
suffix = path.substring(indexOf + 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
let index: number;
|
|
124
|
+
try {
|
|
125
|
+
index = parseInt(indexString);
|
|
126
|
+
} catch (_err) {
|
|
127
|
+
throw new Error(`Unable to parse array index from env var: ${match}`);
|
|
128
|
+
}
|
|
129
|
+
if (index !== undefined) {
|
|
130
|
+
if (index > MAXIMUM_ARGUMENT_ARRAY_SIZE) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Maximum array size exceeded: ${index} in env var: ${match}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (envVarValues[option.name] === undefined) {
|
|
136
|
+
envVarValues[option.name] = [];
|
|
137
|
+
}
|
|
138
|
+
if (suffix.length > 0) {
|
|
139
|
+
if (option.type === ComplexValueTypeName.COMPLEX) {
|
|
140
|
+
if (
|
|
141
|
+
(envVarValues[option.name] as Array<
|
|
142
|
+
PopulatedArgumentSingleValueType
|
|
143
|
+
>)[index] === undefined
|
|
144
|
+
) {
|
|
145
|
+
(envVarValues[option.name] as Array<PopulatedArgumentValues>)[
|
|
146
|
+
index
|
|
147
|
+
] = {};
|
|
148
|
+
}
|
|
149
|
+
(option as ComplexOption).properties.forEach((property) => {
|
|
150
|
+
const propertySegment = property.configurationKey ||
|
|
151
|
+
getKeySegment(property.name);
|
|
152
|
+
getOptionValuesFromEnvVars(
|
|
153
|
+
(envVarValues[option.name] as Array<PopulatedArgumentValues>)[
|
|
154
|
+
index
|
|
155
|
+
],
|
|
156
|
+
matches,
|
|
157
|
+
env,
|
|
158
|
+
property,
|
|
159
|
+
`${envVarNamePrefix}${index}_${propertySegment}`,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
if (
|
|
163
|
+
Object.keys(
|
|
164
|
+
(envVarValues[option.name] as Array<PopulatedArgumentValues>)[
|
|
165
|
+
index
|
|
166
|
+
] as Record<string, string>,
|
|
167
|
+
).length === 0
|
|
168
|
+
) {
|
|
169
|
+
delete (envVarValues[option.name] as Array<
|
|
170
|
+
PopulatedArgumentValues
|
|
171
|
+
>)[index];
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Unidentified suffix ${suffix} in env var: ${match}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
} else if (option.type === ComplexValueTypeName.COMPLEX) {
|
|
179
|
+
throw new Error(`Incomplete argument path in env var: ${match}`);
|
|
180
|
+
} else {
|
|
181
|
+
// set primitive value
|
|
182
|
+
let envVarValue = env[match];
|
|
183
|
+
if (
|
|
184
|
+
(envVarValue != undefined) &&
|
|
185
|
+
(option.type === ArgumentValueTypeName.BOOLEAN)
|
|
186
|
+
) {
|
|
187
|
+
envVarValue = envVarValue.length > 1 ? "true" : "false";
|
|
188
|
+
}
|
|
189
|
+
(envVarValues[option.name] as Array<
|
|
190
|
+
PopulatedArgumentSingleValueType
|
|
191
|
+
>)[
|
|
192
|
+
index
|
|
193
|
+
] = envVarValue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const elements = envVarValues[option.name] as Array<
|
|
197
|
+
PopulatedArgumentSingleValueType
|
|
198
|
+
>;
|
|
199
|
+
for (let i = elements.length - 1; i > -1; i--) {
|
|
200
|
+
if (elements[i] === undefined) {
|
|
201
|
+
elements.length = i;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (elements.length === 0) {
|
|
205
|
+
delete envVarValues[option.name];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
} else if (option.type === ComplexValueTypeName.COMPLEX) {
|
|
210
|
+
(option as ComplexOption).properties.forEach((property) => {
|
|
211
|
+
if (envVarValues[option.name] === undefined) {
|
|
212
|
+
envVarValues[option.name] = {};
|
|
213
|
+
}
|
|
214
|
+
const propertySegment = property.configurationKey ||
|
|
215
|
+
getKeySegment(property.name);
|
|
216
|
+
getOptionValuesFromEnvVars(
|
|
217
|
+
envVarValues[option.name] as PopulatedArgumentValues,
|
|
218
|
+
potentialEnvVarNames,
|
|
219
|
+
env,
|
|
220
|
+
property,
|
|
221
|
+
`${envVarNamePrefix}_${propertySegment}`,
|
|
222
|
+
);
|
|
223
|
+
if (
|
|
224
|
+
Object.keys(envVarValues[option.name] as Record<string, string>)
|
|
225
|
+
.length === 0
|
|
226
|
+
) {
|
|
227
|
+
delete envVarValues[option.name];
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
// simple check for a specific env var name
|
|
232
|
+
let value = process.env[envVarNamePrefix];
|
|
233
|
+
if (value === undefined) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
if (option.type === ArgumentValueTypeName.BOOLEAN) {
|
|
237
|
+
value = value.length > 1 ? "true" : "false";
|
|
238
|
+
}
|
|
239
|
+
// set primitive value
|
|
240
|
+
envVarValues[option.name] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get configured argument values for the specified {@link Option} from environment variables.
|
|
246
|
+
*
|
|
247
|
+
* @param cliConfig the {@link CLIConfig} to use for determining the default configuration key.
|
|
248
|
+
* @param command the {@link SubCommand} to use for determining the default configuration key and containing
|
|
249
|
+
* {@link Positional} and {@link Option} instances to populate values for.
|
|
250
|
+
*/
|
|
251
|
+
export function getSubCommandValuesFromEnvVars(
|
|
252
|
+
cliConfig: CLIConfig,
|
|
253
|
+
command: SubCommand,
|
|
254
|
+
): PopulatedArgumentValues | undefined {
|
|
255
|
+
const envVarValues: PopulatedArgumentValues = {};
|
|
256
|
+
if (command.options) {
|
|
257
|
+
for (const option of command.options) {
|
|
258
|
+
const potentialEnvVarNames = Object.keys(process.env);
|
|
259
|
+
const optionPrefix = getSubCommandArgumentKeyPrefix(
|
|
260
|
+
cliConfig,
|
|
261
|
+
command,
|
|
262
|
+
option as Option,
|
|
263
|
+
);
|
|
264
|
+
getOptionValuesFromEnvVars(
|
|
265
|
+
envVarValues,
|
|
266
|
+
potentialEnvVarNames,
|
|
267
|
+
process.env ?? {},
|
|
268
|
+
option,
|
|
269
|
+
optionPrefix,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (command.positionals) {
|
|
274
|
+
for (const positional of command.positionals) {
|
|
275
|
+
let envVarNamePrefix = getSubCommandArgumentKeyPrefix(
|
|
276
|
+
cliConfig,
|
|
277
|
+
command,
|
|
278
|
+
positional,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (positional.isVarargMultiple) {
|
|
282
|
+
envVarNamePrefix = `${envVarNamePrefix}_`;
|
|
283
|
+
|
|
284
|
+
const matches = Object.keys(process.env).filter((envVarName) =>
|
|
285
|
+
envVarName.startsWith(envVarNamePrefix)
|
|
286
|
+
);
|
|
287
|
+
matches.forEach((match) => {
|
|
288
|
+
let index;
|
|
289
|
+
try {
|
|
290
|
+
index = parseInt(match.substring(envVarNamePrefix.length));
|
|
291
|
+
} catch (_err) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Unable to parse array index from env var: ${match}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (index !== undefined) {
|
|
297
|
+
if (index > MAXIMUM_ARGUMENT_ARRAY_SIZE) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Maximum array size exceeded: ${index} in env var: ${match}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (envVarValues[positional.name] === undefined) {
|
|
303
|
+
envVarValues[positional.name] = [];
|
|
304
|
+
}
|
|
305
|
+
// set primitive value
|
|
306
|
+
let envVarValue = process.env[match];
|
|
307
|
+
if (
|
|
308
|
+
(envVarValue != undefined) &&
|
|
309
|
+
(positional.type === ArgumentValueTypeName.BOOLEAN)
|
|
310
|
+
) {
|
|
311
|
+
envVarValue = envVarValue.length > 1 ? "true" : "false";
|
|
312
|
+
}
|
|
313
|
+
(envVarValues[positional.name] as Array<
|
|
314
|
+
PopulatedArgumentSingleValueType
|
|
315
|
+
>)[index] = envVarValue;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
// simple check for a specific env var name
|
|
320
|
+
let value = process.env[envVarNamePrefix];
|
|
321
|
+
if (value === undefined) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
if (positional.type === ArgumentValueTypeName.BOOLEAN) {
|
|
325
|
+
value = value.length > 1 ? "true" : "false";
|
|
326
|
+
}
|
|
327
|
+
// set primitive value
|
|
328
|
+
envVarValues[positional.name] = value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check if anything was actually populated
|
|
334
|
+
if (Object.keys(envVarValues).length === 0) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
return envVarValues;
|
|
338
|
+
}
|