@zapier/zapier-sdk-cli 0.13.5 → 0.13.6

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +39 -0
  3. package/dist/cli.cjs +354 -71
  4. package/dist/cli.mjs +355 -72
  5. package/dist/index.cjs +353 -70
  6. package/dist/index.d.mts +154 -2
  7. package/dist/index.d.ts +154 -2
  8. package/dist/index.mjs +354 -71
  9. package/dist/package.json +1 -1
  10. package/dist/src/plugins/add/index.d.ts +4 -2
  11. package/dist/src/plugins/add/index.js +89 -98
  12. package/dist/src/plugins/buildManifest/index.d.ts +13 -0
  13. package/dist/src/plugins/buildManifest/index.js +81 -0
  14. package/dist/src/plugins/buildManifest/schemas.d.ts +57 -0
  15. package/dist/src/plugins/buildManifest/schemas.js +17 -0
  16. package/dist/src/plugins/generateAppTypes/index.d.ts +13 -0
  17. package/dist/src/plugins/generateAppTypes/index.js +169 -0
  18. package/dist/src/plugins/generateAppTypes/schemas.d.ts +72 -0
  19. package/dist/src/plugins/generateAppTypes/schemas.js +21 -0
  20. package/dist/src/plugins/index.d.ts +2 -0
  21. package/dist/src/plugins/index.js +2 -0
  22. package/dist/src/sdk.d.ts +2 -2
  23. package/dist/src/sdk.js +16 -14
  24. package/dist/src/types/sdk.d.ts +5 -0
  25. package/dist/src/types/sdk.js +1 -0
  26. package/dist/src/utils/directory-detection.d.ts +5 -0
  27. package/dist/src/utils/directory-detection.js +21 -0
  28. package/dist/src/utils/manifest-helpers.d.ts +13 -0
  29. package/dist/src/utils/manifest-helpers.js +19 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +3 -3
  32. package/src/plugins/add/index.ts +123 -125
  33. package/src/plugins/buildManifest/index.test.ts +612 -0
  34. package/src/plugins/buildManifest/index.ts +128 -0
  35. package/src/plugins/buildManifest/schemas.ts +61 -0
  36. package/src/plugins/generateAppTypes/index.ts +235 -0
  37. package/src/plugins/generateAppTypes/schemas.ts +65 -0
  38. package/src/plugins/index.ts +2 -0
  39. package/src/sdk.ts +23 -20
  40. package/src/types/sdk.ts +8 -0
  41. package/src/utils/directory-detection.ts +23 -0
  42. package/src/utils/manifest-helpers.ts +28 -0
  43. /package/dist/src/{plugins/add → generators}/ast-generator.d.ts +0 -0
  44. /package/dist/src/{plugins/add → generators}/ast-generator.js +0 -0
  45. /package/src/{plugins/add → generators}/ast-generator.ts +0 -0
@@ -0,0 +1,612 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { buildManifestPlugin } from "./index";
3
+ import { BuildManifestSchema } from "./schemas";
4
+ import type {
5
+ AppItem,
6
+ Manifest,
7
+ ManifestEntry,
8
+ UpdateManifestEntryOptions,
9
+ } from "@zapier/zapier-sdk";
10
+ import { createSdk } from "@zapier/zapier-sdk";
11
+
12
+ const mockSlackApp: AppItem = {
13
+ key: "SlackCLIAPI",
14
+ slug: "slack",
15
+ title: "Slack",
16
+ description: "Slack is a collaboration hub",
17
+ version: "1.30.0",
18
+ implementation_id: "SlackCLIAPI@1.30.0",
19
+ };
20
+
21
+ const mockGmailApp: AppItem = {
22
+ key: "GmailCLIAPI",
23
+ slug: "gmail",
24
+ title: "Gmail",
25
+ description: "Gmail is an email service",
26
+ version: "2.5.0",
27
+ implementation_id: "GmailCLIAPI@2.5.0",
28
+ };
29
+
30
+ const mockAppWithoutVersion: AppItem = {
31
+ key: "BrokenAPI",
32
+ slug: "broken",
33
+ title: "Broken App",
34
+ description: "An app without version",
35
+ version: undefined,
36
+ implementation_id: "BrokenAPI",
37
+ };
38
+
39
+ describe("buildManifest plugin", () => {
40
+ let mockUpdateManifestEntry: unknown;
41
+ let mockListAppsItems: unknown;
42
+
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+
46
+ mockUpdateManifestEntry = vi
47
+ .fn()
48
+ .mockImplementation(
49
+ async (
50
+ options: UpdateManifestEntryOptions,
51
+ ): Promise<[string, ManifestEntry, Manifest]> => {
52
+ const manifestKey = options.entry.implementationName;
53
+ const updatedManifest: Manifest = {
54
+ apps: {
55
+ ...options.manifest?.apps,
56
+ [manifestKey]: options.entry,
57
+ },
58
+ };
59
+ return [manifestKey, options.entry, updatedManifest];
60
+ },
61
+ );
62
+
63
+ mockListAppsItems = vi.fn();
64
+ });
65
+
66
+ function createTestSdk(apps: AppItem[] = [mockSlackApp, mockGmailApp]) {
67
+ // Mock async iterator for listApps
68
+ mockListAppsItems.mockReturnValue({
69
+ async *[Symbol.asyncIterator]() {
70
+ for (const app of apps) {
71
+ yield app;
72
+ }
73
+ },
74
+ });
75
+
76
+ return createSdk()
77
+ .addPlugin(() => ({
78
+ context: {
79
+ updateManifestEntry: mockUpdateManifestEntry,
80
+ },
81
+ }))
82
+ .addPlugin(() => ({
83
+ listApps: vi.fn().mockReturnValue({
84
+ items: mockListAppsItems,
85
+ }),
86
+ }))
87
+ .addPlugin(buildManifestPlugin);
88
+ }
89
+
90
+ describe("schema validation", () => {
91
+ it("should accept valid options with skipWrite true", () => {
92
+ const result = BuildManifestSchema.safeParse({
93
+ appKeys: ["slack", "gmail"],
94
+ skipWrite: true,
95
+ });
96
+
97
+ expect(result.success).toBe(true);
98
+ if (result.success) {
99
+ expect(result.data.skipWrite).toBe(true);
100
+ }
101
+ });
102
+
103
+ it("should accept valid options with skipWrite false and configPath", () => {
104
+ const result = BuildManifestSchema.safeParse({
105
+ appKeys: ["slack"],
106
+ skipWrite: false,
107
+ configPath: "/path/to/.zapierrc",
108
+ });
109
+
110
+ expect(result.success).toBe(true);
111
+ if (result.success) {
112
+ expect(result.data.skipWrite).toBe(false);
113
+ expect(result.data.configPath).toBe("/path/to/.zapierrc");
114
+ }
115
+ });
116
+
117
+ it("should accept options without skipWrite (defaults to false)", () => {
118
+ const result = BuildManifestSchema.safeParse({
119
+ appKeys: ["slack"],
120
+ });
121
+
122
+ expect(result.success).toBe(true);
123
+ if (result.success) {
124
+ expect(result.data.skipWrite).toBeUndefined();
125
+ }
126
+ });
127
+
128
+ it("should require at least one appKey", () => {
129
+ const result = BuildManifestSchema.safeParse({
130
+ appKeys: [],
131
+ });
132
+
133
+ expect(result.success).toBe(false);
134
+ });
135
+
136
+ it("should reject empty string appKeys", () => {
137
+ const result = BuildManifestSchema.safeParse({
138
+ appKeys: ["slack", ""],
139
+ });
140
+
141
+ expect(result.success).toBe(false);
142
+ });
143
+
144
+ it("should reject non-array appKeys", () => {
145
+ const result = BuildManifestSchema.safeParse({
146
+ appKeys: "slack",
147
+ });
148
+
149
+ expect(result.success).toBe(false);
150
+ });
151
+ });
152
+
153
+ describe("manifest building with skipWrite: true", () => {
154
+ it("should build manifest entries without writing to disk", async () => {
155
+ const sdk = createTestSdk();
156
+
157
+ const result = await sdk.buildManifest({
158
+ appKeys: ["slack", "gmail"],
159
+ skipWrite: true,
160
+ });
161
+
162
+ // Should still call updateManifestEntry with skipWrite: true
163
+ expect(mockUpdateManifestEntry).toHaveBeenCalledTimes(2);
164
+ expect(mockUpdateManifestEntry).toHaveBeenCalledWith(
165
+ expect.objectContaining({
166
+ skipWrite: true,
167
+ }),
168
+ );
169
+
170
+ // Should return manifest with entries
171
+ expect(result.manifest).toBeDefined();
172
+ expect(result.manifest?.apps).toBeDefined();
173
+ expect(result.errors).toEqual([]);
174
+ });
175
+
176
+ it("should process apps and create manifest entries", async () => {
177
+ const sdk = createTestSdk();
178
+
179
+ const result = await sdk.buildManifest({
180
+ appKeys: ["slack"],
181
+ skipWrite: true,
182
+ });
183
+
184
+ expect(result.manifest?.apps).toBeDefined();
185
+ expect(result.errors).toEqual([]);
186
+ });
187
+
188
+ it("should return empty result when no apps found", async () => {
189
+ const sdk = createTestSdk([]);
190
+
191
+ const result = await sdk.buildManifest({
192
+ appKeys: ["nonexistent"],
193
+ skipWrite: true,
194
+ });
195
+
196
+ expect(result.manifest).toBeUndefined();
197
+ expect(result.errors).toEqual([]);
198
+ });
199
+ });
200
+
201
+ describe("manifest building with skipWrite: false", () => {
202
+ it("should build manifest and write to disk", async () => {
203
+ const sdk = createTestSdk();
204
+
205
+ const result = await sdk.buildManifest({
206
+ appKeys: ["slack", "gmail"],
207
+ skipWrite: false,
208
+ configPath: "/path/to/.zapierrc",
209
+ });
210
+
211
+ // Should call updateManifestEntry for each app
212
+ expect(mockUpdateManifestEntry).toHaveBeenCalledTimes(2);
213
+
214
+ // First call for slack
215
+ expect(mockUpdateManifestEntry).toHaveBeenNthCalledWith(1, {
216
+ appKey: "SlackCLIAPI",
217
+ entry: {
218
+ implementationName: "SlackCLIAPI",
219
+ version: "1.30.0",
220
+ },
221
+ configPath: "/path/to/.zapierrc",
222
+ skipWrite: false,
223
+ manifest: undefined,
224
+ });
225
+
226
+ // Second call for gmail (with manifest from first call)
227
+ expect(mockUpdateManifestEntry).toHaveBeenNthCalledWith(2, {
228
+ appKey: "GmailCLIAPI",
229
+ entry: {
230
+ implementationName: "GmailCLIAPI",
231
+ version: "2.5.0",
232
+ },
233
+ configPath: "/path/to/.zapierrc",
234
+ skipWrite: false,
235
+ manifest: {
236
+ apps: {
237
+ SlackCLIAPI: {
238
+ implementationName: "SlackCLIAPI",
239
+ version: "1.30.0",
240
+ },
241
+ },
242
+ },
243
+ });
244
+
245
+ // Should return updated manifest
246
+ expect(result.manifest).toBeDefined();
247
+ expect(result.manifest?.apps?.SlackCLIAPI).toEqual({
248
+ implementationName: "SlackCLIAPI",
249
+ version: "1.30.0",
250
+ });
251
+ expect(result.manifest?.apps?.GmailCLIAPI).toEqual({
252
+ implementationName: "GmailCLIAPI",
253
+ version: "2.5.0",
254
+ });
255
+ expect(result.errors).toEqual([]);
256
+ });
257
+
258
+ it("should accumulate manifest entries across multiple apps", async () => {
259
+ const sdk = createTestSdk();
260
+
261
+ const result = await sdk.buildManifest({
262
+ appKeys: ["slack", "gmail"],
263
+ skipWrite: false,
264
+ });
265
+
266
+ // Verify that the second call received the manifest from the first call
267
+ const secondCallArgs = mockUpdateManifestEntry.mock.calls[1][0];
268
+ expect(secondCallArgs.manifest).toEqual({
269
+ apps: {
270
+ SlackCLIAPI: {
271
+ implementationName: "SlackCLIAPI",
272
+ version: "1.30.0",
273
+ },
274
+ },
275
+ });
276
+
277
+ // Final result should have both apps
278
+ expect(Object.keys(result.manifest?.apps || {})).toHaveLength(2);
279
+ });
280
+ });
281
+
282
+ describe("progress events", () => {
283
+ it("should emit apps_lookup_start event", async () => {
284
+ const sdk = createTestSdk();
285
+ const onProgress = vi.fn();
286
+
287
+ await sdk.buildManifest({
288
+ appKeys: ["slack", "gmail"],
289
+ skipWrite: true,
290
+ onProgress,
291
+ });
292
+
293
+ expect(onProgress).toHaveBeenCalledWith({
294
+ type: "apps_lookup_start",
295
+ count: 2,
296
+ });
297
+ });
298
+
299
+ it("should emit app_found event for each app", async () => {
300
+ const sdk = createTestSdk();
301
+ const onProgress = vi.fn();
302
+
303
+ await sdk.buildManifest({
304
+ appKeys: ["slack", "gmail"],
305
+ skipWrite: true,
306
+ onProgress,
307
+ });
308
+
309
+ expect(onProgress).toHaveBeenCalledWith({
310
+ type: "app_found",
311
+ app: mockSlackApp,
312
+ });
313
+
314
+ expect(onProgress).toHaveBeenCalledWith({
315
+ type: "app_found",
316
+ app: mockGmailApp,
317
+ });
318
+ });
319
+
320
+ it("should emit apps_lookup_complete event", async () => {
321
+ const sdk = createTestSdk();
322
+ const onProgress = vi.fn();
323
+
324
+ await sdk.buildManifest({
325
+ appKeys: ["slack", "gmail"],
326
+ skipWrite: true,
327
+ onProgress,
328
+ });
329
+
330
+ expect(onProgress).toHaveBeenCalledWith({
331
+ type: "apps_lookup_complete",
332
+ count: 2,
333
+ });
334
+ });
335
+
336
+ it("should emit app_processing_start event for each app", async () => {
337
+ const sdk = createTestSdk();
338
+ const onProgress = vi.fn();
339
+
340
+ await sdk.buildManifest({
341
+ appKeys: ["slack"],
342
+ skipWrite: true,
343
+ onProgress,
344
+ });
345
+
346
+ expect(onProgress).toHaveBeenCalledWith({
347
+ type: "app_processing_start",
348
+ appKey: "SlackCLIAPI",
349
+ slug: "slack",
350
+ });
351
+ });
352
+
353
+ it("should emit manifest_entry_built event", async () => {
354
+ const sdk = createTestSdk();
355
+ const onProgress = vi.fn();
356
+
357
+ await sdk.buildManifest({
358
+ appKeys: ["slack"],
359
+ skipWrite: true,
360
+ onProgress,
361
+ });
362
+
363
+ expect(onProgress).toHaveBeenCalledWith({
364
+ type: "manifest_entry_built",
365
+ appKey: "SlackCLIAPI",
366
+ manifestKey: "SlackCLIAPI",
367
+ version: "1.30.0",
368
+ });
369
+ });
370
+
371
+ it("should emit manifest_updated event when skipWrite is false", async () => {
372
+ const sdk = createTestSdk();
373
+ const onProgress = vi.fn();
374
+
375
+ await sdk.buildManifest({
376
+ appKeys: ["slack"],
377
+ skipWrite: false,
378
+ onProgress,
379
+ });
380
+
381
+ expect(onProgress).toHaveBeenCalledWith({
382
+ type: "manifest_updated",
383
+ appKey: "SlackCLIAPI",
384
+ manifestKey: "SlackCLIAPI",
385
+ version: "1.30.0",
386
+ });
387
+ });
388
+
389
+ it("should emit app_processing_complete event", async () => {
390
+ const sdk = createTestSdk();
391
+ const onProgress = vi.fn();
392
+
393
+ await sdk.buildManifest({
394
+ appKeys: ["slack"],
395
+ skipWrite: true,
396
+ onProgress,
397
+ });
398
+
399
+ expect(onProgress).toHaveBeenCalledWith({
400
+ type: "app_processing_complete",
401
+ appKey: "SlackCLIAPI",
402
+ });
403
+ });
404
+
405
+ it("should emit events in correct order", async () => {
406
+ const sdk = createTestSdk([mockSlackApp]);
407
+ const onProgress = vi.fn();
408
+
409
+ await sdk.buildManifest({
410
+ appKeys: ["slack"],
411
+ skipWrite: false,
412
+ onProgress,
413
+ });
414
+
415
+ const eventTypes = onProgress.mock.calls.map((call) => call[0].type);
416
+ expect(eventTypes).toEqual([
417
+ "apps_lookup_start",
418
+ "app_found",
419
+ "apps_lookup_complete",
420
+ "app_processing_start",
421
+ "manifest_entry_built",
422
+ "manifest_updated",
423
+ "app_processing_complete",
424
+ ]);
425
+ });
426
+ });
427
+
428
+ describe("error handling", () => {
429
+ it("should handle apps without version", async () => {
430
+ const sdk = createTestSdk([mockAppWithoutVersion]);
431
+
432
+ const result = await sdk.buildManifest({
433
+ appKeys: ["broken"],
434
+ skipWrite: true,
435
+ });
436
+
437
+ expect(result.errors).toHaveLength(1);
438
+ expect(result.errors[0]).toEqual({
439
+ appKey: "BrokenAPI",
440
+ error: expect.stringContaining("Failed to process app"),
441
+ });
442
+ });
443
+
444
+ it("should emit app_processing_error event on failure", async () => {
445
+ const sdk = createTestSdk([mockAppWithoutVersion]);
446
+ const onProgress = vi.fn();
447
+
448
+ await sdk.buildManifest({
449
+ appKeys: ["broken"],
450
+ skipWrite: true,
451
+ onProgress,
452
+ });
453
+
454
+ expect(onProgress).toHaveBeenCalledWith({
455
+ type: "app_processing_error",
456
+ appKey: "BrokenAPI",
457
+ error: expect.stringContaining("Failed to process app"),
458
+ });
459
+ });
460
+
461
+ it("should continue processing other apps after error", async () => {
462
+ const sdk = createTestSdk([mockAppWithoutVersion, mockSlackApp]);
463
+
464
+ const result = await sdk.buildManifest({
465
+ appKeys: ["broken", "slack"],
466
+ skipWrite: true,
467
+ });
468
+
469
+ // Should have one error
470
+ expect(result.errors).toHaveLength(1);
471
+ expect(result.errors[0].appKey).toBe("BrokenAPI");
472
+
473
+ // Should still process successful app
474
+ expect(result.manifest?.apps?.SlackCLIAPI).toBeDefined();
475
+ });
476
+
477
+ it("should handle updateManifestEntry failure", async () => {
478
+ mockUpdateManifestEntry.mockRejectedValueOnce(new Error("Write failed"));
479
+ const sdk = createTestSdk();
480
+
481
+ const result = await sdk.buildManifest({
482
+ appKeys: ["slack"],
483
+ skipWrite: false,
484
+ });
485
+
486
+ expect(result.errors).toHaveLength(1);
487
+ expect(result.errors[0]).toEqual({
488
+ appKey: "SlackCLIAPI",
489
+ error: "Failed to process app: Error: Write failed",
490
+ });
491
+ });
492
+
493
+ it("should accumulate multiple errors", async () => {
494
+ mockUpdateManifestEntry
495
+ .mockRejectedValueOnce(new Error("First error"))
496
+ .mockRejectedValueOnce(new Error("Second error"));
497
+
498
+ const sdk = createTestSdk();
499
+
500
+ const result = await sdk.buildManifest({
501
+ appKeys: ["slack", "gmail"],
502
+ skipWrite: false,
503
+ });
504
+
505
+ expect(result.errors).toHaveLength(2);
506
+ expect(result.errors[0].appKey).toBe("SlackCLIAPI");
507
+ expect(result.errors[1].appKey).toBe("GmailCLIAPI");
508
+ });
509
+ });
510
+
511
+ describe("manifest entry creation", () => {
512
+ it("should create correct manifest entry structure", async () => {
513
+ const sdk = createTestSdk([mockSlackApp]);
514
+
515
+ await sdk.buildManifest({
516
+ appKeys: ["slack"],
517
+ skipWrite: false,
518
+ });
519
+
520
+ expect(mockUpdateManifestEntry).toHaveBeenCalledWith(
521
+ expect.objectContaining({
522
+ entry: {
523
+ implementationName: "SlackCLIAPI",
524
+ version: "1.30.0",
525
+ },
526
+ }),
527
+ );
528
+ });
529
+
530
+ it("should use app.key as implementationName", async () => {
531
+ const sdk = createTestSdk([mockGmailApp]);
532
+
533
+ await sdk.buildManifest({
534
+ appKeys: ["gmail"],
535
+ skipWrite: false,
536
+ });
537
+
538
+ expect(mockUpdateManifestEntry).toHaveBeenCalledWith(
539
+ expect.objectContaining({
540
+ entry: {
541
+ implementationName: "GmailCLIAPI",
542
+ version: "2.5.0",
543
+ },
544
+ }),
545
+ );
546
+ });
547
+ });
548
+
549
+ describe("context and metadata", () => {
550
+ it("should provide context with meta information", () => {
551
+ const sdk = createTestSdk();
552
+ const context = sdk.getContext();
553
+
554
+ expect(context.meta.buildManifest).toBeDefined();
555
+ expect(context.meta.buildManifest.inputSchema).toBeDefined();
556
+ expect(context.meta.buildManifest.categories).toEqual(["utility"]);
557
+ });
558
+
559
+ it("should expose BuildManifestSchema in metadata", () => {
560
+ const sdk = createTestSdk();
561
+ const context = sdk.getContext();
562
+
563
+ expect(context.meta.buildManifest.inputSchema).toBe(BuildManifestSchema);
564
+ });
565
+ });
566
+
567
+ describe("edge cases", () => {
568
+ it("should handle empty apps array when no apps are found", async () => {
569
+ const sdk = createTestSdk([]);
570
+
571
+ const result = await sdk.buildManifest({
572
+ appKeys: ["nonexistent"],
573
+ skipWrite: true,
574
+ });
575
+
576
+ expect(result.manifest).toBeUndefined();
577
+ expect(result.errors).toEqual([]);
578
+ });
579
+
580
+ it("should not call updateManifestEntry when no apps found", async () => {
581
+ const sdk = createTestSdk([]);
582
+
583
+ await sdk.buildManifest({
584
+ appKeys: ["nonexistent"],
585
+ skipWrite: false,
586
+ });
587
+
588
+ expect(mockUpdateManifestEntry).not.toHaveBeenCalled();
589
+ });
590
+
591
+ it("should handle apps without slug", async () => {
592
+ const appWithoutSlug: AppItem = {
593
+ ...mockSlackApp,
594
+ slug: undefined as unknown as string,
595
+ };
596
+ const sdk = createTestSdk([appWithoutSlug]);
597
+ const onProgress = vi.fn();
598
+
599
+ await sdk.buildManifest({
600
+ appKeys: ["slack"],
601
+ skipWrite: true,
602
+ onProgress,
603
+ });
604
+
605
+ expect(onProgress).toHaveBeenCalledWith({
606
+ type: "app_processing_start",
607
+ appKey: "SlackCLIAPI",
608
+ slug: undefined,
609
+ });
610
+ });
611
+ });
612
+ });