@zapier/zapier-sdk-cli 0.44.1 → 0.45.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +381 -29
  3. package/bin/zapier-sdk-experimental.mjs +14 -0
  4. package/dist/cli.cjs +585 -37
  5. package/dist/cli.mjs +584 -36
  6. package/dist/experimental.cjs +3519 -0
  7. package/dist/experimental.d.mts +39 -0
  8. package/dist/experimental.d.ts +39 -0
  9. package/dist/experimental.mjs +3483 -0
  10. package/dist/index.cjs +507 -26
  11. package/dist/index.d.mts +3 -514
  12. package/dist/index.d.ts +3 -514
  13. package/dist/index.mjs +505 -24
  14. package/dist/package.json +14 -2
  15. package/dist/sdk-B3nKAZdN.d.mts +516 -0
  16. package/dist/sdk-B3nKAZdN.d.ts +516 -0
  17. package/dist/src/cli.js +26 -2
  18. package/dist/src/experimental.d.ts +33 -0
  19. package/dist/src/experimental.js +83 -0
  20. package/dist/src/generators/ast-generator.d.ts +2 -2
  21. package/dist/src/generators/ast-generator.js +1 -1
  22. package/dist/src/plugins/add/index.d.ts +2 -2
  23. package/dist/src/plugins/bundleCode/index.js +3 -12
  24. package/dist/src/plugins/curl/index.js +2 -2
  25. package/dist/src/plugins/curl/utils.d.ts +11 -1
  26. package/dist/src/plugins/curl/utils.js +14 -5
  27. package/dist/src/plugins/drainTriggerInbox/index.d.ts +46 -0
  28. package/dist/src/plugins/drainTriggerInbox/index.js +178 -0
  29. package/dist/src/plugins/generateAppTypes/index.d.ts +2 -2
  30. package/dist/src/plugins/index.d.ts +2 -0
  31. package/dist/src/plugins/index.js +2 -0
  32. package/dist/src/plugins/mcp/index.d.ts +1 -0
  33. package/dist/src/plugins/mcp/index.js +5 -1
  34. package/dist/src/plugins/watchTriggerInbox/index.d.ts +45 -0
  35. package/dist/src/plugins/watchTriggerInbox/index.js +157 -0
  36. package/dist/src/sdk.js +5 -1
  37. package/dist/src/utils/cli-generator.js +18 -1
  38. package/dist/src/utils/cli-renderer.d.ts +12 -0
  39. package/dist/src/utils/cli-renderer.js +22 -1
  40. package/dist/src/utils/parameter-resolver.d.ts +1 -0
  41. package/dist/src/utils/parameter-resolver.js +55 -9
  42. package/dist/src/utils/triggerDrain.d.ts +144 -0
  43. package/dist/src/utils/triggerDrain.js +351 -0
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +16 -4
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Shared CLI helpers for the `drain-trigger-inbox` and
3
+ * `watch-trigger-inbox` commands. Both wrap the eager SDK callback API
4
+ * and dispatch on the same flag set: interactive default (TTY),
5
+ * `--exec` (binary + argv, no shell), `--exec-shell` (shell handler),
6
+ * `--json` (drain: collect-and-print; watch: NDJSON streaming).
7
+ */
8
+ import { spawn } from "child_process";
9
+ import inquirer from "inquirer";
10
+ import chalk from "chalk";
11
+ import { ZapierAbortDrainSignal, ZapierReleaseTriggerMessageSignal, } from "@zapier/zapier-sdk";
12
+ import { ZapierCliValidationError } from "./errors";
13
+ /**
14
+ * Marker error thrown by `createInteractiveCallback` when the user
15
+ * picks "Skip (let lease expire)". The SDK treats it like any other
16
+ * Error subclass — leave-leased per default `releaseOnError: false`
17
+ * — so the lease-timeout recovery path is preserved. The CLI uses
18
+ * the marker type to distinguish a deliberate skip from an unhandled
19
+ * exception when counting summary lines. Not a `ZapierSignal`: the
20
+ * SDK's signal-handling auto-releases the message, which is exactly
21
+ * what skip-expire does *not* want.
22
+ */
23
+ export class CliSkipLeaseExpireError extends Error {
24
+ constructor() {
25
+ super("user skipped (let lease expire)");
26
+ this.name = "CliSkipLeaseExpireError";
27
+ }
28
+ }
29
+ /**
30
+ * Prompts the user per message. Choices:
31
+ *
32
+ * - **Ack**: returns normally so the SDK acks the message.
33
+ * - **Skip (release after draining)**: throws
34
+ * `ZapierReleaseTriggerMessageSignal`. The SDK marks the message for
35
+ * release; the actual release call is deferred until the drain
36
+ * finishes so the same drain doesn't immediately re-lease it. After
37
+ * drain ends, the message returns to `available` for a future drain
38
+ * run or another consumer.
39
+ * - **Skip (let lease expire)**: throws `CliSkipLeaseExpireError` so
40
+ * the SDK leaves the message leased (it's not a `ZapierSignal`,
41
+ * so the SDK's catch-all "leave on error" path applies); the lease
42
+ * timeout (~5 min default) is the recovery path. Use this when you
43
+ * want backpressure on reprocessing. The CLI catches the marker
44
+ * to count it as a skip in the drain summary.
45
+ * - **Quit**: throws `ZapierAbortDrainSignal` so the SDK stops draining
46
+ * after the current batch finishes (in-flight callbacks complete;
47
+ * not-yet-started messages in the batch are released).
48
+ *
49
+ * Ctrl-C during the prompt is translated to `ZapierAbortDrainSignal`
50
+ * (same as Quit) — inquirer raises `ExitPromptError`, which we catch
51
+ * and re-throw as the abort signal.
52
+ */
53
+ export function createInteractiveCallback() {
54
+ let messageNumber = 0;
55
+ return async (message) => {
56
+ messageNumber++;
57
+ const attrs = message.message_attributes;
58
+ console.log(`\n${chalk.bold(`Message #${messageNumber}`)} ${chalk.dim(message.id)} ` +
59
+ `${chalk.dim(`(lease #${attrs.lease_count})`)}`);
60
+ if (attrs.error_message) {
61
+ console.log(chalk.yellow(` upstream error: ${attrs.error_message}`));
62
+ }
63
+ if (attrs.possible_duplicate_data) {
64
+ console.log(chalk.yellow(" possible duplicate data"));
65
+ }
66
+ while (true) {
67
+ let action;
68
+ try {
69
+ const answer = await inquirer.prompt([
70
+ {
71
+ type: "list",
72
+ name: "action",
73
+ message: "Action?",
74
+ choices: [
75
+ { name: "Ack (remove from inbox)", value: "ack" },
76
+ {
77
+ name: "Skip (release after draining)",
78
+ value: "skip-release",
79
+ },
80
+ { name: "Skip (let lease expire)", value: "skip-expire" },
81
+ { name: "View payload", value: "view" },
82
+ { name: "Quit", value: "quit" },
83
+ ],
84
+ },
85
+ ]);
86
+ action = answer.action;
87
+ }
88
+ catch (error) {
89
+ // Inquirer raises `ExitPromptError` on Ctrl-C / Ctrl-D / ESC.
90
+ // Translate to the SDK abort signal so the drain stops cleanly.
91
+ if (error instanceof Error && error.name === "ExitPromptError") {
92
+ throw new ZapierAbortDrainSignal("user pressed Ctrl-C");
93
+ }
94
+ throw error;
95
+ }
96
+ if (action === "view") {
97
+ console.log(chalk.dim(JSON.stringify(message.payload, null, 2)));
98
+ continue;
99
+ }
100
+ if (action === "ack") {
101
+ return;
102
+ }
103
+ if (action === "skip-release") {
104
+ throw new ZapierReleaseTriggerMessageSignal("user skipped (release)");
105
+ }
106
+ if (action === "skip-expire") {
107
+ throw new CliSkipLeaseExpireError();
108
+ }
109
+ if (action === "quit") {
110
+ throw new ZapierAbortDrainSignal("user requested quit");
111
+ }
112
+ }
113
+ };
114
+ }
115
+ /**
116
+ * Writes one JSON object per line to stdout (NDJSON) for piping. Acks
117
+ * each message after the write resolves; if stdout is closed mid-drain
118
+ * (broken pipe), the write throws and the message stays unacked.
119
+ *
120
+ * The write callback alone provides backpressure: it doesn't fire until
121
+ * the chunk is fully committed to the underlying resource, so awaiting
122
+ * it pauses the drain when stdout is busy. No separate `drain` listener
123
+ * needed — and adding one would race with the callback (drain fires at
124
+ * highWaterMark, callback fires after full commit; if drain wins, the
125
+ * later callback's error gets dropped on the floor).
126
+ */
127
+ export function createNdjsonCallback() {
128
+ return (message) => new Promise((resolve, reject) => {
129
+ process.stdout.write(JSON.stringify(message) + "\n", (err) => {
130
+ if (err)
131
+ reject(err);
132
+ else
133
+ resolve();
134
+ });
135
+ });
136
+ }
137
+ /**
138
+ * Run a subprocess per message, piping the message JSON to the
139
+ * subprocess on stdin. Exit code 0 acks the message (resolves);
140
+ * non-zero rejects so the SDK leaves it unacked.
141
+ *
142
+ * When `shell` is true (used by `runShellCommand`), Node picks the
143
+ * platform's default shell (`sh` on POSIX, `cmd.exe` on Windows) and
144
+ * pipelines/redirects/expansions work everywhere. When false (used by
145
+ * `runExecCommand`), the binary is invoked directly with argv passed
146
+ * literally — no shell parsing, no glob expansion.
147
+ *
148
+ * Threads through the abort signal so a Ctrl-C during forever-watch
149
+ * cleanly terminates in-flight subprocesses rather than leaving them
150
+ * orphaned. On abort, the subprocess is killed and the close-event
151
+ * rejects with `ZapierAbortDrainSignal` so the SDK stops cleanly
152
+ * rather than recording the kill as a handler error.
153
+ */
154
+ function runSubprocess(options) {
155
+ const { command, args, shell, label, message, signal } = options;
156
+ return new Promise((resolve, reject) => {
157
+ const child = spawn(command, args, {
158
+ shell,
159
+ stdio: ["pipe", "inherit", "inherit"],
160
+ });
161
+ let abortListener;
162
+ if (signal) {
163
+ if (signal.aborted) {
164
+ child.kill();
165
+ }
166
+ else {
167
+ abortListener = () => {
168
+ child.kill();
169
+ };
170
+ signal.addEventListener("abort", abortListener, { once: true });
171
+ }
172
+ }
173
+ child.on("error", (err) => {
174
+ if (signal && abortListener) {
175
+ signal.removeEventListener("abort", abortListener);
176
+ }
177
+ reject(err);
178
+ });
179
+ child.on("close", (code) => {
180
+ if (signal && abortListener) {
181
+ signal.removeEventListener("abort", abortListener);
182
+ }
183
+ if (signal?.aborted) {
184
+ reject(new ZapierAbortDrainSignal(`${label} aborted`));
185
+ return;
186
+ }
187
+ if (code === 0)
188
+ resolve();
189
+ else
190
+ reject(new Error(`${label} exited with code ${code} for message ${message.id}`));
191
+ });
192
+ // Subprocess can exit before reading stdin (e.g. `exit 0`), which
193
+ // breaks the pipe and surfaces EPIPE on the stdin stream. Exit code
194
+ // drives the outcome, so a write racing with subprocess exit isn't
195
+ // a failure. Swallow EPIPE; let other errors reject.
196
+ child.stdin.on("error", (err) => {
197
+ if (err.code !== "EPIPE")
198
+ reject(err);
199
+ });
200
+ child.stdin.end(JSON.stringify(message) + "\n");
201
+ });
202
+ }
203
+ export function runShellCommand(command, message, signal) {
204
+ return runSubprocess({
205
+ command,
206
+ args: [],
207
+ shell: true,
208
+ label: "exec-shell",
209
+ message,
210
+ signal,
211
+ });
212
+ }
213
+ /**
214
+ * Run a binary per message with no shell interpretation. `argv[0]` is
215
+ * the executable; the rest are passed literally as argv to the child.
216
+ * Otherwise identical to `runShellCommand`: stdin = message JSON, exit
217
+ * code 0 acks, non-zero rejects, abort signal kills the child.
218
+ */
219
+ export function runExecCommand(argv, message, signal) {
220
+ if (argv.length === 0) {
221
+ return Promise.reject(new Error("exec requires at least one element (the binary)"));
222
+ }
223
+ const [command, ...args] = argv;
224
+ return runSubprocess({
225
+ command,
226
+ args,
227
+ shell: false,
228
+ label: "exec",
229
+ message,
230
+ signal,
231
+ });
232
+ }
233
+ function describeReason(reason) {
234
+ return reason instanceof Error ? reason.message : String(reason);
235
+ }
236
+ /**
237
+ * Print a single per-message error to stderr. Used as the live
238
+ * `onError` body so failures surface as they happen (vs accumulated
239
+ * and dumped at the end, which is broken for `watch-trigger-inbox`
240
+ * where "the end" is Ctrl-C).
241
+ */
242
+ export function printDrainError(reason, message) {
243
+ console.error(chalk.red(`Error processing ${message.id}: ${describeReason(reason)}`));
244
+ }
245
+ /**
246
+ * One-line summary printed when the CLI finished a drain in a mode
247
+ * where the data array is uninformative (interactive, exec, exec-shell).
248
+ * Both the bounded and forever variants use the same format.
249
+ *
250
+ * `skipped` is the interactive-mode count of user-driven Skip choices
251
+ * (release-after-draining + let-lease-expire). Omitted when zero so the
252
+ * subprocess modes — which have no skip semantics — keep their original
253
+ * two-part summary.
254
+ */
255
+ export function printDrainSummary(counts) {
256
+ const skipped = counts.skipped ?? 0;
257
+ const total = counts.fulfilled + counts.rejected + skipped;
258
+ const parts = [`${counts.fulfilled} fulfilled`];
259
+ if (skipped > 0)
260
+ parts.push(`${skipped} skipped`);
261
+ parts.push(`${counts.rejected} rejected`);
262
+ console.log(chalk.dim(`\nProcessed ${total} message${total === 1 ? "" : "s"} (${parts.join(", ")}).`));
263
+ }
264
+ /**
265
+ * Warn on stderr that interactive mode is overriding an explicit
266
+ * `continueOnError: false`. Only fires when the caller (programmatic
267
+ * SDK consumer or future flag form that takes `=false`) opted into
268
+ * fail-fast — bare `--continue-on-error` flag absence leaves it
269
+ * `undefined` and doesn't trigger this. Interactive mode mechanically
270
+ * requires continue-on-error: the "Skip (let lease expire)" choice
271
+ * throws a plain Error to leave the message leased, and a fail-fast
272
+ * drain would tear down on the first skip.
273
+ */
274
+ export function warnInteractiveContinueOnErrorOverride() {
275
+ console.warn(chalk.yellow('Note: continueOnError=false is overridden to true in interactive mode (the "Skip (let lease expire)" choice would otherwise terminate the drain).'));
276
+ }
277
+ /**
278
+ * Throw a CliValidationError if the current process isn't attached to
279
+ * a real terminal on both stdin and stdout. Used by the interactive
280
+ * default to fail fast with a useful message instead of letting
281
+ * inquirer fall over later.
282
+ */
283
+ export function requireInteractiveTty(commandName) {
284
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
285
+ throw new ZapierCliValidationError(`${commandName} needs an interactive terminal by default. ` +
286
+ "Pass --exec '<bin>' (with optional `-- args...`) or " +
287
+ "--exec-shell '<cmd>' to run a script per message, " +
288
+ "or --json for non-interactive output.");
289
+ }
290
+ }
291
+ /**
292
+ * Reject more than one of `--exec`, `--exec-shell`, and `--json`.
293
+ * Each is a non-interactive output path and choosing implicitly is
294
+ * ambiguous.
295
+ */
296
+ export function rejectExecJsonMutex(opts) {
297
+ const picked = [
298
+ opts.exec ? "--exec" : null,
299
+ opts.execShell ? "--exec-shell" : null,
300
+ opts.json ? "--json" : null,
301
+ ].filter((x) => x !== null);
302
+ if (picked.length > 1) {
303
+ throw new ZapierCliValidationError(`${picked.join(", ")} are mutually exclusive. Pick one.`);
304
+ }
305
+ }
306
+ /**
307
+ * Pull argv tokens after the first `--` separator out of `process.argv`.
308
+ * Returns `[]` when there's no `--`. Used by drain/watch to append
309
+ * post-`--` args to the `--exec` binary, mirroring `xargs`, `gh`, and
310
+ * `npm run -- ...` semantics.
311
+ *
312
+ * Read from `process.argv` rather than threading via cli-generator
313
+ * because Commander already consumes `--` at parse time and there's
314
+ * no per-command API for "the args after `--`" exposed through the
315
+ * SDK options object.
316
+ */
317
+ export function getPostDashArgs(argv = process.argv) {
318
+ const idx = argv.indexOf("--");
319
+ if (idx === -1)
320
+ return [];
321
+ return argv.slice(idx + 1);
322
+ }
323
+ /**
324
+ * Combine an optional user signal with an internal one (typically the
325
+ * SIGINT controller) into a single AbortSignal that fires when either
326
+ * source aborts. Returns a `dispose` to detach the listeners; safe to
327
+ * call regardless of whether the combined signal fired.
328
+ *
329
+ * Plain shape rather than the SDK's `combineAbortSignals`, which works
330
+ * over `DisposableAbortSignal` handles and returns a different shape.
331
+ */
332
+ export function combineSignals(a, b) {
333
+ if (!a) {
334
+ return { signal: b, dispose: () => undefined };
335
+ }
336
+ const controller = new AbortController();
337
+ const onAbort = () => controller.abort();
338
+ if (a.aborted || b.aborted)
339
+ controller.abort();
340
+ else {
341
+ a.addEventListener("abort", onAbort, { once: true });
342
+ b.addEventListener("abort", onAbort, { once: true });
343
+ }
344
+ return {
345
+ signal: controller.signal,
346
+ dispose: () => {
347
+ a.removeEventListener("abort", onAbort);
348
+ b.removeEventListener("abort", onAbort);
349
+ },
350
+ };
351
+ }