@ix-xs/node-comfort 1.1.31 → 2.0.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -748
  4. package/index.js +84 -13
  5. package/package.json +201 -21
  6. package/src/Arr.js +901 -0
  7. package/src/Async.js +585 -0
  8. package/src/Cache.js +329 -0
  9. package/src/Checker.js +979 -188
  10. package/src/Cli.js +1034 -0
  11. package/src/Color.js +415 -0
  12. package/src/Crypto.js +707 -0
  13. package/src/Emitter.js +289 -0
  14. package/src/Env.js +555 -0
  15. package/src/FS.js +1483 -768
  16. package/src/Func.js +692 -0
  17. package/src/Http.js +538 -0
  18. package/src/Id.js +418 -0
  19. package/src/Logger.js +739 -307
  20. package/src/Num.js +849 -0
  21. package/src/Obj.js +904 -0
  22. package/src/SQLite.js +1308 -994
  23. package/src/Schema.js +1586 -0
  24. package/src/Str.js +999 -0
  25. package/src/Sys.js +511 -0
  26. package/src/Time.js +1323 -0
  27. package/src/Utils.js +321 -325
  28. package/src/errors.js +292 -0
  29. package/types/index.d.ts +693 -120
  30. package/types/src/Arr.d.ts +562 -0
  31. package/types/src/Arr.d.ts.map +1 -0
  32. package/types/src/Async.d.ts +321 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +195 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +609 -81
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +582 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +294 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +534 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +170 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +382 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +709 -201
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +455 -0
  51. package/types/src/Func.d.ts.map +1 -0
  52. package/types/src/Http.d.ts +374 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +200 -0
  55. package/types/src/Id.d.ts.map +1 -0
  56. package/types/src/Logger.d.ts +374 -24
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +578 -0
  59. package/types/src/Num.d.ts.map +1 -0
  60. package/types/src/Obj.d.ts +475 -0
  61. package/types/src/Obj.d.ts.map +1 -0
  62. package/types/src/SQLite.d.ts +647 -305
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +940 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +696 -0
  67. package/types/src/Str.d.ts.map +1 -0
  68. package/types/src/Sys.d.ts +269 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +664 -0
  71. package/types/src/Time.d.ts.map +1 -0
  72. package/types/src/Utils.d.ts +154 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +292 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/tsconfig.json +0 -18
  77. package/types/index.d.ts.map +0 -1
package/src/Logger.js CHANGED
@@ -1,307 +1,739 @@
1
- /**
2
- * Text styles for console ANSI formatting
3
- * @private
4
- */
5
- const _style = {
6
- normal: 0,
7
- bold: 1,
8
- italic: 3,
9
- underline: 4,
10
- overline: 9,
11
- };
12
-
13
- /**
14
- * Text colors for console ANSI formatting
15
- * @private
16
- */
17
- const _color = {
18
- fg: {
19
- black: 30,
20
- red: 31,
21
- green: 32,
22
- yellow: 33,
23
- blue: 34,
24
- magenta: 35,
25
- cyan: 36,
26
- white: 37,
27
- gray: 90,
28
- redBright: 91,
29
- greenBright: 92,
30
- yellowBright: 93,
31
- blueBright: 94,
32
- magentaBright: 95,
33
- cyanBright: 96,
34
- whiteBright: 97,
35
- rgb: 38,
36
- },
37
- bg: {
38
- black: 40,
39
- red: 41,
40
- green: 42,
41
- yellow: 43,
42
- blue: 44,
43
- magenta: 45,
44
- cyan: 46,
45
- white: 47,
46
- gray: 100,
47
- redBright: 101,
48
- greenBright: 102,
49
- yellowBright: 103,
50
- blueBright: 104,
51
- magentaBright: 105,
52
- cyanBright: 106,
53
- whiteBright: 107,
54
- rgb: 48,
55
- },
56
- };
57
-
58
- /**
59
- * Default logger options
60
- * @private
61
- */
62
- const _options = {
63
- delimiter: {
64
- open: "<%",
65
- close: "%>",
66
- },
67
- timestamp: true,
68
- };
69
-
70
- /**
71
- * Array to manage log groups (indentation)
72
- * @private
73
- */
74
- const _groups = [];
75
-
76
- /**
77
- * Returns the ANSI code for a given style, color, or bg
78
- * @private
79
- * @param {string} value - The style/color name (e.g., "bold", "red", "rgb(255, 0, 0)")
80
- * @returns {number|string|null} The ANSI code, rgb marker, or null if unknown
81
- */
82
- const _getCode = (value) => {
83
- if (_style[value] !== undefined) return _style[value];
84
- if (_color.fg[value] !== undefined) return _color.fg[value];
85
- if (value.startsWith("bg")) {
86
- const bg = value.charAt(2).toLowerCase() + value.slice(3);
87
- if (_color.bg[bg] !== undefined) return _color.bg[bg];
88
- }
89
-
90
- const rgbMatch = value.match(/^rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)$/);
91
- if (rgbMatch) {
92
- const r = parseInt(rgbMatch[1], 10);
93
- const g = parseInt(rgbMatch[2], 10);
94
- const b = parseInt(rgbMatch[3], 10);
95
- if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
96
- return `rgb:${r},${g},${b}`;
97
- }
98
- }
99
-
100
- return null;
101
- };
102
-
103
- /**
104
- * Parses content between delimiters to extract styles and text
105
- * @private
106
- * @param {string} input - Content between delimiters (e.g., "red bold hello" or "rgb(255, 0, 0) hello")
107
- * @returns {{ codes: number[], text: string }} ANSI codes and text to display
108
- */
109
- const _parse = (input) => {
110
- const rgbMatch = input.match(/rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)/);
111
- const rgbValue = rgbMatch ? rgbMatch[0] : null;
112
- const cleanedInput = input.replace(/rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)/, "").trim();
113
- const tokens = cleanedInput.trim().split(/\s+/);
114
- const styles = [];
115
- const fgs = [];
116
- const bgs = [];
117
- const txts = [];
118
-
119
- for (const token of tokens) {
120
- if (!token) continue;
121
- const code = _getCode(token);
122
- if (code === null) {
123
- txts.push(token);
124
- } else {
125
- if (_style[token] !== undefined) {
126
- styles.push(code);
127
- } else if (token.startsWith("bg")) {
128
- bgs.push(code);
129
- } else {
130
- fgs.push(code);
131
- }
132
- }
133
- }
134
-
135
- const codes = [...styles, ...bgs, ...fgs];
136
-
137
- if (rgbValue) {
138
- const rgbParts = rgbValue.match(/rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)/);
139
- if (rgbParts) {
140
- const r = parseInt(rgbParts[1], 10);
141
- const g = parseInt(rgbParts[2], 10);
142
- const b = parseInt(rgbParts[3], 10);
143
- codes.push(38, 2, r, g, b);
144
- }
145
- }
146
-
147
- const text = cleanedInput
148
- .replace(
149
- /^\s*((redBright|greenBright|yellowBright|blueBright|magentaBright|cyanBright|whiteBright|red|green|yellow|blue|magenta|cyan|white|gray|black|bold|italic|underline|overline|bg\w+|rgb\(\d+\s*,\s*\d+\s*,\s*\d+\)|\s+)*)/gi,
150
- "",
151
- )
152
- .trimStart();
153
- return { codes, text };
154
- };
155
-
156
- /**
157
- * Builds the ANSI sequence from codes
158
- * @private
159
- * @param {number[]} codes - Array of ANSI codes
160
- * @returns {string} The ANSI sequence (e.g., "\x1B[1;31m")
161
- */
162
- const _build = (codes) => {
163
- if (codes.length === 0) return "";
164
- return `\x1B[${codes.join(";")}m`;
165
- };
166
-
167
- /**
168
- * Applies ANSI formatting to a string, replacing content between delimiters
169
- * @private
170
- * @param {string} str - String to format (e.g., "<%red hello%>")
171
- * @returns {string} String with ANSI sequences
172
- */
173
- const _apply = (str) => {
174
- const esc_start = _options.delimiter.open.replace(
175
- /[.*+?^${}()|[\]\\]/g,
176
- "\\$&",
177
- );
178
- const esc_end = _options.delimiter.close.replace(
179
- /[.*+?^${}()|[\]\\]/g,
180
- "\\$&",
181
- );
182
-
183
- return str.replace(
184
- new RegExp(`${esc_start}([\\s\\S]*?)${esc_end}`, "g"),
185
- (match, content) => {
186
- const { codes, text } = _parse(content);
187
- if (codes.length === 0 || text === "") return text;
188
-
189
- if (text.includes("\n")) {
190
- return text
191
- .split(/\r?\n/)
192
- .filter((line) => line.trim())
193
- .map((line) => `${_build(codes)}${line}\x1B[0m`)
194
- .join("\n");
195
- }
196
-
197
- return `${_build(codes)}${text}\x1B[0m`;
198
- },
199
- );
200
- };
201
-
202
- /**
203
- * Generates the formatted timestamp using the current delimiter
204
- * @private
205
- * @returns {string} Formatted timestamp (e.g., "<%gray [08:21:54]%>") or "" if disabled
206
- */
207
- const _timestamp = () => {
208
- if (!_options.timestamp) return "";
209
- return _apply(
210
- `${_options.delimiter.open}gray [${new Date().toLocaleTimeString(undefined, { hour12: false })}]${_options.delimiter.close}`,
211
- );
212
- };
213
-
214
- /**
215
- * Returns the current indentation based on the number of active groups
216
- * @private
217
- * @returns {string} Indentation spaces (2 spaces per group)
218
- */
219
- const _indent = () => {
220
- return " ".repeat(_groups.length);
221
- };
222
-
223
- module.exports = {
224
- /**
225
- * Enables or disables timestamp in logs
226
- * @param {boolean} value - True to enable, false to disable
227
- * @returns {Logger} The same Logger instance (for chaining)
228
- */
229
- setTimestamp(value) {
230
- _options.timestamp = value;
231
- return /** @type {Logger} */ (this);
232
- },
233
-
234
- /**
235
- * Changes the delimiters used for text formatting
236
- * @param {object} options - Delimiter options
237
- * @param {string} [options.open] - New opening delimiter
238
- * @param {string} [options.close] - New closing delimiter
239
- * @returns {Logger} The same Logger instance (for chaining)
240
- */
241
- setDelimiter(options = {}) {
242
- _options.delimiter = {
243
- ..._options.delimiter,
244
- ...options,
245
- };
246
- return /** @type {Logger} */ (this);
247
- },
248
-
249
- /**
250
- * Prints a message to the console with ANSI formatting
251
- * @param {*} content - Content to print (string, object, or other)
252
- * @returns {Logger} The same Logger instance (for chaining)
253
- */
254
- log(content) {
255
- let str;
256
- if (typeof content === "string") {
257
- str = content;
258
- } else if (typeof content === "object") {
259
- str = JSON.stringify(content, null, 2);
260
- } else {
261
- str = String(content);
262
- }
263
-
264
- const prefix = `${_timestamp()}${_indent()}`;
265
- const fullStr = prefix ? `${prefix} ${str}` : str;
266
-
267
- const formatted = _apply(fullStr);
268
-
269
- if (formatted.includes("\n")) {
270
- const lines = formatted.split("\n");
271
-
272
- if (lines[0].trim()) console.log(lines[0]);
273
-
274
- const indentOnly = _indent();
275
- for (let i = 1; i < lines.length; i++) {
276
- if (lines[i].trim()) {
277
- console.log(`${indentOnly}${lines[i]}`);
278
- }
279
- }
280
- } else {
281
- console.log(formatted);
282
- }
283
-
284
- return /** @type {Logger} */ (this);
285
- },
286
-
287
- /**
288
- * Start a log group (displays a label and adds indentation)
289
- * @param {string} label - The group name to display
290
- * @returns {Logger} The same Logger instance (for chaining)
291
- */
292
- group(label) {
293
- _groups.push(label);
294
- const prefix = `${_timestamp()}${_indent()}`;
295
- console.log(prefix ? `${prefix}▼ ${label}` : `▼ ${label}`);
296
- return /** @type {Logger} */ (this);
297
- },
298
-
299
- /**
300
- * End the current log group
301
- * @returns {Logger} The same Logger instance (for chaining)
302
- */
303
- groupEnd() {
304
- if (_groups.length > 0) _groups.pop();
305
- return /** @type {Logger} */ (this);
306
- },
307
- };
1
+ "use strict";
2
+
3
+ /**
4
+ * A console logger that works with zero setup: levels, colors, a small
5
+ * markup for styling, groups, timers and tables. In production, switch to
6
+ * one JSON object per line with `LOG_FORMAT=json`, and write to rotating
7
+ * files if you need to.
8
+ *
9
+ * Markup: `<% red bold Error:%> disk is full`. Any style name works, plus
10
+ * `#ff8800`, `bg#1e1e2e`, `rgb(255, 128, 0)` and `bgRgb(...)`.
11
+ *
12
+ * Levels, from chatty to severe: `trace`, `debug`, `info`, `warn`, `error`,
13
+ * `fatal`. The default is `info`, or `debug` when `DEBUG` is set; `LOG_LEVEL`
14
+ * overrides it. Warnings and errors go to stderr.
15
+ *
16
+ * @example
17
+ * nc.info("Server listening on port 3000");
18
+ * nc.warn("Cache is almost full");
19
+ * nc.error(new Error("Payment failed", { cause: err }));
20
+ * nc.log("<% cyan bold Tip:%> press <% bgWhite black q %> to quit");
21
+ */
22
+
23
+ const fs = require("node:fs");
24
+ const path = require("node:path");
25
+ const util = require("node:util");
26
+ const color = require("./Color.js");
27
+ const time = require("./Time.js");
28
+
29
+ /**
30
+ * Log levels, from the most verbose to the most severe. `silent` hides everything.
31
+ * @typedef {"trace" | "debug" | "info" | "warn" | "error" | "fatal" | "silent"} LogLevel
32
+ */
33
+
34
+ /**
35
+ * Where logs go: `process.stdout`, a file stream, or anything with a
36
+ * `write(text)` method.
37
+ * @typedef {{ write(text: string): unknown }} LogStream
38
+ */
39
+
40
+ /**
41
+ * Writing logs to a file.
42
+ * @typedef {object} LogFileOptions
43
+ * @property {string} path The file to append to. Folders are created, colors removed.
44
+ * @property {string | number} [maxSize] Rotate past this size, like `"10MB"`.
45
+ * @property {number} [maxFiles] How many rotated files to keep. Defaults to `5`.
46
+ */
47
+
48
+ /**
49
+ * Options for `createLogger()` and `configure()`.
50
+ * @typedef {object} LoggerOptions
51
+ * @property {LogLevel} [level] Hide messages below this level. Defaults to `LOG_LEVEL`, else `"info"`.
52
+ * @property {string} [scope] Shown before each message, like `[db]`. Child loggers extend it (`db:pool`).
53
+ * @property {boolean | string} [timestamp] `true` shows `[HH:mm:ss]`, `false` hides it, a string is a `time.format` pattern. Defaults to `true`.
54
+ * @property {"pretty" | "json"} [format] `"json"` writes one object per line, for log collectors. Defaults to `LOG_FORMAT`, else `"pretty"`.
55
+ * @property {{ open?: string, close?: string }} [delimiter] Markup delimiters. Defaults to `<%` and `%>`.
56
+ * @property {boolean} [colors] Force colors on or off. By default, it depends on the terminal.
57
+ * @property {LogStream} [stdout] Where `trace` to `info` go. Defaults to `process.stdout`.
58
+ * @property {LogStream} [stderr] Where `warn`, `error` and `fatal` go. Defaults to `process.stderr`.
59
+ * @property {string | LogFileOptions} [file] Also write every message to a file.
60
+ * @property {Record<string, unknown>} [fields] Added to every JSON record, like a service name.
61
+ */
62
+
63
+ /**
64
+ * A logger from `createLogger()`. Every method returns the logger, so calls chain.
65
+ *
66
+ * @typedef {object} Logger
67
+ * @property {(...args: unknown[]) => Logger} log A plain message, no badge. Supports markup.
68
+ * @property {(...args: unknown[]) => Logger} trace Very detailed diagnostics, hidden unless the level is `trace`.
69
+ * @property {(...args: unknown[]) => Logger} debug Diagnostics, hidden unless the level is `debug` or lower.
70
+ * @property {(...args: unknown[]) => Logger} info An `ℹ INFO` message.
71
+ * @property {(...args: unknown[]) => Logger} success A `✔ OK` message.
72
+ * @property {(...args: unknown[]) => Logger} warn A `⚠ WARN` message, on stderr.
73
+ * @property {(...args: unknown[]) => Logger} error An `✖ ERROR` message, on stderr. Errors show their stack and cause.
74
+ * @property {(...args: unknown[]) => Logger} fatal An `✖ FATAL` message, on stderr.
75
+ * @property {(label?: string) => Logger} group Prints a label and indents what follows. Groups nest.
76
+ * @property {() => Logger} groupEnd Closes the current group.
77
+ * @property {(label?: string) => Logger} timeStart Starts a named timer.
78
+ * @property {(label?: string) => Logger} timeEnd Prints how long since `timeStart(label)`.
79
+ * @property {(rows: Array<Record<string, unknown>> | unknown[][], options?: import("./Cli").TableOptions) => Logger} table Prints rows as a table.
80
+ * @property {(text: string, options?: import("./Cli").BoxOptions) => Logger} box Prints text in a box.
81
+ * @property {(title?: string) => Logger} divider Prints a horizontal line.
82
+ * @property {(scope: string, options?: LoggerOptions) => Logger} child A logger with the same settings and a nested scope. Its `fields` are added to the parent's.
83
+ * @property {(level: LogLevel) => Logger} setLevel Changes the minimum level.
84
+ * @property {() => LogLevel} getLevel The current minimum level.
85
+ * @property {(level: LogLevel) => boolean} isLevelEnabled Would this level be printed? Skip expensive debug output when it wouldn't.
86
+ * @property {(options: LoggerOptions) => Logger} configure Changes several settings at once.
87
+ */
88
+
89
+ /** @type {Record<LogLevel, number>} */
90
+ const LEVELS = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60, silent: Infinity };
91
+
92
+ /** @type {Record<string, { label: string, style: string, stream: "stdout" | "stderr", level: LogLevel }>} */
93
+ const BADGES = {
94
+ log: { label: "", style: "", stream: "stdout", level: "info" },
95
+ trace: { label: "… TRACE", style: "gray dim", stream: "stdout", level: "trace" },
96
+ debug: { label: "● DEBUG", style: "gray bold", stream: "stdout", level: "debug" },
97
+ info: { label: "ℹ INFO", style: "cyan bold", stream: "stdout", level: "info" },
98
+ success: { label: "✔ OK", style: "greenBright bold", stream: "stdout", level: "info" },
99
+ warn: { label: "⚠ WARN", style: "yellowBright bold", stream: "stderr", level: "warn" },
100
+ error: { label: "✖ ERROR", style: "redBright bold", stream: "stderr", level: "error" },
101
+ fatal: { label: "✖ FATAL", style: "bgRed whiteBright bold", stream: "stderr", level: "fatal" },
102
+ };
103
+
104
+ const STYLE_NAMES = new Set([
105
+ "reset", "bold", "dim", "italic", "underline", "overline", "inverse", "hidden", "strikethrough",
106
+ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray", "grey",
107
+ "blackBright", "redBright", "greenBright", "yellowBright", "blueBright", "magentaBright", "cyanBright", "whiteBright",
108
+ "bgBlack", "bgRed", "bgGreen", "bgYellow", "bgBlue", "bgMagenta", "bgCyan", "bgWhite", "bgGray", "bgGrey",
109
+ "bgBlackBright", "bgRedBright", "bgGreenBright", "bgYellowBright", "bgBlueBright", "bgMagentaBright", "bgCyanBright", "bgWhiteBright",
110
+ ]);
111
+
112
+ /** @returns {LogLevel} */
113
+ const _defaultLevel = () => {
114
+ const fromEnv = String(process.env.LOG_LEVEL ?? "").toLowerCase();
115
+ if (fromEnv in LEVELS) return /** @type {LogLevel} */ (fromEnv);
116
+ return process.env.DEBUG || process.env.NODE_DEBUG ? "debug" : "info";
117
+ };
118
+
119
+ /** @param {string} value */
120
+ const _escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
121
+
122
+ /**
123
+ * @param {import("./Color").ColorModule} palette
124
+ * @param {string[]} tokens
125
+ * @param {string} text
126
+ * @returns {string}
127
+ */
128
+ const _style = (palette, tokens, text) => {
129
+ /** @type {any} */
130
+ let chain = palette;
131
+ for (const token of tokens) {
132
+ const name = token === "normal" ? "reset" : token;
133
+ let m;
134
+ if (STYLE_NAMES.has(name)) chain = chain[name];
135
+ else if ((m = /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i.exec(name))) chain = chain.rgb(Number(m[1]), Number(m[2]), Number(m[3]));
136
+ else if ((m = /^bgrgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i.exec(name))) chain = chain.bgRgb(Number(m[1]), Number(m[2]), Number(m[3]));
137
+ else if (/^#[0-9a-f]{3,8}$/i.test(name)) chain = chain.hex(name);
138
+ else if (/^bg#[0-9a-f]{3,8}$/i.test(name)) chain = chain.bgHex(name.slice(2));
139
+ }
140
+ return chain === palette ? text : chain(text);
141
+ };
142
+
143
+ /**
144
+ * @param {string} token
145
+ */
146
+ const _isStyleToken = (token) =>
147
+ STYLE_NAMES.has(token) ||
148
+ token === "normal" ||
149
+ /^(bg)?rgb\(\d{1,3},\d{1,3},\d{1,3}\)$/i.test(token) ||
150
+ /^(bg)?#[0-9a-f]{3,8}$/i.test(token);
151
+
152
+ /**
153
+ * Renders `<% styles text %>` markup.
154
+ * @param {string} input
155
+ * @param {{ open: string, close: string }} delimiter
156
+ * @param {import("./Color").ColorModule} palette
157
+ * @returns {string}
158
+ */
159
+ const _markup = (input, delimiter, palette) => {
160
+ if (!input.includes(delimiter.open)) return input;
161
+ const regex = new RegExp(`${_escape(delimiter.open)}([\\s\\S]*?)${_escape(delimiter.close)}`, "g");
162
+ return input.replace(regex, (_, content) => {
163
+ const normalized = String(content).replace(/(bg)?rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/gi, (m0, bg, r, g, b) => `${bg ?? ""}rgb(${r},${g},${b})`);
164
+ /** @type {string[]} */
165
+ const tokens = [];
166
+ let rest = normalized.replace(/^\s+/, "");
167
+ for (;;) {
168
+ const match = /^(\S+)(\s+|$)/.exec(rest);
169
+ if (!match || !_isStyleToken(match[1])) break;
170
+ tokens.push(match[1]);
171
+ rest = rest.slice(match[0].length);
172
+ }
173
+ return _style(palette, tokens, rest);
174
+ });
175
+ };
176
+
177
+ /**
178
+ * @param {unknown} value
179
+ * @param {boolean} colors
180
+ * @returns {string}
181
+ */
182
+ const _stringify = (value, colors) => {
183
+ if (typeof value === "string") return value;
184
+ if (value instanceof Error) {
185
+ let text = value.stack ?? `${value.name}: ${value.message}`;
186
+ let cause = /** @type {any} */ (value).cause;
187
+ let depth = 0;
188
+ while (cause !== undefined && depth++ < 5) {
189
+ text += `\nCaused by: ${cause instanceof Error ? cause.stack ?? cause.message : util.inspect(cause, { colors })}`;
190
+ cause = cause instanceof Error ? /** @type {any} */ (cause).cause : undefined;
191
+ }
192
+ return text;
193
+ }
194
+ if (value !== null && typeof value === "object") {
195
+ const proto = Object.getPrototypeOf(value);
196
+ if (Array.isArray(value) || proto === Object.prototype || proto === null) {
197
+ try {
198
+ const json = JSON.stringify(value, (_, v) => (typeof v === "bigint" ? `${v}n` : v), 2);
199
+ if (json !== undefined) return json;
200
+ } catch {
201
+ // circular or exotic: fall back to util.inspect
202
+ }
203
+ }
204
+ return util.inspect(value, { depth: 6, colors, breakLength: 100 });
205
+ }
206
+ return typeof value === "symbol" || typeof value === "function" ? util.inspect(value, { colors }) : String(value);
207
+ };
208
+
209
+ /**
210
+ * @param {string} level
211
+ * @param {unknown[]} args
212
+ * @param {string | undefined} scope
213
+ * @param {Record<string, unknown>} fields
214
+ * @returns {string}
215
+ */
216
+ const _json = (level, args, scope, fields) => {
217
+ /** @type {Record<string, unknown>} */
218
+ const record = { time: new Date().toISOString(), level, ...fields };
219
+ if (scope) record.scope = scope;
220
+ const messages = [];
221
+ for (const arg of args) {
222
+ if (arg instanceof Error) {
223
+ record.err = { name: arg.name, message: arg.message, stack: arg.stack, code: /** @type {any} */ (arg).code, cause: /** @type {any} */ (arg).cause instanceof Error ? /** @type {any} */ (arg).cause.message : /** @type {any} */ (arg).cause };
224
+ } else if (arg !== null && typeof arg === "object" && !Array.isArray(arg)) {
225
+ Object.assign(record, arg);
226
+ } else {
227
+ messages.push(typeof arg === "string" ? color.strip(arg) : _stringify(arg, false));
228
+ }
229
+ }
230
+ record.msg = messages.join(" ");
231
+ try {
232
+ return JSON.stringify(record, (_, v) => (typeof v === "bigint" ? v.toString() : v));
233
+ } catch {
234
+ return JSON.stringify({ time: record.time, level, msg: `${record.msg} [unserializable fields]` });
235
+ }
236
+ };
237
+
238
+ /**
239
+ * Appends a line, rotating the file when it gets too big.
240
+ * @param {LogFileOptions} file
241
+ * @param {string} line
242
+ */
243
+ const _writeFile = (file, line) => {
244
+ try {
245
+ const target = path.resolve(file.path);
246
+ fs.mkdirSync(path.dirname(target), { recursive: true });
247
+ if (file.maxSize !== undefined) {
248
+ const limit = typeof file.maxSize === "number" ? file.maxSize : require("./Num.js").parseBytes(file.maxSize);
249
+ let size = 0;
250
+ try {
251
+ size = fs.statSync(target).size;
252
+ } catch {
253
+ size = 0;
254
+ }
255
+ if (size > 0 && size + Buffer.byteLength(line) > limit) {
256
+ const keep = Math.max(1, file.maxFiles ?? 5);
257
+ for (let i = keep - 1; i >= 1; i--) {
258
+ if (fs.existsSync(`${target}.${i}`)) fs.renameSync(`${target}.${i}`, `${target}.${i + 1}`);
259
+ }
260
+ if (fs.existsSync(`${target}.${keep + 1}`)) fs.rmSync(`${target}.${keep + 1}`);
261
+ fs.renameSync(target, `${target}.1`);
262
+ }
263
+ }
264
+ fs.appendFileSync(target, `${line}\n`);
265
+ } catch {
266
+ // Logging must never crash the application.
267
+ }
268
+ };
269
+
270
+ /**
271
+ * @param {LoggerOptions} options
272
+ * @returns {Logger}
273
+ */
274
+ const _create = (options) => {
275
+ const settings = {
276
+ level: options.level ?? _defaultLevel(),
277
+ scope: options.scope,
278
+ timestamp: options.timestamp ?? true,
279
+ format: options.format ?? (process.env.LOG_FORMAT === "json" ? "json" : "pretty"),
280
+ delimiter: { open: "<%", close: "%>", ...options.delimiter },
281
+ colors: options.colors,
282
+ stdout: options.stdout,
283
+ stderr: options.stderr,
284
+ file: typeof options.file === "string" ? { path: options.file } : options.file,
285
+ fields: options.fields ?? {},
286
+ };
287
+ /** @type {string[]} */
288
+ const groups = [];
289
+ /** @type {Map<string, bigint>} */
290
+ const timers = new Map();
291
+
292
+ /** @param {"stdout" | "stderr"} which */
293
+ const streamOf = (which) => (which === "stderr" ? settings.stderr ?? process.stderr : settings.stdout ?? process.stdout);
294
+
295
+ /** @param {"stdout" | "stderr"} which */
296
+ const paletteOf = (which) => {
297
+ if (settings.colors === true) return color.create(color.level > 0 ? color.level : 3);
298
+ if (settings.colors === false) return color.create(0);
299
+ const stream = streamOf(which);
300
+ return stream === process.stdout || stream === process.stderr ? color.create(color.detect(/** @type {any} */ (stream))) : color.create(0);
301
+ };
302
+
303
+ const plain = color.create(0);
304
+
305
+ /**
306
+ * @param {keyof typeof BADGES} kind
307
+ * @param {unknown[]} args
308
+ */
309
+ const emit = (kind, args) => {
310
+ const badge = BADGES[kind];
311
+ if (LEVELS[badge.level] < LEVELS[settings.level] || settings.level === "silent") return;
312
+ if (settings.format === "json") {
313
+ const line = _json(kind === "log" ? "info" : kind, args, settings.scope, settings.fields);
314
+ streamOf(badge.stream).write(`${line}\n`);
315
+ if (settings.file) _writeFile(settings.file, line);
316
+ return;
317
+ }
318
+ const render = (/** @type {import("./Color").ColorModule} */ palette) => {
319
+ const colors = palette.level > 0;
320
+ const stamp = settings.timestamp
321
+ ? _style(palette, ["gray"], `[${time.format(new Date(), typeof settings.timestamp === "string" ? settings.timestamp : "HH:mm:ss")}]`)
322
+ : "";
323
+ const scope = settings.scope ? _style(palette, ["magenta"], `[${settings.scope}]`) : "";
324
+ const label = badge.label ? _style(palette, badge.style.split(" "), badge.label) : "";
325
+ const body = args
326
+ .map((arg) => _markup(_stringify(arg, colors), settings.delimiter, palette))
327
+ .map((text) => (kind === "debug" || kind === "trace" ? _style(palette, ["gray"], text) : text))
328
+ .join(" ");
329
+ const indent = " ".repeat(groups.length);
330
+ const tags = [scope, label].filter(Boolean).join(" ");
331
+ const prefix = `${stamp ? `${stamp} ` : ""}${indent}${tags ? `${tags} ` : ""}`;
332
+ const lines = body.replace(/^(\s*\r?\n)+|(\r?\n\s*)+$/g, "").split(/\r?\n/);
333
+ const pad = " ".repeat(color.width(stamp ? `${stamp} ` : "") + indent.length);
334
+ return lines.map((line, i) => (i === 0 ? `${prefix}${line}`.trimEnd() : pad + line)).join("\n");
335
+ };
336
+ streamOf(badge.stream).write(`${render(paletteOf(badge.stream))}\n`);
337
+ if (settings.file) _writeFile(settings.file, render(plain));
338
+ };
339
+
340
+ /** @type {Logger} */
341
+ const logger = {
342
+ log: (...args) => (emit("log", args), logger),
343
+ trace: (...args) => (emit("trace", args), logger),
344
+ debug: (...args) => (emit("debug", args), logger),
345
+ info: (...args) => (emit("info", args), logger),
346
+ success: (...args) => (emit("success", args), logger),
347
+ warn: (...args) => (emit("warn", args), logger),
348
+ error: (...args) => (emit("error", args), logger),
349
+ fatal: (...args) => (emit("fatal", args), logger),
350
+ group: (label = "") => {
351
+ emit("log", [`▼ ${label}`.trimEnd()]);
352
+ groups.push(label);
353
+ return logger;
354
+ },
355
+ groupEnd: () => {
356
+ groups.pop();
357
+ return logger;
358
+ },
359
+ timeStart: (label = "default") => {
360
+ timers.set(label, process.hrtime.bigint());
361
+ return logger;
362
+ },
363
+ timeEnd: (label = "default") => {
364
+ const start = timers.get(label);
365
+ if (start === undefined) return logger.warn(`Timer "${label}" does not exist`);
366
+ timers.delete(label);
367
+ const ms = Number(process.hrtime.bigint() - start) / 1e6;
368
+ return logger.log(`${label}: ${ms < 1000 ? `${Math.round(ms * 100) / 100}ms` : time.formatDuration(ms, { units: 2 })}`);
369
+ },
370
+ table: (rows, tableOptions) => logger.log(require("./Cli.js").table(rows, { ...tableOptions, colors: paletteOf("stdout").level > 0 })),
371
+ box: (text, boxOptions) => logger.log(require("./Cli.js").box(text, { ...boxOptions, colors: paletteOf("stdout").level > 0 })),
372
+ divider: (title) => {
373
+ const columns = /** @type {any} */ (streamOf("stdout")).columns ?? 80;
374
+ const width = Math.max(10, Math.min(columns, 100) - (settings.timestamp ? 11 : 0) - groups.length * 2);
375
+ const text = title ? `── ${title} ${"─".repeat(Math.max(0, width - color.width(title) - 4))}` : "─".repeat(width);
376
+ return logger.log(`${settings.delimiter.open}gray ${text}${settings.delimiter.close}`);
377
+ },
378
+ child: (scope, childOptions = {}) =>
379
+ _create({
380
+ ...settings,
381
+ ...childOptions,
382
+ fields: { ...settings.fields, ...childOptions.fields },
383
+ scope: settings.scope ? `${settings.scope}:${scope}` : scope,
384
+ }),
385
+ setLevel: (level) => {
386
+ if (!(level in LEVELS)) throw new RangeError(`Unknown log level "${level}"`);
387
+ settings.level = level;
388
+ return logger;
389
+ },
390
+ getLevel: () => settings.level,
391
+ isLevelEnabled: (level) => settings.level !== "silent" && LEVELS[level] >= LEVELS[settings.level],
392
+ configure: (next) => {
393
+ if (next.level !== undefined) logger.setLevel(next.level);
394
+ if (next.scope !== undefined) settings.scope = next.scope;
395
+ if (next.timestamp !== undefined) settings.timestamp = next.timestamp;
396
+ if (next.format !== undefined) settings.format = next.format;
397
+ if (next.delimiter !== undefined) settings.delimiter = { ...settings.delimiter, ...next.delimiter };
398
+ if (next.colors !== undefined) settings.colors = next.colors;
399
+ if (next.stdout !== undefined) settings.stdout = next.stdout;
400
+ if (next.stderr !== undefined) settings.stderr = next.stderr;
401
+ if (next.file !== undefined) settings.file = typeof next.file === "string" ? { path: next.file } : next.file;
402
+ if (next.fields !== undefined) settings.fields = next.fields;
403
+ return logger;
404
+ },
405
+ };
406
+ return logger;
407
+ };
408
+
409
+ const _default = _create({});
410
+
411
+ /**
412
+ * Creates a logger with its own settings.
413
+ *
414
+ * @example
415
+ * const log = nc.createLogger({ scope: "api", timestamp: "HH:mm:ss.SSS" });
416
+ * log.info("GET /users 200"); // [14:30:05.120] [api] ℹ INFO GET /users 200
417
+ * log.child("db").debug("query took 12ms");
418
+ *
419
+ * const prod = nc.createLogger({ format: "json", fields: { service: "billing" } });
420
+ * prod.error("charge failed", { orderId: 42 }, error);
421
+ *
422
+ * @param {LoggerOptions} [options]
423
+ * @returns {Logger}
424
+ */
425
+ function createLogger(options = {}) {
426
+ return _create(options);
427
+ }
428
+
429
+ /**
430
+ * Prints a message without a badge. Strings support markup, objects are
431
+ * pretty-printed, and several arguments are joined with spaces.
432
+ *
433
+ * @example
434
+ * nc.log("<% green ✔ Saved %> in <% bold 12ms %>");
435
+ * nc.log("user:", { id: 1, roles: ["admin"] });
436
+ *
437
+ * @param {...unknown} args
438
+ * @returns {typeof import("./Logger")}
439
+ */
440
+ function log(...args) {
441
+ _default.log(...args);
442
+ return module.exports;
443
+ }
444
+
445
+ /**
446
+ * Very detailed diagnostics, shown only at the `trace` level.
447
+ *
448
+ * @param {...unknown} args
449
+ * @returns {typeof import("./Logger")}
450
+ */
451
+ function trace(...args) {
452
+ _default.trace(...args);
453
+ return module.exports;
454
+ }
455
+
456
+ /**
457
+ * Diagnostics for developers, shown at the `debug` level (the default when
458
+ * `DEBUG` is set).
459
+ *
460
+ * @example
461
+ * nc.debug("cache miss", { key });
462
+ *
463
+ * @param {...unknown} args
464
+ * @returns {typeof import("./Logger")}
465
+ */
466
+ function debug(...args) {
467
+ _default.debug(...args);
468
+ return module.exports;
469
+ }
470
+
471
+ /**
472
+ * An informational message.
473
+ *
474
+ * @example
475
+ * nc.info("Server listening on http://localhost:3000");
476
+ *
477
+ * @param {...unknown} args
478
+ * @returns {typeof import("./Logger")}
479
+ */
480
+ function info(...args) {
481
+ _default.info(...args);
482
+ return module.exports;
483
+ }
484
+
485
+ /**
486
+ * A success message.
487
+ *
488
+ * @example
489
+ * nc.success("Database connected");
490
+ *
491
+ * @param {...unknown} args
492
+ * @returns {typeof import("./Logger")}
493
+ */
494
+ function success(...args) {
495
+ _default.success(...args);
496
+ return module.exports;
497
+ }
498
+
499
+ /**
500
+ * A warning, written to stderr.
501
+ *
502
+ * @param {...unknown} args
503
+ * @returns {typeof import("./Logger")}
504
+ */
505
+ function warn(...args) {
506
+ _default.warn(...args);
507
+ return module.exports;
508
+ }
509
+
510
+ /**
511
+ * An error, written to stderr. Error objects show their stack trace and
512
+ * their chain of causes.
513
+ *
514
+ * @example
515
+ * nc.error(new Error("Cannot save order", { cause: dbError }));
516
+ *
517
+ * @param {...unknown} args
518
+ * @returns {typeof import("./Logger")}
519
+ */
520
+ function error(...args) {
521
+ _default.error(...args);
522
+ return module.exports;
523
+ }
524
+
525
+ /**
526
+ * A fatal error, written to stderr. It doesn't stop the process; call
527
+ * `process.exit(1)` yourself if you mean to.
528
+ *
529
+ * @param {...unknown} args
530
+ * @returns {typeof import("./Logger")}
531
+ */
532
+ function fatal(...args) {
533
+ _default.fatal(...args);
534
+ return module.exports;
535
+ }
536
+
537
+ /**
538
+ * Prints a label and indents what follows, until `groupEnd()`. Groups nest.
539
+ *
540
+ * @example
541
+ * nc.group("Startup").info("Loading config");
542
+ * nc.group("Database").success("Connected").groupEnd();
543
+ * nc.groupEnd();
544
+ *
545
+ * @param {string} [label]
546
+ * @returns {typeof import("./Logger")}
547
+ */
548
+ function group(label) {
549
+ _default.group(label);
550
+ return module.exports;
551
+ }
552
+
553
+ /**
554
+ * Closes the current group.
555
+ *
556
+ * @returns {typeof import("./Logger")}
557
+ */
558
+ function groupEnd() {
559
+ _default.groupEnd();
560
+ return module.exports;
561
+ }
562
+
563
+ /**
564
+ * Starts a named timer. `timeEnd()` prints the elapsed time.
565
+ *
566
+ * @example
567
+ * nc.timeStart("import");
568
+ * await importProducts();
569
+ * nc.timeEnd("import"); // "import: 1.24s"
570
+ *
571
+ * @param {string} [label="default"]
572
+ * @returns {typeof import("./Logger")}
573
+ */
574
+ function timeStart(label) {
575
+ _default.timeStart(label);
576
+ return module.exports;
577
+ }
578
+
579
+ /**
580
+ * Prints the time since `timeStart()` with the same label.
581
+ *
582
+ * @param {string} [label="default"]
583
+ * @returns {typeof import("./Logger")}
584
+ */
585
+ function timeEnd(label) {
586
+ _default.timeEnd(label);
587
+ return module.exports;
588
+ }
589
+
590
+ /**
591
+ * Prints rows as a table.
592
+ *
593
+ * @example
594
+ * nc.table([{ name: "Ada", role: "admin" }, { name: "Bob", role: "user" }]);
595
+ *
596
+ * @param {Array<Record<string, unknown>> | unknown[][]} rows
597
+ * @param {import("./Cli").TableOptions} [options]
598
+ * @returns {typeof import("./Logger")}
599
+ */
600
+ function table(rows, options) {
601
+ _default.table(rows, options);
602
+ return module.exports;
603
+ }
604
+
605
+ /**
606
+ * Prints text in a box. Nice for startup banners.
607
+ *
608
+ * @example
609
+ * nc.box("Server ready\nhttp://localhost:3000", { title: "my-app", borderColor: "green" });
610
+ *
611
+ * @param {string} text
612
+ * @param {import("./Cli").BoxOptions} [options]
613
+ * @returns {typeof import("./Logger")}
614
+ */
615
+ function box(text, options) {
616
+ _default.box(text, options);
617
+ return module.exports;
618
+ }
619
+
620
+ /**
621
+ * Prints a horizontal line, with an optional title in it.
622
+ *
623
+ * @param {string} [title]
624
+ * @returns {typeof import("./Logger")}
625
+ */
626
+ function divider(title) {
627
+ _default.divider(title);
628
+ return module.exports;
629
+ }
630
+
631
+ /**
632
+ * Sets the minimum level.
633
+ *
634
+ * @example
635
+ * nc.setLevel("warn"); // warnings and errors only
636
+ * nc.setLevel("silent"); // nothing
637
+ *
638
+ * @param {LogLevel} level
639
+ * @returns {typeof import("./Logger")}
640
+ * @throws {RangeError} For an unknown level.
641
+ */
642
+ function setLevel(level) {
643
+ _default.setLevel(level);
644
+ return module.exports;
645
+ }
646
+
647
+ /**
648
+ * The current minimum level.
649
+ *
650
+ * @returns {LogLevel}
651
+ */
652
+ function getLevel() {
653
+ return _default.getLevel();
654
+ }
655
+
656
+ /**
657
+ * Would this level be printed? Use it to skip building expensive debug
658
+ * output.
659
+ *
660
+ * @example
661
+ * if (nc.isLevelEnabled("debug")) nc.debug(buildHugeReport());
662
+ *
663
+ * @param {LogLevel} level
664
+ * @returns {boolean}
665
+ */
666
+ function isLevelEnabled(level) {
667
+ return _default.isLevelEnabled(level);
668
+ }
669
+
670
+ /**
671
+ * Changes several settings at once.
672
+ *
673
+ * @example
674
+ * nc.configure({ level: "debug", file: "logs/app.log" });
675
+ * nc.configure({ format: "json", fields: { service: "api" } });
676
+ *
677
+ * @param {LoggerOptions} options
678
+ * @returns {typeof import("./Logger")}
679
+ */
680
+ function configure(options) {
681
+ _default.configure(options);
682
+ return module.exports;
683
+ }
684
+
685
+ /**
686
+ * Shows or hides timestamps, or sets their pattern.
687
+ *
688
+ * @example
689
+ * nc.setTimestamp(false);
690
+ * nc.setTimestamp("YYYY-MM-DD HH:mm:ss.SSS");
691
+ *
692
+ * @param {boolean | string} value
693
+ * @returns {typeof import("./Logger")}
694
+ */
695
+ function setTimestamp(value) {
696
+ _default.configure({ timestamp: value });
697
+ return module.exports;
698
+ }
699
+
700
+ /**
701
+ * Changes the markup delimiters, for when `<%` clashes with a template
702
+ * engine.
703
+ *
704
+ * @example
705
+ * nc.setDelimiter({ open: "{{", close: "}}" });
706
+ * nc.log("{{red Hello}} world");
707
+ *
708
+ * @param {{ open?: string, close?: string }} [options]
709
+ * @returns {typeof import("./Logger")}
710
+ */
711
+ function setDelimiter(options = {}) {
712
+ _default.configure({ delimiter: options });
713
+ return module.exports;
714
+ }
715
+
716
+ module.exports = {
717
+ log,
718
+ trace,
719
+ debug,
720
+ info,
721
+ success,
722
+ warn,
723
+ error,
724
+ fatal,
725
+ group,
726
+ groupEnd,
727
+ timeStart,
728
+ timeEnd,
729
+ table,
730
+ box,
731
+ divider,
732
+ setLevel,
733
+ getLevel,
734
+ isLevelEnabled,
735
+ configure,
736
+ setTimestamp,
737
+ setDelimiter,
738
+ createLogger,
739
+ };