@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/cli/serve-webhook.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createServer, type Server, type ServerResponse } from 'node:http';
|
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { TextDecoder } from 'node:util';
|
|
6
6
|
import type { CliIo } from '../cli.js';
|
|
7
|
+
import { DEFAULT_DATA_DIR } from '../daemon-connection.js';
|
|
7
8
|
import { providerInboxEvent } from '../trigger-executor.js';
|
|
8
9
|
import { verifySignature, schemeFor } from '../webhook-signature.js';
|
|
9
10
|
import { TokenBucketLimiter, keyFor, type RateLimitConfig } from '../webhook-rate-limit.js';
|
|
@@ -24,9 +25,12 @@ export function parseWebhookArgs(args: readonly string[]): {
|
|
|
24
25
|
|| !value || value.startsWith('-')) return undefined;
|
|
25
26
|
values.set(flag, value);
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
// `--data-dir` is optional here as it is on every other verb, and defaults to
|
|
29
|
+
// the same directory: the command surface advertises that default, so a
|
|
30
|
+
// receiver started without the flag has to run rather than exit 2.
|
|
31
|
+
const dataDir = values.get('--data-dir') ?? DEFAULT_DATA_DIR;
|
|
28
32
|
const portText = values.get('--port');
|
|
29
|
-
if (!
|
|
33
|
+
if (!portText || !/^\d+$/.test(portText)) return undefined;
|
|
30
34
|
const port = Number(portText);
|
|
31
35
|
if (!Number.isInteger(port) || port < 0 || port > 65535) return undefined;
|
|
32
36
|
const allow = values.get('--allow');
|
|
@@ -215,6 +219,12 @@ async function directory(path: string): Promise<void> {
|
|
|
215
219
|
|
|
216
220
|
export async function runServeWebhook(
|
|
217
221
|
options: { dataDir: string; port: number; admitted?: readonly string[] }, io: CliIo,
|
|
222
|
+
/**
|
|
223
|
+
* Shutdown, owned by the caller. `runCli` supplies either the embedder's
|
|
224
|
+
* signal or one it drives from SIGINT/SIGTERM, so the receiver installs no
|
|
225
|
+
* process-wide handler of its own.
|
|
226
|
+
*/
|
|
227
|
+
signal: AbortSignal,
|
|
218
228
|
): Promise<0 | 1> {
|
|
219
229
|
try {
|
|
220
230
|
const admittedNames = options.admitted === undefined ? undefined : new Set(options.admitted);
|
|
@@ -231,12 +241,9 @@ export async function runServeWebhook(
|
|
|
231
241
|
server.closeAllConnections();
|
|
232
242
|
};
|
|
233
243
|
server.once('error', reject);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
server.once('close', () =>
|
|
237
|
-
process.off('SIGINT', stop);
|
|
238
|
-
process.off('SIGTERM', stop);
|
|
239
|
-
});
|
|
244
|
+
if (signal.aborted) { stop(); return; }
|
|
245
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
246
|
+
server.once('close', () => signal.removeEventListener('abort', stop));
|
|
240
247
|
});
|
|
241
248
|
return 0;
|
|
242
249
|
} catch (error) {
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { DEFAULT_DATA_DIR } from './daemon-connection.js';
|
|
2
|
+
import type { ParsedArgs } from './cli.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The one declaration of the `flows` command surface.
|
|
6
|
+
*
|
|
7
|
+
* Two consumers read this table and nothing else:
|
|
8
|
+
*
|
|
9
|
+
* 1. `parseArgs` in `cli.ts` gates its verb dispatch on {@link CLI_VERB_NAMES},
|
|
10
|
+
* so a token that is not in this table can never reach a parser.
|
|
11
|
+
* 2. `createRelayCliSurface` in `relay-cli.ts` projects it into the
|
|
12
|
+
* `RelayCliSurface.commands` tree the `agent-relay` host mounts.
|
|
13
|
+
*
|
|
14
|
+
* Because both sides derive from one array, `commands` and `run` cannot
|
|
15
|
+
* describe different trees. The drift test proves the remaining direction --
|
|
16
|
+
* that each declared command actually parses -- and {@link CLI_VERBS} carries a
|
|
17
|
+
* compile-time assertion that every `ParsedArgs` variant is claimed by some
|
|
18
|
+
* verb, so extending the union without extending this table fails `tsc`.
|
|
19
|
+
*
|
|
20
|
+
* The types here are deliberately local rather than imported from
|
|
21
|
+
* `@agent-relay/cli-surface`: they are structurally identical, and keeping the
|
|
22
|
+
* import out means `dist/relay-cli.d.ts` stands alone, so a consumer of
|
|
23
|
+
* `@relayflows/sdk` never needs the contract package resolvable. Assignability
|
|
24
|
+
* to the real contract is asserted in `tests/relay-cli-surface.test.ts`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** A positional argument. Mirrors `RelayCliArgSpec`. */
|
|
28
|
+
export interface CliArgSpec {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
required: boolean;
|
|
32
|
+
variadic?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A flag, in commander's flag-string form. Mirrors `RelayCliOptionSpec`. */
|
|
36
|
+
export interface CliOptionSpec {
|
|
37
|
+
flags: string;
|
|
38
|
+
description: string;
|
|
39
|
+
defaultValue?: string | boolean | number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One node of the command tree. Mirrors `RelayCliCommandSpec`. */
|
|
43
|
+
export interface CliCommandSpec {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
aliases?: readonly string[];
|
|
47
|
+
args?: readonly CliArgSpec[];
|
|
48
|
+
options?: readonly CliOptionSpec[];
|
|
49
|
+
subcommands?: readonly CliCommandSpec[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A top-level verb, plus the `ParsedArgs` variants it can produce. */
|
|
53
|
+
export interface CliVerbSpec extends CliCommandSpec {
|
|
54
|
+
/**
|
|
55
|
+
* Every `ParsedArgs.command` this verb can parse to.
|
|
56
|
+
*
|
|
57
|
+
* Usually one, but the verb set and the variant set are not one-to-one:
|
|
58
|
+
* `deploy` produces `deploy` for a digest reference and `cloud-deploy` for
|
|
59
|
+
* authored source, and `run` produces `cloud-run` under `--cloud`.
|
|
60
|
+
*/
|
|
61
|
+
variants: readonly ParsedArgs['command'][];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DATA_DIR_OPTION: CliOptionSpec = {
|
|
65
|
+
flags: '--data-dir <dir>',
|
|
66
|
+
description: 'Daemon data directory',
|
|
67
|
+
defaultValue: DEFAULT_DATA_DIR,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const JSON_OPTION: CliOptionSpec = {
|
|
71
|
+
flags: '--json',
|
|
72
|
+
description: 'Emit one machine-readable JSON object instead of text',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Shared by the verbs that hand a flow to Cloud. Each of them preflights what
|
|
77
|
+
* the source needs connected and offers to connect it; this refuses instead,
|
|
78
|
+
* which is what a non-interactive caller wants.
|
|
79
|
+
*/
|
|
80
|
+
const NO_CONNECT_OPTION: CliOptionSpec = {
|
|
81
|
+
flags: '--no-connect',
|
|
82
|
+
description: 'Refuse a missing integration instead of offering to connect it',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Flags shared by the two verbs that execute a flow locally. */
|
|
86
|
+
const LOCAL_EXECUTION_OPTIONS = [
|
|
87
|
+
JSON_OPTION,
|
|
88
|
+
DATA_DIR_OPTION,
|
|
89
|
+
{ flags: '--local-agent', description: 'Run agent steps in this process instead of a worker' },
|
|
90
|
+
{ flags: '--no-spawn', description: 'Require a running relayflowd rather than starting one' },
|
|
91
|
+
{ flags: '--no-observer-link', description: 'Do not mint an observer link for this run' },
|
|
92
|
+
{
|
|
93
|
+
flags: '--allow-human-influenced',
|
|
94
|
+
description: 'Proceed even though the run carries human-influenced state',
|
|
95
|
+
},
|
|
96
|
+
] as const satisfies readonly CliOptionSpec[];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `as const satisfies` rather than a type annotation on purpose: an annotation
|
|
100
|
+
* would widen every `variants` entry to `ParsedArgs['command']` and the
|
|
101
|
+
* compile-time exhaustiveness assertion below would pass vacuously.
|
|
102
|
+
*/
|
|
103
|
+
export const CLI_VERBS = [
|
|
104
|
+
{
|
|
105
|
+
name: 'add',
|
|
106
|
+
description: 'Install a helper plugin into this project',
|
|
107
|
+
args: [{ name: 'helper', description: 'Helper name or @flows/<helper-name>', required: true }],
|
|
108
|
+
variants: ['add'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'answer',
|
|
112
|
+
description: 'Answer a run’s parked f.human question; `flows resume` then continues the body',
|
|
113
|
+
args: [
|
|
114
|
+
{ name: 'run-id', description: 'Run parked on the question', required: true },
|
|
115
|
+
{ name: 'wait-id', description: 'Which question to answer, named human-<n> in the order the body asked', required: true },
|
|
116
|
+
{ name: 'answer', description: 'The decision, as yes or no (also true or false)', required: true },
|
|
117
|
+
],
|
|
118
|
+
options: [
|
|
119
|
+
JSON_OPTION,
|
|
120
|
+
DATA_DIR_OPTION,
|
|
121
|
+
{ flags: '--no-spawn', description: 'Require a running relayflowd rather than starting one' },
|
|
122
|
+
{ flags: '--note <text>', description: 'Reason recorded on the journal alongside the answer' },
|
|
123
|
+
{ flags: '--by <identity>', description: 'Who answered, when relaying a person’s decision; defaults to the OS user' },
|
|
124
|
+
],
|
|
125
|
+
variants: ['answer'],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'build',
|
|
129
|
+
description: 'Compile a flow into a sealed, content-addressed bundle',
|
|
130
|
+
args: [{ name: 'source', description: 'flow.yaml, flow.ts, or a bundle directory with --verify', required: true }],
|
|
131
|
+
options: [
|
|
132
|
+
{ flags: '--out <dir>', description: 'Directory to write the bundle into; not valid with --verify' },
|
|
133
|
+
{ flags: '--verify', description: 'Verify an existing bundle directory instead of building' },
|
|
134
|
+
JSON_OPTION,
|
|
135
|
+
],
|
|
136
|
+
variants: ['build'],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'check',
|
|
140
|
+
description: 'Compile and preflight a flow without running it, or opening a daemon socket',
|
|
141
|
+
args: [{ name: 'source', description: 'flow.ts, flow.yaml, or spec.json', required: true }],
|
|
142
|
+
options: [JSON_OPTION, { flags: '--watch', description: 'Re-check on every change to the flow and its imports' }],
|
|
143
|
+
variants: ['check'],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'deploy',
|
|
147
|
+
description: 'Copy a sealed bundle into a file bucket, or deploy a hosted trigger listener',
|
|
148
|
+
args: [{ name: 'flow', description: 'flow.ts for a hosted listener, or <flow>@sha256:<digest> for a bundle', required: true }],
|
|
149
|
+
options: [
|
|
150
|
+
{ flags: '--to <file-bucket-uri>', description: 'Destination file bucket for a sealed bundle' },
|
|
151
|
+
{ flags: '--repo <owner/name>', description: 'Repository the hosted listener watches' },
|
|
152
|
+
{ flags: '--on <provider>', description: 'Trigger source, as <provider>[:key=value,...]; repeatable' },
|
|
153
|
+
{ flags: '--approver <handle>', description: 'Handle delivered to every launched run as input.approver' },
|
|
154
|
+
{ flags: '--agents <list>', description: 'Agent harnesses to allow, as claude[,codex]' },
|
|
155
|
+
{ flags: '--name <name>', description: 'Name for the hosted listener' },
|
|
156
|
+
{ flags: '--draft', description: 'Create the listener without activating it' },
|
|
157
|
+
NO_CONNECT_OPTION,
|
|
158
|
+
JSON_OPTION,
|
|
159
|
+
],
|
|
160
|
+
variants: ['deploy', 'cloud-deploy'],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'deployments',
|
|
164
|
+
description: 'List this workspace’s hosted trigger listeners',
|
|
165
|
+
options: [JSON_OPTION],
|
|
166
|
+
variants: ['deployments'],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'hn-monitor',
|
|
170
|
+
description: 'Hacker News monitor: poll for matching stories and launch a flow per hit',
|
|
171
|
+
subcommands: [
|
|
172
|
+
{
|
|
173
|
+
name: 'start',
|
|
174
|
+
description: 'Start polling in the foreground',
|
|
175
|
+
args: [{ name: 'spec', description: 'Monitor spec.json', required: true }],
|
|
176
|
+
options: [
|
|
177
|
+
DATA_DIR_OPTION,
|
|
178
|
+
{ flags: '--poll-interval-ms <ms>', description: 'Milliseconds between polls' },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
variants: ['hn-monitor'],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'observer',
|
|
186
|
+
description: 'Mint a read-only observer link without running a flow',
|
|
187
|
+
options: [DATA_DIR_OPTION],
|
|
188
|
+
variants: ['observer'],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'replay',
|
|
192
|
+
description: 'Replay a finished run from its local journal',
|
|
193
|
+
args: [{ name: 'run-id', description: 'Run id to replay', required: true }],
|
|
194
|
+
options: [
|
|
195
|
+
JSON_OPTION,
|
|
196
|
+
DATA_DIR_OPTION,
|
|
197
|
+
{ flags: '--at <step-id>', description: 'Replay up to this step' },
|
|
198
|
+
{
|
|
199
|
+
flags: '--allow-human-influenced',
|
|
200
|
+
description: 'Proceed even though the run carries human-influenced state',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
variants: ['replay'],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'resume',
|
|
207
|
+
description: 'Resume an interrupted local run from where its journal left off',
|
|
208
|
+
args: [{ name: 'run-id', description: 'Run id to resume', required: true }],
|
|
209
|
+
options: LOCAL_EXECUTION_OPTIONS,
|
|
210
|
+
variants: ['resume'],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'run',
|
|
214
|
+
description: 'Run a flow locally, or submit it to Cloud with --cloud',
|
|
215
|
+
args: [{ name: 'flow', description: 'flow.yaml, flow.ts, spec.json, or <flow>@sha256:<digest>', required: true }],
|
|
216
|
+
options: [
|
|
217
|
+
...LOCAL_EXECUTION_OPTIONS,
|
|
218
|
+
{ flags: '--input <json-or-file>', description: 'Input for an authored .flow.ts, inline JSON or a file path' },
|
|
219
|
+
{ flags: '--bucket <file-bucket-uri>', description: 'File bucket to fetch a sealed bundle from' },
|
|
220
|
+
{ flags: '--reuse-from <run-id>', description: 'Reuse memoized step outputs from an earlier run' },
|
|
221
|
+
{ flags: '--cloud', description: 'Submit to Agent Relay Cloud instead of running locally' },
|
|
222
|
+
{ flags: '--wait', description: 'With --cloud, poll until the hosted run reaches a terminal state' },
|
|
223
|
+
{
|
|
224
|
+
flags: '--sync-code',
|
|
225
|
+
description: 'With --cloud, upload the working directory as the run’s tree; pull results back with `flows sync`',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
flags: '--no-connect',
|
|
229
|
+
description: 'With --cloud, refuse a missing integration instead of offering to connect it',
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
variants: ['run', 'cloud-run'],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'schedule',
|
|
236
|
+
description: 'Register a flow to run in Cloud on a cron or interval, or on the one it declares',
|
|
237
|
+
args: [{ name: 'flow', description: 'flow.yaml or flow.ts submitted on every fire', required: true }],
|
|
238
|
+
options: [
|
|
239
|
+
{
|
|
240
|
+
flags: '--cron <expr>',
|
|
241
|
+
description: 'Cron expression to fire on; with neither this nor --every, the flow’s own schedule.* handler supplies it',
|
|
242
|
+
},
|
|
243
|
+
{ flags: '--every <duration>', description: 'Fixed cadence, as <n><s|m|h|d>; not valid with --cron' },
|
|
244
|
+
{ flags: '--tz <iana>', description: 'IANA timezone the cron is read in' },
|
|
245
|
+
{ flags: '--input <json-or-file>', description: 'Input for an authored .flow.ts, inline JSON or a file path' },
|
|
246
|
+
{ flags: '--name <name>', description: 'Name for the schedule' },
|
|
247
|
+
NO_CONNECT_OPTION,
|
|
248
|
+
JSON_OPTION,
|
|
249
|
+
],
|
|
250
|
+
variants: ['schedule'],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'schedules',
|
|
254
|
+
description: 'List this workspace’s Cloud schedules',
|
|
255
|
+
options: [JSON_OPTION],
|
|
256
|
+
variants: ['schedules'],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'serve-webhook',
|
|
260
|
+
description: 'Run the local webhook receiver that writes provider deliveries into the trigger inbox',
|
|
261
|
+
options: [
|
|
262
|
+
DATA_DIR_OPTION,
|
|
263
|
+
{ flags: '--port <port>', description: 'Port to listen on, bound to 127.0.0.1; required' },
|
|
264
|
+
{ flags: '--allow <names>', description: 'Comma-separated flow names this receiver admits' },
|
|
265
|
+
],
|
|
266
|
+
variants: ['serve-webhook'],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'sync',
|
|
270
|
+
description: 'Apply a hosted run’s code changes to a local tree (replaces `agent-relay cloud sync`)',
|
|
271
|
+
args: [{ name: 'run-id', description: 'Hosted run id whose patch to apply', required: true }],
|
|
272
|
+
options: [
|
|
273
|
+
JSON_OPTION,
|
|
274
|
+
{ flags: '--dry-run', description: 'Print the patch and apply nothing' },
|
|
275
|
+
{ flags: '--dir <path>', description: 'Tree to apply the patch to', defaultValue: '.' },
|
|
276
|
+
],
|
|
277
|
+
variants: ['sync'],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'tick',
|
|
281
|
+
description: 'Interval scheduler: launch a flow on a fixed local cadence',
|
|
282
|
+
subcommands: [
|
|
283
|
+
{
|
|
284
|
+
name: 'start',
|
|
285
|
+
description: 'Start ticking in the foreground',
|
|
286
|
+
args: [{ name: 'spec', description: 'Flow spec.json to launch each tick', required: true }],
|
|
287
|
+
options: [
|
|
288
|
+
DATA_DIR_OPTION,
|
|
289
|
+
{ flags: '--schedule-id <id>', description: 'Stable id identifying this schedule' },
|
|
290
|
+
{ flags: '--interval-ms <ms>', description: 'Milliseconds between ticks' },
|
|
291
|
+
{ flags: '--epoch-ms <ms>', description: 'Epoch the tick grid is aligned to' },
|
|
292
|
+
{ flags: '--max-catch-up <n>', description: 'Most missed ticks to replay after a gap' },
|
|
293
|
+
{ flags: '--poll-interval-ms <ms>', description: 'Milliseconds between schedule polls' },
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
variants: ['tick'],
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'undeploy',
|
|
301
|
+
description: 'Remove a hosted trigger listener',
|
|
302
|
+
args: [{ name: 'deployment-id', description: 'Deployment id to remove', required: true }],
|
|
303
|
+
options: [JSON_OPTION],
|
|
304
|
+
variants: ['undeploy'],
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'unschedule',
|
|
308
|
+
description: 'Remove a Cloud schedule, so it stops firing',
|
|
309
|
+
args: [{ name: 'schedule-id', description: 'Schedule id to remove', required: true }],
|
|
310
|
+
options: [JSON_OPTION],
|
|
311
|
+
variants: ['unschedule'],
|
|
312
|
+
},
|
|
313
|
+
] as const satisfies readonly CliVerbSpec[];
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Every `ParsedArgs` variant claimed by some verb in {@link CLI_VERBS}.
|
|
317
|
+
*
|
|
318
|
+
* Widened from the table rather than written down, so it tracks the table.
|
|
319
|
+
*/
|
|
320
|
+
type DeclaredVariant = (typeof CLI_VERBS)[number]['variants'][number];
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Compile-time drift guard, the half a runtime test cannot cover.
|
|
324
|
+
*
|
|
325
|
+
* Adding a variant to `ParsedArgs` without giving some verb a claim on it
|
|
326
|
+
* leaves `Exclude<ParsedArgs['command'], DeclaredVariant>` non-`never`, and
|
|
327
|
+
* this assignment stops compiling. The reverse direction catches a table entry
|
|
328
|
+
* naming a variant that no longer exists.
|
|
329
|
+
*/
|
|
330
|
+
type AssertNever<T extends never> = T;
|
|
331
|
+
export type _EveryVariantIsDeclared = AssertNever<Exclude<ParsedArgs['command'], DeclaredVariant>>;
|
|
332
|
+
export type _EveryDeclaredVariantExists = AssertNever<Exclude<DeclaredVariant, ParsedArgs['command']>>;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The verbs `parseArgs` accepts. A token outside this set is refused before any
|
|
336
|
+
* per-verb parser sees it, which is what keeps dispatch and {@link CLI_VERBS}
|
|
337
|
+
* from drifting apart.
|
|
338
|
+
*/
|
|
339
|
+
export const CLI_VERB_NAMES: ReadonlySet<string> = new Set(CLI_VERBS.map((verb) => verb.name));
|
package/src/cli-watch.ts
CHANGED
|
@@ -9,24 +9,23 @@ const DEBOUNCE_MS = 150;
|
|
|
9
9
|
type ExitCode = 0 | 1 | 2 | 3;
|
|
10
10
|
|
|
11
11
|
/** A runner around the ordinary CLI, with a fresh module cache for every check. */
|
|
12
|
-
export async function watchCheck(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
export async function watchCheck(
|
|
13
|
+
path: string,
|
|
14
|
+
json: boolean,
|
|
15
|
+
io: CliIo,
|
|
16
|
+
/** Cancellation, owned by the caller; this function installs no signal handler. */
|
|
17
|
+
signal: AbortSignal,
|
|
18
|
+
): Promise<ExitCode> {
|
|
17
19
|
try {
|
|
18
20
|
return await watchChecks({
|
|
19
21
|
path,
|
|
20
|
-
signal
|
|
22
|
+
signal,
|
|
21
23
|
check: () => checkOnce(path, json, io),
|
|
22
24
|
clear: () => { if (!json) io.stdout('\x1b[2J\x1b[H'); },
|
|
23
25
|
});
|
|
24
26
|
} catch (error) {
|
|
25
27
|
io.stderr(`REFUSED [input_unreadable] Could not watch "${path}": ${error instanceof Error ? error.message : String(error)}`);
|
|
26
28
|
return 2;
|
|
27
|
-
} finally {
|
|
28
|
-
process.off('SIGINT', stop);
|
|
29
|
-
process.off('SIGTERM', stop);
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
|
package/src/cli.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { parseBuildArgs, runBuild, type BuildArgs } from './cli/build.js';
|
|
|
36
36
|
import { runHnMonitor } from './cli/hn-monitor.js';
|
|
37
37
|
import { runTickRunner } from './cli/tick-runner.js';
|
|
38
38
|
import { DEFAULT_DATA_DIR } from './daemon-connection.js';
|
|
39
|
+
import { CLI_VERB_NAMES } from './cli-commands.js';
|
|
39
40
|
import {
|
|
40
41
|
mintObserverUrl,
|
|
41
42
|
resolveObserverLinkEnv,
|
|
@@ -50,14 +51,19 @@ export interface CliIo {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
type CliExitCode = 0 | 1 | 2 | 3;
|
|
53
|
-
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Every shape `parseArgs` can produce. Exported for `cli-commands.ts`, whose
|
|
57
|
+
* table must claim each variant or fail to compile.
|
|
58
|
+
*/
|
|
59
|
+
export type ParsedArgs =
|
|
54
60
|
| { command: 'add'; value: string }
|
|
55
61
|
| ReplayArgs
|
|
56
62
|
| BuildArgs
|
|
57
63
|
| DeployArgs
|
|
58
64
|
| { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
|
|
59
65
|
| { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean; noConnect: boolean }
|
|
60
|
-
| { command: 'sync'; runId: string; json: boolean; root: string }
|
|
66
|
+
| { command: 'sync'; runId: string; json: boolean; root: string; dryRun: boolean }
|
|
61
67
|
| CloudDeployArgs
|
|
62
68
|
| { command: 'deployments'; json: boolean }
|
|
63
69
|
| { command: 'undeploy'; agentId: string; json: boolean }
|
|
@@ -92,7 +98,7 @@ const USAGE = [
|
|
|
92
98
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] [--reuse-from <run-id>] <flow.yaml|spec.json>',
|
|
93
99
|
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.yaml|spec.json>',
|
|
94
100
|
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.ts> --input <inline-json-or-file>',
|
|
95
|
-
'flows sync [--json] [--dir <path>] <run-id>',
|
|
101
|
+
'flows sync [--json] [--dry-run] [--dir <path>] <run-id>',
|
|
96
102
|
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>',
|
|
97
103
|
'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
|
|
98
104
|
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
|
|
@@ -117,9 +123,48 @@ const PROCESS_IO: CliIo = {
|
|
|
117
123
|
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
118
124
|
};
|
|
119
125
|
|
|
126
|
+
/** Optional knobs for an embedded caller. `bin/flows.js` passes none. */
|
|
127
|
+
export interface RunCliOptions {
|
|
128
|
+
/**
|
|
129
|
+
* Cancellation for the long-running verbs (`run --cloud`, `check --watch`,
|
|
130
|
+
* `serve-webhook`, `hn-monitor start`, `tick start`).
|
|
131
|
+
*
|
|
132
|
+
* Supply one and `runCli` installs **no** process signal handlers -- required
|
|
133
|
+
* of a CLI surface mounted into another host, which owns SIGINT itself.
|
|
134
|
+
* Omit it and the standalone `flows` binary keeps today's behaviour exactly:
|
|
135
|
+
* SIGINT/SIGTERM are handled here, for the duration of that verb only.
|
|
136
|
+
*/
|
|
137
|
+
signal?: AbortSignal;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run one long-running verb under a cancellation signal.
|
|
142
|
+
*
|
|
143
|
+
* With a caller-supplied signal this installs nothing. Without one it owns
|
|
144
|
+
* SIGINT/SIGTERM for the duration of `body` and removes the handlers after --
|
|
145
|
+
* the pre-existing standalone behaviour, unchanged.
|
|
146
|
+
*/
|
|
147
|
+
async function withInterrupt<T>(
|
|
148
|
+
provided: AbortSignal | undefined,
|
|
149
|
+
body: (signal: AbortSignal) => Promise<T>,
|
|
150
|
+
): Promise<T> {
|
|
151
|
+
if (provided !== undefined) return body(provided);
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const onSignal = (): void => controller.abort();
|
|
154
|
+
process.once('SIGINT', onSignal);
|
|
155
|
+
process.once('SIGTERM', onSignal);
|
|
156
|
+
try {
|
|
157
|
+
return await body(controller.signal);
|
|
158
|
+
} finally {
|
|
159
|
+
process.off('SIGINT', onSignal);
|
|
160
|
+
process.off('SIGTERM', onSignal);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
120
164
|
export async function runCli(
|
|
121
165
|
args: readonly string[],
|
|
122
166
|
io: CliIo = PROCESS_IO,
|
|
167
|
+
options: RunCliOptions = {},
|
|
123
168
|
): Promise<CliExitCode> {
|
|
124
169
|
if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
|
|
125
170
|
io.stdout(USAGE);
|
|
@@ -135,9 +180,13 @@ export async function runCli(
|
|
|
135
180
|
|
|
136
181
|
if (parsed.command === 'add') return addPlugin(parsed.value, io);
|
|
137
182
|
|
|
138
|
-
if (parsed.command === 'serve-webhook')
|
|
183
|
+
if (parsed.command === 'serve-webhook') {
|
|
184
|
+
return withInterrupt(options.signal, (signal) => runServeWebhook(parsed, io, signal));
|
|
185
|
+
}
|
|
139
186
|
|
|
140
|
-
if (parsed.command === 'cloud-run')
|
|
187
|
+
if (parsed.command === 'cloud-run') {
|
|
188
|
+
return withInterrupt(options.signal, (signal) => runCloudCli(parsed, io, signal));
|
|
189
|
+
}
|
|
141
190
|
if (parsed.command === 'sync') return runCloudSyncCli(parsed, io);
|
|
142
191
|
if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
|
|
143
192
|
if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
|
|
@@ -159,7 +208,9 @@ export async function runCli(
|
|
|
159
208
|
if (parsed.command === 'deploy') return runDeploy(parsed, io);
|
|
160
209
|
|
|
161
210
|
if (parsed.command === 'check') {
|
|
162
|
-
if (parsed.watch)
|
|
211
|
+
if (parsed.watch) {
|
|
212
|
+
return withInterrupt(options.signal, (signal) => watchCheck(parsed.value, parsed.json, io, signal));
|
|
213
|
+
}
|
|
163
214
|
// Deliberately daemon-free (kernel/DAEMON-LIFECYCLE.md §4). `checkFlow` is
|
|
164
215
|
// a compile-and-preflight that opens no daemon socket, and the parser
|
|
165
216
|
// refuses `--data-dir` on `check`, so there is no data dir to attach to.
|
|
@@ -174,45 +225,27 @@ export async function runCli(
|
|
|
174
225
|
if (parsed.command === 'observer') return runObserverCommand(io);
|
|
175
226
|
|
|
176
227
|
if (parsed.command === 'hn-monitor') {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
dataDir: parsed.dataDir,
|
|
184
|
-
specPath: parsed.specPath,
|
|
185
|
-
pollIntervalMs: parsed.pollIntervalMs,
|
|
186
|
-
signal: controller.signal,
|
|
187
|
-
}, io);
|
|
188
|
-
} finally {
|
|
189
|
-
process.off('SIGINT', onSignal);
|
|
190
|
-
process.off('SIGTERM', onSignal);
|
|
191
|
-
}
|
|
228
|
+
return withInterrupt(options.signal, (signal) => runHnMonitor({
|
|
229
|
+
dataDir: parsed.dataDir,
|
|
230
|
+
specPath: parsed.specPath,
|
|
231
|
+
pollIntervalMs: parsed.pollIntervalMs,
|
|
232
|
+
signal,
|
|
233
|
+
}, io));
|
|
192
234
|
}
|
|
193
235
|
|
|
194
236
|
if (parsed.command === 'tick') {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
...(parsed.maxCatchUp === undefined ? {} : { maxCatchUp: parsed.maxCatchUp }),
|
|
208
|
-
},
|
|
209
|
-
pollIntervalMs: parsed.pollIntervalMs,
|
|
210
|
-
signal: controller.signal,
|
|
211
|
-
}, io) as CliExitCode;
|
|
212
|
-
} finally {
|
|
213
|
-
process.off('SIGINT', onSignal);
|
|
214
|
-
process.off('SIGTERM', onSignal);
|
|
215
|
-
}
|
|
237
|
+
return withInterrupt(options.signal, async (signal) => await runTickRunner({
|
|
238
|
+
dataDir: parsed.dataDir,
|
|
239
|
+
specPath: parsed.specPath,
|
|
240
|
+
schedule: {
|
|
241
|
+
scheduleId: parsed.scheduleId,
|
|
242
|
+
intervalMs: parsed.intervalMs,
|
|
243
|
+
...(parsed.epochMs === undefined ? {} : { epochMs: parsed.epochMs }),
|
|
244
|
+
...(parsed.maxCatchUp === undefined ? {} : { maxCatchUp: parsed.maxCatchUp }),
|
|
245
|
+
},
|
|
246
|
+
pollIntervalMs: parsed.pollIntervalMs,
|
|
247
|
+
signal,
|
|
248
|
+
}, io) as CliExitCode);
|
|
216
249
|
}
|
|
217
250
|
|
|
218
251
|
// Attach-or-spawn runs inside `runFlow`/`resumeFlow`/`runDirectFlow`, at the
|
|
@@ -458,6 +491,11 @@ function emitWait(
|
|
|
458
491
|
|
|
459
492
|
function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
460
493
|
const command = args[0];
|
|
494
|
+
// The verb set lives in exactly one place -- `CLI_VERBS` in cli-commands.ts --
|
|
495
|
+
// which is also what `createRelayCliSurface` projects into `commands`. Gating
|
|
496
|
+
// dispatch on it means a token the surface does not declare can never reach a
|
|
497
|
+
// parser, so the declared tree and the dispatched tree cannot drift apart.
|
|
498
|
+
if (command === undefined || !CLI_VERB_NAMES.has(command)) return undefined;
|
|
461
499
|
if (command === 'add') return args.length === 2 ? { command: 'add', value: args[1]! } : undefined;
|
|
462
500
|
if (command === 'replay') return parseReplayArgs(args.slice(1));
|
|
463
501
|
if (command === 'build') return parseBuildArgs(args.slice(1));
|
|
@@ -718,9 +756,13 @@ function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
|
718
756
|
* directory at all -- the mint is a pure Relaycast API round-trip. No
|
|
719
757
|
* positional argument, no other flags.
|
|
720
758
|
*/
|
|
721
|
-
/**
|
|
759
|
+
/**
|
|
760
|
+
* `flows sync [--json] [--dry-run] [--dir <path>] <run-id>`: apply a hosted
|
|
761
|
+
* run's patch to a local tree, or with `--dry-run` print it and apply nothing.
|
|
762
|
+
*/
|
|
722
763
|
function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
723
764
|
let json = false;
|
|
765
|
+
let dryRun = false;
|
|
724
766
|
let root: string | undefined;
|
|
725
767
|
const positionals: string[] = [];
|
|
726
768
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -730,6 +772,11 @@ function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
730
772
|
json = true;
|
|
731
773
|
continue;
|
|
732
774
|
}
|
|
775
|
+
if (argument === '--dry-run') {
|
|
776
|
+
if (dryRun) return undefined;
|
|
777
|
+
dryRun = true;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
733
780
|
if (argument === '--dir') {
|
|
734
781
|
const value = args[index + 1];
|
|
735
782
|
if (root !== undefined || value === undefined || value.startsWith('-')) return undefined;
|
|
@@ -741,7 +788,7 @@ function parseSyncArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
741
788
|
positionals.push(argument);
|
|
742
789
|
}
|
|
743
790
|
if (positionals.length !== 1) return undefined;
|
|
744
|
-
return { command: 'sync', runId: positionals[0]!, json, root: root ?? '.' };
|
|
791
|
+
return { command: 'sync', runId: positionals[0]!, json, dryRun, root: root ?? '.' };
|
|
745
792
|
}
|
|
746
793
|
|
|
747
794
|
function parseObserverArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
@@ -956,3 +1003,15 @@ if (isDirectInvocation(process.argv[1])) {
|
|
|
956
1003
|
process.exitCode = exitCode;
|
|
957
1004
|
});
|
|
958
1005
|
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* The argv parser, exported for the CLI-surface drift test.
|
|
1009
|
+
*
|
|
1010
|
+
* The drift test must prove that every command `cli-commands.ts` declares
|
|
1011
|
+
* actually routes to a `ParsedArgs` variant, and that every variant is
|
|
1012
|
+
* reachable from some declared command. Observing that through `runCli` would
|
|
1013
|
+
* mean executing the commands. Not part of the package's public API --
|
|
1014
|
+
* `@relayflows/sdk/cli` exports `runCli`, and `@relayflows/sdk/relay-cli`
|
|
1015
|
+
* exports the surface.
|
|
1016
|
+
*/
|
|
1017
|
+
export { parseArgs as parseCliArgs };
|