@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,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
+ }
@@ -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
+ });
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "paths": {}
5
+ }
6
+ }