@zapier/zapier-sdk-cli 0.0.1
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/bin/zapier-sdk.js +4 -0
- package/bin/zsdk.js +4 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +25 -0
- package/dist/commands/action.d.ts +2 -0
- package/dist/commands/action.js +295 -0
- package/dist/commands/browse.d.ts +2 -0
- package/dist/commands/browse.js +257 -0
- package/dist/commands/bundle.d.ts +2 -0
- package/dist/commands/bundle.js +101 -0
- package/dist/commands/generate.d.ts +9 -0
- package/dist/commands/generate.js +281 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +3 -0
- package/dist/utils/auth-picker.d.ts +17 -0
- package/dist/utils/auth-picker.js +121 -0
- package/dist/utils/cli-generator.d.ts +4 -0
- package/dist/utils/cli-generator.js +414 -0
- package/dist/utils/pager.d.ts +48 -0
- package/dist/utils/pager.js +147 -0
- package/dist/utils/parameter-resolver.d.ts +18 -0
- package/dist/utils/parameter-resolver.js +413 -0
- package/dist/utils/schema-formatter.d.ts +2 -0
- package/dist/utils/schema-formatter.js +72 -0
- package/dist/utils/schema-generator.d.ts +4 -0
- package/dist/utils/schema-generator.js +389 -0
- package/package.json +42 -0
- package/src/cli.ts +32 -0
- package/src/index.ts +2 -0
- package/src/utils/cli-generator.ts +578 -0
- package/src/utils/pager.ts +202 -0
- package/src/utils/parameter-resolver.ts +573 -0
- package/src/utils/schema-formatter.ts +88 -0
- package/test/cli.test.ts +46 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ActionsSDK } from "@zapier/actions-sdk";
|
|
5
|
+
|
|
6
|
+
// For editor-like prompts, we'll use a regular input prompt with multiline support
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
interface ResolvableParameter {
|
|
13
|
+
name: string;
|
|
14
|
+
path: string[];
|
|
15
|
+
schema: z.ZodType;
|
|
16
|
+
description?: string;
|
|
17
|
+
resolverMeta?: any;
|
|
18
|
+
isRequired: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ResolverContext {
|
|
22
|
+
sdk: ActionsSDK;
|
|
23
|
+
currentParams: any;
|
|
24
|
+
resolvedParams: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Schema Parameter Resolver
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export class SchemaParameterResolver {
|
|
32
|
+
async resolveParameters(
|
|
33
|
+
schema: z.ZodSchema,
|
|
34
|
+
providedParams: any,
|
|
35
|
+
sdk: ActionsSDK,
|
|
36
|
+
): Promise<any> {
|
|
37
|
+
// 1. Try to parse with current parameters
|
|
38
|
+
const parseResult = schema.safeParse(providedParams);
|
|
39
|
+
|
|
40
|
+
// Even if parsing succeeds, check if we should resolve optional but important parameters
|
|
41
|
+
const allResolvable = this.findAllResolvableParameters(schema);
|
|
42
|
+
const shouldResolveAnyway = allResolvable.filter((param) => {
|
|
43
|
+
const hasValue =
|
|
44
|
+
this.getNestedValue(providedParams, param.path) !== undefined;
|
|
45
|
+
// Resolve any parameters with resolvers even if optional, unless explicitly provided
|
|
46
|
+
return !hasValue && param.resolverMeta?.resolver;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (parseResult.success && shouldResolveAnyway.length === 0) {
|
|
50
|
+
return parseResult.data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Analyze what's missing and can be resolved
|
|
54
|
+
let missingResolvable: ResolvableParameter[];
|
|
55
|
+
|
|
56
|
+
if (!parseResult.success) {
|
|
57
|
+
missingResolvable = this.findResolvableParameters(
|
|
58
|
+
schema,
|
|
59
|
+
parseResult.error,
|
|
60
|
+
providedParams,
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
// Schema parsing succeeded, but we want to resolve optional important parameters
|
|
64
|
+
missingResolvable = shouldResolveAnyway;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (missingResolvable.length === 0) {
|
|
68
|
+
// No resolvable parameters, throw original error
|
|
69
|
+
throw parseResult.error;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Resolve missing parameters interactively
|
|
73
|
+
const resolvedParams = { ...providedParams };
|
|
74
|
+
const context: ResolverContext = {
|
|
75
|
+
sdk,
|
|
76
|
+
currentParams: providedParams,
|
|
77
|
+
resolvedParams,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Sort by dependency order (parameters that depend on others go last)
|
|
81
|
+
const sortedMissing = this.sortByDependencies(missingResolvable);
|
|
82
|
+
|
|
83
|
+
for (const missing of sortedMissing) {
|
|
84
|
+
try {
|
|
85
|
+
const value = await this.resolveParameter(missing, context);
|
|
86
|
+
this.setNestedValue(resolvedParams, missing.path, value);
|
|
87
|
+
|
|
88
|
+
// Update context with newly resolved value
|
|
89
|
+
context.resolvedParams = resolvedParams;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (this.isUserCancellation(error)) {
|
|
92
|
+
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
console.error(chalk.red(`Failed to resolve ${missing.name}:`), error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4. Validate final parameters
|
|
101
|
+
const finalResult = schema.safeParse(resolvedParams);
|
|
102
|
+
|
|
103
|
+
if (!finalResult.success) {
|
|
104
|
+
console.error(
|
|
105
|
+
chalk.red("❌ Parameter validation failed after resolution:"),
|
|
106
|
+
);
|
|
107
|
+
throw finalResult.error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return finalResult.data;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private findAllResolvableParameters(
|
|
114
|
+
schema: z.ZodSchema,
|
|
115
|
+
): ResolvableParameter[] {
|
|
116
|
+
const resolvable: ResolvableParameter[] = [];
|
|
117
|
+
this.analyzeSchema(schema, [], resolvable);
|
|
118
|
+
return resolvable;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private findResolvableParameters(
|
|
122
|
+
schema: z.ZodSchema,
|
|
123
|
+
error: z.ZodError,
|
|
124
|
+
providedParams: any,
|
|
125
|
+
): ResolvableParameter[] {
|
|
126
|
+
const resolvable = this.findAllResolvableParameters(schema);
|
|
127
|
+
|
|
128
|
+
// Filter to only include parameters that are actually missing/invalid
|
|
129
|
+
const errorPaths = new Set(
|
|
130
|
+
error.issues.map((issue) => issue.path.join(".")),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return resolvable.filter((param) => {
|
|
134
|
+
const paramPath = param.path.join(".");
|
|
135
|
+
const isErrorPath = errorPaths.has(paramPath);
|
|
136
|
+
const hasValue =
|
|
137
|
+
this.getNestedValue(providedParams, param.path) !== undefined;
|
|
138
|
+
|
|
139
|
+
// Include if there's an error for this path or if it's required and missing
|
|
140
|
+
// Also include optional parameters that have resolvers and aren't provided
|
|
141
|
+
const shouldResolve =
|
|
142
|
+
isErrorPath ||
|
|
143
|
+
(param.isRequired && !hasValue) ||
|
|
144
|
+
(!param.isRequired && !hasValue && param.resolverMeta?.resolver);
|
|
145
|
+
|
|
146
|
+
return shouldResolve;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private analyzeSchema(
|
|
151
|
+
schema: z.ZodType,
|
|
152
|
+
currentPath: string[],
|
|
153
|
+
resolvable: ResolvableParameter[],
|
|
154
|
+
): void {
|
|
155
|
+
// Handle different Zod types
|
|
156
|
+
if (schema instanceof z.ZodObject) {
|
|
157
|
+
const shape = schema.shape;
|
|
158
|
+
|
|
159
|
+
Object.entries(shape).forEach(([key, fieldSchema]) => {
|
|
160
|
+
const fieldPath = [...currentPath, key];
|
|
161
|
+
this.analyzeSchemaField(
|
|
162
|
+
key,
|
|
163
|
+
fieldSchema as z.ZodType,
|
|
164
|
+
fieldPath,
|
|
165
|
+
resolvable,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
} else if (schema instanceof z.ZodOptional) {
|
|
169
|
+
this.analyzeSchema(schema.unwrap(), currentPath, resolvable);
|
|
170
|
+
} else if (schema instanceof z.ZodDefault) {
|
|
171
|
+
this.analyzeSchema(schema.removeDefault(), currentPath, resolvable);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private analyzeSchemaField(
|
|
176
|
+
name: string,
|
|
177
|
+
schema: z.ZodType,
|
|
178
|
+
path: string[],
|
|
179
|
+
resolvable: ResolvableParameter[],
|
|
180
|
+
): void {
|
|
181
|
+
const resolverMeta = (schema._def as any).resolverMeta;
|
|
182
|
+
|
|
183
|
+
if (resolverMeta?.resolver) {
|
|
184
|
+
resolvable.push({
|
|
185
|
+
name,
|
|
186
|
+
path,
|
|
187
|
+
schema,
|
|
188
|
+
description: schema.description,
|
|
189
|
+
resolverMeta,
|
|
190
|
+
isRequired: !schema.isOptional(),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Recursively analyze nested objects
|
|
195
|
+
if (schema instanceof z.ZodObject) {
|
|
196
|
+
this.analyzeSchema(schema, path, resolvable);
|
|
197
|
+
} else if (schema instanceof z.ZodOptional) {
|
|
198
|
+
this.analyzeSchemaField(name, schema.unwrap(), path, resolvable);
|
|
199
|
+
} else if (schema instanceof z.ZodDefault) {
|
|
200
|
+
this.analyzeSchemaField(name, schema.removeDefault(), path, resolvable);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private sortByDependencies(
|
|
205
|
+
parameters: ResolvableParameter[],
|
|
206
|
+
): ResolvableParameter[] {
|
|
207
|
+
// Simple topological sort based on 'depends' field
|
|
208
|
+
const sorted: ResolvableParameter[] = [];
|
|
209
|
+
const remaining = [...parameters];
|
|
210
|
+
|
|
211
|
+
while (remaining.length > 0) {
|
|
212
|
+
const canResolve = remaining.filter((param) => {
|
|
213
|
+
const depends = param.resolverMeta?.resolver?.depends || [];
|
|
214
|
+
return depends.every((dep: string) =>
|
|
215
|
+
sorted.some((resolved) => resolved.name === dep),
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (canResolve.length === 0) {
|
|
220
|
+
// No more resolvable parameters, add remaining ones (circular dependency or no dependencies)
|
|
221
|
+
sorted.push(...remaining);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
sorted.push(...canResolve);
|
|
226
|
+
canResolve.forEach((param) => {
|
|
227
|
+
const index = remaining.indexOf(param);
|
|
228
|
+
remaining.splice(index, 1);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return sorted;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async resolveParameter(
|
|
236
|
+
paramInfo: ResolvableParameter,
|
|
237
|
+
context: ResolverContext,
|
|
238
|
+
): Promise<any> {
|
|
239
|
+
const resolver = paramInfo.resolverMeta?.resolver;
|
|
240
|
+
|
|
241
|
+
if (!resolver) {
|
|
242
|
+
return await this.promptStaticInput(paramInfo);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (resolver.type === "static") {
|
|
246
|
+
return await this.promptStaticInput(paramInfo, resolver);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (resolver.type === "dynamic") {
|
|
250
|
+
return await this.resolveDynamicParameter(paramInfo, resolver, context);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (resolver.type === "fields") {
|
|
254
|
+
return await this.resolveFieldsParameter(paramInfo, resolver, context);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
throw new Error(`Unknown resolver type: ${resolver.type}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async promptStaticInput(
|
|
261
|
+
paramInfo: ResolvableParameter,
|
|
262
|
+
resolver?: any,
|
|
263
|
+
): Promise<any> {
|
|
264
|
+
const promptConfig: any = {
|
|
265
|
+
type:
|
|
266
|
+
resolver?.inputType === "editor"
|
|
267
|
+
? "input"
|
|
268
|
+
: resolver?.inputType || "input",
|
|
269
|
+
name: "value",
|
|
270
|
+
message: `Enter ${paramInfo.description || paramInfo.name}${paramInfo.isRequired ? "" : " (optional)"}:`,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Only use placeholder as default for required parameters
|
|
274
|
+
if (resolver?.placeholder && paramInfo.isRequired) {
|
|
275
|
+
promptConfig.default = resolver.placeholder;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Add validation for required parameters
|
|
279
|
+
if (paramInfo.isRequired) {
|
|
280
|
+
promptConfig.validate = (input: any) => {
|
|
281
|
+
if (!input || (typeof input === "string" && input.trim() === "")) {
|
|
282
|
+
return "This field is required. Please enter a value.";
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// For editor-like prompts, provide helpful instructions
|
|
289
|
+
if (resolver?.inputType === "editor") {
|
|
290
|
+
promptConfig.message += " (JSON format)";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const result = await inquirer.prompt([promptConfig]);
|
|
295
|
+
|
|
296
|
+
// For optional parameters, treat empty input as undefined
|
|
297
|
+
if (
|
|
298
|
+
!paramInfo.isRequired &&
|
|
299
|
+
(!result.value || result.value.trim() === "")
|
|
300
|
+
) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Try to parse JSON if it looks like JSON
|
|
305
|
+
if (
|
|
306
|
+
typeof result.value === "string" &&
|
|
307
|
+
result.value.trim().startsWith("{")
|
|
308
|
+
) {
|
|
309
|
+
try {
|
|
310
|
+
return JSON.parse(result.value);
|
|
311
|
+
} catch {
|
|
312
|
+
console.warn(
|
|
313
|
+
chalk.yellow("Warning: Could not parse as JSON, using as string"),
|
|
314
|
+
);
|
|
315
|
+
return result.value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return result.value;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (this.isUserCancellation(error)) {
|
|
322
|
+
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async resolveDynamicParameter(
|
|
330
|
+
paramInfo: ResolvableParameter,
|
|
331
|
+
resolver: any,
|
|
332
|
+
context: ResolverContext,
|
|
333
|
+
): Promise<any> {
|
|
334
|
+
console.log(chalk.gray(`Fetching options for ${paramInfo.name}...`));
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
// Use the new fetch function approach
|
|
338
|
+
const options = await resolver.fetch(context.sdk, context.resolvedParams);
|
|
339
|
+
|
|
340
|
+
if (options.length === 0) {
|
|
341
|
+
console.log(chalk.yellow(`No options available for ${paramInfo.name}`));
|
|
342
|
+
return await this.promptStaticInput(paramInfo);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Generate prompt configuration
|
|
346
|
+
const promptConfig = resolver.prompt(options, context.resolvedParams);
|
|
347
|
+
|
|
348
|
+
// Execute the prompt
|
|
349
|
+
let result;
|
|
350
|
+
try {
|
|
351
|
+
result = await inquirer.prompt([promptConfig]);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (this.isUserCancellation(error)) {
|
|
354
|
+
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
355
|
+
process.exit(0);
|
|
356
|
+
}
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
let value = result[promptConfig.name || "value"];
|
|
360
|
+
|
|
361
|
+
// Handle JSON parsing if the value looks like JSON
|
|
362
|
+
if (typeof value === "string" && value.trim().startsWith("{")) {
|
|
363
|
+
try {
|
|
364
|
+
value = JSON.parse(value);
|
|
365
|
+
} catch {
|
|
366
|
+
console.warn(
|
|
367
|
+
chalk.yellow("Warning: Could not parse as JSON, using as string"),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return value;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (this.isUserCancellation(error)) {
|
|
375
|
+
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
376
|
+
process.exit(0);
|
|
377
|
+
}
|
|
378
|
+
if (error instanceof Error && error.message.includes("401")) {
|
|
379
|
+
console.log(
|
|
380
|
+
chalk.yellow(
|
|
381
|
+
`⚠️ Invalid auth token, falling back to manual input for ${paramInfo.name}`,
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
console.error(
|
|
386
|
+
chalk.red(`Failed to fetch options for ${paramInfo.name}:`),
|
|
387
|
+
error,
|
|
388
|
+
);
|
|
389
|
+
console.log(chalk.yellow("Falling back to manual input..."));
|
|
390
|
+
}
|
|
391
|
+
return await this.promptStaticInput(paramInfo);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async resolveFieldsParameter(
|
|
396
|
+
paramInfo: ResolvableParameter,
|
|
397
|
+
resolver: any,
|
|
398
|
+
context: ResolverContext,
|
|
399
|
+
): Promise<any> {
|
|
400
|
+
console.log(
|
|
401
|
+
chalk.gray(`Fetching field definitions for ${paramInfo.name}...`),
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// Fetch field definitions using the resolver's fetch function
|
|
406
|
+
const fields = await resolver.fetch(context.sdk, context.resolvedParams);
|
|
407
|
+
|
|
408
|
+
if (fields.length === 0) {
|
|
409
|
+
console.log(chalk.yellow(`No input fields required for this action`));
|
|
410
|
+
return {};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(
|
|
414
|
+
chalk.blue(
|
|
415
|
+
`\nConfiguring inputs for ${context.resolvedParams.app} ${context.resolvedParams.type} ${context.resolvedParams.action}:`,
|
|
416
|
+
),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const inputs: Record<string, any> = {};
|
|
420
|
+
|
|
421
|
+
// Separate required and optional fields
|
|
422
|
+
const requiredFields = fields.filter((field: any) => field.required);
|
|
423
|
+
const optionalFields = fields.filter((field: any) => !field.required);
|
|
424
|
+
|
|
425
|
+
// First, prompt for all required fields
|
|
426
|
+
if (requiredFields.length > 0) {
|
|
427
|
+
console.log(
|
|
428
|
+
chalk.cyan(`\nRequired fields (${requiredFields.length}):`),
|
|
429
|
+
);
|
|
430
|
+
for (const field of requiredFields) {
|
|
431
|
+
await this.promptForField(field, inputs);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Then ask if user wants to configure optional fields
|
|
436
|
+
if (optionalFields.length > 0) {
|
|
437
|
+
console.log(
|
|
438
|
+
chalk.gray(
|
|
439
|
+
`\nThere are ${optionalFields.length} optional field(s) available.`,
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
let shouldConfigureOptional;
|
|
444
|
+
try {
|
|
445
|
+
shouldConfigureOptional = await inquirer.prompt([
|
|
446
|
+
{
|
|
447
|
+
type: "confirm",
|
|
448
|
+
name: "configure",
|
|
449
|
+
message: "Would you like to configure optional fields?",
|
|
450
|
+
default: false,
|
|
451
|
+
},
|
|
452
|
+
]);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (this.isUserCancellation(error)) {
|
|
455
|
+
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
456
|
+
process.exit(0);
|
|
457
|
+
}
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (shouldConfigureOptional.configure) {
|
|
462
|
+
console.log(chalk.cyan(`\nOptional fields:`));
|
|
463
|
+
for (const field of optionalFields) {
|
|
464
|
+
await this.promptForField(field, inputs);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return inputs;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (this.isUserCancellation(error)) {
|
|
472
|
+
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
473
|
+
process.exit(0);
|
|
474
|
+
}
|
|
475
|
+
console.error(
|
|
476
|
+
chalk.red(`Failed to fetch field definitions for ${paramInfo.name}:`),
|
|
477
|
+
error,
|
|
478
|
+
);
|
|
479
|
+
console.log(chalk.yellow("Falling back to manual JSON input..."));
|
|
480
|
+
return await this.promptStaticInput(paramInfo);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private async promptForField(
|
|
485
|
+
field: any,
|
|
486
|
+
inputs: Record<string, any>,
|
|
487
|
+
): Promise<void> {
|
|
488
|
+
const fieldPrompt: any = {
|
|
489
|
+
type: "input",
|
|
490
|
+
name: field.key,
|
|
491
|
+
message: `${field.label || field.key}${field.required ? " (required)" : " (optional)"}:`,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Add description/help text if available
|
|
495
|
+
if (field.help_text || field.description) {
|
|
496
|
+
fieldPrompt.message += `\n ${chalk.dim(field.help_text || field.description)}`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Set default value
|
|
500
|
+
if (field.default !== undefined) {
|
|
501
|
+
fieldPrompt.default = field.default;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Add validation for required fields
|
|
505
|
+
if (field.required) {
|
|
506
|
+
fieldPrompt.validate = (input: any) => {
|
|
507
|
+
if (!input || (typeof input === "string" && input.trim() === "")) {
|
|
508
|
+
return "This field is required. Please enter a value.";
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
let result;
|
|
515
|
+
try {
|
|
516
|
+
result = await inquirer.prompt([fieldPrompt]);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (this.isUserCancellation(error)) {
|
|
519
|
+
console.log(chalk.yellow("\n\nOperation cancelled by user"));
|
|
520
|
+
process.exit(0);
|
|
521
|
+
}
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Only include non-empty values
|
|
526
|
+
if (result[field.key] !== undefined && result[field.key] !== "") {
|
|
527
|
+
let value = result[field.key];
|
|
528
|
+
|
|
529
|
+
// Try to parse as appropriate type
|
|
530
|
+
if (field.type === "integer" && typeof value === "string") {
|
|
531
|
+
const parsed = parseInt(value, 10);
|
|
532
|
+
if (!isNaN(parsed)) value = parsed;
|
|
533
|
+
} else if (field.type === "number" && typeof value === "string") {
|
|
534
|
+
const parsed = parseFloat(value);
|
|
535
|
+
if (!isNaN(parsed)) value = parsed;
|
|
536
|
+
} else if (field.type === "boolean" && typeof value === "string") {
|
|
537
|
+
value = value.toLowerCase() === "true" || value === "1";
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
inputs[field.key] = value;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private getNestedValue(obj: any, path: string[]): any {
|
|
545
|
+
return path.reduce((current, key) => current?.[key], obj);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private setNestedValue(obj: any, path: string[], value: any): void {
|
|
549
|
+
const lastKey = path[path.length - 1];
|
|
550
|
+
const parentPath = path.slice(0, -1);
|
|
551
|
+
|
|
552
|
+
const parent = parentPath.reduce((current, key) => {
|
|
553
|
+
if (!current[key]) {
|
|
554
|
+
current[key] = {};
|
|
555
|
+
}
|
|
556
|
+
return current[key];
|
|
557
|
+
}, obj);
|
|
558
|
+
|
|
559
|
+
parent[lastKey] = value;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private isUserCancellation(error: any): boolean {
|
|
563
|
+
// Check for various ways user cancellation can be detected
|
|
564
|
+
return (
|
|
565
|
+
error &&
|
|
566
|
+
(error.name === "ExitPromptError" ||
|
|
567
|
+
error.message?.includes("User force closed the prompt") ||
|
|
568
|
+
error.message?.includes("SIGINT") ||
|
|
569
|
+
error.code === "SIGINT" ||
|
|
570
|
+
error.signal === "SIGINT")
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
getFormatMetadata,
|
|
5
|
+
getOutputSchema,
|
|
6
|
+
type FormatMetadata,
|
|
7
|
+
} from "../../../actions-sdk/dist/output-schemas";
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Generic Schema-Driven Formatter
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export function formatItemsFromSchema(
|
|
14
|
+
inputSchema: z.ZodType,
|
|
15
|
+
items: any[],
|
|
16
|
+
): void {
|
|
17
|
+
// Get the output schema and its format metadata
|
|
18
|
+
const outputSchema = getOutputSchema(inputSchema);
|
|
19
|
+
if (!outputSchema) {
|
|
20
|
+
// Fallback to generic formatting if no output schema
|
|
21
|
+
formatItemsGeneric(items);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const formatMeta = getFormatMetadata(outputSchema);
|
|
26
|
+
if (!formatMeta) {
|
|
27
|
+
// Fallback to generic formatting if no format metadata
|
|
28
|
+
formatItemsGeneric(items);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Format each item using the schema metadata
|
|
33
|
+
items.forEach((item, index) => {
|
|
34
|
+
formatSingleItem(item, index, formatMeta);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatSingleItem(
|
|
39
|
+
item: any,
|
|
40
|
+
index: number,
|
|
41
|
+
formatMeta: FormatMetadata,
|
|
42
|
+
): void {
|
|
43
|
+
// Get the formatted item from the format function
|
|
44
|
+
const formatted = formatMeta.format(item);
|
|
45
|
+
|
|
46
|
+
// Build the main title line
|
|
47
|
+
let titleLine = `${chalk.gray(`${index + 1}.`)} ${chalk.cyan(formatted.title)}`;
|
|
48
|
+
if (formatted.subtitle) {
|
|
49
|
+
titleLine += ` ${chalk.gray(formatted.subtitle)}`;
|
|
50
|
+
}
|
|
51
|
+
console.log(titleLine);
|
|
52
|
+
|
|
53
|
+
// Format detail lines
|
|
54
|
+
for (const detail of formatted.details) {
|
|
55
|
+
const styledText = applyStyle(detail.text, detail.style);
|
|
56
|
+
console.log(` ${styledText}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(); // Empty line between items
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applyStyle(value: string, style: string): string {
|
|
63
|
+
switch (style) {
|
|
64
|
+
case "dim":
|
|
65
|
+
return chalk.dim(value);
|
|
66
|
+
case "accent":
|
|
67
|
+
return chalk.magenta(value);
|
|
68
|
+
case "warning":
|
|
69
|
+
return chalk.red(value);
|
|
70
|
+
case "success":
|
|
71
|
+
return chalk.green(value);
|
|
72
|
+
case "normal":
|
|
73
|
+
default:
|
|
74
|
+
return chalk.blue(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatItemsGeneric(items: any[]): void {
|
|
79
|
+
// Fallback formatting for items without schema metadata
|
|
80
|
+
items.forEach((item, index) => {
|
|
81
|
+
const name = item.name || item.key || item.id || "Item";
|
|
82
|
+
console.log(`${chalk.gray(`${index + 1}.`)} ${chalk.cyan(name)}`);
|
|
83
|
+
if (item.description) {
|
|
84
|
+
console.log(` ${chalk.dim(item.description)}`);
|
|
85
|
+
}
|
|
86
|
+
console.log();
|
|
87
|
+
});
|
|
88
|
+
}
|
package/test/cli.test.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
describe("Zapier CLI", () => {
|
|
6
|
+
const cliPath = path.join(__dirname, "../bin/zapier.js");
|
|
7
|
+
|
|
8
|
+
it("should show help when called with --help", () => {
|
|
9
|
+
const output = execSync(`node ${cliPath} --help`, { encoding: "utf8" });
|
|
10
|
+
|
|
11
|
+
expect(output).toContain("CLI for Zapier SDK");
|
|
12
|
+
expect(output).toContain("Commands:");
|
|
13
|
+
expect(output).toContain("browse");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should show browse help when called with browse --help", () => {
|
|
17
|
+
const output = execSync(`node ${cliPath} browse --help`, {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(output).toContain("Browse available apps and actions");
|
|
22
|
+
expect(output).toContain("apps");
|
|
23
|
+
expect(output).toContain("actions");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should show browse apps help when called with browse apps --help", () => {
|
|
27
|
+
const output = execSync(`node ${cliPath} browse apps --help`, {
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(output).toContain("List all available apps");
|
|
32
|
+
expect(output).toContain("--category");
|
|
33
|
+
expect(output).toContain("--limit");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should show browse actions help when called with browse actions --help", () => {
|
|
37
|
+
const output = execSync(`node ${cliPath} browse actions --help`, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(output).toContain("List available actions");
|
|
42
|
+
expect(output).toContain("--app");
|
|
43
|
+
expect(output).toContain("--type");
|
|
44
|
+
expect(output).toContain("--limit");
|
|
45
|
+
});
|
|
46
|
+
});
|