@travetto/cli 3.4.8 → 3.4.10
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 +5 -1
- package/package.json +1 -1
- package/src/decorators.ts +4 -2
- package/src/execute.ts +4 -5
- package/src/help.ts +3 -2
- package/src/parse.ts +30 -46
- package/src/schema.ts +30 -20
- package/src/types.ts +17 -1
- package/support/cli.main.ts +6 -2
package/README.md
CHANGED
|
@@ -411,6 +411,10 @@ export class RunCommand {
|
|
|
411
411
|
**Code: Anatomy of a Command**
|
|
412
412
|
```typescript
|
|
413
413
|
export interface CliCommandShape<T extends unknown[] = unknown[]> {
|
|
414
|
+
/**
|
|
415
|
+
* Parsed state
|
|
416
|
+
*/
|
|
417
|
+
_parsed?: ParsedState;
|
|
414
418
|
/**
|
|
415
419
|
* Action target of the command
|
|
416
420
|
*/
|
|
@@ -493,7 +497,7 @@ As noted in the example above, `fields` is specified in this execution, with sup
|
|
|
493
497
|
The `module` field is slightly more complex, but is geared towards supporting commands within a monorepo context. This flag ensures that a module is specified if running from the root of the monorepo, and that the module provided is real, and can run the desired command. When running from an explicit module folder in the monorepo, the module flag is ignored.
|
|
494
498
|
|
|
495
499
|
### Custom Validation
|
|
496
|
-
In addition to dependency injection, the command contract also allows for a custom validation function, which will have access to bound command (flags, and args) as well as the unknown arguments. When a command implements this method, any [CliValidationError](https://github.com/travetto/travetto/tree/main/module/cli/src/types.ts#
|
|
500
|
+
In addition to dependency injection, the command contract also allows for a custom validation function, which will have access to bound command (flags, and args) as well as the unknown arguments. When a command implements this method, any [CliValidationError](https://github.com/travetto/travetto/tree/main/module/cli/src/types.ts#L22) errors that are returned will be shared with the user, and fail to invoke the `main` method.
|
|
497
501
|
|
|
498
502
|
**Code: CliValidationError**
|
|
499
503
|
```typescript
|
package/package.json
CHANGED
package/src/decorators.ts
CHANGED
|
@@ -26,8 +26,9 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
|
|
|
26
26
|
const { commandModule } = CliCommandRegistry.registerClass(target, {
|
|
27
27
|
hidden: cfg.hidden,
|
|
28
28
|
preMain: async (cmd) => {
|
|
29
|
-
if (addEnv
|
|
30
|
-
|
|
29
|
+
if (addEnv) {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
31
|
+
defineEnv({ envName: (cmd as { env?: string }).env ?? 'dev' });
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
});
|
|
@@ -47,6 +48,7 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
|
|
|
47
48
|
SchemaRegistry.registerPendingFieldConfig(target, 'module', String, {
|
|
48
49
|
aliases: ['m', CliParseUtil.toEnvField('TRV_MODULE')],
|
|
49
50
|
description: 'Module to run for',
|
|
51
|
+
specifiers: ['module'],
|
|
50
52
|
required: { active: CliUtil.monoRoot }
|
|
51
53
|
});
|
|
52
54
|
}
|
package/src/execute.ts
CHANGED
|
@@ -19,13 +19,12 @@ export class ExecutionManager {
|
|
|
19
19
|
*/
|
|
20
20
|
static async #runCommand(cmd: CliCommandShape, args: string[]): Promise<RunResponse> {
|
|
21
21
|
const schema = await CliCommandSchemaUtil.getSchema(cmd);
|
|
22
|
-
|
|
22
|
+
args = await CliParseUtil.expandArgs(schema, args);
|
|
23
|
+
cmd._parsed = await CliParseUtil.parse(schema, args);
|
|
23
24
|
const cfg = CliCommandRegistry.getConfig(cmd);
|
|
24
25
|
|
|
25
|
-
CliParseUtil.setState(cmd, state);
|
|
26
|
-
|
|
27
26
|
await cmd.preBind?.();
|
|
28
|
-
const known = await CliCommandSchemaUtil.bindInput(cmd,
|
|
27
|
+
const known = await CliCommandSchemaUtil.bindInput(cmd, cmd._parsed);
|
|
29
28
|
|
|
30
29
|
await cmd.preValidate?.();
|
|
31
30
|
await CliCommandSchemaUtil.validate(cmd, known);
|
|
@@ -75,7 +74,7 @@ export class ExecutionManager {
|
|
|
75
74
|
|
|
76
75
|
let command: CliCommandShape | undefined;
|
|
77
76
|
try {
|
|
78
|
-
const { cmd, args, help } =
|
|
77
|
+
const { cmd, args, help } = CliParseUtil.getArgs(argv);
|
|
79
78
|
|
|
80
79
|
if (!cmd) {
|
|
81
80
|
console.info!(await HelpUtil.renderAllHelp());
|
package/src/help.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { CliCommandShape } from './types';
|
|
|
6
6
|
import { CliCommandRegistry } from './registry';
|
|
7
7
|
import { CliCommandSchemaUtil } from './schema';
|
|
8
8
|
import { CliValidationResultError } from './error';
|
|
9
|
+
import { isBoolFlag } from './parse';
|
|
9
10
|
|
|
10
11
|
const validationSourceMap = {
|
|
11
12
|
custom: '',
|
|
@@ -47,7 +48,7 @@ export class HelpUtil {
|
|
|
47
48
|
const flagVal = command[key] as unknown as Exclude<Primitive, Error>;
|
|
48
49
|
|
|
49
50
|
let aliases = flag.flagNames ?? [];
|
|
50
|
-
if (flag
|
|
51
|
+
if (isBoolFlag(flag)) {
|
|
51
52
|
if (flagVal === true) {
|
|
52
53
|
aliases = (flag.flagNames ?? []).filter(x => !/^[-][^-]/.test(x));
|
|
53
54
|
} else {
|
|
@@ -55,7 +56,7 @@ export class HelpUtil {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
const param = [cliTpl`${{ param: aliases.join(', ') }}`];
|
|
58
|
-
if (!(flag
|
|
59
|
+
if (!isBoolFlag(flag)) {
|
|
59
60
|
const type = flag.type === 'string' && flag.choices && flag.choices.length <= 3 ? flag.choices?.join('|') : flag.type;
|
|
60
61
|
param.push(cliTpl`${{ type: `<${type}>` }}`);
|
|
61
62
|
}
|
package/src/parse.ts
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
|
|
3
3
|
import { RootIndex, path } from '@travetto/manifest';
|
|
4
|
-
import { CliCommandInput, CliCommandSchema,
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
type ParsedArg = { type: 'arg', input: string, array?: boolean, index: number };
|
|
8
|
-
type ParsedUnknown = { type: 'unknown', input: string };
|
|
9
|
-
type ParsedInput = ParsedUnknown | ParsedFlag | ParsedArg;
|
|
10
|
-
|
|
11
|
-
export type ParsedState = {
|
|
12
|
-
inputs: string[];
|
|
13
|
-
all: ParsedInput[];
|
|
14
|
-
flags: ParsedFlag[];
|
|
15
|
-
unknown: string[];
|
|
16
|
-
};
|
|
4
|
+
import { CliCommandInput, CliCommandSchema, ParsedState } from './types';
|
|
5
|
+
|
|
6
|
+
type ParsedInput = ParsedState['all'][number];
|
|
17
7
|
|
|
18
8
|
const RAW_SEP = '--';
|
|
19
9
|
const VALID_FLAG = /^-{1,2}[a-z]/i;
|
|
@@ -21,10 +11,9 @@ const HELP_FLAG = /^-h|--help$/;
|
|
|
21
11
|
const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
|
|
22
12
|
const CONFIG_PRE = '+=';
|
|
23
13
|
const ENV_PRE = 'env.';
|
|
24
|
-
const ParsedⲐ = Symbol.for('@travetto/cli:parsed');
|
|
25
14
|
const SPACE = new Set([32, 7, 13, 10]);
|
|
26
15
|
|
|
27
|
-
const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
|
|
16
|
+
export const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
|
|
28
17
|
|
|
29
18
|
const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: string, index?: number, value?: string }): ParsedInput => {
|
|
30
19
|
const { field, input, rawText = input, value, index } = cfg;
|
|
@@ -43,6 +32,7 @@ const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: strin
|
|
|
43
32
|
}
|
|
44
33
|
};
|
|
45
34
|
|
|
35
|
+
|
|
46
36
|
/**
|
|
47
37
|
* Parsing support for the cli
|
|
48
38
|
*/
|
|
@@ -90,20 +80,19 @@ export class CliParseUtil {
|
|
|
90
80
|
/**
|
|
91
81
|
* Read configuration file given flag
|
|
92
82
|
*/
|
|
93
|
-
static async readFlagFile(flag: string): Promise<string[]> {
|
|
83
|
+
static async readFlagFile(flag: string, mod: string): Promise<string[]> {
|
|
94
84
|
const key = flag.replace(CONFIG_PRE, '');
|
|
95
|
-
const mod = RootIndex.mainModuleName;
|
|
96
85
|
|
|
97
86
|
// We have a file
|
|
98
87
|
const rel = (key.includes('/') ? key : `@/support/pack.${key}.flags`)
|
|
99
88
|
.replace('@@/', `${RootIndex.manifest.workspacePath}/`)
|
|
100
89
|
.replace('@/', `${mod}/`)
|
|
101
|
-
.replace(/^(@[^\/]+\/[^\/]+)
|
|
90
|
+
.replace(/^(@[^\/]+\/[^\/]+)(\/.*)$/, (_, imp, rest) => {
|
|
102
91
|
const val = RootIndex.getModule(imp);
|
|
103
92
|
if (!val) {
|
|
104
|
-
throw new Error(`Unknown module file: ${
|
|
93
|
+
throw new Error(`Unknown module file: ${_}, unable to proceed`);
|
|
105
94
|
}
|
|
106
|
-
return val.sourcePath
|
|
95
|
+
return `${val.sourcePath}${rest}`;
|
|
107
96
|
});
|
|
108
97
|
|
|
109
98
|
const file = path.resolve(rel);
|
|
@@ -128,7 +117,7 @@ export class CliParseUtil {
|
|
|
128
117
|
* Parse args to extract command from argv along with other params. Will skip
|
|
129
118
|
* argv[0] and argv[1] if equal to process.argv[0:2]
|
|
130
119
|
*/
|
|
131
|
-
static
|
|
120
|
+
static getArgs(argv: string[]): { cmd?: string, args: string[], help?: boolean } {
|
|
132
121
|
let offset = 0;
|
|
133
122
|
if (argv[0] === process.argv[0] && argv[1] === process.argv[1]) {
|
|
134
123
|
offset = 2;
|
|
@@ -138,19 +127,28 @@ export class CliParseUtil {
|
|
|
138
127
|
const valid = out.slice(0, max);
|
|
139
128
|
const cmd = valid.length > 0 && !valid[0].startsWith('-') ? valid[0] : undefined;
|
|
140
129
|
const helpIdx = valid.findIndex(x => HELP_FLAG.test(x));
|
|
141
|
-
const args =
|
|
142
|
-
for (const item of out.slice(cmd ? 1 : 0)) {
|
|
143
|
-
if (item.startsWith(CONFIG_PRE)) {
|
|
144
|
-
args.push(...await this.readFlagFile(item));
|
|
145
|
-
} else {
|
|
146
|
-
args.push(item);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
args.push(...out.slice(max));
|
|
130
|
+
const args = out.slice(cmd ? 1 : 0);
|
|
150
131
|
const res = { cmd, args, help: helpIdx >= 0 };
|
|
151
132
|
return res;
|
|
152
133
|
}
|
|
153
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Expand flag arguments into full argument list
|
|
137
|
+
*/
|
|
138
|
+
static async expandArgs(schema: CliCommandSchema, args: string[]): Promise<string[]> {
|
|
139
|
+
const SEP = args.includes(RAW_SEP) ? args.indexOf(RAW_SEP) : args.length;
|
|
140
|
+
const input = schema.flags.find(x => x.type === 'module');
|
|
141
|
+
const ENV_KEY = input?.flagNames?.filter(x => x.startsWith(ENV_PRE)).map(x => x.replace(ENV_PRE, ''))[0] ?? '';
|
|
142
|
+
const flags = new Set(input?.flagNames ?? []);
|
|
143
|
+
const check = (k?: string, v?: string): string | undefined => flags.has(k!) ? v : undefined;
|
|
144
|
+
const mod = args.reduce(
|
|
145
|
+
(m, x, i, arr) =>
|
|
146
|
+
(i < SEP ? check(arr[i - 1], x) ?? check(...x.split('=')) : undefined) ?? m,
|
|
147
|
+
process.env[ENV_KEY] || RootIndex.mainModuleName
|
|
148
|
+
);
|
|
149
|
+
return (await Promise.all(args.map((x, i) => x.startsWith(CONFIG_PRE) && (i < SEP || SEP < 0) ? this.readFlagFile(x, mod) : x))).flat();
|
|
150
|
+
}
|
|
151
|
+
|
|
154
152
|
/**
|
|
155
153
|
* Parse inputs to command
|
|
156
154
|
*/
|
|
@@ -210,22 +208,8 @@ export class CliParseUtil {
|
|
|
210
208
|
return {
|
|
211
209
|
inputs,
|
|
212
210
|
all: out,
|
|
213
|
-
unknown: out.filter(
|
|
214
|
-
flags: out.filter((x): x is
|
|
211
|
+
unknown: out.filter(x => x.type === 'unknown').map(x => x.input),
|
|
212
|
+
flags: out.filter((x): x is ParsedInput & { type: 'flag' } => x.type === 'flag')
|
|
215
213
|
};
|
|
216
214
|
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Get parse state from the command
|
|
220
|
-
*/
|
|
221
|
-
static getState(cmd: CliCommandShape & { [ParsedⲐ]?: ParsedState }): ParsedState | undefined {
|
|
222
|
-
return cmd[ParsedⲐ];
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Set state for a command
|
|
227
|
-
*/
|
|
228
|
-
static setState(cmd: CliCommandShape & { [ParsedⲐ]?: ParsedState }, state: ParsedState): void {
|
|
229
|
-
cmd[ParsedⲐ] = state;
|
|
230
|
-
}
|
|
231
215
|
}
|
package/src/schema.ts
CHANGED
|
@@ -2,35 +2,45 @@ import { Class, ConsoleManager, GlobalEnv } from '@travetto/base';
|
|
|
2
2
|
import { BindUtil, FieldConfig, SchemaRegistry, SchemaValidator, ValidationResultError } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { CliCommandRegistry } from './registry';
|
|
5
|
-
import { CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
|
|
5
|
+
import { ParsedState, CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
|
|
6
6
|
import { CliValidationResultError } from './error';
|
|
7
|
-
import { ParsedState } from './parse';
|
|
8
7
|
|
|
9
8
|
const LONG_FLAG = /^--[a-z][^= ]+/i;
|
|
10
9
|
const SHORT_FLAG = /^-[a-z]/i;
|
|
11
10
|
|
|
12
11
|
const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
|
|
13
12
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
13
|
+
function baseType(x: FieldConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
|
|
14
|
+
switch (x.type) {
|
|
15
|
+
case Date: return { type: 'date' };
|
|
16
|
+
case Boolean: return { type: 'boolean' };
|
|
17
|
+
case Number: return { type: 'number' };
|
|
18
|
+
case RegExp: return { type: 'regex' };
|
|
19
|
+
case String: {
|
|
20
|
+
switch (true) {
|
|
21
|
+
case x.specifiers?.includes('module'): return { type: 'module' };
|
|
22
|
+
case x.specifiers?.includes('file'): return {
|
|
23
|
+
type: 'file',
|
|
24
|
+
fileExtensions: x.specifiers?.map(s => s.split('ext:')[1]).filter(s => !!s)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { type: 'string' };
|
|
32
30
|
}
|
|
33
31
|
|
|
32
|
+
const fieldToInput = (x: FieldConfig): CliCommandInput => ({
|
|
33
|
+
...baseType(x),
|
|
34
|
+
name: x.name,
|
|
35
|
+
description: x.description,
|
|
36
|
+
array: x.array,
|
|
37
|
+
required: x.required?.active,
|
|
38
|
+
choices: x.enum?.values,
|
|
39
|
+
default: Array.isArray(x.default) ? x.default.slice(0) : x.default,
|
|
40
|
+
flagNames: (x.aliases ?? []).slice(0).filter(v => !v.startsWith('env.')),
|
|
41
|
+
envVars: (x.aliases ?? []).slice(0).filter(v => v.startsWith('env.')).map(v => v.replace('env.', ''))
|
|
42
|
+
});
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* Allows binding describing/binding inputs for commands
|
|
36
46
|
*/
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,18 @@ type OrProm<T> = T | Promise<T>;
|
|
|
4
4
|
|
|
5
5
|
export type RunResponse = { wait(): Promise<unknown> } | { on(event: 'close', cb: Function): unknown } | Closeable | void | undefined;
|
|
6
6
|
|
|
7
|
+
type ParsedFlag = { type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
|
|
8
|
+
type ParsedArg = { type: 'arg', input: string, array?: boolean, index: number };
|
|
9
|
+
type ParsedUnknown = { type: 'unknown', input: string };
|
|
10
|
+
type ParsedInput = ParsedUnknown | ParsedFlag | ParsedArg;
|
|
11
|
+
|
|
12
|
+
export type ParsedState = {
|
|
13
|
+
inputs: string[];
|
|
14
|
+
all: ParsedInput[];
|
|
15
|
+
flags: ParsedFlag[];
|
|
16
|
+
unknown: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
20
|
* Constrained version of Schema's Validation Error
|
|
9
21
|
*/
|
|
@@ -22,6 +34,10 @@ export type CliValidationError = {
|
|
|
22
34
|
* CLI Command Contract
|
|
23
35
|
*/
|
|
24
36
|
export interface CliCommandShape<T extends unknown[] = unknown[]> {
|
|
37
|
+
/**
|
|
38
|
+
* Parsed state
|
|
39
|
+
*/
|
|
40
|
+
_parsed?: ParsedState;
|
|
25
41
|
/**
|
|
26
42
|
* Action target of the command
|
|
27
43
|
*/
|
|
@@ -84,7 +100,7 @@ export type CliCommandShapeFields = {
|
|
|
84
100
|
export type CliCommandInput = {
|
|
85
101
|
name: string;
|
|
86
102
|
description?: string;
|
|
87
|
-
type: 'string' | 'file' | 'number' | 'boolean' | 'date' | 'regex';
|
|
103
|
+
type: 'string' | 'file' | 'number' | 'boolean' | 'date' | 'regex' | 'module';
|
|
88
104
|
fileExtensions?: string[];
|
|
89
105
|
choices?: unknown[];
|
|
90
106
|
required?: boolean;
|
package/support/cli.main.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
|
|
3
3
|
import { ShutdownManager } from '@travetto/base';
|
|
4
|
-
import { CliCommandShape, CliCommand, CliValidationError,
|
|
4
|
+
import { CliCommandShape, CliCommand, CliValidationError, ParsedState } from '@travetto/cli';
|
|
5
5
|
import { path, RootIndex } from '@travetto/manifest';
|
|
6
|
+
import { Ignore } from '@travetto/schema';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Allows for running of main entry points
|
|
@@ -10,6 +11,9 @@ import { path, RootIndex } from '@travetto/manifest';
|
|
|
10
11
|
@CliCommand({ hidden: true })
|
|
11
12
|
export class MainCommand implements CliCommandShape {
|
|
12
13
|
|
|
14
|
+
@Ignore()
|
|
15
|
+
_parsed: ParsedState;
|
|
16
|
+
|
|
13
17
|
async #getImport(fileOrImport: string): Promise<string | undefined> {
|
|
14
18
|
// If referenced file exists
|
|
15
19
|
let file = fileOrImport;
|
|
@@ -32,7 +36,7 @@ export class MainCommand implements CliCommandShape {
|
|
|
32
36
|
const imp = await this.#getImport(fileOrImport);
|
|
33
37
|
const mod = await import(imp!);
|
|
34
38
|
|
|
35
|
-
await ShutdownManager.exitWithResponse(await mod.main(...args, ...
|
|
39
|
+
await ShutdownManager.exitWithResponse(await mod.main(...args, ...this._parsed.unknown));
|
|
36
40
|
} catch (err) {
|
|
37
41
|
await ShutdownManager.exitWithResponse(err, true);
|
|
38
42
|
}
|