@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.
@@ -0,0 +1,578 @@
1
+ import { Command } from "commander";
2
+ import { z } from "zod";
3
+ import { ActionsSDK } from "@zapier/actions-sdk";
4
+ import { SDKSchemas } from "../../../actions-sdk/dist/schemas";
5
+ import { SchemaParameterResolver } from "./parameter-resolver";
6
+ import { createPager } from "./pager";
7
+ import { formatItemsFromSchema } from "./schema-formatter";
8
+ import chalk from "chalk";
9
+ import util from "util";
10
+
11
+ // ============================================================================
12
+ // JSON Formatting
13
+ // ============================================================================
14
+
15
+ function formatJSONOutput(data: any): void {
16
+ // Show success message for action results
17
+ if (
18
+ data &&
19
+ typeof data === "object" &&
20
+ !Array.isArray(data) &&
21
+ (data.success !== undefined || data.id || data.status)
22
+ ) {
23
+ console.log(chalk.green("āœ… Action completed successfully!\n"));
24
+ }
25
+
26
+ // Use util.inspect for colored output
27
+ console.log(
28
+ util.inspect(data, { colors: true, depth: null, breakLength: 80 }),
29
+ );
30
+ }
31
+
32
+ // ============================================================================
33
+ // Types
34
+ // ============================================================================
35
+
36
+ interface CLICommandConfig {
37
+ description: string;
38
+ parameters: CLIParameter[];
39
+ handler: (...args: any[]) => Promise<void>;
40
+ }
41
+
42
+ interface CLIParameter {
43
+ name: string;
44
+ type: "string" | "number" | "boolean" | "array";
45
+ required: boolean;
46
+ description?: string;
47
+ default?: any;
48
+ choices?: string[];
49
+ resolverMeta?: any;
50
+ }
51
+
52
+ // ============================================================================
53
+ // Schema Analysis
54
+ // ============================================================================
55
+
56
+ function analyzeZodSchema(schema: z.ZodSchema): CLIParameter[] {
57
+ const parameters: CLIParameter[] = [];
58
+
59
+ if (schema instanceof z.ZodObject) {
60
+ const shape = schema.shape;
61
+
62
+ for (const [key, fieldSchema] of Object.entries(shape)) {
63
+ const param = analyzeZodField(key, fieldSchema as z.ZodSchema);
64
+ if (param) {
65
+ parameters.push(param);
66
+ }
67
+ }
68
+ }
69
+
70
+ return parameters;
71
+ }
72
+
73
+ function analyzeZodField(
74
+ name: string,
75
+ schema: z.ZodSchema,
76
+ ): CLIParameter | null {
77
+ let baseSchema = schema;
78
+ let required = true;
79
+ let defaultValue: any = undefined;
80
+
81
+ // Unwrap optional and default wrappers
82
+ if (baseSchema instanceof z.ZodOptional) {
83
+ required = false;
84
+ baseSchema = baseSchema._def.innerType;
85
+ }
86
+
87
+ if (baseSchema instanceof z.ZodDefault) {
88
+ required = false;
89
+ defaultValue = baseSchema._def.defaultValue();
90
+ baseSchema = baseSchema._def.innerType;
91
+ }
92
+
93
+ // Determine parameter type
94
+ let paramType: CLIParameter["type"] = "string";
95
+ let choices: string[] | undefined;
96
+
97
+ if (baseSchema instanceof z.ZodString) {
98
+ paramType = "string";
99
+ } else if (baseSchema instanceof z.ZodNumber) {
100
+ paramType = "number";
101
+ } else if (baseSchema instanceof z.ZodBoolean) {
102
+ paramType = "boolean";
103
+ } else if (baseSchema instanceof z.ZodArray) {
104
+ paramType = "array";
105
+ } else if (baseSchema instanceof z.ZodEnum) {
106
+ paramType = "string";
107
+ choices = baseSchema._def.values;
108
+ } else if (baseSchema instanceof z.ZodRecord) {
109
+ // Handle Record<string, any> as JSON string input
110
+ paramType = "string";
111
+ }
112
+
113
+ // Extract resolver metadata
114
+ const resolverMeta = (schema._def as any).resolverMeta;
115
+
116
+ return {
117
+ name,
118
+ type: paramType,
119
+ required,
120
+ description: schema.description,
121
+ default: defaultValue,
122
+ choices,
123
+ resolverMeta,
124
+ };
125
+ }
126
+
127
+ // ============================================================================
128
+ // CLI Command Generation
129
+ // ============================================================================
130
+
131
+ export function generateCLICommands(program: Command, sdk: ActionsSDK): void {
132
+ // Check if SDKSchemas is available
133
+ if (!SDKSchemas) {
134
+ console.error("SDKSchemas not available");
135
+ return;
136
+ }
137
+
138
+ // Generate namespace commands (apps, actions, auths, fields)
139
+ Object.entries(SDKSchemas).forEach(([namespace, methods]) => {
140
+ if (namespace === "generate" || namespace === "bundle") {
141
+ // Handle root tools separately
142
+ return;
143
+ }
144
+
145
+ const namespaceCommand = program
146
+ .command(namespace)
147
+ .description(`${namespace} management commands`);
148
+
149
+ if (typeof methods === "object" && methods !== null) {
150
+ Object.entries(methods).forEach(([method, schema]) => {
151
+ const config = createCommandConfig(
152
+ namespace,
153
+ method,
154
+ schema as z.ZodSchema,
155
+ sdk,
156
+ );
157
+ addSubCommand(namespaceCommand, method, config);
158
+ });
159
+ }
160
+ });
161
+
162
+ // Generate root tool commands
163
+ if (SDKSchemas.generate) {
164
+ const generateConfig = createCommandConfig(
165
+ "",
166
+ "generate",
167
+ SDKSchemas.generate,
168
+ sdk,
169
+ );
170
+ addSubCommand(program, "generate", generateConfig);
171
+ }
172
+
173
+ if (SDKSchemas.bundle) {
174
+ const bundleConfig = createCommandConfig(
175
+ "",
176
+ "bundle",
177
+ SDKSchemas.bundle,
178
+ sdk,
179
+ );
180
+ addSubCommand(program, "bundle", bundleConfig);
181
+ }
182
+ }
183
+
184
+ function createCommandConfig(
185
+ namespace: string,
186
+ method: string,
187
+ schema: z.ZodSchema,
188
+ sdk: ActionsSDK,
189
+ ): CLICommandConfig {
190
+ const parameters = analyzeZodSchema(schema);
191
+ const description = schema.description || `${namespace} ${method} command`;
192
+
193
+ const handler = async (...args: any[]) => {
194
+ try {
195
+ // The last argument is always the command object with parsed options
196
+ const command = args[args.length - 1];
197
+ const options = command.opts();
198
+
199
+ // Check if this is a list command with pagination support
200
+ const isListCommand = method === "list";
201
+ const hasPaginationParams = parameters.some(
202
+ (p) => p.name === "limit" || p.name === "offset",
203
+ );
204
+ const hasUserSpecifiedLimit =
205
+ "limit" in options && options.limit !== undefined;
206
+ const shouldUsePaging =
207
+ isListCommand && hasPaginationParams && !hasUserSpecifiedLimit;
208
+ const shouldUseJSON = options.json;
209
+
210
+ // Convert CLI args to SDK method parameters
211
+ const rawParams = convertCLIArgsToSDKParams(
212
+ parameters,
213
+ args.slice(0, -1),
214
+ options,
215
+ );
216
+
217
+ // NEW: Resolve missing parameters interactively using schema metadata
218
+ const resolver = new SchemaParameterResolver();
219
+ const resolvedParams = await resolver.resolveParameters(
220
+ schema,
221
+ rawParams,
222
+ sdk,
223
+ );
224
+
225
+ if (shouldUsePaging && !shouldUseJSON) {
226
+ // Use interactive paging for list commands
227
+ await handlePaginatedList(namespace, method, resolvedParams, sdk);
228
+ } else {
229
+ // Call the appropriate SDK method with complete, validated parameters
230
+ let result: any;
231
+ if (namespace === "") {
232
+ // Root tool (generate, bundle)
233
+ result = await (sdk as any)[method](resolvedParams);
234
+ } else {
235
+ // Regular namespace method
236
+ result = await (sdk as any)[namespace][method](resolvedParams);
237
+ }
238
+
239
+ // Special handling for generate and bundle commands - don't output to console if writing to file
240
+ const isRootCommandWithOutput =
241
+ namespace === "" && (method === "generate" || method === "bundle");
242
+ const hasOutputFile = isRootCommandWithOutput && resolvedParams.output;
243
+
244
+ // Output result (JSON or formatted)
245
+ if (!hasOutputFile && (shouldUseJSON || !isListCommand)) {
246
+ // Use raw JSON if --json flag is specified, otherwise use pretty formatting
247
+ if (shouldUseJSON) {
248
+ console.log(JSON.stringify(result, null, 2));
249
+ } else {
250
+ formatJSONOutput(result);
251
+ }
252
+ } else if (!hasOutputFile) {
253
+ // Format list results nicely (non-paginated)
254
+ formatNonPaginatedResults(
255
+ namespace,
256
+ result,
257
+ resolvedParams.limit,
258
+ hasUserSpecifiedLimit,
259
+ shouldUseJSON,
260
+ );
261
+ } else if (hasOutputFile) {
262
+ // Show success message for file output instead of printing generated content
263
+ console.log(chalk.green(`āœ… ${method} completed successfully!`));
264
+ console.log(
265
+ chalk.gray(`Output written to: ${resolvedParams.output}`),
266
+ );
267
+ }
268
+ }
269
+ } catch (error) {
270
+ console.error(
271
+ "Error:",
272
+ error instanceof Error ? error.message : "Unknown error",
273
+ );
274
+ process.exit(1);
275
+ }
276
+ };
277
+
278
+ return {
279
+ description,
280
+ parameters,
281
+ handler,
282
+ };
283
+ }
284
+
285
+ function addSubCommand(
286
+ parentCommand: Command,
287
+ name: string,
288
+ config: CLICommandConfig,
289
+ ): void {
290
+ const command = parentCommand.command(name).description(config.description);
291
+
292
+ // Add parameters to command
293
+ config.parameters.forEach((param) => {
294
+ if (param.resolverMeta?.resolver && param.required) {
295
+ // Required parameters with resolvers become optional positional arguments (resolver handles prompting)
296
+ command.argument(
297
+ `[${param.name}]`,
298
+ param.description || `${param.name} parameter`,
299
+ );
300
+ } else if (param.required) {
301
+ // Required parameters without resolvers become required positional arguments
302
+ command.argument(
303
+ `<${param.name}>`,
304
+ param.description || `${param.name} parameter`,
305
+ );
306
+ } else {
307
+ // Optional parameters become flags (whether they have resolvers or not)
308
+ const flags = [
309
+ `--${param.name.replace(/([A-Z])/g, "-$1").toLowerCase()}`,
310
+ ];
311
+
312
+ if (param.type === "boolean") {
313
+ command.option(flags.join(", "), param.description);
314
+ } else {
315
+ const flagSignature = flags.join(", ") + ` <${param.type}>`;
316
+ command.option(flagSignature, param.description, param.default);
317
+ }
318
+ }
319
+ });
320
+
321
+ // Add formatting options for all commands
322
+ command.option("--json", "Output raw JSON instead of formatted results");
323
+
324
+ command.action(config.handler);
325
+ }
326
+
327
+ // ============================================================================
328
+ // Parameter Conversion
329
+ // ============================================================================
330
+
331
+ function convertCLIArgsToSDKParams(
332
+ parameters: CLIParameter[],
333
+ positionalArgs: any[],
334
+ options: Record<string, any>,
335
+ ): Record<string, any> {
336
+ const sdkParams: Record<string, any> = {};
337
+
338
+ // Handle positional arguments (required parameters only, whether they have resolvers or not)
339
+ let argIndex = 0;
340
+ parameters.forEach((param) => {
341
+ if (param.required && argIndex < positionalArgs.length) {
342
+ sdkParams[param.name] = convertValue(
343
+ positionalArgs[argIndex],
344
+ param.type,
345
+ );
346
+ argIndex++;
347
+ }
348
+ });
349
+
350
+ // Handle option flags
351
+ Object.entries(options).forEach(([key, value]) => {
352
+ // Convert kebab-case back to camelCase
353
+ const camelKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
354
+ const param = parameters.find((p) => p.name === camelKey);
355
+
356
+ if (param && value !== undefined) {
357
+ sdkParams[camelKey] = convertValue(value, param.type);
358
+ }
359
+ });
360
+
361
+ return sdkParams;
362
+ }
363
+
364
+ function convertValue(value: any, type: CLIParameter["type"]): any {
365
+ switch (type) {
366
+ case "number":
367
+ return Number(value);
368
+ case "boolean":
369
+ return Boolean(value);
370
+ case "array":
371
+ return Array.isArray(value) ? value : [value];
372
+ case "string":
373
+ default:
374
+ // Handle JSON string for objects
375
+ if (
376
+ typeof value === "string" &&
377
+ (value.startsWith("{") || value.startsWith("["))
378
+ ) {
379
+ try {
380
+ return JSON.parse(value);
381
+ } catch {
382
+ return value;
383
+ }
384
+ }
385
+ return value;
386
+ }
387
+ }
388
+
389
+ // ============================================================================
390
+ // Pagination Handlers
391
+ // ============================================================================
392
+
393
+ async function handlePaginatedList(
394
+ namespace: string,
395
+ method: string,
396
+ baseParams: any,
397
+ sdk: ActionsSDK,
398
+ ): Promise<void> {
399
+ const limit = baseParams.limit || 20;
400
+ const itemName = getItemName(namespace);
401
+
402
+ console.log(chalk.blue(`šŸ“‹ Fetching ${itemName}...`));
403
+
404
+ const pager = createPager({
405
+ pageSize: Math.min(limit, 20),
406
+ itemName,
407
+ });
408
+
409
+ const displayFunction = (
410
+ items: any[],
411
+ totalShown: number,
412
+ totalAvailable?: number,
413
+ ) => {
414
+ // Only clear screen if we have items to show
415
+ if (items.length > 0) {
416
+ console.clear();
417
+ }
418
+ console.log(chalk.blue(`šŸ“‹ ${getListTitle(namespace)}\n`));
419
+
420
+ if (items.length === 0) {
421
+ console.log(chalk.yellow(`No ${itemName} found.`));
422
+ return;
423
+ }
424
+
425
+ // Get the schema for this namespace/method to extract formatting info
426
+ const schema = SDKSchemas[namespace as keyof typeof SDKSchemas];
427
+ const listSchema =
428
+ schema && typeof schema === "object" && "list" in schema
429
+ ? schema.list
430
+ : null;
431
+
432
+ if (listSchema) {
433
+ formatItemsFromSchema(listSchema as z.ZodType, items);
434
+ } else {
435
+ // Fallback to generic formatting
436
+ formatItemsGeneric(items);
437
+ }
438
+
439
+ const totalInfo = totalAvailable
440
+ ? ` of ${totalAvailable.toLocaleString()} total`
441
+ : "";
442
+ console.log(
443
+ chalk.green(`\nāœ… Showing ${totalShown}${totalInfo} ${itemName}`),
444
+ );
445
+ };
446
+
447
+ await pager.paginate(
448
+ (params) =>
449
+ (sdk as any)[namespace][method]({
450
+ ...baseParams,
451
+ ...params,
452
+ }),
453
+ {},
454
+ displayFunction,
455
+ );
456
+ }
457
+
458
+ function formatNonPaginatedResults(
459
+ namespace: string,
460
+ result: any[],
461
+ requestedLimit?: number,
462
+ userSpecifiedLimit?: boolean,
463
+ useRawJSON?: boolean,
464
+ ): void {
465
+ if (!Array.isArray(result)) {
466
+ if (useRawJSON) {
467
+ console.log(JSON.stringify(result, null, 2));
468
+ } else {
469
+ formatJSONOutput(result);
470
+ }
471
+ return;
472
+ }
473
+
474
+ if (useRawJSON) {
475
+ console.log(JSON.stringify(result, null, 2));
476
+ return;
477
+ }
478
+
479
+ const itemName = getItemName(namespace);
480
+
481
+ if (result.length === 0) {
482
+ console.log(chalk.yellow(`No ${itemName} found.`));
483
+ return;
484
+ }
485
+
486
+ console.log(chalk.green(`\nāœ… Found ${result.length} ${itemName}:\n`));
487
+
488
+ // Get the schema for this namespace/method to extract formatting info
489
+ const schema = SDKSchemas[namespace as keyof typeof SDKSchemas];
490
+ const listSchema =
491
+ schema && typeof schema === "object" && "list" in schema
492
+ ? schema.list
493
+ : null;
494
+
495
+ if (listSchema) {
496
+ formatItemsFromSchema(listSchema as z.ZodType, result);
497
+ } else {
498
+ // Fallback to generic formatting
499
+ formatItemsGeneric(result);
500
+ }
501
+
502
+ // Show appropriate status message
503
+ if (userSpecifiedLimit && requestedLimit) {
504
+ console.log(
505
+ chalk.gray(
506
+ `\nšŸ“„ Showing up to ${requestedLimit} ${itemName} (--limit ${requestedLimit})`,
507
+ ),
508
+ );
509
+ } else {
510
+ console.log(chalk.gray(`\nšŸ“„ All available ${itemName} shown`));
511
+ }
512
+ }
513
+
514
+ function formatItemsGeneric(items: any[]): void {
515
+ // Fallback formatting for items without schema metadata
516
+ items.forEach((item, index) => {
517
+ const name = item.name || item.key || item.id || "Item";
518
+ console.log(`${chalk.gray(`${index + 1}.`)} ${chalk.cyan(name)}`);
519
+ if (item.description) {
520
+ console.log(` ${chalk.dim(item.description)}`);
521
+ }
522
+ console.log();
523
+ });
524
+ }
525
+
526
+ function getItemName(namespace: string): string {
527
+ switch (namespace) {
528
+ case "apps":
529
+ return "apps";
530
+ case "actions":
531
+ return "actions";
532
+ case "auths":
533
+ return "authentications";
534
+ case "fields":
535
+ return "fields";
536
+ default:
537
+ return "items";
538
+ }
539
+ }
540
+
541
+ function getListTitle(namespace: string): string {
542
+ switch (namespace) {
543
+ case "apps":
544
+ return "Available Apps";
545
+ case "actions":
546
+ return "Available Actions";
547
+ case "auths":
548
+ return "Available Authentications";
549
+ case "fields":
550
+ return "Available Fields";
551
+ default:
552
+ return "Available Items";
553
+ }
554
+ }
555
+
556
+ // ============================================================================
557
+ // Help Text Enhancement
558
+ // ============================================================================
559
+
560
+ export function enhanceCommandHelp(program: Command): void {
561
+ // Add custom help that shows schema-driven nature
562
+ program.on("--help", () => {
563
+ console.log("");
564
+ console.log("Commands are automatically generated from SDK schemas.");
565
+ console.log(
566
+ "Each command maps directly to an SDK method with the same parameters.",
567
+ );
568
+ console.log("");
569
+ console.log("Examples:");
570
+ console.log(" zapier-sdk apps list --category=productivity --limit=10");
571
+ console.log(
572
+ ' zapier-sdk actions run slack search user_by_email --inputs=\'{"email":"user@example.com"}\'',
573
+ );
574
+ console.log(" zapier-sdk generate my-app --output=./generated/");
575
+ console.log(" zsdk apps list --limit=5 # Using the shorter alias");
576
+ console.log("");
577
+ });
578
+ }