kailogger 1.0.0-dark.red → 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.
Files changed (102) hide show
  1. package/README.md +195 -53
  2. package/dist/core/Config.d.ts +15 -0
  3. package/dist/core/Config.js +44 -0
  4. package/dist/core/Logger.d.ts +75 -4
  5. package/dist/core/Logger.js +375 -47
  6. package/dist/core/Scope.d.ts +13 -0
  7. package/dist/core/Scope.js +36 -0
  8. package/dist/features/Chart.d.ts +15 -0
  9. package/dist/features/Chart.js +64 -0
  10. package/dist/features/Diff.d.ts +3 -0
  11. package/dist/features/Diff.js +30 -0
  12. package/dist/features/Encrypt.d.ts +10 -0
  13. package/dist/features/Encrypt.js +47 -0
  14. package/dist/features/Notify.d.ts +14 -0
  15. package/dist/features/Notify.js +70 -0
  16. package/dist/features/Screenshot.d.ts +10 -0
  17. package/dist/features/Screenshot.js +106 -0
  18. package/dist/features/Sound.d.ts +12 -0
  19. package/dist/features/Sound.js +116 -0
  20. package/dist/features/Timer.d.ts +6 -0
  21. package/dist/features/Timer.js +38 -0
  22. package/dist/features/Tree.d.ts +7 -0
  23. package/dist/features/Tree.js +25 -0
  24. package/dist/features/index.d.ts +8 -0
  25. package/dist/features/index.js +24 -0
  26. package/dist/icon/logo.png +0 -0
  27. package/dist/index.d.ts +13 -1
  28. package/dist/index.js +21 -1
  29. package/dist/sounds/error.wav +0 -0
  30. package/dist/sounds/notification.wav +0 -0
  31. package/dist/sounds/success.wav +0 -0
  32. package/dist/sounds/warning.wav +0 -0
  33. package/dist/styles/KaiChroma.d.ts +85 -0
  34. package/dist/styles/KaiChroma.js +407 -0
  35. package/dist/styles/gradients.d.ts +28 -0
  36. package/dist/styles/palettes.d.ts +21 -26
  37. package/dist/styles/palettes.js +167 -13
  38. package/dist/transports/ConsoleTransport.d.ts +9 -0
  39. package/dist/transports/ConsoleTransport.js +18 -0
  40. package/dist/transports/FileTransport.d.ts +16 -0
  41. package/dist/transports/FileTransport.js +84 -0
  42. package/dist/transports/WebhookTransport.d.ts +15 -0
  43. package/dist/transports/WebhookTransport.js +31 -0
  44. package/dist/transports/index.d.ts +3 -0
  45. package/dist/transports/index.js +19 -0
  46. package/dist/types/index.d.ts +16 -0
  47. package/dist/types/index.js +11 -0
  48. package/dist/utils/json.d.ts +3 -0
  49. package/dist/utils/json.js +33 -0
  50. package/dist/utils/prettyError.d.ts +3 -0
  51. package/dist/utils/prettyError.js +94 -0
  52. package/dist/utils/progress.d.ts +11 -0
  53. package/dist/utils/progress.js +43 -0
  54. package/dist/utils/prompt.d.ts +4 -0
  55. package/dist/utils/prompt.js +59 -0
  56. package/dist/utils/selection.d.ts +4 -0
  57. package/dist/utils/selection.js +156 -0
  58. package/dist/utils/spinner.d.ts +1 -1
  59. package/dist/utils/spinner.js +9 -13
  60. package/dist/utils/stripAnsi.d.ts +1 -0
  61. package/dist/utils/stripAnsi.js +7 -0
  62. package/dist/utils/table.d.ts +3 -0
  63. package/dist/utils/table.js +35 -0
  64. package/examples/demo.js +134 -0
  65. package/examples/demo.ts +88 -25
  66. package/package.json +20 -6
  67. package/scripts/copy-assets.js +37 -0
  68. package/src/core/Config.ts +44 -0
  69. package/src/core/Logger.ts +427 -51
  70. package/src/core/Scope.ts +35 -0
  71. package/src/features/Chart.ts +81 -0
  72. package/src/features/Diff.ts +25 -0
  73. package/src/features/Encrypt.ts +47 -0
  74. package/src/features/Notify.ts +39 -0
  75. package/src/features/Screenshot.ts +70 -0
  76. package/src/features/Sound.ts +92 -0
  77. package/src/features/Timer.ts +35 -0
  78. package/src/features/Tree.ts +25 -0
  79. package/src/features/index.ts +8 -0
  80. package/src/icon/logo.png +0 -0
  81. package/src/index.ts +13 -1
  82. package/src/sounds/error.wav +0 -0
  83. package/src/sounds/notification.wav +0 -0
  84. package/src/sounds/success.wav +0 -0
  85. package/src/sounds/warning.wav +0 -0
  86. package/src/styles/KaiChroma.ts +370 -0
  87. package/src/styles/palettes.ts +197 -14
  88. package/src/transports/ConsoleTransport.ts +19 -0
  89. package/src/transports/FileTransport.ts +55 -0
  90. package/src/transports/WebhookTransport.ts +37 -0
  91. package/src/transports/index.ts +3 -0
  92. package/src/types/cli-highlight.d.ts +3 -0
  93. package/src/types/index.ts +23 -0
  94. package/src/utils/json.ts +33 -0
  95. package/src/utils/prettyError.ts +65 -0
  96. package/src/utils/progress.ts +56 -0
  97. package/src/utils/prompt.ts +27 -0
  98. package/src/utils/selection.ts +136 -0
  99. package/src/utils/spinner.ts +11 -7
  100. package/src/utils/stripAnsi.ts +6 -0
  101. package/src/utils/table.ts +38 -0
  102. package/src/styles/gradients.ts +0 -22
@@ -0,0 +1,37 @@
1
+ import { LogLevel, Transport } from '../types';
2
+ import { stripAnsi } from '../utils/stripAnsi';
3
+
4
+ export interface WebhookTransportOptions {
5
+ url: string;
6
+ method?: 'POST' | 'PUT';
7
+ headers?: Record<string, string>;
8
+ minLevel?: LogLevel;
9
+ }
10
+ export class WebhookTransport implements Transport {
11
+ name = 'webhook';
12
+ private url: string;
13
+ private method: 'POST' | 'PUT';
14
+ private headers: Record<string, string>;
15
+ constructor(options: WebhookTransportOptions) {
16
+ this.url = options.url;
17
+ this.method = options.method || 'POST';
18
+ this.headers = options.headers || { 'Content-Type': 'application/json' };
19
+ }
20
+ async log(level: LogLevel, message: string, meta?: any): Promise<void> {
21
+ try {
22
+ const payload = {
23
+ timestamp: new Date().toISOString(),
24
+ level: level.toUpperCase(),
25
+ message: stripAnsi(message),
26
+ meta
27
+ };
28
+ await fetch(this.url, {
29
+ method: this.method,
30
+ headers: this.headers,
31
+ body: JSON.stringify(payload)
32
+ });
33
+ } catch (e) {
34
+ console.error('[KaiLogger] Webhook transport failed:', e);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ export * from './ConsoleTransport';
2
+ export * from './FileTransport';
3
+ export * from './WebhookTransport';
@@ -0,0 +1,3 @@
1
+ declare module 'cli-highlight' {
2
+ export function highlight(code: string, options?: any): string;
3
+ }
@@ -0,0 +1,23 @@
1
+ export type LogLevel = 'debug' | 'info' | 'success' | 'warning' | 'error' | 'silent';
2
+ export const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
3
+ debug: 0,
4
+ info: 1,
5
+ success: 2,
6
+ warning: 3,
7
+ error: 4,
8
+ silent: 5
9
+ };
10
+ export interface KaiConfig {
11
+ theme?: string;
12
+ level?: LogLevel;
13
+ timestamp?: 'ISO' | 'locale' | 'relative' | 'none';
14
+ silent?: boolean;
15
+ transports?: Transport[];
16
+ }
17
+ export interface Transport {
18
+ name: string;
19
+ log(level: LogLevel, message: string, meta?: any): void | Promise<void>;
20
+ }
21
+ export interface Formatter {
22
+ format(level: LogLevel, message: string, timestamp: string, scope?: string): string;
23
+ }
@@ -0,0 +1,33 @@
1
+
2
+ import { KaiChroma } from '../styles/KaiChroma';
3
+
4
+ export class KaiJson {
5
+ static print(obj: any, theme: any) {
6
+ const jsonStr = JSON.stringify(obj, null, 2);
7
+
8
+ // Manual colorization for full control using KaiChroma
9
+ const colored = jsonStr
10
+ // Strings (keys)
11
+ .replace(/"([^"]+)":/g, (match, key) => {
12
+ return KaiChroma.hex(theme.info[0], `"${key}"`) + ':';
13
+ })
14
+ // String values
15
+ .replace(/: "([^"]*)"/g, (match, val) => {
16
+ return ': ' + KaiChroma.hex(theme.success[1], `"${val}"`);
17
+ })
18
+ // Numbers
19
+ .replace(/: (\d+\.?\d*)/g, (match, num) => {
20
+ return ': ' + KaiChroma.hex(theme.warning[0], num);
21
+ })
22
+ // Booleans
23
+ .replace(/: (true|false)/g, (match, bool) => {
24
+ return ': ' + KaiChroma.hex(theme.error[0], bool);
25
+ })
26
+ // Null
27
+ .replace(/: (null)/g, (match, n) => {
28
+ return ': ' + KaiChroma.hex(theme.dim, n);
29
+ });
30
+
31
+ console.log(colored);
32
+ }
33
+ }
@@ -0,0 +1,65 @@
1
+
2
+ import * as stackTrace from 'stack-trace';
3
+ import * as fs from 'fs';
4
+ import { KaiChroma } from '../styles/KaiChroma';
5
+ import { paint } from '../styles/palettes';
6
+
7
+ export class PrettyError {
8
+ static handle(error: Error, theme: any) {
9
+ const trace = stackTrace.parse(error);
10
+
11
+ console.log('');
12
+ console.log(paint.apply(` 💥 ${error.name} `, theme.error));
13
+ console.log(KaiChroma.bold(error.message));
14
+
15
+ // Find the first relevant frame (not node internal)
16
+ const frame = trace.find(t => {
17
+ const file = t.getFileName();
18
+ return file && !file.includes('node_modules') && !file.startsWith('node:');
19
+ });
20
+
21
+ if (frame) {
22
+ const fileName = frame.getFileName();
23
+ const lineNumber = frame.getLineNumber();
24
+
25
+ console.log(KaiChroma.hex('#666666', `at ${fileName}:${lineNumber}`));
26
+ console.log(KaiChroma.hex('#666666', '─'.repeat(50)));
27
+
28
+ try {
29
+ const content = fs.readFileSync(fileName, 'utf-8');
30
+ const lines = content.split('\n');
31
+ const start = Math.max(0, lineNumber - 3);
32
+ const end = Math.min(lines.length, lineNumber + 2);
33
+
34
+ for (let i = start; i < end; i++) {
35
+ const isErrorLine = i + 1 === lineNumber;
36
+ const lineNumStr = (i + 1).toString().padEnd(4);
37
+
38
+ if (isErrorLine) {
39
+ const lineContent = KaiChroma.bold(lines[i]);
40
+ console.log(paint.apply(` > ${lineNumStr} | ${lineContent}`, theme.error));
41
+ } else {
42
+ console.log(KaiChroma.hex('#888888', ` ${lineNumStr} | ${lines[i]}`));
43
+ }
44
+ }
45
+ } catch (e) {
46
+ // Fallback if file cannot be read
47
+ console.log(KaiChroma.dim(' (Source code unavailable)'));
48
+ }
49
+ console.log(KaiChroma.hex('#666666', '─'.repeat(50)));
50
+ }
51
+
52
+ // Show simplified stack
53
+ console.log('');
54
+ console.log(KaiChroma.dim('Stack Trace:'));
55
+ trace.forEach(t => {
56
+ const fn = t.getFunctionName() || '<anonymous>';
57
+ const file = t.getFileName();
58
+ const line = t.getLineNumber();
59
+ if (file && !file.includes('node_modules')) {
60
+ console.log(KaiChroma.dim(` at ${fn} (${file}:${line})`));
61
+ }
62
+ });
63
+ console.log('');
64
+ }
65
+ }
@@ -0,0 +1,56 @@
1
+
2
+ import { KaiChroma } from '../styles/KaiChroma';
3
+ import { palettes, ThemeName } from '../styles/palettes';
4
+
5
+ export class KaiProgress {
6
+ private total: number;
7
+ private current: number;
8
+ private width: number;
9
+ private theme: ThemeName;
10
+
11
+ constructor(total: number, width: number = 40, theme: ThemeName = 'zen') {
12
+ this.total = total;
13
+ this.current = 0;
14
+ this.width = width;
15
+ this.theme = theme;
16
+ }
17
+
18
+ public update(current: number) {
19
+ this.current = current;
20
+ this.render();
21
+ }
22
+
23
+ public increment(amount: number = 1) {
24
+ this.current = Math.min(this.total, this.current + amount);
25
+ this.render();
26
+ }
27
+
28
+ private render() {
29
+ const percentage = Math.min(1, this.current / this.total);
30
+ const filledWidth = Math.round(this.width * percentage);
31
+ const emptyWidth = this.width - filledWidth;
32
+
33
+ const filledChar = '█';
34
+ const emptyChar = '░';
35
+
36
+ const filled = filledChar.repeat(filledWidth);
37
+ const empty = emptyChar.repeat(emptyWidth);
38
+
39
+ const palette = palettes[this.theme];
40
+ const colors = palette.info;
41
+ const dimColor = palette.dim;
42
+
43
+ // Utilizamos KaiChroma.gradient en lugar de gradient-string
44
+ const barFilled = KaiChroma.gradient(colors, filled);
45
+ const barEmpty = KaiChroma.hex(dimColor, empty);
46
+ const bar = barFilled + barEmpty;
47
+
48
+ const percentText = Math.round(percentage * 100).toString().padStart(3);
49
+
50
+ process.stdout.write(`\r${bar} ${percentText}%`);
51
+
52
+ if (this.current >= this.total) {
53
+ process.stdout.write('\n');
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,27 @@
1
+
2
+ import * as readline from 'readline';
3
+ import { paint } from '../styles/palettes';
4
+
5
+ export class KaiPrompt {
6
+ static ask(question: string, theme: any): Promise<string> {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout
10
+ });
11
+
12
+ const q = paint.apply(`? ${question} `, theme.info);
13
+
14
+ return new Promise(resolve => {
15
+ rl.question(q, (answer) => {
16
+ rl.close();
17
+ resolve(answer);
18
+ });
19
+ });
20
+ }
21
+
22
+ static confirm(question: string, theme: any): Promise<boolean> {
23
+ return this.ask(`${question} (y/n)`, theme).then(ans => {
24
+ return ans.toLowerCase().startsWith('y');
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,136 @@
1
+
2
+ import { KaiChroma } from '../styles/KaiChroma';
3
+ import { paint } from '../styles/palettes';
4
+ import * as readline from 'readline';
5
+
6
+ export class KaiSelection {
7
+ static async select(question: string, options: string[], theme: any): Promise<string> {
8
+ return new Promise((resolve) => {
9
+ let selectedIndex = 0;
10
+ const stdin = process.stdin;
11
+ const stdout = process.stdout;
12
+
13
+ stdin.setRawMode(true);
14
+ stdin.resume();
15
+ stdin.setEncoding('utf8');
16
+
17
+ const render = () => {
18
+ readline.moveCursor(stdout, 0, -(options.length + 1));
19
+ readline.clearScreenDown(stdout);
20
+
21
+ const q = paint.apply(`? ${question} `, theme.info);
22
+ console.log(q);
23
+
24
+ options.forEach((opt, i) => {
25
+ if (i === selectedIndex) {
26
+ const pointer = paint.apply('>', theme.success);
27
+ const text = paint.apply(opt, theme.success);
28
+ console.log(`${pointer} ${text}`);
29
+ } else {
30
+ console.log(` ${opt}`);
31
+ }
32
+ });
33
+ };
34
+
35
+ console.log('\n'.repeat(options.length));
36
+ render();
37
+
38
+ const handler = (key: string) => {
39
+ if (key === '\u0003') { // Ctrl+C
40
+ process.exit();
41
+ }
42
+
43
+ if (key === '\u001b[A') { // Up arrow
44
+ selectedIndex = (selectedIndex > 0) ? selectedIndex - 1 : options.length - 1;
45
+ render();
46
+ } else if (key === '\u001b[B') { // Down arrow
47
+ selectedIndex = (selectedIndex < options.length - 1) ? selectedIndex + 1 : 0;
48
+ render();
49
+ } else if (key === '\r') { // Enter
50
+ stdin.removeListener('data', handler);
51
+ stdin.setRawMode(false);
52
+ stdin.pause();
53
+
54
+ readline.moveCursor(stdout, 0, -(options.length + 1));
55
+ readline.clearScreenDown(stdout);
56
+ console.log(`${paint.apply(`✔ ${question}`, theme.success)} ${KaiChroma.bold(options[selectedIndex])}`);
57
+
58
+ resolve(options[selectedIndex]);
59
+ }
60
+ };
61
+
62
+ stdin.on('data', handler);
63
+ });
64
+ }
65
+
66
+ static async multiselect(question: string, options: string[], theme: any): Promise<string[]> {
67
+ return new Promise((resolve) => {
68
+ let selectedIndex = 0;
69
+ const selected = new Set<number>();
70
+ const stdin = process.stdin;
71
+ const stdout = process.stdout;
72
+
73
+ stdin.setRawMode(true);
74
+ stdin.resume();
75
+ stdin.setEncoding('utf8');
76
+
77
+ const render = () => {
78
+ readline.moveCursor(stdout, 0, -(options.length + 1));
79
+ readline.clearScreenDown(stdout);
80
+
81
+ const q = paint.apply(`? ${question} `, theme.info);
82
+ console.log(`${q} ${KaiChroma.dim('(Space to select, Enter to confirm)')}`);
83
+
84
+ options.forEach((opt, i) => {
85
+ const isSelected = selected.has(i);
86
+ const isHovered = i === selectedIndex;
87
+
88
+ let prefix = isSelected ? paint.apply('◉', theme.success) : '◯';
89
+ let text = opt;
90
+
91
+ if (isHovered) {
92
+ prefix = paint.apply('>', theme.info) + ' ' + prefix;
93
+ text = paint.apply(text, theme.info);
94
+ } else {
95
+ prefix = ' ' + prefix;
96
+ }
97
+
98
+ console.log(`${prefix} ${text}`);
99
+ });
100
+ };
101
+
102
+ console.log('\n'.repeat(options.length));
103
+ render();
104
+
105
+ const handler = (key: string) => {
106
+ if (key === '\u0003') process.exit();
107
+
108
+ if (key === '\u001b[A') { // Up
109
+ selectedIndex = (selectedIndex > 0) ? selectedIndex - 1 : options.length - 1;
110
+ render();
111
+ } else if (key === '\u001b[B') { // Down
112
+ selectedIndex = (selectedIndex < options.length - 1) ? selectedIndex + 1 : 0;
113
+ render();
114
+ } else if (key === ' ') { // Space
115
+ if (selected.has(selectedIndex)) selected.delete(selectedIndex);
116
+ else selected.add(selectedIndex);
117
+ render();
118
+ } else if (key === '\r') { // Enter
119
+ stdin.removeListener('data', handler);
120
+ stdin.setRawMode(false);
121
+ stdin.pause();
122
+
123
+ readline.moveCursor(stdout, 0, -(options.length + 1));
124
+ readline.clearScreenDown(stdout);
125
+
126
+ const result = options.filter((_, i) => selected.has(i));
127
+ console.log(`${paint.apply(`✔ ${question}`, theme.success)} ${KaiChroma.bold(result.join(', '))}`);
128
+
129
+ resolve(result);
130
+ }
131
+ };
132
+
133
+ stdin.on('data', handler);
134
+ });
135
+ }
136
+ }
@@ -1,22 +1,25 @@
1
- import process from 'process';
2
- import chalk from 'chalk';
1
+
2
+ import { KaiChroma } from '../styles/KaiChroma';
3
3
 
4
4
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
+
5
6
  export class KaiSpinner {
6
7
  private timer: NodeJS.Timeout | null = null;
7
8
  private index = 0;
8
9
  private text = '';
9
- private colorFn = chalk.cyan;
10
+ private colorHex = '#00FFFF'; // Default cyan-ish
11
+
10
12
  start(text: string, colorHex: string = '#00FFFF') {
11
13
  this.stop();
12
14
  this.text = text;
13
- this.colorFn = chalk.hex(colorHex);
15
+ this.colorHex = colorHex;
14
16
  process.stdout.write('\x1B[?25l');
15
17
  this.timer = setInterval(() => {
16
18
  const frame = frames[this.index = ++this.index % frames.length];
17
- process.stdout.write(`\r${this.colorFn(frame)} ${this.text}`);
19
+ process.stdout.write(`\r${KaiChroma.hex(this.colorHex, frame)} ${this.text}`);
18
20
  }, 80);
19
21
  }
22
+
20
23
  stop(symbol: string = '✔', endText?: string, colorHex?: string) {
21
24
  if (this.timer) {
22
25
  clearInterval(this.timer);
@@ -24,11 +27,12 @@ export class KaiSpinner {
24
27
  process.stdout.write('\r\x1B[K');
25
28
  process.stdout.write('\x1B[?25h');
26
29
  if (endText) {
27
- const finalColor = colorHex ? chalk.hex(colorHex) : this.colorFn;
28
- console.log(`${finalColor(symbol)} ${endText}`);
30
+ const finalColor = colorHex || this.colorHex;
31
+ console.log(`${KaiChroma.hex(finalColor, symbol)} ${endText}`);
29
32
  }
30
33
  }
31
34
  }
35
+
32
36
  fail(text: string) {
33
37
  this.stop('✖', text, '#FF0000');
34
38
  }
@@ -0,0 +1,6 @@
1
+
2
+ import { KaiChroma } from '../styles/KaiChroma';
3
+
4
+ export function stripAnsi(str: string): string {
5
+ return KaiChroma.strip(str);
6
+ }
@@ -0,0 +1,38 @@
1
+
2
+ import { KaiChroma } from '../styles/KaiChroma';
3
+ import { paint } from '../styles/palettes';
4
+
5
+ export class KaiTable {
6
+ static print(data: any[], theme: any) {
7
+ if (data.length === 0) return;
8
+
9
+ const keys = Object.keys(data[0]);
10
+ const colWidths = keys.map(key => {
11
+ const maxValLen = Math.max(...data.map(row => String(row[key]).length));
12
+ return Math.max(key.length, maxValLen) + 2;
13
+ });
14
+
15
+ const createRow = (rowItems: string[], isHeader = false) => {
16
+ return rowItems.map((item, i) => {
17
+ const cell = item.padEnd(colWidths[i]);
18
+ return isHeader
19
+ ? paint.apply(cell, theme.info)
20
+ : cell;
21
+ }).join(' │ ');
22
+ };
23
+
24
+ const separator = colWidths.map(w => '─'.repeat(w)).join('─┼─');
25
+ const dimColor = (text: string) => KaiChroma.hex(theme.dim, text);
26
+
27
+ console.log(dimColor(separator));
28
+ console.log(createRow(keys, true));
29
+ console.log(dimColor(separator));
30
+
31
+ data.forEach(row => {
32
+ const values = keys.map(k => String(row[k]));
33
+ console.log(createRow(values));
34
+ });
35
+
36
+ console.log(dimColor(separator));
37
+ }
38
+ }
@@ -1,22 +0,0 @@
1
- import gradient from 'gradient-string';
2
- import { palettes, ThemeName } from './palettes';
3
-
4
- export class GradientEngine {
5
- private currentTheme: ThemeName = 'zen';
6
- setTheme(theme: ThemeName) {
7
- if (palettes[theme]) {
8
- this.currentTheme = theme;
9
- }
10
- }
11
- get text() {
12
- return palettes[this.currentTheme];
13
- }
14
- apply(text: string, colors: string[]) {
15
- return gradient(colors)(text);
16
- }
17
- multiline(text: string, colors: string[]) {
18
- return gradient(colors).multiline(text);
19
- }
20
- }
21
-
22
- export const paint = new GradientEngine();