@zapier/zapier-sdk-cli 0.36.3 → 0.37.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/dist/src/cli.js CHANGED
@@ -5,6 +5,8 @@ import { createZapierCliSdk } from "./sdk";
5
5
  import packageJson from "../package.json" with { type: "json" };
6
6
  import { ZapierCliError } from "./utils/errors";
7
7
  import { checkAndNotifyUpdates } from "./utils/version-checker";
8
+ import { BaseSdkOptionsSchema } from "@zapier/zapier-sdk";
9
+ import { z } from "zod";
8
10
  import { ReservedCliParameter, getReservedCliOption, } from "./utils/cli-options";
9
11
  const EXIT_GRACE_PERIOD_MS = 500;
10
12
  const program = new Command();
@@ -22,8 +24,24 @@ program
22
24
  .option("--credentials-base-url <url>", "Base URL for authentication endpoints")
23
25
  .option("--tracking-base-url <url>", "Base URL for Zapier tracking endpoints")
24
26
  .option("--max-network-retries <count>", "Max retries for rate-limited requests (default: 3)")
25
- .option("--max-network-retry-delay-ms <ms>", "Max delay in ms to wait for rate limit retry (default: 60000)")
26
- .helpOption(`${helpOption.short}, ${helpOption.flag}`, helpOption.description);
27
+ .option("--max-network-retry-delay-ms <ms>", "Max delay in ms to wait for rate limit retry (default: 60000)");
28
+ // Dynamically register boolean flags from BaseSdkOptionsSchema
29
+ const booleanFlags = [];
30
+ for (const [key, fieldSchema] of Object.entries(
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ BaseSdkOptionsSchema.shape)) {
33
+ let inner = fieldSchema;
34
+ if (inner instanceof z.ZodOptional) {
35
+ inner = inner._zod.def.innerType;
36
+ }
37
+ if (inner instanceof z.ZodBoolean && key !== "debug") {
38
+ const kebab = key.replace(/([A-Z])/g, "-$1").toLowerCase();
39
+ const description = fieldSchema._zod?.def?.description ?? "";
40
+ booleanFlags.push({ camelName: key, kebabFlag: `--${kebab}` });
41
+ program.option(`--${kebab}`, description);
42
+ }
43
+ }
44
+ program.helpOption(`${helpOption.short}, ${helpOption.flag}`, helpOption.description);
27
45
  // Check for debug flag early (support both flag and env var)
28
46
  const isDebugMode = process.env.DEBUG === "true" || process.argv.includes("--debug");
29
47
  // Helper to get flag value from argv
@@ -77,6 +95,13 @@ function buildCredentialsFromFlags() {
77
95
  return undefined;
78
96
  }
79
97
  const credentials = buildCredentialsFromFlags();
98
+ // Extract boolean flags from argv
99
+ const flagOverrides = {};
100
+ for (const { camelName, kebabFlag } of booleanFlags) {
101
+ if (process.argv.includes(kebabFlag)) {
102
+ flagOverrides[camelName] = true;
103
+ }
104
+ }
80
105
  // Create CLI SDK instance with all plugins and URL options
81
106
  const sdk = createZapierCliSdk({
82
107
  debug: isDebugMode,
@@ -85,6 +110,7 @@ const sdk = createZapierCliSdk({
85
110
  trackingBaseUrl,
86
111
  maxNetworkRetries,
87
112
  maxNetworkRetryDelayMs,
113
+ ...flagOverrides,
88
114
  });
89
115
  // Auth commands now handled by plugins
90
116
  // Generate CLI commands from SDK schemas (including CLI plugins)
@@ -3,7 +3,7 @@ import chalk from "chalk";
3
3
  import ora from "ora";
4
4
  import { z } from "zod";
5
5
  import { runWithTelemetryContext, } from "@zapier/zapier-sdk";
6
- import { isPositional } from "@zapier/zapier-sdk";
6
+ import { isPositional, buildCapabilityMessage } from "@zapier/zapier-sdk";
7
7
  import { ZapierCliUserCancellationError, ZapierCliMissingParametersError, ZapierCliValidationError, } from "./errors";
8
8
  // Zod validation errors are structured objects; flatten them into a
9
9
  // readable "field: reason" string for CLI error messages.
@@ -146,9 +146,11 @@ export class SchemaParameterResolver {
146
146
  else {
147
147
  for (const param of orderedRequiredParams) {
148
148
  try {
149
- const value = await this.resolveParameter(param, context, functionName);
150
- this.setNestedValue(resolvedParams, param.path, value);
151
- context.resolvedParams = resolvedParams;
149
+ const value = await this.resolveParameter(param, context, functionName, { isOptional: !param.isRequired });
150
+ if (param.isRequired || value !== undefined) {
151
+ this.setNestedValue(resolvedParams, param.path, value);
152
+ context.resolvedParams = resolvedParams;
153
+ }
152
154
  }
153
155
  catch (error) {
154
156
  if (this.isUserCancellation(error)) {
@@ -325,28 +327,116 @@ export class SchemaParameterResolver {
325
327
  }
326
328
  this.debugLog(`Fetching options for ${promptLabel}`);
327
329
  const fetchResult = await dynamicResolver.fetch(context.sdk, context.resolvedParams);
328
- const items = Array.isArray(fetchResult)
329
- ? fetchResult
330
- : (fetchResult?.data ?? []);
331
- const promptConfig = dynamicResolver.prompt(items, context.resolvedParams);
332
- promptConfig.name = promptName;
333
- // Inject a skip option for optional parameters
330
+ // Check if the result is an AsyncIterable of pages (from toIterable())
331
+ let pageIterator = null;
332
+ let items;
333
+ let hasMore = false;
334
+ if (fetchResult != null &&
335
+ typeof fetchResult === "object" &&
336
+ Symbol.asyncIterator in fetchResult) {
337
+ // Paginated: pull the first page from the iterator
338
+ pageIterator = fetchResult[Symbol.asyncIterator]();
339
+ const first = await pageIterator.next();
340
+ if (!first.done && first.value) {
341
+ items = first.value.data;
342
+ hasMore = first.value.nextCursor != null;
343
+ }
344
+ else {
345
+ items = [];
346
+ }
347
+ }
348
+ else if (fetchResult != null &&
349
+ typeof fetchResult === "object" &&
350
+ "data" in fetchResult) {
351
+ const page = fetchResult;
352
+ items = page.data;
353
+ hasMore = page.nextCursor != null;
354
+ if (hasMore) {
355
+ this.debugLog(`Resolver for ${promptLabel} has more pages but no iterator. ` +
356
+ `Use toIterable() to enable "Load more..." support.`);
357
+ }
358
+ }
359
+ else {
360
+ items = fetchResult || [];
361
+ pageIterator = null;
362
+ }
363
+ const LOAD_MORE_SENTINEL = Symbol("LOAD_MORE");
364
+ const SKIP_SENTINEL = Symbol("SKIP");
365
+ let newItemsStartIndex = -1;
334
366
  this.stopSpinner();
335
- if (isOptional && promptConfig.choices) {
336
- const SKIP_SENTINEL = Symbol("SKIP");
337
- promptConfig.choices = [
338
- { name: chalk.dim("(Skip)"), value: SKIP_SENTINEL },
339
- ...promptConfig.choices,
340
- ];
367
+ while (true) {
368
+ const promptConfig = dynamicResolver.prompt(items, context.resolvedParams);
369
+ promptConfig.name = promptName;
370
+ if (isOptional && promptConfig.choices) {
371
+ promptConfig.choices.unshift({
372
+ name: chalk.dim("(Skip)"),
373
+ value: SKIP_SENTINEL,
374
+ });
375
+ }
376
+ // Add "Load more..." option if paginated
377
+ if (hasMore && pageIterator && promptConfig.choices) {
378
+ promptConfig.choices.push({
379
+ name: chalk.dim("(Load more...)"),
380
+ value: LOAD_MORE_SENTINEL,
381
+ });
382
+ }
383
+ // Show hints for capabilities that would expand results
384
+ if (!hasMore &&
385
+ promptConfig.choices &&
386
+ dynamicResolver.requireCapabilities) {
387
+ const capContext = context.sdk.getContext();
388
+ if (capContext.hasCapability) {
389
+ for (const cap of dynamicResolver.requireCapabilities) {
390
+ const enabled = await capContext.hasCapability(cap);
391
+ if (!enabled) {
392
+ promptConfig.choices.push({
393
+ name: chalk.dim(buildCapabilityMessage(cap)),
394
+ value: SKIP_SENTINEL,
395
+ disabled: true,
396
+ });
397
+ }
398
+ }
399
+ }
400
+ }
401
+ // After loading more, jump cursor to first new item.
402
+ // Offset by the number of injected choices before the data items (e.g. Skip).
403
+ if (newItemsStartIndex >= 0 && promptConfig.choices) {
404
+ const injectedBefore = isOptional ? 1 : 0;
405
+ const adjustedIndex = newItemsStartIndex + injectedBefore;
406
+ if (promptConfig.choices[adjustedIndex]) {
407
+ promptConfig.default = promptConfig.choices[adjustedIndex].value;
408
+ }
409
+ newItemsStartIndex = -1;
410
+ }
341
411
  const answers = await inquirer.prompt([promptConfig]);
342
- const value = answers[promptName];
343
- if (value === SKIP_SENTINEL) {
412
+ let selected = answers[promptName];
413
+ if (selected === SKIP_SENTINEL) {
344
414
  return undefined;
345
415
  }
346
- return value;
416
+ const wantsMore = Array.isArray(selected)
417
+ ? selected.includes(LOAD_MORE_SENTINEL)
418
+ : selected === LOAD_MORE_SENTINEL;
419
+ if (wantsMore && pageIterator) {
420
+ if (Array.isArray(selected)) {
421
+ selected = selected.filter((v) => v !== LOAD_MORE_SENTINEL);
422
+ }
423
+ const prevLength = items.length;
424
+ this.startSpinner();
425
+ this.debugLog("Fetching more options...");
426
+ const next = await pageIterator.next();
427
+ this.stopSpinner();
428
+ if (!next.done && next.value) {
429
+ items = [...items, ...next.value.data];
430
+ hasMore = next.value.nextCursor != null;
431
+ newItemsStartIndex = prevLength;
432
+ }
433
+ else {
434
+ hasMore = false;
435
+ }
436
+ continue;
437
+ }
438
+ return selected;
347
439
  }
348
- const answers = await inquirer.prompt([promptConfig]);
349
- return answers[promptName];
350
440
  }
351
441
  else if (resolver.type === "fields") {
352
442
  if (isOptional && !inArrayContext) {