@zapier/zapier-sdk-cli 0.16.1 → 0.16.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/cli.cjs +7 -7
- package/dist/cli.mjs +7 -7
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/package.json +8 -2
- package/dist/src/cli.js +2 -1
- package/dist/src/utils/cli-generator.js +8 -7
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -5
- package/src/cli.test.ts +0 -28
- package/src/cli.ts +0 -96
- package/src/generators/ast-generator.test.ts +0 -908
- package/src/generators/ast-generator.ts +0 -774
- package/src/index.ts +0 -12
- package/src/plugins/add/index.test.ts +0 -58
- package/src/plugins/add/index.ts +0 -177
- package/src/plugins/add/schemas.ts +0 -35
- package/src/plugins/buildManifest/index.test.ts +0 -679
- package/src/plugins/buildManifest/index.ts +0 -131
- package/src/plugins/buildManifest/schemas.ts +0 -55
- package/src/plugins/bundleCode/index.ts +0 -128
- package/src/plugins/bundleCode/schemas.ts +0 -24
- package/src/plugins/generateAppTypes/index.test.ts +0 -679
- package/src/plugins/generateAppTypes/index.ts +0 -227
- package/src/plugins/generateAppTypes/schemas.ts +0 -61
- package/src/plugins/getLoginConfigPath/index.ts +0 -45
- package/src/plugins/getLoginConfigPath/schemas.ts +0 -10
- package/src/plugins/index.ts +0 -8
- package/src/plugins/login/index.ts +0 -135
- package/src/plugins/login/schemas.ts +0 -13
- package/src/plugins/logout/index.ts +0 -37
- package/src/plugins/logout/schemas.ts +0 -8
- package/src/plugins/mcp/index.ts +0 -43
- package/src/plugins/mcp/schemas.ts +0 -13
- package/src/sdk.ts +0 -45
- package/src/telemetry/builders.ts +0 -113
- package/src/telemetry/events.ts +0 -39
- package/src/types/sdk.ts +0 -8
- package/src/utils/api/client.ts +0 -44
- package/src/utils/auth/login.ts +0 -214
- package/src/utils/cli-generator-utils.ts +0 -169
- package/src/utils/cli-generator.test.ts +0 -347
- package/src/utils/cli-generator.ts +0 -807
- package/src/utils/constants.ts +0 -9
- package/src/utils/directory-detection.ts +0 -23
- package/src/utils/errors.ts +0 -26
- package/src/utils/getCallablePromise.ts +0 -21
- package/src/utils/log.ts +0 -23
- package/src/utils/manifest-helpers.ts +0 -25
- package/src/utils/package-manager-detector.ts +0 -83
- package/src/utils/parameter-resolver.ts +0 -1075
- package/src/utils/schema-formatter.ts +0 -153
- package/src/utils/serializeAsync.ts +0 -26
- package/src/utils/spinner.ts +0 -23
- package/src/utils/version-checker.test.ts +0 -239
- package/src/utils/version-checker.ts +0 -237
- package/tsconfig.build.json +0 -18
- package/tsconfig.json +0 -19
- 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
|
-
}
|