@zapier/zapier-sdk-cli 0.16.1 → 0.16.3

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 (60) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli.cjs +7 -7
  3. package/dist/cli.mjs +7 -7
  4. package/dist/index.cjs +1 -1
  5. package/dist/index.mjs +1 -1
  6. package/dist/package.json +8 -2
  7. package/dist/src/cli.js +2 -1
  8. package/dist/src/utils/cli-generator.js +8 -7
  9. package/dist/tsconfig.tsbuildinfo +1 -1
  10. package/package.json +11 -5
  11. package/src/cli.test.ts +0 -28
  12. package/src/cli.ts +0 -96
  13. package/src/generators/ast-generator.test.ts +0 -908
  14. package/src/generators/ast-generator.ts +0 -774
  15. package/src/index.ts +0 -12
  16. package/src/plugins/add/index.test.ts +0 -58
  17. package/src/plugins/add/index.ts +0 -177
  18. package/src/plugins/add/schemas.ts +0 -35
  19. package/src/plugins/buildManifest/index.test.ts +0 -679
  20. package/src/plugins/buildManifest/index.ts +0 -131
  21. package/src/plugins/buildManifest/schemas.ts +0 -55
  22. package/src/plugins/bundleCode/index.ts +0 -128
  23. package/src/plugins/bundleCode/schemas.ts +0 -24
  24. package/src/plugins/generateAppTypes/index.test.ts +0 -679
  25. package/src/plugins/generateAppTypes/index.ts +0 -227
  26. package/src/plugins/generateAppTypes/schemas.ts +0 -61
  27. package/src/plugins/getLoginConfigPath/index.ts +0 -45
  28. package/src/plugins/getLoginConfigPath/schemas.ts +0 -10
  29. package/src/plugins/index.ts +0 -8
  30. package/src/plugins/login/index.ts +0 -135
  31. package/src/plugins/login/schemas.ts +0 -13
  32. package/src/plugins/logout/index.ts +0 -37
  33. package/src/plugins/logout/schemas.ts +0 -8
  34. package/src/plugins/mcp/index.ts +0 -43
  35. package/src/plugins/mcp/schemas.ts +0 -13
  36. package/src/sdk.ts +0 -45
  37. package/src/telemetry/builders.ts +0 -113
  38. package/src/telemetry/events.ts +0 -39
  39. package/src/types/sdk.ts +0 -8
  40. package/src/utils/api/client.ts +0 -44
  41. package/src/utils/auth/login.ts +0 -214
  42. package/src/utils/cli-generator-utils.ts +0 -169
  43. package/src/utils/cli-generator.test.ts +0 -347
  44. package/src/utils/cli-generator.ts +0 -807
  45. package/src/utils/constants.ts +0 -9
  46. package/src/utils/directory-detection.ts +0 -23
  47. package/src/utils/errors.ts +0 -26
  48. package/src/utils/getCallablePromise.ts +0 -21
  49. package/src/utils/log.ts +0 -23
  50. package/src/utils/manifest-helpers.ts +0 -25
  51. package/src/utils/package-manager-detector.ts +0 -83
  52. package/src/utils/parameter-resolver.ts +0 -1075
  53. package/src/utils/schema-formatter.ts +0 -153
  54. package/src/utils/serializeAsync.ts +0 -26
  55. package/src/utils/spinner.ts +0 -23
  56. package/src/utils/version-checker.test.ts +0 -239
  57. package/src/utils/version-checker.ts +0 -237
  58. package/tsconfig.build.json +0 -18
  59. package/tsconfig.json +0 -19
  60. package/tsup.config.ts +0 -23
@@ -1,1075 +0,0 @@
1
- import inquirer from "inquirer";
2
- import chalk from "chalk";
3
- import { z } from "zod";
4
- import type { ZapierSdk, ActionTypeProperty } from "@zapier/zapier-sdk";
5
- import { ZapierCliUserCancellationError } from "./errors";
6
-
7
- // ============================================================================
8
- // Types
9
- // ============================================================================
10
-
11
- interface ResolvableParameter {
12
- name: string;
13
- path: string[];
14
- schema: z.ZodType;
15
- description?: string;
16
- isRequired: boolean;
17
- }
18
-
19
- interface ResolverContext {
20
- sdk: ZapierSdk;
21
- currentParams: Record<string, unknown>;
22
- resolvedParams: Record<string, unknown>;
23
- functionName?: string;
24
- }
25
-
26
- interface FieldMetadata {
27
- key: string;
28
- title: string;
29
- description?: string;
30
- isRequired: boolean;
31
- defaultValue?: unknown;
32
- valueType: string;
33
- hasDropdown: boolean;
34
- isMultiSelect: boolean;
35
- }
36
-
37
- // ============================================================================
38
- // Local Resolution Helper Functions
39
- // ============================================================================
40
-
41
- /**
42
- * Resolve dependency chain for a parameter using local resolvers
43
- * Returns parameters in the order they need to be resolved
44
- */
45
- function getLocalResolutionOrder(
46
- paramName: string,
47
- resolvers: Record<string, any>,
48
- resolved: Set<string> = new Set(),
49
- ): string[] {
50
- const resolver = resolvers[paramName];
51
- if (!resolver || resolver.type === "static") {
52
- return [paramName];
53
- }
54
-
55
- const order: string[] = [];
56
-
57
- if ("depends" in resolver && resolver.depends) {
58
- for (const dependency of resolver.depends) {
59
- if (!resolved.has(dependency)) {
60
- order.push(...getLocalResolutionOrder(dependency, resolvers, resolved));
61
- resolved.add(dependency);
62
- }
63
- }
64
- }
65
-
66
- if (!resolved.has(paramName)) {
67
- order.push(paramName);
68
- resolved.add(paramName);
69
- }
70
-
71
- return order;
72
- }
73
-
74
- /**
75
- * Get resolution order for multiple parameters using local resolvers
76
- */
77
- function getLocalResolutionOrderForParams(
78
- paramNames: string[],
79
- resolvers: Record<string, any>,
80
- ): string[] {
81
- const resolved = new Set<string>();
82
- const order: string[] = [];
83
-
84
- for (const paramName of paramNames) {
85
- const paramOrder = getLocalResolutionOrder(paramName, resolvers, resolved);
86
- for (const param of paramOrder) {
87
- if (!order.includes(param)) {
88
- order.push(param);
89
- }
90
- }
91
- }
92
-
93
- return order;
94
- }
95
-
96
- // ============================================================================
97
- // Schema Parameter Resolver
98
- // ============================================================================
99
-
100
- export class SchemaParameterResolver {
101
- async resolveParameters(
102
- schema: z.ZodSchema,
103
- providedParams: unknown,
104
- sdk: ZapierSdk,
105
- functionName?: string,
106
- ): Promise<unknown> {
107
- // 1. Try to parse with current parameters
108
- const parseResult = schema.safeParse(providedParams);
109
-
110
- // Get all schema parameters to check which ones have resolvers
111
- const allParams = this.extractParametersFromSchema(schema);
112
- const resolvableParams = allParams.filter((param) =>
113
- this.hasResolver(param.name, sdk, functionName),
114
- );
115
-
116
- // Get all missing parameters that have resolvers
117
- const missingResolvable = resolvableParams.filter((param) => {
118
- const hasValue =
119
- this.getNestedValue(providedParams, param.path) !== undefined;
120
- return !hasValue;
121
- });
122
-
123
- // Determine parameter resolution categories:
124
- // - functionally required: must be provided (inputs)
125
- // - always prompt: should be prompted for but can be skipped (authenticationId)
126
- // - truly optional: only ask if user wants to be prompted
127
- const functionallyRequired = missingResolvable.filter((param) => {
128
- // Schema-required parameters are always functionally required
129
- if (param.isRequired) return true;
130
-
131
- // Only inputs is functionally required for run-action
132
- if (param.name === "inputs") {
133
- return true;
134
- }
135
-
136
- return false;
137
- });
138
-
139
- // Parameters that should always be prompted for directly, but can be skipped
140
- const alwaysPrompt = missingResolvable.filter((param) => {
141
- if (functionallyRequired.includes(param)) return false;
142
-
143
- // authenticationId should always be prompted for (since it's usually needed)
144
- // but can be skipped with "Continue without authentication"
145
- if (param.name === "authenticationId") {
146
- return true;
147
- }
148
-
149
- return false;
150
- });
151
-
152
- const trulyOptional = missingResolvable.filter(
153
- (param) =>
154
- !functionallyRequired.includes(param) && !alwaysPrompt.includes(param),
155
- );
156
-
157
- if (
158
- parseResult.success &&
159
- functionallyRequired.length === 0 &&
160
- alwaysPrompt.length === 0
161
- ) {
162
- return parseResult.data;
163
- }
164
-
165
- if (functionallyRequired.length === 0 && alwaysPrompt.length === 0) {
166
- // No functionally required parameters missing, but check if we can parse
167
- if (!parseResult.success) {
168
- throw parseResult.error;
169
- }
170
- return parseResult.data;
171
- }
172
-
173
- // 2. Resolve functionally required parameters first
174
- const resolvedParams = { ...(providedParams as Record<string, unknown>) };
175
- const context: ResolverContext = {
176
- sdk,
177
- currentParams: providedParams as Record<string, unknown>,
178
- resolvedParams,
179
- functionName,
180
- };
181
-
182
- // Get local resolvers for this function
183
- const localResolvers = this.getLocalResolvers(sdk, functionName);
184
-
185
- if (functionallyRequired.length > 0) {
186
- const requiredParamNames = functionallyRequired.map((p) => p.name);
187
- const requiredResolutionOrder = getLocalResolutionOrderForParams(
188
- requiredParamNames,
189
- localResolvers,
190
- );
191
-
192
- // Find all parameters that need to be resolved (including dependencies)
193
- // from the available resolvable parameters
194
- const orderedRequiredParams = requiredResolutionOrder
195
- .map((paramName) => {
196
- // First try to find in functionally required
197
- let param = functionallyRequired.find((p) => p.name === paramName);
198
- // If not found, try always prompt (for dependencies like authenticationId)
199
- if (!param) {
200
- param = alwaysPrompt.find((p) => p.name === paramName);
201
- }
202
- // If not found, try truly optional (for other dependencies)
203
- if (!param) {
204
- param = trulyOptional.find((p) => p.name === paramName);
205
- }
206
- return param;
207
- })
208
- .filter((param): param is ResolvableParameter => param !== undefined);
209
-
210
- for (const param of orderedRequiredParams) {
211
- try {
212
- const value = await this.resolveParameter(
213
- param,
214
- context,
215
- functionName,
216
- );
217
- this.setNestedValue(resolvedParams, param.path, value);
218
-
219
- // Update context with newly resolved value
220
- context.resolvedParams = resolvedParams;
221
- } catch (error) {
222
- if (this.isUserCancellation(error)) {
223
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
224
- throw new ZapierCliUserCancellationError();
225
- }
226
- throw error;
227
- }
228
- }
229
-
230
- // Remove resolved dependencies from other categories to avoid double-prompting
231
- const resolvedParamNames = new Set(
232
- orderedRequiredParams.map((p) => p.name),
233
- );
234
- alwaysPrompt.splice(
235
- 0,
236
- alwaysPrompt.length,
237
- ...alwaysPrompt.filter((p) => !resolvedParamNames.has(p.name)),
238
- );
239
- trulyOptional.splice(
240
- 0,
241
- trulyOptional.length,
242
- ...trulyOptional.filter((p) => !resolvedParamNames.has(p.name)),
243
- );
244
- }
245
-
246
- // 3. Resolve parameters that should always be prompted for (but can be skipped)
247
- if (alwaysPrompt.length > 0) {
248
- const alwaysPromptNames = alwaysPrompt.map((p) => p.name);
249
- const alwaysPromptResolutionOrder = getLocalResolutionOrderForParams(
250
- alwaysPromptNames,
251
- localResolvers,
252
- );
253
-
254
- const orderedAlwaysPromptParams = alwaysPromptResolutionOrder
255
- .map((paramName) => alwaysPrompt.find((p) => p.name === paramName))
256
- .filter((param): param is ResolvableParameter => param !== undefined);
257
-
258
- for (const param of orderedAlwaysPromptParams) {
259
- try {
260
- const value = await this.resolveParameter(
261
- param,
262
- context,
263
- functionName,
264
- );
265
- this.setNestedValue(resolvedParams, param.path, value);
266
-
267
- // Update context with newly resolved value
268
- context.resolvedParams = resolvedParams;
269
- } catch (error) {
270
- if (this.isUserCancellation(error)) {
271
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
272
- throw new ZapierCliUserCancellationError();
273
- }
274
- throw error;
275
- }
276
- }
277
- }
278
-
279
- // 4. Ask user if they want to resolve truly optional parameters
280
- if (trulyOptional.length > 0) {
281
- const optionalNames = trulyOptional.map((p) => p.name).join(", ");
282
- const shouldResolveOptional = await inquirer.prompt([
283
- {
284
- type: "confirm",
285
- name: "resolveOptional",
286
- message: `Would you like to be prompted for optional parameters (${optionalNames})?`,
287
- default: false,
288
- },
289
- ]);
290
-
291
- if (shouldResolveOptional.resolveOptional) {
292
- // Resolve optional parameters using their resolvers
293
- const optionalParamNames = trulyOptional.map((p) => p.name);
294
- const optionalResolutionOrder = getLocalResolutionOrderForParams(
295
- optionalParamNames,
296
- localResolvers,
297
- );
298
-
299
- const orderedOptionalParams = optionalResolutionOrder
300
- .map((paramName) => trulyOptional.find((p) => p.name === paramName))
301
- .filter((param): param is ResolvableParameter => param !== undefined);
302
-
303
- for (const param of orderedOptionalParams) {
304
- try {
305
- const value = await this.resolveParameter(
306
- param,
307
- context,
308
- functionName,
309
- );
310
- this.setNestedValue(resolvedParams, param.path, value);
311
-
312
- // Update context with newly resolved value
313
- context.resolvedParams = resolvedParams;
314
- } catch (error) {
315
- if (this.isUserCancellation(error)) {
316
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
317
- throw new ZapierCliUserCancellationError();
318
- }
319
- throw error;
320
- }
321
- }
322
- }
323
- }
324
-
325
- // 3. Validate final parameters
326
- const finalResult = schema.safeParse(resolvedParams);
327
-
328
- if (!finalResult.success) {
329
- console.error(
330
- chalk.red("❌ Parameter validation failed after resolution:"),
331
- );
332
- throw finalResult.error;
333
- }
334
-
335
- return finalResult.data;
336
- }
337
-
338
- private extractParametersFromSchema(
339
- schema: z.ZodSchema,
340
- ): ResolvableParameter[] {
341
- const parameters: ResolvableParameter[] = [];
342
-
343
- // Only handle ZodObject at the top level
344
- if (schema instanceof z.ZodObject) {
345
- const shape = schema.shape;
346
- for (const [key, fieldSchema] of Object.entries<z.ZodSchema>(shape)) {
347
- const param = this.analyzeFieldSchema(key, fieldSchema);
348
- if (param) {
349
- parameters.push(param);
350
- }
351
- }
352
- }
353
-
354
- return parameters;
355
- }
356
-
357
- private analyzeFieldSchema(
358
- fieldName: string,
359
- fieldSchema: z.ZodSchema,
360
- ): ResolvableParameter | null {
361
- let baseSchema = fieldSchema;
362
- let isRequired = true;
363
-
364
- // Check if field is optional or has default
365
- if (baseSchema instanceof z.ZodOptional) {
366
- isRequired = false;
367
- baseSchema = baseSchema._zod.def.innerType as z.ZodSchema;
368
- }
369
-
370
- if (baseSchema instanceof z.ZodDefault) {
371
- isRequired = false;
372
- baseSchema = baseSchema._zod.def.innerType as z.ZodSchema;
373
- }
374
-
375
- return this.createResolvableParameter([fieldName], baseSchema, isRequired);
376
- }
377
-
378
- private createResolvableParameter(
379
- path: string[],
380
- schema: z.ZodSchema,
381
- isRequired: boolean,
382
- ): ResolvableParameter | null {
383
- if (path.length === 0) return null;
384
-
385
- const name = path[path.length - 1];
386
-
387
- return {
388
- name,
389
- path,
390
- schema,
391
- description: schema.description,
392
- isRequired,
393
- };
394
- }
395
-
396
- private async resolveParameter(
397
- param: ResolvableParameter,
398
- context: ResolverContext,
399
- functionName?: string,
400
- ): Promise<unknown> {
401
- const resolver = this.getResolver(param.name, context.sdk, functionName);
402
- if (!resolver) {
403
- throw new Error(`No resolver found for parameter: ${param.name}`);
404
- }
405
-
406
- console.log(chalk.blue(`\n🔍 Resolving ${param.name}...`));
407
-
408
- const typedResolver = resolver as {
409
- type: string;
410
- inputType?: string;
411
- placeholder?: string;
412
- fetch?: Function;
413
- prompt?: Function;
414
- };
415
-
416
- if (typedResolver.type === "static") {
417
- // Static resolver - just prompt for input
418
- const promptConfig = {
419
- type: typedResolver.inputType === "password" ? "password" : "input",
420
- name: param.name,
421
- message: `Enter ${param.name}:`,
422
- ...(typedResolver.placeholder && {
423
- default: typedResolver.placeholder,
424
- }),
425
- };
426
-
427
- const answers = await inquirer.prompt([promptConfig as any]);
428
- return answers[param.name];
429
- } else if (typedResolver.type === "dynamic") {
430
- // Dynamic resolver - fetch options and prompt for selection
431
- try {
432
- // Only show "Fetching..." for required parameters that typically have many options
433
- if (param.isRequired && param.name !== "authenticationId") {
434
- console.log(chalk.gray(`Fetching options for ${param.name}...`));
435
- }
436
- const items = await typedResolver.fetch!(
437
- context.sdk,
438
- context.resolvedParams,
439
- );
440
-
441
- // Let the resolver's prompt handle empty lists (e.g., authenticationId can show "skip authentication")
442
- const safeItems = items || [];
443
- const promptConfig = typedResolver.prompt!(
444
- safeItems,
445
- context.resolvedParams,
446
- );
447
- const answers = await inquirer.prompt([promptConfig as any]);
448
- return answers[param.name];
449
- } catch (error) {
450
- // Let the main CLI error handler display user-friendly errors
451
- throw error;
452
- }
453
- } else if (typedResolver.type === "fields") {
454
- // Fields resolver - fetch field definitions and prompt for each input with recursive field resolution
455
- return await this.resolveFieldsRecursively(
456
- resolver as unknown,
457
- context,
458
- param,
459
- );
460
- }
461
-
462
- throw new Error(`Unknown resolver type for ${param.name}`);
463
- }
464
-
465
- private async resolveFieldsRecursively(
466
- resolver: unknown,
467
- context: ResolverContext,
468
- param: ResolvableParameter,
469
- ): Promise<Record<string, unknown>> {
470
- const typedResolver = resolver as { fetch: Function };
471
- const inputs: Record<string, unknown> = {};
472
- let processedFieldKeys = new Set<string>();
473
- let iteration = 0;
474
- const maxIterations = 10; // Prevent infinite loops
475
-
476
- while (iteration < maxIterations) {
477
- iteration++;
478
-
479
- // Update context with current inputs so they're passed to listInputFields
480
- const updatedContext = {
481
- ...context,
482
- resolvedParams: {
483
- ...context.resolvedParams,
484
- inputs,
485
- },
486
- };
487
-
488
- console.log(
489
- chalk.gray(
490
- `Fetching input fields for ${param.name}${iteration > 1 ? ` (iteration ${iteration})` : ""}...`,
491
- ),
492
- );
493
-
494
- const rootFieldItems = await typedResolver.fetch(
495
- updatedContext.sdk,
496
- updatedContext.resolvedParams,
497
- );
498
-
499
- if (!rootFieldItems || rootFieldItems.length === 0) {
500
- if (iteration === 1) {
501
- console.log(
502
- chalk.yellow(`No input fields required for this action.`),
503
- );
504
- }
505
- break;
506
- }
507
-
508
- // Process fields recursively, maintaining fieldset structure
509
- const fieldStats = await this.processFieldItems(
510
- rootFieldItems,
511
- inputs,
512
- processedFieldKeys,
513
- [],
514
- iteration,
515
- updatedContext,
516
- );
517
-
518
- // If no new fields were processed, we're done
519
- if (fieldStats.newRequired === 0 && fieldStats.newOptional === 0) {
520
- break;
521
- }
522
-
523
- // If we only processed optional fields and skipped them, no need to re-fetch
524
- if (fieldStats.newRequired === 0 && fieldStats.optionalSkipped) {
525
- break;
526
- }
527
- }
528
-
529
- if (iteration >= maxIterations) {
530
- console.log(
531
- chalk.yellow(
532
- `\n⚠️ Maximum field resolution iterations reached. Some dynamic fields may not have been discovered.`,
533
- ),
534
- );
535
- }
536
-
537
- return inputs;
538
- }
539
-
540
- /**
541
- * Recursively processes fieldsets and their fields, maintaining natural structure
542
- * and creating nested inputs as needed (e.g., fieldset "foo" becomes inputs.foo = [{}])
543
- */
544
- private async processFieldItems(
545
- items: unknown[],
546
- targetInputs: Record<string, unknown>,
547
- processedFieldKeys: Set<string>,
548
- fieldsetPath: string[] = [],
549
- iteration: number = 1,
550
- context?: ResolverContext,
551
- ): Promise<{
552
- newRequired: number;
553
- newOptional: number;
554
- optionalSkipped: boolean;
555
- }> {
556
- let newRequiredCount = 0;
557
- let newOptionalCount = 0;
558
- let optionalSkipped = false;
559
-
560
- for (const item of items) {
561
- const typedItem = item as {
562
- type?: string;
563
- key?: string;
564
- title?: string;
565
- fields?: unknown[];
566
- is_required?: boolean;
567
- };
568
-
569
- if (typedItem.type === "fieldset" && typedItem.fields && typedItem.key) {
570
- // Show fieldset context to user
571
- const fieldsetTitle = typedItem.title || typedItem.key;
572
- const pathDisplay =
573
- fieldsetPath.length > 0 ? ` (in ${fieldsetPath.join(" > ")})` : "";
574
- console.log(
575
- chalk.cyan(
576
- `\n📁 Processing fieldset: ${fieldsetTitle}${pathDisplay}`,
577
- ),
578
- );
579
-
580
- // Create fieldset array in target inputs if it doesn't exist
581
- if (!targetInputs[typedItem.key]) {
582
- targetInputs[typedItem.key] = [{}];
583
- }
584
-
585
- // Process fields within this fieldset recursively
586
- const fieldsetTarget = (
587
- targetInputs[typedItem.key] as Record<string, unknown>[]
588
- )[0];
589
- const nestedPath = [...fieldsetPath, fieldsetTitle];
590
-
591
- const nestedStats = await this.processFieldItems(
592
- typedItem.fields,
593
- fieldsetTarget,
594
- processedFieldKeys,
595
- nestedPath,
596
- iteration,
597
- context,
598
- );
599
-
600
- newRequiredCount += nestedStats.newRequired;
601
- newOptionalCount += nestedStats.newOptional;
602
- if (nestedStats.optionalSkipped) {
603
- optionalSkipped = true;
604
- }
605
- } else if (typedItem.type === "input_field" && typedItem.key) {
606
- // Skip if already processed
607
- if (processedFieldKeys.has(typedItem.key)) {
608
- continue;
609
- }
610
-
611
- const isRequired = typedItem.is_required || false;
612
-
613
- if (isRequired) {
614
- // Process required field immediately
615
- newRequiredCount++;
616
- if (newRequiredCount === 1 && fieldsetPath.length === 0) {
617
- // Only show this message once at root level
618
- console.log(
619
- chalk.blue(
620
- `\n📝 Please provide values for the following ${iteration === 1 ? "" : "additional "}input fields:`,
621
- ),
622
- );
623
- }
624
-
625
- await this.promptForField(typedItem, targetInputs, context);
626
- processedFieldKeys.add(typedItem.key);
627
- } else {
628
- // Collect optional fields for batch processing
629
- newOptionalCount++;
630
- }
631
- }
632
- // Skip info fields - they're for display only
633
- }
634
-
635
- // Handle optional fields after processing all required fields
636
- if (newOptionalCount > 0) {
637
- const optionalFields = items.filter((item: unknown) => {
638
- const typedItem = item as {
639
- type?: string;
640
- key?: string;
641
- is_required?: boolean;
642
- };
643
- return (
644
- typedItem.type === "input_field" &&
645
- typedItem.key &&
646
- !typedItem.is_required &&
647
- !processedFieldKeys.has(typedItem.key)
648
- );
649
- });
650
-
651
- if (optionalFields.length > 0) {
652
- const pathContext =
653
- fieldsetPath.length > 0 ? ` in ${fieldsetPath.join(" > ")}` : "";
654
- console.log(
655
- chalk.gray(
656
- `\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`,
657
- ),
658
- );
659
-
660
- try {
661
- const shouldConfigureOptional = await inquirer.prompt([
662
- {
663
- type: "confirm",
664
- name: "configure",
665
- message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`,
666
- default: false,
667
- },
668
- ]);
669
-
670
- if (shouldConfigureOptional.configure) {
671
- console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
672
- for (const field of optionalFields) {
673
- await this.promptForField(field, targetInputs, context);
674
- const typedField = field as { key: string };
675
- processedFieldKeys.add(typedField.key);
676
- }
677
- } else {
678
- optionalSkipped = true;
679
- // Mark these fields as processed even if skipped to avoid re-asking
680
- optionalFields.forEach((field: unknown) => {
681
- const typedField = field as { key: string };
682
- processedFieldKeys.add(typedField.key);
683
- });
684
- }
685
- } catch (error) {
686
- if (this.isUserCancellation(error)) {
687
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
688
- throw new ZapierCliUserCancellationError();
689
- }
690
- throw error;
691
- }
692
- }
693
- }
694
-
695
- return {
696
- newRequired: newRequiredCount,
697
- newOptional: newOptionalCount,
698
- optionalSkipped,
699
- };
700
- }
701
-
702
- private getNestedValue(obj: unknown, path: string[]): unknown {
703
- return path.reduce(
704
- (current, key) => (current as Record<string, unknown>)?.[key],
705
- obj,
706
- );
707
- }
708
-
709
- private setNestedValue(obj: unknown, path: string[], value: unknown): void {
710
- const lastKey = path[path.length - 1];
711
- const parent = path.slice(0, -1).reduce((current, key) => {
712
- const currentObj = current as Record<string, unknown>;
713
- if (!(key in currentObj)) {
714
- currentObj[key] = {};
715
- }
716
- return currentObj[key];
717
- }, obj) as Record<string, unknown>;
718
- parent[lastKey] = value;
719
- }
720
-
721
- /**
722
- * Extract and normalize field metadata from raw field object
723
- */
724
- private extractFieldMetadata(field: unknown): FieldMetadata {
725
- const fieldObj = field as {
726
- type?: string;
727
- key: string;
728
- title?: string;
729
- label?: string;
730
- is_required?: boolean;
731
- description?: string;
732
- helpText?: string;
733
- default_value?: unknown;
734
- default?: unknown;
735
- value_type?: string;
736
- format?: string;
737
- items?: { type: string };
738
- };
739
-
740
- const valueType = fieldObj.value_type || "string";
741
-
742
- return {
743
- key: fieldObj.key,
744
- title: fieldObj.title || fieldObj.label || fieldObj.key,
745
- description: fieldObj.description || fieldObj.helpText,
746
- isRequired: fieldObj.is_required || false,
747
- defaultValue: fieldObj.default_value ?? fieldObj.default,
748
- valueType,
749
- hasDropdown: fieldObj.format === "SELECT",
750
- isMultiSelect: Boolean(
751
- valueType === "array" ||
752
- (fieldObj.items && fieldObj.items.type !== undefined),
753
- ),
754
- };
755
- }
756
-
757
- /**
758
- * Fetch a page of choices for a dropdown field
759
- */
760
- private async fetchChoices(
761
- fieldMeta: FieldMetadata,
762
- inputs: Record<string, unknown>,
763
- context: ResolverContext,
764
- cursor?: string,
765
- ): Promise<{
766
- choices: Array<{ label: string; value: unknown }>;
767
- nextCursor?: string;
768
- }> {
769
- try {
770
- console.log(
771
- chalk.gray(
772
- cursor
773
- ? ` Fetching more choices...`
774
- : ` Fetching choices for ${fieldMeta.title}...`,
775
- ),
776
- );
777
-
778
- const page = await context.sdk.listInputFieldChoices({
779
- appKey: context.resolvedParams.appKey as string,
780
- actionKey: context.resolvedParams.actionKey as string,
781
- actionType: context.resolvedParams.actionType as ActionTypeProperty,
782
- authenticationId: context.resolvedParams.authenticationId as
783
- | number
784
- | null,
785
- inputFieldKey: fieldMeta.key,
786
- inputs,
787
- ...(cursor && { cursor }),
788
- });
789
-
790
- const choices = page.data.map((choice) => ({
791
- label: choice.label || choice.key || String(choice.value),
792
- value: choice.value ?? choice.key,
793
- }));
794
-
795
- if (choices.length === 0 && !cursor) {
796
- console.log(
797
- chalk.yellow(` No choices available for ${fieldMeta.title}`),
798
- );
799
- }
800
-
801
- return {
802
- choices,
803
- nextCursor: page.nextCursor,
804
- };
805
- } catch (error) {
806
- console.warn(
807
- chalk.yellow(` ⚠️ Failed to fetch choices for ${fieldMeta.title}:`),
808
- error,
809
- );
810
- return { choices: [] };
811
- }
812
- }
813
-
814
- /**
815
- * Prompt user with choices (handles both single and multi-select with pagination)
816
- */
817
- private async promptWithChoices({
818
- fieldMeta,
819
- choices: initialChoices,
820
- nextCursor: initialCursor,
821
- inputs,
822
- context,
823
- }: {
824
- fieldMeta: FieldMetadata;
825
- choices: Array<{ label: string; value: unknown }>;
826
- nextCursor?: string;
827
- inputs: Record<string, unknown>;
828
- context?: ResolverContext;
829
- }): Promise<unknown> {
830
- const choices = [...initialChoices];
831
- let nextCursor = initialCursor;
832
- const LOAD_MORE_SENTINEL = Symbol("LOAD_MORE");
833
-
834
- // Progressive loading loop
835
- while (true) {
836
- const promptChoices = choices.map((choice) => ({
837
- name: choice.label,
838
- value: choice.value,
839
- }));
840
-
841
- // Add "(Load more...)" option if there are more pages
842
- if (nextCursor) {
843
- promptChoices.push({
844
- name: chalk.dim("(Load more...)"),
845
- value: LOAD_MORE_SENTINEL,
846
- });
847
- }
848
-
849
- // Add skip option for optional fields (single-select only)
850
- if (!fieldMeta.isRequired && !fieldMeta.isMultiSelect) {
851
- promptChoices.push({ name: "(Skip)", value: undefined });
852
- }
853
-
854
- const promptConfig = {
855
- type: fieldMeta.isMultiSelect ? "checkbox" : "list",
856
- name: fieldMeta.key,
857
- message: `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"}:`,
858
- choices: promptChoices,
859
- ...(fieldMeta.isMultiSelect && {
860
- validate: (input: unknown[]) => {
861
- if (fieldMeta.isRequired && (!input || input.length === 0)) {
862
- return "At least one selection is required";
863
- }
864
- return true;
865
- },
866
- }),
867
- };
868
-
869
- const answer = await inquirer.prompt([promptConfig as any]);
870
- let selectedValue = answer[fieldMeta.key];
871
-
872
- // Check if user selected "Load more..."
873
- const wantsMore = fieldMeta.isMultiSelect
874
- ? Array.isArray(selectedValue) &&
875
- selectedValue.includes(LOAD_MORE_SENTINEL)
876
- : selectedValue === LOAD_MORE_SENTINEL;
877
-
878
- if (wantsMore && nextCursor && context) {
879
- // Remove sentinel from multi-select
880
- if (fieldMeta.isMultiSelect && Array.isArray(selectedValue)) {
881
- selectedValue = selectedValue.filter((v) => v !== LOAD_MORE_SENTINEL);
882
- }
883
-
884
- // Fetch next page
885
- const result = await this.fetchChoices(
886
- fieldMeta,
887
- inputs,
888
- context,
889
- nextCursor,
890
- );
891
- choices.push(...result.choices);
892
- nextCursor = result.nextCursor;
893
-
894
- // Re-prompt with updated choices
895
- continue;
896
- }
897
-
898
- return selectedValue;
899
- }
900
- }
901
-
902
- /**
903
- * Prompt user for free-form input (text or boolean)
904
- */
905
- private async promptFreeForm(fieldMeta: FieldMetadata): Promise<unknown> {
906
- const promptConfig: Record<string, unknown> = {
907
- name: fieldMeta.key,
908
- message: `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"}:`,
909
- };
910
-
911
- if (fieldMeta.valueType === "boolean") {
912
- promptConfig.type = "confirm";
913
- promptConfig.default =
914
- fieldMeta.defaultValue !== undefined
915
- ? Boolean(fieldMeta.defaultValue)
916
- : undefined;
917
- } else {
918
- promptConfig.type = "input";
919
- promptConfig.default = fieldMeta.defaultValue;
920
- promptConfig.validate = (input: string) => {
921
- if (fieldMeta.isRequired && !input) {
922
- return "This field is required";
923
- }
924
- return true;
925
- };
926
- }
927
-
928
- // Add help text if available
929
- if (fieldMeta.description) {
930
- promptConfig.prefix = chalk.gray(`ℹ ${fieldMeta.description}\n`);
931
- }
932
-
933
- try {
934
- const answer = await inquirer.prompt([promptConfig as any]);
935
- return answer[fieldMeta.key];
936
- } catch (error) {
937
- if (this.isUserCancellation(error)) {
938
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
939
- throw new ZapierCliUserCancellationError();
940
- }
941
- throw error;
942
- }
943
- }
944
-
945
- /**
946
- * Store field value in inputs object with validation
947
- */
948
- private storeFieldValue(
949
- inputs: Record<string, unknown>,
950
- key: string,
951
- value: unknown,
952
- isRequired: boolean,
953
- ): void {
954
- try {
955
- if (value !== undefined && value !== "") {
956
- inputs[key] = value;
957
- } else if (isRequired) {
958
- throw new Error(`Required field ${key} cannot be empty`);
959
- }
960
- } catch (error) {
961
- if (this.isUserCancellation(error)) {
962
- console.log(chalk.yellow("\n\nOperation cancelled by user"));
963
- throw new ZapierCliUserCancellationError();
964
- }
965
- throw error;
966
- }
967
- }
968
-
969
- private async promptForField(
970
- field: unknown,
971
- inputs: Record<string, unknown>,
972
- context?: ResolverContext,
973
- ): Promise<void> {
974
- const fieldMeta = this.extractFieldMetadata(field);
975
-
976
- // Fetch choices if field has dropdown
977
- let choices: Array<{ label: string; value: unknown }> = [];
978
- let nextCursor: string | undefined;
979
- if (fieldMeta.hasDropdown && context) {
980
- const result = await this.fetchChoices(fieldMeta, inputs, context);
981
- choices = result.choices;
982
- nextCursor = result.nextCursor;
983
- }
984
-
985
- // Prompt user based on field type
986
- let selectedValue: unknown;
987
- if (choices.length > 0) {
988
- selectedValue = await this.promptWithChoices({
989
- fieldMeta,
990
- choices,
991
- nextCursor,
992
- inputs,
993
- context,
994
- });
995
- } else {
996
- selectedValue = await this.promptFreeForm(fieldMeta);
997
- }
998
-
999
- // Store result
1000
- this.storeFieldValue(
1001
- inputs,
1002
- fieldMeta.key,
1003
- selectedValue,
1004
- fieldMeta.isRequired,
1005
- );
1006
- }
1007
-
1008
- private isUserCancellation(error: unknown): boolean {
1009
- const errorObj = error as {
1010
- name?: string;
1011
- message?: string;
1012
- isTTYError?: boolean;
1013
- };
1014
- return (
1015
- errorObj?.name === "ExitPromptError" ||
1016
- errorObj?.message?.includes("User force closed") ||
1017
- errorObj?.isTTYError === true
1018
- );
1019
- }
1020
-
1021
- private hasResolver(
1022
- paramName: string,
1023
- sdk: ZapierSdk,
1024
- functionName?: string,
1025
- ): boolean {
1026
- // Check plugin-specific resolvers first
1027
- if (functionName && typeof sdk.getRegistry === "function") {
1028
- const registry = sdk.getRegistry({ package: "cli" });
1029
- const functionInfo = registry.functions.find(
1030
- (f) => f.name === functionName,
1031
- );
1032
- if (functionInfo && functionInfo.resolvers?.[paramName]) {
1033
- return true;
1034
- }
1035
- }
1036
-
1037
- // No global registry fallback
1038
- return false;
1039
- }
1040
-
1041
- private getResolver(
1042
- paramName: string,
1043
- sdk: ZapierSdk,
1044
- functionName?: string,
1045
- ): any {
1046
- // Check plugin-specific resolvers first
1047
- if (functionName && typeof sdk.getRegistry === "function") {
1048
- const registry = sdk.getRegistry({ package: "cli" });
1049
- const functionInfo = registry.functions.find(
1050
- (f) => f.name === functionName,
1051
- );
1052
- if (functionInfo && functionInfo.resolvers?.[paramName]) {
1053
- return functionInfo.resolvers[paramName];
1054
- }
1055
- }
1056
-
1057
- // No global registry fallback
1058
- return null;
1059
- }
1060
-
1061
- private getLocalResolvers(
1062
- sdk: ZapierSdk,
1063
- functionName?: string,
1064
- ): Record<string, any> {
1065
- if (!functionName || typeof sdk.getRegistry !== "function") {
1066
- return {};
1067
- }
1068
-
1069
- const registry = sdk.getRegistry();
1070
- const functionInfo = registry.functions.find(
1071
- (f) => f.name === functionName,
1072
- );
1073
- return functionInfo?.resolvers || {};
1074
- }
1075
- }