@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/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 { tryClientMode } from "./client-mode.js";
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
- * A signal that aborts on the first Ctrl+C while a command runs in this process (roadmap R14):
195
- * the procedure gets the abort (a shell command stops its process tree). A second Ctrl+C exits
196
- * at once. `release()` removes the listener when the command ends.
197
- *
198
- * @deprecated The MINOR of CLI-26 removes this handler: the runner of `@mark1russell7/service`
199
- * owns the signals of each command (decision D17). The exit codes then change: 130 after the
200
- * first Ctrl-C (now 1, when the aborted command fails), and 1 after a second signal (now 130).
201
- */
202
- function interruptSignal(): { signal: AbortSignal; release: () => void } {
203
- const controller = new AbortController();
204
- const onInterrupt = (): void => {
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
- const interrupt = interruptSignal();
347
+ let runtime: CliContext;
323
348
  try {
324
- const { client } = await local();
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: ${error instanceof Error ? error.message : String(error)}`);
332
- } finally {
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
- fail(`mark ${path.join(" ")} failed: ${failure}`);
446
+ failCommand(`mark ${path.join(" ")} failed: ${failure}`);
404
447
  }
405
448
  };
406
449
 
407
- // Try client mode: connect to running server if available (unless --local)
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 clientResult = await tryClientMode(path, validated, printItem, ctx.fsRoot);
410
- if (clientResult !== null) {
411
- if (clientResult.success) {
412
- if (!clientResult.printed) {
413
- printItem(clientResult.result);
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
- reportFailure();
416
- return;
417
- }
418
- // The server ran (or started to run) the command and it failed. Running it again
419
- // locally could repeat its side effects, so report the error instead.
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 command runs in this process: the procedures load now (a ManifestContext)
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: ${error instanceof Error ? error.message : String(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
- try {
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
- if (outputFormat === "streaming") {
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
- // A streaming procedure gives many items: print each one when it arrives. A
453
- // request/response procedure gives one item, so its output does not change.
454
- let items = 0;
455
- const interrupt = interruptSignal();
456
- try {
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
- if (spinner) {
467
- if (failure === undefined) {
468
- spinner.succeed(`${path.join(" ")} complete`);
469
- } else {
470
- spinner.fail(`${path.join(" ")} failed`);
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
- reportFailure();
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: ${error instanceof Error ? error.message : String(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 };
@@ -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
- * Falls back to local execution if no server is available.
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
- * Try to execute command via running server. The input is the procedure input that the CLI
20
- * parsed and validated already.
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 tryClientMode(
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: lockfile.endpoint,
55
- defaultHeaders: { Authorization: `Bearer ${lockfile.token}` },
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
- // Execute remotely. From here on, do not fall back to local execution: the server may
69
- // have run (part of) the command, and running it again would repeat its side effects.
70
- try {
71
- // Each item of the response as it arrives (a streaming procedure gives many). The result
72
- // is the last item.
73
- let last: { value: unknown } | undefined;
74
- for await (const item of client.stream(method, input)) {
75
- last = { value: item };
76
- onItem?.(item);
77
- }
78
- if (!last) {
79
- throw new Error("No response received from the CLI server");
80
- }
81
- return {
82
- success: true,
83
- result: last.value,
84
- printed: onItem !== undefined,
85
- };
86
- } catch (error) {
87
- return {
88
- success: false,
89
- error: error instanceof Error ? error.message : String(error),
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
  }