@travetto/cli 7.0.0-rc.1 → 7.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/trv.js +3 -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 +17 -18
- package/src/registry/registry-adapter.ts +11 -11
- package/src/registry/registry-index.ts +25 -28
- package/src/schema-export.ts +20 -20
- package/src/schema.ts +9 -9
- package/src/scm.ts +10 -10
- package/src/service.ts +42 -42
- package/src/types.ts +2 -2
- package/src/util.ts +88 -23
- package/support/cli.cli_schema.ts +2 -2
- package/support/cli.main.ts +2 -2
- package/support/cli.service.ts +18 -18
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, getClass, getParentClass, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
1
|
+
import { Class, getClass, getParentClass, isClass, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
2
2
|
import { RegistryAdapter, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
3
3
|
import { SchemaClassConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
4
4
|
|
|
@@ -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
|
|
|
@@ -32,29 +32,26 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
32
32
|
|
|
33
33
|
store = new RegistryIndexStore(CliCommandRegistryAdapter);
|
|
34
34
|
|
|
35
|
+
/** @private */ constructor(source: unknown) { Registry.validateConstructor(source); }
|
|
36
|
+
|
|
35
37
|
/**
|
|
36
38
|
* Get list of all commands available
|
|
37
39
|
*/
|
|
38
40
|
get #commandMapping(): Map<string, string> {
|
|
39
41
|
if (!this.#fileMapping) {
|
|
40
42
|
const all = new Map<string, string>();
|
|
41
|
-
for (const
|
|
42
|
-
module:
|
|
43
|
-
folder:
|
|
44
|
-
file:
|
|
43
|
+
for (const entry of RuntimeIndex.find({
|
|
44
|
+
module: mod => !Runtime.production || mod.prod,
|
|
45
|
+
folder: folder => folder === 'support',
|
|
46
|
+
file: file => file.role === 'std' && CLI_FILE_REGEX.test(file.sourceFile)
|
|
45
47
|
})) {
|
|
46
|
-
all.set(getName(
|
|
48
|
+
all.set(getName(entry.sourceFile), entry.import);
|
|
47
49
|
}
|
|
48
50
|
this.#fileMapping = all;
|
|
49
51
|
}
|
|
50
52
|
return this.#fileMapping;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
process(): void {
|
|
55
|
-
// Do nothing for now?
|
|
56
|
-
}
|
|
57
|
-
|
|
58
55
|
/**
|
|
59
56
|
* Import command into an instance
|
|
60
57
|
*/
|
|
@@ -70,32 +67,32 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
70
67
|
const found = this.#commandMapping.get(name)!;
|
|
71
68
|
const values = Object.values(await Runtime.importFrom<Record<string, Class>>(found));
|
|
72
69
|
const filtered = values
|
|
73
|
-
.filter(
|
|
74
|
-
.reduce<Class[]>((
|
|
75
|
-
const parent = getParentClass(
|
|
76
|
-
if (parent && !
|
|
77
|
-
|
|
70
|
+
.filter(isClass)
|
|
71
|
+
.reduce<Class[]>((classes, cls) => {
|
|
72
|
+
const parent = getParentClass(cls);
|
|
73
|
+
if (parent && !classes.includes(parent)) {
|
|
74
|
+
classes.push(parent);
|
|
78
75
|
}
|
|
79
|
-
|
|
80
|
-
return
|
|
76
|
+
classes.push(cls);
|
|
77
|
+
return classes;
|
|
81
78
|
}, []);
|
|
82
79
|
|
|
83
80
|
const uninitialized = filtered
|
|
84
|
-
.filter(
|
|
81
|
+
.filter(cls => !this.store.finalized(cls));
|
|
85
82
|
|
|
86
83
|
|
|
87
84
|
// Initialize any uninitialized commands
|
|
88
85
|
if (uninitialized.length) {
|
|
89
86
|
// Ensure processed
|
|
90
|
-
Registry.process(uninitialized
|
|
87
|
+
Registry.process(uninitialized);
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
for (const
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
90
|
+
for (const cls of values) {
|
|
91
|
+
const config = this.store.get(cls);
|
|
92
|
+
if (!config) {
|
|
96
93
|
continue;
|
|
97
94
|
}
|
|
98
|
-
const result =
|
|
95
|
+
const result = config.getInstance();
|
|
99
96
|
if (result.isActive !== undefined && !result.isActive()) {
|
|
100
97
|
continue;
|
|
101
98
|
}
|
|
@@ -112,11 +109,11 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
112
109
|
async load(names?: string[]): Promise<CliCommandLoadResult[]> {
|
|
113
110
|
const keys = names ?? [...this.#commandMapping.keys()];
|
|
114
111
|
|
|
115
|
-
const list = await Promise.all(keys.map(async
|
|
116
|
-
const instance = await this.#getInstance(
|
|
112
|
+
const list = await Promise.all(keys.map(async key => {
|
|
113
|
+
const instance = await this.#getInstance(key);
|
|
117
114
|
const config = this.store.get(getClass(instance)).get();
|
|
118
115
|
const schema = SchemaRegistryIndex.getConfig(getClass(instance));
|
|
119
|
-
return { command:
|
|
116
|
+
return { command: key, instance, config, schema };
|
|
120
117
|
}));
|
|
121
118
|
|
|
122
119
|
return list.sort((a, b) => a.command.localeCompare(b.command));
|
package/src/schema-export.ts
CHANGED
|
@@ -35,20 +35,20 @@ export interface CliCommandSchema<K extends string = string> {
|
|
|
35
35
|
export class CliSchemaExportUtil {
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
static baseInputType(
|
|
41
|
-
switch (
|
|
38
|
+
* Get the base type for a CLI command input
|
|
39
|
+
*/
|
|
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/types.ts
CHANGED
|
@@ -88,9 +88,9 @@ export type CliCommandShapeFields = {
|
|
|
88
88
|
*/
|
|
89
89
|
debugIpc?: boolean;
|
|
90
90
|
/**
|
|
91
|
-
* Should the invocation run with auto-restart
|
|
91
|
+
* Should the invocation run with auto-restart on source changes
|
|
92
92
|
*/
|
|
93
|
-
|
|
93
|
+
restartForDev?: boolean;
|
|
94
94
|
/**
|
|
95
95
|
* The module to run the command from
|
|
96
96
|
*/
|
package/src/util.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
import { describeFunction, Env, ExecUtil, Runtime } from '@travetto/runtime';
|
|
3
|
+
import { describeFunction, Env, ExecUtil, Runtime, listenForSourceChanges, type ExecutionResult, ShutdownManager } from '@travetto/runtime';
|
|
4
4
|
|
|
5
5
|
import { CliCommandShape, CliCommandShapeFields } from './types.ts';
|
|
6
6
|
|
|
7
|
+
const CODE_RESTART = { type: 'code_change', code: 200 };
|
|
7
8
|
const IPC_ALLOWED_ENV = new Set(['NODE_OPTIONS']);
|
|
8
9
|
const IPC_INVALID_ENV = new Set(['PS1', 'INIT_CWD', 'COLOR', 'LANGUAGE', 'PROFILEHOME', '_']);
|
|
9
|
-
const validEnv = (
|
|
10
|
+
const validEnv = (key: string): boolean => IPC_ALLOWED_ENV.has(key) || (
|
|
11
|
+
!IPC_INVALID_ENV.has(key) && !/^(npm_|GTK|GDK|TRV|NODE|GIT|TERM_)/.test(key) && !/VSCODE/.test(key)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const isCodeRestart = (input: unknown): input is typeof CODE_RESTART =>
|
|
15
|
+
typeof input === 'object' && !!input && 'type' in input && input.type === CODE_RESTART.type;
|
|
16
|
+
|
|
17
|
+
type RunWithRestartOptions = {
|
|
18
|
+
maxRetriesPerMinute?: number;
|
|
19
|
+
relayInterrupt?: boolean;
|
|
20
|
+
};
|
|
10
21
|
|
|
11
22
|
export class CliUtil {
|
|
12
23
|
/**
|
|
@@ -26,21 +37,68 @@ export class CliUtil {
|
|
|
26
37
|
/**
|
|
27
38
|
* Run a command as restartable, linking into self
|
|
28
39
|
*/
|
|
29
|
-
static
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
static async runWithRestartOnCodeChanges<T extends CliCommandShapeFields & CliCommandShape>(cmd: T, config?: RunWithRestartOptions): Promise<boolean> {
|
|
41
|
+
|
|
42
|
+
if (Env.TRV_CAN_RESTART.isFalse || cmd.restartForDev !== true) {
|
|
43
|
+
process.on('message', event => isCodeRestart(event) && process.exit(event.code));
|
|
44
|
+
return false;
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
|
|
47
|
+
let result: ExecutionResult | undefined;
|
|
48
|
+
let exhaustedRestarts = false;
|
|
49
|
+
let subProcess: ChildProcess | undefined;
|
|
50
|
+
|
|
51
|
+
const env = { ...process.env, ...Env.TRV_CAN_RESTART.export(false) };
|
|
52
|
+
const maxRetries = config?.maxRetriesPerMinute ?? 5;
|
|
53
|
+
const relayInterrupt = config?.relayInterrupt ?? true;
|
|
54
|
+
const restarts: number[] = [];
|
|
55
|
+
listenForSourceChanges(() => { subProcess?.send(CODE_RESTART); });
|
|
56
|
+
|
|
57
|
+
if (!relayInterrupt) {
|
|
58
|
+
process.removeAllListeners('SIGINT'); // Remove any existing listeners
|
|
59
|
+
process.on('SIGINT', () => { }); // Prevents SIGINT from killing parent process, the child will handle
|
|
36
60
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
|
|
62
|
+
while (
|
|
63
|
+
(result === undefined || result.code === CODE_RESTART.code) &&
|
|
64
|
+
!exhaustedRestarts
|
|
65
|
+
) {
|
|
66
|
+
if (restarts.length) {
|
|
67
|
+
console.error('Restarting...', { pid: process.pid, time: restarts[0] });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ensure restarts length is capped
|
|
71
|
+
subProcess = spawn(process.argv0, process.argv.slice(1), { env, stdio: [0, 1, 2, 'ipc'] })
|
|
72
|
+
.on('message', value => process.send?.(value));
|
|
73
|
+
|
|
74
|
+
const interrupt = (): void => { subProcess?.kill('SIGINT'); };
|
|
75
|
+
const toMessage = (value: unknown): void => { subProcess?.send(value!); };
|
|
76
|
+
|
|
77
|
+
// Proxy kill requests
|
|
78
|
+
process.on('message', toMessage);
|
|
79
|
+
if (relayInterrupt) {
|
|
80
|
+
process.on('SIGINT', interrupt);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
result = await ExecUtil.getResult(subProcess, { catch: true });
|
|
84
|
+
process.exitCode = subProcess.exitCode;
|
|
85
|
+
process.off('message', toMessage);
|
|
86
|
+
process.off('SIGINT', interrupt);
|
|
87
|
+
|
|
88
|
+
if (restarts.length >= maxRetries) {
|
|
89
|
+
exhaustedRestarts = (Date.now() - restarts[0]) < (10 * 1000);
|
|
90
|
+
restarts.shift();
|
|
91
|
+
}
|
|
92
|
+
restarts.push(Date.now());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if (exhaustedRestarts) {
|
|
97
|
+
console.error(`Bailing, due to ${maxRetries} restarts in under 10s`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await ShutdownManager.gracefulShutdown('cli-restart');
|
|
101
|
+
process.exit();
|
|
44
102
|
}
|
|
45
103
|
|
|
46
104
|
/**
|
|
@@ -51,14 +109,14 @@ export class CliUtil {
|
|
|
51
109
|
return false;
|
|
52
110
|
}
|
|
53
111
|
|
|
54
|
-
const info = await fetch(Env.TRV_CLI_IPC.
|
|
112
|
+
const info = await fetch(Env.TRV_CLI_IPC.value!).catch(() => ({ ok: false }));
|
|
55
113
|
|
|
56
114
|
if (!info.ok) { // Server not running
|
|
57
115
|
return false;
|
|
58
116
|
}
|
|
59
117
|
|
|
60
118
|
const env: Record<string, string> = {};
|
|
61
|
-
const
|
|
119
|
+
const request = {
|
|
62
120
|
type: `@travetto/cli:${action}`,
|
|
63
121
|
data: {
|
|
64
122
|
name: cmd._cfg!.name,
|
|
@@ -69,10 +127,10 @@ export class CliUtil {
|
|
|
69
127
|
args: process.argv.slice(3),
|
|
70
128
|
}
|
|
71
129
|
};
|
|
72
|
-
console.log('Triggering IPC request',
|
|
130
|
+
console.log('Triggering IPC request', request);
|
|
73
131
|
|
|
74
|
-
Object.entries(process.env).forEach(([
|
|
75
|
-
const sent = await fetch(Env.TRV_CLI_IPC.
|
|
132
|
+
Object.entries(process.env).forEach(([key, value]) => validEnv(key) && (env[key] = value!));
|
|
133
|
+
const sent = await fetch(Env.TRV_CLI_IPC.value!, { method: 'POST', body: JSON.stringify(request) });
|
|
76
134
|
return sent.ok;
|
|
77
135
|
}
|
|
78
136
|
|
|
@@ -80,13 +138,20 @@ export class CliUtil {
|
|
|
80
138
|
* Debug if IPC available
|
|
81
139
|
*/
|
|
82
140
|
static async debugIfIpc<T extends CliCommandShapeFields & CliCommandShape>(cmd: T): Promise<boolean> {
|
|
83
|
-
return
|
|
141
|
+
return cmd.debugIpc === true && this.triggerIpc('run', cmd);
|
|
84
142
|
}
|
|
85
143
|
|
|
86
144
|
/**
|
|
87
145
|
* Write data to channel and ensure its flushed before continuing
|
|
88
146
|
*/
|
|
89
147
|
static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
|
|
90
|
-
return await new Promise(
|
|
148
|
+
return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data : JSON.stringify(data, null, 2), () => resolve()));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Read extended options from cli inputs, in the form of -o key:value or -o key
|
|
153
|
+
*/
|
|
154
|
+
static readExtendedOptions(options?: string[]): Record<string, string | boolean> {
|
|
155
|
+
return Object.fromEntries((options ?? [])?.map(option => [...option.split(':'), true]));
|
|
91
156
|
}
|
|
92
157
|
}
|
|
@@ -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
|
|