@travetto/cli 6.0.1 → 7.0.0-rc.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 +31 -33
- package/__index__.ts +4 -2
- package/package.json +3 -3
- package/src/error.ts +1 -1
- package/src/execute.ts +3 -3
- package/src/help.ts +53 -48
- package/src/module.ts +20 -0
- package/src/parse.ts +64 -43
- package/src/registry/decorator.ts +153 -0
- package/src/registry/registry-adapter.ts +96 -0
- package/src/registry/registry-index.ts +124 -0
- package/src/schema-export.ts +90 -0
- package/src/schema.ts +16 -109
- package/src/types.ts +4 -36
- package/src/util.ts +5 -3
- package/support/cli.cli_schema.ts +25 -22
- package/support/cli.main.ts +3 -2
- package/src/decorators.ts +0 -154
- package/src/registry.ts +0 -96
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Class, ClassInstance, Env, Runtime, RuntimeIndex, describeFunction, getClass } from '@travetto/runtime';
|
|
2
|
+
import { SchemaFieldConfig, SchemaRegistryIndex, ValidationError } from '@travetto/schema';
|
|
3
|
+
|
|
4
|
+
import { CliCommandShape } from '../types.ts';
|
|
5
|
+
import { CliCommandRegistryIndex } from './registry-index.ts';
|
|
6
|
+
import { CliModuleUtil } from '../module.ts';
|
|
7
|
+
import { CliParseUtil } from '../parse.ts';
|
|
8
|
+
import { CliUtil } from '../util.ts';
|
|
9
|
+
|
|
10
|
+
type Cmd = CliCommandShape & { env?: string };
|
|
11
|
+
|
|
12
|
+
type CliCommandConfigOptions = {
|
|
13
|
+
runTarget?: boolean;
|
|
14
|
+
runtimeModule?: 'current' | 'command';
|
|
15
|
+
with?: {
|
|
16
|
+
/** Application environment */
|
|
17
|
+
env?: boolean;
|
|
18
|
+
/** Module to run for */
|
|
19
|
+
module?: boolean;
|
|
20
|
+
/** Should debug invocation trigger via ipc */
|
|
21
|
+
debugIpc?: boolean;
|
|
22
|
+
/** Should the invocation automatically restart on exit */
|
|
23
|
+
canRestart?: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const FIELD_CONFIG: {
|
|
28
|
+
name: keyof Exclude<CliCommandConfigOptions['with'], undefined>;
|
|
29
|
+
field: Partial<SchemaFieldConfig>;
|
|
30
|
+
run: (cmd: Cmd) => (Promise<unknown> | unknown);
|
|
31
|
+
}[] =
|
|
32
|
+
[
|
|
33
|
+
{
|
|
34
|
+
name: 'env',
|
|
35
|
+
run: cmd => Env.TRV_ENV.set(cmd.env || Runtime.env),
|
|
36
|
+
field: {
|
|
37
|
+
type: String,
|
|
38
|
+
aliases: ['-e', CliParseUtil.toEnvField(Env.TRV_ENV.key)],
|
|
39
|
+
description: 'Application environment',
|
|
40
|
+
required: { active: false },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'module',
|
|
45
|
+
run: (): void => { },
|
|
46
|
+
field: {
|
|
47
|
+
type: String,
|
|
48
|
+
aliases: ['-m', CliParseUtil.toEnvField(Env.TRV_MODULE.key)],
|
|
49
|
+
description: 'Module to run for',
|
|
50
|
+
specifiers: ['module'],
|
|
51
|
+
required: { active: Runtime.monoRoot },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'debugIpc',
|
|
56
|
+
run: cmd => CliUtil.debugIfIpc(cmd).then((v) => v && process.exit(0)),
|
|
57
|
+
field: {
|
|
58
|
+
type: Boolean,
|
|
59
|
+
aliases: ['-di'],
|
|
60
|
+
description: 'Should debug invocation trigger via ipc',
|
|
61
|
+
default: true,
|
|
62
|
+
required: { active: false },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'canRestart',
|
|
67
|
+
run: cmd => CliUtil.runWithRestart(cmd)?.then((v) => v && process.exit(0)),
|
|
68
|
+
field: {
|
|
69
|
+
type: Boolean,
|
|
70
|
+
aliases: ['-cr'],
|
|
71
|
+
description: 'Should the invocation automatically restart on exit',
|
|
72
|
+
default: false,
|
|
73
|
+
required: { active: false },
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decorator to register a CLI command
|
|
80
|
+
*
|
|
81
|
+
* @augments `@travetto/schema:Schema`
|
|
82
|
+
* @example main
|
|
83
|
+
* @kind decorator
|
|
84
|
+
*/
|
|
85
|
+
export function CliCommand(cfg: CliCommandConfigOptions = {}) {
|
|
86
|
+
return function <T extends CliCommandShape>(target: Class<T>): void {
|
|
87
|
+
const adapter = SchemaRegistryIndex.getForRegister(target);
|
|
88
|
+
const description = describeFunction(target) ?? {};
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if (!target.Ⲑid || description.abstract) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const VALID_FIELDS = FIELD_CONFIG.filter(f => !!cfg.with?.[f.name]);
|
|
96
|
+
|
|
97
|
+
CliCommandRegistryIndex.getForRegister(target).register({
|
|
98
|
+
runTarget: cfg.runTarget,
|
|
99
|
+
preMain: async (cmd: Cmd) => {
|
|
100
|
+
for (const field of VALID_FIELDS) {
|
|
101
|
+
await field.run(cmd);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const commandModule = description.module;
|
|
107
|
+
|
|
108
|
+
for (const { name, field: { type, ...field } } of VALID_FIELDS) {
|
|
109
|
+
adapter.registerField(name, field, { type });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const runtimeModule = cfg.runtimeModule ?? (cfg.with?.module ? 'current' : undefined);
|
|
113
|
+
|
|
114
|
+
if (runtimeModule) { // Validate module
|
|
115
|
+
adapter.register({
|
|
116
|
+
validators: [async ({ module: mod }): Promise<ValidationError | undefined> => {
|
|
117
|
+
const runModule = (runtimeModule === 'command' ? commandModule : mod) || Runtime.main.name;
|
|
118
|
+
|
|
119
|
+
// If we need to run as a specific module
|
|
120
|
+
if (runModule !== Runtime.main.name) {
|
|
121
|
+
try {
|
|
122
|
+
RuntimeIndex.reinitForModule(runModule);
|
|
123
|
+
} catch {
|
|
124
|
+
return { source: 'flag', message: `${runModule} is an unknown module`, kind: 'custom', path: '.' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!(await CliModuleUtil.moduleHasDependency(runModule, commandModule))) {
|
|
129
|
+
return { source: 'flag', message: `${runModule} does not have ${commandModule} as a dependency`, kind: 'custom', path: '.' };
|
|
130
|
+
}
|
|
131
|
+
}],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Decorator to register a CLI command flag
|
|
139
|
+
* @augments `@travetto/schema:Input`
|
|
140
|
+
* @kind decorator
|
|
141
|
+
*/
|
|
142
|
+
export function CliFlag(cfg: { full?: string, short?: string, fileExtensions?: string[], envVars?: string[] } = {}) {
|
|
143
|
+
return function (instance: ClassInstance, property: string | symbol): void {
|
|
144
|
+
const aliases = [
|
|
145
|
+
...(cfg.full ? [cfg.full.startsWith('-') ? cfg.full : `--${cfg.full}`] : []),
|
|
146
|
+
...(cfg.short ? [cfg.short.startsWith('-') ? cfg.short : `-${cfg.short}`] : []),
|
|
147
|
+
...(cfg.envVars ? cfg.envVars.map(CliParseUtil.toEnvField) : [])
|
|
148
|
+
];
|
|
149
|
+
const specifiers = cfg.fileExtensions?.length ? ['file', ...cfg.fileExtensions.map(x => `ext:${x.replace(/[*.]/g, '')}`)] : [];
|
|
150
|
+
|
|
151
|
+
SchemaRegistryIndex.getForRegister(getClass(instance)).registerField(property, { aliases, specifiers });
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Class, classConstruct, describeFunction } from '@travetto/runtime';
|
|
2
|
+
import { RegistryAdapter } from '@travetto/registry';
|
|
3
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
4
|
+
|
|
5
|
+
import { CliCommandConfig, CliCommandShape } from '../types.ts';
|
|
6
|
+
import { CliParseUtil, ENV_PREFIX } from '../parse.ts';
|
|
7
|
+
|
|
8
|
+
const CLI_FILE_REGEX = /\/cli[.](?<name>.{0,100}?)([.]tsx?)?$/;
|
|
9
|
+
|
|
10
|
+
const getName = (s: string): string => (s.match(CLI_FILE_REGEX)?.groups?.name ?? s).replaceAll('_', ':');
|
|
11
|
+
const stripDashes = (x?: string): string | undefined => x?.replace(/^-+/, '');
|
|
12
|
+
const toFlagName = (x: string): string => x.replace(/([a-z])([A-Z])/g, (_, l: string, r: string) => `${l}-${r.toLowerCase()}`);
|
|
13
|
+
|
|
14
|
+
export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConfig> {
|
|
15
|
+
#cls: Class;
|
|
16
|
+
#config: CliCommandConfig;
|
|
17
|
+
|
|
18
|
+
constructor(cls: Class) {
|
|
19
|
+
this.#cls = cls;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
finalize(): void {
|
|
23
|
+
// Add help command
|
|
24
|
+
const schema = SchemaRegistryIndex.getConfig(this.#cls);
|
|
25
|
+
|
|
26
|
+
// Add help to every command
|
|
27
|
+
(schema.fields ??= {}).help = {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
name: 'help',
|
|
30
|
+
owner: this.#cls,
|
|
31
|
+
description: 'display help for command',
|
|
32
|
+
required: { active: false },
|
|
33
|
+
default: false,
|
|
34
|
+
access: 'readonly',
|
|
35
|
+
aliases: ['-h', '--help']
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const used = new Set(Object.values(schema.fields)
|
|
39
|
+
.flatMap(f => f.aliases ?? [])
|
|
40
|
+
.filter(x => !x.startsWith(ENV_PREFIX))
|
|
41
|
+
.map(stripDashes)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
for (const field of Object.values(schema.fields)) {
|
|
45
|
+
const fieldName = field.name.toString();
|
|
46
|
+
const { long: longAliases, short: shortAliases, raw: rawAliases, env: envAliases } = CliParseUtil.parseAliases(field.aliases ?? []);
|
|
47
|
+
|
|
48
|
+
let short = stripDashes(shortAliases?.[0]) ?? rawAliases.find(x => x.length <= 2);
|
|
49
|
+
const long = stripDashes(longAliases?.[0]) ?? rawAliases.find(x => x.length >= 3) ?? toFlagName(fieldName);
|
|
50
|
+
const aliases: string[] = field.aliases = [...envAliases];
|
|
51
|
+
|
|
52
|
+
if (short === undefined) {
|
|
53
|
+
short = fieldName.charAt(0);
|
|
54
|
+
if (!used.has(short)) {
|
|
55
|
+
aliases.push(`-${short}`);
|
|
56
|
+
used.add(short);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
aliases.push(`-${short}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
aliases.push(`--${long}`);
|
|
63
|
+
|
|
64
|
+
if (field.type === Boolean) {
|
|
65
|
+
aliases.push(`--no-${long}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get(): CliCommandConfig {
|
|
71
|
+
return this.#config;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Registers a cli command
|
|
76
|
+
*/
|
|
77
|
+
register(...cfg: Partial<CliCommandConfig>[]): CliCommandConfig {
|
|
78
|
+
const meta = describeFunction(this.#cls);
|
|
79
|
+
this.#config ??= {
|
|
80
|
+
cls: this.#cls,
|
|
81
|
+
preMain: undefined,
|
|
82
|
+
name: getName(meta.import),
|
|
83
|
+
};
|
|
84
|
+
Object.assign(this.#config, ...cfg);
|
|
85
|
+
return this.#config;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get instance of the command
|
|
90
|
+
*/
|
|
91
|
+
getInstance(): CliCommandShape {
|
|
92
|
+
const instance: CliCommandShape = classConstruct(this.#cls);
|
|
93
|
+
instance._cfg = this.#config;
|
|
94
|
+
return instance;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Class, getClass, getParentClass, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
2
|
+
import { RegistryAdapter, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
3
|
+
import { SchemaClassConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
4
|
+
|
|
5
|
+
import { CliCommandConfig, CliCommandShape } from '../types.ts';
|
|
6
|
+
import { CliUnknownCommandError } from '../error.ts';
|
|
7
|
+
import { CliCommandRegistryAdapter } from './registry-adapter.ts';
|
|
8
|
+
|
|
9
|
+
const CLI_FILE_REGEX = /\/cli[.](?<name>.{0,100}?)([.]tsx?)?$/;
|
|
10
|
+
const getName = (s: string): string => (s.match(CLI_FILE_REGEX)?.groups?.name ?? s).replaceAll('_', ':');
|
|
11
|
+
|
|
12
|
+
type CliCommandLoadResult = { command: string, config: CliCommandConfig, instance: CliCommandShape, schema: SchemaClassConfig };
|
|
13
|
+
|
|
14
|
+
export class CliCommandRegistryIndex implements RegistryIndex {
|
|
15
|
+
|
|
16
|
+
static #instance = Registry.registerIndex(this);
|
|
17
|
+
|
|
18
|
+
static getForRegister(cls: Class): RegistryAdapter<CliCommandConfig> {
|
|
19
|
+
return this.#instance.store.getForRegister(cls);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static get(cls: Class): CliCommandConfig {
|
|
23
|
+
return this.#instance.store.get(cls).get();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static load(names?: string[]): Promise<CliCommandLoadResult[]> {
|
|
27
|
+
return this.#instance.load(names);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#fileMapping: Map<string, string>;
|
|
31
|
+
#instanceMapping: Map<string, CliCommandShape> = new Map();
|
|
32
|
+
|
|
33
|
+
store = new RegistryIndexStore(CliCommandRegistryAdapter);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get list of all commands available
|
|
37
|
+
*/
|
|
38
|
+
get #commandMapping(): Map<string, string> {
|
|
39
|
+
if (!this.#fileMapping) {
|
|
40
|
+
const all = new Map<string, string>();
|
|
41
|
+
for (const e of RuntimeIndex.find({
|
|
42
|
+
module: m => !Runtime.production || m.prod,
|
|
43
|
+
folder: f => f === 'support',
|
|
44
|
+
file: f => f.role === 'std' && CLI_FILE_REGEX.test(f.sourceFile)
|
|
45
|
+
})) {
|
|
46
|
+
all.set(getName(e.sourceFile), e.import);
|
|
47
|
+
}
|
|
48
|
+
this.#fileMapping = all;
|
|
49
|
+
}
|
|
50
|
+
return this.#fileMapping;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
process(): void {
|
|
55
|
+
// Do nothing for now?
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Import command into an instance
|
|
60
|
+
*/
|
|
61
|
+
async #getInstance(name: string): Promise<CliCommandShape> {
|
|
62
|
+
if (!this.hasCommand(name)) {
|
|
63
|
+
throw new CliUnknownCommandError(name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (this.#instanceMapping.has(name)) {
|
|
67
|
+
return this.#instanceMapping.get(name)!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const found = this.#commandMapping.get(name)!;
|
|
71
|
+
const values = Object.values(await Runtime.importFrom<Record<string, Class>>(found));
|
|
72
|
+
const filtered = values
|
|
73
|
+
.filter((v): v is Class => typeof v === 'function')
|
|
74
|
+
.reduce<Class[]>((acc, v) => {
|
|
75
|
+
const parent = getParentClass(v);
|
|
76
|
+
if (parent && !acc.includes(parent)) {
|
|
77
|
+
acc.push(parent);
|
|
78
|
+
}
|
|
79
|
+
acc.push(v);
|
|
80
|
+
return acc;
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const uninitialized = filtered
|
|
84
|
+
.filter(v => !this.store.finalized(v));
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// Initialize any uninitialized commands
|
|
88
|
+
if (uninitialized.length) {
|
|
89
|
+
// Ensure processed
|
|
90
|
+
Registry.process(uninitialized.map(v => ({ type: 'added', curr: v })));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const v of values) {
|
|
94
|
+
const cfg = this.store.get(v);
|
|
95
|
+
if (!cfg) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const result = cfg.getInstance();
|
|
99
|
+
if (result.isActive !== undefined && !result.isActive()) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
this.#instanceMapping.set(name, result);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
throw new CliUnknownCommandError(name);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
hasCommand(name: string): boolean {
|
|
109
|
+
return this.#commandMapping.has(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async load(names?: string[]): Promise<CliCommandLoadResult[]> {
|
|
113
|
+
const keys = names ?? [...this.#commandMapping.keys()];
|
|
114
|
+
|
|
115
|
+
const list = await Promise.all(keys.map(async x => {
|
|
116
|
+
const instance = await this.#getInstance(x);
|
|
117
|
+
const config = this.store.get(getClass(instance)).get();
|
|
118
|
+
const schema = SchemaRegistryIndex.getConfig(getClass(instance));
|
|
119
|
+
return { command: x, instance, config, schema };
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
return list.sort((a, b) => a.command.localeCompare(b.command));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Class, describeFunction } from '@travetto/runtime';
|
|
2
|
+
import { SchemaInputConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
3
|
+
|
|
4
|
+
import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CLI Command argument/flag shape
|
|
8
|
+
*/
|
|
9
|
+
export type CliCommandInput<K extends string = string> = {
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
type: 'string' | 'file' | 'number' | 'boolean' | 'date' | 'regex' | 'module';
|
|
13
|
+
fileExtensions?: string[];
|
|
14
|
+
choices?: unknown[];
|
|
15
|
+
required?: boolean;
|
|
16
|
+
array?: boolean;
|
|
17
|
+
default?: unknown;
|
|
18
|
+
flagNames?: K[];
|
|
19
|
+
envVars?: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* CLI Command schema shape
|
|
24
|
+
*/
|
|
25
|
+
export interface CliCommandSchema<K extends string = string> {
|
|
26
|
+
name: string;
|
|
27
|
+
module: string;
|
|
28
|
+
commandModule: string;
|
|
29
|
+
runTarget?: boolean;
|
|
30
|
+
description?: string;
|
|
31
|
+
args: CliCommandInput[];
|
|
32
|
+
flags: CliCommandInput<K>[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class CliSchemaExportUtil {
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the base type for a CLI command input
|
|
39
|
+
*/
|
|
40
|
+
static baseInputType(x: SchemaInputConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
|
|
41
|
+
switch (x.type) {
|
|
42
|
+
case Date: return { type: 'date' };
|
|
43
|
+
case Boolean: return { type: 'boolean' };
|
|
44
|
+
case Number: return { type: 'number' };
|
|
45
|
+
case RegExp: return { type: 'regex' };
|
|
46
|
+
case String: {
|
|
47
|
+
switch (true) {
|
|
48
|
+
case x.specifiers?.includes('module'): return { type: 'module' };
|
|
49
|
+
case x.specifiers?.includes('file'): return {
|
|
50
|
+
type: 'file',
|
|
51
|
+
fileExtensions: x.specifiers?.map(s => s.split('ext:')[1]).filter(s => !!s)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { type: 'string' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Process input configuration for CLI commands
|
|
61
|
+
*/
|
|
62
|
+
static processInput(x: SchemaInputConfig): CliCommandInput {
|
|
63
|
+
return {
|
|
64
|
+
...this.baseInputType(x),
|
|
65
|
+
...(('name' in x && typeof x.name === 'string') ? { name: x.name } : { name: '' }),
|
|
66
|
+
description: x.description,
|
|
67
|
+
array: x.array,
|
|
68
|
+
required: x.required?.active !== false,
|
|
69
|
+
choices: x.enum?.values,
|
|
70
|
+
default: Array.isArray(x.default) ? x.default.slice(0) : x.default,
|
|
71
|
+
flagNames: (x.aliases ?? []).slice(0).filter(v => !v.startsWith('env.')),
|
|
72
|
+
envVars: (x.aliases ?? []).slice(0).filter(v => v.startsWith('env.')).map(v => v.replace('env.', ''))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static exportSchema(cls: Class): CliCommandSchema {
|
|
77
|
+
const schema = SchemaRegistryIndex.getConfig(cls);
|
|
78
|
+
const config = CliCommandRegistryIndex.get(cls);
|
|
79
|
+
const processed = Object.values(schema.fields).map(v => this.processInput(v));
|
|
80
|
+
return {
|
|
81
|
+
name: config.name,
|
|
82
|
+
module: describeFunction(config.cls).module,
|
|
83
|
+
description: schema.description,
|
|
84
|
+
flags: processed.filter(v => v.flagNames && v.flagNames.length > 0),
|
|
85
|
+
args: processed.filter(v => !v.flagNames || v.flagNames.length === 0),
|
|
86
|
+
runTarget: config.runTarget ?? false,
|
|
87
|
+
commandModule: describeFunction(cls).module,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -1,120 +1,27 @@
|
|
|
1
|
-
import { castKey, castTo,
|
|
2
|
-
import { BindUtil,
|
|
1
|
+
import { castKey, castTo, getClass } from '@travetto/runtime';
|
|
2
|
+
import { BindUtil, SchemaRegistryIndex, SchemaValidator, ValidationResultError } from '@travetto/schema';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { ParsedState, CliCommandInput, CliCommandSchema, CliCommandShape } from './types.ts';
|
|
4
|
+
import { ParsedState, CliCommandShape, CliValidationError } from './types.ts';
|
|
6
5
|
import { CliValidationResultError } from './error.ts';
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
}
|
|
7
|
+
const getSource = (source: string | undefined, def: CliValidationError['source']): CliValidationError['source'] => {
|
|
8
|
+
switch (source) {
|
|
9
|
+
case 'custom':
|
|
10
|
+
case 'arg':
|
|
11
|
+
case 'flag': return source;
|
|
12
|
+
case undefined: return def;
|
|
13
|
+
default: return 'custom';
|
|
28
14
|
}
|
|
29
|
-
|
|
30
|
-
}
|
|
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
|
-
});
|
|
15
|
+
};
|
|
43
16
|
|
|
44
17
|
/**
|
|
45
18
|
* Allows binding describing/binding inputs for commands
|
|
46
19
|
*/
|
|
47
20
|
export class CliCommandSchemaUtil {
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get schema for a given command
|
|
51
|
-
*/
|
|
52
|
-
static async getSchema(src: Class | CliCommandShape): Promise<CliCommandSchema> {
|
|
53
|
-
const cls = 'main' in src ? CliCommandRegistry.getClass(src) : src;
|
|
54
|
-
|
|
55
|
-
// Ensure finalized
|
|
56
|
-
const parent = SchemaRegistry.getParentClass(cls);
|
|
57
|
-
if (parent?.Ⲑid) {
|
|
58
|
-
SchemaRegistry.onInstall(parent, { type: 'added', curr: parent });
|
|
59
|
-
}
|
|
60
|
-
SchemaRegistry.onInstall(cls, { type: 'added', curr: cls });
|
|
61
|
-
|
|
62
|
-
const schema = await SchemaRegistry.getViewSchema(cls);
|
|
63
|
-
const flags = Object.values(schema.schema).map(fieldToInput);
|
|
64
|
-
|
|
65
|
-
// Add help command
|
|
66
|
-
flags.push({ name: 'help', flagNames: ['h'], description: 'display help for command', type: 'boolean' });
|
|
67
|
-
|
|
68
|
-
const method = SchemaRegistry.getMethodSchema(cls, 'main').map(fieldToInput);
|
|
69
|
-
|
|
70
|
-
const used = new Set(flags
|
|
71
|
-
.flatMap(f => f.flagNames ?? [])
|
|
72
|
-
.filter(x => SHORT_FLAG.test(x) || x.replaceAll('-', '').length < 3)
|
|
73
|
-
.map(x => x.replace(/^-+/, ''))
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
for (const flag of flags) {
|
|
77
|
-
let short = (flag.flagNames ?? []).find(x => SHORT_FLAG.test(x) || x.replaceAll('-', '').length < 3)?.replace(/^-+/, '');
|
|
78
|
-
const long = (flag.flagNames ?? []).find(x => LONG_FLAG.test(x) || x.replaceAll('-', '').length > 2)?.replace(/^-+/, '') ??
|
|
79
|
-
flag.name.replace(/([a-z])([A-Z])/g, (_, l, r: string) => `${l}-${r.toLowerCase()}`);
|
|
80
|
-
const aliases: string[] = flag.flagNames = [];
|
|
81
|
-
|
|
82
|
-
if (short === undefined) {
|
|
83
|
-
if (!(isBoolFlag(flag) && flag.default === true)) {
|
|
84
|
-
short = flag.name.charAt(0);
|
|
85
|
-
if (!used.has(short)) {
|
|
86
|
-
aliases.push(`-${short}`);
|
|
87
|
-
used.add(short);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
aliases.push(`-${short}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
aliases.push(`--${long}`);
|
|
95
|
-
|
|
96
|
-
if (isBoolFlag(flag)) {
|
|
97
|
-
aliases.push(`--no-${long}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const fullSchema = SchemaRegistry.get(cls);
|
|
102
|
-
const { cls: _cls, preMain: _preMain, ...meta } = CliCommandRegistry.getByClass(cls)!;
|
|
103
|
-
const cfg: CliCommandSchema = {
|
|
104
|
-
...meta,
|
|
105
|
-
args: method,
|
|
106
|
-
flags,
|
|
107
|
-
title: fullSchema.title ?? cls.name,
|
|
108
|
-
description: fullSchema.description ?? ''
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
return cfg;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
21
|
/**
|
|
115
22
|
* Bind parsed inputs to command
|
|
116
23
|
*/
|
|
117
|
-
static
|
|
24
|
+
static bindInput<T extends CliCommandShape>(cmd: T, state: ParsedState): unknown[] {
|
|
118
25
|
const template: Partial<T> = {};
|
|
119
26
|
const bound: unknown[] = [];
|
|
120
27
|
|
|
@@ -140,7 +47,7 @@ export class CliCommandSchemaUtil {
|
|
|
140
47
|
}
|
|
141
48
|
}
|
|
142
49
|
|
|
143
|
-
const cls =
|
|
50
|
+
const cls = getClass(cmd);
|
|
144
51
|
BindUtil.bindSchemaToObject(cls, cmd, template);
|
|
145
52
|
return BindUtil.coerceMethodParams(cls, 'main', bound);
|
|
146
53
|
}
|
|
@@ -149,8 +56,8 @@ export class CliCommandSchemaUtil {
|
|
|
149
56
|
* Validate command shape with the given arguments
|
|
150
57
|
*/
|
|
151
58
|
static async validate(cmd: CliCommandShape, args: unknown[]): Promise<typeof cmd> {
|
|
152
|
-
const cls =
|
|
153
|
-
const paramNames =
|
|
59
|
+
const cls = getClass(cmd);
|
|
60
|
+
const paramNames = SchemaRegistryIndex.get(cls).getMethod('main').parameters.map(x => x.name!);
|
|
154
61
|
|
|
155
62
|
const validators = [
|
|
156
63
|
(): Promise<void> => SchemaValidator.validate(cls, cmd).then(() => { }),
|
|
@@ -169,7 +76,7 @@ export class CliCommandSchemaUtil {
|
|
|
169
76
|
if (!(err instanceof CliValidationResultError) && !(err instanceof ValidationResultError)) {
|
|
170
77
|
throw err;
|
|
171
78
|
}
|
|
172
|
-
return err.details.errors.map(v => ({ source: SOURCES[i]
|
|
79
|
+
return err.details.errors.map(v => ({ ...v, source: getSource(v.source, SOURCES[i]) }));
|
|
173
80
|
}));
|
|
174
81
|
|
|
175
82
|
const errors = (await Promise.all(results)).flatMap(x => (x ?? []));
|
package/src/types.ts
CHANGED
|
@@ -6,18 +6,6 @@ type ParsedArg = { type: 'arg', input: string, array?: boolean, index: number };
|
|
|
6
6
|
type ParsedUnknown = { type: 'unknown', input: string };
|
|
7
7
|
type ParsedInput = ParsedUnknown | ParsedFlag | ParsedArg;
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Command configuration
|
|
11
|
-
*/
|
|
12
|
-
export type CliCommandConfig = {
|
|
13
|
-
name: string;
|
|
14
|
-
commandModule: string;
|
|
15
|
-
runTarget?: boolean;
|
|
16
|
-
cls: Class<CliCommandShape>;
|
|
17
|
-
hidden?: boolean;
|
|
18
|
-
preMain?: (cmd: CliCommandShape) => void | Promise<void>;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
9
|
export type ParsedState = {
|
|
22
10
|
inputs: string[];
|
|
23
11
|
all: ParsedInput[];
|
|
@@ -45,7 +33,6 @@ export interface CliValidationError {
|
|
|
45
33
|
* @concrete
|
|
46
34
|
*/
|
|
47
35
|
export interface CliCommandShape<T extends unknown[] = unknown[]> {
|
|
48
|
-
|
|
49
36
|
/**
|
|
50
37
|
* Parsed state
|
|
51
38
|
*/
|
|
@@ -110,31 +97,12 @@ export type CliCommandShapeFields = {
|
|
|
110
97
|
module?: string;
|
|
111
98
|
};
|
|
112
99
|
|
|
113
|
-
/**
|
|
114
|
-
* CLI Command argument/flag shape
|
|
115
|
-
*/
|
|
116
|
-
export type CliCommandInput<K extends string = string> = {
|
|
117
|
-
name: string;
|
|
118
|
-
description?: string;
|
|
119
|
-
type: 'string' | 'file' | 'number' | 'boolean' | 'date' | 'regex' | 'module';
|
|
120
|
-
fileExtensions?: string[];
|
|
121
|
-
choices?: unknown[];
|
|
122
|
-
required?: boolean;
|
|
123
|
-
array?: boolean;
|
|
124
|
-
default?: unknown;
|
|
125
|
-
flagNames?: K[];
|
|
126
|
-
envVars?: string[];
|
|
127
|
-
};
|
|
128
|
-
|
|
129
100
|
/**
|
|
130
101
|
* CLI Command schema shape
|
|
131
102
|
*/
|
|
132
|
-
export interface
|
|
103
|
+
export interface CliCommandConfig {
|
|
104
|
+
cls: Class<CliCommandShape>;
|
|
133
105
|
name: string;
|
|
134
|
-
title: string;
|
|
135
|
-
commandModule: string;
|
|
136
106
|
runTarget?: boolean;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
flags: CliCommandInput<K>[];
|
|
140
|
-
}
|
|
107
|
+
preMain?: (cmd: CliCommandShape) => void | Promise<void>;
|
|
108
|
+
}
|