@mytmpvpn/mytmpvpn-cli 1.1.0

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,349 @@
1
+ #!/usr/bin/env -S NODE_NO_WARNINGS=1 node
2
+ // Use NODE_NO_WARNINGS=1 to get rid of the fetch node deprecated API
3
+ // https://github.com/netlify/cli/issues/4608 -- but it hangs the process: https://github.com/nodejs/node/issues/21960
4
+ import * as fs from 'fs'
5
+ import * as path from 'path'
6
+ import { Argument, Command, Option } from 'commander'
7
+ import * as log from 'loglevel'
8
+ log.setDefaultLevel("info")
9
+
10
+ import * as vpnlib from '@mytmpvpn/mytmpvpn-common/models/vpn'
11
+ import * as peanuts from '@mytmpvpn/mytmpvpn-common/models/peanuts'
12
+ import { getDefaultAppConfigFile, loadAppConfig } from '@mytmpvpn/mytmpvpn-client/appconfig'
13
+ import { getDefaultUserConfigFile, getDefaultUserProfile, getDefaultUserConfigDir, loadUserConfig, UserConfig } from '@mytmpvpn/mytmpvpn-client/userconfig'
14
+ import * as auth from '@mytmpvpn/mytmpvpn-client/auth'
15
+ import { MyTmpVpnClient } from '@mytmpvpn/mytmpvpn-client/mytmpvpn-client'
16
+
17
+ const program = new Command()
18
+
19
+ function handleError(error: any, verbose: boolean = false) {
20
+ if (error.response) {
21
+ // The request was made and the server responded with a status code
22
+ // that falls out of the range of 2xx
23
+ try {
24
+ log.error(`[${error.response.status}] - ${JSON.stringify(error.response.data)}`)
25
+ } catch (Error) {
26
+ log.error(`[${error.response.status}] - ${JSON.stringify(error.message)}`)
27
+ }
28
+
29
+ log.debug(error.response.headers)
30
+ } else if (error.request) {
31
+ // The request was made but no response was received
32
+ // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
33
+ // http.ClientRequest in node.js
34
+ log.error(`No reponse received. Check your profile and your appConfig`)
35
+ } else {
36
+ // Something happened in setting up the request that triggered an Error
37
+ log.error(`Error while setting up the request ${JSON.stringify(error)}`)
38
+ }
39
+ log.trace(`Stack trace: ${error}`)
40
+ }
41
+
42
+ program
43
+ .name('mytmpvpn-cli')
44
+ .description('MyTmpVpn CLI')
45
+ .version('0.0.1')
46
+ .option('--verbose', 'Produce more logs')
47
+ .option('--appConfig <file>', 'Path to the application config file', getDefaultAppConfigFile())
48
+ .option('--userConfig <file>', 'Path to the user config file', getDefaultUserConfigFile())
49
+ .option('--profile <name>', 'Name of the profile in the user config file to use', getDefaultUserProfile())
50
+ program.on('option:verbose', function () {
51
+ log.setDefaultLevel("trace")
52
+ })
53
+
54
+ program.command('list-peanuts-packs')
55
+ .description(`Returns the list of peanuts packs you can purchase. A peanuts pack contains a given number of peanuts. You need a minimum of ${peanuts.PEANUTS_CONFIG.min} peanuts to create a vpn`)
56
+ .action((_, command) => {
57
+ const options = command.optsWithGlobals()
58
+ const appConfig = loadAppConfig(options.appConfig)
59
+ // We don't need authenticated user to call this API
60
+ const client = new MyTmpVpnClient(appConfig.apiUrl)
61
+ client.listPeanutsPacks()
62
+ .then(((packs: peanuts.PeanutsPack[]) => {
63
+ log.info(JSON.stringify(packs, null, 2))
64
+ }))
65
+ .catch((err) => handleError(err))
66
+ })
67
+
68
+ program.command('get-peanuts-balance')
69
+ .description('Get the current peanuts balance')
70
+ .action((_, command) => {
71
+ const options = command.optsWithGlobals()
72
+ log.debug(`Get peanuts balance`)
73
+ auth.getLoggedInClientFromFiles({ appConfigFile: options.appConfig, userConfigFile: options.userConfig, profileName: options.profile }, (err, client) => {
74
+ if (err) {
75
+ handleError(err, options.verbose)
76
+ return
77
+ }
78
+ client.getPeanutsBalance().then(balance => {
79
+ log.info(balance)
80
+ }).catch(err => {
81
+ handleError(err)
82
+ })
83
+ })
84
+ })
85
+
86
+ program.command('list-regions')
87
+ .description('Returns the list of all regions where vpn can be created')
88
+ .action((_, command) => {
89
+ const options = command.optsWithGlobals()
90
+ const appConfig = loadAppConfig(options.appConfig)
91
+ // We don't need authenticated user to call this API
92
+ const client = new MyTmpVpnClient(appConfig.apiUrl)
93
+ client.listRegions()
94
+ .then(((regions: any) => {
95
+ log.info(JSON.stringify(regions, null, 2))
96
+ }))
97
+ .catch((err) => handleError(err))
98
+ })
99
+
100
+
101
+ program.command('create')
102
+ .description('Create a new vpn')
103
+ .argument('<region>', 'region where the vpn should be created, as returned by list-regions')
104
+ .option('--sync', 'wait for vpn creation completion')
105
+ .addOption(new Option('--type <type>', 'Type of VPN')
106
+ .choices(vpnlib.getVpnConfigTypes())
107
+ .default(vpnlib.VpnType.WireGuard))
108
+ .option('--peanuts <nb>', 'Max number of peanuts to use (specify -1 for maximum)', '-1')
109
+ .action((region: string, _, command) => {
110
+ const options = command.optsWithGlobals()
111
+ const syncStr = options.sync ? "synchronously" : "asynchronously"
112
+ log.debug(`Creating new ${options.type} vpn into ${region} ${syncStr}`)
113
+ auth.getLoggedInClientFromFiles({ appConfigFile: options.appConfig, userConfigFile: options.userConfig, profileName: options.profile }, (err, client) => {
114
+ if (err) {
115
+ handleError(err, options.verbose)
116
+ return
117
+ }
118
+ client.createVpn(region, { type: options.type, maxPeanuts: options.peanuts }).then(vpn => {
119
+ if (!options.sync) {
120
+ log.info(vpn)
121
+ return
122
+ }
123
+ client.waitUntilVpnStateIs(vpn.vpnId, vpnlib.VpnState.Running).then(updatedVpn => {
124
+ log.info(updatedVpn)
125
+ }).catch(err => {
126
+ handleError(err)
127
+ })
128
+ }).catch(err => {
129
+ handleError(err)
130
+ })
131
+ })
132
+ })
133
+
134
+
135
+ program.command('delete')
136
+ .description('Delete a vpn')
137
+ .argument('<vpnId>', 'vpnId to delete')
138
+ .option('--sync', 'wait for vpn deletion completion')
139
+ .action((vpnId, _, command) => {
140
+ const options = command.optsWithGlobals()
141
+ const syncStr = options.sync ? "synchronously" : "asynchronously"
142
+ log.debug(`Deleting vpn ${vpnId} ${syncStr}`)
143
+ auth.getLoggedInClientFromFiles({ appConfigFile: options.appConfig, userConfigFile: options.userConfig, profileName: options.profile }, (err, client) => {
144
+ if (err) {
145
+ handleError(err.message || JSON.stringify(err))
146
+ return
147
+ }
148
+ client.deleteVpn(vpnId).then(vpn => {
149
+ if (!options.sync) {
150
+ log.info(JSON.stringify(vpn))
151
+ return
152
+ }
153
+ client.waitUntilVpnStateIs(vpnId, vpnlib.VpnState.Deleted).then(updatedVpn => {
154
+ log.info(updatedVpn)
155
+ return
156
+ }).catch(err => {
157
+ handleError(err.response?.data || JSON.stringify(err))
158
+ })
159
+ }).catch(err => {
160
+ handleError(err.response?.data || JSON.stringify(err))
161
+ })
162
+ })
163
+ })
164
+
165
+ program.command('get')
166
+ .description('Get information on a vpn')
167
+ .argument('<vpnId>', 'vpnId to get information from')
168
+ .action((vpnId, _, command) => {
169
+ const options = command.optsWithGlobals()
170
+ auth.getLoggedInClientFromFiles({ appConfigFile: options.appConfig, userConfigFile: options.userConfig, profileName: options.profile }, (err, client) => {
171
+ if (err) {
172
+ handleError(err)
173
+ return
174
+ }
175
+ client.getVpn(vpnId)
176
+ .then(result => log.info(result))
177
+ .catch(err => handleError(err))
178
+ })
179
+ })
180
+
181
+ program.command('download-config')
182
+ .description('Download configuration of the given vpn to the given file')
183
+ .argument('<vpnId>', 'vpnId to get config file from')
184
+ .option('--file <file>', 'file where the config should be downloaded to. Default is <vpnId>.tar.gz')
185
+ .option('--path <path>', 'path where the config file should be written to', getDefaultUserConfigDir())
186
+ .action((vpnId, _, command) => {
187
+ const options = command.optsWithGlobals()
188
+ const file = options.file ? options.file : `${vpnId}.tar.gz`
189
+ const fullpath = path.join(options.path, file)
190
+ auth.getLoggedInClientFromFiles({ appConfigFile: options.appConfig, userConfigFile: options.userConfig, profileName: options.profile }, (err, client) => {
191
+ if (err) {
192
+ handleError(err)
193
+ return
194
+ }
195
+ client.getVpnConfig(vpnId, fs.createWriteStream(fullpath))
196
+ .then(() => log.info(fullpath))
197
+ .catch(err => handleError(err))
198
+ })
199
+ })
200
+
201
+ program.command('list')
202
+ .description('List all vpns')
203
+ .option('--region <region>', 'region to list vpns from')
204
+ .option('--exclude-state <state>', 'state to exclude from the list', 'DELETED')
205
+ .action((_, command) => {
206
+ const options = command.optsWithGlobals()
207
+ auth.getLoggedInClientFromFiles({ appConfigFile: options.appConfig, userConfigFile: options.userConfig, profileName: options.profile }, (err, client) => {
208
+ if (err) {
209
+ handleError(err)
210
+ return
211
+ }
212
+ client.listVpns(options.state)
213
+ .then(vpns => {
214
+ log.info(JSON.stringify(vpns, null, 2))
215
+ })
216
+ .catch(err => handleError(err))
217
+ })
218
+ })
219
+
220
+ program.command('wait')
221
+ .description('Wait for vpn operation completion')
222
+ .argument('<vpnId>', 'the vpnId to wait a status change for')
223
+ .argument('<state>', 'the state to wait for')
224
+ .action((vpnId, state: keyof typeof vpnlib.VpnState, _, command) => {
225
+ const options = command.optsWithGlobals()
226
+ const actualState = vpnlib.VpnState[state]
227
+ if (!actualState) {
228
+ handleError(`Unknown state: ${state}. Valid states: ${Object.values(vpnlib.VpnState)}`)
229
+ return
230
+ }
231
+ log.debug(`Waiting for ${vpnId} state to be (at least) ${state}`)
232
+ auth.getLoggedInClientFromFiles({ appConfigFile: options.appConfig, userConfigFile: options.userConfig, profileName: options.profile }, (err, client) => {
233
+ if (err) {
234
+ handleError(err)
235
+ return
236
+ }
237
+ client.waitUntilVpnStateIs(vpnId, actualState)
238
+ .then(vpn => {
239
+ log.info(vpn)
240
+ })
241
+ .catch(err => handleError(err))
242
+ })
243
+ })
244
+
245
+ program.command('register')
246
+ .description('Register a new user to the MyTmpVpn application')
247
+ .argument('<username>', 'the email/phone that will identify your account')
248
+ .argument('<password>', 'a password')
249
+ .action((username, password, _, command) => {
250
+ const options = command.optsWithGlobals()
251
+ var userConfig: UserConfig
252
+ if (fs.existsSync(options.userConfig)) {
253
+ userConfig = loadUserConfig(options.userConfig)
254
+ if (userConfig.profiles[options.profile]) {
255
+ handleError(`Profile ${options.profile} already exists in ${options.userConfig}, specify another profile name using --profile`)
256
+ return
257
+ }
258
+ userConfig.profiles[options.profile] = {
259
+ username: username,
260
+ password: password
261
+ }
262
+ } else {
263
+ userConfig = {
264
+ version: 1,
265
+ profiles: {
266
+ default: {
267
+ username: username,
268
+ password: password
269
+ }
270
+ }
271
+ }
272
+ }
273
+ auth.registerUserFromFiles(options.appConfig, username, password, (err, result) => {
274
+ if (err) {
275
+ handleError(err)
276
+ return
277
+ }
278
+ log.info(`Please confirm your identity with the code sent to ${username}`)
279
+ fs.mkdir(path.dirname(options.userConfig), { recursive: true }, (err, path?) => {
280
+ if (err) {
281
+ handleError(err)
282
+ return
283
+ }
284
+ log.debug(`Directory: ${path} created`)
285
+ fs.writeFile(options.userConfig, JSON.stringify(userConfig, null, 2), (err) => {
286
+ if (err) {
287
+ handleError(err)
288
+ return
289
+ }
290
+ })
291
+ log.info(`A new profile has been created for ${options.profile} in ${options.userConfig}`)
292
+ })
293
+ })
294
+ })
295
+
296
+ program.command('confirm-registration')
297
+ .description('Confirm registration to the MyTmpVpn application using the code that was sent to email/phone')
298
+ .argument('<username>', 'the email/phone that identify your account')
299
+ .argument('<code>', 'the code received')
300
+ .action((username, code, _, command) => {
301
+ const options = command.optsWithGlobals()
302
+ auth.confirmUserFromFiles(options.appConfig, username, code, (err, result) => {
303
+ if (err) {
304
+ handleError(err)
305
+ return
306
+ }
307
+ log.debug(result)
308
+ log.info(`User ${username} confirmed!`)
309
+ })
310
+ })
311
+
312
+ program.addHelpText('after', `
313
+
314
+ Examples:
315
+
316
+ # Before doing anything, you need to register to the MyTmpVpn service:
317
+
318
+ $ mytmpvpn register 'your@email.com' 'StrongPassword' # Min: 8 characters with digit, lower, upper and symbols
319
+ $ mytmpvpn confirm-registration 'code' # The code you've received by email
320
+
321
+ # You then need to buy some peanuts, the list of peanuts packs can be fetched using:
322
+
323
+ $ mytmpvpn list-peanuts-packs
324
+
325
+ # Visit one of those URLs returned, and complete the purchase.
326
+ # There is no CLI for purchasing peanuts-packs.
327
+ # Once you have purchased some peanuts, your peanuts balance should show it:
328
+
329
+ $ mytmpvpn get-peanuts-balance
330
+
331
+ # Then you can use the following command at will:
332
+
333
+ # List available regions:
334
+ $ mytmpvpn list-regions
335
+
336
+ # Create a new vpn in Paris:
337
+ $ mytmpvpn --sync create 'paris' # This takes several minutes, please be patient or remove --sync
338
+
339
+ # List all my vpns:
340
+ $ mytmpvpn list
341
+
342
+ # Fetch the configuration of a given vpn
343
+ $ mytmpvpn download-config 'vpn-id' # The vpn-id is provided by the list command
344
+
345
+ # Delete a given vpn
346
+ $ mytmpvpn delete 'vpn-id' # The vpn-id is provided by the list command
347
+ `)
348
+
349
+ program.parse()
package/tsconfig.json ADDED
@@ -0,0 +1,115 @@
1
+ {
2
+ // this project builds all of the following:
3
+ "compilerOptions": {
4
+ /* Visit https://aka.ms/tsconfig to read more about this file */
5
+ "outDir": "dist", /* Specify an output folder for all emitted files. */
6
+
7
+ // we want subprojects to inherit these options:
8
+ "baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */
9
+ "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
10
+ "mytmpvpn-cli/*": [
11
+ "dist/*"
12
+ ]
13
+ },
14
+
15
+ /* Projects */
16
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
17
+ "composite": false, /* Enable constraints that allow a TypeScript project to be used with project references. */
18
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
19
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
20
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
21
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
22
+
23
+ /* Language and Environment */
24
+ "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
25
+ "lib": ["ES2020"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
26
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
27
+ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
28
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
29
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
30
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
31
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
32
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
33
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
34
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
35
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
36
+
37
+ /* Modules */
38
+ "module": "commonjs", /* Specify what module code is generated. */
39
+ // "rootDir": "./src", /* Specify the root folder within your source files. */
40
+ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
41
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
42
+ //"typeRoots": ["./node_modules/@types"], /* Specify multiple folders that act like './node_modules/@types'. */
43
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
44
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
45
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
46
+ // "resolveJsonModule": true, /* Enable importing .json files. */
47
+ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
48
+
49
+ /* JavaScript Support */
50
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
51
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
52
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
53
+
54
+ /* Emit */
55
+ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
56
+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
57
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
58
+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
59
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
60
+
61
+ // "removeComments": true, /* Disable emitting comments. */
62
+ // "noEmit": true, /* Disable emitting files from a compilation. */
63
+ // "importHelpers": false, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
64
+ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
65
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
66
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
67
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
68
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
69
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
70
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
71
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
72
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
73
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
74
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
75
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
76
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
77
+ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
78
+
79
+ /* Interop Constraints */
80
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
81
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
82
+ // "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
83
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
84
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
85
+
86
+ /* Type Checking */
87
+ "strict": true, /* Enable all strict type-checking options. */
88
+ "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
89
+ "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
90
+ "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
91
+ "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
92
+ "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
93
+ "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
94
+ "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
95
+ "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
96
+ "noUnusedLocals": false, /* Enable error reporting when local variables aren't read. */
97
+ "noUnusedParameters": false, /* Raise an error when a function parameter isn't read. */
98
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
99
+ "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
100
+ "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
101
+ "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
102
+ "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
103
+ "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
104
+ "allowUnusedLabels": false, /* Disable error reporting for unused labels. */
105
+ "allowUnreachableCode": false, /* Disable error reporting for unreachable code. */
106
+
107
+ /* Completeness */
108
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
109
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
110
+ },
111
+ "include": [
112
+ "src/**/*",
113
+ "test/**/*",
114
+ ],
115
+ }