@procedures/mark 0.23.0 → 0.24.0
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/CHANGELOG.md +55 -0
- package/README.md +49 -12
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +4 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +37 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +157 -78
- package/dist/cli.js.map +1 -1
- package/dist/client-mode.d.ts +26 -4
- package/dist/client-mode.d.ts.map +1 -1
- package/dist/client-mode.js +54 -33
- package/dist/client-mode.js.map +1 -1
- package/dist/command.d.ts +115 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +110 -0
- package/dist/command.js.map +1 -0
- package/dist/manifest.json +2 -2
- package/dist/server-mode.d.ts +2 -1
- package/dist/server-mode.d.ts.map +1 -1
- package/dist/server-mode.js +5 -4
- package/dist/server-mode.js.map +1 -1
- package/package.json +50 -49
- package/src/bin.ts +4 -1
- package/src/cli.ts +183 -79
- package/src/client-mode.ts +73 -38
- package/src/command.ts +181 -0
- package/src/server-mode.ts +5 -4
package/src/cli.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { print } from "./print.js";
|
|
10
|
-
import type { Method, AnyProcedure } from "@procedures/client";
|
|
10
|
+
import type { Method, AnyProcedure, ProcedureRegistry } from "@procedures/client";
|
|
11
|
+
import type { RequestEnd, RunOptions } from "@mark1russell7/service";
|
|
11
12
|
import { readFileSync, realpathSync } from "node:fs";
|
|
12
13
|
import { dirname, resolve } from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -17,7 +18,8 @@ import { askMissing, canAsk, missingFields, processTerminal, terminalAsker, type
|
|
|
17
18
|
import { formatOutput, type OutputFormat, type Print } from "./format.js";
|
|
18
19
|
import { loadEcosystemRegistry } from "./ecosystem.js";
|
|
19
20
|
import { startServerMode, parsePort, parseTransport } from "./server-mode.js";
|
|
20
|
-
import {
|
|
21
|
+
import { connectCliServer } from "./client-mode.js";
|
|
22
|
+
import type { ColdCommand, CommandRun } from "./command.js";
|
|
21
23
|
import { startRepl } from "./repl.js";
|
|
22
24
|
import { findChildren, findProcedure, parseCommandLine, parseLeadingGlobals } from "./args.js";
|
|
23
25
|
import { failureOf } from "./failure.js";
|
|
@@ -165,8 +167,17 @@ async function eventPatterns(value: string | boolean | undefined): Promise<strin
|
|
|
165
167
|
* CLI execution context, created once and reused
|
|
166
168
|
*/
|
|
167
169
|
export interface CliContext {
|
|
170
|
+
/**
|
|
171
|
+
* A client of the registry in this process, for a program. `executeArgs()` does not use it: a
|
|
172
|
+
* command in this process runs on the procedures host of the cold mode (CLI-26).
|
|
173
|
+
*/
|
|
168
174
|
client: InstanceType<typeof import("@procedures/client").Client>;
|
|
169
175
|
procedures: AnyProcedure[];
|
|
176
|
+
/**
|
|
177
|
+
* The registry of the procedures: the registry of the host of the cold mode. Without it, the
|
|
178
|
+
* host gets a registry of `procedures`.
|
|
179
|
+
*/
|
|
180
|
+
registry?: ProcedureRegistry | undefined;
|
|
170
181
|
verbose: boolean;
|
|
171
182
|
/** The absolute path of the root of the `fs.*` procedures (`--fs-root`), or undefined. */
|
|
172
183
|
fsRoot?: string | undefined;
|
|
@@ -184,29 +195,36 @@ export interface ManifestContext {
|
|
|
184
195
|
load: () => Promise<CliContext>;
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
/** The terminal and the asker of the questions for missing arguments (tests give fakes) */
|
|
198
|
+
/** The terminal and the asker of the questions for missing arguments (tests give fakes), and the options of `run()` */
|
|
188
199
|
export interface ExecuteOptions {
|
|
189
200
|
terminal?: Terminal;
|
|
190
201
|
ask?: Asker;
|
|
202
|
+
/**
|
|
203
|
+
* The options of `run()` of `@mark1russell7/service` for the service of each command (CLI-26):
|
|
204
|
+
* the process port and the clock. A test gives `fakeProcess()` of `@mark1russell7/service/testing`.
|
|
205
|
+
*/
|
|
206
|
+
run?: RunOptions | undefined;
|
|
207
|
+
/**
|
|
208
|
+
* True when this run owns its process, as the bin of `mark` does. Without `run`, each command
|
|
209
|
+
* then runs on the process of Node: the runner owns the signals, and it ends the process with
|
|
210
|
+
* the exit code of the command. False (the default): the port is `embeddedProcess()`. A command
|
|
211
|
+
* then installs no signal handler, does not end the process, and records its exit code in `process.exitCode`.
|
|
212
|
+
*/
|
|
213
|
+
ownsProcess?: boolean | undefined;
|
|
191
214
|
}
|
|
192
215
|
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (controller.signal.aborted) process.exit(130);
|
|
206
|
-
controller.abort();
|
|
207
|
-
};
|
|
208
|
-
process.on("SIGINT", onInterrupt);
|
|
209
|
-
return { signal: controller.signal, release: () => process.off("SIGINT", onInterrupt) };
|
|
216
|
+
/** The registry of the host of a context: its registry, or a registry of its procedures. */
|
|
217
|
+
async function registryOf(ctx: CliContext): Promise<ProcedureRegistry> {
|
|
218
|
+
if (ctx.registry) return ctx.registry;
|
|
219
|
+
const { ProcedureRegistry } = await import("@procedures/client");
|
|
220
|
+
const registry = new ProcedureRegistry();
|
|
221
|
+
for (const procedure of ctx.procedures) registry.register(procedure);
|
|
222
|
+
return registry;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The text of an error. */
|
|
226
|
+
function messageOf(error: unknown): string {
|
|
227
|
+
return error instanceof Error ? error.message : String(error);
|
|
210
228
|
}
|
|
211
229
|
|
|
212
230
|
/**
|
|
@@ -229,7 +247,7 @@ export async function initCli(verbose = false, fsRoot?: string): Promise<CliCont
|
|
|
229
247
|
|
|
230
248
|
const procedures = registry.getAll();
|
|
231
249
|
|
|
232
|
-
return { client, procedures, verbose, ...(ecosystem.fsRoot !== undefined ? { fsRoot: ecosystem.fsRoot } : {}) };
|
|
250
|
+
return { client, procedures, registry, verbose, ...(ecosystem.fsRoot !== undefined ? { fsRoot: ecosystem.fsRoot } : {}) };
|
|
233
251
|
}
|
|
234
252
|
|
|
235
253
|
/**
|
|
@@ -242,6 +260,12 @@ export async function initCli(verbose = false, fsRoot?: string): Promise<CliCont
|
|
|
242
260
|
*
|
|
243
261
|
* With a ManifestContext, the help and a call to the CLI server need no procedures: they load
|
|
244
262
|
* only when the command runs in this process (roadmap 1.3).
|
|
263
|
+
*
|
|
264
|
+
* A command that runs a procedure starts under `run()` of `@mark1russell7/service` (CLI-26). On
|
|
265
|
+
* the CLI server (the warm mode), the service has only the command. In this process (the cold
|
|
266
|
+
* mode), the service also has the procedures host of the CLI server, with the same registry and
|
|
267
|
+
* policy. The runner owns the signals: a first Ctrl-C aborts the call. `options.run` gives the
|
|
268
|
+
* process port. The help and the problems of the command line start no service.
|
|
245
269
|
*/
|
|
246
270
|
export async function executeArgs(
|
|
247
271
|
argv: string[],
|
|
@@ -306,7 +330,8 @@ export async function executeArgs(
|
|
|
306
330
|
return;
|
|
307
331
|
}
|
|
308
332
|
|
|
309
|
-
// Handle --json flag for raw procedure reference execution
|
|
333
|
+
// Handle --json flag for raw procedure reference execution. The reference runs in this process,
|
|
334
|
+
// as the one request of the cold mode under run() (CLI-26): a Ctrl-C aborts its signal.
|
|
310
335
|
if (globals.json !== undefined) {
|
|
311
336
|
let parsedJson: unknown;
|
|
312
337
|
try {
|
|
@@ -319,19 +344,32 @@ export async function executeArgs(
|
|
|
319
344
|
fail('--json needs a procedure reference: an object with "$proc".');
|
|
320
345
|
return;
|
|
321
346
|
}
|
|
322
|
-
|
|
347
|
+
let runtime: CliContext;
|
|
323
348
|
try {
|
|
324
|
-
|
|
325
|
-
const result = await client.exec(parsedJson as Parameters<typeof client.exec>[0], undefined, { signal: interrupt.signal });
|
|
326
|
-
formatOutput(print as unknown as Print, result, "json");
|
|
327
|
-
if (failureOf(result)) {
|
|
328
|
-
process.exitCode = 1;
|
|
329
|
-
}
|
|
349
|
+
runtime = await local();
|
|
330
350
|
} catch (error) {
|
|
331
|
-
fail(`Error: ${
|
|
332
|
-
|
|
333
|
-
interrupt.release();
|
|
351
|
+
fail(`Error: ${messageOf(error)}`);
|
|
352
|
+
return;
|
|
334
353
|
}
|
|
354
|
+
const cold = await startCold(runtime, options);
|
|
355
|
+
if (!cold) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
let failed = false;
|
|
359
|
+
const end = await cold.command.request(async (signal) => {
|
|
360
|
+
try {
|
|
361
|
+
const result = await cold.client.exec(parsedJson as Parameters<typeof cold.client.exec>[0], undefined, { signal });
|
|
362
|
+
formatOutput(print as unknown as Print, result, "json");
|
|
363
|
+
if (failureOf(result)) {
|
|
364
|
+
failed = true;
|
|
365
|
+
process.exitCode = 1;
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
failed = true;
|
|
369
|
+
fail(`Error: ${messageOf(error)}`);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
await endCommand(cold.command, end, failed);
|
|
335
373
|
return;
|
|
336
374
|
}
|
|
337
375
|
|
|
@@ -394,40 +432,53 @@ export async function executeArgs(
|
|
|
394
432
|
|
|
395
433
|
// Each item prints when it arrives. A failed item makes the command fail.
|
|
396
434
|
let failure: string | undefined;
|
|
435
|
+
let failed = false;
|
|
436
|
+
const failCommand = (message: string): void => {
|
|
437
|
+
failed = true;
|
|
438
|
+
fail(message);
|
|
439
|
+
};
|
|
397
440
|
const printItem = (item: unknown): void => {
|
|
398
441
|
formatOutput(print as unknown as Print, item, outputFormat);
|
|
399
442
|
failure ??= failureOf(item);
|
|
400
443
|
};
|
|
401
444
|
const reportFailure = (): void => {
|
|
402
445
|
if (failure !== undefined) {
|
|
403
|
-
|
|
446
|
+
failCommand(`mark ${path.join(" ")} failed: ${failure}`);
|
|
404
447
|
}
|
|
405
448
|
};
|
|
406
449
|
|
|
407
|
-
//
|
|
450
|
+
// The warm mode: a running CLI server takes the command (unless --local). The command runs under
|
|
451
|
+
// run() too, so a Ctrl-C aborts the call on the server (CLI-26).
|
|
408
452
|
if (!globals.local) {
|
|
409
|
-
const
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
453
|
+
const server = await connectCliServer(path, ctx.fsRoot);
|
|
454
|
+
if (server !== null) {
|
|
455
|
+
const { startWarmCommand } = await import("./command.js");
|
|
456
|
+
const command = await startWarmCommand(options);
|
|
457
|
+
const end = await command.request(async (signal) => {
|
|
458
|
+
const clientResult = await server.call(validated, printItem, signal);
|
|
459
|
+
if (clientResult.success) {
|
|
460
|
+
if (!clientResult.printed) {
|
|
461
|
+
printItem(clientResult.result);
|
|
462
|
+
}
|
|
463
|
+
reportFailure();
|
|
464
|
+
return;
|
|
414
465
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
fail(clientResult.error ?? "The command failed on the CLI server.");
|
|
466
|
+
// The server ran (or started to run) the command and it failed. Running it again
|
|
467
|
+
// locally could repeat its side effects, so report the error instead.
|
|
468
|
+
failCommand(clientResult.error ?? "The command failed on the CLI server.");
|
|
469
|
+
});
|
|
470
|
+
await endCommand(command, end, failed);
|
|
421
471
|
return;
|
|
422
472
|
}
|
|
423
473
|
}
|
|
424
474
|
|
|
425
|
-
// The
|
|
475
|
+
// The cold mode: the procedures load now (a ManifestContext). The command runs on the procedures
|
|
476
|
+
// host of the CLI server, in this process, under run() (CLI-26).
|
|
426
477
|
let runtime: CliContext;
|
|
427
478
|
try {
|
|
428
479
|
runtime = await local();
|
|
429
480
|
} catch (error) {
|
|
430
|
-
fail(`Error: ${
|
|
481
|
+
fail(`Error: ${messageOf(error)}`);
|
|
431
482
|
return;
|
|
432
483
|
}
|
|
433
484
|
const target = findProcedure(runtime.procedures, path);
|
|
@@ -439,40 +490,88 @@ export async function executeArgs(
|
|
|
439
490
|
return;
|
|
440
491
|
}
|
|
441
492
|
|
|
442
|
-
|
|
493
|
+
const cold = await startCold(runtime, options);
|
|
494
|
+
if (!cold) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const end = await cold.command.request(async (signal) => {
|
|
443
498
|
let spinner: ReturnType<typeof print.spin> | undefined;
|
|
499
|
+
try {
|
|
500
|
+
if (outputFormat === "streaming") {
|
|
501
|
+
spinner = print.spin(`Running ${path.join(" ")}...`);
|
|
502
|
+
}
|
|
444
503
|
|
|
445
|
-
|
|
446
|
-
spinner = print.spin(`Running ${path.join(" ")}...`);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const method = await pathToMethod(path);
|
|
450
|
-
const { client } = runtime;
|
|
504
|
+
const method = await pathToMethod(path);
|
|
451
505
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
for await (const item of client.stream(method, validated, { signal: interrupt.signal })) {
|
|
506
|
+
// A streaming procedure gives many items: print each one when it arrives. A
|
|
507
|
+
// request/response procedure gives one item, so its output does not change. The signal of
|
|
508
|
+
// the request goes with the call: the drain of a stop aborts it (CLI-26).
|
|
509
|
+
let items = 0;
|
|
510
|
+
for await (const item of cold.client.stream(method, validated, { signal })) {
|
|
458
511
|
if (spinner && items === 0) spinner.stop();
|
|
459
512
|
items++;
|
|
460
513
|
printItem(item);
|
|
461
514
|
}
|
|
462
|
-
} finally {
|
|
463
|
-
interrupt.release();
|
|
464
|
-
}
|
|
465
515
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
516
|
+
if (spinner) {
|
|
517
|
+
if (failure === undefined) {
|
|
518
|
+
spinner.succeed(`${path.join(" ")} complete`);
|
|
519
|
+
} else {
|
|
520
|
+
spinner.fail(`${path.join(" ")} failed`);
|
|
521
|
+
}
|
|
471
522
|
}
|
|
523
|
+
reportFailure();
|
|
524
|
+
} catch (error) {
|
|
525
|
+
// Before, a spinner kept its timer after an error
|
|
526
|
+
spinner?.stop();
|
|
527
|
+
failCommand(`Error: ${messageOf(error)}`);
|
|
472
528
|
}
|
|
473
|
-
|
|
529
|
+
});
|
|
530
|
+
await endCommand(cold.command, end, failed);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* This function starts the cold mode of a command: the procedures host of the CLI server, in this
|
|
535
|
+
* process, under `run()` (CLI-26). After a startup that failed, it prints the errors, waits for the
|
|
536
|
+
* exit of the lifecycle and gives undefined.
|
|
537
|
+
*/
|
|
538
|
+
async function startCold(runtime: CliContext, options: ExecuteOptions): Promise<ColdCommand | undefined> {
|
|
539
|
+
const { startColdCommand } = await import("./command.js");
|
|
540
|
+
let started: Awaited<ReturnType<typeof startColdCommand>>;
|
|
541
|
+
try {
|
|
542
|
+
started = await startColdCommand(await registryOf(runtime), options);
|
|
474
543
|
} catch (error) {
|
|
475
|
-
fail(`Error: ${
|
|
544
|
+
fail(`Error: ${messageOf(error)}`);
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
if ("errors" in started) {
|
|
548
|
+
print.error("The procedures host of the command did not start:");
|
|
549
|
+
for (const error of started.errors) print.error(` ${error}`);
|
|
550
|
+
process.exitCode = await started.life.done;
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
return started;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* This function ends the service of a command with the exit code of the command: 1 when it
|
|
558
|
+
* failed, else 0 (CLI-26). A signal that started the stop first keeps its exit code. With the
|
|
559
|
+
* process of Node, the runner then ends the process. With another port, the exit code of the
|
|
560
|
+
* lifecycle goes to `process.exitCode` when it is not 0.
|
|
561
|
+
*/
|
|
562
|
+
async function endCommand(command: CommandRun, end: RequestEnd, failed: boolean): Promise<void> {
|
|
563
|
+
let code = failed ? 1 : 0;
|
|
564
|
+
if (end.kind === "failed") {
|
|
565
|
+
fail(`Error: ${messageOf(end.error)}`);
|
|
566
|
+
code = 1;
|
|
567
|
+
} else if (end.kind === "refused") {
|
|
568
|
+
// The stop started before the command: a signal while the service started
|
|
569
|
+
fail(`The command did not run: ${end.cause}`);
|
|
570
|
+
code = 1;
|
|
571
|
+
}
|
|
572
|
+
const exitCode = await command.end(code);
|
|
573
|
+
if (exitCode !== 0) {
|
|
574
|
+
process.exitCode = exitCode;
|
|
476
575
|
}
|
|
477
576
|
}
|
|
478
577
|
|
|
@@ -559,10 +658,14 @@ async function startMcp(args: string[], fsRoot?: string): Promise<void> {
|
|
|
559
658
|
await runDevToolsServer(config);
|
|
560
659
|
}
|
|
561
660
|
|
|
661
|
+
/** The options of `run()` and `main()`: the process of the service of each command (CLI-26). */
|
|
662
|
+
export type RunCliOptions = Pick<ExecuteOptions, "run" | "ownsProcess">;
|
|
663
|
+
|
|
562
664
|
/**
|
|
563
|
-
* Run the CLI
|
|
665
|
+
* Run the CLI. By default, the caller owns the process: a command records its exit code in
|
|
666
|
+
* `process.exitCode`, and the process does not end. The bin gives `ownsProcess: true`.
|
|
564
667
|
*/
|
|
565
|
-
async function run(argv: string[]): Promise<void> {
|
|
668
|
+
async function run(argv: string[], options: RunCliOptions = {}): Promise<void> {
|
|
566
669
|
// The global flags before the command: read without loading the procedures
|
|
567
670
|
const leading = parseLeadingGlobals(argv);
|
|
568
671
|
if (leading.errors.length > 0) {
|
|
@@ -649,7 +752,7 @@ async function run(argv: string[]): Promise<void> {
|
|
|
649
752
|
// server do not load them (roadmap 1.3)
|
|
650
753
|
const manifest = readFreshManifest();
|
|
651
754
|
if (manifest) {
|
|
652
|
-
await executeArgs(argv, { commands: manifest.commands, verbose, fsRoot, load: () => initCli(verbose, fsRoot) });
|
|
755
|
+
await executeArgs(argv, { commands: manifest.commands, verbose, fsRoot, load: () => initCli(verbose, fsRoot) }, options);
|
|
653
756
|
return;
|
|
654
757
|
}
|
|
655
758
|
|
|
@@ -663,17 +766,17 @@ async function run(argv: string[]): Promise<void> {
|
|
|
663
766
|
print.warning(`mark: the manifest was not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
664
767
|
}
|
|
665
768
|
}
|
|
666
|
-
await executeArgs(argv, ctx);
|
|
769
|
+
await executeArgs(argv, ctx, options);
|
|
667
770
|
}
|
|
668
771
|
|
|
669
772
|
/**
|
|
670
773
|
* Run the CLI and report an error that escapes it as one line, with exit code 1. With
|
|
671
774
|
* `--verbose`, the stack follows. (Before, an error such as an invalid port showed only an
|
|
672
|
-
* unhandled-rejection stack: deep dive CLI-16.)
|
|
775
|
+
* unhandled-rejection stack: deep dive CLI-16.) The bin gives `ownsProcess: true` (CLI-26).
|
|
673
776
|
*/
|
|
674
|
-
export async function main(argv: readonly string[]): Promise<void> {
|
|
777
|
+
export async function main(argv: readonly string[], options: RunCliOptions = {}): Promise<void> {
|
|
675
778
|
try {
|
|
676
|
-
await run([...argv]);
|
|
779
|
+
await run([...argv], options);
|
|
677
780
|
} catch (error) {
|
|
678
781
|
print.error(error instanceof Error ? error.message : String(error));
|
|
679
782
|
if ((argv.includes("--verbose") || argv.includes("-V")) && error instanceof Error && error.stack) {
|
|
@@ -702,9 +805,10 @@ function isEntryPoint(): boolean {
|
|
|
702
805
|
}
|
|
703
806
|
}
|
|
704
807
|
|
|
705
|
-
// Importing @procedures/mark does not run the CLI (deep dive CLI-17)
|
|
808
|
+
// Importing @procedures/mark does not run the CLI (deep dive CLI-17). The entry owns its process:
|
|
809
|
+
// the runner of each command owns the signals and the exit (CLI-26).
|
|
706
810
|
if (isEntryPoint()) {
|
|
707
|
-
await main(process.argv.slice(2));
|
|
811
|
+
await main(process.argv.slice(2), { ownsProcess: true });
|
|
708
812
|
}
|
|
709
813
|
|
|
710
814
|
export { run };
|
package/src/client-mode.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI Client Mode
|
|
3
3
|
*
|
|
4
|
-
* Connects to a running CLI server and executes commands remotely.
|
|
5
|
-
*
|
|
4
|
+
* Connects to a running CLI server and executes commands remotely (the warm mode).
|
|
5
|
+
* Without a server, the command runs in this process (the cold mode, `command.ts`).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { currentBuild, findServer } from "./lockfile.js";
|
|
8
|
+
import { currentBuild, findServer, type LockfileData } from "./lockfile.js";
|
|
9
9
|
|
|
10
10
|
interface ClientModeResult {
|
|
11
11
|
success: boolean;
|
|
@@ -15,17 +15,20 @@ interface ClientModeResult {
|
|
|
15
15
|
printed?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** A connection to the CLI server for one command. */
|
|
19
|
+
export interface CliServerConnection {
|
|
20
|
+
/**
|
|
21
|
+
* This method runs the command on the server. The input is the procedure input that the CLI
|
|
22
|
+
* parsed and validated already. The signal stops the call: the server gets the abort (CLI-26).
|
|
23
|
+
*/
|
|
24
|
+
call(input: unknown, onItem?: (item: unknown) => void, signal?: AbortSignal): Promise<ClientModeResult>;
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Returns null if no server available (should fall back to local)
|
|
28
|
+
* This function connects to the CLI server that a command of this run can use. It gives null when
|
|
29
|
+
* no such server runs: then the command runs in this process.
|
|
22
30
|
*/
|
|
23
|
-
export async function
|
|
24
|
-
path: string[],
|
|
25
|
-
input: unknown,
|
|
26
|
-
onItem?: (item: unknown) => void,
|
|
27
|
-
fsRoot?: string
|
|
28
|
-
): Promise<ClientModeResult | null> {
|
|
31
|
+
export async function connectCliServer(path: string[], fsRoot?: string): Promise<CliServerConnection | null> {
|
|
29
32
|
// A running server of this build, started in this folder, that answers as the peer of its
|
|
30
33
|
// lockfile and has a token. Otherwise the CLI runs the command itself. (Before, any server
|
|
31
34
|
// with a live PID was used: relative paths resolved in the server's folder, an old build ran
|
|
@@ -41,7 +44,18 @@ export async function tryClientMode(
|
|
|
41
44
|
if (!lockfile?.token || lockfile.transport === "websocket") {
|
|
42
45
|
return null;
|
|
43
46
|
}
|
|
47
|
+
return cliServerConnection({ endpoint: lockfile.endpoint, token: lockfile.token }, path);
|
|
48
|
+
}
|
|
44
49
|
|
|
50
|
+
/**
|
|
51
|
+
* This function gives a connection to the CLI server of a lockfile for the command of a path: the
|
|
52
|
+
* HTTP endpoint and the token. It gives null when the client cannot load: then nothing went to
|
|
53
|
+
* the server, and the command runs in this process.
|
|
54
|
+
*/
|
|
55
|
+
export async function cliServerConnection(
|
|
56
|
+
server: Pick<LockfileData, "endpoint"> & { readonly token: string },
|
|
57
|
+
path: string[]
|
|
58
|
+
): Promise<CliServerConnection | null> {
|
|
45
59
|
let client: InstanceType<typeof import("@procedures/client").Client>;
|
|
46
60
|
let method: { service: string; operation: string };
|
|
47
61
|
try {
|
|
@@ -51,8 +65,8 @@ export async function tryClientMode(
|
|
|
51
65
|
|
|
52
66
|
// Connect to server
|
|
53
67
|
const transport = new HttpTransport({
|
|
54
|
-
baseUrl:
|
|
55
|
-
defaultHeaders: { Authorization: `Bearer ${
|
|
68
|
+
baseUrl: server.endpoint,
|
|
69
|
+
defaultHeaders: { Authorization: `Bearer ${server.token}` },
|
|
56
70
|
});
|
|
57
71
|
client = new Client({ transport });
|
|
58
72
|
|
|
@@ -65,28 +79,49 @@ export async function tryClientMode(
|
|
|
65
79
|
return null;
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
return {
|
|
83
|
+
async call(input, onItem, signal) {
|
|
84
|
+
// Execute remotely. From here on, do not fall back to local execution: the server may
|
|
85
|
+
// have run (part of) the command, and running it again would repeat its side effects.
|
|
86
|
+
try {
|
|
87
|
+
// Each item of the response as it arrives (a streaming procedure gives many). The result
|
|
88
|
+
// is the last item. The signal aborts the request, and the server aborts the call (before
|
|
89
|
+
// CLI-26, the call had no signal: a Ctrl-C ended this process, and the server continued).
|
|
90
|
+
let last: { value: unknown } | undefined;
|
|
91
|
+
for await (const item of client.stream(method, input, signal ? { signal } : undefined)) {
|
|
92
|
+
last = { value: item };
|
|
93
|
+
onItem?.(item);
|
|
94
|
+
}
|
|
95
|
+
if (!last) {
|
|
96
|
+
throw new Error("No response received from the CLI server");
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
result: last.value,
|
|
101
|
+
printed: onItem !== undefined,
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
error: error instanceof Error ? error.message : String(error),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Try to execute command via running server. The input is the procedure input that the CLI
|
|
115
|
+
* parsed and validated already. The signal stops the call on the server.
|
|
116
|
+
* Returns null if no server available (should fall back to local)
|
|
117
|
+
*/
|
|
118
|
+
export async function tryClientMode(
|
|
119
|
+
path: string[],
|
|
120
|
+
input: unknown,
|
|
121
|
+
onItem?: (item: unknown) => void,
|
|
122
|
+
fsRoot?: string,
|
|
123
|
+
signal?: AbortSignal
|
|
124
|
+
): Promise<ClientModeResult | null> {
|
|
125
|
+
const connection = await connectCliServer(path, fsRoot);
|
|
126
|
+
return connection === null ? null : connection.call(input, onItem, signal);
|
|
92
127
|
}
|