@zapier/zapier-sdk-cli
Advanced tools
+13
-0
| # @zapier/zapier-sdk-cli | ||
| ## 0.36.0 | ||
| ### Minor Changes | ||
| - d4534f3: Add support for Tables. | ||
| ### Patch Changes | ||
| - Updated dependencies [d4534f3] | ||
| - @zapier/zapier-sdk-cli-login@0.9.0 | ||
| - @zapier/zapier-sdk-mcp@0.10.0 | ||
| - @zapier/zapier-sdk@0.35.0 | ||
| ## 0.35.1 | ||
@@ -4,0 +17,0 @@ |
| { | ||
| "name": "@zapier/zapier-sdk-cli", | ||
| "version": "0.35.1", | ||
| "version": "0.36.0", | ||
| "description": "Command line interface for Zapier SDK", | ||
@@ -81,2 +81,3 @@ "main": "dist/index.cjs", | ||
| "typescript": "^5.8.3", | ||
| "wrap-ansi": "^10.0.0", | ||
| "zod": "4.2.1" | ||
@@ -83,0 +84,0 @@ }, |
@@ -106,11 +106,12 @@ import { z } from "zod"; | ||
| }, | ||
| delete: { | ||
| messageBefore: "You are about to delete this record.", | ||
| }, | ||
| delete: (itemType) => ({ | ||
| messageBefore: `You are about to delete the ${itemType || "item"}.`, | ||
| }), | ||
| }; | ||
| async function promptConfirm(confirmType) { | ||
| async function promptConfirm(confirmType, itemType) { | ||
| if (!confirmType || !CONFIRM_MESSAGES[confirmType]) { | ||
| return { confirmed: true }; // No confirmation needed | ||
| } | ||
| const { messageBefore, messageAfter } = CONFIRM_MESSAGES[confirmType]; | ||
| const configOrFn = CONFIRM_MESSAGES[confirmType]; | ||
| const { messageBefore, messageAfter } = typeof configOrFn === "function" ? configOrFn(itemType) : configOrFn; | ||
| console.log(chalk.yellow(`\n${messageBefore}\n`)); | ||
@@ -200,2 +201,3 @@ const { confirmed } = await inquirer.prompt([ | ||
| let paramType = "string"; | ||
| let elementType; | ||
| let choices; | ||
@@ -213,2 +215,13 @@ if (baseSchema instanceof z.ZodString) { | ||
| paramType = "array"; | ||
| const elementSchema = baseSchema._zod.def.element; | ||
| if (elementSchema instanceof z.ZodObject || | ||
| elementSchema instanceof z.ZodRecord) { | ||
| elementType = "object"; | ||
| } | ||
| else if (elementSchema instanceof z.ZodNumber) { | ||
| elementType = "number"; | ||
| } | ||
| else if (elementSchema instanceof z.ZodBoolean) { | ||
| elementType = "boolean"; | ||
| } | ||
| } | ||
@@ -239,2 +252,3 @@ else if (baseSchema instanceof z.ZodEnum) { | ||
| isPositional: isPositional(schema), | ||
| elementType, | ||
| }; | ||
@@ -438,3 +452,8 @@ } | ||
| const resolver = new SchemaParameterResolver(); | ||
| resolvedParams = (await resolver.resolveParameters(schema, rawParams, sdk, functionInfo.name, { interactiveMode })); | ||
| resolvedParams = (await resolver.resolveParameters(schema, rawParams, sdk, functionInfo.name, { | ||
| interactiveMode, | ||
| debug: !!options.debug || | ||
| process.env.DEBUG === "true" || | ||
| process.argv.includes("--debug"), | ||
| })); | ||
| } | ||
@@ -447,3 +466,3 @@ else { | ||
| if (confirm && interactiveMode) { | ||
| const confirmResult = await promptConfirm(confirm); | ||
| const confirmResult = await promptConfirm(confirm, functionInfo.itemType); | ||
| if (!confirmResult.confirmed) { | ||
@@ -652,3 +671,3 @@ console.log(chalk.yellow("Operation cancelled.")); | ||
| // Use the original camelCase parameter name for the SDK | ||
| sdkParams[param.name] = convertValue(positionalArgs[argIndex], param.type); | ||
| sdkParams[param.name] = convertValue(positionalArgs[argIndex], param.type, param.elementType); | ||
| argIndex++; | ||
@@ -671,3 +690,3 @@ } | ||
| } | ||
| sdkParams[camelKey] = convertValue(value, param.type); | ||
| sdkParams[camelKey] = convertValue(value, param.type, param.elementType); | ||
| } | ||
@@ -677,3 +696,3 @@ }); | ||
| } | ||
| function convertValue(value, type) { | ||
| function convertValue(value, type, elementType) { | ||
| // Don't convert undefined values - let the resolver system handle them | ||
@@ -688,4 +707,19 @@ if (value === undefined) { | ||
| return Boolean(value); | ||
| case "array": | ||
| return Array.isArray(value) ? value : [value]; | ||
| case "array": { | ||
| const arr = Array.isArray(value) ? value : [value]; | ||
| if (elementType !== "object") | ||
| return arr; | ||
| return arr.flatMap((item) => { | ||
| if (typeof item === "string" && | ||
| (item.startsWith("{") || item.startsWith("["))) { | ||
| try { | ||
| return JSON.parse(item); | ||
| } | ||
| catch { | ||
| return item; | ||
| } | ||
| } | ||
| return item; | ||
| }); | ||
| } | ||
| case "string": | ||
@@ -692,0 +726,0 @@ return value; |
| import { z } from "zod"; | ||
| import { type ZapierSdk } from "@zapier/zapier-sdk"; | ||
| export declare class SchemaParameterResolver { | ||
| private debug; | ||
| private spinner; | ||
| private debugLog; | ||
| private startSpinner; | ||
| private stopSpinner; | ||
| resolveParameters(schema: z.ZodSchema, providedParams: unknown, sdk: ZapierSdk, functionName?: string, options?: { | ||
| interactiveMode?: boolean; | ||
| debug?: boolean; | ||
| }): Promise<unknown>; | ||
@@ -23,4 +29,9 @@ private extractParametersFromSchema; | ||
| private resolveParameter; | ||
| private resolveWithResolver; | ||
| private resolveFieldsRecursively; | ||
| /** | ||
| * Resolves an array parameter by repeatedly prompting for items until user says no | ||
| */ | ||
| private resolveArrayRecursively; | ||
| /** | ||
| * Recursively processes fieldsets and their fields, maintaining natural structure | ||
@@ -27,0 +38,0 @@ * and creating nested inputs as needed (e.g., fieldset "foo" becomes inputs.foo = [{}]) |
| import inquirer from "inquirer"; | ||
| import chalk from "chalk"; | ||
| import ora from "ora"; | ||
| import { z } from "zod"; | ||
@@ -64,5 +65,27 @@ import { runWithTelemetryContext, } from "@zapier/zapier-sdk"; | ||
| export class SchemaParameterResolver { | ||
| constructor() { | ||
| this.debug = false; | ||
| this.spinner = null; | ||
| } | ||
| debugLog(message) { | ||
| if (this.debug) { | ||
| this.stopSpinner(); | ||
| console.log(chalk.gray(`[Zapier CLI] ${message}`)); | ||
| } | ||
| } | ||
| startSpinner() { | ||
| if (!this.debug && !this.spinner) { | ||
| this.spinner = ora({ text: "", spinner: "dots" }).start(); | ||
| } | ||
| } | ||
| stopSpinner() { | ||
| if (this.spinner) { | ||
| this.spinner.stop(); | ||
| this.spinner = null; | ||
| } | ||
| } | ||
| async resolveParameters(schema, providedParams, sdk, functionName, options) { | ||
| return runWithTelemetryContext(async () => { | ||
| const interactiveMode = options?.interactiveMode ?? true; | ||
| this.debug = options?.debug ?? false; | ||
| const interactiveMode = (options?.interactiveMode ?? true) && !!process.stdin.isTTY; | ||
| // 1. Try to parse with current parameters | ||
@@ -78,37 +101,18 @@ const parseResult = schema.safeParse(providedParams); | ||
| }); | ||
| // Determine parameter resolution categories: | ||
| // - functionally required: must be provided (inputs) | ||
| // - always prompt: should be prompted for but can be skipped (connectionId) | ||
| // - truly optional: only ask if user wants to be prompted | ||
| const functionallyRequired = missingResolvable.filter((param) => { | ||
| // Schema-required parameters are always functionally required | ||
| // Split missing resolvable params into required vs optional. | ||
| // "inputs" is treated as required in interactive mode. | ||
| const required = missingResolvable.filter((param) => { | ||
| if (param.isRequired) | ||
| return true; | ||
| // Keep inputs prompting in interactive mode, but do not force it in | ||
| // non-interactive mode (--json), where optional inputs should remain optional. | ||
| if (param.name === "inputs") { | ||
| if (param.name === "inputs") | ||
| return interactiveMode; | ||
| } | ||
| return false; | ||
| }); | ||
| // Parameters that should always be prompted for directly, but can be skipped | ||
| const alwaysPrompt = missingResolvable.filter((param) => { | ||
| if (functionallyRequired.includes(param)) | ||
| return false; | ||
| // connectionId should always be prompted for (since it's usually needed) | ||
| // but can be skipped with "Continue without connection" | ||
| if (param.name === "connectionId") { | ||
| return true; | ||
| } | ||
| return false; | ||
| }); | ||
| const trulyOptional = missingResolvable.filter((param) => !functionallyRequired.includes(param) && | ||
| !alwaysPrompt.includes(param)); | ||
| const optional = missingResolvable.filter((param) => !required.includes(param)); | ||
| if (parseResult.success && | ||
| functionallyRequired.length === 0 && | ||
| alwaysPrompt.length === 0) { | ||
| required.length === 0 && | ||
| optional.length === 0) { | ||
| return parseResult.data; | ||
| } | ||
| if (functionallyRequired.length === 0 && alwaysPrompt.length === 0) { | ||
| // No functionally required parameters missing, but check if we can parse | ||
| if (required.length === 0 && optional.length === 0) { | ||
| if (!parseResult.success) { | ||
@@ -119,3 +123,3 @@ throw new ZapierCliValidationError(formatZodError(parseResult.error)); | ||
| } | ||
| // 2. Resolve functionally required parameters first | ||
| // 2. Resolve required parameters | ||
| const resolvedParams = { ...providedParams }; | ||
@@ -128,21 +132,13 @@ const context = { | ||
| }; | ||
| // Get local resolvers for this function | ||
| const localResolvers = this.getLocalResolvers(sdk, functionName); | ||
| if (functionallyRequired.length > 0) { | ||
| const requiredParamNames = functionallyRequired.map((p) => p.name); | ||
| if (required.length > 0) { | ||
| const requiredParamNames = required.map((p) => p.name); | ||
| const requiredResolutionOrder = getLocalResolutionOrderForParams(requiredParamNames, localResolvers); | ||
| // Find all parameters that need to be resolved (including dependencies) | ||
| // from the available resolvable parameters | ||
| const orderedRequiredParams = requiredResolutionOrder | ||
| .map((paramName) => { | ||
| // First try to find in functionally required | ||
| let param = functionallyRequired.find((p) => p.name === paramName); | ||
| // If not found, try always prompt (for dependencies like connectionId) | ||
| let param = required.find((p) => p.name === paramName); | ||
| if (!param) { | ||
| param = alwaysPrompt.find((p) => p.name === paramName); | ||
| param = optional.find((p) => p.name === paramName); | ||
| } | ||
| // If not found, try truly optional (for other dependencies) | ||
| if (!param) { | ||
| param = trulyOptional.find((p) => p.name === paramName); | ||
| } | ||
| return param; | ||
@@ -159,3 +155,2 @@ }) | ||
| this.setNestedValue(resolvedParams, param.path, value); | ||
| // Update context with newly resolved value | ||
| context.resolvedParams = resolvedParams; | ||
@@ -172,22 +167,21 @@ } | ||
| } | ||
| // Remove resolved dependencies from other categories to avoid double-prompting | ||
| // Remove resolved dependencies from optional to avoid double-prompting | ||
| const resolvedParamNames = new Set(orderedRequiredParams.map((p) => p.name)); | ||
| alwaysPrompt.splice(0, alwaysPrompt.length, ...alwaysPrompt.filter((p) => !resolvedParamNames.has(p.name))); | ||
| trulyOptional.splice(0, trulyOptional.length, ...trulyOptional.filter((p) => !resolvedParamNames.has(p.name))); | ||
| optional.splice(0, optional.length, ...optional.filter((p) => !resolvedParamNames.has(p.name))); | ||
| } | ||
| // 3. Resolve parameters that should always be prompted for (but can be skipped). | ||
| // Skipped entirely in non-interactive mode - if connectionId was needed, it should | ||
| // have been passed explicitly via --connection-id. | ||
| if (interactiveMode && alwaysPrompt.length > 0) { | ||
| const alwaysPromptNames = alwaysPrompt.map((p) => p.name); | ||
| const alwaysPromptResolutionOrder = getLocalResolutionOrderForParams(alwaysPromptNames, localResolvers); | ||
| const orderedAlwaysPromptParams = alwaysPromptResolutionOrder | ||
| .map((paramName) => alwaysPrompt.find((p) => p.name === paramName)) | ||
| // 3. Resolve optional parameters individually (each is skippable). | ||
| // Skipped in non-interactive mode. | ||
| if (interactiveMode && optional.length > 0) { | ||
| const optionalParamNames = optional.map((p) => p.name); | ||
| const optionalResolutionOrder = getLocalResolutionOrderForParams(optionalParamNames, localResolvers); | ||
| const orderedOptionalParams = optionalResolutionOrder | ||
| .map((paramName) => optional.find((p) => p.name === paramName)) | ||
| .filter((param) => param !== undefined); | ||
| for (const param of orderedAlwaysPromptParams) { | ||
| for (const param of orderedOptionalParams) { | ||
| try { | ||
| const value = await this.resolveParameter(param, context, functionName); | ||
| this.setNestedValue(resolvedParams, param.path, value); | ||
| // Update context with newly resolved value | ||
| context.resolvedParams = resolvedParams; | ||
| const value = await this.resolveParameter(param, context, functionName, { isOptional: true }); | ||
| if (value !== undefined) { | ||
| this.setNestedValue(resolvedParams, param.path, value); | ||
| context.resolvedParams = resolvedParams; | ||
| } | ||
| } | ||
@@ -203,38 +197,3 @@ catch (error) { | ||
| } | ||
| // 4. Ask user if they want to resolve truly optional parameters (skipped in non-interactive mode) | ||
| if (interactiveMode && trulyOptional.length > 0) { | ||
| const optionalNames = trulyOptional.map((p) => p.name).join(", "); | ||
| const shouldResolveOptional = await inquirer.prompt([ | ||
| { | ||
| type: "confirm", | ||
| name: "resolveOptional", | ||
| message: `Would you like to be prompted for optional parameters (${optionalNames})?`, | ||
| default: false, | ||
| }, | ||
| ]); | ||
| if (shouldResolveOptional.resolveOptional) { | ||
| // Resolve optional parameters using their resolvers | ||
| const optionalParamNames = trulyOptional.map((p) => p.name); | ||
| const optionalResolutionOrder = getLocalResolutionOrderForParams(optionalParamNames, localResolvers); | ||
| const orderedOptionalParams = optionalResolutionOrder | ||
| .map((paramName) => trulyOptional.find((p) => p.name === paramName)) | ||
| .filter((param) => param !== undefined); | ||
| for (const param of orderedOptionalParams) { | ||
| try { | ||
| const value = await this.resolveParameter(param, context, functionName); | ||
| this.setNestedValue(resolvedParams, param.path, value); | ||
| // Update context with newly resolved value | ||
| context.resolvedParams = resolvedParams; | ||
| } | ||
| catch (error) { | ||
| if (this.isUserCancellation(error)) { | ||
| console.log(chalk.yellow("\n\nOperation cancelled by user")); | ||
| throw new ZapierCliUserCancellationError(); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // 5. Validate final parameters | ||
| // 4. Validate final parameters | ||
| const finalResult = schema.safeParse(resolvedParams); | ||
@@ -331,3 +290,3 @@ if (!finalResult.success) { | ||
| } | ||
| async resolveParameter(param, context, functionName) { | ||
| async resolveParameter(param, context, functionName, options) { | ||
| const resolver = this.getResolver(param.name, context.sdk, functionName); | ||
@@ -337,3 +296,14 @@ if (!resolver) { | ||
| } | ||
| console.log(chalk.blue(`\n🔍 Resolving ${param.name}...`)); | ||
| return this.resolveWithResolver(resolver, param, context, { | ||
| isOptional: options?.isOptional, | ||
| }); | ||
| } | ||
| async resolveWithResolver(resolver, param, context, options = {}) { | ||
| const { arrayIndex, isOptional } = options; | ||
| const inArrayContext = arrayIndex != null; | ||
| const promptLabel = inArrayContext | ||
| ? `${param.name}[${arrayIndex}]` | ||
| : param.name; | ||
| const promptName = inArrayContext ? "value" : param.name; | ||
| this.debugLog(`Resolving ${promptLabel}${isOptional ? " (optional)" : ""}`); | ||
| if (resolver.type === "static") { | ||
@@ -343,4 +313,4 @@ const staticResolver = resolver; | ||
| type: staticResolver.inputType === "password" ? "password" : "input", | ||
| name: param.name, | ||
| message: `Enter ${param.name}:`, | ||
| name: promptName, | ||
| message: `Enter ${promptLabel}${isOptional ? " (optional)" : ""}:`, | ||
| ...(staticResolver.placeholder && { | ||
@@ -350,28 +320,66 @@ default: staticResolver.placeholder, | ||
| }; | ||
| this.stopSpinner(); | ||
| const answers = await inquirer.prompt([promptConfig]); | ||
| return answers[param.name]; | ||
| const value = answers[promptName]; | ||
| if (isOptional && (value === undefined || value === "")) { | ||
| return undefined; | ||
| } | ||
| return value; | ||
| } | ||
| else if (resolver.type === "dynamic") { | ||
| const dynamicResolver = resolver; | ||
| this.startSpinner(); | ||
| const autoResolution = await this.tryAutoResolve(dynamicResolver, context); | ||
| if (autoResolution != null) { | ||
| this.stopSpinner(); | ||
| return autoResolution.resolvedValue; | ||
| } | ||
| // Only show "Fetching..." for required parameters that typically have many options | ||
| if (param.isRequired && param.name !== "connectionId") { | ||
| console.log(chalk.gray(`Fetching options for ${param.name}...`)); | ||
| this.debugLog(`Fetching options for ${promptLabel}`); | ||
| const fetchResult = await dynamicResolver.fetch(context.sdk, context.resolvedParams); | ||
| const items = Array.isArray(fetchResult) | ||
| ? fetchResult | ||
| : (fetchResult?.data ?? []); | ||
| const promptConfig = dynamicResolver.prompt(items, context.resolvedParams); | ||
| promptConfig.name = promptName; | ||
| // Inject a skip option for optional parameters | ||
| this.stopSpinner(); | ||
| if (isOptional && promptConfig.choices) { | ||
| const SKIP_SENTINEL = Symbol("SKIP"); | ||
| promptConfig.choices = [ | ||
| { name: chalk.dim("(Skip)"), value: SKIP_SENTINEL }, | ||
| ...promptConfig.choices, | ||
| ]; | ||
| const answers = await inquirer.prompt([promptConfig]); | ||
| const value = answers[promptName]; | ||
| if (value === SKIP_SENTINEL) { | ||
| return undefined; | ||
| } | ||
| return value; | ||
| } | ||
| const items = await dynamicResolver.fetch(context.sdk, context.resolvedParams); | ||
| // Let the resolver's prompt handle empty lists (e.g., connectionId can show "skip connection") | ||
| const safeItems = items || []; | ||
| const promptConfig = dynamicResolver.prompt(safeItems, context.resolvedParams); | ||
| const answers = await inquirer.prompt([promptConfig]); | ||
| return answers[param.name]; | ||
| return answers[promptName]; | ||
| } | ||
| else if (resolver.type === "fields") { | ||
| return await this.resolveFieldsRecursively(resolver, context, param); | ||
| if (isOptional && !inArrayContext) { | ||
| this.stopSpinner(); | ||
| const { confirm } = await inquirer.prompt([ | ||
| { | ||
| type: "confirm", | ||
| name: "confirm", | ||
| message: `Add ${promptLabel}?`, | ||
| default: false, | ||
| }, | ||
| ]); | ||
| if (!confirm) { | ||
| return undefined; | ||
| } | ||
| } | ||
| return await this.resolveFieldsRecursively(resolver, context, param, { inArrayContext }); | ||
| } | ||
| throw new Error(`Unknown resolver type for ${param.name}`); | ||
| else if (resolver.type === "array") { | ||
| return await this.resolveArrayRecursively(resolver, context, param); | ||
| } | ||
| throw new Error(`Unknown resolver type for ${promptLabel}`); | ||
| } | ||
| async resolveFieldsRecursively(resolver, context, param) { | ||
| async resolveFieldsRecursively(resolver, context, param, options = {}) { | ||
| const inputs = {}; | ||
@@ -391,4 +399,6 @@ let processedFieldKeys = new Set(); | ||
| }; | ||
| console.log(chalk.gray(`Fetching input fields for ${param.name}${iteration > 1 ? ` (iteration ${iteration})` : ""}...`)); | ||
| this.debugLog(`Fetching input fields for ${param.name}${iteration > 1 ? ` (iteration ${iteration})` : ""}`); | ||
| this.startSpinner(); | ||
| const rootFieldItems = await resolver.fetch(updatedContext.sdk, updatedContext.resolvedParams); | ||
| this.stopSpinner(); | ||
| if (!rootFieldItems || rootFieldItems.length === 0) { | ||
@@ -401,3 +411,3 @@ if (iteration === 1) { | ||
| // Process fields recursively, maintaining fieldset structure | ||
| const fieldStats = await this.processFieldItems(rootFieldItems, inputs, processedFieldKeys, [], iteration, updatedContext); | ||
| const fieldStats = await this.processFieldItems(rootFieldItems, inputs, processedFieldKeys, [], iteration, updatedContext, { inArrayContext: options.inArrayContext }); | ||
| // If no new fields were processed, we're done | ||
@@ -415,9 +425,52 @@ if (fieldStats.newRequired === 0 && fieldStats.newOptional === 0) { | ||
| } | ||
| // Apply transform if provided | ||
| if (resolver.transform) { | ||
| return resolver.transform(inputs); | ||
| } | ||
| return inputs; | ||
| } | ||
| /** | ||
| * Resolves an array parameter by repeatedly prompting for items until user says no | ||
| */ | ||
| async resolveArrayRecursively(resolver, context, param) { | ||
| const items = []; | ||
| const minItems = resolver.minItems ?? 0; | ||
| const maxItems = resolver.maxItems ?? Infinity; | ||
| while (items.length < maxItems) { | ||
| const currentIndex = items.length; | ||
| // Skip confirmation if we haven't hit minItems yet - just collect the item | ||
| if (currentIndex >= minItems) { | ||
| this.stopSpinner(); | ||
| const confirmAnswer = await inquirer.prompt([ | ||
| { | ||
| type: "confirm", | ||
| name: "addItem", | ||
| message: `Add ${param.name}[${currentIndex}]?`, | ||
| default: false, | ||
| }, | ||
| ]); | ||
| if (!confirmAnswer.addItem) { | ||
| break; | ||
| } | ||
| } | ||
| // Fetch the inner resolver for this item | ||
| const innerResolver = await resolver.fetch(context.sdk, context.resolvedParams); | ||
| const itemValue = await this.resolveWithResolver(innerResolver, param, context, { arrayIndex: currentIndex }); | ||
| items.push(itemValue); | ||
| // Update context with current array for subsequent iterations | ||
| context.resolvedParams = { | ||
| ...context.resolvedParams, | ||
| [param.name]: items, | ||
| }; | ||
| } | ||
| if (items.length >= maxItems) { | ||
| console.log(chalk.gray(`Maximum of ${maxItems} items reached.`)); | ||
| } | ||
| return items; | ||
| } | ||
| /** | ||
| * Recursively processes fieldsets and their fields, maintaining natural structure | ||
| * and creating nested inputs as needed (e.g., fieldset "foo" becomes inputs.foo = [{}]) | ||
| */ | ||
| async processFieldItems(items, targetInputs, processedFieldKeys, fieldsetPath = [], iteration = 1, context) { | ||
| async processFieldItems(items, targetInputs, processedFieldKeys, fieldsetPath = [], iteration = 1, context, options = {}) { | ||
| let newRequiredCount = 0; | ||
@@ -440,3 +493,3 @@ let newOptionalCount = 0; | ||
| const nestedPath = [...fieldsetPath, fieldsetTitle]; | ||
| const nestedStats = await this.processFieldItems(typedItem.fields, fieldsetTarget, processedFieldKeys, nestedPath, iteration, context); | ||
| const nestedStats = await this.processFieldItems(typedItem.fields, fieldsetTarget, processedFieldKeys, nestedPath, iteration, context, options); | ||
| newRequiredCount += nestedStats.newRequired; | ||
@@ -457,7 +510,14 @@ newOptionalCount += nestedStats.newOptional; | ||
| newRequiredCount++; | ||
| if (newRequiredCount === 1 && fieldsetPath.length === 0) { | ||
| // Only show this message once at root level | ||
| console.log(chalk.blue(`\n📝 Please provide values for the following ${iteration === 1 ? "" : "additional "}input fields:`)); | ||
| if (typedItem.resolver && context) { | ||
| const param = { | ||
| name: typedItem.key, | ||
| path: [typedItem.key], | ||
| schema: z.unknown(), | ||
| isRequired: true, | ||
| }; | ||
| targetInputs[typedItem.key] = await this.resolveWithResolver(typedItem.resolver, param, context, { isOptional: false }); | ||
| } | ||
| await this.promptForField(typedItem, targetInputs, context); | ||
| else { | ||
| await this.promptForField(typedItem, targetInputs, context); | ||
| } | ||
| processedFieldKeys.add(typedItem.key); | ||
@@ -483,36 +543,46 @@ } | ||
| const pathContext = fieldsetPath.length > 0 ? ` in ${fieldsetPath.join(" > ")}` : ""; | ||
| console.log(chalk.gray(`\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`)); | ||
| try { | ||
| const shouldConfigureOptional = await inquirer.prompt([ | ||
| { | ||
| type: "confirm", | ||
| name: "configure", | ||
| message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`, | ||
| default: false, | ||
| }, | ||
| ]); | ||
| if (shouldConfigureOptional.configure) { | ||
| console.log(chalk.cyan(`\nOptional fields${pathContext}:`)); | ||
| for (const field of optionalFields) { | ||
| await this.promptForField(field, targetInputs, context); | ||
| const typedField = field; | ||
| processedFieldKeys.add(typedField.key); | ||
| // In array context, prompt for all fields directly without confirmation | ||
| if (options.inArrayContext) { | ||
| for (const field of optionalFields) { | ||
| await this.promptForField(field, targetInputs, context); | ||
| const typedField = field; | ||
| processedFieldKeys.add(typedField.key); | ||
| } | ||
| } | ||
| else { | ||
| console.log(chalk.gray(`\nThere are ${optionalFields.length} ${iteration === 1 ? "" : "additional "}optional field(s) available${pathContext}.`)); | ||
| try { | ||
| const shouldConfigureOptional = await inquirer.prompt([ | ||
| { | ||
| type: "confirm", | ||
| name: "configure", | ||
| message: `Would you like to configure ${iteration === 1 ? "" : "these additional "}optional fields${pathContext}?`, | ||
| default: false, | ||
| }, | ||
| ]); | ||
| if (shouldConfigureOptional.configure) { | ||
| console.log(chalk.cyan(`\nOptional fields${pathContext}:`)); | ||
| for (const field of optionalFields) { | ||
| await this.promptForField(field, targetInputs, context); | ||
| const typedField = field; | ||
| processedFieldKeys.add(typedField.key); | ||
| } | ||
| } | ||
| else { | ||
| optionalSkipped = true; | ||
| // Mark these fields as processed even if skipped to avoid re-asking | ||
| optionalFields.forEach((field) => { | ||
| const typedField = field; | ||
| processedFieldKeys.add(typedField.key); | ||
| }); | ||
| } | ||
| } | ||
| else { | ||
| optionalSkipped = true; | ||
| // Mark these fields as processed even if skipped to avoid re-asking | ||
| optionalFields.forEach((field) => { | ||
| const typedField = field; | ||
| processedFieldKeys.add(typedField.key); | ||
| }); | ||
| catch (error) { | ||
| if (this.isUserCancellation(error)) { | ||
| console.log(chalk.yellow("\n\nOperation cancelled by user")); | ||
| throw new ZapierCliUserCancellationError(); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| catch (error) { | ||
| if (this.isUserCancellation(error)) { | ||
| console.log(chalk.yellow("\n\nOperation cancelled by user")); | ||
| throw new ZapierCliUserCancellationError(); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
@@ -553,5 +623,6 @@ } | ||
| valueType, | ||
| hasDropdown: fieldObj.format === "SELECT", | ||
| hasDropdown: fieldObj.format === "SELECT" || Boolean(fieldObj.choices), | ||
| isMultiSelect: Boolean(valueType === "array" || | ||
| (fieldObj.items && fieldObj.items.type !== undefined)), | ||
| inlineChoices: fieldObj.choices, | ||
| }; | ||
@@ -564,5 +635,6 @@ } | ||
| try { | ||
| console.log(chalk.gray(cursor | ||
| ? ` Fetching more choices...` | ||
| : ` Fetching choices for ${fieldMeta.title}...`)); | ||
| this.debugLog(cursor | ||
| ? `Fetching more choices for ${fieldMeta.title}` | ||
| : `Fetching choices for ${fieldMeta.title}`); | ||
| this.startSpinner(); | ||
| const page = await context.sdk.listInputFieldChoices({ | ||
@@ -577,2 +649,3 @@ appKey: context.resolvedParams.appKey, | ||
| }); | ||
| this.stopSpinner(); | ||
| const choices = page.data.map((choice) => ({ | ||
@@ -583,3 +656,3 @@ label: choice.label || choice.key || String(choice.value), | ||
| if (choices.length === 0 && !cursor) { | ||
| console.log(chalk.yellow(` No choices available for ${fieldMeta.title}`)); | ||
| console.log(chalk.yellow(`No choices available for ${fieldMeta.title}`)); | ||
| } | ||
@@ -592,3 +665,4 @@ return { | ||
| catch (error) { | ||
| console.warn(chalk.yellow(` ⚠️ Failed to fetch choices for ${fieldMeta.title}:`), error); | ||
| this.stopSpinner(); | ||
| console.warn(chalk.yellow(`Failed to fetch choices for ${fieldMeta.title}:`), error); | ||
| return { choices: [] }; | ||
@@ -601,2 +675,3 @@ } | ||
| async promptWithChoices({ fieldMeta, choices: initialChoices, nextCursor: initialCursor, inputs, context, }) { | ||
| this.stopSpinner(); | ||
| const choices = [...initialChoices]; | ||
@@ -673,2 +748,27 @@ let nextCursor = initialCursor; | ||
| } | ||
| else if (fieldMeta.valueType === "array") { | ||
| promptConfig.type = "input"; | ||
| promptConfig.default = fieldMeta.defaultValue; | ||
| promptConfig.message = `${fieldMeta.title}${fieldMeta.isRequired ? " (required)" : " (optional)"} (JSON array or comma-separated):`; | ||
| promptConfig.validate = (input) => { | ||
| if (fieldMeta.isRequired && !input) { | ||
| return "This field is required"; | ||
| } | ||
| return true; | ||
| }; | ||
| promptConfig.filter = (input) => { | ||
| if (!input) | ||
| return input; | ||
| const trimmed = input.trim(); | ||
| if (trimmed.startsWith("[")) { | ||
| try { | ||
| return JSON.parse(trimmed); | ||
| } | ||
| catch { | ||
| // Fall through to comma-separated | ||
| } | ||
| } | ||
| return trimmed.split(",").map((s) => s.trim()); | ||
| }; | ||
| } | ||
| else { | ||
@@ -722,6 +822,11 @@ promptConfig.type = "input"; | ||
| const fieldMeta = this.extractFieldMetadata(field); | ||
| // Fetch choices if field has dropdown | ||
| // Get choices - either inline or fetched from API | ||
| let choices = []; | ||
| let nextCursor; | ||
| if (fieldMeta.hasDropdown && context) { | ||
| if (fieldMeta.inlineChoices) { | ||
| // Use inline choices directly | ||
| choices = fieldMeta.inlineChoices; | ||
| } | ||
| else if (fieldMeta.hasDropdown && context) { | ||
| // Fetch choices from API | ||
| const result = await this.fetchChoices(fieldMeta, inputs, context); | ||
@@ -728,0 +833,0 @@ choices = result.choices; |
| import type { z } from "zod"; | ||
| import type { OutputFormatter, ZapierSdk } from "@zapier/zapier-sdk"; | ||
| export declare function formatJsonOutput(data: unknown): void; | ||
@@ -6,2 +7,6 @@ export declare function formatItemsFromSchema(functionInfo: { | ||
| outputSchema?: z.ZodType; | ||
| }, items: unknown[], startingNumber?: number): void; | ||
| }, items: unknown[], startingNumber?: number, options?: { | ||
| formatter?: OutputFormatter; | ||
| sdk?: ZapierSdk; | ||
| params?: Record<string, unknown>; | ||
| }): Promise<void>; |
| import chalk from "chalk"; | ||
| import util from "util"; | ||
| import wrapAnsi from "wrap-ansi"; | ||
| // These functions are internal to SDK, implementing basic formatting fallback | ||
@@ -27,3 +28,17 @@ // TODO: Consider exposing these utilities or implementing proper CLI formatting | ||
| // ============================================================================ | ||
| export function formatItemsFromSchema(functionInfo, items, startingNumber = 0) { | ||
| export async function formatItemsFromSchema(functionInfo, items, startingNumber = 0, options) { | ||
| // If a registry-level OutputFormatter is provided, use it | ||
| if (options?.formatter) { | ||
| let context; | ||
| if (options.formatter.fetch && options.sdk && options.params) { | ||
| for (const item of items) { | ||
| context = await options.formatter.fetch(options.sdk, options.params, item, context); | ||
| } | ||
| } | ||
| items.forEach((item, index) => { | ||
| const formatted = options.formatter.format(item, context); | ||
| formatSingleItem(formatted, startingNumber + index); | ||
| }); | ||
| return; | ||
| } | ||
| // Get the output schema from function info or fall back to input schema output schema | ||
@@ -80,7 +95,34 @@ const outputSchema = functionInfo.outputSchema || getOutputSchema(functionInfo.inputSchema); | ||
| for (const detail of formatted.details) { | ||
| const styledText = applyStyle(detail.text, detail.style); | ||
| console.log(` ${styledText}`); | ||
| if (detail.label) { | ||
| const isMultiline = detail.text.includes("\n"); | ||
| if (isMultiline) { | ||
| console.log(` ${chalk.gray(detail.label + ":")}`); | ||
| const displayText = formatDetailText(detail.text, DETAIL_INDENT + " "); | ||
| const styledText = applyStyle(displayText, detail.style); | ||
| console.log(`${DETAIL_INDENT} ${styledText}`); | ||
| } | ||
| else { | ||
| const styledValue = applyStyle(detail.text, detail.style); | ||
| console.log(` ${chalk.gray(detail.label + ":")} ${styledValue}`); | ||
| } | ||
| } | ||
| else { | ||
| const displayText = formatDetailText(detail.text, DETAIL_INDENT); | ||
| const styledText = applyStyle(displayText, detail.style); | ||
| console.log(` ${styledText}`); | ||
| } | ||
| } | ||
| console.log(); // Empty line between items | ||
| } | ||
| const DETAIL_INDENT = " "; | ||
| const DETAIL_MAX_LINES = 5; | ||
| function formatDetailText(text, indent = DETAIL_INDENT) { | ||
| const columns = Math.max((process.stdout.columns || 80) - indent.length, 40); | ||
| const wrapped = wrapAnsi(text, columns, { hard: true, trim: false }); | ||
| const lines = wrapped.split("\n"); | ||
| if (lines.length <= DETAIL_MAX_LINES) { | ||
| return lines.join("\n" + indent); | ||
| } | ||
| return (lines.slice(0, DETAIL_MAX_LINES).join("\n" + indent) + "\n" + indent + "…"); | ||
| } | ||
| function applyStyle(value, style) { | ||
@@ -87,0 +129,0 @@ switch (style) { |
+5
-4
| { | ||
| "name": "@zapier/zapier-sdk-cli", | ||
| "version": "0.35.1", | ||
| "version": "0.36.0", | ||
| "description": "Command line interface for Zapier SDK", | ||
@@ -69,6 +69,7 @@ "main": "dist/index.cjs", | ||
| "typescript": "^5.8.3", | ||
| "wrap-ansi": "^10.0.0", | ||
| "zod": "4.2.1", | ||
| "@zapier/zapier-sdk-mcp": "0.9.23", | ||
| "@zapier/zapier-sdk-cli-login": "0.8.3", | ||
| "@zapier/zapier-sdk": "0.34.1" | ||
| "@zapier/zapier-sdk": "0.35.0", | ||
| "@zapier/zapier-sdk-cli-login": "0.9.0", | ||
| "@zapier/zapier-sdk-mcp": "0.10.0" | ||
| }, | ||
@@ -75,0 +76,0 @@ "devDependencies": { |
+232
-0
@@ -35,2 +35,15 @@ # @zapier/zapier-sdk-cli | ||
| - [`curl`](#curl) | ||
| - [Tables](#tables) | ||
| - [`create-table`](#create-table) | ||
| - [`create-table-fields`](#create-table-fields) | ||
| - [`create-table-records`](#create-table-records) | ||
| - [`delete-table`](#delete-table) | ||
| - [`delete-table-fields`](#delete-table-fields) | ||
| - [`delete-table-records`](#delete-table-records) | ||
| - [`get-table`](#get-table) | ||
| - [`get-table-record`](#get-table-record) | ||
| - [`list-table-fields`](#list-table-fields) | ||
| - [`list-table-records`](#list-table-records) | ||
| - [`list-tables`](#list-tables) | ||
| - [`update-table-records`](#update-table-records) | ||
| - [Utilities](#utilities) | ||
@@ -516,2 +529,221 @@ - [`add`](#add) | ||
| ### Tables | ||
| #### `create-table` | ||
| Create a new table | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | --------------- | -------- | -------- | ------- | --------------- | ------------------------------------ | | ||
| | `<name>` | `string` | ✅ | — | — | The name for the new table | | ||
| | `--description` | `string` | ❌ | — | — | An optional description of the table | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk create-table <name> [--description] | ||
| ``` | ||
| #### `create-table-fields` | ||
| Create one or more fields in a table | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | ------------ | -------- | -------- | ------- | --------------- | ------------------------------------ | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table | | ||
| | `<fields>` | `array` | ✅ | — | — | Array of field definitions to create | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk create-table-fields <table-id> <fields> | ||
| ``` | ||
| #### `create-table-records` | ||
| Create one or more records in a table | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | ------------ | -------- | -------- | --------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table | | ||
| | `<records>` | `array` | ✅ | — | — | Array of records to create (max 100) | | ||
| | `--key-mode` | `string` | ❌ | `"names"` | — | How to interpret field keys in record data. "names" (default) uses human-readable field names, "ids" uses raw field IDs (f1, f2). | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk create-table-records <table-id> <records> [--key-mode] | ||
| ``` | ||
| #### `delete-table` | ||
| Delete a table by its ID | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | ------------ | -------- | -------- | ------- | --------------- | -------------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table to delete | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk delete-table <table-id> | ||
| ``` | ||
| #### `delete-table-fields` | ||
| Delete one or more fields from a table | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | -------------- | -------- | -------- | ------- | --------------- | ------------------------------------------------------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table | | ||
| | `<field-keys>` | `array` | ✅ | — | — | Fields to delete. Accepts field names (e.g., "Email") or IDs (e.g., "f6", "6", or 6). | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk delete-table-fields <table-id> <field-keys> | ||
| ``` | ||
| #### `delete-table-records` | ||
| Delete one or more records from a table | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | -------------- | -------- | -------- | ------- | --------------- | --------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table | | ||
| | `<record-ids>` | `array` | ✅ | — | — | Array of record IDs to delete (max 100) | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk delete-table-records <table-id> <record-ids> | ||
| ``` | ||
| #### `get-table` | ||
| Get detailed information about a specific table | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | ------------ | -------- | -------- | ------- | --------------- | ---------------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table to retrieve | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk get-table <table-id> | ||
| ``` | ||
| #### `get-table-record` | ||
| Get a single record from a table by ID | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | ------------- | -------- | -------- | --------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table | | ||
| | `<record-id>` | `string` | ✅ | — | — | The unique identifier of the record | | ||
| | `--key-mode` | `string` | ❌ | `"names"` | — | How to interpret field keys in record data. "names" (default) uses human-readable field names, "ids" uses raw field IDs (f1, f2). | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk get-table-record <table-id> <record-id> [--key-mode] | ||
| ``` | ||
| #### `list-table-fields` | ||
| List fields for a table | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | -------------- | -------- | -------- | ------- | --------------- | ---------------------------------------------------------------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table | | ||
| | `--field-keys` | `array` | ❌ | — | — | Filter by specific fields. Accepts field names (e.g., "Email") or IDs (e.g., "f6", "6", or 6). | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk list-table-fields <table-id> [--field-keys] | ||
| ``` | ||
| #### `list-table-records` | ||
| List records in a table with optional filtering and sorting | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | --------------- | -------- | -------- | --------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table | | ||
| | `--filters` | `array` | ❌ | — | — | Filter conditions for the query | | ||
| | `--sort` | `object` | ❌ | — | — | Sort records by a field | | ||
| | ↳ `<field-key>` | `string` | ✅ | — | — | The field key to sort by | | ||
| | ↳ `--direction` | `string` | ❌ | `"asc"` | — | Sort direction | | ||
| | `--page-size` | `number` | ❌ | — | — | Number of records per page (max 1000) | | ||
| | `--max-items` | `number` | ❌ | — | — | Maximum total items to return across all pages | | ||
| | `--cursor` | `string` | ❌ | — | — | Cursor to start from | | ||
| | `--key-mode` | `string` | ❌ | `"names"` | — | How to interpret field keys in record data. "names" (default) uses human-readable field names, "ids" uses raw field IDs (f1, f2). | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk list-table-records <table-id> <field-key> [--filters] [--sort] [--direction] [--page-size] [--max-items] [--cursor] [--key-mode] | ||
| ``` | ||
| #### `list-tables` | ||
| List tables available to the authenticated user | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | ------------- | -------- | -------- | ------- | -------------------------------- | --------------------------------------------------------------------------- | | ||
| | `--table-ids` | `array` | ❌ | — | — | Filter by specific table IDs | | ||
| | `--kind` | `string` | ❌ | — | `table`, `virtual_table`, `both` | Filter by table type | | ||
| | `--search` | `string` | ❌ | — | — | Search term to filter tables by name | | ||
| | `--owner` | `string` | ❌ | — | — | Filter by table owner. Use "me" for the current user, or a numeric user ID. | | ||
| | `--page-size` | `number` | ❌ | — | — | Number of tables per page | | ||
| | `--max-items` | `number` | ❌ | — | — | Maximum total items to return across all pages | | ||
| | `--cursor` | `string` | ❌ | — | — | Cursor to start from | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk list-tables [--table-ids] [--kind] [--search] [--owner] [--page-size] [--max-items] [--cursor] | ||
| ``` | ||
| #### `update-table-records` | ||
| Update one or more records in a table | ||
| **Options:** | ||
| | Option | Type | Required | Default | Possible Values | Description | | ||
| | ------------ | -------- | -------- | --------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `<table-id>` | `string` | ✅ | — | — | The unique identifier of the table | | ||
| | `<records>` | `array` | ✅ | — | — | Array of records to update (max 100) | | ||
| | `--key-mode` | `string` | ❌ | `"names"` | — | How to interpret field keys in record data. "names" (default) uses human-readable field names, "ids" uses raw field IDs (f1, f2). | | ||
| **Usage:** | ||
| ```bash | ||
| npx zapier-sdk update-table-records <table-id> <records> [--key-mode] | ||
| ``` | ||
| ### Utilities | ||
@@ -518,0 +750,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
1003581
3.12%20928
2.62%884
35.58%20
5.26%77
4.05%82
6.49%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
Updated