heicat-cli 0.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,655 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { createServer } from 'http';
4
+ import { readFileSync, watch as fsWatch, readdirSync } from 'fs';
5
+ import { resolve, extname } from 'path';
6
+ import { ContractEngine, loadContracts } from 'heicat-core';
7
+
8
+ export function watchCommand(options: { contractsPath: string; port: string }) {
9
+ const contractsPath = resolve(process.cwd(), options.contractsPath);
10
+ const port = parseInt(options.port);
11
+
12
+ console.log(chalk.blue('šŸš€ Starting Heicat Watch Mode'));
13
+ console.log(chalk.gray(`šŸ“ Watching: ${contractsPath}`));
14
+ console.log(chalk.gray(`🌐 GUI Server: http://localhost:${port}`));
15
+
16
+ let engine: ContractEngine;
17
+ let contracts: any[] = [];
18
+
19
+ // Initialize engine
20
+ const initEngine = async () => {
21
+ try {
22
+ contracts = await loadContracts(contractsPath);
23
+ engine = new ContractEngine('dev');
24
+ await engine.loadContracts(contractsPath);
25
+ console.log(chalk.green(`āœ… Loaded ${contracts.length} contracts`));
26
+ } catch (error) {
27
+ console.error(chalk.red('āŒ Failed to initialize:', error));
28
+ process.exit(1);
29
+ }
30
+ };
31
+
32
+ // Start GUI server
33
+ const server = createServer((req, res) => {
34
+ if (req.url === '/' || req.url === '/index.html') {
35
+ res.writeHead(200, { 'Content-Type': 'text/html' });
36
+ res.end(generateHTML());
37
+ } else if (req.url === '/api/violations') {
38
+ res.writeHead(200, { 'Content-Type': 'application/json' });
39
+ res.end(JSON.stringify({
40
+ violations: engine?.getViolations() || [],
41
+ stats: engine?.getStats() || { total: 0, errors: 0, warnings: 0 }
42
+ }));
43
+ } else if (req.url === '/api/contracts') {
44
+ res.writeHead(200, { 'Content-Type': 'application/json' });
45
+ res.end(JSON.stringify(contracts));
46
+ } else {
47
+ res.writeHead(404);
48
+ res.end('Not found');
49
+ }
50
+ });
51
+
52
+ server.listen(port, async () => {
53
+ await initEngine();
54
+
55
+ // Watch for contract file changes
56
+ fsWatch(contractsPath, { recursive: true }, async (eventType, filename) => {
57
+ if (filename && extname(filename) === '.json' && filename.endsWith('.contract.json')) {
58
+ console.log(chalk.blue(`šŸ“ Contract changed: ${filename}`));
59
+ try {
60
+ await initEngine();
61
+ console.log(chalk.green('āœ… Reloaded contracts'));
62
+ } catch (error) {
63
+ console.error(chalk.red('āŒ Failed to reload contracts:', error));
64
+ }
65
+ }
66
+ });
67
+
68
+ console.log(chalk.green(`\nšŸŽ‰ Heicat watch mode active!`));
69
+ console.log(chalk.gray(`Open http://localhost:${port} to see violations`));
70
+ });
71
+
72
+ // Handle graceful shutdown
73
+ process.on('SIGINT', () => {
74
+ console.log(chalk.blue('\nšŸ‘‹ Shutting down Heicat watch mode'));
75
+ server.close();
76
+ process.exit(0);
77
+ });
78
+ }
79
+
80
+ function generateHTML(): string {
81
+ return `
82
+ <!DOCTYPE html>
83
+ <html lang="en">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>Heicat - Violations Dashboard</title>
88
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
89
+ <style>
90
+ :root {
91
+ --background: 222.2 84% 4.9%;
92
+ --foreground: 210 40% 98%;
93
+ --card: 222.2 84% 4.9%;
94
+ --card-foreground: 210 40% 98%;
95
+ --popover: 222.2 84% 4.9%;
96
+ --popover-foreground: 210 40% 98%;
97
+ --primary: 210 40% 96%;
98
+ --primary-foreground: 222.2 84% 4.9%;
99
+ --secondary: 217.2 32.6% 17.5%;
100
+ --secondary-foreground: 210 40% 98%;
101
+ --muted: 217.2 32.6% 17.5%;
102
+ --muted-foreground: 215 20.2% 65.1%;
103
+ --accent: 217.2 32.6% 17.5%;
104
+ --accent-foreground: 210 40% 98%;
105
+ --destructive: 0 62.8% 30.6%;
106
+ --destructive-foreground: 210 40% 98%;
107
+ --border: 217.2 32.6% 17.5%;
108
+ --input: 217.2 32.6% 17.5%;
109
+ --ring: 212.7 26.8% 83.9%;
110
+ --radius: 0.5rem;
111
+ }
112
+
113
+ * {
114
+ margin: 0;
115
+ padding: 0;
116
+ box-sizing: border-box;
117
+ }
118
+
119
+ html {
120
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
121
+ }
122
+
123
+ body {
124
+ background-color: hsl(var(--background));
125
+ color: hsl(var(--foreground));
126
+ font-feature-settings: "rlig" 1, "calt" 1;
127
+ line-height: 1.5;
128
+ min-height: 100vh;
129
+ }
130
+
131
+ .container {
132
+ max-width: 1200px;
133
+ margin: 0 auto;
134
+ padding: 1.5rem;
135
+ }
136
+
137
+ /* Header */
138
+ .header {
139
+ background: hsl(var(--background));
140
+ border-bottom: 1px solid hsl(var(--border));
141
+ padding: 2rem 0;
142
+ margin-bottom: 2rem;
143
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
144
+ }
145
+
146
+ .header-content {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: space-between;
150
+ gap: 2rem;
151
+ }
152
+
153
+ .header-left {
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 0.5rem;
157
+ }
158
+
159
+ .header-title {
160
+ font-size: 2rem;
161
+ font-weight: 700;
162
+ color: hsl(var(--foreground));
163
+ letter-spacing: -0.025em;
164
+ }
165
+
166
+ .header-subtitle {
167
+ font-size: 1rem;
168
+ color: hsl(var(--muted-foreground));
169
+ font-weight: 400;
170
+ }
171
+
172
+ .header-actions {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 1rem;
176
+ }
177
+
178
+ .status-indicator {
179
+ display: inline-flex;
180
+ align-items: center;
181
+ gap: 0.5rem;
182
+ padding: 0.5rem 1rem;
183
+ border-radius: calc(var(--radius) - 0.125rem);
184
+ font-size: 0.875rem;
185
+ font-weight: 500;
186
+ background: hsl(142.1 76.2% 36.3% / 0.1);
187
+ color: hsl(142.1 76.2% 36.3%);
188
+ border: 1px solid hsl(142.1 76.2% 36.3% / 0.2);
189
+ }
190
+
191
+ .status-dot {
192
+ width: 0.5rem;
193
+ height: 0.5rem;
194
+ border-radius: 50%;
195
+ background: hsl(142.1 76.2% 36.3%);
196
+ animation: pulse 2s infinite;
197
+ }
198
+
199
+ @keyframes pulse {
200
+ 0%, 100% { opacity: 1; }
201
+ 50% { opacity: 0.5; }
202
+ }
203
+
204
+ /* Stats Grid */
205
+ .stats-grid {
206
+ display: grid;
207
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
208
+ gap: 1.5rem;
209
+ margin-bottom: 2rem;
210
+ }
211
+
212
+ .stat-card {
213
+ background: hsl(var(--card));
214
+ border: 1px solid hsl(var(--border));
215
+ border-radius: calc(var(--radius) + 0.125rem);
216
+ padding: 2rem;
217
+ transition: all 0.2s ease-in-out;
218
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
219
+ position: relative;
220
+ overflow: hidden;
221
+ }
222
+
223
+ .stat-card::before {
224
+ content: '';
225
+ position: absolute;
226
+ top: 0;
227
+ left: 0;
228
+ right: 0;
229
+ height: 4px;
230
+ background: linear-gradient(90deg, hsl(var(--primary)) 0%, hsl(var(--primary) / 0.8) 100%);
231
+ }
232
+
233
+ .stat-card:hover {
234
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
235
+ transform: translateY(-2px);
236
+ }
237
+
238
+ .stat-content {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: 0.5rem;
242
+ }
243
+
244
+ .stat-number {
245
+ font-size: 3rem;
246
+ font-weight: 800;
247
+ line-height: 1;
248
+ color: hsl(var(--foreground));
249
+ font-variant-numeric: tabular-nums;
250
+ }
251
+
252
+ .stat-label {
253
+ font-size: 0.875rem;
254
+ font-weight: 600;
255
+ color: hsl(var(--muted-foreground));
256
+ text-transform: uppercase;
257
+ letter-spacing: 0.1em;
258
+ margin: 0;
259
+ }
260
+
261
+ .stat-description {
262
+ font-size: 0.75rem;
263
+ color: hsl(var(--muted-foreground));
264
+ font-weight: 400;
265
+ margin: 0;
266
+ }
267
+
268
+ /* Violations Section */
269
+ .violations-section {
270
+ background: hsl(var(--card));
271
+ border: 1px solid hsl(var(--border));
272
+ border-radius: calc(var(--radius) + 0.125rem);
273
+ overflow: hidden;
274
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
275
+ }
276
+
277
+ .violations-header {
278
+ padding: 2rem;
279
+ border-bottom: 1px solid hsl(var(--border));
280
+ background: hsl(var(--muted) / 0.2);
281
+ }
282
+
283
+ .violations-title {
284
+ font-size: 1.5rem;
285
+ font-weight: 700;
286
+ color: hsl(var(--foreground));
287
+ letter-spacing: -0.025em;
288
+ }
289
+
290
+ .violations-list {
291
+ max-height: 600px;
292
+ overflow-y: auto;
293
+ }
294
+
295
+ .violation-item {
296
+ padding: 2rem;
297
+ border-bottom: 1px solid hsl(var(--border));
298
+ transition: background-color 0.15s ease;
299
+ }
300
+
301
+ .violation-item:last-child {
302
+ border-bottom: none;
303
+ }
304
+
305
+ .violation-item:hover {
306
+ background: hsl(var(--muted) / 0.3);
307
+ }
308
+
309
+ .violation-header {
310
+ display: flex;
311
+ align-items: flex-start;
312
+ justify-content: space-between;
313
+ gap: 1.5rem;
314
+ margin-bottom: 1rem;
315
+ }
316
+
317
+ .violation-endpoint {
318
+ display: flex;
319
+ flex-direction: column;
320
+ gap: 0.25rem;
321
+ flex: 1;
322
+ }
323
+
324
+ .violation-method {
325
+ font-size: 0.75rem;
326
+ font-weight: 700;
327
+ padding: 0.25rem 0.75rem;
328
+ background: hsl(var(--primary));
329
+ color: hsl(var(--primary-foreground));
330
+ border-radius: calc(var(--radius) - 0.125rem);
331
+ text-transform: uppercase;
332
+ letter-spacing: 0.1em;
333
+ width: fit-content;
334
+ }
335
+
336
+ .violation-path {
337
+ font-size: 1rem;
338
+ font-weight: 600;
339
+ color: hsl(var(--foreground));
340
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
341
+ letter-spacing: -0.025em;
342
+ }
343
+
344
+ .violation-severity {
345
+ padding: 0.5rem 1rem;
346
+ border-radius: calc(var(--radius) - 0.125rem);
347
+ font-size: 0.75rem;
348
+ font-weight: 700;
349
+ text-transform: uppercase;
350
+ letter-spacing: 0.1em;
351
+ flex-shrink: 0;
352
+ border: 1px solid;
353
+ }
354
+
355
+ .severity-error {
356
+ background: hsl(0 84.2% 60.2% / 0.05);
357
+ color: hsl(var(--destructive));
358
+ border-color: hsl(0 84.2% 60.2% / 0.2);
359
+ }
360
+
361
+ .severity-warning {
362
+ background: hsl(38 92% 50% / 0.05);
363
+ color: hsl(38 92% 50%);
364
+ border-color: hsl(38 92% 50% / 0.2);
365
+ }
366
+
367
+ .violation-message {
368
+ font-size: 0.875rem;
369
+ color: hsl(var(--muted-foreground));
370
+ line-height: 1.6;
371
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
372
+ background: hsl(var(--muted) / 0.4);
373
+ padding: 1rem;
374
+ border-radius: calc(var(--radius) - 0.125rem);
375
+ border: 1px solid hsl(var(--border));
376
+ margin-top: 0.5rem;
377
+ }
378
+
379
+ .no-violations {
380
+ text-align: center;
381
+ padding: 4rem 2rem;
382
+ color: hsl(var(--muted-foreground));
383
+ }
384
+
385
+ .no-violations-icon {
386
+ font-size: 3rem;
387
+ margin-bottom: 1.5rem;
388
+ display: block;
389
+ opacity: 0.5;
390
+ }
391
+
392
+ .no-violations-title {
393
+ font-size: 1.25rem;
394
+ font-weight: 700;
395
+ color: hsl(var(--foreground));
396
+ margin-bottom: 0.75rem;
397
+ letter-spacing: -0.025em;
398
+ }
399
+
400
+ .no-violations-text {
401
+ font-size: 0.875rem;
402
+ line-height: 1.5;
403
+ max-width: 400px;
404
+ margin: 0 auto;
405
+ }
406
+
407
+ /* Buttons */
408
+ .btn {
409
+ display: inline-flex;
410
+ align-items: center;
411
+ justify-content: center;
412
+ gap: 0.5rem;
413
+ padding: 0.5rem 1rem;
414
+ border-radius: calc(var(--radius) - 0.125rem);
415
+ font-size: 0.875rem;
416
+ font-weight: 500;
417
+ transition: all 0.2s ease-in-out;
418
+ cursor: pointer;
419
+ border: 1px solid hsl(var(--border));
420
+ background: hsl(var(--background));
421
+ color: hsl(var(--foreground));
422
+ text-decoration: none;
423
+ white-space: nowrap;
424
+ }
425
+
426
+ .btn:hover {
427
+ background: hsl(var(--muted));
428
+ }
429
+
430
+ .btn:focus-visible {
431
+ outline: 2px solid hsl(var(--ring));
432
+ outline-offset: 2px;
433
+ }
434
+
435
+ .btn-primary {
436
+ background: hsl(var(--primary));
437
+ color: hsl(var(--primary-foreground));
438
+ border-color: hsl(var(--primary));
439
+ }
440
+
441
+ .btn-primary:hover {
442
+ background: hsl(var(--primary) / 0.9);
443
+ border-color: hsl(var(--primary) / 0.9);
444
+ }
445
+
446
+ .btn-ghost {
447
+ border-color: transparent;
448
+ background: transparent;
449
+ }
450
+
451
+ .btn-ghost:hover {
452
+ background: hsl(var(--muted));
453
+ }
454
+
455
+ /* Responsive */
456
+ @media (max-width: 768px) {
457
+ .container {
458
+ padding: 1rem;
459
+ }
460
+
461
+ .header-content {
462
+ flex-direction: column;
463
+ align-items: flex-start;
464
+ gap: 1rem;
465
+ }
466
+
467
+ .stats-grid {
468
+ grid-template-columns: 1fr;
469
+ gap: 1rem;
470
+ }
471
+
472
+ .violation-header {
473
+ flex-direction: column;
474
+ align-items: flex-start;
475
+ gap: 0.75rem;
476
+ }
477
+
478
+ .violation-endpoint {
479
+ flex-direction: column;
480
+ align-items: flex-start;
481
+ gap: 0.5rem;
482
+ }
483
+ }
484
+
485
+ /* Scrollbar */
486
+ .violations-list::-webkit-scrollbar {
487
+ width: 6px;
488
+ }
489
+
490
+ .violations-list::-webkit-scrollbar-track {
491
+ background: hsl(var(--muted));
492
+ border-radius: 3px;
493
+ }
494
+
495
+ .violations-list::-webkit-scrollbar-thumb {
496
+ background: hsl(var(--muted-foreground) / 0.3);
497
+ border-radius: 3px;
498
+ }
499
+
500
+ .violations-list::-webkit-scrollbar-thumb:hover {
501
+ background: hsl(var(--muted-foreground) / 0.5);
502
+ }
503
+ </style>
504
+ </head>
505
+ <body>
506
+ <header class="header">
507
+ <div class="container">
508
+ <div class="header-content">
509
+ <div class="header-left">
510
+ <h1 class="header-title">Heicat</h1>
511
+ <p class="header-subtitle">Runtime API Contract Enforcement Dashboard</p>
512
+ <div class="status-indicator">
513
+ <div class="status-dot"></div>
514
+ <span>Monitoring Active</span>
515
+ </div>
516
+ </div>
517
+ <div class="header-actions">
518
+ <button class="btn btn-ghost" onclick="clearViolations()">
519
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
520
+ <polyline points="3,6 5,6 21,6"></polyline>
521
+ <path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"></path>
522
+ <line x1="10" y1="11" x2="10" y2="17"></line>
523
+ <line x1="14" y1="11" x2="14" y2="17"></line>
524
+ </svg>
525
+ Clear Logs
526
+ </button>
527
+ <button class="btn btn-primary" onclick="loadData()">
528
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
529
+ <polyline points="23,4 23,10 17,10"></polyline>
530
+ <polyline points="1,20 1,14 7,14"></polyline>
531
+ <path d="M20.49,9A9,9,0,0,0,5.64,5.64L1,10m22,4l-4.64,4.36A9,9,0,0,1,3.51,15"></path>
532
+ </svg>
533
+ Refresh Data
534
+ </button>
535
+ </div>
536
+ </div>
537
+ </div>
538
+ </header>
539
+
540
+ <main class="container">
541
+ <div class="stats-grid" id="stats"></div>
542
+
543
+ <section class="violations-section">
544
+ <div class="violations-header">
545
+ <h2 class="violations-title">Contract Validation Log</h2>
546
+ </div>
547
+ <div class="violations-list" id="violations-list"></div>
548
+ </section>
549
+ </main>
550
+
551
+ <script>
552
+ let violations = [];
553
+ let stats = { total: 0, errors: 0, warnings: 0 };
554
+
555
+ async function loadData() {
556
+ try {
557
+ const response = await fetch('http://localhost:3000/api/violations');
558
+ const data = await response.json();
559
+ violations = data.violations || [];
560
+ // Parse API stats format to GUI format
561
+ const apiStats = data.stats || {};
562
+ stats = {
563
+ total: apiStats.total || 0,
564
+ errors: (apiStats.bySeverity && apiStats.bySeverity.error) || 0,
565
+ warnings: (apiStats.bySeverity && apiStats.bySeverity.warning) || 0
566
+ };
567
+ renderStats();
568
+ renderViolations();
569
+ } catch (error) {
570
+ console.error('Failed to load data:', error);
571
+ // Show error state in GUI
572
+ violations = [];
573
+ stats = { total: 0, errors: 0, warnings: 0 };
574
+ renderStats();
575
+ renderViolations();
576
+ }
577
+ }
578
+
579
+ function renderStats() {
580
+ document.getElementById('stats').innerHTML = \`
581
+ <div class="stat-card">
582
+ <div class="stat-content">
583
+ <div class="stat-number">\${stats.total}</div>
584
+ <h3 class="stat-label">Total Violations</h3>
585
+ <p class="stat-description">All contract validation issues</p>
586
+ </div>
587
+ </div>
588
+ <div class="stat-card">
589
+ <div class="stat-content">
590
+ <div class="stat-number" style="color: hsl(var(--destructive));">\${stats.errors}</div>
591
+ <h3 class="stat-label">Critical Errors</h3>
592
+ <p class="stat-description">Requests blocked by validation</p>
593
+ </div>
594
+ </div>
595
+ <div class="stat-card">
596
+ <div class="stat-content">
597
+ <div class="stat-number" style="color: hsl(38 92% 50%);">\${stats.warnings}</div>
598
+ <h3 class="stat-label">Response Warnings</h3>
599
+ <p class="stat-description">Schema compliance issues</p>
600
+ </div>
601
+ </div>
602
+ \`;
603
+ }
604
+
605
+ async function clearViolations() {
606
+ try {
607
+ // In a real implementation, this would call an API endpoint to clear violations
608
+ // For now, we'll just refresh the data
609
+ violations = [];
610
+ stats = { total: 0, errors: 0, warnings: 0 };
611
+ renderStats();
612
+ renderViolations();
613
+ console.log('Violations cleared');
614
+ } catch (error) {
615
+ console.error('Failed to clear violations:', error);
616
+ }
617
+ }
618
+
619
+ function renderViolations() {
620
+ const list = document.getElementById('violations-list');
621
+
622
+ if (violations.length === 0) {
623
+ list.innerHTML = \`
624
+ <div class="no-violations">
625
+ <span class="no-violations-icon">āœ“</span>
626
+ <div class="no-violations-title">System Status: Normal</div>
627
+ <div class="no-violations-text">All API contracts are being enforced correctly. No validation issues detected.</div>
628
+ </div>
629
+ \`;
630
+ return;
631
+ }
632
+
633
+ list.innerHTML = violations.slice(0, 50).map(v => \`
634
+ <div class="violation-item">
635
+ <div class="violation-header">
636
+ <div class="violation-endpoint">
637
+ <span class="violation-method">\${v.endpoint.method}</span>
638
+ <span class="violation-path">\${v.endpoint.path}</span>
639
+ </div>
640
+ <span class="violation-severity severity-\${v.severity}">\${v.severity}</span>
641
+ </div>
642
+ <div class="violation-message">\${v.message}</div>
643
+ </div>
644
+ \`).join('');
645
+ }
646
+
647
+ // Auto-refresh every 3 seconds
648
+ setInterval(loadData, 3000);
649
+
650
+ // Initial load
651
+ loadData();
652
+ </script>
653
+ </body>
654
+ </html>`;
655
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { initCommand } from './commands/init';
2
+ export { validateCommand } from './commands/validate';
3
+ export { statusCommand } from './commands/status';
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
18
+ }