@travetto/cli 7.0.0-rc.1 → 7.0.0-rc.2
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 +3 -3
- package/bin/trv.js +1 -1
- package/package.json +3 -3
- package/src/error.ts +3 -3
- package/src/execute.ts +11 -11
- package/src/help.ts +21 -21
- package/src/module.ts +21 -21
- package/src/parse.ts +35 -35
- package/src/registry/decorator.ts +12 -13
- package/src/registry/registry-adapter.ts +10 -10
- package/src/registry/registry-index.ts +22 -22
- package/src/schema-export.ts +18 -18
- package/src/schema.ts +9 -9
- package/src/scm.ts +10 -10
- package/src/service.ts +42 -42
- package/src/util.ts +9 -7
- package/support/cli.cli_schema.ts +2 -2
- package/support/cli.main.ts +2 -2
- package/support/cli.service.ts +18 -18
package/README.md
CHANGED
|
@@ -133,9 +133,9 @@ HELLO
|
|
|
133
133
|
|
|
134
134
|
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L85) supports the following data types for flags:
|
|
135
135
|
* Boolean values
|
|
136
|
-
* Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
137
|
-
* String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
138
|
-
* Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
136
|
+
* Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L166), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L173), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L159), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L93) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L104) decorators help provide additional validation.
|
|
137
|
+
* String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L93), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L104), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L84) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L58) provide additional constraints
|
|
138
|
+
* Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L93) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L104) decorators help provide additional validation.
|
|
139
139
|
* String lists. Same as String, but allowing multiple values.
|
|
140
140
|
* Numeric lists. Same as Number, but allowing multiple values.
|
|
141
141
|
|
package/bin/trv.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/cli",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.2",
|
|
4
4
|
"description": "CLI infrastructure for Travetto framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"directory": "module/cli"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/schema": "^7.0.0-rc.
|
|
32
|
-
"@travetto/terminal": "^7.0.0-rc.
|
|
31
|
+
"@travetto/schema": "^7.0.0-rc.2",
|
|
32
|
+
"@travetto/terminal": "^7.0.0-rc.2"
|
|
33
33
|
},
|
|
34
34
|
"travetto": {
|
|
35
35
|
"displayName": "Command Line Interface",
|
package/src/error.ts
CHANGED
|
@@ -21,9 +21,9 @@ const COMMAND_PACKAGE = [
|
|
|
21
21
|
export class CliUnknownCommandError extends Error {
|
|
22
22
|
|
|
23
23
|
#getMissingCommandHelp(cmd: string): string | undefined {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
const [, pkg, prod] =
|
|
24
|
+
const matchedConfig = COMMAND_PACKAGE.find(([re]) => re.test(cmd));
|
|
25
|
+
if (matchedConfig) {
|
|
26
|
+
const [, pkg, prod] = matchedConfig;
|
|
27
27
|
const install = PackageUtil.getInstallCommand(Runtime, `@travetto/${pkg}`, prod);
|
|
28
28
|
return cliTpl`
|
|
29
29
|
${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: cmd }} please run:\n
|
package/src/execute.ts
CHANGED
|
@@ -13,20 +13,20 @@ import { CliCommandShape } from './types.ts';
|
|
|
13
13
|
export class ExecutionManager {
|
|
14
14
|
|
|
15
15
|
/** Error handler */
|
|
16
|
-
static async #onError(
|
|
16
|
+
static async #onError(error: unknown): Promise<void> {
|
|
17
17
|
process.exitCode ??= 1;
|
|
18
|
-
if (
|
|
19
|
-
console.error!(await HelpUtil.renderValidationError(
|
|
20
|
-
console.error!(await HelpUtil.renderCommandHelp(
|
|
21
|
-
} else if (
|
|
22
|
-
if (
|
|
23
|
-
console.error!(
|
|
18
|
+
if (error instanceof CliValidationResultError) {
|
|
19
|
+
console.error!(await HelpUtil.renderValidationError(error));
|
|
20
|
+
console.error!(await HelpUtil.renderCommandHelp(error.command));
|
|
21
|
+
} else if (error instanceof CliUnknownCommandError) {
|
|
22
|
+
if (error.help) {
|
|
23
|
+
console.error!(error.help);
|
|
24
24
|
} else {
|
|
25
|
-
console.error!(
|
|
25
|
+
console.error!(error.defaultMessage, '\n');
|
|
26
26
|
console.error!(await HelpUtil.renderAllHelp(''));
|
|
27
27
|
}
|
|
28
28
|
} else {
|
|
29
|
-
console.error!(
|
|
29
|
+
console.error!(error);
|
|
30
30
|
}
|
|
31
31
|
console.error!();
|
|
32
32
|
}
|
|
@@ -72,8 +72,8 @@ export class ExecutionManager {
|
|
|
72
72
|
} else {
|
|
73
73
|
await this.#runCommand(cmd, args);
|
|
74
74
|
}
|
|
75
|
-
} catch (
|
|
76
|
-
await this.#onError(
|
|
75
|
+
} catch (error) {
|
|
76
|
+
await this.#onError(error);
|
|
77
77
|
} finally {
|
|
78
78
|
await ShutdownManager.gracefulShutdown('@travetto/cli:execute');
|
|
79
79
|
}
|
package/src/help.ts
CHANGED
|
@@ -14,8 +14,8 @@ const validationSourceMap: Record<string, string> = {
|
|
|
14
14
|
flag: 'Flag'
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
const ifDefined = <T>(
|
|
18
|
-
(
|
|
17
|
+
const ifDefined = <T>(value: T | null | '' | undefined): T | undefined =>
|
|
18
|
+
(value === null || value === '' || value === undefined) ? undefined : value;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Utilities for showing help
|
|
@@ -47,11 +47,11 @@ export class HelpUtil {
|
|
|
47
47
|
|
|
48
48
|
for (const field of Object.values(schema.fields)) {
|
|
49
49
|
const key = castKey<CliCommandShape>(field.name);
|
|
50
|
-
const
|
|
50
|
+
const defaultValue = ifDefined(command[key]) ?? ifDefined(field.default);
|
|
51
51
|
const aliases = (field.aliases ?? [])
|
|
52
|
-
.filter(
|
|
53
|
-
.filter(
|
|
54
|
-
(field.type !== Boolean) || ((
|
|
52
|
+
.filter(flag => flag.startsWith('-'))
|
|
53
|
+
.filter(flag =>
|
|
54
|
+
(field.type !== Boolean) || ((defaultValue !== true || field.name === 'help') ? !flag.startsWith('--no-') : flag.startsWith('--'))
|
|
55
55
|
);
|
|
56
56
|
let type: string | undefined;
|
|
57
57
|
|
|
@@ -69,14 +69,14 @@ export class HelpUtil {
|
|
|
69
69
|
params.push(param.join(' '));
|
|
70
70
|
const desc = [cliTpl`${{ title: field.description }}`];
|
|
71
71
|
|
|
72
|
-
if (key !== 'help' &&
|
|
73
|
-
desc.push(cliTpl`(default: ${{ input: JSON.stringify(
|
|
72
|
+
if (key !== 'help' && defaultValue !== undefined) {
|
|
73
|
+
desc.push(cliTpl`(default: ${{ input: JSON.stringify(defaultValue) }})`);
|
|
74
74
|
}
|
|
75
75
|
descriptions.push(desc.join(' '));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const paramWidths = params.map(
|
|
79
|
-
const descWidths = descriptions.map(
|
|
78
|
+
const paramWidths = params.map(item => util.stripVTControlCharacters(item).length);
|
|
79
|
+
const descWidths = descriptions.map(item => util.stripVTControlCharacters(item).length);
|
|
80
80
|
|
|
81
81
|
const paramWidth = Math.max(...paramWidths);
|
|
82
82
|
const descWidth = Math.max(...descWidths);
|
|
@@ -95,7 +95,7 @@ export class HelpUtil {
|
|
|
95
95
|
),
|
|
96
96
|
'',
|
|
97
97
|
...helpText
|
|
98
|
-
].map(
|
|
98
|
+
].map(line => line.trimEnd()).join('\n');
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
@@ -113,11 +113,11 @@ export class HelpUtil {
|
|
|
113
113
|
if (schema && !schema.private) {
|
|
114
114
|
rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: schema.description || '' }}`);
|
|
115
115
|
}
|
|
116
|
-
} catch (
|
|
117
|
-
if (
|
|
118
|
-
rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ failure:
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof Error) {
|
|
118
|
+
rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ failure: error.message.split(/\n/)[0] }}`);
|
|
119
119
|
} else {
|
|
120
|
-
throw
|
|
120
|
+
throw error;
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -126,20 +126,20 @@ export class HelpUtil {
|
|
|
126
126
|
|
|
127
127
|
lines.unshift(title ? cliTpl`${{ title }}` : cliTpl`${{ title: 'Usage:' }} ${{ param: '[options]' }} ${{ param: '[command]' }}`, '');
|
|
128
128
|
|
|
129
|
-
return lines.map(
|
|
129
|
+
return lines.map(line => line.trimEnd()).join('\n');
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Render validation error to a string
|
|
134
134
|
*/
|
|
135
|
-
static renderValidationError(
|
|
135
|
+
static renderValidationError(validationError: CliValidationResultError): string {
|
|
136
136
|
return [
|
|
137
137
|
cliTpl`${{ failure: 'Execution failed' }}:`,
|
|
138
|
-
...
|
|
139
|
-
if (
|
|
140
|
-
return cliTpl` * ${{ identifier: validationSourceMap[
|
|
138
|
+
...validationError.details.errors.map(error => {
|
|
139
|
+
if (error.source && error.source in validationSourceMap) {
|
|
140
|
+
return cliTpl` * ${{ identifier: validationSourceMap[error.source] }} ${{ subtitle: error.message }}`;
|
|
141
141
|
}
|
|
142
|
-
return cliTpl` * ${{ failure:
|
|
142
|
+
return cliTpl` * ${{ failure: error.message }}`;
|
|
143
143
|
}),
|
|
144
144
|
'',
|
|
145
145
|
].join('\n');
|
package/src/module.ts
CHANGED
|
@@ -47,41 +47,41 @@ export class CliModuleUtil {
|
|
|
47
47
|
static async findModules(mode: 'all' | 'changed' | 'workspace', fromHash?: string, toHash?: string): Promise<IndexedModule[]> {
|
|
48
48
|
return (mode === 'changed' ?
|
|
49
49
|
await this.findChangedModulesRecursive(fromHash, toHash, true) :
|
|
50
|
-
[...RuntimeIndex.getModuleList(mode)].map(
|
|
51
|
-
).filter(
|
|
50
|
+
[...RuntimeIndex.getModuleList(mode)].map(name => RuntimeIndex.getModule(name)!)
|
|
51
|
+
).filter(mod => mod.sourcePath !== Runtime.workspace.path);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Get module dependency graph, fully collapsed
|
|
56
56
|
*/
|
|
57
|
-
static getDependencyGraph(
|
|
57
|
+
static getDependencyGraph(modules: IndexedModule[]): Record<string, string[]> {
|
|
58
58
|
const childMap: Map<string, ModuleGraphEntry> = new Map();
|
|
59
59
|
const get = (name: string): ModuleGraphEntry =>
|
|
60
60
|
childMap.has(name) ? childMap.get(name)! : childMap.set(name, { children: new Set(), name, active: new Set() }).get(name)!;
|
|
61
61
|
|
|
62
|
-
for (const
|
|
63
|
-
get(
|
|
64
|
-
for (const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
for (const mod of modules) {
|
|
63
|
+
get(mod.name).parents = mod.parents;
|
|
64
|
+
for (const parentModule of mod.parents) {
|
|
65
|
+
const parent = get(parentModule);
|
|
66
|
+
parent.children.add(mod.name); // Store child into parent
|
|
67
|
+
parent.active.add(mod.name);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const output: Record<string, string[]> = {};
|
|
72
72
|
|
|
73
73
|
while (childMap.size > 0) {
|
|
74
|
-
for (const
|
|
75
|
-
output[
|
|
76
|
-
for (const parent of
|
|
74
|
+
for (const item of [...childMap.values()].filter(entry => entry.active.size === 0)) {
|
|
75
|
+
output[item.name] = [...item.children];
|
|
76
|
+
for (const parent of item.parents ?? []) {
|
|
77
77
|
const par = childMap.get(parent)!;
|
|
78
78
|
// Extend children into parents
|
|
79
|
-
for (const
|
|
80
|
-
par.children.add(
|
|
79
|
+
for (const child of item.children) {
|
|
80
|
+
par.children.add(child);
|
|
81
81
|
}
|
|
82
|
-
par.active.delete(
|
|
82
|
+
par.active.delete(item.name);
|
|
83
83
|
}
|
|
84
|
-
childMap.delete(
|
|
84
|
+
childMap.delete(item.name);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
return output;
|
|
@@ -106,16 +106,16 @@ export class CliModuleUtil {
|
|
|
106
106
|
if (config.since) {
|
|
107
107
|
try {
|
|
108
108
|
const files = await CliScmUtil.findChangedFiles(config.since, 'HEAD');
|
|
109
|
-
return files.filter(
|
|
110
|
-
} catch (
|
|
111
|
-
if (config.logError &&
|
|
112
|
-
console.error(
|
|
109
|
+
return files.filter(file => !file.endsWith('package.json') && !file.endsWith('package-lock.json'));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (config.logError && error instanceof Error) {
|
|
112
|
+
console.error(error.message);
|
|
113
113
|
}
|
|
114
114
|
return [];
|
|
115
115
|
}
|
|
116
116
|
} else {
|
|
117
117
|
const mods = await this.findModules(config.changed ? 'changed' : 'workspace', undefined, 'HEAD');
|
|
118
|
-
return mods.map(
|
|
118
|
+
return mods.map(mod => mod.sourcePath);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
}
|
package/src/parse.ts
CHANGED
|
@@ -16,7 +16,7 @@ const CONFIG_PRE = '+=';
|
|
|
16
16
|
const SPACE = new Set([32, 7, 13, 10]);
|
|
17
17
|
|
|
18
18
|
export const ENV_PREFIX = 'env.';
|
|
19
|
-
export const isBoolFlag = (
|
|
19
|
+
export const isBoolFlag = (value?: SchemaInputConfig): boolean => value?.type === Boolean && !value.array;
|
|
20
20
|
|
|
21
21
|
export type AliasesParseResult = Record<'long' | 'short' | 'raw' | 'env', string[]>;
|
|
22
22
|
|
|
@@ -25,8 +25,8 @@ export type AliasesParseResult = Record<'long' | 'short' | 'raw' | 'env', string
|
|
|
25
25
|
*/
|
|
26
26
|
export class CliParseUtil {
|
|
27
27
|
|
|
28
|
-
static toEnvField(
|
|
29
|
-
return
|
|
28
|
+
static toEnvField(key: string): string {
|
|
29
|
+
return key.startsWith(ENV_PREFIX) ? key : `${ENV_PREFIX}${key}`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
static readToken(text: string, start = 0): { next: number, value?: string } {
|
|
@@ -69,13 +69,13 @@ export class CliParseUtil {
|
|
|
69
69
|
*/
|
|
70
70
|
static getSpecifiedModule(schema: SchemaClassConfig, args: string[]): string | undefined {
|
|
71
71
|
const SEP = args.includes(RAW_SEP) ? args.indexOf(RAW_SEP) : args.length;
|
|
72
|
-
const input = Object.values(schema.fields).find(
|
|
73
|
-
const ENV_KEY = input?.aliases?.filter(
|
|
72
|
+
const input = Object.values(schema.fields).find(config => config.specifiers?.includes('module'));
|
|
73
|
+
const ENV_KEY = input?.aliases?.filter(alias => alias.startsWith(ENV_PREFIX)).map(alias => alias.replace(ENV_PREFIX, ''))[0] ?? '';
|
|
74
74
|
const flags = new Set(input?.aliases ?? []);
|
|
75
|
-
const check = (
|
|
75
|
+
const check = (key?: string, value?: string): string | undefined => flags.has(key!) ? value : undefined;
|
|
76
76
|
return args.reduce(
|
|
77
|
-
(
|
|
78
|
-
(i < SEP ? check(
|
|
77
|
+
(name, value, i, values) =>
|
|
78
|
+
(i < SEP ? check(values[i - 1], value) ?? check(...value.split('=')) : undefined) ?? name,
|
|
79
79
|
process.env[ENV_KEY]
|
|
80
80
|
);
|
|
81
81
|
}
|
|
@@ -88,10 +88,10 @@ export class CliParseUtil {
|
|
|
88
88
|
const overrides = { '@': mod ?? Runtime.main.name };
|
|
89
89
|
|
|
90
90
|
// We have a file
|
|
91
|
-
const
|
|
91
|
+
const relativePath = (key.includes('/') ? key : `@#support/pack.${key}.flags`)
|
|
92
92
|
.replace(/^(@[^#]*)#(.*)$/, (_, imp, rest) => `${Runtime.modulePath(imp, overrides)}/${rest}`);
|
|
93
93
|
|
|
94
|
-
const file = path.resolve(
|
|
94
|
+
const file = path.resolve(relativePath);
|
|
95
95
|
|
|
96
96
|
if (!await fs.stat(file).catch(() => false)) {
|
|
97
97
|
throw new Error(`Missing flag file: ${key}, unable to proceed`);
|
|
@@ -122,10 +122,10 @@ export class CliParseUtil {
|
|
|
122
122
|
const max = out.includes(RAW_SEP) ? out.indexOf(RAW_SEP) : out.length;
|
|
123
123
|
const valid = out.slice(0, max);
|
|
124
124
|
const cmd = valid.length > 0 && !valid[0].startsWith('-') ? valid[0] : undefined;
|
|
125
|
-
const helpIdx = valid.findIndex(
|
|
125
|
+
const helpIdx = valid.findIndex(flag => HELP_FLAG.test(flag));
|
|
126
126
|
const args = out.slice(cmd ? 1 : 0);
|
|
127
|
-
const
|
|
128
|
-
return
|
|
127
|
+
const result = { cmd, args, help: helpIdx >= 0 };
|
|
128
|
+
return result;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
@@ -134,8 +134,8 @@ export class CliParseUtil {
|
|
|
134
134
|
static async expandArgs(schema: SchemaClassConfig, args: string[]): Promise<string[]> {
|
|
135
135
|
const SEP = args.includes(RAW_SEP) ? args.indexOf(RAW_SEP) : args.length;
|
|
136
136
|
const mod = this.getSpecifiedModule(schema, args);
|
|
137
|
-
return (await Promise.all(args.map((
|
|
138
|
-
|
|
137
|
+
return (await Promise.all(args.map((arg, i) =>
|
|
138
|
+
arg.startsWith(CONFIG_PRE) && (i < SEP || SEP < 0) ? this.readFlagFile(arg, mod) : arg))).flat();
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -143,21 +143,21 @@ export class CliParseUtil {
|
|
|
143
143
|
*/
|
|
144
144
|
static async parse(schema: SchemaClassConfig, inputs: string[]): Promise<ParsedState> {
|
|
145
145
|
const flagMap = new Map<string, SchemaFieldConfig>(
|
|
146
|
-
Object.values(schema.fields).flatMap(
|
|
146
|
+
Object.values(schema.fields).flatMap(field => (field.aliases ?? []).map(name => [name, field]))
|
|
147
147
|
);
|
|
148
148
|
|
|
149
149
|
const out: ParsedInput[] = [];
|
|
150
150
|
|
|
151
151
|
// Load env vars to front
|
|
152
152
|
for (const field of Object.values(schema.fields)) {
|
|
153
|
-
for (const envName of (field.aliases ?? []).filter(
|
|
153
|
+
for (const envName of (field.aliases ?? []).filter(alias => alias.startsWith(ENV_PREFIX))) {
|
|
154
154
|
const simple = envName.replace(ENV_PREFIX, '');
|
|
155
155
|
if (simple in process.env) {
|
|
156
156
|
const value: string = process.env[simple]!;
|
|
157
157
|
if (field.array) {
|
|
158
|
-
out.push(...value.split(/\s*,\s*/g).map(
|
|
158
|
+
out.push(...value.split(/\s*,\s*/g).map(item => ({ type: 'flag', fieldName: field.name, input: envName, value: item }) as const));
|
|
159
159
|
} else {
|
|
160
|
-
out.push({ type: 'flag', fieldName: field.name
|
|
160
|
+
out.push({ type: 'flag', fieldName: field.name, input: envName, value });
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -169,13 +169,13 @@ export class CliParseUtil {
|
|
|
169
169
|
const input = inputs[i];
|
|
170
170
|
|
|
171
171
|
if (input === RAW_SEP) { // Raw separator
|
|
172
|
-
out.push(...inputs.slice(i + 1).map((
|
|
172
|
+
out.push(...inputs.slice(i + 1).map((arg, idx) => ({ type: 'unknown', input: arg, index: argIdx + idx }) as const));
|
|
173
173
|
break;
|
|
174
174
|
} else if (LONG_FLAG_WITH_EQ.test(input)) {
|
|
175
|
-
const [
|
|
176
|
-
const field = flagMap.get(
|
|
175
|
+
const [key, ...values] = input.split('=');
|
|
176
|
+
const field = flagMap.get(key);
|
|
177
177
|
if (field) {
|
|
178
|
-
out.push({ type: 'flag', fieldName: field.name
|
|
178
|
+
out.push({ type: 'flag', fieldName: field.name, input: key, value: values.join('=') });
|
|
179
179
|
} else {
|
|
180
180
|
out.push({ type: 'unknown', input });
|
|
181
181
|
}
|
|
@@ -185,7 +185,7 @@ export class CliParseUtil {
|
|
|
185
185
|
out.push({ type: 'unknown', input });
|
|
186
186
|
} else {
|
|
187
187
|
const next = inputs[i + 1];
|
|
188
|
-
const base = { type: 'flag', fieldName: field.name
|
|
188
|
+
const base = { type: 'flag', fieldName: field.name, input, array: field.array } as const;
|
|
189
189
|
if ((next && (VALID_FLAG.test(next) || next === RAW_SEP)) || isBoolFlag(field)) {
|
|
190
190
|
if (isBoolFlag(field)) {
|
|
191
191
|
out.push({ ...base, value: !input.startsWith('--no-') });
|
|
@@ -211,8 +211,8 @@ export class CliParseUtil {
|
|
|
211
211
|
return {
|
|
212
212
|
inputs,
|
|
213
213
|
all: out,
|
|
214
|
-
unknown: out.filter(
|
|
215
|
-
flags: out.filter(
|
|
214
|
+
unknown: out.filter(input => input.type === 'unknown').map(input => input.input),
|
|
215
|
+
flags: out.filter(input => input.type === 'flag')
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -220,19 +220,19 @@ export class CliParseUtil {
|
|
|
220
220
|
* Parse aliases into categories for registration
|
|
221
221
|
*/
|
|
222
222
|
static parseAliases(aliases: string[]): AliasesParseResult {
|
|
223
|
-
return aliases.reduce<AliasesParseResult>((
|
|
224
|
-
if (VALID_FLAG.test(
|
|
225
|
-
if (
|
|
226
|
-
|
|
223
|
+
return aliases.reduce<AliasesParseResult>((result, alias) => {
|
|
224
|
+
if (VALID_FLAG.test(alias)) {
|
|
225
|
+
if (alias.startsWith('--')) {
|
|
226
|
+
result.long.push(alias);
|
|
227
227
|
} else {
|
|
228
|
-
|
|
228
|
+
result.short.push(alias);
|
|
229
229
|
}
|
|
230
|
-
} else if (
|
|
231
|
-
|
|
230
|
+
} else if (alias.startsWith(ENV_PREFIX)) {
|
|
231
|
+
result.env.push(alias);
|
|
232
232
|
} else {
|
|
233
|
-
|
|
233
|
+
result.raw.push(alias);
|
|
234
234
|
}
|
|
235
|
-
return
|
|
235
|
+
return result;
|
|
236
236
|
}, { long: [], short: [], raw: [], env: [] });
|
|
237
237
|
}
|
|
238
238
|
}
|
|
@@ -53,7 +53,7 @@ const FIELD_CONFIG: {
|
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
name: 'debugIpc',
|
|
56
|
-
run: cmd => CliUtil.debugIfIpc(cmd).then((
|
|
56
|
+
run: cmd => CliUtil.debugIfIpc(cmd).then((flag) => flag && process.exit(0)),
|
|
57
57
|
field: {
|
|
58
58
|
type: Boolean,
|
|
59
59
|
aliases: ['-di'],
|
|
@@ -64,7 +64,7 @@ const FIELD_CONFIG: {
|
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
name: 'canRestart',
|
|
67
|
-
run: cmd => CliUtil.runWithRestart(cmd)?.then((
|
|
67
|
+
run: cmd => CliUtil.runWithRestart(cmd)?.then((flag) => flag && process.exit(0)),
|
|
68
68
|
field: {
|
|
69
69
|
type: Boolean,
|
|
70
70
|
aliases: ['-cr'],
|
|
@@ -82,20 +82,19 @@ const FIELD_CONFIG: {
|
|
|
82
82
|
* @example main
|
|
83
83
|
* @kind decorator
|
|
84
84
|
*/
|
|
85
|
-
export function CliCommand(
|
|
85
|
+
export function CliCommand(config: CliCommandConfigOptions = {}) {
|
|
86
86
|
return function <T extends CliCommandShape>(target: Class<T>): void {
|
|
87
87
|
const adapter = SchemaRegistryIndex.getForRegister(target);
|
|
88
88
|
const description = describeFunction(target) ?? {};
|
|
89
89
|
|
|
90
|
-
|
|
91
90
|
if (!target.Ⲑid || description.abstract) {
|
|
92
91
|
return;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
const VALID_FIELDS = FIELD_CONFIG.filter(
|
|
94
|
+
const VALID_FIELDS = FIELD_CONFIG.filter(field => !!config.with?.[field.name]);
|
|
96
95
|
|
|
97
96
|
CliCommandRegistryIndex.getForRegister(target).register({
|
|
98
|
-
runTarget:
|
|
97
|
+
runTarget: config.runTarget,
|
|
99
98
|
preMain: async (cmd: Cmd) => {
|
|
100
99
|
for (const field of VALID_FIELDS) {
|
|
101
100
|
await field.run(cmd);
|
|
@@ -109,7 +108,7 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
|
|
|
109
108
|
adapter.registerField(name, field, { type });
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
const runtimeModule =
|
|
111
|
+
const runtimeModule = config.runtimeModule ?? (config.with?.module ? 'current' : undefined);
|
|
113
112
|
|
|
114
113
|
if (runtimeModule) { // Validate module
|
|
115
114
|
adapter.register({
|
|
@@ -139,14 +138,14 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
|
|
|
139
138
|
* @augments `@travetto/schema:Input`
|
|
140
139
|
* @kind decorator
|
|
141
140
|
*/
|
|
142
|
-
export function CliFlag(
|
|
143
|
-
return function (instance: ClassInstance, property: string
|
|
141
|
+
export function CliFlag(config: { full?: string, short?: string, fileExtensions?: string[], envVars?: string[] } = {}) {
|
|
142
|
+
return function (instance: ClassInstance, property: string): void {
|
|
144
143
|
const aliases = [
|
|
145
|
-
...(
|
|
146
|
-
...(
|
|
147
|
-
...(
|
|
144
|
+
...(config.full ? [config.full.startsWith('-') ? config.full : `--${config.full}`] : []),
|
|
145
|
+
...(config.short ? [config.short.startsWith('-') ? config.short : `-${config.short}`] : []),
|
|
146
|
+
...(config.envVars ? config.envVars.map(CliParseUtil.toEnvField) : [])
|
|
148
147
|
];
|
|
149
|
-
const specifiers =
|
|
148
|
+
const specifiers = config.fileExtensions?.length ? ['file', ...config.fileExtensions.map(ext => `ext:${ext.replace(/[*.]/g, '')}`)] : [];
|
|
150
149
|
|
|
151
150
|
SchemaRegistryIndex.getForRegister(getClass(instance)).registerField(property, { aliases, specifiers });
|
|
152
151
|
};
|
|
@@ -7,9 +7,9 @@ import { CliParseUtil, ENV_PREFIX } from '../parse.ts';
|
|
|
7
7
|
|
|
8
8
|
const CLI_FILE_REGEX = /\/cli[.](?<name>.{0,100}?)([.]tsx?)?$/;
|
|
9
9
|
|
|
10
|
-
const getName = (
|
|
11
|
-
const stripDashes = (
|
|
12
|
-
const toFlagName = (
|
|
10
|
+
const getName = (name: string): string => (name.match(CLI_FILE_REGEX)?.groups?.name ?? name).replaceAll('_', ':');
|
|
11
|
+
const stripDashes = (flag?: string): string | undefined => flag?.replace(/^-+/, '');
|
|
12
|
+
const toFlagName = (field: string): string => field.replace(/([a-z])([A-Z])/g, (_, left: string, right: string) => `${left}-${right.toLowerCase()}`);
|
|
13
13
|
|
|
14
14
|
export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConfig> {
|
|
15
15
|
#cls: Class;
|
|
@@ -36,17 +36,17 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const used = new Set(Object.values(schema.fields)
|
|
39
|
-
.flatMap(
|
|
40
|
-
.filter(
|
|
39
|
+
.flatMap(field => field.aliases ?? [])
|
|
40
|
+
.filter(alias => !alias.startsWith(ENV_PREFIX))
|
|
41
41
|
.map(stripDashes)
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
for (const field of Object.values(schema.fields)) {
|
|
45
|
-
const fieldName = field.name
|
|
45
|
+
const fieldName = field.name;
|
|
46
46
|
const { long: longAliases, short: shortAliases, raw: rawAliases, env: envAliases } = CliParseUtil.parseAliases(field.aliases ?? []);
|
|
47
47
|
|
|
48
|
-
let short = stripDashes(shortAliases?.[0]) ?? rawAliases.find(
|
|
49
|
-
const long = stripDashes(longAliases?.[0]) ?? rawAliases.find(
|
|
48
|
+
let short = stripDashes(shortAliases?.[0]) ?? rawAliases.find(alias => alias.length <= 2);
|
|
49
|
+
const long = stripDashes(longAliases?.[0]) ?? rawAliases.find(alias => alias.length >= 3) ?? toFlagName(fieldName);
|
|
50
50
|
const aliases: string[] = field.aliases = [...envAliases];
|
|
51
51
|
|
|
52
52
|
if (short === undefined) {
|
|
@@ -74,14 +74,14 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
|
|
|
74
74
|
/**
|
|
75
75
|
* Registers a cli command
|
|
76
76
|
*/
|
|
77
|
-
register(...
|
|
77
|
+
register(...configs: Partial<CliCommandConfig>[]): CliCommandConfig {
|
|
78
78
|
const meta = describeFunction(this.#cls);
|
|
79
79
|
this.#config ??= {
|
|
80
80
|
cls: this.#cls,
|
|
81
81
|
preMain: undefined,
|
|
82
82
|
name: getName(meta.import),
|
|
83
83
|
};
|
|
84
|
-
Object.assign(this.#config, ...
|
|
84
|
+
Object.assign(this.#config, ...configs);
|
|
85
85
|
return this.#config;
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -7,7 +7,7 @@ import { CliUnknownCommandError } from '../error.ts';
|
|
|
7
7
|
import { CliCommandRegistryAdapter } from './registry-adapter.ts';
|
|
8
8
|
|
|
9
9
|
const CLI_FILE_REGEX = /\/cli[.](?<name>.{0,100}?)([.]tsx?)?$/;
|
|
10
|
-
const getName = (
|
|
10
|
+
const getName = (field: string): string => (field.match(CLI_FILE_REGEX)?.groups?.name ?? field).replaceAll('_', ':');
|
|
11
11
|
|
|
12
12
|
type CliCommandLoadResult = { command: string, config: CliCommandConfig, instance: CliCommandShape, schema: SchemaClassConfig };
|
|
13
13
|
|
|
@@ -38,12 +38,12 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
38
38
|
get #commandMapping(): Map<string, string> {
|
|
39
39
|
if (!this.#fileMapping) {
|
|
40
40
|
const all = new Map<string, string>();
|
|
41
|
-
for (const
|
|
42
|
-
module:
|
|
43
|
-
folder:
|
|
44
|
-
file:
|
|
41
|
+
for (const entry of RuntimeIndex.find({
|
|
42
|
+
module: mod => !Runtime.production || mod.prod,
|
|
43
|
+
folder: folder => folder === 'support',
|
|
44
|
+
file: file => file.role === 'std' && CLI_FILE_REGEX.test(file.sourceFile)
|
|
45
45
|
})) {
|
|
46
|
-
all.set(getName(
|
|
46
|
+
all.set(getName(entry.sourceFile), entry.import);
|
|
47
47
|
}
|
|
48
48
|
this.#fileMapping = all;
|
|
49
49
|
}
|
|
@@ -70,32 +70,32 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
70
70
|
const found = this.#commandMapping.get(name)!;
|
|
71
71
|
const values = Object.values(await Runtime.importFrom<Record<string, Class>>(found));
|
|
72
72
|
const filtered = values
|
|
73
|
-
.filter((
|
|
74
|
-
.reduce<Class[]>((
|
|
75
|
-
const parent = getParentClass(
|
|
76
|
-
if (parent && !
|
|
77
|
-
|
|
73
|
+
.filter((value): value is Class => typeof value === 'function')
|
|
74
|
+
.reduce<Class[]>((classes, cls) => {
|
|
75
|
+
const parent = getParentClass(cls);
|
|
76
|
+
if (parent && !classes.includes(parent)) {
|
|
77
|
+
classes.push(parent);
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
return
|
|
79
|
+
classes.push(cls);
|
|
80
|
+
return classes;
|
|
81
81
|
}, []);
|
|
82
82
|
|
|
83
83
|
const uninitialized = filtered
|
|
84
|
-
.filter(
|
|
84
|
+
.filter(cls => !this.store.finalized(cls));
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
// Initialize any uninitialized commands
|
|
88
88
|
if (uninitialized.length) {
|
|
89
89
|
// Ensure processed
|
|
90
|
-
Registry.process(uninitialized.map(
|
|
90
|
+
Registry.process(uninitialized.map(cls => ({ type: 'added', current: cls })));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
for (const
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
93
|
+
for (const cls of values) {
|
|
94
|
+
const config = this.store.get(cls);
|
|
95
|
+
if (!config) {
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
|
-
const result =
|
|
98
|
+
const result = config.getInstance();
|
|
99
99
|
if (result.isActive !== undefined && !result.isActive()) {
|
|
100
100
|
continue;
|
|
101
101
|
}
|
|
@@ -112,11 +112,11 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
112
112
|
async load(names?: string[]): Promise<CliCommandLoadResult[]> {
|
|
113
113
|
const keys = names ?? [...this.#commandMapping.keys()];
|
|
114
114
|
|
|
115
|
-
const list = await Promise.all(keys.map(async
|
|
116
|
-
const instance = await this.#getInstance(
|
|
115
|
+
const list = await Promise.all(keys.map(async key => {
|
|
116
|
+
const instance = await this.#getInstance(key);
|
|
117
117
|
const config = this.store.get(getClass(instance)).get();
|
|
118
118
|
const schema = SchemaRegistryIndex.getConfig(getClass(instance));
|
|
119
|
-
return { command:
|
|
119
|
+
return { command: key, instance, config, schema };
|
|
120
120
|
}));
|
|
121
121
|
|
|
122
122
|
return list.sort((a, b) => a.command.localeCompare(b.command));
|
package/src/schema-export.ts
CHANGED
|
@@ -37,18 +37,18 @@ export class CliSchemaExportUtil {
|
|
|
37
37
|
/**
|
|
38
38
|
* Get the base type for a CLI command input
|
|
39
39
|
*/
|
|
40
|
-
static baseInputType(
|
|
41
|
-
switch (
|
|
40
|
+
static baseInputType(config: SchemaInputConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
|
|
41
|
+
switch (config.type) {
|
|
42
42
|
case Date: return { type: 'date' };
|
|
43
43
|
case Boolean: return { type: 'boolean' };
|
|
44
44
|
case Number: return { type: 'number' };
|
|
45
45
|
case RegExp: return { type: 'regex' };
|
|
46
46
|
case String: {
|
|
47
47
|
switch (true) {
|
|
48
|
-
case
|
|
49
|
-
case
|
|
48
|
+
case config.specifiers?.includes('module'): return { type: 'module' };
|
|
49
|
+
case config.specifiers?.includes('file'): return {
|
|
50
50
|
type: 'file',
|
|
51
|
-
fileExtensions:
|
|
51
|
+
fileExtensions: config.specifiers?.map(specifier => specifier.split('ext:')[1]).filter(specifier => !!specifier)
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -59,30 +59,30 @@ export class CliSchemaExportUtil {
|
|
|
59
59
|
/**
|
|
60
60
|
* Process input configuration for CLI commands
|
|
61
61
|
*/
|
|
62
|
-
static processInput(
|
|
62
|
+
static processInput(config: SchemaInputConfig): CliCommandInput {
|
|
63
63
|
return {
|
|
64
|
-
...this.baseInputType(
|
|
65
|
-
...(('name' in
|
|
66
|
-
description:
|
|
67
|
-
array:
|
|
68
|
-
required:
|
|
69
|
-
choices:
|
|
70
|
-
default: Array.isArray(
|
|
71
|
-
flagNames: (
|
|
72
|
-
envVars: (
|
|
64
|
+
...this.baseInputType(config),
|
|
65
|
+
...(('name' in config && typeof config.name === 'string') ? { name: config.name } : { name: '' }),
|
|
66
|
+
description: config.description,
|
|
67
|
+
array: config.array,
|
|
68
|
+
required: config.required?.active !== false,
|
|
69
|
+
choices: config.enum?.values,
|
|
70
|
+
default: Array.isArray(config.default) ? config.default.slice(0) : config.default,
|
|
71
|
+
flagNames: (config.aliases ?? []).slice(0).filter(value => !value.startsWith('env.')),
|
|
72
|
+
envVars: (config.aliases ?? []).slice(0).filter(value => value.startsWith('env.')).map(value => value.replace('env.', ''))
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
static exportSchema(cls: Class): CliCommandSchema {
|
|
77
77
|
const schema = SchemaRegistryIndex.getConfig(cls);
|
|
78
78
|
const config = CliCommandRegistryIndex.get(cls);
|
|
79
|
-
const processed = Object.values(schema.fields).map(
|
|
79
|
+
const processed = Object.values(schema.fields).map(value => this.processInput(value));
|
|
80
80
|
return {
|
|
81
81
|
name: config.name,
|
|
82
82
|
module: describeFunction(config.cls).module,
|
|
83
83
|
description: schema.description,
|
|
84
|
-
flags: processed.filter(
|
|
85
|
-
args: processed.filter(
|
|
84
|
+
flags: processed.filter(value => value.flagNames && value.flagNames.length > 0),
|
|
85
|
+
args: processed.filter(value => !value.flagNames || value.flagNames.length === 0),
|
|
86
86
|
runTarget: config.runTarget ?? false,
|
|
87
87
|
commandModule: describeFunction(cls).module,
|
|
88
88
|
};
|
package/src/schema.ts
CHANGED
|
@@ -57,29 +57,29 @@ export class CliCommandSchemaUtil {
|
|
|
57
57
|
*/
|
|
58
58
|
static async validate(cmd: CliCommandShape, args: unknown[]): Promise<typeof cmd> {
|
|
59
59
|
const cls = getClass(cmd);
|
|
60
|
-
const paramNames = SchemaRegistryIndex.get(cls).getMethod('main').parameters.map(
|
|
60
|
+
const paramNames = SchemaRegistryIndex.get(cls).getMethod('main').parameters.map(config => config.name!);
|
|
61
61
|
|
|
62
62
|
const validators = [
|
|
63
63
|
(): Promise<void> => SchemaValidator.validate(cls, cmd).then(() => { }),
|
|
64
64
|
(): Promise<void> => SchemaValidator.validateMethod(cls, 'main', args, paramNames),
|
|
65
65
|
async (): Promise<void> => {
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
throw new CliValidationResultError(cmd, Array.isArray(
|
|
66
|
+
const result = await cmd.validate?.(...args);
|
|
67
|
+
if (result) {
|
|
68
|
+
throw new CliValidationResultError(cmd, Array.isArray(result) ? result : [result]);
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
];
|
|
72
72
|
|
|
73
73
|
const SOURCES = ['flag', 'arg', 'custom'] as const;
|
|
74
74
|
|
|
75
|
-
const results = validators.map((
|
|
76
|
-
if (!(
|
|
77
|
-
throw
|
|
75
|
+
const results = validators.map((validator, i) => validator().catch(error => {
|
|
76
|
+
if (!(error instanceof CliValidationResultError) && !(error instanceof ValidationResultError)) {
|
|
77
|
+
throw error;
|
|
78
78
|
}
|
|
79
|
-
return
|
|
79
|
+
return error.details.errors.map(value => ({ ...value, source: getSource(value.source, SOURCES[i]) }));
|
|
80
80
|
}));
|
|
81
81
|
|
|
82
|
-
const errors = (await Promise.all(results)).flatMap(
|
|
82
|
+
const errors = (await Promise.all(results)).flatMap(result => (result ?? []));
|
|
83
83
|
if (errors.length) {
|
|
84
84
|
throw new CliValidationResultError(cmd, errors);
|
|
85
85
|
}
|
package/src/scm.ts
CHANGED
|
@@ -38,7 +38,7 @@ export class CliScmUtil {
|
|
|
38
38
|
const result = await ExecUtil.getResult(spawn('git', ['log', '--pretty=oneline'], { cwd: Runtime.workspace.path }));
|
|
39
39
|
return result.stdout
|
|
40
40
|
.split(/\n/)
|
|
41
|
-
.find(
|
|
41
|
+
.find(line => /Publish /.test(line))?.split(/\s+/)?.[0];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -47,14 +47,14 @@ export class CliScmUtil {
|
|
|
47
47
|
* @returns
|
|
48
48
|
*/
|
|
49
49
|
static async findChangedFiles(fromHash: string, toHash: string = 'HEAD'): Promise<string[]> {
|
|
50
|
-
const
|
|
51
|
-
const result = await ExecUtil.getResult(spawn('git', ['diff', '--name-only', `${fromHash}..${toHash}`, ':!**/DOC.*', ':!**/README.*'], { cwd:
|
|
50
|
+
const rootPath = Runtime.workspace.path;
|
|
51
|
+
const result = await ExecUtil.getResult(spawn('git', ['diff', '--name-only', `${fromHash}..${toHash}`, ':!**/DOC.*', ':!**/README.*'], { cwd: rootPath }), { catch: true });
|
|
52
52
|
if (!result.valid) {
|
|
53
53
|
throw new AppError('Unable to detect changes between', { category: 'data', details: { fromHash, toHash, output: (result.stderr || result.stdout) } });
|
|
54
54
|
}
|
|
55
55
|
const out = new Set<string>();
|
|
56
56
|
for (const line of result.stdout.split(/\n/g)) {
|
|
57
|
-
const entry = RuntimeIndex.getEntry(path.resolve(
|
|
57
|
+
const entry = RuntimeIndex.getEntry(path.resolve(rootPath, line));
|
|
58
58
|
if (entry) {
|
|
59
59
|
out.add(entry.sourceFile);
|
|
60
60
|
}
|
|
@@ -71,10 +71,10 @@ export class CliScmUtil {
|
|
|
71
71
|
static async findChangedModules(fromHash: string, toHash?: string): Promise<IndexedModule[]> {
|
|
72
72
|
const files = await this.findChangedFiles(fromHash, toHash);
|
|
73
73
|
const mods = files
|
|
74
|
-
.map(
|
|
75
|
-
.filter(
|
|
76
|
-
.map(
|
|
77
|
-
.filter(
|
|
74
|
+
.map(file => RuntimeIndex.getFromSource(file))
|
|
75
|
+
.filter(file => !!file)
|
|
76
|
+
.map(file => RuntimeIndex.getModule(file.module))
|
|
77
|
+
.filter(mod => !!mod);
|
|
78
78
|
|
|
79
79
|
return [...new Set(mods)]
|
|
80
80
|
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
@@ -84,7 +84,7 @@ export class CliScmUtil {
|
|
|
84
84
|
* Create a commit
|
|
85
85
|
*/
|
|
86
86
|
static createCommit(message: string): Promise<string> {
|
|
87
|
-
return ExecUtil.getResult(spawn('git', ['commit', '.', '-m', message])).then(
|
|
87
|
+
return ExecUtil.getResult(spawn('git', ['commit', '.', '-m', message])).then(result => result.stdout);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
@@ -92,7 +92,7 @@ export class CliScmUtil {
|
|
|
92
92
|
*/
|
|
93
93
|
static createTag(version: string): Promise<string> {
|
|
94
94
|
version = version.replace(/[^0-9a-z_\-.]/g, '');
|
|
95
|
-
return ExecUtil.getResult(spawn('git', ['tag', '-a', `${version}`, '-m', `Release ${version}`])).then(
|
|
95
|
+
return ExecUtil.getResult(spawn('git', ['tag', '-a', `${version}`, '-m', `Release ${version}`])).then(result => result.stdout);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
package/src/service.ts
CHANGED
|
@@ -5,11 +5,11 @@ import net from 'node:net';
|
|
|
5
5
|
|
|
6
6
|
import { ExecUtil, TimeUtil, Util } from '@travetto/runtime';
|
|
7
7
|
|
|
8
|
-
const ports = (
|
|
9
|
-
typeof
|
|
10
|
-
[
|
|
8
|
+
const ports = (value: number | `${number}:${number}`): [number, number] =>
|
|
9
|
+
typeof value === 'number' ?
|
|
10
|
+
[value, value] :
|
|
11
11
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
12
|
-
|
|
12
|
+
value.split(':').map(number => parseInt(number, 10)) as [number, number];
|
|
13
13
|
|
|
14
14
|
type BodyCheck = (body: string) => boolean;
|
|
15
15
|
|
|
@@ -36,26 +36,26 @@ export type ServiceAction = 'start' | 'stop' | 'status' | 'restart';
|
|
|
36
36
|
*/
|
|
37
37
|
export class ServiceRunner {
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
constructor(
|
|
39
|
+
#descriptor: ServiceDescriptor;
|
|
40
|
+
constructor(descriptor: ServiceDescriptor) { this.#descriptor = descriptor; }
|
|
41
41
|
|
|
42
42
|
async #isRunning(full = false): Promise<boolean> {
|
|
43
|
-
const port = ports(this.
|
|
43
|
+
const port = ports(this.#descriptor.port!)[0];
|
|
44
44
|
const start = Date.now();
|
|
45
|
-
const timeoutMs = TimeUtil.asMillis(full ? this.
|
|
45
|
+
const timeoutMs = TimeUtil.asMillis(full ? this.#descriptor.startupTimeout ?? 5000 : 100);
|
|
46
46
|
while ((Date.now() - start) < timeoutMs) {
|
|
47
47
|
try {
|
|
48
48
|
const sock = net.createConnection(port, 'localhost');
|
|
49
|
-
await new Promise<void>((
|
|
50
|
-
sock.on('connect',
|
|
49
|
+
await new Promise<void>((resolve, reject) =>
|
|
50
|
+
sock.on('connect', resolve).on('timeout', reject).on('error', reject)
|
|
51
51
|
).finally(() => sock.destroy());
|
|
52
52
|
|
|
53
|
-
if (!this.
|
|
53
|
+
if (!this.#descriptor.ready?.url || !full) {
|
|
54
54
|
return true;
|
|
55
55
|
} else {
|
|
56
|
-
const response = await fetch(this.
|
|
56
|
+
const response = await fetch(this.#descriptor.ready.url, { method: 'GET' });
|
|
57
57
|
const text = await response.text();
|
|
58
|
-
if (response.ok && (this.
|
|
58
|
+
if (response.ok && (this.#descriptor.ready.test?.(text) ?? true)) {
|
|
59
59
|
return true;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -68,14 +68,14 @@ export class ServiceRunner {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async #hasImage(): Promise<boolean> {
|
|
71
|
-
const result = await ExecUtil.getResult(spawn('docker', ['image', 'inspect', this.
|
|
71
|
+
const result = await ExecUtil.getResult(spawn('docker', ['image', 'inspect', this.#descriptor.image]), { catch: true });
|
|
72
72
|
return result.valid;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
async * #pullImage(): AsyncIterable<string> {
|
|
76
|
-
const
|
|
77
|
-
yield* rl.createInterface(
|
|
78
|
-
await ExecUtil.getResult(
|
|
76
|
+
const subProcess = spawn('docker', ['pull', this.#descriptor.image], { stdio: [0, 'pipe', 'pipe'] });
|
|
77
|
+
yield* rl.createInterface(subProcess.stdout!);
|
|
78
|
+
await ExecUtil.getResult(subProcess);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
async #startContainer(): Promise<string> {
|
|
@@ -83,16 +83,16 @@ export class ServiceRunner {
|
|
|
83
83
|
'run',
|
|
84
84
|
'--rm',
|
|
85
85
|
'--detach',
|
|
86
|
-
...this.
|
|
87
|
-
'--label', `trv-${this.
|
|
88
|
-
...Object.entries(this.
|
|
89
|
-
...this.
|
|
90
|
-
...Object.entries(this.
|
|
91
|
-
this.
|
|
92
|
-
...this.
|
|
86
|
+
...this.#descriptor.privileged ? ['--privileged'] : [],
|
|
87
|
+
'--label', `trv-${this.#descriptor.name}`,
|
|
88
|
+
...Object.entries(this.#descriptor.env ?? {}).flatMap(([key, value]) => ['--env', `${key}=${value}`]),
|
|
89
|
+
...this.#descriptor.port ? ['-p', ports(this.#descriptor.port).join(':')] : [],
|
|
90
|
+
...Object.entries(this.#descriptor.volumes ?? {}).flatMap(([key, value]) => ['--volume', `${key}:${value}`]),
|
|
91
|
+
this.#descriptor.image,
|
|
92
|
+
...this.#descriptor.args ?? [],
|
|
93
93
|
];
|
|
94
94
|
|
|
95
|
-
for (const item of Object.keys(this.
|
|
95
|
+
for (const item of Object.keys(this.#descriptor.volumes ?? {})) {
|
|
96
96
|
await fs.mkdir(item, { recursive: true });
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -100,38 +100,38 @@ export class ServiceRunner {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
async #getContainerId(): Promise<string | undefined> {
|
|
103
|
-
return (await ExecUtil.getResult(spawn('docker', ['ps', '-q', '--filter', `label=trv-${this.
|
|
103
|
+
return (await ExecUtil.getResult(spawn('docker', ['ps', '-q', '--filter', `label=trv-${this.#descriptor.name}`]))).stdout.trim();
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
async #killContainer(
|
|
107
|
-
await ExecUtil.getResult(spawn('docker', ['kill',
|
|
106
|
+
async #killContainer(containerId: string): Promise<void> {
|
|
107
|
+
await ExecUtil.getResult(spawn('docker', ['kill', containerId]));
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
async * action(
|
|
110
|
+
async * action(operation: ServiceAction): AsyncIterable<['success' | 'failure' | 'message', string]> {
|
|
111
111
|
try {
|
|
112
|
-
const
|
|
113
|
-
const port = this.
|
|
114
|
-
const running = !!
|
|
112
|
+
const containerId = await this.#getContainerId();
|
|
113
|
+
const port = this.#descriptor.port ? ports(this.#descriptor.port)[0] : 0;
|
|
114
|
+
const running = !!containerId && (!port || await this.#isRunning());
|
|
115
115
|
|
|
116
|
-
if (running && !
|
|
117
|
-
return yield [
|
|
116
|
+
if (running && !containerId) { // We don't own
|
|
117
|
+
return yield [operation === 'status' ? 'message' : 'failure', 'Running but not managed'];
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
if (
|
|
121
|
-
return yield !
|
|
122
|
-
} else if (
|
|
120
|
+
if (operation === 'status') {
|
|
121
|
+
return yield !containerId ? ['message', 'Not running'] : ['success', `Running ${containerId}`];
|
|
122
|
+
} else if (operation === 'start' && running) {
|
|
123
123
|
return yield ['message', 'Skipping, already running'];
|
|
124
|
-
} else if (
|
|
124
|
+
} else if (operation === 'stop' && !running) {
|
|
125
125
|
return yield ['message', 'Skipping, already stopped'];
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
if (running && (
|
|
128
|
+
if (running && (operation === 'restart' || operation === 'stop')) {
|
|
129
129
|
yield ['message', 'Stopping'];
|
|
130
|
-
await this.#killContainer(
|
|
130
|
+
await this.#killContainer(containerId);
|
|
131
131
|
yield ['success', 'Stopped'];
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
if (
|
|
134
|
+
if (operation === 'restart' || operation === 'start') {
|
|
135
135
|
if (!await this.#hasImage()) {
|
|
136
136
|
yield ['message', 'Starting image download'];
|
|
137
137
|
for await (const line of await this.#pullImage()) {
|
|
@@ -144,7 +144,7 @@ export class ServiceRunner {
|
|
|
144
144
|
const out = await this.#startContainer();
|
|
145
145
|
|
|
146
146
|
if (port) {
|
|
147
|
-
yield ['message', `Waiting for ${this.
|
|
147
|
+
yield ['message', `Waiting for ${this.#descriptor.ready?.url ?? 'container'}...`];
|
|
148
148
|
if (!await this.#isRunning(true)) {
|
|
149
149
|
yield ['failure', 'Failed to start service correctly'];
|
|
150
150
|
}
|
package/src/util.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { CliCommandShape, CliCommandShapeFields } from './types.ts';
|
|
|
6
6
|
|
|
7
7
|
const IPC_ALLOWED_ENV = new Set(['NODE_OPTIONS']);
|
|
8
8
|
const IPC_INVALID_ENV = new Set(['PS1', 'INIT_CWD', 'COLOR', 'LANGUAGE', 'PROFILEHOME', '_']);
|
|
9
|
-
const validEnv = (
|
|
9
|
+
const validEnv = (key: string): boolean => IPC_ALLOWED_ENV.has(key) || (
|
|
10
|
+
!IPC_INVALID_ENV.has(key) && !/^(npm_|GTK|GDK|TRV|NODE|GIT|TERM_)/.test(key) && !/VSCODE/.test(key)
|
|
11
|
+
);
|
|
10
12
|
|
|
11
13
|
export class CliUtil {
|
|
12
14
|
/**
|
|
@@ -51,14 +53,14 @@ export class CliUtil {
|
|
|
51
53
|
return false;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
const info = await fetch(Env.TRV_CLI_IPC.
|
|
56
|
+
const info = await fetch(Env.TRV_CLI_IPC.value!).catch(() => ({ ok: false }));
|
|
55
57
|
|
|
56
58
|
if (!info.ok) { // Server not running
|
|
57
59
|
return false;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
const env: Record<string, string> = {};
|
|
61
|
-
const
|
|
63
|
+
const request = {
|
|
62
64
|
type: `@travetto/cli:${action}`,
|
|
63
65
|
data: {
|
|
64
66
|
name: cmd._cfg!.name,
|
|
@@ -69,10 +71,10 @@ export class CliUtil {
|
|
|
69
71
|
args: process.argv.slice(3),
|
|
70
72
|
}
|
|
71
73
|
};
|
|
72
|
-
console.log('Triggering IPC request',
|
|
74
|
+
console.log('Triggering IPC request', request);
|
|
73
75
|
|
|
74
|
-
Object.entries(process.env).forEach(([
|
|
75
|
-
const sent = await fetch(Env.TRV_CLI_IPC.
|
|
76
|
+
Object.entries(process.env).forEach(([key, value]) => validEnv(key) && (env[key] = value!));
|
|
77
|
+
const sent = await fetch(Env.TRV_CLI_IPC.value!, { method: 'POST', body: JSON.stringify(request) });
|
|
76
78
|
return sent.ok;
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -87,6 +89,6 @@ export class CliUtil {
|
|
|
87
89
|
* Write data to channel and ensure its flushed before continuing
|
|
88
90
|
*/
|
|
89
91
|
static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
|
|
90
|
-
return await new Promise(
|
|
92
|
+
return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data : JSON.stringify(data, null, 2), () => resolve()));
|
|
91
93
|
}
|
|
92
94
|
}
|
|
@@ -20,7 +20,7 @@ export class CliSchemaCommand implements CliCommandShape {
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
const resolved = await CliCommandRegistryIndex.load(names);
|
|
23
|
-
const invalid = names.find(
|
|
23
|
+
const invalid = names.find(name => !resolved.find(result => result.command === name));
|
|
24
24
|
|
|
25
25
|
if (invalid) {
|
|
26
26
|
return {
|
|
@@ -38,7 +38,7 @@ export class CliSchemaCommand implements CliCommandShape {
|
|
|
38
38
|
const resolved = await CliCommandRegistryIndex.load(names);
|
|
39
39
|
|
|
40
40
|
const output = resolved
|
|
41
|
-
.map(
|
|
41
|
+
.map(result => CliSchemaExportUtil.exportSchema(result.config.cls));
|
|
42
42
|
|
|
43
43
|
await CliUtil.writeAndEnsureComplete(output);
|
|
44
44
|
}
|
package/support/cli.main.ts
CHANGED
|
@@ -25,8 +25,8 @@ export class MainCommand implements CliCommandShape {
|
|
|
25
25
|
try {
|
|
26
26
|
const mod = await Runtime.importFrom<{ main(..._: unknown[]): Promise<unknown> }>(fileOrImport);
|
|
27
27
|
result = await mod.main(...args, ...this._parsed.unknown);
|
|
28
|
-
} catch (
|
|
29
|
-
result =
|
|
28
|
+
} catch (error) {
|
|
29
|
+
result = error;
|
|
30
30
|
process.exitCode = Math.max(process.exitCode ? +process.exitCode : 1, 1);
|
|
31
31
|
}
|
|
32
32
|
|
package/support/cli.service.ts
CHANGED
|
@@ -13,14 +13,14 @@ export class CliServiceCommand implements CliCommandShape {
|
|
|
13
13
|
async #getServices(services: string[]): Promise<ServiceDescriptor[]> {
|
|
14
14
|
return (await Promise.all(
|
|
15
15
|
RuntimeIndex.find({
|
|
16
|
-
module:
|
|
17
|
-
folder:
|
|
18
|
-
file:
|
|
16
|
+
module: mod => mod.roles.includes('std'),
|
|
17
|
+
folder: folder => folder === 'support',
|
|
18
|
+
file: file => /support\/service[.]/.test(file.sourceFile)
|
|
19
19
|
})
|
|
20
|
-
.map(
|
|
20
|
+
.map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
|
|
21
21
|
))
|
|
22
|
-
.filter(
|
|
23
|
-
.filter(
|
|
22
|
+
.filter(file => !!file)
|
|
23
|
+
.filter(file => services?.length ? services.includes(file.name) : true)
|
|
24
24
|
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -37,29 +37,29 @@ export class CliServiceCommand implements CliCommandShape {
|
|
|
37
37
|
return [
|
|
38
38
|
cliTpl`${{ title: 'Available Services' }}`,
|
|
39
39
|
'-'.repeat(20),
|
|
40
|
-
...all.map(
|
|
40
|
+
...all.map(service => cliTpl` * ${{ identifier: service.name }}@${{ type: service.version }}`)
|
|
41
41
|
];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
async main(action: ServiceAction, services: string[] = []): Promise<void> {
|
|
45
45
|
const all = await this.#getServices(services);
|
|
46
|
-
const maxName = Math.max(...all.map(
|
|
47
|
-
const maxVersion = Math.max(...all.map(
|
|
46
|
+
const maxName = Math.max(...all.map(service => service.name.length), 'Service'.length) + 3;
|
|
47
|
+
const maxVersion = Math.max(...all.map(service => `${service.version}`.length), 'Version'.length) + 3;
|
|
48
48
|
const maxStatus = 20;
|
|
49
|
-
const
|
|
49
|
+
const queue = new AsyncQueue<{ idx: number, text: string, done?: boolean }>();
|
|
50
50
|
|
|
51
|
-
const jobs = all.map(async (
|
|
52
|
-
const identifier =
|
|
53
|
-
const type = `${
|
|
51
|
+
const jobs = all.map(async (descriptor, i) => {
|
|
52
|
+
const identifier = descriptor.name.padEnd(maxName);
|
|
53
|
+
const type = `${descriptor.version}`.padStart(maxVersion - 3).padEnd(maxVersion);
|
|
54
54
|
let msg: string;
|
|
55
|
-
for await (const [valueType, value] of new ServiceRunner(
|
|
55
|
+
for await (const [valueType, value] of new ServiceRunner(descriptor).action(action)) {
|
|
56
56
|
const details = { [valueType === 'message' ? 'subtitle' : valueType]: value };
|
|
57
|
-
|
|
57
|
+
queue.add({ idx: i, text: msg = cliTpl`${{ identifier }} ${{ type }} ${details}` });
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
queue.add({ idx: i, done: true, text: msg! });
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
Promise.all(jobs).then(() => Util.queueMacroTask()).then(() =>
|
|
62
|
+
Promise.all(jobs).then(() => Util.queueMacroTask()).then(() => queue.close());
|
|
63
63
|
|
|
64
64
|
const term = new Terminal();
|
|
65
65
|
await term.writer.writeLines([
|
|
@@ -68,6 +68,6 @@ export class CliServiceCommand implements CliCommandShape {
|
|
|
68
68
|
''.padEnd(maxName + maxVersion + maxStatus + 3, '-'),
|
|
69
69
|
]).commit();
|
|
70
70
|
|
|
71
|
-
await term.streamList(
|
|
71
|
+
await term.streamList(queue);
|
|
72
72
|
}
|
|
73
73
|
}
|