@relayflows/sdk 2.0.17 → 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.
Files changed (161) hide show
  1. package/dist/authored-flow-error.d.ts +21 -1
  2. package/dist/authored-flow-error.d.ts.map +1 -1
  3. package/dist/authored-flow-error.js +14 -0
  4. package/dist/authored-flow-error.js.map +1 -1
  5. package/dist/authored-flow-executor.d.ts.map +1 -1
  6. package/dist/authored-flow-executor.js +47 -3
  7. package/dist/authored-flow-executor.js.map +1 -1
  8. package/dist/authored-human.d.ts +49 -0
  9. package/dist/authored-human.d.ts.map +1 -0
  10. package/dist/authored-human.js +97 -0
  11. package/dist/authored-human.js.map +1 -0
  12. package/dist/authored-node-entry.js +3 -2
  13. package/dist/authored-node-entry.js.map +1 -1
  14. package/dist/authored-node-runner.d.ts.map +1 -1
  15. package/dist/authored-node-runner.js +15 -4
  16. package/dist/authored-node-runner.js.map +1 -1
  17. package/dist/authored-root.d.ts.map +1 -1
  18. package/dist/authored-root.js +29 -1
  19. package/dist/authored-root.js.map +1 -1
  20. package/dist/cli/answer.d.ts +17 -0
  21. package/dist/cli/answer.d.ts.map +1 -0
  22. package/dist/cli/answer.js +89 -0
  23. package/dist/cli/answer.js.map +1 -0
  24. package/dist/cli/build.d.ts +9 -0
  25. package/dist/cli/build.d.ts.map +1 -1
  26. package/dist/cli/build.js +26 -7
  27. package/dist/cli/build.js.map +1 -1
  28. package/dist/cli/check-triggers.d.ts.map +1 -1
  29. package/dist/cli/check-triggers.js +2 -0
  30. package/dist/cli/check-triggers.js.map +1 -1
  31. package/dist/cli/check.d.ts +11 -0
  32. package/dist/cli/check.d.ts.map +1 -1
  33. package/dist/cli/check.js +14 -2
  34. package/dist/cli/check.js.map +1 -1
  35. package/dist/cli/cloud-connect-cli.d.ts +54 -0
  36. package/dist/cli/cloud-connect-cli.d.ts.map +1 -0
  37. package/dist/cli/cloud-connect-cli.js +109 -0
  38. package/dist/cli/cloud-connect-cli.js.map +1 -0
  39. package/dist/cli/cloud-deploy.d.ts +3 -1
  40. package/dist/cli/cloud-deploy.d.ts.map +1 -1
  41. package/dist/cli/cloud-deploy.js +44 -6
  42. package/dist/cli/cloud-deploy.js.map +1 -1
  43. package/dist/cli/cloud-run.d.ts +9 -2
  44. package/dist/cli/cloud-run.d.ts.map +1 -1
  45. package/dist/cli/cloud-run.js +27 -15
  46. package/dist/cli/cloud-run.js.map +1 -1
  47. package/dist/cli/cloud-schedule.d.ts +3 -1
  48. package/dist/cli/cloud-schedule.d.ts.map +1 -1
  49. package/dist/cli/cloud-schedule.js +31 -6
  50. package/dist/cli/cloud-schedule.js.map +1 -1
  51. package/dist/cli/cloud-sync.d.ts +8 -1
  52. package/dist/cli/cloud-sync.d.ts.map +1 -1
  53. package/dist/cli/cloud-sync.js +79 -8
  54. package/dist/cli/cloud-sync.js.map +1 -1
  55. package/dist/cli/deploy.d.ts +6 -0
  56. package/dist/cli/deploy.d.ts.map +1 -1
  57. package/dist/cli/deploy.js +29 -4
  58. package/dist/cli/deploy.js.map +1 -1
  59. package/dist/cli/direct-run.d.ts.map +1 -1
  60. package/dist/cli/direct-run.js +5 -1
  61. package/dist/cli/direct-run.js.map +1 -1
  62. package/dist/cli/run.d.ts +22 -2
  63. package/dist/cli/run.d.ts.map +1 -1
  64. package/dist/cli/run.js +34 -1
  65. package/dist/cli/run.js.map +1 -1
  66. package/dist/cli/serve-webhook.d.ts +7 -1
  67. package/dist/cli/serve-webhook.d.ts.map +1 -1
  68. package/dist/cli/serve-webhook.js +19 -9
  69. package/dist/cli/serve-webhook.js.map +1 -1
  70. package/dist/cli-commands.d.ts +398 -0
  71. package/dist/cli-commands.d.ts.map +1 -0
  72. package/dist/cli-commands.js +254 -0
  73. package/dist/cli-commands.js.map +1 -0
  74. package/dist/cli-watch.d.ts +3 -1
  75. package/dist/cli-watch.d.ts.map +1 -1
  76. package/dist/cli-watch.js +4 -10
  77. package/dist/cli-watch.js.map +1 -1
  78. package/dist/cli.d.ts +127 -1
  79. package/dist/cli.d.ts.map +1 -1
  80. package/dist/cli.js +195 -57
  81. package/dist/cli.js.map +1 -1
  82. package/dist/cloud-connect.d.ts +52 -0
  83. package/dist/cloud-connect.d.ts.map +1 -0
  84. package/dist/cloud-connect.js +138 -0
  85. package/dist/cloud-connect.js.map +1 -0
  86. package/dist/cloud-deploy.d.ts +19 -1
  87. package/dist/cloud-deploy.d.ts.map +1 -1
  88. package/dist/cloud-deploy.js +40 -1
  89. package/dist/cloud-deploy.js.map +1 -1
  90. package/dist/cloud-http.d.ts +2 -2
  91. package/dist/cloud-http.d.ts.map +1 -1
  92. package/dist/cloud-http.js.map +1 -1
  93. package/dist/cloud-schedule.d.ts +7 -0
  94. package/dist/cloud-schedule.d.ts.map +1 -1
  95. package/dist/cloud-schedule.js +22 -1
  96. package/dist/cloud-schedule.js.map +1 -1
  97. package/dist/cloud-sync.d.ts +85 -2
  98. package/dist/cloud-sync.d.ts.map +1 -1
  99. package/dist/cloud-sync.js +123 -10
  100. package/dist/cloud-sync.js.map +1 -1
  101. package/dist/failure-kinds.d.ts +1 -1
  102. package/dist/failure-kinds.d.ts.map +1 -1
  103. package/dist/failure-kinds.js +2 -0
  104. package/dist/failure-kinds.js.map +1 -1
  105. package/dist/flow-requirements.d.ts +68 -0
  106. package/dist/flow-requirements.d.ts.map +1 -0
  107. package/dist/flow-requirements.js +152 -0
  108. package/dist/flow-requirements.js.map +1 -0
  109. package/dist/index.d.ts +3 -1
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +3 -1
  112. package/dist/index.js.map +1 -1
  113. package/dist/journal-client.d.ts +11 -0
  114. package/dist/journal-client.d.ts.map +1 -1
  115. package/dist/journal-client.js +13 -0
  116. package/dist/journal-client.js.map +1 -1
  117. package/dist/progress.d.ts +2 -1
  118. package/dist/progress.d.ts.map +1 -1
  119. package/dist/progress.js +9 -3
  120. package/dist/progress.js.map +1 -1
  121. package/dist/protocol.d.ts +25 -1
  122. package/dist/protocol.d.ts.map +1 -1
  123. package/dist/relay-cli.d.ts +50 -0
  124. package/dist/relay-cli.d.ts.map +1 -0
  125. package/dist/relay-cli.js +64 -0
  126. package/dist/relay-cli.js.map +1 -0
  127. package/package.json +7 -2
  128. package/src/authored-flow-error.ts +29 -0
  129. package/src/authored-flow-executor.ts +54 -2
  130. package/src/authored-human.ts +150 -0
  131. package/src/authored-node-entry.ts +3 -2
  132. package/src/authored-node-runner.ts +20 -5
  133. package/src/authored-root.ts +28 -1
  134. package/src/cli/answer.ts +98 -0
  135. package/src/cli/build.ts +21 -7
  136. package/src/cli/check-triggers.ts +2 -0
  137. package/src/cli/check.ts +24 -2
  138. package/src/cli/cloud-connect-cli.ts +130 -0
  139. package/src/cli/cloud-deploy.ts +40 -6
  140. package/src/cli/cloud-run.ts +26 -15
  141. package/src/cli/cloud-schedule.ts +31 -6
  142. package/src/cli/cloud-sync.ts +85 -8
  143. package/src/cli/deploy.ts +27 -5
  144. package/src/cli/direct-run.ts +5 -0
  145. package/src/cli/run.ts +48 -2
  146. package/src/cli/serve-webhook.ts +15 -8
  147. package/src/cli-commands.ts +339 -0
  148. package/src/cli-watch.ts +8 -9
  149. package/src/cli.ts +206 -54
  150. package/src/cloud-connect.ts +187 -0
  151. package/src/cloud-deploy.ts +57 -2
  152. package/src/cloud-http.ts +1 -1
  153. package/src/cloud-schedule.ts +20 -3
  154. package/src/cloud-sync.ts +164 -11
  155. package/src/failure-kinds.ts +2 -0
  156. package/src/flow-requirements.ts +201 -0
  157. package/src/index.ts +12 -2
  158. package/src/journal-client.ts +20 -0
  159. package/src/progress.ts +11 -4
  160. package/src/protocol.ts +23 -0
  161. package/src/relay-cli.ts +117 -0
@@ -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
- const dataDir = values.get('--data-dir');
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 (!dataDir || !portText || !/^\d+$/.test(portText)) return undefined;
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
- process.once('SIGINT', stop);
235
- process.once('SIGTERM', stop);
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(path: string, json: boolean, io: CliIo): Promise<ExitCode> {
13
- const controller = new AbortController();
14
- const stop = (): void => controller.abort();
15
- process.once('SIGINT', stop);
16
- process.once('SIGTERM', stop);
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: controller.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