@travetto/cli 8.0.0-alpha.3 → 8.0.0-alpha.30
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 +108 -86
- package/__index__.ts +10 -9
- package/bin/trv.js +3 -1
- package/package.json +3 -3
- package/src/color.ts +1 -1
- package/src/execute.ts +38 -20
- package/src/help.ts +129 -76
- package/src/module.ts +9 -10
- package/src/parse.ts +57 -42
- package/src/registry/decorator.ts +52 -47
- package/src/registry/registry-adapter.ts +22 -25
- package/src/registry/registry-index.ts +30 -26
- package/src/schema-export.ts +25 -16
- package/src/schema.ts +15 -10
- package/src/scm.ts +19 -10
- package/src/service.ts +34 -33
- package/src/types.ts +9 -7
- package/src/util.ts +25 -26
- package/support/cli.cli_schema.ts +8 -7
- package/support/cli.main.ts +12 -8
- package/support/cli.service.ts +25 -19
- package/support/entry.trv.ts +3 -2
package/src/service.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
-
import rl from 'node:readline/promises';
|
|
4
3
|
import net from 'node:net';
|
|
4
|
+
import rl from 'node:readline/promises';
|
|
5
5
|
|
|
6
6
|
import { ExecUtil, Runtime, RuntimeIndex, TimeUtil, Util } from '@travetto/runtime';
|
|
7
7
|
|
|
8
8
|
const ports = (value: number | `${number}:${number}`): [number, number] =>
|
|
9
|
-
typeof value === 'number' ?
|
|
10
|
-
[value, value] :
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
12
|
-
value.split(':').map(number => parseInt(number, 10)) as [number, number];
|
|
9
|
+
typeof value === 'number' ? [value, value] : (value.split(':').map(number => parseInt(number, 10)) as [number, number]);
|
|
13
10
|
|
|
14
11
|
type BodyCheck = (body: string) => boolean;
|
|
15
12
|
|
|
@@ -23,7 +20,7 @@ export interface ServiceDescriptor {
|
|
|
23
20
|
privileged?: boolean;
|
|
24
21
|
image: string;
|
|
25
22
|
args?: string[];
|
|
26
|
-
ready?: { url: string
|
|
23
|
+
ready?: { url: string; test?: BodyCheck };
|
|
27
24
|
volumes?: Record<string, string>;
|
|
28
25
|
env?: Record<string, string>;
|
|
29
26
|
startupTimeout?: number;
|
|
@@ -35,37 +32,39 @@ export type ServiceAction = 'start' | 'stop' | 'status' | 'restart';
|
|
|
35
32
|
* Service runner
|
|
36
33
|
*/
|
|
37
34
|
export class ServiceRunner {
|
|
38
|
-
|
|
39
35
|
/**
|
|
40
36
|
* Find all services
|
|
41
37
|
*/
|
|
42
38
|
static async findServices(services: string[]): Promise<ServiceDescriptor[]> {
|
|
43
|
-
return (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
|
|
50
|
-
|
|
39
|
+
return (
|
|
40
|
+
await Promise.all(
|
|
41
|
+
RuntimeIndex.find({
|
|
42
|
+
module: module => module.roles.includes('std'),
|
|
43
|
+
folder: folder => folder === 'support',
|
|
44
|
+
file: file => /support\/service[.]/.test(file.sourceFile)
|
|
45
|
+
}).map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
|
|
46
|
+
)
|
|
47
|
+
)
|
|
51
48
|
.filter(file => !!file)
|
|
52
|
-
.filter(file => services?.length ? services.includes(file.name) : true)
|
|
49
|
+
.filter(file => (services?.length ? services.includes(file.name) : true))
|
|
53
50
|
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
#descriptor: ServiceDescriptor;
|
|
57
|
-
constructor(descriptor: ServiceDescriptor) {
|
|
54
|
+
constructor(descriptor: ServiceDescriptor) {
|
|
55
|
+
this.#descriptor = descriptor;
|
|
56
|
+
}
|
|
58
57
|
|
|
59
58
|
async #isRunning(full = false): Promise<boolean> {
|
|
60
59
|
const port = ports(this.#descriptor.port!)[0];
|
|
61
60
|
const start = Date.now();
|
|
62
|
-
const timeoutMs = TimeUtil.duration(full ? this.#descriptor.startupTimeout ?? 5000 : 100, 'ms');
|
|
63
|
-
while (
|
|
61
|
+
const timeoutMs = TimeUtil.duration(full ? (this.#descriptor.startupTimeout ?? 5000) : 100, 'ms');
|
|
62
|
+
while (Date.now() - start < timeoutMs) {
|
|
64
63
|
try {
|
|
65
64
|
const sock = net.createConnection(port, 'localhost');
|
|
66
|
-
await new Promise<void>((resolve, reject) =>
|
|
67
|
-
sock.
|
|
68
|
-
)
|
|
65
|
+
await new Promise<void>((resolve, reject) => sock.on('connect', resolve).on('timeout', reject).on('error', reject)).finally(() =>
|
|
66
|
+
sock.destroy()
|
|
67
|
+
);
|
|
69
68
|
|
|
70
69
|
if (!this.#descriptor.ready?.url || !full) {
|
|
71
70
|
return true;
|
|
@@ -89,7 +88,7 @@ export class ServiceRunner {
|
|
|
89
88
|
return result.valid;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
async
|
|
91
|
+
async *#pullImage(): AsyncIterable<string> {
|
|
93
92
|
const subProcess = spawn('docker', ['pull', this.#descriptor.image], { stdio: [0, 'pipe', 'pipe'] });
|
|
94
93
|
yield* rl.createInterface(subProcess.stdout!);
|
|
95
94
|
await ExecUtil.getResult(subProcess);
|
|
@@ -100,13 +99,14 @@ export class ServiceRunner {
|
|
|
100
99
|
'run',
|
|
101
100
|
'--rm',
|
|
102
101
|
'--detach',
|
|
103
|
-
...this.#descriptor.privileged ? ['--privileged'] : [],
|
|
104
|
-
'--label',
|
|
102
|
+
...(this.#descriptor.privileged ? ['--privileged'] : []),
|
|
103
|
+
'--label',
|
|
104
|
+
`trv-${this.#descriptor.name}`,
|
|
105
105
|
...Object.entries(this.#descriptor.env ?? {}).flatMap(([key, value]) => ['--env', `${key}=${value}`]),
|
|
106
|
-
...this.#descriptor.port ? ['-p', ports(this.#descriptor.port).join(':')] : [],
|
|
106
|
+
...(this.#descriptor.port ? ['-p', ports(this.#descriptor.port).join(':')] : []),
|
|
107
107
|
...Object.entries(this.#descriptor.volumes ?? {}).flatMap(([key, value]) => ['--volume', `${key}:${value}`]),
|
|
108
108
|
this.#descriptor.image,
|
|
109
|
-
...this.#descriptor.args ?? []
|
|
109
|
+
...(this.#descriptor.args ?? [])
|
|
110
110
|
];
|
|
111
111
|
|
|
112
112
|
for (const item of Object.keys(this.#descriptor.volumes ?? {})) {
|
|
@@ -124,13 +124,14 @@ export class ServiceRunner {
|
|
|
124
124
|
await ExecUtil.getResult(spawn('docker', ['kill', containerId]));
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
async *
|
|
127
|
+
async *action(operation: ServiceAction): AsyncIterable<['success' | 'failure' | 'message', string]> {
|
|
128
128
|
try {
|
|
129
129
|
const containerId = await this.#getContainerId();
|
|
130
130
|
const port = this.#descriptor.port ? ports(this.#descriptor.port)[0] : 0;
|
|
131
|
-
const running = !!containerId && (!port || await this.#isRunning());
|
|
131
|
+
const running = !!containerId && (!port || (await this.#isRunning()));
|
|
132
132
|
|
|
133
|
-
if (running && !containerId) {
|
|
133
|
+
if (running && !containerId) {
|
|
134
|
+
// We don't own
|
|
134
135
|
return yield [operation === 'status' ? 'message' : 'failure', 'Running but not managed'];
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -149,7 +150,7 @@ export class ServiceRunner {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
if (operation === 'restart' || operation === 'start') {
|
|
152
|
-
if (!await this.#hasImage()) {
|
|
153
|
+
if (!(await this.#hasImage())) {
|
|
153
154
|
yield ['message', 'Starting image download'];
|
|
154
155
|
for await (const line of this.#pullImage()) {
|
|
155
156
|
yield ['message', `Downloading: ${line}`];
|
|
@@ -162,7 +163,7 @@ export class ServiceRunner {
|
|
|
162
163
|
|
|
163
164
|
if (port) {
|
|
164
165
|
yield ['message', `Waiting for ${this.#descriptor.ready?.url ?? 'container'}...`];
|
|
165
|
-
if (!await this.#isRunning(true)) {
|
|
166
|
+
if (!(await this.#isRunning(true))) {
|
|
166
167
|
yield ['failure', 'Failed to start service correctly'];
|
|
167
168
|
}
|
|
168
169
|
}
|
|
@@ -172,4 +173,4 @@ export class ServiceRunner {
|
|
|
172
173
|
yield ['failure', 'Failed to start'];
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
|
-
}
|
|
176
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { Class } from '@travetto/runtime';
|
|
1
|
+
import type { Any, Class } from '@travetto/runtime';
|
|
2
|
+
|
|
3
|
+
export const HELP_FLAG = '--help';
|
|
2
4
|
|
|
3
5
|
type OrProm<T> = T | Promise<T>;
|
|
4
|
-
type ParsedFlag = { type: 'flag'
|
|
5
|
-
type ParsedArg = { type: 'arg'
|
|
6
|
-
type ParsedUnknown = { type: 'unknown'
|
|
6
|
+
type ParsedFlag = { type: 'flag'; input: string; array?: boolean; fieldName: string; value?: unknown };
|
|
7
|
+
type ParsedArg = { type: 'arg'; input: string; array?: boolean; index: number };
|
|
8
|
+
type ParsedUnknown = { type: 'unknown'; input: string };
|
|
7
9
|
type ParsedInput = ParsedUnknown | ParsedFlag | ParsedArg;
|
|
8
10
|
|
|
9
11
|
export type ParsedState = {
|
|
@@ -32,7 +34,7 @@ export interface CliCommandShape {
|
|
|
32
34
|
help?(): OrProm<string[]>;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
type PreMainHandler = (cmd:
|
|
37
|
+
export type PreMainHandler<T extends Any = Any> = { priority: number; handler: (cmd: T) => Any };
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* CLI Command schema shape
|
|
@@ -41,5 +43,5 @@ export interface CliCommandConfig {
|
|
|
41
43
|
cls: Class<CliCommandShape>;
|
|
42
44
|
name: string;
|
|
43
45
|
runTarget?: boolean;
|
|
44
|
-
preMain
|
|
45
|
-
}
|
|
46
|
+
preMain: PreMainHandler[];
|
|
47
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ChildProcess, spawn } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Env, ExecUtil, JSONUtil, Runtime, RuntimeError, ShutdownManager, Util, WatchUtil } from '@travetto/runtime';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const IPC_INVALID_ENV = new Set(
|
|
7
|
-
|
|
8
|
-
!IPC_INVALID_ENV.has(key) && !/^(npm_|GTK|GDK|TRV|NODE|GIT|TERM_)/.test(key) && !/VSCODE/.test(key)
|
|
5
|
+
const IPC_VALID_ENV = new Set(['NODE_OPTIONS', 'PATH', Env.DEBUG.key, Env.NODE_ENV.key]);
|
|
6
|
+
const IPC_INVALID_ENV = new Set(
|
|
7
|
+
[Env.TRV_CLI_IPC, Env.TRV_DEBUG_IPC, Env.TRV_DEBUG_BREAK, Env.TRV_MANIFEST, Env.TRV_MODULE, Env.TRV_RESTART_TARGET].map(item => item.key)
|
|
9
8
|
);
|
|
9
|
+
const validEnv = ([key]: [key: string, value: unknown]): boolean =>
|
|
10
|
+
IPC_VALID_ENV.has(key) || (key.startsWith('TRV_') && !IPC_INVALID_ENV.has(key));
|
|
10
11
|
|
|
11
12
|
export class CliUtil {
|
|
12
13
|
/**
|
|
13
14
|
* Get a simplified version of a module name
|
|
14
15
|
*/
|
|
15
16
|
static getSimpleModuleName(placeholder: string, module?: string): string {
|
|
16
|
-
const simple = (module ?? Runtime.main.name).replace(/[
|
|
17
|
-
|
|
18
|
-
return placeholder.replace('<module>', targetModule);
|
|
17
|
+
const simple = (module ?? Runtime.main.name).replace(/[/]/, '_').replace(/@/, '');
|
|
18
|
+
return simple ? placeholder.replace('<module>', simple) : placeholder;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -33,10 +33,9 @@ export class CliUtil {
|
|
|
33
33
|
ShutdownManager.disableInterrupt();
|
|
34
34
|
|
|
35
35
|
let child: ChildProcess | undefined;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.on('message', msg => child?.send?.(msg!));
|
|
36
|
+
await WatchUtil.watchCompilerEvents('file', () => ShutdownManager.shutdownChild(child!, { reason: 'restart', mode: 'exit' }));
|
|
37
|
+
|
|
38
|
+
process.on('SIGINT', () => ShutdownManager.shutdownChild(child!, { mode: 'exit' })).on('message', message => child?.send?.(message!));
|
|
40
39
|
|
|
41
40
|
const env = { ...process.env, ...Env.TRV_RESTART_TARGET.export(true) };
|
|
42
41
|
|
|
@@ -50,12 +49,13 @@ export class CliUtil {
|
|
|
50
49
|
maxRetries: 5,
|
|
51
50
|
onRetry: async (state, config) => {
|
|
52
51
|
const duration = WatchUtil.computeRestartDelay(state, config);
|
|
53
|
-
console.error(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
console.error('[cli-restart] Restarting subprocess due to change...', {
|
|
53
|
+
waiting: duration,
|
|
54
|
+
iteration: state.iteration,
|
|
55
|
+
errorIterations: state.errorIterations || undefined
|
|
56
|
+
});
|
|
57
57
|
await Util.nonBlockingTimeout(duration);
|
|
58
|
-
}
|
|
58
|
+
}
|
|
59
59
|
}
|
|
60
60
|
);
|
|
61
61
|
|
|
@@ -77,32 +77,31 @@ export class CliUtil {
|
|
|
77
77
|
return; // Server not running, run normal
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const env: Record<string, string> = {};
|
|
81
80
|
const request = {
|
|
82
81
|
type: '@travetto/cli:run',
|
|
83
82
|
data: {
|
|
84
83
|
name,
|
|
85
|
-
env,
|
|
84
|
+
env: Object.fromEntries(Object.entries(process.env).filter(validEnv)),
|
|
86
85
|
cwd: process.cwd(),
|
|
87
|
-
args: process.argv.slice(3)
|
|
86
|
+
args: process.argv.slice(3)
|
|
88
87
|
}
|
|
89
88
|
};
|
|
90
|
-
console.log('Triggering IPC request', request);
|
|
91
89
|
|
|
92
|
-
|
|
90
|
+
console.log('Triggering IPC request', request);
|
|
93
91
|
const sent = await doFetch({ method: 'POST', body: JSONUtil.toUTF8(request) });
|
|
94
92
|
|
|
95
93
|
if (!sent.ok) {
|
|
96
94
|
throw new RuntimeError(`IPC Request failed: ${sent.status} ${await sent.text()}`);
|
|
97
95
|
}
|
|
96
|
+
|
|
97
|
+
await ShutdownManager.shutdown({ mode: 'exit' });
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* Write data to channel and ensure its flushed before continuing
|
|
102
102
|
*/
|
|
103
103
|
static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
|
|
104
|
-
|
|
105
|
-
JSONUtil.toUTF8Pretty(data), () => resolve()));
|
|
104
|
+
await new Promise<unknown>(resolve => process[channel].write(typeof data === 'string' ? data : JSONUtil.toUTF8Pretty(data), resolve));
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
/**
|
|
@@ -111,4 +110,4 @@ export class CliUtil {
|
|
|
111
110
|
static readExtendedOptions(options?: string[]): Record<string, string | boolean> {
|
|
112
111
|
return Object.fromEntries((options ?? [])?.map(option => [...option.split(':'), true]));
|
|
113
112
|
}
|
|
114
|
-
}
|
|
113
|
+
}
|
|
@@ -2,10 +2,10 @@ import { Env } from '@travetto/runtime';
|
|
|
2
2
|
import { IsPrivate, MethodValidator, type ValidationError } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { CliCommand } from '../src/registry/decorator.ts';
|
|
5
|
-
import type { CliCommandShape } from '../src/types.ts';
|
|
6
5
|
import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
|
|
7
|
-
import { CliUtil } from '../src/util.ts';
|
|
8
6
|
import { CliSchemaExportUtil } from '../src/schema-export.ts';
|
|
7
|
+
import type { CliCommandShape } from '../src/types.ts';
|
|
8
|
+
import { CliUtil } from '../src/util.ts';
|
|
9
9
|
|
|
10
10
|
async function nameValidator(names?: string[]): Promise<ValidationError | undefined> {
|
|
11
11
|
if (!names || names.length === 0) {
|
|
@@ -25,12 +25,14 @@ async function nameValidator(names?: string[]): Promise<ValidationError | undefi
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Exports machine-readable command metadata for automation and tooling.
|
|
29
|
+
*
|
|
30
|
+
* Used by editor integrations to discover runnable commands and inputs.
|
|
31
|
+
* Used by guidance workflows to validate command signatures.
|
|
29
32
|
*/
|
|
30
33
|
@CliCommand()
|
|
31
34
|
@IsPrivate()
|
|
32
35
|
export class CliSchemaCommand implements CliCommandShape {
|
|
33
|
-
|
|
34
36
|
finalize(): void {
|
|
35
37
|
Env.DEBUG.set(false);
|
|
36
38
|
}
|
|
@@ -39,9 +41,8 @@ export class CliSchemaCommand implements CliCommandShape {
|
|
|
39
41
|
async main(names?: string[]): Promise<void> {
|
|
40
42
|
const resolved = await CliCommandRegistryIndex.load(names);
|
|
41
43
|
|
|
42
|
-
const output = resolved
|
|
43
|
-
.map(result => CliSchemaExportUtil.exportSchema(result.config.cls));
|
|
44
|
+
const output = resolved.map(result => CliSchemaExportUtil.exportSchema(result.config.cls));
|
|
44
45
|
|
|
45
46
|
await CliUtil.writeAndEnsureComplete(output);
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|
package/support/cli.main.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { CliCommand, type CliCommandShape, CliParseUtil } from '@travetto/cli';
|
|
1
2
|
import { JSONUtil, Runtime } from '@travetto/runtime';
|
|
2
|
-
import { type CliCommandShape, CliCommand, CliParseUtil } from '@travetto/cli';
|
|
3
3
|
import { IsPrivate, MethodValidator, type ValidationError } from '@travetto/schema';
|
|
4
4
|
|
|
5
5
|
async function validateMain(fileOrImport: string): Promise<ValidationError | undefined> {
|
|
@@ -8,31 +8,35 @@ async function validateMain(fileOrImport: string): Promise<ValidationError | und
|
|
|
8
8
|
} catch {
|
|
9
9
|
return { message: `Unknown file: ${fileOrImport}`, source: 'arg', kind: 'invalid', path: 'fileOrImport' };
|
|
10
10
|
}
|
|
11
|
-
}
|
|
11
|
+
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Execute a module `main()` entrypoint directly.
|
|
15
|
+
*
|
|
16
|
+
* This internal command resolves an import/source target, invokes its exported
|
|
17
|
+
* `main` function, and forwards unknown CLI args to that function.
|
|
15
18
|
*/
|
|
16
19
|
@CliCommand()
|
|
17
20
|
@IsPrivate()
|
|
18
21
|
export class MainCommand implements CliCommandShape {
|
|
19
|
-
|
|
20
22
|
@MethodValidator(validateMain)
|
|
21
23
|
async main(fileOrImport: string, args: string[] = []): Promise<void> {
|
|
22
24
|
const parsed = CliParseUtil.getState(this);
|
|
23
25
|
let result: unknown;
|
|
24
26
|
try {
|
|
25
27
|
const module = await Runtime.importFrom<{ main(..._: unknown[]): Promise<unknown> }>(fileOrImport);
|
|
26
|
-
result = await module.main(...args, ...parsed?.unknown ?? []);
|
|
28
|
+
result = await module.main(...args, ...(parsed?.unknown ?? []));
|
|
27
29
|
} catch (error) {
|
|
28
30
|
result = error;
|
|
29
31
|
process.exitCode = Math.max(process.exitCode ? +process.exitCode : 1, 1);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
if (result !== undefined) {
|
|
33
|
-
if (process.connected) {
|
|
34
|
-
|
|
35
|
+
if (process.connected) {
|
|
36
|
+
process.send?.(result);
|
|
37
|
+
}
|
|
38
|
+
const payload = typeof result === 'string' ? result : result instanceof Error ? result.stack : JSONUtil.toUTF8(result);
|
|
35
39
|
process[process.exitCode ? 'stderr' : 'stdout'].write(`${payload}\n`);
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
|
-
}
|
|
42
|
+
}
|
package/support/cli.service.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from 'node:util';
|
|
2
2
|
|
|
3
|
-
import { type CliCommandShape,
|
|
4
|
-
import { Terminal } from '@travetto/terminal';
|
|
3
|
+
import { CliCommand, type CliCommandShape, cliTpl } from '@travetto/cli';
|
|
5
4
|
import { AsyncQueue, Util } from '@travetto/runtime';
|
|
6
5
|
import { MethodValidator, type ValidationError } from '@travetto/schema';
|
|
6
|
+
import { Terminal } from '@travetto/terminal';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { type ServiceAction, ServiceRunner } from '../src/service.ts';
|
|
9
9
|
|
|
10
10
|
async function validateService(_: ServiceAction, services: string[]): Promise<ValidationError | undefined> {
|
|
11
11
|
const all = await ServiceRunner.findServices(services);
|
|
@@ -16,11 +16,13 @@ async function validateService(_: ServiceAction, services: string[]): Promise<Va
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* Manage development services (start/stop/restart/status) across the workspace.
|
|
20
|
+
*
|
|
21
|
+
* Services are discovered from registered descriptors and executed with streamed
|
|
22
|
+
* terminal feedback, including optional quiet mode.
|
|
20
23
|
*/
|
|
21
24
|
@CliCommand()
|
|
22
25
|
export class CliServiceCommand implements CliCommandShape {
|
|
23
|
-
|
|
24
26
|
quiet = false;
|
|
25
27
|
|
|
26
28
|
async help(): Promise<string[]> {
|
|
@@ -38,44 +40,48 @@ export class CliServiceCommand implements CliCommandShape {
|
|
|
38
40
|
const maxName = Math.max(...all.map(service => service.name.length), 'Service'.length) + 3;
|
|
39
41
|
const maxVersion = Math.max(...all.map(service => `${service.version}`.length), 'Version'.length) + 3;
|
|
40
42
|
const maxStatus = 20;
|
|
41
|
-
const queue = new AsyncQueue<{ idx: number
|
|
43
|
+
const queue = new AsyncQueue<{ idx: number; text: string; done?: boolean }>();
|
|
42
44
|
|
|
43
45
|
const failureMessages: string[] = [];
|
|
44
46
|
|
|
45
47
|
const jobs = all.map(async (descriptor, i) => {
|
|
46
48
|
const identifier = descriptor.name.padEnd(maxName);
|
|
47
49
|
const type = `${descriptor.version}`.padStart(maxVersion - 3).padEnd(maxVersion);
|
|
48
|
-
let
|
|
50
|
+
let message: string;
|
|
49
51
|
for await (const [valueType, value] of new ServiceRunner(descriptor).action(action)) {
|
|
50
52
|
const details = { [valueType === 'message' ? 'subtitle' : valueType]: value };
|
|
51
|
-
queue.add({ idx: i, text:
|
|
53
|
+
queue.add({ idx: i, text: (message = cliTpl`${{ identifier }} ${{ type }} ${details}`) });
|
|
52
54
|
if (valueType === 'failure') {
|
|
53
|
-
failureMessages.push(
|
|
55
|
+
failureMessages.push(message);
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
|
-
queue.add({ idx: i, done: true, text:
|
|
58
|
+
queue.add({ idx: i, done: true, text: message! });
|
|
57
59
|
});
|
|
58
60
|
|
|
59
|
-
Promise.all(jobs)
|
|
60
|
-
|
|
61
|
+
Promise.all(jobs)
|
|
62
|
+
.then(() => Util.queueMacroTask())
|
|
63
|
+
.then(() => queue.close());
|
|
61
64
|
|
|
62
65
|
if (this.quiet) {
|
|
63
|
-
for await (const _ of queue) {
|
|
66
|
+
for await (const _ of queue) {
|
|
67
|
+
}
|
|
64
68
|
if (failureMessages.length) {
|
|
65
69
|
console.error('Failure');
|
|
66
70
|
failureMessages.map(stripVTControlCharacters).map(item => console.error(item));
|
|
67
71
|
}
|
|
68
72
|
} else {
|
|
69
73
|
const term = new Terminal();
|
|
70
|
-
await term.writer
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
await term.writer
|
|
75
|
+
.writeLines([
|
|
76
|
+
'',
|
|
77
|
+
cliTpl`${{ title: 'Service'.padEnd(maxName) }} ${{ title: 'Version'.padEnd(maxVersion) }} ${{ title: 'Status' }}`,
|
|
78
|
+
''.padEnd(maxName + maxVersion + maxStatus + 3, '-')
|
|
79
|
+
])
|
|
80
|
+
.commit();
|
|
75
81
|
|
|
76
82
|
await term.streamList(queue);
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
process.exitCode = failureMessages.length ? 1 : 0;
|
|
80
86
|
}
|
|
81
|
-
}
|
|
87
|
+
}
|
package/support/entry.trv.ts
CHANGED