@rsdk/cli.core 5.7.0-next.6 → 6.0.0-next.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.
- package/README.MD +1 -1
- package/dist/cli.d.ts +17 -22
- package/dist/cli.js +37 -32
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/loaders/implementations/index.d.ts +2 -0
- package/dist/loaders/implementations/index.js +19 -0
- package/dist/loaders/implementations/index.js.map +1 -0
- package/dist/loaders/implementations/local-path.loader.d.ts +9 -0
- package/dist/loaders/implementations/local-path.loader.js +49 -0
- package/dist/loaders/implementations/local-path.loader.js.map +1 -0
- package/dist/loaders/implementations/pkg.loader.d.ts +11 -0
- package/dist/loaders/implementations/pkg.loader.js +31 -0
- package/dist/loaders/implementations/pkg.loader.js.map +1 -0
- package/dist/loaders/index.d.ts +1 -0
- package/dist/loaders/index.js +18 -0
- package/dist/loaders/index.js.map +1 -0
- package/dist/loaders/loader.interface.d.ts +5 -0
- package/dist/loaders/loader.interface.js +3 -0
- package/dist/loaders/loader.interface.js.map +1 -0
- package/dist/resolvers/command.resolver.d.ts +27 -0
- package/dist/resolvers/command.resolver.js +81 -0
- package/dist/resolvers/command.resolver.js.map +1 -0
- package/dist/resolvers/options.resolver.d.ts +35 -0
- package/dist/resolvers/options.resolver.js +80 -0
- package/dist/resolvers/options.resolver.js.map +1 -0
- package/jest.config.js +1 -0
- package/jest.config.unit.js +1 -0
- package/package.json +19 -4
- package/src/cli.ts +52 -41
- package/src/index.ts +11 -3
- package/src/loaders/implementations/index.ts +2 -0
- package/src/loaders/implementations/local-path.loader.ts +53 -0
- package/src/loaders/implementations/pkg.loader.ts +33 -0
- package/src/loaders/index.ts +1 -0
- package/src/loaders/loader.interface.ts +6 -0
- package/src/resolvers/command.resolver.ts +103 -0
- package/src/resolvers/options.resolver.ts +92 -0
- package/test/cli.spec.ts +131 -0
- package/test/commands/command.single.ts +30 -0
- package/test/commands/command.with-all.ts +93 -0
- package/test/commands/command.with-subcommands.ts +55 -0
- package/test/commands/command.with-variations.ts +55 -0
- package/test/commands/js/command.single.js +47 -0
- package/test/commands-resolver.spec.ts +125 -0
- package/test/loaders.spec.ts +61 -0
- package/test/options-resolver.spec.ts +119 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rsdk/cli.core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0-next.1",
|
|
4
4
|
"description": "entry package to cli functions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -12,13 +12,28 @@
|
|
|
12
12
|
],
|
|
13
13
|
"license": "Apache License 2.0",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"
|
|
15
|
+
"@rsdk/cli.common": "6.0.0-next.1",
|
|
16
|
+
"@rsdk/common": "6.0.0-next.1",
|
|
17
|
+
"@rsdk/env": "6.0.0-next.1",
|
|
18
|
+
"@rsdk/logging": "6.0.0-next.1",
|
|
19
|
+
"commander": "^12.1.0",
|
|
20
|
+
"lodash": "^4.17.21"
|
|
16
21
|
},
|
|
17
22
|
"peerDependencies": {
|
|
18
23
|
"@rsdk/cli.common": "*",
|
|
19
24
|
"@rsdk/logging": "*",
|
|
20
25
|
"commander": "^12.1.0",
|
|
21
|
-
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
|
26
|
+
"reflect-metadata": "^0.1.12 || ^0.2.0",
|
|
27
|
+
"ts-node": "10.9.2",
|
|
28
|
+
"typescript": "5.7.3"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"ts-node": {
|
|
32
|
+
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"typescript": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
22
37
|
},
|
|
23
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "3114a76df17cf98d6f3cbb8a931599a883af6783"
|
|
24
39
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,57 +1,68 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { IRunnable } from '@rsdk/cli.common';
|
|
2
|
+
import { MetadataRegistry } from '@rsdk/cli.common';
|
|
3
|
+
import { Assert, type Constructor } from '@rsdk/common';
|
|
4
|
+
import { Env } from '@rsdk/env';
|
|
5
|
+
import type { ILogger } from '@rsdk/logging';
|
|
6
|
+
import type { Command } from 'commander';
|
|
7
|
+
|
|
8
|
+
import { CommandResolver } from './resolvers/command.resolver';
|
|
9
|
+
import { OptionsResolver } from './resolvers/options.resolver';
|
|
10
|
+
import { LocalPathLoader, PkgLoader } from './loaders';
|
|
11
|
+
|
|
12
|
+
export interface CliOptions {
|
|
13
|
+
plugins?: (string | { command: any; cmd: string })[];
|
|
14
|
+
discoverPlugins?: boolean;
|
|
15
|
+
cliPackageName: string;
|
|
16
|
+
}
|
|
3
17
|
|
|
4
|
-
/**
|
|
5
|
-
* @internal
|
|
6
|
-
*/
|
|
7
18
|
export class Cli {
|
|
8
19
|
constructor(
|
|
9
|
-
readonly
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
cliPackageName: string;
|
|
13
|
-
},
|
|
20
|
+
private readonly program: Command,
|
|
21
|
+
private readonly logger: ILogger,
|
|
22
|
+
private readonly opts?: CliOptions,
|
|
14
23
|
) {}
|
|
15
24
|
|
|
16
|
-
async handle(
|
|
17
|
-
|
|
25
|
+
async handle(arg: string | undefined): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
if (!arg || arg.startsWith('-')) {
|
|
28
|
+
await this.program.parseAsync();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
const cmd = rsdkCommand.split(':')[0];
|
|
32
|
+
const env = new Env();
|
|
33
|
+
const optionsResolver = new OptionsResolver(env, this.logger);
|
|
34
|
+
const commandResolver = new CommandResolver(optionsResolver, this.logger);
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
let rsdkCliCommand;
|
|
36
|
+
const command = await this.loadCommand(arg);
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
const { subcommands, variations } = MetadataRegistry.getCommand(command);
|
|
39
|
+
|
|
40
|
+
this.program.addCommand(commandResolver.resolve(command, subcommands));
|
|
41
|
+
|
|
42
|
+
for (const command of variations || []) {
|
|
43
|
+
this.program.addCommand(commandResolver.resolve(command));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await this.program.parseAsync();
|
|
31
47
|
} catch (error) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
rsdkCliCommandName,
|
|
35
|
-
{
|
|
36
|
-
cause: error,
|
|
37
|
-
},
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
if (!('Cmd' in rsdkCliCommand)) {
|
|
41
|
-
throw new Error('Invalid cli cmd package', {
|
|
42
|
-
cause: { rsdkCliCommand, rsdkCliCommandName },
|
|
43
|
-
});
|
|
48
|
+
Assert.isError(error);
|
|
49
|
+
this.logger.error(error);
|
|
44
50
|
}
|
|
45
|
-
|
|
51
|
+
}
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
private async loadCommand(
|
|
54
|
+
rsdkCommand: string,
|
|
55
|
+
): Promise<Constructor<IRunnable>> {
|
|
56
|
+
const [cmd] = rsdkCommand.split(':');
|
|
57
|
+
|
|
58
|
+
const loader = this.isLocalPath(cmd)
|
|
59
|
+
? new LocalPathLoader()
|
|
60
|
+
: new PkgLoader(this.opts?.cliPackageName);
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
return await loader.load(cmd);
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
private
|
|
55
|
-
return
|
|
65
|
+
private isLocalPath(cmd: string): boolean {
|
|
66
|
+
return cmd.startsWith('./') || cmd.startsWith('/') || cmd.includes('\\');
|
|
56
67
|
}
|
|
57
68
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { LoggerFactory } from '@rsdk/logging';
|
|
2
|
+
import { program } from 'commander';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
import './reconfigure-logger';
|
|
@@ -14,13 +15,20 @@ const getCliPackageName = (): string => {
|
|
|
14
15
|
).name;
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Выполняет команду
|
|
20
|
+
*/
|
|
17
21
|
export const execute = async (opts?: {
|
|
18
|
-
plugins?: (string | { command:
|
|
22
|
+
plugins?: (string | { command: any; cmd: string })[];
|
|
19
23
|
discoverPlugins?: boolean;
|
|
20
24
|
scope?: string;
|
|
21
25
|
}): Promise<void> => {
|
|
22
26
|
const cliPackageName = opts?.scope ?? getCliPackageName();
|
|
23
27
|
|
|
24
28
|
process.title = cliPackageName.split('/')[0].slice(1);
|
|
25
|
-
|
|
29
|
+
|
|
30
|
+
const logger = LoggerFactory.create(cliPackageName);
|
|
31
|
+
const cli = new Cli(program, logger, { ...opts, cliPackageName });
|
|
32
|
+
|
|
33
|
+
await cli.handle(process.argv.at(2));
|
|
26
34
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Constructor } from '@rsdk/common';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { ICommandLoader } from '../loader.interface';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Загружает команды из файла
|
|
8
|
+
*/
|
|
9
|
+
export class LocalPathLoader implements ICommandLoader {
|
|
10
|
+
async load(filename: string): Promise<Constructor> {
|
|
11
|
+
const resolvedPath = path.isAbsolute(filename)
|
|
12
|
+
? filename
|
|
13
|
+
: path.resolve(process.cwd(), filename);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Регистрируем ts-node до импорта файла
|
|
17
|
+
if (resolvedPath.endsWith('.ts')) {
|
|
18
|
+
this.registerTsNode(resolvedPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { default: command } = await require(resolvedPath);
|
|
22
|
+
|
|
23
|
+
return command;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Error loading command from file: ${resolvedPath}`, {
|
|
26
|
+
cause: error,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private registerTsNode(resolvedPath: string): void {
|
|
32
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
33
|
+
const tsnode = require('ts-node');
|
|
34
|
+
const typescript = require('typescript');
|
|
35
|
+
/* eslint-enable @typescript-eslint/no-var-requires */
|
|
36
|
+
|
|
37
|
+
// Ищем ближайший tsconfig.json
|
|
38
|
+
const tsconfigPath = typescript.findConfigFile(
|
|
39
|
+
path.dirname(resolvedPath),
|
|
40
|
+
typescript.sys.fileExists,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
tsnode.register({
|
|
44
|
+
transpileOnly: true, // Для более быстрой компиляции
|
|
45
|
+
project: tsconfigPath || undefined, // Используем найденный tsconfig или undefined
|
|
46
|
+
compilerOptions: {
|
|
47
|
+
// Базовые опции, которые будут перезаписаны настройками из tsconfig
|
|
48
|
+
module: 'commonjs',
|
|
49
|
+
target: 'es2017',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { IRunnable } from '@rsdk/cli.common';
|
|
2
|
+
import type { Constructor } from '@rsdk/common';
|
|
3
|
+
|
|
4
|
+
import type { ICommandLoader } from '../loader.interface';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Загружает команды из пакета
|
|
8
|
+
*/
|
|
9
|
+
export class PkgLoader implements ICommandLoader {
|
|
10
|
+
constructor(private readonly cliPackageName?: string) {}
|
|
11
|
+
|
|
12
|
+
async load(cmd: string): Promise<Constructor<IRunnable>> {
|
|
13
|
+
/* Пример вызова из консоли:
|
|
14
|
+
* $ rsdk init
|
|
15
|
+
* где:
|
|
16
|
+
* cliPackageName = '@rsdk/cli'
|
|
17
|
+
* cmd = 'init'
|
|
18
|
+
* pkg = '@rsdk/cli.cmd.init'
|
|
19
|
+
*/
|
|
20
|
+
const pkg = [this.cliPackageName, 'cmd', cmd].join('.');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { default: command } = await require(pkg);
|
|
24
|
+
|
|
25
|
+
return command;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Error loading package, try install it: yarn add -D ${pkg}`,
|
|
29
|
+
{ cause: error },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './implementations';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Type } from '@nestjs/common';
|
|
2
|
+
import type {
|
|
3
|
+
IRunnable,
|
|
4
|
+
OptionMetadata,
|
|
5
|
+
ParameterIndex,
|
|
6
|
+
} from '@rsdk/cli.common';
|
|
7
|
+
import { MetadataRegistry } from '@rsdk/cli.common';
|
|
8
|
+
import type { Constructor } from '@rsdk/common';
|
|
9
|
+
import type { ILogger } from '@rsdk/logging';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
|
|
13
|
+
import type { OptionsResolver } from './options.resolver';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Класс для преобразования классов команд в стиле rsdk в объекты Commander.Command.
|
|
17
|
+
* Обрабатывает метаданные команд, опций и подкоманд,
|
|
18
|
+
* создавая соответствующую структуру.
|
|
19
|
+
*/
|
|
20
|
+
export class CommandResolver {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly optionsResolver: OptionsResolver,
|
|
23
|
+
private readonly logger: ILogger,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Преобразует класс с декоратором @Cmd в объект Commander.Command.
|
|
28
|
+
* Создает команду с опциями и подкомандами на основе метаданных класса.
|
|
29
|
+
*
|
|
30
|
+
* @param {Type} ctor - Конструктор класса команды
|
|
31
|
+
* @returns {Command} Объект Commander.Command с настроенными опциями и подкомандами
|
|
32
|
+
* @throws {Error} Если не найдены метаданные команды (отсутствует декоратор @Cmd)
|
|
33
|
+
*/
|
|
34
|
+
resolve(
|
|
35
|
+
ctor: Constructor<IRunnable>,
|
|
36
|
+
subcommands: Constructor<IRunnable>[] = [],
|
|
37
|
+
): Command {
|
|
38
|
+
this.logger.trace('resolving command', { ctor });
|
|
39
|
+
this.assertNoParams(ctor);
|
|
40
|
+
|
|
41
|
+
const command = this.createCommand(ctor);
|
|
42
|
+
|
|
43
|
+
for (const subcommand of subcommands) {
|
|
44
|
+
command.addCommand(this.createCommand(subcommand));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return command;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private createCommand(ctor: Type): Command {
|
|
51
|
+
// Duplication
|
|
52
|
+
const commandMetadata = MetadataRegistry.getRunnable(ctor);
|
|
53
|
+
|
|
54
|
+
const instance = Reflect.construct(ctor, []);
|
|
55
|
+
if (typeof instance['run'] !== 'function') {
|
|
56
|
+
throw new TypeError('Command must implement "run" method');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { name, description } = commandMetadata;
|
|
60
|
+
const command = new Command(name).description(description);
|
|
61
|
+
|
|
62
|
+
const options = MetadataRegistry.getOptions(ctor);
|
|
63
|
+
|
|
64
|
+
for (const option of options.values()) {
|
|
65
|
+
const envKey = this.optionsResolver.createEnvKey(option);
|
|
66
|
+
const optionName = `--${option.name}${option.parser ? ' <value>' : ''}`;
|
|
67
|
+
const optionDescription = `${option.description} [eq ${envKey} in env]`;
|
|
68
|
+
|
|
69
|
+
command.option(optionName, optionDescription, option.defaultValue);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
command.action(this.createHandler(options, instance['run'].bind(instance)));
|
|
73
|
+
|
|
74
|
+
return command;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private createHandler(
|
|
78
|
+
optionsByIndex: Map<ParameterIndex, OptionMetadata>,
|
|
79
|
+
method: (...args: any[]) => any,
|
|
80
|
+
) {
|
|
81
|
+
return (__: any, cmd: Command): any => {
|
|
82
|
+
/* Преобразуем опции в массив аргументов для вызова метода:
|
|
83
|
+
* 1. Конвертируем Map в массив пар [индекс, метаданные]
|
|
84
|
+
* 2. Получаем значения опций через OptionsResolver
|
|
85
|
+
* (с учетом приоритета источников)
|
|
86
|
+
* 3. Сортируем по индексу для правильного порядка аргументов
|
|
87
|
+
*/
|
|
88
|
+
const args = _([...optionsByIndex])
|
|
89
|
+
.map(([index, metadata]) => ({ index, metadata }))
|
|
90
|
+
.sortBy((x) => x.index)
|
|
91
|
+
.map((x) => this.optionsResolver.resolve(x.metadata, cmd.opts()));
|
|
92
|
+
|
|
93
|
+
return method(...args.value());
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private assertNoParams(ctor: Type): void {
|
|
98
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', ctor);
|
|
99
|
+
if (paramTypes?.length > 0) {
|
|
100
|
+
throw new Error('Command constructor must have no parameters');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { OptionMetadata } from '@rsdk/cli.common';
|
|
2
|
+
import { BoolParser, Case } from '@rsdk/common';
|
|
3
|
+
import type { Env } from '@rsdk/env';
|
|
4
|
+
import type { ILogger } from '@rsdk/logging';
|
|
5
|
+
import type { OptionValues } from 'commander';
|
|
6
|
+
import _ from 'lodash';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Класс для разрешения значений опций командной строки.
|
|
10
|
+
* Обрабатывает значения из трех источников: переменные окружения, флаги командной строки и значения по умолчанию.
|
|
11
|
+
*/
|
|
12
|
+
export class OptionsResolver {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly env: Env,
|
|
15
|
+
private readonly logger: ILogger,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Разрешает значение опции командной строки на основе метаданных и переданных параметров.
|
|
20
|
+
* Проверяет три источника значений в порядке приоритета:
|
|
21
|
+
* 1. Переменные окружения
|
|
22
|
+
* 2. Флаги командной строки
|
|
23
|
+
* 3. Значения по умолчанию
|
|
24
|
+
*
|
|
25
|
+
* Такой приоритет выбран для удобства конфигурирования в разных окружениях:
|
|
26
|
+
* - В локальной разработке проблем с ENV обычно не возникает
|
|
27
|
+
* - В Kubernetes удобнее редактировать ConfigMap, чем Deployment
|
|
28
|
+
* @param {OptionMetadata} optionMetadata - Метаданные опции, содержащие имя, парсер и значение по умолчанию
|
|
29
|
+
* @param {OptionValues} passedOptions - Объект с переданными опциями командной строки
|
|
30
|
+
* @returns {any} Разрешенное значение опции
|
|
31
|
+
*/
|
|
32
|
+
resolve(optionMetadata: OptionMetadata, passedOptions: OptionValues): any {
|
|
33
|
+
this.logger.trace('handle arg', { optionMetadata });
|
|
34
|
+
|
|
35
|
+
const parser = optionMetadata.parser ?? new BoolParser();
|
|
36
|
+
|
|
37
|
+
// Находим значение переменной окружения
|
|
38
|
+
const envKey = this.createEnvKey(optionMetadata);
|
|
39
|
+
const variable = this.env.optional(envKey, parser);
|
|
40
|
+
|
|
41
|
+
// Находим значение переданное в виде флага
|
|
42
|
+
const optsKey = _.camelCase(optionMetadata.name);
|
|
43
|
+
const isPassed = Object.hasOwn(passedOptions, optsKey);
|
|
44
|
+
const valueFromFlag = passedOptions[optsKey];
|
|
45
|
+
|
|
46
|
+
if (variable) {
|
|
47
|
+
if (isPassed) {
|
|
48
|
+
this.logger.warn(
|
|
49
|
+
`Environment variable ${envKey} overrides command flags`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Возвращаем значение переменной окружения
|
|
54
|
+
return variable.value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.logger.trace(`Environment variable ${envKey} not provided`);
|
|
58
|
+
|
|
59
|
+
if (isPassed) {
|
|
60
|
+
this.logger.trace(`Use flag value: ${valueFromFlag}`, {
|
|
61
|
+
key: optsKey,
|
|
62
|
+
opts: passedOptions,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return parser.parse(valueFromFlag);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const defaultValue = optionMetadata.defaultValue;
|
|
69
|
+
|
|
70
|
+
this.logger.trace(`Use default value: ${defaultValue}`, {
|
|
71
|
+
key: optsKey,
|
|
72
|
+
opts: passedOptions,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* undefined - вполне нормально значение по умолчанию
|
|
77
|
+
*/
|
|
78
|
+
return defaultValue === undefined ? undefined : parser.parse(defaultValue);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Создает ключ переменной окружения на основе метаданных опции.
|
|
83
|
+
*
|
|
84
|
+
* @param {OptionMetadata} optionMetadata - Метаданные опции, содержащие имя и стратегию.
|
|
85
|
+
* @returns {string} Ключ переменной окружения в формате верхнего регистра с подчеркиванием.
|
|
86
|
+
*/
|
|
87
|
+
createEnvKey(optionMetadata: OptionMetadata): string {
|
|
88
|
+
return Case.toUpperSnake(
|
|
89
|
+
`${optionMetadata.name}${optionMetadata.parser ? '' : '_enabled'}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/test/cli.spec.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ILogger } from '@rsdk/logging';
|
|
2
|
+
import { NoopLogger } from '@rsdk/logging';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
|
|
5
|
+
import { Cli } from '../src/cli';
|
|
6
|
+
|
|
7
|
+
import WithAll from './commands/command.with-all';
|
|
8
|
+
|
|
9
|
+
describe('Cli', () => {
|
|
10
|
+
let logger: ILogger;
|
|
11
|
+
let program: Command;
|
|
12
|
+
let cli: Cli;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
logger = new NoopLogger();
|
|
16
|
+
program = new Command();
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
19
|
+
program.parseAsync = jest.fn().mockResolvedValue(undefined);
|
|
20
|
+
|
|
21
|
+
cli = new Cli(program, logger, {
|
|
22
|
+
cliPackageName: '@rsdk/cli.core',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
// Переопределяем program перед каждым тестом
|
|
28
|
+
jest.resetModules();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('handle', () => {
|
|
32
|
+
describe('when loading command with subcommands and variations', () => {
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
const mockLoadCommand = jest.spyOn(cli as any, 'loadCommand');
|
|
35
|
+
|
|
36
|
+
mockLoadCommand.mockResolvedValue(WithAll);
|
|
37
|
+
|
|
38
|
+
await cli.handle('with-all');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should load command correctly', () => {
|
|
42
|
+
const [mainCommand] = program.commands;
|
|
43
|
+
|
|
44
|
+
expect(mainCommand.name()).toBe('with-all');
|
|
45
|
+
expect(mainCommand.description()).toBe(
|
|
46
|
+
'Test command with subcommands and variations',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should register variations correctly', () => {
|
|
51
|
+
const [, ...variations] = program.commands;
|
|
52
|
+
|
|
53
|
+
expect(variations).toHaveLength(2);
|
|
54
|
+
|
|
55
|
+
const firstVariation = variations.find(
|
|
56
|
+
(cmd) => cmd.name() === 'with-variations:first',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(firstVariation).toBeDefined();
|
|
60
|
+
expect(firstVariation?.description()).toBe('First variation example');
|
|
61
|
+
|
|
62
|
+
const secondVariation = variations.find(
|
|
63
|
+
(cmd) => cmd.name() === 'with-variations:second',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(secondVariation).toBeDefined();
|
|
67
|
+
expect(secondVariation?.description()).toBe('Second variation example');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should register subcommands correctly', () => {
|
|
71
|
+
const [mainCommand] = program.commands;
|
|
72
|
+
const subcommands = mainCommand.commands.filter(
|
|
73
|
+
(cmd) => cmd.name() !== 'with-all',
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(subcommands).toHaveLength(2);
|
|
77
|
+
|
|
78
|
+
const first = subcommands.find(
|
|
79
|
+
(cmd) => cmd.name() === 'first-subcommand',
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(first).toBeDefined();
|
|
83
|
+
expect(first?.description()).toBe('First subcommand example');
|
|
84
|
+
|
|
85
|
+
const second = subcommands.find(
|
|
86
|
+
(cmd) => cmd.name() === 'second-subcommand',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(second).toBeDefined();
|
|
90
|
+
expect(second?.description()).toBe('Second subcommand example');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('error handling', () => {
|
|
95
|
+
it('should handle command loading errors', async () => {
|
|
96
|
+
// Arrange
|
|
97
|
+
const error = new Error('Command load failed');
|
|
98
|
+
const mockLoadCommand = jest.spyOn(cli as any, 'loadCommand');
|
|
99
|
+
|
|
100
|
+
mockLoadCommand.mockRejectedValue(error);
|
|
101
|
+
|
|
102
|
+
// Act
|
|
103
|
+
await cli.handle('invalid-command');
|
|
104
|
+
|
|
105
|
+
expect(program.parseAsync).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should skip command loading when no arg provided', async () => {
|
|
109
|
+
// Arrange
|
|
110
|
+
const mockLoadCommand = jest.spyOn(cli as any, 'loadCommand');
|
|
111
|
+
|
|
112
|
+
// Act
|
|
113
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
114
|
+
await cli.handle(undefined);
|
|
115
|
+
|
|
116
|
+
expect(mockLoadCommand).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should skip command loading when arg starts with -', async () => {
|
|
120
|
+
// Arrange
|
|
121
|
+
const mockLoadCommand = jest.spyOn(cli as any, 'loadCommand');
|
|
122
|
+
|
|
123
|
+
// Act
|
|
124
|
+
await cli.handle('--help');
|
|
125
|
+
|
|
126
|
+
expect(mockLoadCommand).not.toHaveBeenCalled();
|
|
127
|
+
expect(program.commands).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command, IRunnable, Option, ValueOption } from '@rsdk/cli.common';
|
|
2
|
+
import { StringParser } from '@rsdk/common';
|
|
3
|
+
import { LoggerFactory } from '@rsdk/logging';
|
|
4
|
+
|
|
5
|
+
@Command('cmd-single', {
|
|
6
|
+
description: 'Test command that logs parameters',
|
|
7
|
+
})
|
|
8
|
+
export default class SingleCommand implements IRunnable {
|
|
9
|
+
async run(
|
|
10
|
+
@ValueOption('message', new StringParser(), {
|
|
11
|
+
description: 'Message to log',
|
|
12
|
+
defaultValue: 'Hello World!',
|
|
13
|
+
alias: 'm',
|
|
14
|
+
})
|
|
15
|
+
message: string,
|
|
16
|
+
|
|
17
|
+
@Option('verbose', { description: 'Verbose output' })
|
|
18
|
+
verbose: boolean,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const logger = LoggerFactory.create(SingleCommand);
|
|
21
|
+
|
|
22
|
+
if (verbose) {
|
|
23
|
+
logger.info('Command parameters:');
|
|
24
|
+
logger.info(`message: ${message}`);
|
|
25
|
+
logger.info(`verbose: ${verbose}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
logger.info(message);
|
|
29
|
+
}
|
|
30
|
+
}
|