@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.
@@ -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 = (s: string): string => (s.match(CLI_FILE_REGEX)?.groups?.name ?? s).replaceAll('_', ':');
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 e of RuntimeIndex.find({
42
- module: m => !Runtime.production || m.prod,
43
- folder: f => f === 'support',
44
- file: f => f.role === 'std' && CLI_FILE_REGEX.test(f.sourceFile)
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(e.sourceFile), e.import);
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((v): v is Class => typeof v === 'function')
74
- .reduce<Class[]>((acc, v) => {
75
- const parent = getParentClass(v);
76
- if (parent && !acc.includes(parent)) {
77
- acc.push(parent);
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
- acc.push(v);
80
- return acc;
76
+ classes.push(cls);
77
+ return classes;
81
78
  }, []);
82
79
 
83
80
  const uninitialized = filtered
84
- .filter(v => !this.store.finalized(v));
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.map(v => ({ type: 'added', curr: v })));
87
+ Registry.process(uninitialized);
91
88
  }
92
89
 
93
- for (const v of values) {
94
- const cfg = this.store.get(v);
95
- if (!cfg) {
90
+ for (const cls of values) {
91
+ const config = this.store.get(cls);
92
+ if (!config) {
96
93
  continue;
97
94
  }
98
- const result = cfg.getInstance();
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 x => {
116
- const instance = await this.#getInstance(x);
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: x, instance, config, schema };
116
+ return { command: key, instance, config, schema };
120
117
  }));
121
118
 
122
119
  return list.sort((a, b) => a.command.localeCompare(b.command));
@@ -35,20 +35,20 @@ export interface CliCommandSchema<K extends string = string> {
35
35
  export class CliSchemaExportUtil {
36
36
 
37
37
  /**
38
- * Get the base type for a CLI command input
39
- */
40
- static baseInputType(x: SchemaInputConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
41
- switch (x.type) {
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 x.specifiers?.includes('module'): return { type: 'module' };
49
- case x.specifiers?.includes('file'): return {
48
+ case config.specifiers?.includes('module'): return { type: 'module' };
49
+ case config.specifiers?.includes('file'): return {
50
50
  type: 'file',
51
- fileExtensions: x.specifiers?.map(s => s.split('ext:')[1]).filter(s => !!s)
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(x: SchemaInputConfig): CliCommandInput {
62
+ static processInput(config: SchemaInputConfig): CliCommandInput {
63
63
  return {
64
- ...this.baseInputType(x),
65
- ...(('name' in x && typeof x.name === 'string') ? { name: x.name } : { name: '' }),
66
- description: x.description,
67
- array: x.array,
68
- required: x.required?.active !== false,
69
- choices: x.enum?.values,
70
- default: Array.isArray(x.default) ? x.default.slice(0) : x.default,
71
- flagNames: (x.aliases ?? []).slice(0).filter(v => !v.startsWith('env.')),
72
- envVars: (x.aliases ?? []).slice(0).filter(v => v.startsWith('env.')).map(v => v.replace('env.', ''))
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(v => this.processInput(v));
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(v => v.flagNames && v.flagNames.length > 0),
85
- args: processed.filter(v => !v.flagNames || v.flagNames.length === 0),
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(x => x.name!);
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 res = await cmd.validate?.(...args);
67
- if (res) {
68
- throw new CliValidationResultError(cmd, Array.isArray(res) ? res : [res]);
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((x, i) => x().catch(err => {
76
- if (!(err instanceof CliValidationResultError) && !(err instanceof ValidationResultError)) {
77
- throw err;
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 err.details.errors.map(v => ({ ...v, source: getSource(v.source, SOURCES[i]) }));
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(x => (x ?? []));
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(x => /Publish /.test(x))?.split(/\s+/)?.[0];
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 ws = Runtime.workspace.path;
51
- const result = await ExecUtil.getResult(spawn('git', ['diff', '--name-only', `${fromHash}..${toHash}`, ':!**/DOC.*', ':!**/README.*'], { cwd: ws }), { catch: true });
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(ws, line));
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(x => RuntimeIndex.getFromSource(x))
75
- .filter(x => !!x)
76
- .map(x => RuntimeIndex.getModule(x.module))
77
- .filter(x => !!x);
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(r => r.stdout);
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(r => r.stdout);
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 = (val: number | `${number}:${number}`): [number, number] =>
9
- typeof val === 'number' ?
10
- [val, val] :
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
- val.split(':').map(x => parseInt(x, 10)) as [number, number];
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
- svc: ServiceDescriptor;
40
- constructor(svc: ServiceDescriptor) { this.svc = svc; }
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.svc.port!)[0];
43
+ const port = ports(this.#descriptor.port!)[0];
44
44
  const start = Date.now();
45
- const timeoutMs = TimeUtil.asMillis(full ? this.svc.startupTimeout ?? 5000 : 100);
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>((res, rej) =>
50
- sock.on('connect', res).on('timeout', rej).on('error', rej)
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.svc.ready?.url || !full) {
53
+ if (!this.#descriptor.ready?.url || !full) {
54
54
  return true;
55
55
  } else {
56
- const response = await fetch(this.svc.ready.url, { method: 'GET' });
56
+ const response = await fetch(this.#descriptor.ready.url, { method: 'GET' });
57
57
  const text = await response.text();
58
- if (response.ok && (this.svc.ready.test?.(text) ?? true)) {
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.svc.image]), { catch: true });
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 proc = spawn('docker', ['pull', this.svc.image], { stdio: [0, 'pipe', 'pipe'] });
77
- yield* rl.createInterface(proc.stdout!);
78
- await ExecUtil.getResult(proc);
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.svc.privileged ? ['--privileged'] : [],
87
- '--label', `trv-${this.svc.name}`,
88
- ...Object.entries(this.svc.env ?? {}).flatMap(([k, v]) => ['--env', `${k}=${v}`]),
89
- ...this.svc.port ? ['-p', ports(this.svc.port).join(':')] : [],
90
- ...Object.entries(this.svc.volumes ?? {}).flatMap(([k, v]) => ['--volume', `${k}:${v}`]),
91
- this.svc.image,
92
- ...this.svc.args ?? [],
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.svc.volumes ?? {})) {
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.svc.name}`]))).stdout.trim();
103
+ return (await ExecUtil.getResult(spawn('docker', ['ps', '-q', '--filter', `label=trv-${this.#descriptor.name}`]))).stdout.trim();
104
104
  }
105
105
 
106
- async #killContainer(cid: string): Promise<void> {
107
- await ExecUtil.getResult(spawn('docker', ['kill', cid]));
106
+ async #killContainer(containerId: string): Promise<void> {
107
+ await ExecUtil.getResult(spawn('docker', ['kill', containerId]));
108
108
  }
109
109
 
110
- async * action(op: ServiceAction): AsyncIterable<['success' | 'failure' | 'message', string]> {
110
+ async * action(operation: ServiceAction): AsyncIterable<['success' | 'failure' | 'message', string]> {
111
111
  try {
112
- const cid = await this.#getContainerId();
113
- const port = this.svc.port ? ports(this.svc.port)[0] : 0;
114
- const running = !!cid && (!port || await this.#isRunning());
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 && !cid) { // We don't own
117
- return yield [op === 'status' ? 'message' : 'failure', 'Running but not managed'];
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 (op === 'status') {
121
- return yield !cid ? ['message', 'Not running'] : ['success', `Running ${cid}`];
122
- } else if (op === 'start' && running) {
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 (op === 'stop' && !running) {
124
+ } else if (operation === 'stop' && !running) {
125
125
  return yield ['message', 'Skipping, already stopped'];
126
126
  }
127
127
 
128
- if (running && (op === 'restart' || op === 'stop')) {
128
+ if (running && (operation === 'restart' || operation === 'stop')) {
129
129
  yield ['message', 'Stopping'];
130
- await this.#killContainer(cid);
130
+ await this.#killContainer(containerId);
131
131
  yield ['success', 'Stopped'];
132
132
  }
133
133
 
134
- if (op === 'restart' || op === 'start') {
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.svc.ready?.url ?? 'container'}...`];
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
- canRestart?: boolean;
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 = (k: string): boolean => IPC_ALLOWED_ENV.has(k) || (!IPC_INVALID_ENV.has(k) && !/^(npm_|GTK|GDK|TRV|NODE|GIT|TERM_)/.test(k) && !/VSCODE/.test(k));
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 runWithRestart<T extends CliCommandShapeFields & CliCommandShape>(cmd: T, ipc?: boolean): Promise<unknown> | undefined {
30
- if (ipc && process.connected) {
31
- process.once('disconnect', () => process.exit());
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
- if (Env.TRV_CAN_RESTART.isFalse || !(cmd.canRestart ?? !Runtime.production)) {
34
- Env.TRV_CAN_RESTART.clear();
35
- return;
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
- return ExecUtil.withRestart(() => spawn(process.argv0, process.argv.slice(1), {
38
- env: {
39
- ...process.env,
40
- ...Env.TRV_CAN_RESTART.export(false)
41
- },
42
- stdio: [0, 1, 2, ipc ? 'ipc' : undefined]
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.val!).catch(() => ({ ok: false }));
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 req = {
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', req);
130
+ console.log('Triggering IPC request', request);
73
131
 
74
- Object.entries(process.env).forEach(([k, v]) => validEnv(k) && (env[k] = v!));
75
- const sent = await fetch(Env.TRV_CLI_IPC.val!, { method: 'POST', body: JSON.stringify(req) });
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 (cmd.debugIpc ?? !Runtime.production) && this.triggerIpc('run', cmd);
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(r => process[channel].write(typeof data === 'string' ? data : JSON.stringify(data, null, 2), () => r()));
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(x => !resolved.find(r => r.command === x));
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(x => CliSchemaExportUtil.exportSchema(x.config.cls));
41
+ .map(result => CliSchemaExportUtil.exportSchema(result.config.cls));
42
42
 
43
43
  await CliUtil.writeAndEnsureComplete(output);
44
44
  }
@@ -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 (err) {
29
- result = err;
28
+ } catch (error) {
29
+ result = error;
30
30
  process.exitCode = Math.max(process.exitCode ? +process.exitCode : 1, 1);
31
31
  }
32
32