@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,84 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
type LoggerFunction = (
|
|
3
|
+
message: object | (() => string) | string,
|
|
4
|
+
...optionalParams: unknown[]
|
|
5
|
+
) => void;
|
|
6
|
+
|
|
7
|
+
export interface Logger {
|
|
8
|
+
trace: LoggerFunction;
|
|
9
|
+
debug: LoggerFunction;
|
|
10
|
+
info: LoggerFunction;
|
|
11
|
+
warn: LoggerFunction;
|
|
12
|
+
error: LoggerFunction;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const debugEnabled = process.env["CLI_DEBUG"] !== undefined;
|
|
16
|
+
|
|
17
|
+
function getDefaultLogger(): Logger {
|
|
18
|
+
if (debugEnabled) {
|
|
19
|
+
return {
|
|
20
|
+
trace: () => {},
|
|
21
|
+
debug: (message, ...optionalParams) => {
|
|
22
|
+
console.debug(message, optionalParams);
|
|
23
|
+
},
|
|
24
|
+
info: (message, ...optionalParams) => {
|
|
25
|
+
console.info(message, optionalParams);
|
|
26
|
+
},
|
|
27
|
+
warn: (message, ...optionalParams) => {
|
|
28
|
+
console.warn(message, optionalParams);
|
|
29
|
+
},
|
|
30
|
+
error: (message, ...optionalParams) => {
|
|
31
|
+
console.error(message, optionalParams);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
trace: () => {},
|
|
37
|
+
debug: () => {},
|
|
38
|
+
info: () => {},
|
|
39
|
+
warn: () => {},
|
|
40
|
+
error: (message, ...optionalParams) => {
|
|
41
|
+
console.error(message, optionalParams);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function wrapWithLoggerName(
|
|
47
|
+
loggerName: string,
|
|
48
|
+
loggerFunction: LoggerFunction,
|
|
49
|
+
): LoggerFunction {
|
|
50
|
+
return (message, ...optionalParams) => {
|
|
51
|
+
if (message instanceof Object) {
|
|
52
|
+
message.loggerName = loggerName;
|
|
53
|
+
loggerFunction(message, optionalParams);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (message instanceof Function) {
|
|
57
|
+
loggerFunction(`${loggerName} ${message()}`, optionalParams);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
loggerFunction(`${loggerName} ${message}`, optionalParams);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default function getLogger(loggerName: string): Logger {
|
|
65
|
+
if (globalThis.defaultLogger === undefined) {
|
|
66
|
+
globalThis.defaultLogger = getDefaultLogger();
|
|
67
|
+
}
|
|
68
|
+
if (debugEnabled) {
|
|
69
|
+
return {
|
|
70
|
+
trace: () => {},
|
|
71
|
+
debug: wrapWithLoggerName(loggerName, globalThis.defaultLogger.debug),
|
|
72
|
+
info: wrapWithLoggerName(loggerName, globalThis.defaultLogger.info),
|
|
73
|
+
warn: wrapWithLoggerName(loggerName, globalThis.defaultLogger.warn),
|
|
74
|
+
error: wrapWithLoggerName(loggerName, globalThis.defaultLogger.error),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
trace: () => {},
|
|
79
|
+
debug: () => {},
|
|
80
|
+
info: () => {},
|
|
81
|
+
warn: () => {},
|
|
82
|
+
error: wrapWithLoggerName(loggerName, globalThis.defaultLogger.error),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type PrinterService from "../api/service/core/PrinterService.ts";
|
|
2
|
+
import {
|
|
3
|
+
Icon,
|
|
4
|
+
PRINTER_SERVICE_ID,
|
|
5
|
+
} from "../api/service/core/PrinterService.ts";
|
|
6
|
+
import type { ParseResult } from "../runtime/parser.ts";
|
|
7
|
+
import {
|
|
8
|
+
isGlobalCommand,
|
|
9
|
+
isGlobalModifierCommand,
|
|
10
|
+
} from "../runtime/command/CommandTypeGuards.ts";
|
|
11
|
+
import type Context from "../api/Context.ts";
|
|
12
|
+
import { getInvalidArgumentString } from "../runtime/values/argumentValueValidation.ts";
|
|
13
|
+
|
|
14
|
+
function getCommandString(
|
|
15
|
+
printerService: PrinterService,
|
|
16
|
+
parseResult: ParseResult,
|
|
17
|
+
): string {
|
|
18
|
+
const { command, groupCommand } = parseResult;
|
|
19
|
+
|
|
20
|
+
if (groupCommand) {
|
|
21
|
+
return printerService.yellow(
|
|
22
|
+
`'${groupCommand.name}:${command.name}'`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return printerService.yellow(`'${command.name}'`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function printParseResultError(
|
|
29
|
+
context: Context,
|
|
30
|
+
parseResult: ParseResult,
|
|
31
|
+
isDefaultCommand = false,
|
|
32
|
+
) {
|
|
33
|
+
const printerService = context.getServiceById(
|
|
34
|
+
PRINTER_SERVICE_ID,
|
|
35
|
+
) as PrinterService;
|
|
36
|
+
const commandErrorString = isDefaultCommand
|
|
37
|
+
? "Parse error: "
|
|
38
|
+
: `Parse error for command: ${
|
|
39
|
+
getCommandString(printerService, parseResult)
|
|
40
|
+
}`;
|
|
41
|
+
const { command, invalidArguments } = parseResult;
|
|
42
|
+
|
|
43
|
+
let errorString = "=> ";
|
|
44
|
+
|
|
45
|
+
const skipArgName = isGlobalModifierCommand(command) ||
|
|
46
|
+
isGlobalCommand(command);
|
|
47
|
+
const argsString = invalidArguments.map(
|
|
48
|
+
(arg) => getInvalidArgumentString(arg, skipArgName),
|
|
49
|
+
).join(", ");
|
|
50
|
+
errorString = `${errorString}${printerService.yellow(argsString)}`;
|
|
51
|
+
|
|
52
|
+
await printerService.error(
|
|
53
|
+
`${commandErrorString}\n ${errorString}\n\n`,
|
|
54
|
+
Icon.FAILURE,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function printCommandExecutionError(
|
|
59
|
+
context: Context,
|
|
60
|
+
parseResult: ParseResult,
|
|
61
|
+
err: Error,
|
|
62
|
+
isDefaultCommand = false,
|
|
63
|
+
) {
|
|
64
|
+
const printerService = context.getServiceById(
|
|
65
|
+
PRINTER_SERVICE_ID,
|
|
66
|
+
) as PrinterService;
|
|
67
|
+
const commandErrorString = isDefaultCommand
|
|
68
|
+
? "Execution error: "
|
|
69
|
+
: `Execution error for command: ${
|
|
70
|
+
getCommandString(printerService, parseResult)
|
|
71
|
+
}`;
|
|
72
|
+
if (err !== undefined) {
|
|
73
|
+
await printerService.error(
|
|
74
|
+
`${commandErrorString}\n => ${printerService.yellow(err.message)}\n\n`,
|
|
75
|
+
Icon.FAILURE,
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
await printerService.error(
|
|
79
|
+
`${commandErrorString}\n => ${
|
|
80
|
+
printerService.yellow("error is undefined")
|
|
81
|
+
}\n\n`,
|
|
82
|
+
Icon.FAILURE,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function printNoCommandSpecifiedError(context: Context) {
|
|
88
|
+
const printerService = context.getServiceById(
|
|
89
|
+
PRINTER_SERVICE_ID,
|
|
90
|
+
) as PrinterService;
|
|
91
|
+
await printerService.error("No command specified\n\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function printUnusedArgsWarning(
|
|
95
|
+
context: Context,
|
|
96
|
+
overallUnusedArgs: ReadonlyArray<string>,
|
|
97
|
+
) {
|
|
98
|
+
const printerService = context.getServiceById(
|
|
99
|
+
PRINTER_SERVICE_ID,
|
|
100
|
+
) as PrinterService;
|
|
101
|
+
if (overallUnusedArgs.length === 1) {
|
|
102
|
+
await printerService.warn(
|
|
103
|
+
`Unused arg: ${overallUnusedArgs[0]}\n\n`,
|
|
104
|
+
Icon.ALERT,
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
await printerService.warn(
|
|
108
|
+
`Unused args: ${overallUnusedArgs.join(" ")}\n\n`,
|
|
109
|
+
Icon.ALERT,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import {
|
|
6
|
+
getGlobalModifierCommandWithArgument,
|
|
7
|
+
getSubCommand,
|
|
8
|
+
getSubCommandWithOptionAndPositional,
|
|
9
|
+
} from "../fixtures/Command.ts";
|
|
10
|
+
import { getCLIConfig } from "../fixtures/CLIConfig.ts";
|
|
11
|
+
import { RunState } from "../../src/api/RunResult.ts";
|
|
12
|
+
import { ArgumentValueTypeName } from "../../src/api/argument/ArgumentValueTypes.ts";
|
|
13
|
+
import BaseCLI from "../../src/cli/BaseCLI.ts";
|
|
14
|
+
import type KeyValueService from "../../src/api/service/core/KeyValueService.ts";
|
|
15
|
+
import {
|
|
16
|
+
KEY_VALUE_SERVICE_ID,
|
|
17
|
+
} from "../../src/api/service/core/KeyValueService.ts";
|
|
18
|
+
import type {
|
|
19
|
+
ServiceInfo,
|
|
20
|
+
ServiceProvider,
|
|
21
|
+
} from "../../src/api/service/ServiceProvider.ts";
|
|
22
|
+
import type Context from "../../src/api/Context.ts";
|
|
23
|
+
import type CLIConfig from "../../src/api/CLIConfig.ts";
|
|
24
|
+
import StreamString from "../fixtures/StreamString.ts";
|
|
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";
|
|
28
|
+
|
|
29
|
+
describe("BaseCLI Tests", () => {
|
|
30
|
+
test("BaseCLI no command specified works", async () => {
|
|
31
|
+
const config = getCLIConfig();
|
|
32
|
+
const dummyStdout = new StreamString();
|
|
33
|
+
const dummyStderr = new StreamString();
|
|
34
|
+
const terminal = new TtyTerminal(dummyStdout.writeStream);
|
|
35
|
+
const baseCLI = new BaseCLI(
|
|
36
|
+
config,
|
|
37
|
+
dummyStdout.writableStream,
|
|
38
|
+
dummyStderr.writableStream,
|
|
39
|
+
false,
|
|
40
|
+
false,
|
|
41
|
+
terminal,
|
|
42
|
+
new TtyStyler(3),
|
|
43
|
+
false,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
baseCLI.addCommand(getSubCommandWithOptionAndPositional());
|
|
47
|
+
|
|
48
|
+
const runResult = await baseCLI.run([]);
|
|
49
|
+
|
|
50
|
+
expect(runResult.runState).toEqual(RunState.NO_COMMAND);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("BaseCLI command execution works", async () => {
|
|
54
|
+
const config = getCLIConfig();
|
|
55
|
+
const dummyStdout = new StreamString();
|
|
56
|
+
const dummyStderr = new StreamString();
|
|
57
|
+
const terminal = new TtyTerminal(dummyStderr.writeStream);
|
|
58
|
+
const baseCLI = new BaseCLI(
|
|
59
|
+
config,
|
|
60
|
+
dummyStdout.writableStream,
|
|
61
|
+
dummyStderr.writableStream,
|
|
62
|
+
false,
|
|
63
|
+
false,
|
|
64
|
+
terminal,
|
|
65
|
+
new TtyStyler(3),
|
|
66
|
+
false,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
let modifierHasRun = false;
|
|
70
|
+
let subHasRun = false;
|
|
71
|
+
|
|
72
|
+
const modifierCommand = getGlobalModifierCommandWithArgument(
|
|
73
|
+
"modifier",
|
|
74
|
+
"m",
|
|
75
|
+
1,
|
|
76
|
+
{
|
|
77
|
+
type: ArgumentValueTypeName.STRING,
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
const option = {
|
|
81
|
+
name: "foo",
|
|
82
|
+
type: ArgumentValueTypeName.STRING,
|
|
83
|
+
shortAlias: "f",
|
|
84
|
+
};
|
|
85
|
+
const subCommand = getSubCommand("command", [option], []);
|
|
86
|
+
|
|
87
|
+
modifierCommand.execute = (): Promise<void> => {
|
|
88
|
+
modifierHasRun = true;
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
};
|
|
91
|
+
subCommand.execute = (): Promise<void> => {
|
|
92
|
+
subHasRun = true;
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
baseCLI.addCommand(modifierCommand);
|
|
97
|
+
baseCLI.addCommand(subCommand);
|
|
98
|
+
|
|
99
|
+
const runResult = await baseCLI.run([
|
|
100
|
+
"unused",
|
|
101
|
+
"--modifier=bar",
|
|
102
|
+
"command",
|
|
103
|
+
"--foo",
|
|
104
|
+
"bar",
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
expect(runResult.runState).toEqual(RunState.SUCCESS);
|
|
108
|
+
expect(modifierHasRun).toBeTrue();
|
|
109
|
+
expect(subHasRun).toBeTrue();
|
|
110
|
+
expectStringEquals(dummyStdout.getString(), "");
|
|
111
|
+
expectStringIncludes(dummyStderr.getString(), "Unused arg: unused");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("Command and service key value scope isolation works", async () => {
|
|
115
|
+
// ensure we can remove the config file written by the test
|
|
116
|
+
const dummyStdout = new StreamString();
|
|
117
|
+
const dummyStderr = new StreamString();
|
|
118
|
+
const appName = "foo" + Math.random();
|
|
119
|
+
const baseCLI = new BaseCLI(
|
|
120
|
+
getCLIConfig(appName),
|
|
121
|
+
dummyStdout.writableStream,
|
|
122
|
+
dummyStderr.writableStream,
|
|
123
|
+
false,
|
|
124
|
+
false,
|
|
125
|
+
new TtyTerminal(dummyStderr.writeStream),
|
|
126
|
+
new TtyStyler(3),
|
|
127
|
+
false,
|
|
128
|
+
true,
|
|
129
|
+
true,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
let serviceProvider1Initialised = false;
|
|
133
|
+
let serviceProvider2Initialised = false;
|
|
134
|
+
let service1MethodInvoked = false;
|
|
135
|
+
let service2MethodInvoked = false;
|
|
136
|
+
let modifierHasRun = false;
|
|
137
|
+
let subHasRun = false;
|
|
138
|
+
|
|
139
|
+
const SERVICE_1 = "service1";
|
|
140
|
+
const SERVICE_2 = "service2";
|
|
141
|
+
|
|
142
|
+
interface ServiceInterface {
|
|
143
|
+
serviceMethod(context: Context): void;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
class DefaultService1 implements ServiceInterface {
|
|
147
|
+
copyOfKeyValueService: KeyValueService | undefined;
|
|
148
|
+
|
|
149
|
+
serviceMethod(context: Context): void {
|
|
150
|
+
// should only have access to the modifierCommand KV scope
|
|
151
|
+
expect(this.copyOfKeyValueService!.hasKey("name")).toBeTrue();
|
|
152
|
+
expect(
|
|
153
|
+
this.copyOfKeyValueService!.getKey("name"),
|
|
154
|
+
).toEqual(
|
|
155
|
+
"modifierCommand",
|
|
156
|
+
);
|
|
157
|
+
this.copyOfKeyValueService!.setKey(
|
|
158
|
+
"name",
|
|
159
|
+
"defaultService1+modifierCommand",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const keyValueService = context.getServiceById(
|
|
163
|
+
KEY_VALUE_SERVICE_ID,
|
|
164
|
+
) as KeyValueService;
|
|
165
|
+
|
|
166
|
+
// should only have access to the modifierCommand KV scope
|
|
167
|
+
expect(keyValueService.hasKey("name")).toBeTrue();
|
|
168
|
+
expect(
|
|
169
|
+
this.copyOfKeyValueService!.getKey("name"),
|
|
170
|
+
).toEqual(
|
|
171
|
+
"defaultService1+modifierCommand",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
service1MethodInvoked = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class DefaultService2 implements ServiceInterface {
|
|
179
|
+
copyOfKeyValueService: KeyValueService | undefined;
|
|
180
|
+
|
|
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(
|
|
185
|
+
"subCommand",
|
|
186
|
+
);
|
|
187
|
+
this.copyOfKeyValueService!.setKey(
|
|
188
|
+
"name",
|
|
189
|
+
"defaultService2+subCommand",
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const keyValueService = context.getServiceById(
|
|
193
|
+
KEY_VALUE_SERVICE_ID,
|
|
194
|
+
) as KeyValueService;
|
|
195
|
+
|
|
196
|
+
// should only have access to the subCommand KV scope
|
|
197
|
+
expect(keyValueService.hasKey("name")).toBeTrue();
|
|
198
|
+
expect(
|
|
199
|
+
this.copyOfKeyValueService!.getKey("name"),
|
|
200
|
+
).toEqual(
|
|
201
|
+
"defaultService2+subCommand",
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
service2MethodInvoked = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
class ServiceProvider1 implements ServiceProvider {
|
|
209
|
+
readonly serviceId = SERVICE_1;
|
|
210
|
+
readonly servicePriority = 1;
|
|
211
|
+
|
|
212
|
+
defaultService1: DefaultService1 | undefined;
|
|
213
|
+
|
|
214
|
+
provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
215
|
+
this.defaultService1 = new DefaultService1();
|
|
216
|
+
return Promise.resolve({
|
|
217
|
+
service: this.defaultService1,
|
|
218
|
+
commands: [],
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
initService(context: Context): Promise<void> {
|
|
223
|
+
const keyValueService = context.getServiceById(
|
|
224
|
+
KEY_VALUE_SERVICE_ID,
|
|
225
|
+
) as KeyValueService;
|
|
226
|
+
|
|
227
|
+
// should only have access to the ServiceProvider1 KV scope
|
|
228
|
+
expect(keyValueService.hasKey("name")).toBeFalse();
|
|
229
|
+
keyValueService.setKey("name", "defaultService2");
|
|
230
|
+
|
|
231
|
+
// take a copy to check later when service is accessed via a command
|
|
232
|
+
this.defaultService1!.copyOfKeyValueService = keyValueService;
|
|
233
|
+
|
|
234
|
+
serviceProvider1Initialised = true;
|
|
235
|
+
return Promise.resolve(undefined);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
class ServiceProvider2 implements ServiceProvider {
|
|
240
|
+
readonly serviceId = SERVICE_2;
|
|
241
|
+
readonly servicePriority = 2;
|
|
242
|
+
|
|
243
|
+
defaultService2: DefaultService2 | undefined;
|
|
244
|
+
|
|
245
|
+
provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
246
|
+
this.defaultService2 = new DefaultService2();
|
|
247
|
+
return Promise.resolve({
|
|
248
|
+
service: this.defaultService2,
|
|
249
|
+
commands: [],
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
initService(context: Context): Promise<void> {
|
|
254
|
+
const keyValueService = context.getServiceById(
|
|
255
|
+
KEY_VALUE_SERVICE_ID,
|
|
256
|
+
) as KeyValueService;
|
|
257
|
+
|
|
258
|
+
// should only have access to the ServiceProvider2 KV scope
|
|
259
|
+
expect(keyValueService.hasKey("name")).toBeFalse();
|
|
260
|
+
keyValueService.setKey("name", "defaultService2");
|
|
261
|
+
|
|
262
|
+
// take a copy to check later when service is accessed via a command
|
|
263
|
+
this.defaultService2!.copyOfKeyValueService = keyValueService;
|
|
264
|
+
|
|
265
|
+
serviceProvider2Initialised = true;
|
|
266
|
+
return Promise.resolve(undefined);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const modifierCommand = getGlobalModifierCommandWithArgument(
|
|
271
|
+
"modifier",
|
|
272
|
+
"m",
|
|
273
|
+
1,
|
|
274
|
+
{
|
|
275
|
+
type: ArgumentValueTypeName.STRING,
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
const option = {
|
|
279
|
+
name: "foo",
|
|
280
|
+
type: ArgumentValueTypeName.STRING,
|
|
281
|
+
shortAlias: "f",
|
|
282
|
+
};
|
|
283
|
+
const subCommand = getSubCommand("command", [option], []);
|
|
284
|
+
|
|
285
|
+
modifierCommand.execute = (context): Promise<void> => {
|
|
286
|
+
const keyValueService = context.getServiceById(
|
|
287
|
+
KEY_VALUE_SERVICE_ID,
|
|
288
|
+
) as KeyValueService;
|
|
289
|
+
|
|
290
|
+
// should only have access to the modifierCommand KV scope
|
|
291
|
+
expect(keyValueService.hasKey("name")).toBeFalse();
|
|
292
|
+
keyValueService.setKey("name", "modifierCommand");
|
|
293
|
+
|
|
294
|
+
const service1 = context.getServiceById(SERVICE_1) as ServiceInterface;
|
|
295
|
+
|
|
296
|
+
service1.serviceMethod(context);
|
|
297
|
+
|
|
298
|
+
modifierHasRun = true;
|
|
299
|
+
return Promise.resolve();
|
|
300
|
+
};
|
|
301
|
+
subCommand.execute = (context): Promise<void> => {
|
|
302
|
+
const keyValueService = context.getServiceById(
|
|
303
|
+
KEY_VALUE_SERVICE_ID,
|
|
304
|
+
) as KeyValueService;
|
|
305
|
+
|
|
306
|
+
// should only have access to the subCommand KV scope
|
|
307
|
+
expect(keyValueService.hasKey("name")).toBeFalse();
|
|
308
|
+
keyValueService.setKey("name", "subCommand");
|
|
309
|
+
|
|
310
|
+
const service2 = context.getServiceById(SERVICE_2) as ServiceInterface;
|
|
311
|
+
|
|
312
|
+
service2.serviceMethod(context);
|
|
313
|
+
|
|
314
|
+
subHasRun = true;
|
|
315
|
+
return Promise.resolve();
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
baseCLI.addServiceProvider(new ServiceProvider1());
|
|
319
|
+
baseCLI.addServiceProvider(new ServiceProvider2());
|
|
320
|
+
baseCLI.addCommand(modifierCommand);
|
|
321
|
+
baseCLI.addCommand(subCommand);
|
|
322
|
+
|
|
323
|
+
const runResult = await baseCLI.run([
|
|
324
|
+
"unused",
|
|
325
|
+
"--modifier=bar",
|
|
326
|
+
"command",
|
|
327
|
+
"--foo",
|
|
328
|
+
"bar",
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
expect(runResult.runState).toEqual(RunState.SUCCESS);
|
|
332
|
+
expect(serviceProvider1Initialised).toBeTrue();
|
|
333
|
+
expect(serviceProvider2Initialised).toBeTrue();
|
|
334
|
+
expect(modifierHasRun).toBeTrue();
|
|
335
|
+
expect(service1MethodInvoked).toBeTrue();
|
|
336
|
+
expect(subHasRun).toBeTrue();
|
|
337
|
+
expect(service2MethodInvoked).toBeTrue();
|
|
338
|
+
|
|
339
|
+
// cleanup
|
|
340
|
+
await fs.rm(
|
|
341
|
+
path.join(process.env.HOME!, `.${appName.replace(/\W/g, "")}.json`),
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
});
|