@zapier/zapier-sdk-cli 0.35.0 → 0.36.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.
@@ -1,5 +1,6 @@
1
1
  import inquirer from "inquirer";
2
2
  import chalk from "chalk";
3
+ import ora from "ora";
3
4
  import { z } from "zod";
4
5
  import { runWithTelemetryContext, } from "@zapier/zapier-sdk";
5
6
  import { isPositional } from "@zapier/zapier-sdk";
@@ -61,9 +62,31 @@ function getLocalResolutionOrderForParams(paramNames, resolvers) {
61
62
  // Schema Parameter Resolver
62
63
  // ============================================================================
63
64
  export class SchemaParameterResolver {
65
+ constructor() {
66
+ this.debug = false;
67
+ this.spinner = null;
68
+ }
69
+ debugLog(message) {
70
+ if (this.debug) {
71
+ this.stopSpinner();
72
+ console.log(chalk.gray(`[Zapier CLI] ${message}`));
73
+ }
74
+ }
75
+ startSpinner() {
76
+ if (!this.debug && !this.spinner) {
77
+ this.spinner = ora({ text: "", spinner: "dots" }).start();
78
+ }
79
+ }
80
+ stopSpinner() {
81
+ if (this.spinner) {
82
+ this.spinner.stop();
83
+ this.spinner = null;
84
+ }
85
+ }
64
86
  async resolveParameters(schema, providedParams, sdk, functionName, options) {
65
87
  return runWithTelemetryContext(async () => {
66
- const interactiveMode = options?.interactiveMode ?? true;
88
+ this.debug = options?.debug ?? false;
89
+ const interactiveMode = (options?.interactiveMode ?? true) && !!process.stdin.isTTY;
67
90
  // 1. Try to parse with current parameters
68
91
  const parseResult = schema.safeParse(providedParams);
69
92
  // Get all schema parameters to check which ones have resolvers
@@ -74,47 +97,28 @@ export class SchemaParameterResolver {
74
97
  const hasValue = this.getNestedValue(providedParams, param.path) !== undefined;
75
98
  return !hasValue;
76
99
  });
77
- // Determine parameter resolution categories:
78
- // - functionally required: must be provided (inputs)
79
- // - always prompt: should be prompted for but can be skipped (connectionId)
80
- // - truly optional: only ask if user wants to be prompted
81
- const functionallyRequired = missingResolvable.filter((param) => {
82
- // Schema-required parameters are always functionally required
100
+ // Split missing resolvable params into required vs optional.
101
+ // "inputs" is treated as required in interactive mode.
102
+ const required = missingResolvable.filter((param) => {
83
103
  if (param.isRequired)
84
104
  return true;
85
- // Keep inputs prompting in interactive mode, but do not force it in
86
- // non-interactive mode (--json), where optional inputs should remain optional.
87
- if (param.name === "inputs") {
105
+ if (param.name === "inputs")
88
106
  return interactiveMode;
89
- }
90
- return false;
91
- });
92
- // Parameters that should always be prompted for directly, but can be skipped
93
- const alwaysPrompt = missingResolvable.filter((param) => {
94
- if (functionallyRequired.includes(param))
95
- return false;
96
- // connectionId should always be prompted for (since it's usually needed)
97
- // but can be skipped with "Continue without connection"
98
- if (param.name === "connectionId") {
99
- return true;
100
- }
101
107
  return false;
102
108
  });
103
- const trulyOptional = missingResolvable.filter((param) => !functionallyRequired.includes(param) &&
104
- !alwaysPrompt.includes(param));
109
+ const optional = missingResolvable.filter((param) => !required.includes(param));
105
110
  if (parseResult.success &&
106
- functionallyRequired.length === 0 &&
107
- alwaysPrompt.length === 0) {
111
+ required.length === 0 &&
112
+ optional.length === 0) {
108
113
  return parseResult.data;
109
114
  }
110
- if (functionallyRequired.length === 0 && alwaysPrompt.length === 0) {
111
- // No functionally required parameters missing, but check if we can parse
115
+ if (required.length === 0 && optional.length === 0) {
112
116
  if (!parseResult.success) {
113
117
  throw new ZapierCliValidationError(formatZodError(parseResult.error));
114
118
  }
115
119
  return parseResult.data;
116
120
  }
117
- // 2. Resolve functionally required parameters first
121
+ // 2. Resolve required parameters
118
122
  const resolvedParams = { ...providedParams };
119
123
  const context = {
120
124
  sdk,
@@ -122,24 +126,16 @@ export class SchemaParameterResolver {
122
126
  resolvedParams,
123
127
  functionName,
124
128
  };
125
- // Get local resolvers for this function
126
129
  const localResolvers = this.getLocalResolvers(sdk, functionName);
127
- if (functionallyRequired.length > 0) {
128
- const requiredParamNames = functionallyRequired.map((p) => p.name);
130
+ if (required.length > 0) {
131
+ const requiredParamNames = required.map((p) => p.name);
129
132
  const requiredResolutionOrder = getLocalResolutionOrderForParams(requiredParamNames, localResolvers);
130
133
  // Find all parameters that need to be resolved (including dependencies)
131
- // from the available resolvable parameters
132
134
  const orderedRequiredParams = requiredResolutionOrder
133
135
  .map((paramName) => {
134
- // First try to find in functionally required
135
- let param = functionallyRequired.find((p) => p.name === paramName);
136
- // If not found, try always prompt (for dependencies like connectionId)
136
+ let param = required.find((p) => p.name === paramName);
137
137
  if (!param) {
138
- param = alwaysPrompt.find((p) => p.name === paramName);
139
- }
140
- // If not found, try truly optional (for other dependencies)
141
- if (!param) {
142
- param = trulyOptional.find((p) => p.name === paramName);
138
+ param = optional.find((p) => p.name === paramName);
143
139
  }
144
140
  return param;
145
141
  })
@@ -152,7 +148,6 @@ export class SchemaParameterResolver {
152
148
  try {
153
149
  const value = await this.resolveParameter(param, context, functionName);
154
150
  this.setNestedValue(resolvedParams, param.path, value);
155
- // Update context with newly resolved value
156
151
  context.resolvedParams = resolvedParams;
157
152
  }
158
153
  catch (error) {
@@ -164,26 +159,25 @@ export class SchemaParameterResolver {
164
159
  }
165
160
  }
166
161
  }
167
- // Remove resolved dependencies from other categories to avoid double-prompting
162
+ // Remove resolved dependencies from optional to avoid double-prompting
168
163
  const resolvedParamNames = new Set(orderedRequiredParams.map((p) => p.name));
169
- alwaysPrompt.splice(0, alwaysPrompt.length, ...alwaysPrompt.filter((p) => !resolvedParamNames.has(p.name)));
170
- trulyOptional.splice(0, trulyOptional.length, ...trulyOptional.filter((p) => !resolvedParamNames.has(p.name)));
171
- }
172
- // 3. Resolve parameters that should always be prompted for (but can be skipped).
173
- // Skipped entirely in non-interactive mode - if connectionId was needed, it should
174
- // have been passed explicitly via --connection-id.
175
- if (interactiveMode && alwaysPrompt.length > 0) {
176
- const alwaysPromptNames = alwaysPrompt.map((p) => p.name);
177
- const alwaysPromptResolutionOrder = getLocalResolutionOrderForParams(alwaysPromptNames, localResolvers);
178
- const orderedAlwaysPromptParams = alwaysPromptResolutionOrder
179
- .map((paramName) => alwaysPrompt.find((p) => p.name === paramName))
164
+ optional.splice(0, optional.length, ...optional.filter((p) => !resolvedParamNames.has(p.name)));
165
+ }
166
+ // 3. Resolve optional parameters individually (each is skippable).
167
+ // Skipped in non-interactive mode.
168
+ if (interactiveMode && optional.length > 0) {
169
+ const optionalParamNames = optional.map((p) => p.name);
170
+ const optionalResolutionOrder = getLocalResolutionOrderForParams(optionalParamNames, localResolvers);
171
+ const orderedOptionalParams = optionalResolutionOrder
172
+ .map((paramName) => optional.find((p) => p.name === paramName))
180
173
  .filter((param) => param !== undefined);
181
- for (const param of orderedAlwaysPromptParams) {
174
+ for (const param of orderedOptionalParams) {
182
175
  try {
183
- const value = await this.resolveParameter(param, context, functionName);
184
- this.setNestedValue(resolvedParams, param.path, value);
185
- // Update context with newly resolved value
186
- context.resolvedParams = resolvedParams;
176
+ const value = await this.resolveParameter(param, context, functionName, { isOptional: true });
177
+ if (value !== undefined) {
178
+ this.setNestedValue(resolvedParams, param.path, value);
179
+ context.resolvedParams = resolvedParams;
180
+ }
187
181
  }
188
182
  catch (error) {
189
183
  if (this.isUserCancellation(error)) {
@@ -194,42 +188,7 @@ export class SchemaParameterResolver {
194
188
  }
195
189
  }
196
190
  }
197
- // 4. Ask user if they want to resolve truly optional parameters (skipped in non-interactive mode)
198
- if (interactiveMode && trulyOptional.length > 0) {
199
- const optionalNames = trulyOptional.map((p) => p.name).join(", ");
200
- const shouldResolveOptional = await inquirer.prompt([
201
- {
202
- type: "confirm",
203
- name: "resolveOptional",
204
- message: `Would you like to be prompted for optional parameters (${optionalNames})?`,
205
- default: false,
206
- },
207
- ]);
208
- if (shouldResolveOptional.resolveOptional) {
209
- // Resolve optional parameters using their resolvers
210
- const optionalParamNames = trulyOptional.map((p) => p.name);
211
- const optionalResolutionOrder = getLocalResolutionOrderForParams(optionalParamNames, localResolvers);
212
- const orderedOptionalParams = optionalResolutionOrder
213
- .map((paramName) => trulyOptional.find((p) => p.name === paramName))
214
- .filter((param) => param !== undefined);
215
- for (const param of orderedOptionalParams) {
216
- try {
217
- const value = await this.resolveParameter(param, context, functionName);
218
- this.setNestedValue(resolvedParams, param.path, value);
219
- // Update context with newly resolved value
220
- context.resolvedParams = resolvedParams;
221
- }
222
- catch (error) {
223
- if (this.isUserCancellation(error)) {
224
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
225
- throw new ZapierCliUserCancellationError();
226
- }
227
- throw error;
228
- }
229
- }
230
- }
231
- }
232
- // 5. Validate final parameters
191
+ // 4. Validate final parameters
233
192
  const finalResult = schema.safeParse(resolvedParams);
234
193
  if (!finalResult.success) {
235
194
  throw new ZapierCliValidationError(`Parameter validation failed: ${formatZodError(finalResult.error)}`);
@@ -321,48 +280,97 @@ export class SchemaParameterResolver {
321
280
  throw new ZapierCliMissingParametersError(missingParams);
322
281
  }
323
282
  }
324
- async resolveParameter(param, context, functionName) {
283
+ async resolveParameter(param, context, functionName, options) {
325
284
  const resolver = this.getResolver(param.name, context.sdk, functionName);
326
285
  if (!resolver) {
327
286
  throw new Error(`No resolver found for parameter: ${param.name}`);
328
287
  }
329
- console.log(chalk.blue(`\n🔍 Resolving ${param.name}...`));
288
+ return this.resolveWithResolver(resolver, param, context, {
289
+ isOptional: options?.isOptional,
290
+ });
291
+ }
292
+ async resolveWithResolver(resolver, param, context, options = {}) {
293
+ const { arrayIndex, isOptional } = options;
294
+ const inArrayContext = arrayIndex != null;
295
+ const promptLabel = inArrayContext
296
+ ? `${param.name}[${arrayIndex}]`
297
+ : param.name;
298
+ const promptName = inArrayContext ? "value" : param.name;
299
+ this.debugLog(`Resolving ${promptLabel}${isOptional ? " (optional)" : ""}`);
330
300
  if (resolver.type === "static") {
331
301
  const staticResolver = resolver;
332
302
  const promptConfig = {
333
303
  type: staticResolver.inputType === "password" ? "password" : "input",
334
- name: param.name,
335
- message: `Enter ${param.name}:`,
304
+ name: promptName,
305
+ message: `Enter ${promptLabel}${isOptional ? " (optional)" : ""}:`,
336
306
  ...(staticResolver.placeholder && {
337
307
  default: staticResolver.placeholder,
338
308
  }),
339
309
  };
310
+ this.stopSpinner();
340
311
  const answers = await inquirer.prompt([promptConfig]);
341
- return answers[param.name];
312
+ const value = answers[promptName];
313
+ if (isOptional && (value === undefined || value === "")) {
314
+ return undefined;
315
+ }
316
+ return value;
342
317
  }
343
318
  else if (resolver.type === "dynamic") {
344
319
  const dynamicResolver = resolver;
320
+ this.startSpinner();
345
321
  const autoResolution = await this.tryAutoResolve(dynamicResolver, context);
346
322
  if (autoResolution != null) {
323
+ this.stopSpinner();
347
324
  return autoResolution.resolvedValue;
348
325
  }
349
- // Only show "Fetching..." for required parameters that typically have many options
350
- if (param.isRequired && param.name !== "connectionId") {
351
- console.log(chalk.gray(`Fetching options for ${param.name}...`));
326
+ this.debugLog(`Fetching options for ${promptLabel}`);
327
+ 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
334
+ 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
+ ];
341
+ const answers = await inquirer.prompt([promptConfig]);
342
+ const value = answers[promptName];
343
+ if (value === SKIP_SENTINEL) {
344
+ return undefined;
345
+ }
346
+ return value;
352
347
  }
353
- const items = await dynamicResolver.fetch(context.sdk, context.resolvedParams);
354
- // Let the resolver's prompt handle empty lists (e.g., connectionId can show "skip connection")
355
- const safeItems = items || [];
356
- const promptConfig = dynamicResolver.prompt(safeItems, context.resolvedParams);
357
348
  const answers = await inquirer.prompt([promptConfig]);
358
- return answers[param.name];
349
+ return answers[promptName];
359
350
  }
360
351
  else if (resolver.type === "fields") {
361
- return await this.resolveFieldsRecursively(resolver, context, param);
352
+ if (isOptional && !inArrayContext) {
353
+ this.stopSpinner();
354
+ const { confirm } = await inquirer.prompt([
355
+ {
356
+ type: "confirm",
357
+ name: "confirm",
358
+ message: `Add ${promptLabel}?`,
359
+ default: false,
360
+ },
361
+ ]);
362
+ if (!confirm) {
363
+ return undefined;
364
+ }
365
+ }
366
+ return await this.resolveFieldsRecursively(resolver, context, param, { inArrayContext });
367
+ }
368
+ else if (resolver.type === "array") {
369
+ return await this.resolveArrayRecursively(resolver, context, param);
362
370
  }
363
- throw new Error(`Unknown resolver type for ${param.name}`);
371
+ throw new Error(`Unknown resolver type for ${promptLabel}`);
364
372
  }
365
- async resolveFieldsRecursively(resolver, context, param) {
373
+ async resolveFieldsRecursively(resolver, context, param, options = {}) {
366
374
  const inputs = {};
367
375
  let processedFieldKeys = new Set();
368
376
  let iteration = 0;
@@ -377,8 +385,10 @@ export class SchemaParameterResolver {
377
385
  inputs,
378
386
  },
379
387
  };
380
- console.log(chalk.gray(`Fetching input fields for ${param.name}${iteration > 1 ? ` (iteration ${iteration})` : ""}...`));
388
+ this.debugLog(`Fetching input fields for ${param.name}${iteration > 1 ? ` (iteration ${iteration})` : ""}`);
389
+ this.startSpinner();
381
390
  const rootFieldItems = await resolver.fetch(updatedContext.sdk, updatedContext.resolvedParams);
391
+ this.stopSpinner();
382
392
  if (!rootFieldItems || rootFieldItems.length === 0) {
383
393
  if (iteration === 1) {
384
394
  console.log(chalk.yellow(`No input fields required for this action.`));
@@ -386,7 +396,7 @@ export class SchemaParameterResolver {
386
396
  break;
387
397
  }
388
398
  // Process fields recursively, maintaining fieldset structure
389
- const fieldStats = await this.processFieldItems(rootFieldItems, inputs, processedFieldKeys, [], iteration, updatedContext);
399
+ const fieldStats = await this.processFieldItems(rootFieldItems, inputs, processedFieldKeys, [], iteration, updatedContext, { inArrayContext: options.inArrayContext });
390
400
  // If no new fields were processed, we're done
391
401
  if (fieldStats.newRequired === 0 && fieldStats.newOptional === 0) {
392
402
  break;
@@ -399,13 +409,56 @@ export class SchemaParameterResolver {
399
409
  if (iteration >= maxIterations) {
400
410
  console.log(chalk.yellow(`\n⚠️ Maximum field resolution iterations reached. Some dynamic fields may not have been discovered.`));
401
411
  }
412
+ // Apply transform if provided
413
+ if (resolver.transform) {
414
+ return resolver.transform(inputs);
415
+ }
402
416
  return inputs;
403
417
  }
418
+ /**
419
+ * Resolves an array parameter by repeatedly prompting for items until user says no
420
+ */
421
+ async resolveArrayRecursively(resolver, context, param) {
422
+ const items = [];
423
+ const minItems = resolver.minItems ?? 0;
424
+ const maxItems = resolver.maxItems ?? Infinity;
425
+ while (items.length < maxItems) {
426
+ const currentIndex = items.length;
427
+ // Skip confirmation if we haven't hit minItems yet - just collect the item
428
+ if (currentIndex >= minItems) {
429
+ this.stopSpinner();
430
+ const confirmAnswer = await inquirer.prompt([
431
+ {
432
+ type: "confirm",
433
+ name: "addItem",
434
+ message: `Add ${param.name}[${currentIndex}]?`,
435
+ default: false,
436
+ },
437
+ ]);
438
+ if (!confirmAnswer.addItem) {
439
+ break;
440
+ }
441
+ }
442
+ // Fetch the inner resolver for this item
443
+ const innerResolver = await resolver.fetch(context.sdk, context.resolvedParams);
444
+ const itemValue = await this.resolveWithResolver(innerResolver, param, context, { arrayIndex: currentIndex });
445
+ items.push(itemValue);
446
+ // Update context with current array for subsequent iterations
447
+ context.resolvedParams = {
448
+ ...context.resolvedParams,
449
+ [param.name]: items,
450
+ };
451
+ }
452
+ if (items.length >= maxItems) {
453
+ console.log(chalk.gray(`Maximum of ${maxItems} items reached.`));
454
+ }
455
+ return items;
456
+ }
404
457
  /**
405
458
  * Recursively processes fieldsets and their fields, maintaining natural structure
406
459
  * and creating nested inputs as needed (e.g., fieldset "foo" becomes inputs.foo = [{}])
407
460
  */
408
- async processFieldItems(items, targetInputs, processedFieldKeys, fieldsetPath = [], iteration = 1, context) {
461
+ async processFieldItems(items, targetInputs, processedFieldKeys, fieldsetPath = [], iteration = 1, context, options = {}) {
409
462
  let newRequiredCount = 0;
410
463
  let newOptionalCount = 0;
411
464
  let optionalSkipped = false;
@@ -423,7 +476,7 @@ export class SchemaParameterResolver {
423
476
  // Process fields within this fieldset recursively
424
477
  const fieldsetTarget = targetInputs[typedItem.key][0];
425
478
  const nestedPath = [...fieldsetPath, fieldsetTitle];
426
- const nestedStats = await this.processFieldItems(typedItem.fields, fieldsetTarget, processedFieldKeys, nestedPath, iteration, context);
479
+ const nestedStats = await this.processFieldItems(typedItem.fields, fieldsetTarget, processedFieldKeys, nestedPath, iteration, context, options);
427
480
  newRequiredCount += nestedStats.newRequired;
428
481
  newOptionalCount += nestedStats.newOptional;
429
482
  if (nestedStats.optionalSkipped) {
@@ -439,11 +492,18 @@ export class SchemaParameterResolver {
439
492
  if (isRequired) {
440
493
  // Process required field immediately
441
494
  newRequiredCount++;
442
- if (newRequiredCount === 1 && fieldsetPath.length === 0) {
443
- // Only show this message once at root level
444
- console.log(chalk.blue(`\n📝 Please provide values for the following ${iteration === 1 ? "" : "additional "}input fields:`));
495
+ if (typedItem.resolver && context) {
496
+ const param = {
497
+ name: typedItem.key,
498
+ path: [typedItem.key],
499
+ schema: z.unknown(),
500
+ isRequired: true,
501
+ };
502
+ targetInputs[typedItem.key] = await this.resolveWithResolver(typedItem.resolver, param, context, { isOptional: false });
503
+ }
504
+ else {
505
+ await this.promptForField(typedItem, targetInputs, context);
445
506
  }
446
- await this.promptForField(typedItem, targetInputs, context);
447
507
  processedFieldKeys.add(typedItem.key);
448
508
  }
449
509
  else {
@@ -464,39 +524,49 @@ export class SchemaParameterResolver {
464
524
  });
465
525
  if (optionalFields.length > 0) {
466
526
  const pathContext = fieldsetPath.length > 0 ? ` in ${fieldsetPath.join(" > ")}` : "";
467
- console.log(chalk.gray(`\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`));
468
- try {
469
- const shouldConfigureOptional = await inquirer.prompt([
470
- {
471
- type: "confirm",
472
- name: "configure",
473
- message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`,
474
- default: false,
475
- },
476
- ]);
477
- if (shouldConfigureOptional.configure) {
478
- console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
479
- for (const field of optionalFields) {
480
- await this.promptForField(field, targetInputs, context);
481
- const typedField = field;
482
- processedFieldKeys.add(typedField.key);
483
- }
484
- }
485
- else {
486
- optionalSkipped = true;
487
- // Mark these fields as processed even if skipped to avoid re-asking
488
- optionalFields.forEach((field) => {
489
- const typedField = field;
490
- processedFieldKeys.add(typedField.key);
491
- });
527
+ // In array context, prompt for all fields directly without confirmation
528
+ if (options.inArrayContext) {
529
+ for (const field of optionalFields) {
530
+ await this.promptForField(field, targetInputs, context);
531
+ const typedField = field;
532
+ processedFieldKeys.add(typedField.key);
492
533
  }
493
534
  }
494
- catch (error) {
495
- if (this.isUserCancellation(error)) {
496
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
497
- throw new ZapierCliUserCancellationError();
535
+ else {
536
+ console.log(chalk.gray(`\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`));
537
+ try {
538
+ const shouldConfigureOptional = await inquirer.prompt([
539
+ {
540
+ type: "confirm",
541
+ name: "configure",
542
+ message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`,
543
+ default: false,
544
+ },
545
+ ]);
546
+ if (shouldConfigureOptional.configure) {
547
+ console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
548
+ for (const field of optionalFields) {
549
+ await this.promptForField(field, targetInputs, context);
550
+ const typedField = field;
551
+ processedFieldKeys.add(typedField.key);
552
+ }
553
+ }
554
+ else {
555
+ optionalSkipped = true;
556
+ // Mark these fields as processed even if skipped to avoid re-asking
557
+ optionalFields.forEach((field) => {
558
+ const typedField = field;
559
+ processedFieldKeys.add(typedField.key);
560
+ });
561
+ }
562
+ }
563
+ catch (error) {
564
+ if (this.isUserCancellation(error)) {
565
+ console.log(chalk.yellow("\n\nOperation cancelled by user"));
566
+ throw new ZapierCliUserCancellationError();
567
+ }
568
+ throw error;
498
569
  }
499
- throw error;
500
570
  }
501
571
  }
502
572
  }
@@ -533,9 +603,10 @@ export class SchemaParameterResolver {
533
603
  isRequired: fieldObj.is_required || false,
534
604
  defaultValue: fieldObj.default_value ?? fieldObj.default,
535
605
  valueType,
536
- hasDropdown: fieldObj.format === "SELECT",
606
+ hasDropdown: fieldObj.format === "SELECT" || Boolean(fieldObj.choices),
537
607
  isMultiSelect: Boolean(valueType === "array" ||
538
608
  (fieldObj.items && fieldObj.items.type !== undefined)),
609
+ inlineChoices: fieldObj.choices,
539
610
  };
540
611
  }
541
612
  /**
@@ -543,9 +614,10 @@ export class SchemaParameterResolver {
543
614
  */
544
615
  async fetchChoices(fieldMeta, inputs, context, cursor) {
545
616
  try {
546
- console.log(chalk.gray(cursor
547
- ? ` Fetching more choices...`
548
- : ` Fetching choices for ${fieldMeta.title}...`));
617
+ this.debugLog(cursor
618
+ ? `Fetching more choices for ${fieldMeta.title}`
619
+ : `Fetching choices for ${fieldMeta.title}`);
620
+ this.startSpinner();
549
621
  const page = await context.sdk.listInputFieldChoices({
550
622
  appKey: context.resolvedParams.appKey,
551
623
  actionKey: context.resolvedParams.actionKey,
@@ -555,12 +627,13 @@ export class SchemaParameterResolver {
555
627
  inputs,
556
628
  ...(cursor && { cursor }),
557
629
  });
630
+ this.stopSpinner();
558
631
  const choices = page.data.map((choice) => ({
559
632
  label: choice.label || choice.key || String(choice.value),
560
633
  value: choice.value ?? choice.key,
561
634
  }));
562
635
  if (choices.length === 0 && !cursor) {
563
- console.log(chalk.yellow(` No choices available for ${fieldMeta.title}`));
636
+ console.log(chalk.yellow(`No choices available for ${fieldMeta.title}`));
564
637
  }
565
638
  return {
566
639
  choices,
@@ -568,7 +641,8 @@ export class SchemaParameterResolver {
568
641
  };
569
642
  }
570
643
  catch (error) {
571
- console.warn(chalk.yellow(` ⚠️ Failed to fetch choices for ${fieldMeta.title}:`), error);
644
+ this.stopSpinner();
645
+ console.warn(chalk.yellow(`Failed to fetch choices for ${fieldMeta.title}:`), error);
572
646
  return { choices: [] };
573
647
  }
574
648
  }
@@ -576,6 +650,7 @@ export class SchemaParameterResolver {
576
650
  * Prompt user with choices (handles both single and multi-select with pagination)
577
651
  */
578
652
  async promptWithChoices({ fieldMeta, choices: initialChoices, nextCursor: initialCursor, inputs, context, }) {
653
+ this.stopSpinner();
579
654
  const choices = [...initialChoices];
580
655
  let nextCursor = initialCursor;
581
656
  const LOAD_MORE_SENTINEL = Symbol("LOAD_MORE");
@@ -647,6 +722,31 @@ export class SchemaParameterResolver {
647
722
  ? Boolean(fieldMeta.defaultValue)
648
723
  : undefined;
649
724
  }
725
+ else if (fieldMeta.valueType === "array") {
726
+ promptConfig.type = "input";
727
+ promptConfig.default = fieldMeta.defaultValue;
728
+ promptConfig.message = `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"} (JSON array or comma-separated):`;
729
+ promptConfig.validate = (input) => {
730
+ if (fieldMeta.isRequired && !input) {
731
+ return "This field is required";
732
+ }
733
+ return true;
734
+ };
735
+ promptConfig.filter = (input) => {
736
+ if (!input)
737
+ return input;
738
+ const trimmed = input.trim();
739
+ if (trimmed.startsWith("[")) {
740
+ try {
741
+ return JSON.parse(trimmed);
742
+ }
743
+ catch {
744
+ // Fall through to comma-separated
745
+ }
746
+ }
747
+ return trimmed.split(",").map((s) => s.trim());
748
+ };
749
+ }
650
750
  else {
651
751
  promptConfig.type = "input";
652
752
  promptConfig.default = fieldMeta.defaultValue;
@@ -695,10 +795,15 @@ export class SchemaParameterResolver {
695
795
  }
696
796
  async promptForField(field, inputs, context) {
697
797
  const fieldMeta = this.extractFieldMetadata(field);
698
- // Fetch choices if field has dropdown
798
+ // Get choices - either inline or fetched from API
699
799
  let choices = [];
700
800
  let nextCursor;
701
- if (fieldMeta.hasDropdown && context) {
801
+ if (fieldMeta.inlineChoices) {
802
+ // Use inline choices directly
803
+ choices = fieldMeta.inlineChoices;
804
+ }
805
+ else if (fieldMeta.hasDropdown && context) {
806
+ // Fetch choices from API
702
807
  const result = await this.fetchChoices(fieldMeta, inputs, context);
703
808
  choices = result.choices;
704
809
  nextCursor = result.nextCursor;
@@ -1,6 +1,11 @@
1
1
  import type { z } from "zod";
2
+ import type { OutputFormatter, ZapierSdk } from "@zapier/zapier-sdk";
2
3
  export declare function formatJsonOutput(data: unknown): void;
3
4
  export declare function formatItemsFromSchema(functionInfo: {
4
5
  inputSchema: z.ZodType;
5
6
  outputSchema?: z.ZodType;
6
- }, items: unknown[], startingNumber?: number): void;
7
+ }, items: unknown[], startingNumber?: number, options?: {
8
+ formatter?: OutputFormatter;
9
+ sdk?: ZapierSdk;
10
+ params?: Record<string, unknown>;
11
+ }): Promise<void>;