hanzio 1.0.0 → 1.0.3
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/README.md +30 -1
- package/dist/cool-console-log/index.d.ts +108 -0
- package/dist/cool-console-log/index.js +111 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +258 -0
- package/package.json +1 -37
- package/src/cool-console-log/index.ts +229 -0
- package/src/index.ts +4 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A small TypeScript-first utility library for common application code: arrays,
|
|
4
4
|
strings, promises, caching, state machines, dates, URLs, Zod helpers, API clients,
|
|
5
|
-
JWTs, gzip, sitemap crawling, and
|
|
5
|
+
JWTs, gzip, sitemap crawling, queues, and colorful terminal logging.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -30,6 +30,7 @@ import { jwtSign } from 'hanzio/jwt'
|
|
|
30
30
|
import { compressString } from 'hanzio/gzip'
|
|
31
31
|
import { getDomainSitemap } from 'hanzio/sitemap'
|
|
32
32
|
import PQueue from 'hanzio/p-queue'
|
|
33
|
+
import { createCoolLogger } from 'hanzio/cool-console-log'
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
The package currently publishes `src/**/*.ts` intentionally so Bun can consume
|
|
@@ -500,6 +501,34 @@ try {
|
|
|
500
501
|
}
|
|
501
502
|
```
|
|
502
503
|
|
|
504
|
+
### Cool console log
|
|
505
|
+
|
|
506
|
+
ANSI-colored logging for terminals. Exports include `terminalColors`, `createCoolLogger`,
|
|
507
|
+
`logOperationSummary`, `colorize`, and `logBanner`. By default colors and banners are
|
|
508
|
+
development-only (`NODE_ENV !== 'production'`); loggers honor `colorsInDevOnly`
|
|
509
|
+
and callbacks via `CoolLoggerOptions.onLog`.
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
import {
|
|
513
|
+
createCoolLogger,
|
|
514
|
+
colorize,
|
|
515
|
+
logBanner,
|
|
516
|
+
logOperationSummary,
|
|
517
|
+
terminalColors
|
|
518
|
+
} from 'hanzio'
|
|
519
|
+
|
|
520
|
+
const log = createCoolLogger()
|
|
521
|
+
log.info('User logged in', { userId: '123' })
|
|
522
|
+
log.warn('Rate limit approaching', { remaining: 10 })
|
|
523
|
+
|
|
524
|
+
const start = performance.now()
|
|
525
|
+
await doSomething()
|
|
526
|
+
logOperationSummary('user.create', performance.now() - start, true)
|
|
527
|
+
|
|
528
|
+
console.log(colorize('OK', 'brightGreen'))
|
|
529
|
+
logBanner('Server started', 'cyan')
|
|
530
|
+
```
|
|
531
|
+
|
|
503
532
|
## Development
|
|
504
533
|
|
|
505
534
|
```bash
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** ANSI color codes for terminal output */
|
|
2
|
+
export declare const terminalColors: {
|
|
3
|
+
readonly reset: "\u001B[0m";
|
|
4
|
+
readonly bold: "\u001B[1m";
|
|
5
|
+
readonly dim: "\u001B[2m";
|
|
6
|
+
readonly italic: "\u001B[3m";
|
|
7
|
+
readonly underline: "\u001B[4m";
|
|
8
|
+
readonly black: "\u001B[30m";
|
|
9
|
+
readonly red: "\u001B[31m";
|
|
10
|
+
readonly green: "\u001B[32m";
|
|
11
|
+
readonly yellow: "\u001B[33m";
|
|
12
|
+
readonly blue: "\u001B[34m";
|
|
13
|
+
readonly magenta: "\u001B[35m";
|
|
14
|
+
readonly cyan: "\u001B[36m";
|
|
15
|
+
readonly white: "\u001B[37m";
|
|
16
|
+
readonly gray: "\u001B[90m";
|
|
17
|
+
readonly brightRed: "\u001B[91m";
|
|
18
|
+
readonly brightGreen: "\u001B[92m";
|
|
19
|
+
readonly brightYellow: "\u001B[93m";
|
|
20
|
+
readonly brightBlue: "\u001B[94m";
|
|
21
|
+
readonly brightMagenta: "\u001B[95m";
|
|
22
|
+
readonly brightCyan: "\u001B[96m";
|
|
23
|
+
readonly brightWhite: "\u001B[97m";
|
|
24
|
+
readonly bgBlack: "\u001B[40m";
|
|
25
|
+
readonly bgRed: "\u001B[41m";
|
|
26
|
+
readonly bgGreen: "\u001B[42m";
|
|
27
|
+
readonly bgYellow: "\u001B[43m";
|
|
28
|
+
readonly bgBlue: "\u001B[44m";
|
|
29
|
+
readonly bgMagenta: "\u001B[45m";
|
|
30
|
+
readonly bgCyan: "\u001B[46m";
|
|
31
|
+
readonly bgWhite: "\u001B[47m";
|
|
32
|
+
};
|
|
33
|
+
export type TerminalColor = keyof typeof terminalColors;
|
|
34
|
+
/** Log levels supported by the logger */
|
|
35
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
36
|
+
/** Attributes that can be passed to log methods */
|
|
37
|
+
export type LogAttributes = Record<string, string | number | boolean>;
|
|
38
|
+
/** Log method signature */
|
|
39
|
+
export type LogMethod = (message: string, attributes?: LogAttributes) => void;
|
|
40
|
+
/** Logger interface with colored output */
|
|
41
|
+
export type CoolLogger = {
|
|
42
|
+
debug: LogMethod;
|
|
43
|
+
info: LogMethod;
|
|
44
|
+
warn: LogMethod;
|
|
45
|
+
error: LogMethod;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Options for creating a cool logger
|
|
49
|
+
*/
|
|
50
|
+
export type CoolLoggerOptions = {
|
|
51
|
+
/** Only use colors in development (default: true) */
|
|
52
|
+
colorsInDevOnly?: boolean;
|
|
53
|
+
/** Callback for each log entry (e.g., to add to OpenTelemetry span) */
|
|
54
|
+
onLog?: (level: LogLevel, message: string, attributes?: LogAttributes) => void;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Creates a logger with colorful console output.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const logger = createCoolLogger()
|
|
62
|
+
* logger.info('User logged in', { userId: '123' })
|
|
63
|
+
* logger.warn('Rate limit approaching', { remaining: 10 })
|
|
64
|
+
* logger.error('Payment failed', { reason: 'insufficient_funds' })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function createCoolLogger(options?: CoolLoggerOptions): CoolLogger;
|
|
68
|
+
/**
|
|
69
|
+
* Logs a request/operation summary with colored status and duration.
|
|
70
|
+
* Shows: `operationName ✓ · 12.3ms` (green) or `operationName ✗ · 45.2ms` (red)
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const start = performance.now()
|
|
75
|
+
* try {
|
|
76
|
+
* await doSomething()
|
|
77
|
+
* logOperationSummary('user.create', performance.now() - start, true)
|
|
78
|
+
* } catch {
|
|
79
|
+
* logOperationSummary('user.create', performance.now() - start, false)
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function logOperationSummary(operationName: string, durationMs: number, success: boolean, options?: {
|
|
84
|
+
devOnly?: boolean;
|
|
85
|
+
error?: Error | unknown;
|
|
86
|
+
}): void;
|
|
87
|
+
/**
|
|
88
|
+
* Helper to colorize text for console output
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* console.log(colorize('Success!', 'green', 'bold'))
|
|
93
|
+
* console.log(colorize('Warning', 'yellow'))
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export declare function colorize(text: string, color: TerminalColor, ...modifiers: TerminalColor[]): string;
|
|
97
|
+
/**
|
|
98
|
+
* Log a styled box/banner to the console (dev only)
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* logBanner('Server Started', 'green')
|
|
103
|
+
* logBanner('⚠️ Warning', 'yellow')
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export declare function logBanner(text: string, color?: TerminalColor, options?: {
|
|
107
|
+
devOnly?: boolean;
|
|
108
|
+
}): void;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/cool-console-log/index.ts
|
|
2
|
+
var terminalColors = {
|
|
3
|
+
reset: "\x1B[0m",
|
|
4
|
+
bold: "\x1B[1m",
|
|
5
|
+
dim: "\x1B[2m",
|
|
6
|
+
italic: "\x1B[3m",
|
|
7
|
+
underline: "\x1B[4m",
|
|
8
|
+
black: "\x1B[30m",
|
|
9
|
+
red: "\x1B[31m",
|
|
10
|
+
green: "\x1B[32m",
|
|
11
|
+
yellow: "\x1B[33m",
|
|
12
|
+
blue: "\x1B[34m",
|
|
13
|
+
magenta: "\x1B[35m",
|
|
14
|
+
cyan: "\x1B[36m",
|
|
15
|
+
white: "\x1B[37m",
|
|
16
|
+
gray: "\x1B[90m",
|
|
17
|
+
brightRed: "\x1B[91m",
|
|
18
|
+
brightGreen: "\x1B[92m",
|
|
19
|
+
brightYellow: "\x1B[93m",
|
|
20
|
+
brightBlue: "\x1B[94m",
|
|
21
|
+
brightMagenta: "\x1B[95m",
|
|
22
|
+
brightCyan: "\x1B[96m",
|
|
23
|
+
brightWhite: "\x1B[97m",
|
|
24
|
+
bgBlack: "\x1B[40m",
|
|
25
|
+
bgRed: "\x1B[41m",
|
|
26
|
+
bgGreen: "\x1B[42m",
|
|
27
|
+
bgYellow: "\x1B[43m",
|
|
28
|
+
bgBlue: "\x1B[44m",
|
|
29
|
+
bgMagenta: "\x1B[45m",
|
|
30
|
+
bgCyan: "\x1B[46m",
|
|
31
|
+
bgWhite: "\x1B[47m"
|
|
32
|
+
};
|
|
33
|
+
var levelColors = {
|
|
34
|
+
debug: { badge: terminalColors.gray, text: terminalColors.dim },
|
|
35
|
+
info: { badge: terminalColors.cyan, text: terminalColors.reset },
|
|
36
|
+
warn: { badge: terminalColors.yellow, text: terminalColors.yellow },
|
|
37
|
+
error: { badge: terminalColors.red, text: terminalColors.brightRed }
|
|
38
|
+
};
|
|
39
|
+
function formatAttributes(attrs) {
|
|
40
|
+
if (!attrs || Object.keys(attrs).length === 0)
|
|
41
|
+
return "";
|
|
42
|
+
const formatted = Object.entries(attrs).map(([k, v]) => `${terminalColors.dim}${k}=${terminalColors.reset}${v}`).join(" ");
|
|
43
|
+
return ` ${formatted}`;
|
|
44
|
+
}
|
|
45
|
+
function createCoolLogger(options = {}) {
|
|
46
|
+
const { colorsInDevOnly = true, onLog } = options;
|
|
47
|
+
const isDev = true;
|
|
48
|
+
const useColors = colorsInDevOnly ? isDev : true;
|
|
49
|
+
const createLogMethod = (level) => (message, attributes) => {
|
|
50
|
+
onLog?.(level, message, attributes);
|
|
51
|
+
if (level === "debug" && !isDev)
|
|
52
|
+
return;
|
|
53
|
+
if (useColors) {
|
|
54
|
+
const { badge, text } = levelColors[level];
|
|
55
|
+
const label = level.toUpperCase().padEnd(5);
|
|
56
|
+
const attrStr = formatAttributes(attributes);
|
|
57
|
+
console.log(`${badge}[${label}]${terminalColors.reset} ${text}${message}${terminalColors.reset}${attrStr}`);
|
|
58
|
+
} else {
|
|
59
|
+
console[level](`[${level.toUpperCase()}]`, message, attributes ?? "");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
debug: createLogMethod("debug"),
|
|
64
|
+
info: createLogMethod("info"),
|
|
65
|
+
warn: createLogMethod("warn"),
|
|
66
|
+
error: createLogMethod("error")
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function logOperationSummary(operationName, durationMs, success, options = {}) {
|
|
70
|
+
const { devOnly = true, error } = options;
|
|
71
|
+
const isDev = true;
|
|
72
|
+
if (devOnly && !isDev)
|
|
73
|
+
return;
|
|
74
|
+
const c = terminalColors;
|
|
75
|
+
const statusIcon = success ? `${c.brightGreen}✓${c.reset}` : `${c.brightRed}✗${c.reset}`;
|
|
76
|
+
const statusColor = success ? c.green : c.red;
|
|
77
|
+
const durationStr = `${c.dim}${durationMs.toFixed(1)}ms${c.reset}`;
|
|
78
|
+
const errorMessage = error ? ` ${c.dim}·${c.reset} ${c.red}${error instanceof Error ? error.message : String(error)}${c.reset}` : "";
|
|
79
|
+
console.log(`${statusColor}${c.bold}${operationName}${c.reset} ${statusIcon} ${c.dim}·${c.reset} ${durationStr}${errorMessage}`);
|
|
80
|
+
}
|
|
81
|
+
function colorize(text, color, ...modifiers) {
|
|
82
|
+
const isDev = true;
|
|
83
|
+
if (!isDev)
|
|
84
|
+
return text;
|
|
85
|
+
const colorCodes = [
|
|
86
|
+
terminalColors[color],
|
|
87
|
+
...modifiers.map((m) => terminalColors[m])
|
|
88
|
+
].join("");
|
|
89
|
+
return `${colorCodes}${text}${terminalColors.reset}`;
|
|
90
|
+
}
|
|
91
|
+
function logBanner(text, color = "cyan", options = {}) {
|
|
92
|
+
const { devOnly = true } = options;
|
|
93
|
+
const isDev = true;
|
|
94
|
+
if (devOnly && !isDev) {
|
|
95
|
+
console.log(`=== ${text} ===`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const c = terminalColors;
|
|
99
|
+
const padding = 2;
|
|
100
|
+
const line = "═".repeat(text.length + padding * 2);
|
|
101
|
+
console.log(`${c[color]}╔${line}╗${c.reset}`);
|
|
102
|
+
console.log(`${c[color]}║${" ".repeat(padding)}${c.bold}${text}${c.reset}${c[color]}${" ".repeat(padding)}║${c.reset}`);
|
|
103
|
+
console.log(`${c[color]}╚${line}╝${c.reset}`);
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
terminalColors,
|
|
107
|
+
logOperationSummary,
|
|
108
|
+
logBanner,
|
|
109
|
+
createCoolLogger,
|
|
110
|
+
colorize
|
|
111
|
+
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -879,6 +879,150 @@ class PQueue {
|
|
|
879
879
|
}
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
+
// src/jwt/index.ts
|
|
883
|
+
import * as jose from "jose";
|
|
884
|
+
async function jwtSign(payload, secret) {
|
|
885
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
886
|
+
return await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().sign(secretKey);
|
|
887
|
+
}
|
|
888
|
+
async function jwtVerify2(token, secret) {
|
|
889
|
+
try {
|
|
890
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
891
|
+
const { payload } = await jose.jwtVerify(token, secretKey);
|
|
892
|
+
return payload;
|
|
893
|
+
} catch (_error) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/gzip/index.ts
|
|
899
|
+
import pako from "pako";
|
|
900
|
+
function compressString(input) {
|
|
901
|
+
const uint8Array = new TextEncoder().encode(input);
|
|
902
|
+
const compressed = pako.deflate(uint8Array);
|
|
903
|
+
return btoa(String.fromCharCode.apply(null, compressed));
|
|
904
|
+
}
|
|
905
|
+
function decompressString(compressedString) {
|
|
906
|
+
const compressedData = new Uint8Array(atob(compressedString).split("").map((char) => char.charCodeAt(0)));
|
|
907
|
+
const decompressed = pako.inflate(compressedData);
|
|
908
|
+
return new TextDecoder().decode(decompressed);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/sitemap/index.ts
|
|
912
|
+
var createTimeoutController = (timeout) => {
|
|
913
|
+
const controller = new AbortController;
|
|
914
|
+
setTimeout(() => controller.abort(), timeout);
|
|
915
|
+
return controller;
|
|
916
|
+
};
|
|
917
|
+
var discoverSitemaps = async (domain) => {
|
|
918
|
+
const normalizedDomain = domain.replace(/\/$/, "");
|
|
919
|
+
const commonLocations = [
|
|
920
|
+
"/sitemap.xml",
|
|
921
|
+
"/sitemap_index.xml",
|
|
922
|
+
"/sitemap/",
|
|
923
|
+
"/sitemaps.xml",
|
|
924
|
+
"/sitemap/sitemap.xml"
|
|
925
|
+
];
|
|
926
|
+
const sitemaps = new Set;
|
|
927
|
+
const requestOptions = {
|
|
928
|
+
headers: {
|
|
929
|
+
"User-Agent": "SitemapCrawler/1.0"
|
|
930
|
+
},
|
|
931
|
+
signal: createTimeoutController(5000).signal
|
|
932
|
+
};
|
|
933
|
+
try {
|
|
934
|
+
const robotsTxt = await fetch(`${normalizedDomain}/robots.txt`, {
|
|
935
|
+
...requestOptions
|
|
936
|
+
});
|
|
937
|
+
if (robotsTxt.ok) {
|
|
938
|
+
const text = await robotsTxt.text();
|
|
939
|
+
const sitemapMatches = text.match(/Sitemap: (.*)/gi);
|
|
940
|
+
if (sitemapMatches) {
|
|
941
|
+
sitemapMatches.forEach((match) => {
|
|
942
|
+
const url = match.replace("Sitemap:", "").trim();
|
|
943
|
+
sitemaps.add(url);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
if (sitemaps.size > 0) {
|
|
947
|
+
return [...sitemaps];
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} catch (error) {
|
|
951
|
+
console.warn("Could not fetch robots.txt:", error instanceof Error ? error.message : "Unknown error");
|
|
952
|
+
}
|
|
953
|
+
for (const location of commonLocations) {
|
|
954
|
+
try {
|
|
955
|
+
const response = await fetch(`${normalizedDomain}${location}`, {
|
|
956
|
+
...requestOptions,
|
|
957
|
+
method: "HEAD"
|
|
958
|
+
});
|
|
959
|
+
if (response.ok) {
|
|
960
|
+
return [`${normalizedDomain}${location}`];
|
|
961
|
+
}
|
|
962
|
+
} catch (_error) {}
|
|
963
|
+
}
|
|
964
|
+
return [];
|
|
965
|
+
};
|
|
966
|
+
var getDomainSitemap = async (domain, options = {}) => {
|
|
967
|
+
const {
|
|
968
|
+
filterIndexes,
|
|
969
|
+
concurrency = 10,
|
|
970
|
+
maxDepth = 5,
|
|
971
|
+
timeout = 1e4
|
|
972
|
+
} = options;
|
|
973
|
+
let normalizedDomain = domain.replace(/\/$/, "");
|
|
974
|
+
if (!normalizedDomain.startsWith("http")) {
|
|
975
|
+
normalizedDomain = `https://${normalizedDomain}`;
|
|
976
|
+
}
|
|
977
|
+
const sitemapUrls = await discoverSitemaps(normalizedDomain);
|
|
978
|
+
if (sitemapUrls.length === 0) {
|
|
979
|
+
throw new Error("No sitemaps found for the domain");
|
|
980
|
+
}
|
|
981
|
+
const queue = new PQueue({ concurrency });
|
|
982
|
+
const result = {
|
|
983
|
+
links: new Set,
|
|
984
|
+
visited: new Set
|
|
985
|
+
};
|
|
986
|
+
const locRegex = /<loc>([^<]+)<\/loc>/g;
|
|
987
|
+
const isXmlSitemap = (url) => url.endsWith(".xml") || url.toLowerCase().includes("sitemap");
|
|
988
|
+
const processSitemap = async (sitemapUrl, depth = 0) => {
|
|
989
|
+
if (depth > maxDepth || result.visited.has(sitemapUrl)) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
result.visited.add(sitemapUrl);
|
|
993
|
+
try {
|
|
994
|
+
const controller = createTimeoutController(timeout);
|
|
995
|
+
const response = await fetch(sitemapUrl, {
|
|
996
|
+
headers: {
|
|
997
|
+
"User-Agent": "SitemapCrawler/1.0"
|
|
998
|
+
},
|
|
999
|
+
signal: controller.signal
|
|
1000
|
+
});
|
|
1001
|
+
if (!response.ok)
|
|
1002
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1003
|
+
const data = await response.text();
|
|
1004
|
+
const matches = [...data.matchAll(locRegex)].map((match) => match[1].trim()).filter((link) => {
|
|
1005
|
+
if (filterIndexes && isXmlSitemap(link)) {
|
|
1006
|
+
return link.includes(filterIndexes);
|
|
1007
|
+
}
|
|
1008
|
+
return true;
|
|
1009
|
+
});
|
|
1010
|
+
for (const link of matches) {
|
|
1011
|
+
if (isXmlSitemap(link)) {
|
|
1012
|
+
queue.add(() => processSitemap(link, depth + 1));
|
|
1013
|
+
} else {
|
|
1014
|
+
result.links.add(link);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
console.warn(`Failed to process sitemap ${sitemapUrl}:`, error instanceof Error ? error.message : "Unknown error");
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
await Promise.all(sitemapUrls.map((sitemapUrl) => queue.add(() => processSitemap(sitemapUrl))));
|
|
1022
|
+
await queue.onIdle();
|
|
1023
|
+
return [...result.links];
|
|
1024
|
+
};
|
|
1025
|
+
|
|
882
1026
|
// src/typedswitch/index.ts
|
|
883
1027
|
function runSwitch(value, keyOrCases, casesOrDefault, defaultCase) {
|
|
884
1028
|
let key;
|
|
@@ -1514,6 +1658,110 @@ function formatPeriodLabel(start, end, includeDay = false) {
|
|
|
1514
1658
|
function getDomainFaviconUrl(domain, size = 128) {
|
|
1515
1659
|
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=${size}`;
|
|
1516
1660
|
}
|
|
1661
|
+
// src/cool-console-log/index.ts
|
|
1662
|
+
var terminalColors = {
|
|
1663
|
+
reset: "\x1B[0m",
|
|
1664
|
+
bold: "\x1B[1m",
|
|
1665
|
+
dim: "\x1B[2m",
|
|
1666
|
+
italic: "\x1B[3m",
|
|
1667
|
+
underline: "\x1B[4m",
|
|
1668
|
+
black: "\x1B[30m",
|
|
1669
|
+
red: "\x1B[31m",
|
|
1670
|
+
green: "\x1B[32m",
|
|
1671
|
+
yellow: "\x1B[33m",
|
|
1672
|
+
blue: "\x1B[34m",
|
|
1673
|
+
magenta: "\x1B[35m",
|
|
1674
|
+
cyan: "\x1B[36m",
|
|
1675
|
+
white: "\x1B[37m",
|
|
1676
|
+
gray: "\x1B[90m",
|
|
1677
|
+
brightRed: "\x1B[91m",
|
|
1678
|
+
brightGreen: "\x1B[92m",
|
|
1679
|
+
brightYellow: "\x1B[93m",
|
|
1680
|
+
brightBlue: "\x1B[94m",
|
|
1681
|
+
brightMagenta: "\x1B[95m",
|
|
1682
|
+
brightCyan: "\x1B[96m",
|
|
1683
|
+
brightWhite: "\x1B[97m",
|
|
1684
|
+
bgBlack: "\x1B[40m",
|
|
1685
|
+
bgRed: "\x1B[41m",
|
|
1686
|
+
bgGreen: "\x1B[42m",
|
|
1687
|
+
bgYellow: "\x1B[43m",
|
|
1688
|
+
bgBlue: "\x1B[44m",
|
|
1689
|
+
bgMagenta: "\x1B[45m",
|
|
1690
|
+
bgCyan: "\x1B[46m",
|
|
1691
|
+
bgWhite: "\x1B[47m"
|
|
1692
|
+
};
|
|
1693
|
+
var levelColors = {
|
|
1694
|
+
debug: { badge: terminalColors.gray, text: terminalColors.dim },
|
|
1695
|
+
info: { badge: terminalColors.cyan, text: terminalColors.reset },
|
|
1696
|
+
warn: { badge: terminalColors.yellow, text: terminalColors.yellow },
|
|
1697
|
+
error: { badge: terminalColors.red, text: terminalColors.brightRed }
|
|
1698
|
+
};
|
|
1699
|
+
function formatAttributes(attrs) {
|
|
1700
|
+
if (!attrs || Object.keys(attrs).length === 0)
|
|
1701
|
+
return "";
|
|
1702
|
+
const formatted = Object.entries(attrs).map(([k, v]) => `${terminalColors.dim}${k}=${terminalColors.reset}${v}`).join(" ");
|
|
1703
|
+
return ` ${formatted}`;
|
|
1704
|
+
}
|
|
1705
|
+
function createCoolLogger(options = {}) {
|
|
1706
|
+
const { colorsInDevOnly = true, onLog } = options;
|
|
1707
|
+
const isDev = true;
|
|
1708
|
+
const useColors = colorsInDevOnly ? isDev : true;
|
|
1709
|
+
const createLogMethod = (level) => (message, attributes) => {
|
|
1710
|
+
onLog?.(level, message, attributes);
|
|
1711
|
+
if (level === "debug" && !isDev)
|
|
1712
|
+
return;
|
|
1713
|
+
if (useColors) {
|
|
1714
|
+
const { badge, text } = levelColors[level];
|
|
1715
|
+
const label = level.toUpperCase().padEnd(5);
|
|
1716
|
+
const attrStr = formatAttributes(attributes);
|
|
1717
|
+
console.log(`${badge}[${label}]${terminalColors.reset} ${text}${message}${terminalColors.reset}${attrStr}`);
|
|
1718
|
+
} else {
|
|
1719
|
+
console[level](`[${level.toUpperCase()}]`, message, attributes ?? "");
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
return {
|
|
1723
|
+
debug: createLogMethod("debug"),
|
|
1724
|
+
info: createLogMethod("info"),
|
|
1725
|
+
warn: createLogMethod("warn"),
|
|
1726
|
+
error: createLogMethod("error")
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function logOperationSummary(operationName, durationMs, success, options = {}) {
|
|
1730
|
+
const { devOnly = true, error } = options;
|
|
1731
|
+
const isDev = true;
|
|
1732
|
+
if (devOnly && !isDev)
|
|
1733
|
+
return;
|
|
1734
|
+
const c = terminalColors;
|
|
1735
|
+
const statusIcon = success ? `${c.brightGreen}✓${c.reset}` : `${c.brightRed}✗${c.reset}`;
|
|
1736
|
+
const statusColor = success ? c.green : c.red;
|
|
1737
|
+
const durationStr = `${c.dim}${durationMs.toFixed(1)}ms${c.reset}`;
|
|
1738
|
+
const errorMessage = error ? ` ${c.dim}·${c.reset} ${c.red}${error instanceof Error ? error.message : String(error)}${c.reset}` : "";
|
|
1739
|
+
console.log(`${statusColor}${c.bold}${operationName}${c.reset} ${statusIcon} ${c.dim}·${c.reset} ${durationStr}${errorMessage}`);
|
|
1740
|
+
}
|
|
1741
|
+
function colorize(text, color, ...modifiers) {
|
|
1742
|
+
const isDev = true;
|
|
1743
|
+
if (!isDev)
|
|
1744
|
+
return text;
|
|
1745
|
+
const colorCodes = [
|
|
1746
|
+
terminalColors[color],
|
|
1747
|
+
...modifiers.map((m) => terminalColors[m])
|
|
1748
|
+
].join("");
|
|
1749
|
+
return `${colorCodes}${text}${terminalColors.reset}`;
|
|
1750
|
+
}
|
|
1751
|
+
function logBanner(text, color = "cyan", options = {}) {
|
|
1752
|
+
const { devOnly = true } = options;
|
|
1753
|
+
const isDev = true;
|
|
1754
|
+
if (devOnly && !isDev) {
|
|
1755
|
+
console.log(`=== ${text} ===`);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const c = terminalColors;
|
|
1759
|
+
const padding = 2;
|
|
1760
|
+
const line = "═".repeat(text.length + padding * 2);
|
|
1761
|
+
console.log(`${c[color]}╔${line}╗${c.reset}`);
|
|
1762
|
+
console.log(`${c[color]}║${" ".repeat(padding)}${c.bold}${text}${c.reset}${c[color]}${" ".repeat(padding)}║${c.reset}`);
|
|
1763
|
+
console.log(`${c[color]}╚${line}╝${c.reset}`);
|
|
1764
|
+
}
|
|
1517
1765
|
export {
|
|
1518
1766
|
zodToTypeString,
|
|
1519
1767
|
zodToExample,
|
|
@@ -1524,6 +1772,7 @@ export {
|
|
|
1524
1772
|
typedSwitch,
|
|
1525
1773
|
tryCatch,
|
|
1526
1774
|
toGteLteFilter,
|
|
1775
|
+
terminalColors,
|
|
1527
1776
|
slugify,
|
|
1528
1777
|
seriesColorKey,
|
|
1529
1778
|
resolveSeriesColorMap,
|
|
@@ -1531,8 +1780,12 @@ export {
|
|
|
1531
1780
|
pickItemsInArray,
|
|
1532
1781
|
partition,
|
|
1533
1782
|
normalizeSingleOrArray,
|
|
1783
|
+
logOperationSummary,
|
|
1784
|
+
logBanner,
|
|
1534
1785
|
linearRegressionTrend,
|
|
1535
1786
|
keyBy,
|
|
1787
|
+
jwtVerify2 as jwtVerify,
|
|
1788
|
+
jwtSign,
|
|
1536
1789
|
jsonToZodSchemaMapper,
|
|
1537
1790
|
isSuccess,
|
|
1538
1791
|
isFailure,
|
|
@@ -1542,16 +1795,21 @@ export {
|
|
|
1542
1795
|
getUniqueValues,
|
|
1543
1796
|
getPreviousReportPeriod,
|
|
1544
1797
|
getHeatmapCellStyle,
|
|
1798
|
+
getDomainSitemap,
|
|
1545
1799
|
getDomainFaviconUrl,
|
|
1546
1800
|
generateId,
|
|
1547
1801
|
formatPeriodLabel,
|
|
1548
1802
|
findArrayDifferenceByKey,
|
|
1549
1803
|
extractNumber,
|
|
1804
|
+
decompressString,
|
|
1550
1805
|
createZId,
|
|
1551
1806
|
createResolveColorsByKeys,
|
|
1807
|
+
createCoolLogger,
|
|
1552
1808
|
createApiWrapper,
|
|
1553
1809
|
createApiClient,
|
|
1810
|
+
compressString,
|
|
1554
1811
|
compact,
|
|
1812
|
+
colorize,
|
|
1555
1813
|
chunkArray,
|
|
1556
1814
|
cacheFunction,
|
|
1557
1815
|
backgroundPromiseSync,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanzio",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A set of utility functions for TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -34,42 +34,6 @@
|
|
|
34
34
|
"bun": "./src/index.ts",
|
|
35
35
|
"import": "./dist/index.js",
|
|
36
36
|
"default": "./dist/index.js"
|
|
37
|
-
},
|
|
38
|
-
"./zod": {
|
|
39
|
-
"types": "./dist/zod/index.d.ts",
|
|
40
|
-
"bun": "./src/zod/index.ts",
|
|
41
|
-
"import": "./dist/zod/index.js",
|
|
42
|
-
"default": "./dist/zod/index.js"
|
|
43
|
-
},
|
|
44
|
-
"./api-wrapper": {
|
|
45
|
-
"types": "./dist/api-wrapper/index.d.ts",
|
|
46
|
-
"bun": "./src/api-wrapper/index.ts",
|
|
47
|
-
"import": "./dist/api-wrapper/index.js",
|
|
48
|
-
"default": "./dist/api-wrapper/index.js"
|
|
49
|
-
},
|
|
50
|
-
"./jwt": {
|
|
51
|
-
"types": "./dist/jwt/index.d.ts",
|
|
52
|
-
"bun": "./src/jwt/index.ts",
|
|
53
|
-
"import": "./dist/jwt/index.js",
|
|
54
|
-
"default": "./dist/jwt/index.js"
|
|
55
|
-
},
|
|
56
|
-
"./gzip": {
|
|
57
|
-
"types": "./dist/gzip/index.d.ts",
|
|
58
|
-
"bun": "./src/gzip/index.ts",
|
|
59
|
-
"import": "./dist/gzip/index.js",
|
|
60
|
-
"default": "./dist/gzip/index.js"
|
|
61
|
-
},
|
|
62
|
-
"./sitemap": {
|
|
63
|
-
"types": "./dist/sitemap/index.d.ts",
|
|
64
|
-
"bun": "./src/sitemap/index.ts",
|
|
65
|
-
"import": "./dist/sitemap/index.js",
|
|
66
|
-
"default": "./dist/sitemap/index.js"
|
|
67
|
-
},
|
|
68
|
-
"./p-queue": {
|
|
69
|
-
"types": "./dist/p-queue/index.d.ts",
|
|
70
|
-
"bun": "./src/p-queue/index.ts",
|
|
71
|
-
"import": "./dist/p-queue/index.js",
|
|
72
|
-
"default": "./dist/p-queue/index.js"
|
|
73
37
|
}
|
|
74
38
|
},
|
|
75
39
|
"scripts": {
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// Cool Console Log - Colorful Terminal Logging Utility
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
/** ANSI color codes for terminal output */
|
|
6
|
+
export const terminalColors = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
italic: '\x1b[3m',
|
|
11
|
+
underline: '\x1b[4m',
|
|
12
|
+
// Foreground colors
|
|
13
|
+
black: '\x1b[30m',
|
|
14
|
+
red: '\x1b[31m',
|
|
15
|
+
green: '\x1b[32m',
|
|
16
|
+
yellow: '\x1b[33m',
|
|
17
|
+
blue: '\x1b[34m',
|
|
18
|
+
magenta: '\x1b[35m',
|
|
19
|
+
cyan: '\x1b[36m',
|
|
20
|
+
white: '\x1b[37m',
|
|
21
|
+
gray: '\x1b[90m',
|
|
22
|
+
// Bright variants
|
|
23
|
+
brightRed: '\x1b[91m',
|
|
24
|
+
brightGreen: '\x1b[92m',
|
|
25
|
+
brightYellow: '\x1b[93m',
|
|
26
|
+
brightBlue: '\x1b[94m',
|
|
27
|
+
brightMagenta: '\x1b[95m',
|
|
28
|
+
brightCyan: '\x1b[96m',
|
|
29
|
+
brightWhite: '\x1b[97m',
|
|
30
|
+
// Background colors
|
|
31
|
+
bgBlack: '\x1b[40m',
|
|
32
|
+
bgRed: '\x1b[41m',
|
|
33
|
+
bgGreen: '\x1b[42m',
|
|
34
|
+
bgYellow: '\x1b[43m',
|
|
35
|
+
bgBlue: '\x1b[44m',
|
|
36
|
+
bgMagenta: '\x1b[45m',
|
|
37
|
+
bgCyan: '\x1b[46m',
|
|
38
|
+
bgWhite: '\x1b[47m'
|
|
39
|
+
} as const
|
|
40
|
+
|
|
41
|
+
export type TerminalColor = keyof typeof terminalColors
|
|
42
|
+
|
|
43
|
+
/** Log levels supported by the logger */
|
|
44
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
45
|
+
|
|
46
|
+
/** Attributes that can be passed to log methods */
|
|
47
|
+
export type LogAttributes = Record<string, string | number | boolean>
|
|
48
|
+
|
|
49
|
+
/** Log method signature */
|
|
50
|
+
export type LogMethod = (message: string, attributes?: LogAttributes) => void
|
|
51
|
+
|
|
52
|
+
/** Logger interface with colored output */
|
|
53
|
+
export type CoolLogger = {
|
|
54
|
+
debug: LogMethod
|
|
55
|
+
info: LogMethod
|
|
56
|
+
warn: LogMethod
|
|
57
|
+
error: LogMethod
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Color config per log level */
|
|
61
|
+
const levelColors: Record<LogLevel, { badge: string; text: string }> = {
|
|
62
|
+
debug: { badge: terminalColors.gray, text: terminalColors.dim },
|
|
63
|
+
info: { badge: terminalColors.cyan, text: terminalColors.reset },
|
|
64
|
+
warn: { badge: terminalColors.yellow, text: terminalColors.yellow },
|
|
65
|
+
error: { badge: terminalColors.red, text: terminalColors.brightRed }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Format attributes as a compact colored string */
|
|
69
|
+
function formatAttributes(attrs?: LogAttributes): string {
|
|
70
|
+
if (!attrs || Object.keys(attrs).length === 0) return ''
|
|
71
|
+
const formatted = Object.entries(attrs)
|
|
72
|
+
.map(([k, v]) => `${terminalColors.dim}${k}=${terminalColors.reset}${v}`)
|
|
73
|
+
.join(' ')
|
|
74
|
+
return ` ${formatted}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Options for creating a cool logger
|
|
79
|
+
*/
|
|
80
|
+
export type CoolLoggerOptions = {
|
|
81
|
+
/** Only use colors in development (default: true) */
|
|
82
|
+
colorsInDevOnly?: boolean
|
|
83
|
+
/** Callback for each log entry (e.g., to add to OpenTelemetry span) */
|
|
84
|
+
onLog?: (level: LogLevel, message: string, attributes?: LogAttributes) => void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a logger with colorful console output.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const logger = createCoolLogger()
|
|
93
|
+
* logger.info('User logged in', { userId: '123' })
|
|
94
|
+
* logger.warn('Rate limit approaching', { remaining: 10 })
|
|
95
|
+
* logger.error('Payment failed', { reason: 'insufficient_funds' })
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function createCoolLogger(options: CoolLoggerOptions = {}): CoolLogger {
|
|
99
|
+
const { colorsInDevOnly = true, onLog } = options
|
|
100
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
101
|
+
const useColors = colorsInDevOnly ? isDev : true
|
|
102
|
+
|
|
103
|
+
const createLogMethod =
|
|
104
|
+
(level: LogLevel): LogMethod =>
|
|
105
|
+
(message, attributes) => {
|
|
106
|
+
// Call the onLog callback if provided
|
|
107
|
+
onLog?.(level, message, attributes)
|
|
108
|
+
|
|
109
|
+
// Skip debug logs in production
|
|
110
|
+
if (level === 'debug' && !isDev) return
|
|
111
|
+
|
|
112
|
+
if (useColors) {
|
|
113
|
+
const { badge, text } = levelColors[level]
|
|
114
|
+
const label = level.toUpperCase().padEnd(5)
|
|
115
|
+
const attrStr = formatAttributes(attributes)
|
|
116
|
+
console.log(
|
|
117
|
+
`${badge}[${label}]${terminalColors.reset} ${text}${message}${terminalColors.reset}${attrStr}`
|
|
118
|
+
)
|
|
119
|
+
} else {
|
|
120
|
+
console[level](`[${level.toUpperCase()}]`, message, attributes ?? '')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
debug: createLogMethod('debug'),
|
|
126
|
+
info: createLogMethod('info'),
|
|
127
|
+
warn: createLogMethod('warn'),
|
|
128
|
+
error: createLogMethod('error')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Logs a request/operation summary with colored status and duration.
|
|
134
|
+
* Shows: `operationName ✓ · 12.3ms` (green) or `operationName ✗ · 45.2ms` (red)
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* const start = performance.now()
|
|
139
|
+
* try {
|
|
140
|
+
* await doSomething()
|
|
141
|
+
* logOperationSummary('user.create', performance.now() - start, true)
|
|
142
|
+
* } catch {
|
|
143
|
+
* logOperationSummary('user.create', performance.now() - start, false)
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function logOperationSummary(
|
|
148
|
+
operationName: string,
|
|
149
|
+
durationMs: number,
|
|
150
|
+
success: boolean,
|
|
151
|
+
options: { devOnly?: boolean; error?: Error | unknown } = {}
|
|
152
|
+
): void {
|
|
153
|
+
const { devOnly = true, error } = options
|
|
154
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
155
|
+
|
|
156
|
+
if (devOnly && !isDev) return
|
|
157
|
+
|
|
158
|
+
const c = terminalColors
|
|
159
|
+
const statusIcon = success
|
|
160
|
+
? `${c.brightGreen}✓${c.reset}`
|
|
161
|
+
: `${c.brightRed}✗${c.reset}`
|
|
162
|
+
const statusColor = success ? c.green : c.red
|
|
163
|
+
const durationStr = `${c.dim}${durationMs.toFixed(1)}ms${c.reset}`
|
|
164
|
+
|
|
165
|
+
const errorMessage = error
|
|
166
|
+
? ` ${c.dim}·${c.reset} ${c.red}${error instanceof Error ? error.message : String(error)}${c.reset}`
|
|
167
|
+
: ''
|
|
168
|
+
|
|
169
|
+
console.log(
|
|
170
|
+
`${statusColor}${c.bold}${operationName}${c.reset} ${statusIcon} ${c.dim}·${c.reset} ${durationStr}${errorMessage}`
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Helper to colorize text for console output
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* console.log(colorize('Success!', 'green', 'bold'))
|
|
180
|
+
* console.log(colorize('Warning', 'yellow'))
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function colorize(
|
|
184
|
+
text: string,
|
|
185
|
+
color: TerminalColor,
|
|
186
|
+
...modifiers: TerminalColor[]
|
|
187
|
+
): string {
|
|
188
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
189
|
+
if (!isDev) return text
|
|
190
|
+
|
|
191
|
+
const colorCodes = [
|
|
192
|
+
terminalColors[color],
|
|
193
|
+
...modifiers.map((m) => terminalColors[m])
|
|
194
|
+
].join('')
|
|
195
|
+
return `${colorCodes}${text}${terminalColors.reset}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Log a styled box/banner to the console (dev only)
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* logBanner('Server Started', 'green')
|
|
204
|
+
* logBanner('⚠️ Warning', 'yellow')
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export function logBanner(
|
|
208
|
+
text: string,
|
|
209
|
+
color: TerminalColor = 'cyan',
|
|
210
|
+
options: { devOnly?: boolean } = {}
|
|
211
|
+
): void {
|
|
212
|
+
const { devOnly = true } = options
|
|
213
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
214
|
+
|
|
215
|
+
if (devOnly && !isDev) {
|
|
216
|
+
console.log(`=== ${text} ===`)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const c = terminalColors
|
|
221
|
+
const padding = 2
|
|
222
|
+
const line = '═'.repeat(text.length + padding * 2)
|
|
223
|
+
|
|
224
|
+
console.log(`${c[color]}╔${line}╗${c.reset}`)
|
|
225
|
+
console.log(
|
|
226
|
+
`${c[color]}║${' '.repeat(padding)}${c.bold}${text}${c.reset}${c[color]}${' '.repeat(padding)}║${c.reset}`
|
|
227
|
+
)
|
|
228
|
+
console.log(`${c[color]}╚${line}╝${c.reset}`)
|
|
229
|
+
}
|