@travetto/cli 6.0.0 → 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 +40 -46
- package/__index__.ts +4 -2
- package/package.json +3 -3
- package/src/error.ts +1 -1
- package/src/execute.ts +4 -4
- package/src/help.ts +54 -49
- 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 -116
- package/src/service.ts +4 -4
- 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 -153
- package/src/registry.ts +0 -96
- package/support/transformer.cli.ts +0 -53
|
@@ -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,125 +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
|
-
static #schemas = new Map<Class, CliCommandSchema>();
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get schema for a given command
|
|
53
|
-
*/
|
|
54
|
-
static async getSchema(src: Class | CliCommandShape): Promise<CliCommandSchema> {
|
|
55
|
-
const cls = 'main' in src ? CliCommandRegistry.getClass(src) : src;
|
|
56
|
-
if (this.#schemas.has(cls)) {
|
|
57
|
-
return this.#schemas.get(cls)!;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Ensure finalized
|
|
61
|
-
const parent = SchemaRegistry.getParentClass(cls);
|
|
62
|
-
if (parent?.Ⲑid) {
|
|
63
|
-
SchemaRegistry.onInstall(parent, { type: 'added', curr: parent });
|
|
64
|
-
}
|
|
65
|
-
SchemaRegistry.onInstall(cls, { type: 'added', curr: cls });
|
|
66
|
-
|
|
67
|
-
const schema = await SchemaRegistry.getViewSchema(cls);
|
|
68
|
-
const flags = Object.values(schema.schema).map(fieldToInput);
|
|
69
|
-
|
|
70
|
-
// Add help command
|
|
71
|
-
flags.push({ name: 'help', flagNames: ['h'], description: 'display help for command', type: 'boolean' });
|
|
72
|
-
|
|
73
|
-
const method = SchemaRegistry.getMethodSchema(cls, 'main').map(fieldToInput);
|
|
74
|
-
|
|
75
|
-
const used = new Set(flags
|
|
76
|
-
.flatMap(f => f.flagNames ?? [])
|
|
77
|
-
.filter(x => SHORT_FLAG.test(x) || x.replaceAll('-', '').length < 3)
|
|
78
|
-
.map(x => x.replace(/^-+/, ''))
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
for (const flag of flags) {
|
|
82
|
-
let short = (flag.flagNames ?? []).find(x => SHORT_FLAG.test(x) || x.replaceAll('-', '').length < 3)?.replace(/^-+/, '');
|
|
83
|
-
const long = (flag.flagNames ?? []).find(x => LONG_FLAG.test(x) || x.replaceAll('-', '').length > 2)?.replace(/^-+/, '') ??
|
|
84
|
-
flag.name.replace(/([a-z])([A-Z])/g, (_, l, r: string) => `${l}-${r.toLowerCase()}`);
|
|
85
|
-
const aliases: string[] = flag.flagNames = [];
|
|
86
|
-
|
|
87
|
-
if (short === undefined) {
|
|
88
|
-
if (!(isBoolFlag(flag) && flag.default === true)) {
|
|
89
|
-
short = flag.name.charAt(0);
|
|
90
|
-
if (!used.has(short)) {
|
|
91
|
-
aliases.push(`-${short}`);
|
|
92
|
-
used.add(short);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
aliases.push(`-${short}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
aliases.push(`--${long}`);
|
|
100
|
-
|
|
101
|
-
if (isBoolFlag(flag)) {
|
|
102
|
-
aliases.push(`--no-${long}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const fullSchema = SchemaRegistry.get(cls);
|
|
107
|
-
const { cls: _cls, preMain: _preMain, ...meta } = CliCommandRegistry.getByClass(cls)!;
|
|
108
|
-
const cfg: CliCommandSchema = {
|
|
109
|
-
...meta,
|
|
110
|
-
args: method,
|
|
111
|
-
flags,
|
|
112
|
-
title: fullSchema.title ?? cls.name,
|
|
113
|
-
description: fullSchema.description ?? ''
|
|
114
|
-
};
|
|
115
|
-
this.#schemas.set(cls, cfg);
|
|
116
|
-
return cfg;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
11
|
/**
|
|
120
12
|
* Bind parsed inputs to command
|
|
121
13
|
*/
|
|
122
|
-
static
|
|
14
|
+
static bindInput<T extends CliCommandShape>(cmd: T, state: ParsedState): unknown[] {
|
|
123
15
|
const template: Partial<T> = {};
|
|
124
16
|
const bound: unknown[] = [];
|
|
125
17
|
|
|
@@ -145,7 +37,7 @@ export class CliCommandSchemaUtil {
|
|
|
145
37
|
}
|
|
146
38
|
}
|
|
147
39
|
|
|
148
|
-
const cls =
|
|
40
|
+
const cls = getClass(cmd);
|
|
149
41
|
BindUtil.bindSchemaToObject(cls, cmd, template);
|
|
150
42
|
return BindUtil.coerceMethodParams(cls, 'main', bound);
|
|
151
43
|
}
|
|
@@ -154,8 +46,8 @@ export class CliCommandSchemaUtil {
|
|
|
154
46
|
* Validate command shape with the given arguments
|
|
155
47
|
*/
|
|
156
48
|
static async validate(cmd: CliCommandShape, args: unknown[]): Promise<typeof cmd> {
|
|
157
|
-
const cls =
|
|
158
|
-
const paramNames =
|
|
49
|
+
const cls = getClass(cmd);
|
|
50
|
+
const paramNames = SchemaRegistryIndex.getMethodConfig(cls, 'main').parameters.map(x => x.name!);
|
|
159
51
|
|
|
160
52
|
const validators = [
|
|
161
53
|
(): Promise<void> => SchemaValidator.validate(cls, cmd).then(() => { }),
|
|
@@ -168,7 +60,7 @@ export class CliCommandSchemaUtil {
|
|
|
168
60
|
},
|
|
169
61
|
];
|
|
170
62
|
|
|
171
|
-
const SOURCES = ['flag', 'arg', 'custom']
|
|
63
|
+
const SOURCES = ['flag', 'arg', 'custom'];
|
|
172
64
|
|
|
173
65
|
const results = validators.map((x, i) => x().catch(err => {
|
|
174
66
|
if (!(err instanceof CliValidationResultError) && !(err instanceof ValidationResultError)) {
|
package/src/service.ts
CHANGED
|
@@ -68,7 +68,7 @@ export class ServiceRunner {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async #hasImage(): Promise<boolean> {
|
|
71
|
-
const result = await ExecUtil.getResult(spawn('docker', ['image', 'inspect', this.svc.image]
|
|
71
|
+
const result = await ExecUtil.getResult(spawn('docker', ['image', 'inspect', this.svc.image]), { catch: true });
|
|
72
72
|
return result.valid;
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -96,15 +96,15 @@ export class ServiceRunner {
|
|
|
96
96
|
await fs.mkdir(item, { recursive: true });
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
return (await ExecUtil.getResult(spawn('docker', args, {
|
|
99
|
+
return (await ExecUtil.getResult(spawn('docker', args, { stdio: [0, 'pipe', 2] }))).stdout;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
async #getContainerId(): Promise<string | undefined> {
|
|
103
|
-
return (await ExecUtil.getResult(spawn('docker', ['ps', '-q', '--filter', `label=trv-${this.svc.name}`]
|
|
103
|
+
return (await ExecUtil.getResult(spawn('docker', ['ps', '-q', '--filter', `label=trv-${this.svc.name}`]))).stdout.trim();
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
async #killContainer(cid: string): Promise<void> {
|
|
107
|
-
await ExecUtil.getResult(spawn('docker', ['kill', cid]
|
|
107
|
+
await ExecUtil.getResult(spawn('docker', ['kill', cid]));
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
async * action(op: ServiceAction): AsyncIterable<['success' | 'failure' | 'message', string]> {
|