@travetto/cli 7.0.0-rc.2 → 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 +2 -2
- package/bin/trv.js +2 -0
- package/package.json +3 -3
- package/src/error.ts +1 -1
- package/src/registry/decorator.ts +7 -7
- package/src/registry/registry-adapter.ts +1 -1
- package/src/registry/registry-index.ts +5 -8
- package/src/schema-export.ts +2 -2
- package/src/types.ts +2 -2
- package/src/util.ts +79 -16
package/README.md
CHANGED
|
@@ -472,7 +472,7 @@ import type { WebHttpServer } from '../src/types.ts';
|
|
|
472
472
|
/**
|
|
473
473
|
* Run a web server
|
|
474
474
|
*/
|
|
475
|
-
@CliCommand({ runTarget: true, with: { debugIpc: true,
|
|
475
|
+
@CliCommand({ runTarget: true, with: { debugIpc: true, restartForDev: true, module: true, env: true } })
|
|
476
476
|
export class WebHttpCommand implements CliCommandShape {
|
|
477
477
|
|
|
478
478
|
/** Port to run on */
|
|
@@ -502,7 +502,7 @@ export class WebHttpCommand implements CliCommandShape {
|
|
|
502
502
|
}
|
|
503
503
|
```
|
|
504
504
|
|
|
505
|
-
As noted in the example above, `fields` is specified in this execution, with support for `module`, and `env`. These env flag is directly tied to the [Runtime](https://github.com/travetto/travetto/tree/main/module/runtime/src/context.ts#
|
|
505
|
+
As noted in the example above, `fields` is specified in this execution, with support for `module`, and `env`. These env flag is directly tied to the [Runtime](https://github.com/travetto/travetto/tree/main/module/runtime/src/context.ts#L13) `name` defined in the [Runtime](https://github.com/travetto/travetto/tree/main/module/runtime#readme "Runtime for travetto applications.") module.
|
|
506
506
|
|
|
507
507
|
The `module` field is slightly more complex, but is geared towards supporting commands within a monorepo context. This flag ensures that a module is specified if running from the root of the monorepo, and that the module provided is real, and can run the desired command. When running from an explicit module folder in the monorepo, the module flag is ignored.
|
|
508
508
|
|
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.3",
|
|
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.3",
|
|
32
|
+
"@travetto/terminal": "^7.0.0-rc.3"
|
|
33
33
|
},
|
|
34
34
|
"travetto": {
|
|
35
35
|
"displayName": "Command Line Interface",
|
package/src/error.ts
CHANGED
|
@@ -21,7 +21,7 @@ const COMMAND_PACKAGE = [
|
|
|
21
21
|
export class CliUnknownCommandError extends Error {
|
|
22
22
|
|
|
23
23
|
#getMissingCommandHelp(cmd: string): string | undefined {
|
|
24
|
-
const matchedConfig = COMMAND_PACKAGE.find(([
|
|
24
|
+
const matchedConfig = COMMAND_PACKAGE.find(([regex]) => regex.test(cmd));
|
|
25
25
|
if (matchedConfig) {
|
|
26
26
|
const [, pkg, prod] = matchedConfig;
|
|
27
27
|
const install = PackageUtil.getInstallCommand(Runtime, `@travetto/${pkg}`, prod);
|
|
@@ -19,8 +19,8 @@ type CliCommandConfigOptions = {
|
|
|
19
19
|
module?: boolean;
|
|
20
20
|
/** Should debug invocation trigger via ipc */
|
|
21
21
|
debugIpc?: boolean;
|
|
22
|
-
/** Should
|
|
23
|
-
|
|
22
|
+
/** Should restart on code change */
|
|
23
|
+
restartForDev?: boolean;
|
|
24
24
|
};
|
|
25
25
|
};
|
|
26
26
|
|
|
@@ -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((executed) => executed && process.exit(0)),
|
|
57
57
|
field: {
|
|
58
58
|
type: Boolean,
|
|
59
59
|
aliases: ['-di'],
|
|
@@ -63,13 +63,13 @@ const FIELD_CONFIG: {
|
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
|
-
name: '
|
|
67
|
-
run: cmd => CliUtil.
|
|
66
|
+
name: 'restartForDev',
|
|
67
|
+
run: cmd => CliUtil.runWithRestartOnCodeChanges(cmd),
|
|
68
68
|
field: {
|
|
69
69
|
type: Boolean,
|
|
70
70
|
aliases: ['-cr'],
|
|
71
|
-
description: 'Should the invocation automatically restart on
|
|
72
|
-
default:
|
|
71
|
+
description: 'Should the invocation automatically restart on source changes',
|
|
72
|
+
default: Runtime.env === 'development',
|
|
73
73
|
required: { active: false },
|
|
74
74
|
},
|
|
75
75
|
}
|
|
@@ -27,7 +27,7 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
|
|
|
27
27
|
(schema.fields ??= {}).help = {
|
|
28
28
|
type: Boolean,
|
|
29
29
|
name: 'help',
|
|
30
|
-
|
|
30
|
+
class: this.#cls,
|
|
31
31
|
description: 'display help for command',
|
|
32
32
|
required: { active: false },
|
|
33
33
|
default: false,
|
|
@@ -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
|
|
|
@@ -32,6 +32,8 @@ 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
|
*/
|
|
@@ -50,11 +52,6 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
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,7 +67,7 @@ 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(
|
|
70
|
+
.filter(isClass)
|
|
74
71
|
.reduce<Class[]>((classes, cls) => {
|
|
75
72
|
const parent = getParentClass(cls);
|
|
76
73
|
if (parent && !classes.includes(parent)) {
|
|
@@ -87,7 +84,7 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
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
90
|
for (const cls of values) {
|
package/src/schema-export.ts
CHANGED
|
@@ -35,8 +35,8 @@ export interface CliCommandSchema<K extends string = string> {
|
|
|
35
35
|
export class CliSchemaExportUtil {
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
* Get the base type for a CLI command input
|
|
39
|
+
*/
|
|
40
40
|
static baseInputType(config: SchemaInputConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
|
|
41
41
|
switch (config.type) {
|
|
42
42
|
case Date: return { type: 'date' };
|
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,15 +1,24 @@
|
|
|
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
10
|
const validEnv = (key: string): boolean => IPC_ALLOWED_ENV.has(key) || (
|
|
10
11
|
!IPC_INVALID_ENV.has(key) && !/^(npm_|GTK|GDK|TRV|NODE|GIT|TERM_)/.test(key) && !/VSCODE/.test(key)
|
|
11
12
|
);
|
|
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
|
+
};
|
|
21
|
+
|
|
13
22
|
export class CliUtil {
|
|
14
23
|
/**
|
|
15
24
|
* Get a simplified version of a module name
|
|
@@ -28,21 +37,68 @@ export class CliUtil {
|
|
|
28
37
|
/**
|
|
29
38
|
* Run a command as restartable, linking into self
|
|
30
39
|
*/
|
|
31
|
-
static
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
34
45
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
38
60
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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();
|
|
46
102
|
}
|
|
47
103
|
|
|
48
104
|
/**
|
|
@@ -82,7 +138,7 @@ export class CliUtil {
|
|
|
82
138
|
* Debug if IPC available
|
|
83
139
|
*/
|
|
84
140
|
static async debugIfIpc<T extends CliCommandShapeFields & CliCommandShape>(cmd: T): Promise<boolean> {
|
|
85
|
-
return
|
|
141
|
+
return cmd.debugIpc === true && this.triggerIpc('run', cmd);
|
|
86
142
|
}
|
|
87
143
|
|
|
88
144
|
/**
|
|
@@ -91,4 +147,11 @@ export class CliUtil {
|
|
|
91
147
|
static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
|
|
92
148
|
return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data : JSON.stringify(data, null, 2), () => resolve()));
|
|
93
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]));
|
|
156
|
+
}
|
|
94
157
|
}
|