@relayflows/sdk 2.0.18 → 2.0.19
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/dist/cli/build.d.ts +9 -0
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +25 -6
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/cloud-run.d.ts +7 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +12 -14
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/cloud-sync.d.ts +8 -1
- package/dist/cli/cloud-sync.d.ts.map +1 -1
- package/dist/cli/cloud-sync.js +79 -8
- package/dist/cli/cloud-sync.js.map +1 -1
- package/dist/cli/deploy.d.ts +6 -0
- package/dist/cli/deploy.d.ts.map +1 -1
- package/dist/cli/deploy.js +29 -4
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/serve-webhook.d.ts +7 -1
- package/dist/cli/serve-webhook.d.ts.map +1 -1
- package/dist/cli/serve-webhook.js +19 -9
- package/dist/cli/serve-webhook.js.map +1 -1
- package/dist/cli-commands.d.ts +398 -0
- package/dist/cli-commands.d.ts.map +1 -0
- package/dist/cli-commands.js +254 -0
- package/dist/cli-commands.js.map +1 -0
- package/dist/cli-watch.d.ts +3 -1
- package/dist/cli-watch.d.ts.map +1 -1
- package/dist/cli-watch.js +4 -10
- package/dist/cli-watch.js.map +1 -1
- package/dist/cli.d.ts +127 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +81 -48
- package/dist/cli.js.map +1 -1
- package/dist/cloud-sync.d.ts +85 -2
- package/dist/cloud-sync.d.ts.map +1 -1
- package/dist/cloud-sync.js +123 -10
- package/dist/cloud-sync.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/relay-cli.d.ts +50 -0
- package/dist/relay-cli.d.ts.map +1 -0
- package/dist/relay-cli.js +64 -0
- package/dist/relay-cli.js.map +1 -0
- package/package.json +7 -2
- package/src/cli/build.ts +20 -6
- package/src/cli/cloud-run.ts +11 -12
- package/src/cli/cloud-sync.ts +85 -8
- package/src/cli/deploy.ts +27 -5
- package/src/cli/serve-webhook.ts +15 -8
- package/src/cli-commands.ts +339 -0
- package/src/cli-watch.ts +8 -9
- package/src/cli.ts +103 -44
- package/src/cloud-sync.ts +164 -11
- package/src/index.ts +4 -2
- package/src/relay-cli.ts +117 -0
package/src/cloud-sync.ts
CHANGED
|
@@ -272,37 +272,188 @@ export interface CloudPatch {
|
|
|
272
272
|
hasChanges: boolean;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
/**
|
|
276
|
-
export
|
|
275
|
+
/** One entry of a multi-path run's patch map: the mounted path's name and its diff. */
|
|
276
|
+
export interface CloudPathPatch extends CloudPatch {
|
|
277
|
+
name: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* What `/patch` answered, in both shapes the endpoint can produce.
|
|
282
|
+
*
|
|
283
|
+
* A run that declared mounted `paths` gets one `changes-<name>.patch` per path
|
|
284
|
+
* and the route answers `{ patches: { <name>: { patch, hasChanges } } }`; every
|
|
285
|
+
* other run gets `changes.patch` and the flat `{ patch, hasChanges }`. The
|
|
286
|
+
* multi-path shape is not a v1 relic -- `paths` is orthogonal to
|
|
287
|
+
* `relayflowVersion`, so a v2 run that submits several paths returns it too.
|
|
288
|
+
*/
|
|
289
|
+
export type CloudPatchSet =
|
|
290
|
+
| { kind: 'single'; patch: string; hasChanges: boolean }
|
|
291
|
+
| { kind: 'multi-path'; patches: readonly CloudPathPatch[]; hasChanges: boolean };
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The sandbox's post-run diff, in whichever shape the run produced.
|
|
295
|
+
*
|
|
296
|
+
* Both shapes are modelled rather than one refused at the transport, so a
|
|
297
|
+
* caller can show a multi-path run's patches (`flows sync --dry-run`) before
|
|
298
|
+
* deciding what to do with them. Applying them is still the caller's refusal
|
|
299
|
+
* to make: they target different repositories and no single tree is the right
|
|
300
|
+
* destination.
|
|
301
|
+
*/
|
|
302
|
+
export async function downloadCloudPatchSet(
|
|
303
|
+
runId: string, options: CloudConnectionOptions,
|
|
304
|
+
): Promise<CloudPatchSet> {
|
|
277
305
|
const payload = await cloudRequest(`/api/v1/workflows/runs/${encodeURIComponent(cloudRunId(runId))}/patch`, options);
|
|
278
306
|
if (!isCloudRecord(payload)) throw new CloudFlowError('invalid_response', 'Cloud patch response was not an object.');
|
|
279
307
|
if (isCloudRecord(payload.patches)) {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
308
|
+
const patches: CloudPathPatch[] = [];
|
|
309
|
+
for (const [name, entry] of Object.entries(payload.patches)) {
|
|
310
|
+
if (!isCloudRecord(entry) || typeof entry.patch !== 'string' || typeof entry.hasChanges !== 'boolean') {
|
|
311
|
+
throw new CloudFlowError('invalid_response', `Cloud patch response has an unusable entry for path "${name}".`);
|
|
312
|
+
}
|
|
313
|
+
patches.push({ name, patch: entry.patch, hasChanges: entry.hasChanges });
|
|
314
|
+
}
|
|
315
|
+
return { kind: 'multi-path', patches, hasChanges: patches.some(entry => entry.hasChanges && entry.patch.trim() !== '') };
|
|
283
316
|
}
|
|
284
317
|
if (typeof payload.patch !== 'string' || typeof payload.hasChanges !== 'boolean') {
|
|
285
318
|
throw new CloudFlowError('invalid_response', 'Cloud patch response is missing patch or hasChanges.');
|
|
286
319
|
}
|
|
287
|
-
return { patch: payload.patch, hasChanges: payload.hasChanges };
|
|
320
|
+
return { kind: 'single', patch: payload.patch, hasChanges: payload.hasChanges };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The sandbox's post-run diff. Multi-path runs carry several patches and are refused here. */
|
|
324
|
+
export async function downloadCloudPatch(runId: string, options: CloudConnectionOptions): Promise<CloudPatch> {
|
|
325
|
+
const set = await downloadCloudPatchSet(runId, options);
|
|
326
|
+
if (set.kind === 'multi-path') {
|
|
327
|
+
const names = set.patches.map(entry => entry.name);
|
|
328
|
+
throw new CloudFlowError('sync_unsupported',
|
|
329
|
+
`Run ${runId} produced ${names.length} path-scoped patches (${names.join(', ')}); flows sync applies single-tree runs only.`);
|
|
330
|
+
}
|
|
331
|
+
return { patch: set.patch, hasChanges: set.hasChanges };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Paths a synced patch must never write, the single home for the list.
|
|
336
|
+
*
|
|
337
|
+
* These are the agent runtime's own bookkeeping inside a synced tree: helper
|
|
338
|
+
* binaries staged for the sandbox, the relayfile mount's ACL and state files
|
|
339
|
+
* (including the temporaries a mid-write state leaves behind), trajectory
|
|
340
|
+
* records and workflow context. The sandbox commits its baseline before the
|
|
341
|
+
* run, so every one of them shows up in the post-run diff as a creation or a
|
|
342
|
+
* modification -- applying that diff verbatim drags the run's own plumbing into
|
|
343
|
+
* the user's checkout, where at best it is noise in `git diff` and at worst it
|
|
344
|
+
* overwrites the mount state of the tree being synced into.
|
|
345
|
+
*
|
|
346
|
+
* `git apply --exclude` matches these with wildmatch, anchored at the patch
|
|
347
|
+
* root and with `*` stopping at a `/`: `.agent-bin/**` drops
|
|
348
|
+
* `.agent-bin/nested/tool` but deliberately not `packages/x/.agent-bin/tool`,
|
|
349
|
+
* which belongs to a different tree than the one being synced.
|
|
350
|
+
*/
|
|
351
|
+
export const CLOUD_SYNC_PATCH_EXCLUDES = [
|
|
352
|
+
'.agent-bin/**',
|
|
353
|
+
'.relayfile.acl',
|
|
354
|
+
'.relayfile-mount-state.json',
|
|
355
|
+
'.relayfile-mount-state.json.tmp-*',
|
|
356
|
+
'.trajectories/**',
|
|
357
|
+
'.workflow-context/**',
|
|
358
|
+
] as const;
|
|
359
|
+
|
|
360
|
+
/** The `a/` and `b/` sides of every `diff --git` header, in file order. */
|
|
361
|
+
function patchHeaders(patch: string): { old: string; new: string }[] {
|
|
362
|
+
const headers: { old: string; new: string }[] = [];
|
|
363
|
+
for (const match of patch.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gmu)) {
|
|
364
|
+
headers.push({ old: match[1]!, new: match[2]! });
|
|
365
|
+
}
|
|
366
|
+
return headers;
|
|
288
367
|
}
|
|
289
368
|
|
|
290
369
|
/** Every path a unified diff touches, deletions included, in order of first appearance. */
|
|
291
370
|
export function patchedPaths(patch: string): string[] {
|
|
292
371
|
const paths: string[] = [];
|
|
293
|
-
for (const
|
|
294
|
-
for (const path of [
|
|
372
|
+
for (const header of patchHeaders(patch)) {
|
|
373
|
+
for (const path of [header.old, header.new]) if (!paths.includes(path)) paths.push(path);
|
|
295
374
|
}
|
|
296
375
|
return paths;
|
|
297
376
|
}
|
|
298
377
|
|
|
378
|
+
/**
|
|
379
|
+
* `git apply --exclude`'s wildmatch, as a matcher over a patch's own paths.
|
|
380
|
+
*
|
|
381
|
+
* Anchored at the patch root, `**` crosses `/` and `*`/`?` do not -- the subset
|
|
382
|
+
* of wildmatch {@link CLOUD_SYNC_PATCH_EXCLUDES} uses. Kept honest by a test
|
|
383
|
+
* that runs the same patterns through `git apply --numstat` and requires the
|
|
384
|
+
* two answers to agree, so a divergence fails here rather than silently
|
|
385
|
+
* reporting a path as dropped that git actually wrote.
|
|
386
|
+
*/
|
|
387
|
+
function matchesExclude(path: string, pattern: string): boolean {
|
|
388
|
+
let expression = '^';
|
|
389
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
390
|
+
const character = pattern[index]!;
|
|
391
|
+
if (character === '*') {
|
|
392
|
+
if (pattern[index + 1] === '*') { expression += '.*'; index += 1; continue; }
|
|
393
|
+
expression += '[^/]*';
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
expression += character === '?' ? '[^/]' : character.replace(/[.+^${}()|[\]\\]/gu, '\\$&');
|
|
397
|
+
}
|
|
398
|
+
return new RegExp(`${expression}$`, 'u').test(path);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The subset of a patch's paths `exclude` drops, in order of first appearance.
|
|
403
|
+
*
|
|
404
|
+
* Decided per `diff --git` header on its `b/` side, which is the name `git
|
|
405
|
+
* apply` itself tests -- so a rename is dropped or kept whole, never half. A
|
|
406
|
+
* deletion names the same path on both sides, so it is covered by the same rule.
|
|
407
|
+
*/
|
|
408
|
+
export function excludedPatchPaths(
|
|
409
|
+
patch: string, exclude: readonly string[] = CLOUD_SYNC_PATCH_EXCLUDES,
|
|
410
|
+
): string[] {
|
|
411
|
+
const paths: string[] = [];
|
|
412
|
+
for (const header of patchHeaders(patch)) {
|
|
413
|
+
if (!exclude.some(pattern => matchesExclude(header.new, pattern))) continue;
|
|
414
|
+
for (const path of [header.old, header.new]) if (!paths.includes(path)) paths.push(path);
|
|
415
|
+
}
|
|
416
|
+
return paths;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Options for {@link applyCloudPatch}. */
|
|
420
|
+
export interface ApplyCloudPatchOptions {
|
|
421
|
+
/**
|
|
422
|
+
* Path patterns to drop, defaulting to {@link CLOUD_SYNC_PATCH_EXCLUDES}.
|
|
423
|
+
* Pass `[]` to apply a patch whole -- including the runtime artifacts the
|
|
424
|
+
* default list exists to keep out of a working tree.
|
|
425
|
+
*/
|
|
426
|
+
exclude?: readonly string[];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** What {@link applyCloudPatch} wrote, and what it dropped on the way. */
|
|
430
|
+
export interface AppliedCloudPatch {
|
|
431
|
+
/** Paths the apply wrote, in order of first appearance in the patch. */
|
|
432
|
+
files: string[];
|
|
433
|
+
/** Paths `exclude` dropped, in order of first appearance in the patch. */
|
|
434
|
+
excluded: string[];
|
|
435
|
+
}
|
|
436
|
+
|
|
299
437
|
/**
|
|
300
438
|
* `git apply --check` then `git apply`; a conflict leaves the tree untouched.
|
|
301
439
|
* The patch lands in the working tree uncommitted, so what the run changed is
|
|
302
|
-
* reviewed with `git diff` before anything is kept
|
|
440
|
+
* reviewed with `git diff` before anything is kept -- the same contract as v1.
|
|
441
|
+
*
|
|
442
|
+
* Both invocations carry the identical `--exclude` arguments. A check run
|
|
443
|
+
* without them is a different question than the apply answers: it can pass on
|
|
444
|
+
* an excluded hunk that the apply then never writes, or fail on one and refuse
|
|
445
|
+
* a patch whose applied part was clean. The exclusions are a property of the
|
|
446
|
+
* patch that lands, so they belong to both halves or neither.
|
|
447
|
+
*
|
|
448
|
+
* A patch whose every path is excluded is a no-op, not a failure: `git apply`
|
|
449
|
+
* exits 0 having written nothing, and the returned `files` is empty.
|
|
303
450
|
*/
|
|
304
|
-
export function applyCloudPatch(
|
|
305
|
-
|
|
451
|
+
export function applyCloudPatch(
|
|
452
|
+
root: string, patch: string, options: ApplyCloudPatchOptions = {},
|
|
453
|
+
): AppliedCloudPatch {
|
|
454
|
+
const exclude = options.exclude ?? CLOUD_SYNC_PATCH_EXCLUDES;
|
|
455
|
+
const args = ['-C', resolve(root), 'apply', '--whitespace=nowarn',
|
|
456
|
+
...exclude.map(pattern => `--exclude=${pattern}`)];
|
|
306
457
|
const check = spawnSync('git', [...args, '--check'], { input: patch, encoding: 'utf8' });
|
|
307
458
|
if (check.status !== 0) {
|
|
308
459
|
throw new CloudFlowError('patch_conflict',
|
|
@@ -312,4 +463,6 @@ export function applyCloudPatch(root: string, patch: string): void {
|
|
|
312
463
|
if (apply.status !== 0) {
|
|
313
464
|
throw new CloudFlowError('patch_conflict', `git apply failed:\n${apply.stderr.trim()}`);
|
|
314
465
|
}
|
|
466
|
+
const excluded = excludedPatchPaths(patch, exclude);
|
|
467
|
+
return { files: patchedPaths(patch).filter(path => !excluded.includes(path)), excluded };
|
|
315
468
|
}
|
package/src/index.ts
CHANGED
|
@@ -64,8 +64,10 @@ export {
|
|
|
64
64
|
type CloudFlowSource, type RunInCloudOptions, type CloudRunReceipt, type CloudRunState,
|
|
65
65
|
} from './cloud-run.js';
|
|
66
66
|
export {
|
|
67
|
-
downloadCloudPatch, applyCloudPatch, packWorkingTree, patchedPaths,
|
|
68
|
-
|
|
67
|
+
downloadCloudPatch, downloadCloudPatchSet, applyCloudPatch, packWorkingTree, patchedPaths,
|
|
68
|
+
excludedPatchPaths, CLOUD_SYNC_PATCH_EXCLUDES, MAX_SYNC_BYTES,
|
|
69
|
+
type CloudPatch, type CloudPathPatch, type CloudPatchSet, type PackedTree,
|
|
70
|
+
type ApplyCloudPatchOptions, type AppliedCloudPatch,
|
|
69
71
|
} from './cloud-sync.js';
|
|
70
72
|
export {
|
|
71
73
|
scheduleInCloud, listCloudSchedules, unscheduleInCloud, everyToCron, declaredScheduleCron,
|
package/src/relay-cli.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { runCli } from './cli.js';
|
|
4
|
+
import { CLI_VERBS, CLI_VERB_NAMES, type CliCommandSpec } from './cli-commands.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `@relayflows/sdk/relay-cli` entrypoint: a mountable CLI surface.
|
|
8
|
+
*
|
|
9
|
+
* `agent-relay` mounts this as `agent-relay flows`. The surface is a thin
|
|
10
|
+
* projection over the CLI this package already ships -- `commands` comes from
|
|
11
|
+
* the same `CLI_VERBS` table `parseArgs` dispatches on, and `run` delegates
|
|
12
|
+
* straight to `runCli`. No command is reimplemented here, and
|
|
13
|
+
* `packages/relayflows/bin/flows.js` keeps calling `runCli` on the same path.
|
|
14
|
+
*
|
|
15
|
+
* Structurally typed against `@agent-relay/cli-surface` without importing it,
|
|
16
|
+
* so `dist/relay-cli.d.ts` has no dependency on the contract package and this
|
|
17
|
+
* package gains no runtime dependency on relay. `tests/relay-cli-surface.test.ts`
|
|
18
|
+
* asserts the assignability and runs the contract's own conformance checks.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Host-supplied output sink. Mirrors `RelayCliIo`. */
|
|
22
|
+
export interface RelayCliIo {
|
|
23
|
+
stdout(chunk: string): void;
|
|
24
|
+
stderr(chunk: string): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A mountable product CLI. Mirrors `RelayCliSurface`. */
|
|
28
|
+
export interface RelayCliSurface {
|
|
29
|
+
id: string;
|
|
30
|
+
version: string;
|
|
31
|
+
contract: 1;
|
|
32
|
+
commands: readonly CliCommandSpec[];
|
|
33
|
+
run(argv: readonly string[], io: RelayCliIo): Promise<number>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Options for {@link createRelayCliSurface}. */
|
|
37
|
+
export interface CreateRelayCliSurfaceOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Cancellation for the long-running verbs, owned by the host.
|
|
40
|
+
*
|
|
41
|
+
* A surface must install no global signal handlers, so one is always passed
|
|
42
|
+
* to `runCli` -- a never-aborting signal when the host supplies none. Pass a
|
|
43
|
+
* real one to get graceful cancellation (for example, the
|
|
44
|
+
* "Stopped observing; the hosted run has not been cancelled" path on
|
|
45
|
+
* `run --cloud --wait`) instead of the host's SIGINT killing the process.
|
|
46
|
+
*/
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Exit code for an argv the surface cannot route, per the contract. */
|
|
51
|
+
const EXIT_UNKNOWN_COMMAND = 2;
|
|
52
|
+
|
|
53
|
+
/** Drop `variants` -- the routing detail the host has no use for. */
|
|
54
|
+
function toCommandSpec(verb: CliCommandSpec & { variants?: unknown }): CliCommandSpec {
|
|
55
|
+
const { variants: _variants, ...spec } = verb;
|
|
56
|
+
return spec;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read this package's version from its own manifest.
|
|
61
|
+
*
|
|
62
|
+
* Resolved from `import.meta.url` rather than imported, because `package.json`
|
|
63
|
+
* sits outside `rootDir` and a hardcoded literal would silently go stale at the
|
|
64
|
+
* next release. `src/` and `dist/` are both one level below the manifest, so
|
|
65
|
+
* the same relative path is correct before and after a build.
|
|
66
|
+
*/
|
|
67
|
+
function packageVersion(): string {
|
|
68
|
+
try {
|
|
69
|
+
const manifest: unknown = JSON.parse(
|
|
70
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
71
|
+
);
|
|
72
|
+
const version = (manifest as { version?: unknown }).version;
|
|
73
|
+
return typeof version === 'string' && version.length > 0 ? version : '0.0.0';
|
|
74
|
+
} catch {
|
|
75
|
+
// A surface that cannot read its own manifest is still perfectly runnable;
|
|
76
|
+
// refusing to mount over a cosmetic field would be the worse failure.
|
|
77
|
+
return '0.0.0';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the relayflows CLI surface.
|
|
83
|
+
*
|
|
84
|
+
* @param options - Host-supplied cancellation.
|
|
85
|
+
* @returns A surface whose `run` resolves to an exit code, writes only through
|
|
86
|
+
* the supplied io, and installs no process signal handlers.
|
|
87
|
+
*/
|
|
88
|
+
export function createRelayCliSurface(
|
|
89
|
+
options: CreateRelayCliSurfaceOptions = {},
|
|
90
|
+
): RelayCliSurface {
|
|
91
|
+
return {
|
|
92
|
+
id: 'relayflows',
|
|
93
|
+
version: packageVersion(),
|
|
94
|
+
contract: 1,
|
|
95
|
+
commands: CLI_VERBS.map(toCommandSpec),
|
|
96
|
+
async run(argv: readonly string[], io: RelayCliIo): Promise<number> {
|
|
97
|
+
const verb = argv[0];
|
|
98
|
+
// `runCli` would refuse this too, but with the full usage block. Naming
|
|
99
|
+
// the offending token is the more useful answer when the host has just
|
|
100
|
+
// routed `agent-relay flows <typo>` here.
|
|
101
|
+
if (verb !== undefined && !verb.startsWith('-') && !CLI_VERB_NAMES.has(verb)) {
|
|
102
|
+
io.stderr(`error: '${verb}' is not a command of relayflows\n`);
|
|
103
|
+
io.stderr("Run 'agent-relay flows --help' for the available commands.\n");
|
|
104
|
+
return EXIT_UNKNOWN_COMMAND;
|
|
105
|
+
}
|
|
106
|
+
return runCli(
|
|
107
|
+
argv,
|
|
108
|
+
// `CliIo` is line-oriented and the contract's io is chunk-oriented, so
|
|
109
|
+
// the terminator is added here rather than by every call site.
|
|
110
|
+
{ stdout: (line) => io.stdout(`${line}\n`), stderr: (line) => io.stderr(`${line}\n`) },
|
|
111
|
+
// Always pass a signal: that is what keeps `runCli` from installing the
|
|
112
|
+
// SIGINT/SIGTERM handlers the standalone binary still relies on.
|
|
113
|
+
{ signal: options.signal ?? new AbortController().signal },
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|