@travetto/cli 6.0.1 → 7.0.0-rc.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/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 +8 -111
- 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,17 @@
|
|
|
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 } from './types.ts';
|
|
6
5
|
import { CliValidationResultError } from './error.ts';
|
|
7
6
|
|
|
8
|
-
const LONG_FLAG = /^--[a-z][^= ]+/i;
|
|
9
|
-
const SHORT_FLAG = /^-[a-z]/i;
|
|
10
|
-
|
|
11
|
-
const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
|
|
12
|
-
|
|
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' };
|
|
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
|
-
});
|
|
43
|
-
|
|
44
7
|
/**
|
|
45
8
|
* Allows binding describing/binding inputs for commands
|
|
46
9
|
*/
|
|
47
10
|
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
11
|
/**
|
|
115
12
|
* Bind parsed inputs to command
|
|
116
13
|
*/
|
|
117
|
-
static
|
|
14
|
+
static bindInput<T extends CliCommandShape>(cmd: T, state: ParsedState): unknown[] {
|
|
118
15
|
const template: Partial<T> = {};
|
|
119
16
|
const bound: unknown[] = [];
|
|
120
17
|
|
|
@@ -140,7 +37,7 @@ export class CliCommandSchemaUtil {
|
|
|
140
37
|
}
|
|
141
38
|
}
|
|
142
39
|
|
|
143
|
-
const cls =
|
|
40
|
+
const cls = getClass(cmd);
|
|
144
41
|
BindUtil.bindSchemaToObject(cls, cmd, template);
|
|
145
42
|
return BindUtil.coerceMethodParams(cls, 'main', bound);
|
|
146
43
|
}
|
|
@@ -149,8 +46,8 @@ export class CliCommandSchemaUtil {
|
|
|
149
46
|
* Validate command shape with the given arguments
|
|
150
47
|
*/
|
|
151
48
|
static async validate(cmd: CliCommandShape, args: unknown[]): Promise<typeof cmd> {
|
|
152
|
-
const cls =
|
|
153
|
-
const paramNames =
|
|
49
|
+
const cls = getClass(cmd);
|
|
50
|
+
const paramNames = SchemaRegistryIndex.getMethodConfig(cls, 'main').parameters.map(x => x.name!);
|
|
154
51
|
|
|
155
52
|
const validators = [
|
|
156
53
|
(): Promise<void> => SchemaValidator.validate(cls, cmd).then(() => { }),
|
|
@@ -163,7 +60,7 @@ export class CliCommandSchemaUtil {
|
|
|
163
60
|
},
|
|
164
61
|
];
|
|
165
62
|
|
|
166
|
-
const SOURCES = ['flag', 'arg', 'custom']
|
|
63
|
+
const SOURCES = ['flag', 'arg', 'custom'];
|
|
167
64
|
|
|
168
65
|
const results = validators.map((x, i) => x().catch(err => {
|
|
169
66
|
if (!(err instanceof CliValidationResultError) && !(err instanceof ValidationResultError)) {
|
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
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
import { Env, ExecUtil, Runtime } from '@travetto/runtime';
|
|
3
|
+
import { describeFunction, Env, ExecUtil, Runtime } from '@travetto/runtime';
|
|
4
4
|
|
|
5
5
|
import { CliCommandShape, CliCommandShapeFields } from './types.ts';
|
|
6
6
|
|
|
@@ -61,8 +61,10 @@ export class CliUtil {
|
|
|
61
61
|
const req = {
|
|
62
62
|
type: `@travetto/cli:${action}`,
|
|
63
63
|
data: {
|
|
64
|
-
name: cmd._cfg!.name,
|
|
65
|
-
|
|
64
|
+
name: cmd._cfg!.name,
|
|
65
|
+
env,
|
|
66
|
+
// TODO: Is this needed?
|
|
67
|
+
commandModule: describeFunction(cmd.constructor).module,
|
|
66
68
|
module: Runtime.main.name,
|
|
67
69
|
args: process.argv.slice(3),
|
|
68
70
|
}
|