@ktmcp-cli/nordigen 1.0.0 → 1.0.2

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/src/index.js ADDED
@@ -0,0 +1,633 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, isConfigured } from './config.js';
5
+ import {
6
+ listInstitutions,
7
+ getInstitution,
8
+ listAgreements,
9
+ getAgreement,
10
+ createAgreement,
11
+ deleteAgreement,
12
+ acceptAgreement,
13
+ listRequisitions,
14
+ getRequisition,
15
+ createRequisition,
16
+ deleteRequisition,
17
+ getAccountMetadata,
18
+ getAccountBalances,
19
+ getAccountDetails,
20
+ getAccountTransactions
21
+ } from './api.js';
22
+
23
+ const program = new Command();
24
+
25
+ // ============================================================
26
+ // Helpers
27
+ // ============================================================
28
+
29
+ function printSuccess(message) {
30
+ console.log(chalk.green('✓') + ' ' + message);
31
+ }
32
+
33
+ function printError(message) {
34
+ console.error(chalk.red('✗') + ' ' + message);
35
+ }
36
+
37
+ function printTable(data, columns) {
38
+ if (!data || data.length === 0) {
39
+ console.log(chalk.yellow('No results found.'));
40
+ return;
41
+ }
42
+
43
+ const widths = {};
44
+ columns.forEach(col => {
45
+ widths[col.key] = col.label.length;
46
+ data.forEach(row => {
47
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
48
+ if (val.length > widths[col.key]) widths[col.key] = val.length;
49
+ });
50
+ widths[col.key] = Math.min(widths[col.key], 50);
51
+ });
52
+
53
+ const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
54
+ console.log(chalk.bold(chalk.cyan(header)));
55
+ console.log(chalk.dim('─'.repeat(header.length)));
56
+
57
+ data.forEach(row => {
58
+ const line = columns.map(col => {
59
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
60
+ return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
61
+ }).join(' ');
62
+ console.log(line);
63
+ });
64
+
65
+ console.log(chalk.dim(`\n${data.length} result(s)`));
66
+ }
67
+
68
+ function printJson(data) {
69
+ console.log(JSON.stringify(data, null, 2));
70
+ }
71
+
72
+ async function withSpinner(message, fn) {
73
+ const spinner = ora(message).start();
74
+ try {
75
+ const result = await fn();
76
+ spinner.stop();
77
+ return result;
78
+ } catch (error) {
79
+ spinner.stop();
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ function requireAuth() {
85
+ if (!isConfigured()) {
86
+ printError('Nordigen credentials not configured.');
87
+ console.log('\nRun the following to configure:');
88
+ console.log(chalk.cyan(' nordigencom config set --secret-id <id> --secret-key <key>'));
89
+ console.log(chalk.cyan(' nordigencom auth login'));
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ // ============================================================
95
+ // Program metadata
96
+ // ============================================================
97
+
98
+ program
99
+ .name('nordigencom')
100
+ .description(chalk.bold('Nordigen CLI') + ' - Account Information Services from your terminal')
101
+ .version('1.0.0');
102
+
103
+ // ============================================================
104
+ // CONFIG
105
+ // ============================================================
106
+
107
+ const configCmd = program.command('config').description('Manage CLI configuration');
108
+
109
+ configCmd
110
+ .command('set')
111
+ .description('Set configuration values')
112
+ .option('--secret-id <id>', 'Nordigen Secret ID')
113
+ .option('--secret-key <key>', 'Nordigen Secret Key')
114
+ .action((options) => {
115
+ if (options.secretId) {
116
+ setConfig('secretId', options.secretId);
117
+ printSuccess(`Secret ID set`);
118
+ }
119
+ if (options.secretKey) {
120
+ setConfig('secretKey', options.secretKey);
121
+ printSuccess(`Secret Key set`);
122
+ }
123
+ if (!options.secretId && !options.secretKey) {
124
+ printError('No options provided. Use --secret-id or --secret-key');
125
+ }
126
+ });
127
+
128
+ configCmd
129
+ .command('show')
130
+ .description('Show current configuration')
131
+ .action(() => {
132
+ const secretId = getConfig('secretId');
133
+ const secretKey = getConfig('secretKey');
134
+ const hasToken = !!getConfig('accessToken');
135
+ const tokenExpiry = getConfig('tokenExpiry');
136
+
137
+ console.log(chalk.bold('\nNordigen CLI Configuration\n'));
138
+ console.log('Secret ID: ', secretId ? chalk.green(secretId) : chalk.red('not set'));
139
+ console.log('Secret Key: ', secretKey ? chalk.green('*'.repeat(8)) : chalk.red('not set'));
140
+ console.log('Access Token: ', hasToken ? chalk.green('set') : chalk.red('not set'));
141
+ if (tokenExpiry) {
142
+ const expiry = new Date(tokenExpiry);
143
+ const isValid = tokenExpiry > Date.now();
144
+ console.log('Token Expiry: ', isValid ? chalk.green(expiry.toLocaleString()) : chalk.red(`expired (${expiry.toLocaleString()})`));
145
+ }
146
+ console.log('');
147
+ });
148
+
149
+ // ============================================================
150
+ // AUTH
151
+ // ============================================================
152
+
153
+ const authCmd = program.command('auth').description('Manage authentication');
154
+
155
+ authCmd
156
+ .command('login')
157
+ .description('Authenticate with Nordigen and obtain JWT token')
158
+ .action(async () => {
159
+ if (!isConfigured()) {
160
+ printError('Please configure your credentials first:');
161
+ console.log(chalk.cyan(' nordigencom config set --secret-id <id> --secret-key <key>'));
162
+ process.exit(1);
163
+ }
164
+
165
+ try {
166
+ // Import the obtainAccessToken logic directly
167
+ const { obtainAccessToken } = await import('./api.js');
168
+ await withSpinner('Obtaining access token...', async () => {
169
+ // Trigger token fetch by calling an API endpoint
170
+ const institutions = await listInstitutions({ country: 'GB' });
171
+ return institutions;
172
+ });
173
+ printSuccess('Successfully authenticated with Nordigen');
174
+ } catch (error) {
175
+ printError(error.message);
176
+ process.exit(1);
177
+ }
178
+ });
179
+
180
+ authCmd
181
+ .command('status')
182
+ .description('Check authentication status')
183
+ .action(() => {
184
+ const hasToken = !!getConfig('accessToken');
185
+ const tokenExpiry = getConfig('tokenExpiry');
186
+
187
+ if (!hasToken) {
188
+ printError('Not authenticated. Run: nordigencom auth login');
189
+ process.exit(1);
190
+ }
191
+
192
+ const isValid = tokenExpiry > Date.now();
193
+ if (isValid) {
194
+ printSuccess('Authenticated with Nordigen');
195
+ console.log('Token expires:', new Date(tokenExpiry).toLocaleString());
196
+ } else {
197
+ printError('Token expired. Run: nordigencom auth login');
198
+ process.exit(1);
199
+ }
200
+ });
201
+
202
+ // ============================================================
203
+ // INSTITUTIONS
204
+ // ============================================================
205
+
206
+ const institutionsCmd = program.command('institutions').description('Manage financial institutions');
207
+
208
+ institutionsCmd
209
+ .command('list')
210
+ .description('List all available institutions')
211
+ .option('--country <code>', 'Filter by country code (e.g., GB, DE, FR)')
212
+ .option('--json', 'Output as JSON')
213
+ .action(async (options) => {
214
+ requireAuth();
215
+ try {
216
+ const institutions = await withSpinner('Fetching institutions...', () =>
217
+ listInstitutions({ country: options.country })
218
+ );
219
+
220
+ if (options.json) {
221
+ printJson(institutions);
222
+ return;
223
+ }
224
+
225
+ printTable(institutions, [
226
+ { key: 'id', label: 'ID' },
227
+ { key: 'name', label: 'Name' },
228
+ { key: 'bic', label: 'BIC' },
229
+ { key: 'transaction_total_days', label: 'Transaction Days' },
230
+ { key: 'countries', label: 'Countries', format: (v) => Array.isArray(v) ? v.join(', ') : v }
231
+ ]);
232
+ } catch (error) {
233
+ printError(error.message);
234
+ process.exit(1);
235
+ }
236
+ });
237
+
238
+ institutionsCmd
239
+ .command('get <institution-id>')
240
+ .description('Get details about a specific institution')
241
+ .option('--json', 'Output as JSON')
242
+ .action(async (institutionId, options) => {
243
+ requireAuth();
244
+ try {
245
+ const institution = await withSpinner('Fetching institution...', () => getInstitution(institutionId));
246
+
247
+ if (options.json) {
248
+ printJson(institution);
249
+ return;
250
+ }
251
+
252
+ console.log(chalk.bold('\nInstitution Details\n'));
253
+ console.log('ID: ', chalk.cyan(institution.id));
254
+ console.log('Name: ', chalk.bold(institution.name));
255
+ console.log('BIC: ', institution.bic || 'N/A');
256
+ console.log('Transaction Days: ', institution.transaction_total_days || 'N/A');
257
+ console.log('Countries: ', Array.isArray(institution.countries) ? institution.countries.join(', ') : 'N/A');
258
+ console.log('Logo: ', institution.logo || 'N/A');
259
+ } catch (error) {
260
+ printError(error.message);
261
+ process.exit(1);
262
+ }
263
+ });
264
+
265
+ // ============================================================
266
+ // AGREEMENTS
267
+ // ============================================================
268
+
269
+ const agreementsCmd = program.command('agreements').description('Manage end user agreements');
270
+
271
+ agreementsCmd
272
+ .command('list')
273
+ .description('List all agreements')
274
+ .option('--limit <n>', 'Maximum number of results', '100')
275
+ .option('--offset <n>', 'Offset for pagination', '0')
276
+ .option('--json', 'Output as JSON')
277
+ .action(async (options) => {
278
+ requireAuth();
279
+ try {
280
+ const agreements = await withSpinner('Fetching agreements...', () =>
281
+ listAgreements({ limit: parseInt(options.limit), offset: parseInt(options.offset) })
282
+ );
283
+
284
+ if (options.json) {
285
+ printJson(agreements);
286
+ return;
287
+ }
288
+
289
+ printTable(agreements, [
290
+ { key: 'id', label: 'ID', format: (v) => v?.substring(0, 8) + '...' },
291
+ { key: 'institution_id', label: 'Institution' },
292
+ { key: 'created', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' },
293
+ { key: 'max_historical_days', label: 'Historical Days' },
294
+ { key: 'access_valid_for_days', label: 'Valid For Days' },
295
+ { key: 'accepted', label: 'Accepted', format: (v) => v ? new Date(v).toLocaleDateString() : 'No' }
296
+ ]);
297
+ } catch (error) {
298
+ printError(error.message);
299
+ process.exit(1);
300
+ }
301
+ });
302
+
303
+ agreementsCmd
304
+ .command('get <agreement-id>')
305
+ .description('Get a specific agreement')
306
+ .option('--json', 'Output as JSON')
307
+ .action(async (agreementId, options) => {
308
+ requireAuth();
309
+ try {
310
+ const agreement = await withSpinner('Fetching agreement...', () => getAgreement(agreementId));
311
+
312
+ if (options.json) {
313
+ printJson(agreement);
314
+ return;
315
+ }
316
+
317
+ console.log(chalk.bold('\nAgreement Details\n'));
318
+ console.log('ID: ', chalk.cyan(agreement.id));
319
+ console.log('Institution ID: ', agreement.institution_id);
320
+ console.log('Created: ', new Date(agreement.created).toLocaleString());
321
+ console.log('Max Historical Days: ', agreement.max_historical_days);
322
+ console.log('Valid For Days: ', agreement.access_valid_for_days);
323
+ console.log('Accepted: ', agreement.accepted ? new Date(agreement.accepted).toLocaleString() : chalk.yellow('Not accepted'));
324
+ console.log('Access Scope: ', Array.isArray(agreement.access_scope) ? agreement.access_scope.join(', ') : 'All');
325
+ } catch (error) {
326
+ printError(error.message);
327
+ process.exit(1);
328
+ }
329
+ });
330
+
331
+ agreementsCmd
332
+ .command('create')
333
+ .description('Create a new end user agreement')
334
+ .requiredOption('--institution-id <id>', 'Institution ID')
335
+ .option('--max-historical-days <n>', 'Maximum historical days', '90')
336
+ .option('--access-valid-for-days <n>', 'Access valid for days', '90')
337
+ .option('--access-scope <scopes>', 'Comma-separated access scopes (balances,details,transactions)')
338
+ .option('--json', 'Output as JSON')
339
+ .action(async (options) => {
340
+ requireAuth();
341
+ const accessScope = options.accessScope ? options.accessScope.split(',') : [];
342
+
343
+ try {
344
+ const agreement = await withSpinner('Creating agreement...', () =>
345
+ createAgreement({
346
+ institutionId: options.institutionId,
347
+ maxHistoricalDays: parseInt(options.maxHistoricalDays),
348
+ accessValidForDays: parseInt(options.accessValidForDays),
349
+ accessScope
350
+ })
351
+ );
352
+
353
+ if (options.json) {
354
+ printJson(agreement);
355
+ return;
356
+ }
357
+
358
+ printSuccess(`Agreement created: ${chalk.bold(agreement.id)}`);
359
+ console.log('Institution ID:', agreement.institution_id);
360
+ console.log('Created: ', new Date(agreement.created).toLocaleString());
361
+ } catch (error) {
362
+ printError(error.message);
363
+ process.exit(1);
364
+ }
365
+ });
366
+
367
+ agreementsCmd
368
+ .command('delete <agreement-id>')
369
+ .description('Delete an agreement')
370
+ .action(async (agreementId) => {
371
+ requireAuth();
372
+ try {
373
+ await withSpinner('Deleting agreement...', () => deleteAgreement(agreementId));
374
+ printSuccess(`Agreement ${agreementId} deleted`);
375
+ } catch (error) {
376
+ printError(error.message);
377
+ process.exit(1);
378
+ }
379
+ });
380
+
381
+ // ============================================================
382
+ // REQUISITIONS
383
+ // ============================================================
384
+
385
+ const requisitionsCmd = program.command('requisitions').description('Manage requisitions (bank connections)');
386
+
387
+ requisitionsCmd
388
+ .command('list')
389
+ .description('List all requisitions')
390
+ .option('--limit <n>', 'Maximum number of results', '100')
391
+ .option('--offset <n>', 'Offset for pagination', '0')
392
+ .option('--json', 'Output as JSON')
393
+ .action(async (options) => {
394
+ requireAuth();
395
+ try {
396
+ const requisitions = await withSpinner('Fetching requisitions...', () =>
397
+ listRequisitions({ limit: parseInt(options.limit), offset: parseInt(options.offset) })
398
+ );
399
+
400
+ if (options.json) {
401
+ printJson(requisitions);
402
+ return;
403
+ }
404
+
405
+ printTable(requisitions, [
406
+ { key: 'id', label: 'ID', format: (v) => v?.substring(0, 8) + '...' },
407
+ { key: 'reference', label: 'Reference' },
408
+ { key: 'status', label: 'Status' },
409
+ { key: 'institution_id', label: 'Institution' },
410
+ { key: 'created', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' },
411
+ { key: 'accounts', label: 'Accounts', format: (v) => Array.isArray(v) ? v.length : 0 }
412
+ ]);
413
+ } catch (error) {
414
+ printError(error.message);
415
+ process.exit(1);
416
+ }
417
+ });
418
+
419
+ requisitionsCmd
420
+ .command('get <requisition-id>')
421
+ .description('Get a specific requisition')
422
+ .option('--json', 'Output as JSON')
423
+ .action(async (requisitionId, options) => {
424
+ requireAuth();
425
+ try {
426
+ const requisition = await withSpinner('Fetching requisition...', () => getRequisition(requisitionId));
427
+
428
+ if (options.json) {
429
+ printJson(requisition);
430
+ return;
431
+ }
432
+
433
+ console.log(chalk.bold('\nRequisition Details\n'));
434
+ console.log('ID: ', chalk.cyan(requisition.id));
435
+ console.log('Reference: ', requisition.reference);
436
+ console.log('Status: ', chalk.bold(requisition.status));
437
+ console.log('Institution: ', requisition.institution_id);
438
+ console.log('Created: ', new Date(requisition.created).toLocaleString());
439
+ console.log('Link: ', requisition.link || 'N/A');
440
+ console.log('Accounts: ', Array.isArray(requisition.accounts) ? requisition.accounts.join(', ') : 'None');
441
+ } catch (error) {
442
+ printError(error.message);
443
+ process.exit(1);
444
+ }
445
+ });
446
+
447
+ requisitionsCmd
448
+ .command('create')
449
+ .description('Create a new requisition')
450
+ .requiredOption('--institution-id <id>', 'Institution ID')
451
+ .requiredOption('--redirect <url>', 'Redirect URL after authentication')
452
+ .requiredOption('--reference <ref>', 'Unique reference for this requisition')
453
+ .option('--agreement-id <id>', 'End user agreement ID')
454
+ .option('--user-language <lang>', 'User language (EN, DE, FR, etc.)', 'EN')
455
+ .option('--json', 'Output as JSON')
456
+ .action(async (options) => {
457
+ requireAuth();
458
+ try {
459
+ const requisition = await withSpinner('Creating requisition...', () =>
460
+ createRequisition({
461
+ institutionId: options.institutionId,
462
+ redirect: options.redirect,
463
+ reference: options.reference,
464
+ agreementId: options.agreementId,
465
+ userLanguage: options.userLanguage
466
+ })
467
+ );
468
+
469
+ if (options.json) {
470
+ printJson(requisition);
471
+ return;
472
+ }
473
+
474
+ printSuccess(`Requisition created: ${chalk.bold(requisition.id)}`);
475
+ console.log('Reference: ', requisition.reference);
476
+ console.log('Status: ', requisition.status);
477
+ console.log('Link: ', chalk.cyan(requisition.link));
478
+ console.log('\nSend this link to the user to authorize bank access.');
479
+ } catch (error) {
480
+ printError(error.message);
481
+ process.exit(1);
482
+ }
483
+ });
484
+
485
+ requisitionsCmd
486
+ .command('delete <requisition-id>')
487
+ .description('Delete a requisition')
488
+ .action(async (requisitionId) => {
489
+ requireAuth();
490
+ try {
491
+ await withSpinner('Deleting requisition...', () => deleteRequisition(requisitionId));
492
+ printSuccess(`Requisition ${requisitionId} deleted`);
493
+ } catch (error) {
494
+ printError(error.message);
495
+ process.exit(1);
496
+ }
497
+ });
498
+
499
+ // ============================================================
500
+ // ACCOUNTS
501
+ // ============================================================
502
+
503
+ const accountsCmd = program.command('accounts').description('Access account information');
504
+
505
+ accountsCmd
506
+ .command('get <account-id>')
507
+ .description('Get account metadata')
508
+ .option('--json', 'Output as JSON')
509
+ .action(async (accountId, options) => {
510
+ requireAuth();
511
+ try {
512
+ const account = await withSpinner('Fetching account metadata...', () => getAccountMetadata(accountId));
513
+
514
+ if (options.json) {
515
+ printJson(account);
516
+ return;
517
+ }
518
+
519
+ console.log(chalk.bold('\nAccount Metadata\n'));
520
+ console.log('Account ID: ', chalk.cyan(account.id));
521
+ console.log('IBAN: ', account.iban || 'N/A');
522
+ console.log('Institution: ', account.institution_id);
523
+ console.log('Created: ', new Date(account.created).toLocaleString());
524
+ console.log('Status: ', account.status);
525
+ } catch (error) {
526
+ printError(error.message);
527
+ process.exit(1);
528
+ }
529
+ });
530
+
531
+ accountsCmd
532
+ .command('balances <account-id>')
533
+ .description('Get account balances')
534
+ .option('--json', 'Output as JSON')
535
+ .action(async (accountId, options) => {
536
+ requireAuth();
537
+ try {
538
+ const data = await withSpinner('Fetching balances...', () => getAccountBalances(accountId));
539
+
540
+ if (options.json) {
541
+ printJson(data);
542
+ return;
543
+ }
544
+
545
+ const balances = data.balances || [];
546
+ if (balances.length === 0) {
547
+ console.log(chalk.yellow('No balances found.'));
548
+ return;
549
+ }
550
+
551
+ console.log(chalk.bold('\nAccount Balances\n'));
552
+ balances.forEach(balance => {
553
+ console.log(`${balance.balanceType || 'Balance'}:`);
554
+ console.log(` Amount: ${balance.balanceAmount?.amount} ${balance.balanceAmount?.currency}`);
555
+ console.log(` Date: ${balance.referenceDate || 'N/A'}`);
556
+ console.log('');
557
+ });
558
+ } catch (error) {
559
+ printError(error.message);
560
+ process.exit(1);
561
+ }
562
+ });
563
+
564
+ accountsCmd
565
+ .command('details <account-id>')
566
+ .description('Get account details')
567
+ .option('--json', 'Output as JSON')
568
+ .action(async (accountId, options) => {
569
+ requireAuth();
570
+ try {
571
+ const data = await withSpinner('Fetching account details...', () => getAccountDetails(accountId));
572
+
573
+ if (options.json) {
574
+ printJson(data);
575
+ return;
576
+ }
577
+
578
+ const account = data.account || {};
579
+ console.log(chalk.bold('\nAccount Details\n'));
580
+ console.log('IBAN: ', account.iban || 'N/A');
581
+ console.log('Name: ', account.name || 'N/A');
582
+ console.log('Currency: ', account.currency || 'N/A');
583
+ console.log('Owner Name: ', account.ownerName || 'N/A');
584
+ console.log('Product: ', account.product || 'N/A');
585
+ } catch (error) {
586
+ printError(error.message);
587
+ process.exit(1);
588
+ }
589
+ });
590
+
591
+ accountsCmd
592
+ .command('transactions <account-id>')
593
+ .description('Get account transactions')
594
+ .option('--json', 'Output as JSON')
595
+ .action(async (accountId, options) => {
596
+ requireAuth();
597
+ try {
598
+ const data = await withSpinner('Fetching transactions...', () => getAccountTransactions(accountId));
599
+
600
+ if (options.json) {
601
+ printJson(data);
602
+ return;
603
+ }
604
+
605
+ const transactions = data.transactions?.booked || [];
606
+ if (transactions.length === 0) {
607
+ console.log(chalk.yellow('No transactions found.'));
608
+ return;
609
+ }
610
+
611
+ printTable(transactions, [
612
+ { key: 'transactionId', label: 'Transaction ID', format: (v) => v?.substring(0, 12) + '...' },
613
+ { key: 'bookingDate', label: 'Date' },
614
+ { key: 'transactionAmount', label: 'Amount', format: (v) => `${v?.amount} ${v?.currency}` },
615
+ { key: 'creditorName', label: 'Creditor', format: (v) => v || 'N/A' },
616
+ { key: 'debtorName', label: 'Debtor', format: (v) => v || 'N/A' },
617
+ { key: 'remittanceInformationUnstructured', label: 'Description', format: (v) => v?.substring(0, 30) || 'N/A' }
618
+ ]);
619
+ } catch (error) {
620
+ printError(error.message);
621
+ process.exit(1);
622
+ }
623
+ });
624
+
625
+ // ============================================================
626
+ // Parse
627
+ // ============================================================
628
+
629
+ program.parse(process.argv);
630
+
631
+ if (process.argv.length <= 2) {
632
+ program.help();
633
+ }
package/.env.example DELETED
@@ -1,11 +0,0 @@
1
- # Nordigen API Credentials
2
- # Get these from https://nordigen.com dashboard
3
-
4
- NORDIGEN_SECRET_ID=your_secret_id_here
5
- NORDIGEN_SECRET_KEY=your_secret_key_here
6
-
7
- # Optional: Default country for commands
8
- NORDIGEN_DEFAULT_COUNTRY=GB
9
-
10
- # Optional: Enable debug mode
11
- # DEBUG=1
package/.eslintrc.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "env": {
3
- "es2021": true,
4
- "node": true
5
- },
6
- "extends": "eslint:recommended",
7
- "parserOptions": {
8
- "ecmaVersion": "latest",
9
- "sourceType": "module"
10
- },
11
- "rules": {
12
- "indent": ["error", 2],
13
- "linebreak-style": ["error", "unix"],
14
- "quotes": ["error", "single"],
15
- "semi": ["error", "always"]
16
- }
17
- }