@zapier/zapier-sdk-cli 0.9.0 → 0.11.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,17 +1,59 @@
1
1
  import inquirer from "inquirer";
2
2
  import chalk from "chalk";
3
3
  import { z } from "zod";
4
- import { getResolver, hasResolver, getResolutionOrderForParams, } from "@zapier/zapier-sdk";
4
+ // ============================================================================
5
+ // Local Resolution Helper Functions
6
+ // ============================================================================
7
+ /**
8
+ * Resolve dependency chain for a parameter using local resolvers
9
+ * Returns parameters in the order they need to be resolved
10
+ */
11
+ function getLocalResolutionOrder(paramName, resolvers, resolved = new Set()) {
12
+ const resolver = resolvers[paramName];
13
+ if (!resolver || resolver.type === "static") {
14
+ return [paramName];
15
+ }
16
+ const order = [];
17
+ if ("depends" in resolver && resolver.depends) {
18
+ for (const dependency of resolver.depends) {
19
+ if (!resolved.has(dependency)) {
20
+ order.push(...getLocalResolutionOrder(dependency, resolvers, resolved));
21
+ resolved.add(dependency);
22
+ }
23
+ }
24
+ }
25
+ if (!resolved.has(paramName)) {
26
+ order.push(paramName);
27
+ resolved.add(paramName);
28
+ }
29
+ return order;
30
+ }
31
+ /**
32
+ * Get resolution order for multiple parameters using local resolvers
33
+ */
34
+ function getLocalResolutionOrderForParams(paramNames, resolvers) {
35
+ const resolved = new Set();
36
+ const order = [];
37
+ for (const paramName of paramNames) {
38
+ const paramOrder = getLocalResolutionOrder(paramName, resolvers, resolved);
39
+ for (const param of paramOrder) {
40
+ if (!order.includes(param)) {
41
+ order.push(param);
42
+ }
43
+ }
44
+ }
45
+ return order;
46
+ }
5
47
  // ============================================================================
6
48
  // Schema Parameter Resolver
7
49
  // ============================================================================
8
50
  export class SchemaParameterResolver {
9
- async resolveParameters(schema, providedParams, sdk) {
51
+ async resolveParameters(schema, providedParams, sdk, functionName) {
10
52
  // 1. Try to parse with current parameters
11
53
  const parseResult = schema.safeParse(providedParams);
12
54
  // Get all schema parameters to check which ones have resolvers
13
55
  const allParams = this.extractParametersFromSchema(schema);
14
- const resolvableParams = allParams.filter((param) => hasResolver(param.name));
56
+ const resolvableParams = allParams.filter((param) => this.hasResolver(param.name, sdk, functionName));
15
57
  // Get all missing parameters that have resolvers
16
58
  const missingResolvable = resolvableParams.filter((param) => {
17
59
  const hasValue = this.getNestedValue(providedParams, param.path) !== undefined;
@@ -61,10 +103,13 @@ export class SchemaParameterResolver {
61
103
  sdk,
62
104
  currentParams: providedParams,
63
105
  resolvedParams,
106
+ functionName,
64
107
  };
108
+ // Get local resolvers for this function
109
+ const localResolvers = this.getLocalResolvers(sdk, functionName);
65
110
  if (functionallyRequired.length > 0) {
66
111
  const requiredParamNames = functionallyRequired.map((p) => p.name);
67
- const requiredResolutionOrder = getResolutionOrderForParams(requiredParamNames);
112
+ const requiredResolutionOrder = getLocalResolutionOrderForParams(requiredParamNames, localResolvers);
68
113
  // Find all parameters that need to be resolved (including dependencies)
69
114
  // from the available resolvable parameters
70
115
  const orderedRequiredParams = requiredResolutionOrder
@@ -84,7 +129,7 @@ export class SchemaParameterResolver {
84
129
  .filter((param) => param !== undefined);
85
130
  for (const param of orderedRequiredParams) {
86
131
  try {
87
- const value = await this.resolveParameter(param, context);
132
+ const value = await this.resolveParameter(param, context, functionName);
88
133
  this.setNestedValue(resolvedParams, param.path, value);
89
134
  // Update context with newly resolved value
90
135
  context.resolvedParams = resolvedParams;
@@ -105,13 +150,13 @@ export class SchemaParameterResolver {
105
150
  // 3. Resolve parameters that should always be prompted for (but can be skipped)
106
151
  if (alwaysPrompt.length > 0) {
107
152
  const alwaysPromptNames = alwaysPrompt.map((p) => p.name);
108
- const alwaysPromptResolutionOrder = getResolutionOrderForParams(alwaysPromptNames);
153
+ const alwaysPromptResolutionOrder = getLocalResolutionOrderForParams(alwaysPromptNames, localResolvers);
109
154
  const orderedAlwaysPromptParams = alwaysPromptResolutionOrder
110
155
  .map((paramName) => alwaysPrompt.find((p) => p.name === paramName))
111
156
  .filter((param) => param !== undefined);
112
157
  for (const param of orderedAlwaysPromptParams) {
113
158
  try {
114
- const value = await this.resolveParameter(param, context);
159
+ const value = await this.resolveParameter(param, context, functionName);
115
160
  this.setNestedValue(resolvedParams, param.path, value);
116
161
  // Update context with newly resolved value
117
162
  context.resolvedParams = resolvedParams;
@@ -139,13 +184,13 @@ export class SchemaParameterResolver {
139
184
  if (shouldResolveOptional.resolveOptional) {
140
185
  // Resolve optional parameters using their resolvers
141
186
  const optionalParamNames = trulyOptional.map((p) => p.name);
142
- const optionalResolutionOrder = getResolutionOrderForParams(optionalParamNames);
187
+ const optionalResolutionOrder = getLocalResolutionOrderForParams(optionalParamNames, localResolvers);
143
188
  const orderedOptionalParams = optionalResolutionOrder
144
189
  .map((paramName) => trulyOptional.find((p) => p.name === paramName))
145
190
  .filter((param) => param !== undefined);
146
191
  for (const param of orderedOptionalParams) {
147
192
  try {
148
- const value = await this.resolveParameter(param, context);
193
+ const value = await this.resolveParameter(param, context, functionName);
149
194
  this.setNestedValue(resolvedParams, param.path, value);
150
195
  // Update context with newly resolved value
151
196
  context.resolvedParams = resolvedParams;
@@ -208,8 +253,8 @@ export class SchemaParameterResolver {
208
253
  isRequired,
209
254
  };
210
255
  }
211
- async resolveParameter(param, context) {
212
- const resolver = getResolver(param.name);
256
+ async resolveParameter(param, context, functionName) {
257
+ const resolver = this.getResolver(param.name, context.sdk, functionName);
213
258
  if (!resolver) {
214
259
  throw new Error(`No resolver found for parameter: ${param.name}`);
215
260
  }
@@ -258,7 +303,7 @@ export class SchemaParameterResolver {
258
303
  const inputs = {};
259
304
  let processedFieldKeys = new Set();
260
305
  let iteration = 0;
261
- const maxIterations = 5; // Prevent infinite loops
306
+ const maxIterations = 10; // Prevent infinite loops
262
307
  while (iteration < maxIterations) {
263
308
  iteration++;
264
309
  // Update context with current inputs so they're passed to listInputFields
@@ -270,44 +315,118 @@ export class SchemaParameterResolver {
270
315
  },
271
316
  };
272
317
  console.log(chalk.gray(`Fetching input fields for ${param.name}${iteration > 1 ? ` (iteration ${iteration})` : ""}...`));
273
- const fields = await typedResolver.fetch(updatedContext.sdk, updatedContext.resolvedParams);
274
- if (!fields || fields.length === 0) {
318
+ const rootFieldItems = await typedResolver.fetch(updatedContext.sdk, updatedContext.resolvedParams);
319
+ if (!rootFieldItems || rootFieldItems.length === 0) {
275
320
  if (iteration === 1) {
276
321
  console.log(chalk.yellow(`No input fields required for this action.`));
277
322
  }
278
323
  break;
279
324
  }
280
- // Find new fields that we haven't processed yet
281
- const newFields = fields.filter((field) => !(field.key &&
282
- processedFieldKeys.has(field.key)));
283
- if (newFields.length === 0) {
284
- // No new fields, we're done
325
+ // Process fields recursively, maintaining fieldset structure
326
+ const fieldStats = await this.processFieldItems(rootFieldItems, inputs, processedFieldKeys, [], iteration);
327
+ // If no new fields were processed, we're done
328
+ if (fieldStats.newRequired === 0 && fieldStats.newOptional === 0) {
329
+ break;
330
+ }
331
+ // If we only processed optional fields and skipped them, no need to re-fetch
332
+ if (fieldStats.newRequired === 0 && fieldStats.optionalSkipped) {
285
333
  break;
286
334
  }
287
- // Separate new required and optional fields
288
- const newRequiredFields = newFields.filter((field) => field.required);
289
- const newOptionalFields = newFields.filter((field) => !field.required);
290
- // Prompt for new required fields
291
- if (newRequiredFields.length > 0) {
292
- console.log(chalk.blue(`\n📝 Please provide values for the following ${iteration === 1 ? "" : "additional "}input fields:`));
293
- for (const field of newRequiredFields) {
294
- await this.promptForField(field, inputs);
295
- processedFieldKeys.add(field.key);
335
+ }
336
+ if (iteration >= maxIterations) {
337
+ console.log(chalk.yellow(`\n⚠️ Maximum field resolution iterations reached. Some dynamic fields may not have been discovered.`));
338
+ }
339
+ return inputs;
340
+ }
341
+ /**
342
+ * Recursively processes fieldsets and their fields, maintaining natural structure
343
+ * and creating nested inputs as needed (e.g., fieldset "foo" becomes inputs.foo = [{}])
344
+ */
345
+ async processFieldItems(items, targetInputs, processedFieldKeys, fieldsetPath = [], iteration = 1) {
346
+ let newRequiredCount = 0;
347
+ let newOptionalCount = 0;
348
+ let optionalSkipped = false;
349
+ for (const item of items) {
350
+ const typedItem = item;
351
+ if (typedItem.type === "fieldset" && typedItem.fields && typedItem.key) {
352
+ // Show fieldset context to user
353
+ const fieldsetTitle = typedItem.title || typedItem.key;
354
+ const pathDisplay = fieldsetPath.length > 0 ? ` (in ${fieldsetPath.join(" > ")})` : "";
355
+ console.log(chalk.cyan(`\n📁 Processing fieldset: ${fieldsetTitle}${pathDisplay}`));
356
+ // Create fieldset array in target inputs if it doesn't exist
357
+ if (!targetInputs[typedItem.key]) {
358
+ targetInputs[typedItem.key] = [{}];
359
+ }
360
+ // Process fields within this fieldset recursively
361
+ const fieldsetTarget = targetInputs[typedItem.key][0];
362
+ const nestedPath = [...fieldsetPath, fieldsetTitle];
363
+ const nestedStats = await this.processFieldItems(typedItem.fields, fieldsetTarget, processedFieldKeys, nestedPath, iteration);
364
+ newRequiredCount += nestedStats.newRequired;
365
+ newOptionalCount += nestedStats.newOptional;
366
+ if (nestedStats.optionalSkipped) {
367
+ optionalSkipped = true;
368
+ }
369
+ }
370
+ else if (typedItem.type === "input_field" && typedItem.key) {
371
+ // Skip if already processed
372
+ if (processedFieldKeys.has(typedItem.key)) {
373
+ continue;
374
+ }
375
+ const isRequired = typedItem.is_required || false;
376
+ if (isRequired) {
377
+ // Process required field immediately
378
+ newRequiredCount++;
379
+ if (newRequiredCount === 1 && fieldsetPath.length === 0) {
380
+ // Only show this message once at root level
381
+ console.log(chalk.blue(`\n📝 Please provide values for the following ${iteration === 1 ? "" : "additional "}input fields:`));
382
+ }
383
+ await this.promptForField(typedItem, targetInputs);
384
+ processedFieldKeys.add(typedItem.key);
385
+ }
386
+ else {
387
+ // Collect optional fields for batch processing
388
+ newOptionalCount++;
296
389
  }
297
390
  }
298
- // Ask about optional fields
299
- let shouldConfigureOptional = { configure: false };
300
- if (newOptionalFields.length > 0) {
301
- console.log(chalk.gray(`\nThere are ${newOptionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available.`));
391
+ // Skip info fields - they're for display only
392
+ }
393
+ // Handle optional fields after processing all required fields
394
+ if (newOptionalCount > 0) {
395
+ const optionalFields = items.filter((item) => {
396
+ const typedItem = item;
397
+ return (typedItem.type === "input_field" &&
398
+ typedItem.key &&
399
+ !typedItem.is_required &&
400
+ !processedFieldKeys.has(typedItem.key));
401
+ });
402
+ if (optionalFields.length > 0) {
403
+ const pathContext = fieldsetPath.length > 0 ? ` in ${fieldsetPath.join(" > ")}` : "";
404
+ console.log(chalk.gray(`\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`));
302
405
  try {
303
- shouldConfigureOptional = await inquirer.prompt([
406
+ const shouldConfigureOptional = await inquirer.prompt([
304
407
  {
305
408
  type: "confirm",
306
409
  name: "configure",
307
- message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields?`,
410
+ message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`,
308
411
  default: false,
309
412
  },
310
413
  ]);
414
+ if (shouldConfigureOptional.configure) {
415
+ console.log(chalk.cyan(`\nOptional fields${pathContext}:`));
416
+ for (const field of optionalFields) {
417
+ await this.promptForField(field, targetInputs);
418
+ const typedField = field;
419
+ processedFieldKeys.add(typedField.key);
420
+ }
421
+ }
422
+ else {
423
+ optionalSkipped = true;
424
+ // Mark these fields as processed even if skipped to avoid re-asking
425
+ optionalFields.forEach((field) => {
426
+ const typedField = field;
427
+ processedFieldKeys.add(typedField.key);
428
+ });
429
+ }
311
430
  }
312
431
  catch (error) {
313
432
  if (this.isUserCancellation(error)) {
@@ -316,28 +435,13 @@ export class SchemaParameterResolver {
316
435
  }
317
436
  throw error;
318
437
  }
319
- if (shouldConfigureOptional.configure) {
320
- console.log(chalk.cyan(`\nOptional fields:`));
321
- for (const field of newOptionalFields) {
322
- await this.promptForField(field, inputs);
323
- processedFieldKeys.add(field.key);
324
- }
325
- }
326
- else {
327
- // Mark these fields as processed even if skipped to avoid re-asking
328
- newOptionalFields.forEach((field) => processedFieldKeys.add(field.key));
329
- }
330
- }
331
- // If we only processed optional fields and skipped them, no need to re-fetch
332
- if (newRequiredFields.length === 0 &&
333
- (!newOptionalFields.length || !shouldConfigureOptional.configure)) {
334
- break;
335
438
  }
336
439
  }
337
- if (iteration >= maxIterations) {
338
- console.log(chalk.yellow(`\n⚠️ Maximum field resolution iterations reached. Some dynamic fields may not have been discovered.`));
339
- }
340
- return inputs;
440
+ return {
441
+ newRequired: newRequiredCount,
442
+ newOptional: newOptionalCount,
443
+ optionalSkipped,
444
+ };
341
445
  }
342
446
  getNestedValue(obj, path) {
343
447
  return path.reduce((current, key) => current?.[key], obj);
@@ -358,7 +462,7 @@ export class SchemaParameterResolver {
358
462
  const fieldPrompt = {
359
463
  type: fieldObj.type === "boolean" ? "confirm" : "input",
360
464
  name: fieldObj.key,
361
- message: `${fieldObj.label || fieldObj.key}${fieldObj.required ? " (required)" : " (optional)"}:`,
465
+ message: `${fieldObj.label || fieldObj.key}${fieldObj.is_required ? " (required)" : " (optional)"}:`,
362
466
  };
363
467
  if (fieldObj.helpText) {
364
468
  fieldPrompt.prefix = chalk.gray(`ℹ ${fieldObj.helpText}\n`);
@@ -381,7 +485,7 @@ export class SchemaParameterResolver {
381
485
  if (answer[fieldObj.key] !== undefined && answer[fieldObj.key] !== "") {
382
486
  inputs[fieldObj.key] = answer[fieldObj.key];
383
487
  }
384
- else if (fieldObj.required) {
488
+ else if (fieldObj.is_required) {
385
489
  throw new Error(`Required field ${fieldObj.key} cannot be empty`);
386
490
  }
387
491
  }
@@ -399,4 +503,36 @@ export class SchemaParameterResolver {
399
503
  errorObj?.message?.includes("User force closed") ||
400
504
  errorObj?.isTTYError === true);
401
505
  }
506
+ hasResolver(paramName, sdk, functionName) {
507
+ // Check plugin-specific resolvers first
508
+ if (functionName && typeof sdk.getRegistry === "function") {
509
+ const registry = sdk.getRegistry();
510
+ const functionInfo = registry.functions.find((f) => f.name === functionName);
511
+ if (functionInfo && functionInfo.resolvers?.[paramName]) {
512
+ return true;
513
+ }
514
+ }
515
+ // No global registry fallback
516
+ return false;
517
+ }
518
+ getResolver(paramName, sdk, functionName) {
519
+ // Check plugin-specific resolvers first
520
+ if (functionName && typeof sdk.getRegistry === "function") {
521
+ const registry = sdk.getRegistry();
522
+ const functionInfo = registry.functions.find((f) => f.name === functionName);
523
+ if (functionInfo && functionInfo.resolvers?.[paramName]) {
524
+ return functionInfo.resolvers[paramName];
525
+ }
526
+ }
527
+ // No global registry fallback
528
+ return null;
529
+ }
530
+ getLocalResolvers(sdk, functionName) {
531
+ if (!functionName || typeof sdk.getRegistry !== "function") {
532
+ return {};
533
+ }
534
+ const registry = sdk.getRegistry();
535
+ const functionInfo = registry.functions.find((f) => f.name === functionName);
536
+ return functionInfo?.resolvers || {};
537
+ }
402
538
  }
@@ -1,2 +1,6 @@
1
1
  import type { z } from "zod";
2
- export declare function formatItemsFromSchema(inputSchema: z.ZodType, items: unknown[], startingNumber?: number): void;
2
+ export declare function formatJsonOutput(data: unknown): void;
3
+ export declare function formatItemsFromSchema(functionInfo: {
4
+ inputSchema: z.ZodType;
5
+ outputSchema?: z.ZodType;
6
+ }, items: unknown[], startingNumber?: number): void;
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import util from "util";
2
3
  function getFormatMetadata(schema) {
3
4
  return schema?._def
4
5
  ?.formatMeta;
@@ -7,11 +8,22 @@ function getOutputSchema(schema) {
7
8
  return schema?._def?.outputSchema;
8
9
  }
9
10
  // ============================================================================
11
+ // JSON Formatting
12
+ // ============================================================================
13
+ export function formatJsonOutput(data) {
14
+ // Don't print anything for undefined results (commands that just perform actions)
15
+ if (data === undefined) {
16
+ return;
17
+ }
18
+ // Use util.inspect for colored output
19
+ console.log(util.inspect(data, { colors: true, depth: null, breakLength: 80 }));
20
+ }
21
+ // ============================================================================
10
22
  // Generic Schema-Driven Formatter
11
23
  // ============================================================================
12
- export function formatItemsFromSchema(inputSchema, items, startingNumber = 0) {
13
- // Get the output schema and its format metadata
14
- const outputSchema = getOutputSchema(inputSchema);
24
+ export function formatItemsFromSchema(functionInfo, items, startingNumber = 0) {
25
+ // Get the output schema from function info or fall back to input schema output schema
26
+ const outputSchema = functionInfo.outputSchema || getOutputSchema(functionInfo.inputSchema);
15
27
  if (!outputSchema) {
16
28
  // Fallback to generic formatting if no output schema
17
29
  formatItemsGeneric(items, startingNumber);
@@ -25,18 +37,31 @@ export function formatItemsFromSchema(inputSchema, items, startingNumber = 0) {
25
37
  }
26
38
  // Format each item using the schema metadata
27
39
  items.forEach((item, index) => {
28
- formatSingleItem(item, startingNumber + index, formatMeta);
40
+ const formatted = formatMeta.format(item);
41
+ formatSingleItem(formatted, startingNumber + index);
29
42
  });
30
43
  }
31
- function formatSingleItem(item, itemNumber, formatMeta) {
32
- // Get the formatted item from the format function
33
- const formatted = formatMeta.format(item);
34
- // Build the main title line
44
+ function formatSingleItem(formatted, itemNumber) {
45
+ // Build the main title line with optional subtitle
35
46
  let titleLine = `${chalk.gray(`${itemNumber + 1}.`)} ${chalk.cyan(formatted.title)}`;
36
- if (formatted.subtitle) {
37
- titleLine += ` ${chalk.gray(formatted.subtitle)}`;
47
+ // Generate subtitle from id or key
48
+ if (formatted.id) {
49
+ titleLine += ` ${chalk.gray(`(ID: ${formatted.id})`)}`;
50
+ }
51
+ else if (formatted.key) {
52
+ titleLine += ` ${chalk.gray(`(${formatted.key})`)}`;
38
53
  }
39
54
  console.log(titleLine);
55
+ // Show description if available
56
+ if (formatted.description) {
57
+ console.log(` ${chalk.dim(formatted.description)}`);
58
+ }
59
+ // If data is provided, use JSON formatting instead of details
60
+ if (formatted.data !== undefined) {
61
+ formatJsonOutput(formatted.data);
62
+ console.log(); // Empty line between items
63
+ return;
64
+ }
40
65
  // Format detail lines
41
66
  for (const detail of formatted.details) {
42
67
  const styledText = applyStyle(detail.text, detail.style);
@@ -59,15 +84,20 @@ function applyStyle(value, style) {
59
84
  return chalk.blue(value);
60
85
  }
61
86
  }
87
+ function convertGenericItemToFormattedItem(item) {
88
+ const itemObj = item;
89
+ return {
90
+ title: itemObj.title || itemObj.name || itemObj.key || itemObj.id || "Item",
91
+ id: itemObj.id,
92
+ key: itemObj.key,
93
+ description: itemObj.description,
94
+ details: [],
95
+ };
96
+ }
62
97
  function formatItemsGeneric(items, startingNumber = 0) {
63
- // Fallback formatting for items without schema metadata
98
+ // Convert generic items to FormattedItem and use formatSingleItem
64
99
  items.forEach((item, index) => {
65
- const itemObj = item;
66
- const name = itemObj.title || itemObj.name || itemObj.key || itemObj.id || "Item";
67
- console.log(`${chalk.gray(`${startingNumber + index + 1}.`)} ${chalk.cyan(name)}`);
68
- if (itemObj.description) {
69
- console.log(` ${chalk.dim(itemObj.description)}`);
70
- }
71
- console.log();
100
+ const formatted = convertGenericItemToFormattedItem(item);
101
+ formatSingleItem(formatted, startingNumber + index);
72
102
  });
73
103
  }