@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.
- package/CHANGELOG.md +12 -0
- package/README.md +381 -29
- package/bin/zapier-sdk-experimental.mjs +14 -0
- package/dist/cli.cjs +585 -37
- package/dist/cli.mjs +584 -36
- package/dist/experimental.cjs +3519 -0
- package/dist/experimental.d.mts +39 -0
- package/dist/experimental.d.ts +39 -0
- package/dist/experimental.mjs +3483 -0
- package/dist/index.cjs +507 -26
- package/dist/index.d.mts +3 -514
- package/dist/index.d.ts +3 -514
- package/dist/index.mjs +505 -24
- package/dist/package.json +14 -2
- package/dist/sdk-B3nKAZdN.d.mts +516 -0
- package/dist/sdk-B3nKAZdN.d.ts +516 -0
- package/dist/src/cli.js +26 -2
- package/dist/src/experimental.d.ts +33 -0
- package/dist/src/experimental.js +83 -0
- package/dist/src/generators/ast-generator.d.ts +2 -2
- package/dist/src/generators/ast-generator.js +1 -1
- package/dist/src/plugins/add/index.d.ts +2 -2
- package/dist/src/plugins/bundleCode/index.js +3 -12
- package/dist/src/plugins/curl/index.js +2 -2
- package/dist/src/plugins/curl/utils.d.ts +11 -1
- package/dist/src/plugins/curl/utils.js +14 -5
- package/dist/src/plugins/drainTriggerInbox/index.d.ts +46 -0
- package/dist/src/plugins/drainTriggerInbox/index.js +178 -0
- package/dist/src/plugins/generateAppTypes/index.d.ts +2 -2
- package/dist/src/plugins/index.d.ts +2 -0
- package/dist/src/plugins/index.js +2 -0
- package/dist/src/plugins/mcp/index.d.ts +1 -0
- package/dist/src/plugins/mcp/index.js +5 -1
- package/dist/src/plugins/watchTriggerInbox/index.d.ts +45 -0
- package/dist/src/plugins/watchTriggerInbox/index.js +157 -0
- package/dist/src/sdk.js +5 -1
- package/dist/src/utils/cli-generator.js +18 -1
- package/dist/src/utils/cli-renderer.d.ts +12 -0
- package/dist/src/utils/cli-renderer.js +22 -1
- package/dist/src/utils/parameter-resolver.d.ts +1 -0
- package/dist/src/utils/parameter-resolver.js +55 -9
- package/dist/src/utils/triggerDrain.d.ts +144 -0
- package/dist/src/utils/triggerDrain.js +351 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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
|
+
}
|