@ix-xs/node-comfort 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/Logger.js CHANGED
@@ -1,378 +1,963 @@
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
- /**
224
- * Coerces any value to a string for the level helpers (info/warn/...).
225
- * @private
226
- * @param {*} content - Value to stringify.
227
- * @returns {string} The string representation.
228
- */
229
- const _stringify = (content) => {
230
- if (typeof content === "string") return content;
231
- if (typeof content === "object" && content !== null) {
232
- try {
233
- return JSON.stringify(content, null, 2);
234
- } catch {
235
- return String(content);
236
- }
237
- }
238
- return String(content);
239
- };
240
-
241
- module.exports = {
242
- /**
243
- * Enables or disables timestamp in logs
244
- * @param {boolean} value - True to enable, false to disable
245
- * @returns {Logger} The same Logger instance (for chaining)
246
- */
247
- setTimestamp(value) {
248
- _options.timestamp = value;
249
- return /** @type {Logger} */ (this);
250
- },
251
-
252
- /**
253
- * Changes the delimiters used for text formatting
254
- * @param {object} options - Delimiter options
255
- * @param {string} [options.open] - New opening delimiter
256
- * @param {string} [options.close] - New closing delimiter
257
- * @returns {Logger} The same Logger instance (for chaining)
258
- */
259
- setDelimiter(options = {}) {
260
- _options.delimiter = {
261
- ..._options.delimiter,
262
- ...options,
263
- };
264
- return /** @type {Logger} */ (this);
265
- },
266
-
267
- /**
268
- * Prints a message to the console with ANSI formatting
269
- * @param {*} content - Content to print (string, object, or other)
270
- * @returns {Logger} The same Logger instance (for chaining)
271
- */
272
- log(content) {
273
- let str;
274
- if (typeof content === "string") {
275
- str = content;
276
- } else if (typeof content === "object") {
277
- str = JSON.stringify(content, null, 2);
278
- } else {
279
- str = String(content);
280
- }
281
-
282
- const prefix = `${_timestamp()}${_indent()}`;
283
- const fullStr = prefix ? `${prefix} ${str}` : str;
284
-
285
- const formatted = _apply(fullStr);
286
-
287
- if (formatted.includes("\n")) {
288
- const lines = formatted.split("\n");
289
-
290
- if (lines[0].trim()) console.log(lines[0]);
291
-
292
- const indentOnly = _indent();
293
- for (let i = 1; i < lines.length; i++) {
294
- if (lines[i].trim()) {
295
- console.log(`${indentOnly}${lines[i]}`);
296
- }
297
- }
298
- } else {
299
- console.log(formatted);
300
- }
301
-
302
- return /** @type {Logger} */ (this);
303
- },
304
-
305
- /**
306
- * Logs an informational message, prefixed with a cyan `ℹ INFO` label.
307
- * @param {*} content - Content to print.
308
- * @returns {Logger} The same Logger instance (for chaining).
309
- */
310
- info(content) {
311
- return this.log(`<% cyan bold ℹ INFO %> ${_stringify(content)}`);
312
- },
313
-
314
- /**
315
- * Logs a success message, prefixed with a green `✔ OK` label.
316
- * @param {*} content - Content to print.
317
- * @returns {Logger} The same Logger instance (for chaining).
318
- */
319
- success(content) {
320
- return this.log(`<% greenBright bold ✔ OK %> ${_stringify(content)}`);
321
- },
322
-
323
- /**
324
- * Logs a warning, prefixed with a yellow `⚠ WARN` label (writes to stderr).
325
- * @param {*} content - Content to print.
326
- * @returns {Logger} The same Logger instance (for chaining).
327
- */
328
- warn(content) {
329
- return this.log(`<% yellowBright bold ⚠ WARN %> ${_stringify(content)}`);
330
- },
331
-
332
- /**
333
- * Logs an error, prefixed with a red `✖ ERROR` label.
334
- *
335
- * Accepts an `Error` and prints its stack when available.
336
- * @param {*} content - Content or Error to print.
337
- * @returns {Logger} The same Logger instance (for chaining).
338
- */
339
- error(content) {
340
- const text =
341
- content instanceof Error ? content.stack ?? content.message : _stringify(content);
342
- return this.log(`<% redBright bold ✖ ERROR %> ${text}`);
343
- },
344
-
345
- /**
346
- * Logs a debug message (only when `DEBUG`/`NODE_DEBUG` env is set), prefixed
347
- * with a gray `● DEBUG` label.
348
- * @param {*} content - Content to print.
349
- * @returns {Logger} The same Logger instance (for chaining).
350
- */
351
- debug(content) {
352
- if (!process.env.DEBUG && !process.env.NODE_DEBUG) {
353
- return /** @type {Logger} */ (this);
354
- }
355
- return this.log(`<% gray bold ● DEBUG %> <% gray ${_stringify(content)} %>`);
356
- },
357
-
358
- /**
359
- * Start a log group (displays a label and adds indentation)
360
- * @param {string} label - The group name to display
361
- * @returns {Logger} The same Logger instance (for chaining)
362
- */
363
- group(label) {
364
- _groups.push(label);
365
- const prefix = `${_timestamp()}${_indent()}`;
366
- console.log(prefix ? `${prefix}▼ ${label}` : `▼ ${label}`);
367
- return /** @type {Logger} */ (this);
368
- },
369
-
370
- /**
371
- * End the current log group
372
- * @returns {Logger} The same Logger instance (for chaining)
373
- */
374
- groupEnd() {
375
- if (_groups.length > 0) _groups.pop();
376
- return /** @type {Logger} */ (this);
377
- },
378
- };
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`; `LOG_LEVEL` changes it. `DEBUG=1` shows
14
+ * debug messages, and `DEBUG=db,http:*` only the debug and trace messages of
15
+ * the loggers with these scopes. Warnings and errors go to stderr.
16
+ *
17
+ * @example
18
+ * nc.info("Server listening on port 3000");
19
+ * nc.warn("Cache is almost full");
20
+ * nc.error(new Error("Payment failed", { cause: err }));
21
+ * nc.log("<% cyan bold Tip:%> press <% bgWhite black q %> to quit");
22
+ */
23
+
24
+ const fs = require("node:fs");
25
+ const path = require("node:path");
26
+ const util = require("node:util");
27
+ const color = require("./Color.js");
28
+ const { timeConfig } = require("./internal/shared.js");
29
+
30
+ /**
31
+ * Log levels, from the most verbose to the most severe. `silent` hides everything.
32
+ * @typedef {"trace" | "debug" | "info" | "warn" | "error" | "fatal" | "silent"} LogLevel
33
+ */
34
+
35
+ /**
36
+ * Where logs go: `process.stdout`, a file stream, or anything with a
37
+ * `write(text)` method.
38
+ * @typedef {{ write(text: string): unknown }} LogStream
39
+ */
40
+
41
+ /**
42
+ * Writing logs to a file.
43
+ * @typedef {object} LogFileOptions
44
+ * @property {string} path The file to append to. Folders are created, colors removed.
45
+ * @property {string | number} [maxSize] Rotate past this size, like `"10MB"`.
46
+ * @property {number} [maxFiles] How many rotated files to keep. Defaults to `5`.
47
+ */
48
+
49
+ /**
50
+ * A message as `onLog` receives it.
51
+ * @typedef {object} LogEntry
52
+ * @property {Date} time
53
+ * @property {LogLevel} level
54
+ * @property {"log" | "trace" | "debug" | "info" | "success" | "warn" | "error" | "fatal"} kind The method that was called.
55
+ * @property {string | undefined} scope
56
+ * @property {string} message The line as written, without colors or timestamp.
57
+ * @property {unknown[]} args What was passed to the method, untouched.
58
+ */
59
+
60
+ /**
61
+ * Options for `createLogger()` and `configure()`.
62
+ * @typedef {object} LoggerOptions
63
+ * @property {LogLevel} [level] Hide messages below this level. Defaults to `LOG_LEVEL`, else `"info"`.
64
+ * @property {string} [scope] Shown before each message, like `[db]`. Child loggers extend it (`db:pool`).
65
+ * @property {boolean | string} [timestamp] `true` shows `[HH:mm:ss]`, `false` hides it, a string is a `time.format` pattern. Defaults to `true`.
66
+ * @property {"pretty" | "json"} [format] `"json"` writes one object per line, for log collectors. Defaults to `LOG_FORMAT`, else `"pretty"`.
67
+ * @property {{ open?: string, close?: string }} [delimiter] Markup delimiters. Defaults to `<%` and `%>`.
68
+ * @property {boolean} [colors] Force colors on or off. By default, it depends on the terminal.
69
+ * @property {LogStream} [stdout] Where `trace` to `info` go. Defaults to `process.stdout`.
70
+ * @property {LogStream} [stderr] Where `warn`, `error` and `fatal` go. Defaults to `process.stderr`.
71
+ * @property {string | LogFileOptions} [file] Also write every message to a file.
72
+ * @property {Record<string, unknown>} [fields] Added to every JSON record, like a service name.
73
+ * @property {string[]} [redact] Keys whose values are replaced by `"[redacted]"` wherever they appear in logged objects, like `["password", "authorization"]`. Case doesn't matter.
74
+ * @property {(entry: LogEntry) => void | Promise<void>} [onLog] Called for every message that passes the level, to send logs somewhere else. It isn't awaited, and an error it throws (or a promise it rejects) is ignored: logging never breaks the program.
75
+ */
76
+
77
+ /**
78
+ * A logger from `createLogger()`. Every method returns the logger, so calls chain.
79
+ *
80
+ * @typedef {object} Logger
81
+ * @property {(...args: unknown[]) => Logger} log A plain message, no badge. Supports markup.
82
+ * @property {(...args: unknown[]) => Logger} trace Very detailed diagnostics, hidden unless the level is `trace`.
83
+ * @property {(...args: unknown[]) => Logger} debug Diagnostics, hidden unless the level is `debug` or lower.
84
+ * @property {(...args: unknown[]) => Logger} info An `ℹ INFO` message.
85
+ * @property {(...args: unknown[]) => Logger} success A `✔ OK` message.
86
+ * @property {(...args: unknown[]) => Logger} warn A `⚠ WARN` message, on stderr.
87
+ * @property {(...args: unknown[]) => Logger} error An `✖ ERROR` message, on stderr. Errors show their stack and cause.
88
+ * @property {(...args: unknown[]) => Logger} fatal An `✖ FATAL` message, on stderr.
89
+ * @property {(label?: string) => Logger} group Prints a label and indents what follows. Groups nest.
90
+ * @property {() => Logger} groupEnd Closes the current group.
91
+ * @property {(label?: string) => Logger} timeStart Starts a named timer.
92
+ * @property {(label?: string) => Logger} timeEnd Prints how long since `timeStart(label)`.
93
+ * @property {(rows: Array<Record<string, unknown>> | unknown[][], options?: import("./Cli").TableOptions) => Logger} table Prints rows as a table.
94
+ * @property {(text: string, options?: import("./Cli").BoxOptions) => Logger} box Prints text in a box.
95
+ * @property {(title?: string) => Logger} divider Prints a horizontal line.
96
+ * @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.
97
+ * @property {(level: LogLevel) => Logger} setLevel Changes the minimum level.
98
+ * @property {() => LogLevel} getLevel The current minimum level.
99
+ * @property {(level: LogLevel) => boolean} isLevelEnabled Would this level be printed? Skip expensive debug output when it wouldn't.
100
+ * @property {(options: LoggerOptions) => Logger} configure Changes several settings at once.
101
+ */
102
+
103
+ /** @type {Record<LogLevel, number>} */
104
+ const LEVELS = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60, silent: Infinity };
105
+
106
+ /** @type {Record<string, { label: string, style: string, stream: "stdout" | "stderr", level: LogLevel }>} */
107
+ const BADGES = {
108
+ log: { label: "", style: "", stream: "stdout", level: "info" },
109
+ trace: { label: "… TRACE", style: "gray dim", stream: "stdout", level: "trace" },
110
+ debug: { label: "● DEBUG", style: "gray bold", stream: "stdout", level: "debug" },
111
+ info: { label: "ℹ INFO", style: "cyan bold", stream: "stdout", level: "info" },
112
+ success: { label: "✔ OK", style: "greenBright bold", stream: "stdout", level: "info" },
113
+ warn: { label: "⚠ WARN", style: "yellowBright bold", stream: "stderr", level: "warn" },
114
+ error: { label: "✖ ERROR", style: "redBright bold", stream: "stderr", level: "error" },
115
+ fatal: { label: "✖ FATAL", style: "bgRed whiteBright bold", stream: "stderr", level: "fatal" },
116
+ };
117
+
118
+ const STYLE_NAMES = new Set([
119
+ "reset", "bold", "dim", "italic", "underline", "overline", "inverse", "hidden", "strikethrough",
120
+ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray", "grey",
121
+ "blackBright", "redBright", "greenBright", "yellowBright", "blueBright", "magentaBright", "cyanBright", "whiteBright",
122
+ "bgBlack", "bgRed", "bgGreen", "bgYellow", "bgBlue", "bgMagenta", "bgCyan", "bgWhite", "bgGray", "bgGrey",
123
+ "bgBlackBright", "bgRedBright", "bgGreenBright", "bgYellowBright", "bgBlueBright", "bgMagentaBright", "bgCyanBright", "bgWhiteBright",
124
+ ]);
125
+
126
+ const REDACTED = "[redacted]";
127
+ const TRIM_LINES = /^(\s*\r?\n)+|(\r?\n\s*)+$/g;
128
+ const RGB = /(bg)?rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/gi;
129
+ const TOKEN = /^(\S+)(\s+|$)/;
130
+
131
+ /** @returns {LogLevel} */
132
+ const _defaultLevel = () => {
133
+ const fromEnv = String(process.env.LOG_LEVEL ?? "").toLowerCase();
134
+ if (Object.hasOwn(LEVELS, fromEnv)) return /** @type {LogLevel} */ (fromEnv);
135
+ return _debugAll(process.env.DEBUG) || process.env.NODE_DEBUG ? "debug" : "info";
136
+ };
137
+
138
+ /**
139
+ * Does `DEBUG` ask for everything? `DEBUG=1`, `DEBUG=true` and `DEBUG=*` do.
140
+ * @param {string | undefined} value
141
+ */
142
+ const _debugAll = (value) => value !== undefined && /^(?:1|true|yes|on|\*)$/i.test(value.trim());
143
+
144
+ /**
145
+ * Does `DEBUG` name this scope? It's a list of scopes, where `*` stands for
146
+ * anything and a leading `-` excludes: `DEBUG=db,http:*,-http:static`.
147
+ * @param {string | undefined} value
148
+ * @param {string | undefined} scope
149
+ */
150
+ const _debugScope = (value, scope) => {
151
+ if (!value || !scope || _debugAll(value)) return false;
152
+ let wanted = false;
153
+ for (const part of value.split(/[\s,]+/)) {
154
+ if (!part) continue;
155
+ const exclude = part[0] === "-";
156
+ const pattern = new RegExp(`^${(exclude ? part.slice(1) : part).split("*").map(_escape).join(".*")}$`);
157
+ if (!pattern.test(scope)) continue;
158
+ if (exclude) return false;
159
+ wanted = true;
160
+ }
161
+ return wanted;
162
+ };
163
+
164
+ /** @param {string} value */
165
+ const _escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
+
167
+ /**
168
+ * @param {import("./Color").ColorModule} palette
169
+ * @param {string[]} tokens
170
+ * @param {string} text
171
+ * @returns {string}
172
+ */
173
+ const _style = (palette, tokens, text) => {
174
+ /** @type {any} */
175
+ let chain = palette;
176
+ for (const token of tokens) {
177
+ const name = token === "normal" ? "reset" : token;
178
+ let m;
179
+ if (STYLE_NAMES.has(name)) chain = chain[name];
180
+ 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]));
181
+ 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]));
182
+ else if (/^#[0-9a-f]{3,8}$/i.test(name)) chain = chain.hex(name);
183
+ else if (/^bg#[0-9a-f]{3,8}$/i.test(name)) chain = chain.bgHex(name.slice(2));
184
+ }
185
+ return chain === palette ? text : chain(text);
186
+ };
187
+
188
+ /**
189
+ * @param {string} token
190
+ */
191
+ const _isStyleToken = (token) =>
192
+ STYLE_NAMES.has(token) ||
193
+ token === "normal" ||
194
+ /^(bg)?rgb\(\d{1,3},\d{1,3},\d{1,3}\)$/i.test(token) ||
195
+ /^(bg)?#[0-9a-f]{3,8}$/i.test(token);
196
+
197
+ /**
198
+ * Renders `<% styles text %>` markup.
199
+ * @param {string} input
200
+ * @param {{ open: string, close: string, regex: RegExp }} delimiter
201
+ * @param {import("./Color").ColorModule} palette
202
+ * @returns {string}
203
+ */
204
+ const _markup = (input, delimiter, palette) => {
205
+ if (!input.includes(delimiter.open)) return input;
206
+ return input.replace(delimiter.regex, (_, content) => {
207
+ const normalized = String(content).replace(RGB, (m0, bg, r, g, b) => `${bg ?? ""}rgb(${r},${g},${b})`);
208
+ /** @type {string[]} */
209
+ const tokens = [];
210
+ let rest = normalized.replace(/^\s+/, "");
211
+ for (;;) {
212
+ const match = TOKEN.exec(rest);
213
+ if (!match || !_isStyleToken(match[1])) break;
214
+ tokens.push(match[1]);
215
+ rest = rest.slice(match[0].length);
216
+ }
217
+ return _style(palette, tokens, rest);
218
+ });
219
+ };
220
+
221
+ /**
222
+ * A copy of a value with the secret keys masked, for values `JSON.stringify`
223
+ * can't handle on its own: class instances, circular references.
224
+ * @param {unknown} value
225
+ * @param {Set<string>} secrets Lower-cased key names.
226
+ * @param {Map<object, unknown>} seen
227
+ * @returns {unknown}
228
+ */
229
+ const _redact = (value, secrets, seen) => {
230
+ if (value === null || typeof value !== "object" || value instanceof Date || value instanceof RegExp || value instanceof Error || ArrayBuffer.isView(value)) return value;
231
+ const known = seen.get(value);
232
+ if (known !== undefined) return known;
233
+ if (Array.isArray(value)) {
234
+ /** @type {unknown[]} */
235
+ const list = [];
236
+ seen.set(value, list);
237
+ for (const item of value) list.push(_redact(item, secrets, seen));
238
+ return list;
239
+ }
240
+ if (value instanceof Map || value instanceof Set) return value;
241
+ /** @type {Record<string, unknown>} */
242
+ const copy = Object.create(Object.getPrototypeOf(value));
243
+ seen.set(value, copy);
244
+ for (const key of Object.keys(value)) {
245
+ copy[key] = secrets.has(key.toLowerCase()) ? REDACTED : _redact(/** @type {Record<string, unknown>} */ (value)[key], secrets, seen);
246
+ }
247
+ return copy;
248
+ };
249
+
250
+ /**
251
+ * @param {unknown} value
252
+ * @param {boolean} colors
253
+ * @param {Set<string> | undefined} secrets
254
+ * @returns {string}
255
+ */
256
+ const _stringify = (value, colors, secrets) => {
257
+ if (typeof value === "string") return value;
258
+ if (value instanceof Error) {
259
+ let text = value.stack ?? `${value.name}: ${value.message}`;
260
+ let cause = /** @type {any} */ (value).cause;
261
+ let depth = 0;
262
+ while (cause !== undefined && depth++ < 5) {
263
+ text += `\nCaused by: ${cause instanceof Error ? cause.stack ?? cause.message : util.inspect(cause, { colors })}`;
264
+ cause = cause instanceof Error ? /** @type {any} */ (cause).cause : undefined;
265
+ }
266
+ return text;
267
+ }
268
+ if (value !== null && typeof value === "object") {
269
+ const proto = Object.getPrototypeOf(value);
270
+ if (Array.isArray(value) || proto === Object.prototype || proto === null) {
271
+ try {
272
+ const json = JSON.stringify(value, (key, v) => (secrets?.has(key.toLowerCase()) ? REDACTED : typeof v === "bigint" ? `${v}n` : v), 2);
273
+ if (json !== undefined) return json;
274
+ } catch {
275
+ // circular or exotic: fall back to util.inspect
276
+ }
277
+ }
278
+ return util.inspect(secrets ? _redact(value, secrets, new Map()) : value, { depth: 6, colors, breakLength: 100 });
279
+ }
280
+ return typeof value === "symbol" || typeof value === "function" ? util.inspect(value, { colors }) : String(value);
281
+ };
282
+
283
+ /**
284
+ * @param {string} level
285
+ * @param {unknown[]} args
286
+ * @param {string | undefined} scope
287
+ * @param {Record<string, unknown>} fields
288
+ * @param {Set<string> | undefined} secrets
289
+ * @param {Date} now
290
+ * @returns {string}
291
+ */
292
+ const _json = (level, args, scope, fields, secrets, now) => {
293
+ /** @type {Record<string, unknown>} */
294
+ const record = { time: now.toISOString(), level, ...fields };
295
+ if (scope) record.scope = scope;
296
+ const messages = [];
297
+ for (const arg of args) {
298
+ if (arg instanceof Error) {
299
+ 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 };
300
+ } else if (arg !== null && typeof arg === "object" && !Array.isArray(arg)) {
301
+ Object.assign(record, arg);
302
+ } else {
303
+ messages.push(typeof arg === "string" ? color.strip(arg) : _stringify(arg, false, secrets));
304
+ }
305
+ }
306
+ record.msg = messages.join(" ");
307
+ try {
308
+ return JSON.stringify(record, (key, v) => (secrets?.has(key.toLowerCase()) ? REDACTED : typeof v === "bigint" ? v.toString() : v));
309
+ } catch {
310
+ return JSON.stringify({ time: record.time, level, msg: `${record.msg} [unserializable fields]` });
311
+ }
312
+ };
313
+
314
+ /**
315
+ * A log file: where it is, how big it may get, how big it is now.
316
+ * @typedef {{ target: string, limit: number | undefined, keep: number, size: number, ready: boolean }} _LogFile
317
+ */
318
+
319
+ /**
320
+ * @param {LogFileOptions} file
321
+ * @returns {_LogFile}
322
+ */
323
+ const _openFile = (file) => ({
324
+ target: path.resolve(file.path),
325
+ limit: file.maxSize === undefined ? undefined : typeof file.maxSize === "number" ? file.maxSize : require("./Num.js").parseBytes(file.maxSize),
326
+ keep: Math.max(1, file.maxFiles ?? 5),
327
+ size: 0,
328
+ ready: false,
329
+ });
330
+
331
+ /** @param {string} target */
332
+ const _sizeOf = (target) => {
333
+ try {
334
+ return fs.statSync(target).size;
335
+ } catch {
336
+ return 0;
337
+ }
338
+ };
339
+
340
+ /**
341
+ * Appends a line, rotating the file when it gets too big. The folder is
342
+ * made once and the size is counted here, so a line costs one write.
343
+ * @param {_LogFile} file
344
+ * @param {string} line
345
+ */
346
+ const _writeFile = (file, line) => {
347
+ const text = `${line}\n`;
348
+ try {
349
+ if (!file.ready) {
350
+ fs.mkdirSync(path.dirname(file.target), { recursive: true });
351
+ file.size = _sizeOf(file.target);
352
+ file.ready = true;
353
+ }
354
+ const bytes = Buffer.byteLength(text);
355
+ if (file.limit !== undefined && file.size > 0 && file.size + bytes - 1 > file.limit) {
356
+ // Another process may have rotated or removed the file: look before moving anything.
357
+ file.size = _sizeOf(file.target);
358
+ if (file.size > 0 && file.size + bytes - 1 > file.limit) {
359
+ for (let i = file.keep - 1; i >= 1; i--) {
360
+ if (fs.existsSync(`${file.target}.${i}`)) fs.renameSync(`${file.target}.${i}`, `${file.target}.${i + 1}`);
361
+ }
362
+ if (fs.existsSync(`${file.target}.${file.keep + 1}`)) fs.rmSync(`${file.target}.${file.keep + 1}`);
363
+ fs.renameSync(file.target, `${file.target}.1`);
364
+ file.size = 0;
365
+ }
366
+ }
367
+ try {
368
+ fs.appendFileSync(file.target, text);
369
+ } catch {
370
+ // The folder was removed since: make it again, once.
371
+ fs.mkdirSync(path.dirname(file.target), { recursive: true });
372
+ fs.appendFileSync(file.target, text);
373
+ file.size = 0;
374
+ }
375
+ file.size += bytes;
376
+ } catch {
377
+ // Logging must never crash the application.
378
+ }
379
+ };
380
+
381
+ /** @param {number} n */
382
+ const _two = (n) => (n < 10 ? `0${n}` : String(n));
383
+
384
+ /**
385
+ * What a message looks like on one output: its colors, and the parts that
386
+ * are the same on every line, styled once.
387
+ * @typedef {{ palette: import("./Color").ColorModule, colors: boolean, labels: Record<string, string>, scope: string, stamp: string, stampAt: number, stampWidth: number }} _Face
388
+ */
389
+
390
+ /**
391
+ * @param {LoggerOptions} options
392
+ * @returns {Logger}
393
+ */
394
+ const _create = (options) => {
395
+ const settings = {
396
+ level: options.level ?? _defaultLevel(),
397
+ scope: options.scope,
398
+ timestamp: options.timestamp ?? true,
399
+ format: options.format ?? (process.env.LOG_FORMAT === "json" ? "json" : "pretty"),
400
+ delimiter: { open: "<%", close: "%>", ...options.delimiter },
401
+ colors: options.colors,
402
+ stdout: options.stdout,
403
+ stderr: options.stderr,
404
+ file: typeof options.file === "string" ? { path: options.file } : options.file,
405
+ fields: options.fields ?? {},
406
+ redact: options.redact,
407
+ onLog: options.onLog,
408
+ };
409
+ /** @type {string[]} */
410
+ const groups = [];
411
+ /** @type {Map<string, bigint>} */
412
+ const timers = new Map();
413
+
414
+ // Everything below is worked out from the settings, and again when they change.
415
+ let threshold = 0;
416
+ let debugScope = false;
417
+ // What each method does with a message, decided once: a call that prints
418
+ // nothing must cost next to nothing.
419
+ /** @type {Record<keyof typeof BADGES, boolean>} */
420
+ const on = { log: true, trace: false, debug: false, info: true, success: true, warn: true, error: true, fatal: true };
421
+ /** @type {{ open: string, close: string, regex: RegExp }} */
422
+ let delimiter;
423
+ /** @type {Set<string> | undefined} */
424
+ let secrets;
425
+ /** @type {_LogFile | undefined} */
426
+ let file;
427
+ /** @type {{ stdout?: _Face, stderr?: _Face, file?: _Face }} */
428
+ let faces = {};
429
+
430
+ const gate = () => {
431
+ threshold = LEVELS[settings.level];
432
+ for (const kind of /** @type {Array<keyof typeof BADGES>} */ (Object.keys(BADGES))) {
433
+ on[kind] = LEVELS[BADGES[kind].level] >= threshold || (debugScope && threshold !== Infinity && (kind === "debug" || kind === "trace"));
434
+ }
435
+ };
436
+
437
+ const refresh = () => {
438
+ // DEBUG=db,http:* turns debug on for those scopes only, whatever the level.
439
+ debugScope = _debugScope(process.env.DEBUG, settings.scope);
440
+ gate();
441
+ const { open, close } = settings.delimiter;
442
+ delimiter = { open, close, regex: new RegExp(`${_escape(open)}([\\s\\S]*?)${_escape(close)}`, "g") };
443
+ secrets = settings.redact?.length ? new Set(settings.redact.map((key) => String(key).toLowerCase())) : undefined;
444
+ file = settings.file ? _openFile(settings.file) : undefined;
445
+ faces = {};
446
+ };
447
+
448
+ /** @param {"stdout" | "stderr"} which */
449
+ const streamOf = (which) => (which === "stderr" ? settings.stderr ?? process.stderr : settings.stdout ?? process.stdout);
450
+
451
+ /** @param {"stdout" | "stderr" | "file"} which */
452
+ const paletteOf = (which) => {
453
+ if (which === "file" || settings.colors === false) return color.create(0);
454
+ if (settings.colors === true) return color.create(color.level > 0 ? color.level : 3);
455
+ const stream = streamOf(which);
456
+ return stream === process.stdout || stream === process.stderr ? color.create(color.detect(/** @type {any} */ (stream))) : color.create(0);
457
+ };
458
+
459
+ /**
460
+ * @param {"stdout" | "stderr" | "file"} which
461
+ * @returns {_Face}
462
+ */
463
+ const faceOf = (which) => {
464
+ let face = faces[which];
465
+ if (face) return face;
466
+ const palette = paletteOf(which);
467
+ face = { palette, colors: palette.level > 0, labels: {}, scope: settings.scope ? palette.magenta(`[${settings.scope}]`) : "", stamp: "", stampAt: -1, stampWidth: 0 };
468
+ for (const kind of Object.keys(BADGES)) face.labels[kind] = BADGES[kind].label ? _style(palette, BADGES[kind].style.split(" "), BADGES[kind].label) : "";
469
+ faces[which] = face;
470
+ return face;
471
+ };
472
+
473
+ /**
474
+ * The timestamp, written again only when the second changes.
475
+ * @param {_Face} face
476
+ * @param {Date} now
477
+ */
478
+ const stampOf = (face, now) => {
479
+ const second = Math.floor(now.getTime() / 1000);
480
+ if (second === face.stampAt) return;
481
+ face.stampAt = second;
482
+ // A pattern, or a time zone set with time.setTimezone(), needs the real formatter.
483
+ const text = typeof settings.timestamp === "string" || timeConfig.timeZone !== undefined
484
+ ? require("./Time.js").format(now, typeof settings.timestamp === "string" ? settings.timestamp : "HH:mm:ss")
485
+ : `${_two(now.getHours())}:${_two(now.getMinutes())}:${_two(now.getSeconds())}`;
486
+ face.stamp = `${face.palette.gray(`[${text}]`)} `;
487
+ face.stampWidth = color.width(text) + 3;
488
+ };
489
+
490
+ /**
491
+ * @param {_Face} face
492
+ * @param {keyof typeof BADGES} kind
493
+ * @param {unknown[]} args
494
+ * @param {Date} now
495
+ * @param {boolean} stamped
496
+ * @returns {string}
497
+ */
498
+ const render = (face, kind, args, now, stamped) => {
499
+ const { palette } = face;
500
+ let body = "";
501
+ for (let i = 0; i < args.length; i++) {
502
+ const text = _markup(_stringify(args[i], face.colors, secrets), delimiter, palette);
503
+ body += (i ? " " : "") + (face.colors && (kind === "debug" || kind === "trace") ? palette.gray(text) : text);
504
+ }
505
+ let prefix = "";
506
+ let padWidth = 0;
507
+ if (stamped) {
508
+ stampOf(face, now);
509
+ prefix = face.stamp;
510
+ padWidth = face.stampWidth;
511
+ }
512
+ if (groups.length) {
513
+ prefix += " ".repeat(groups.length);
514
+ padWidth += groups.length * 2;
515
+ }
516
+ if (face.scope) prefix += `${face.scope} `;
517
+ if (face.labels[kind]) prefix += `${face.labels[kind]} `;
518
+ if (body.indexOf("\n") === -1) return (prefix + body).trimEnd();
519
+ const lines = body.replace(TRIM_LINES, "").split(/\r?\n/);
520
+ const pad = " ".repeat(padWidth);
521
+ let out = (prefix + lines[0]).trimEnd();
522
+ for (let i = 1; i < lines.length; i++) out += `\n${pad}${lines[i]}`;
523
+ return out;
524
+ };
525
+
526
+ /**
527
+ * @param {keyof typeof BADGES} kind
528
+ * @param {unknown[]} args
529
+ */
530
+ const emit = (kind, args) => {
531
+ if (!on[kind]) return;
532
+ const badge = BADGES[kind];
533
+ const now = new Date();
534
+ const stream = streamOf(badge.stream);
535
+ if (settings.format === "json") {
536
+ const line = _json(kind === "log" ? "info" : kind, args, settings.scope, settings.fields, secrets, now);
537
+ stream.write(`${line}\n`);
538
+ if (file) _writeFile(file, line);
539
+ } else {
540
+ const face = faceOf(badge.stream);
541
+ const line = render(face, kind, args, now, Boolean(settings.timestamp));
542
+ stream.write(`${line}\n`);
543
+ // The file gets the same line without colors: the one just made, when it has none.
544
+ if (file) _writeFile(file, face.colors ? render(faceOf("file"), kind, args, now, Boolean(settings.timestamp)) : line);
545
+ }
546
+ if (settings.onLog) {
547
+ try {
548
+ /** @type {unknown} */
549
+ const pending = settings.onLog({ time: now, level: badge.level, kind: /** @type {LogEntry["kind"]} */ (kind), scope: settings.scope, message: render(faceOf("file"), kind, args, now, false), args });
550
+ // An async hook that fails would otherwise be an unhandled rejection.
551
+ if (pending && typeof (/** @type {any} */ (pending).then) === "function") /** @type {PromiseLike<unknown>} */ (pending).then(undefined, () => {});
552
+ } catch {
553
+ // A broken hook must not take the program down with it.
554
+ }
555
+ }
556
+ };
557
+
558
+ refresh();
559
+
560
+ /** @type {Logger} */
561
+ const logger = {
562
+ log: (...args) => (on.log && emit("log", args), logger),
563
+ trace: (...args) => (on.trace && emit("trace", args), logger),
564
+ debug: (...args) => (on.debug && emit("debug", args), logger),
565
+ info: (...args) => (on.info && emit("info", args), logger),
566
+ success: (...args) => (on.success && emit("success", args), logger),
567
+ warn: (...args) => (on.warn && emit("warn", args), logger),
568
+ error: (...args) => (on.error && emit("error", args), logger),
569
+ fatal: (...args) => (on.fatal && emit("fatal", args), logger),
570
+ group: (label = "") => {
571
+ emit("log", [`▼ ${label}`.trimEnd()]);
572
+ groups.push(label);
573
+ return logger;
574
+ },
575
+ groupEnd: () => {
576
+ groups.pop();
577
+ return logger;
578
+ },
579
+ timeStart: (label = "default") => {
580
+ timers.set(label, process.hrtime.bigint());
581
+ return logger;
582
+ },
583
+ timeEnd: (label = "default") => {
584
+ const start = timers.get(label);
585
+ if (start === undefined) return logger.warn(`Timer "${label}" does not exist`);
586
+ timers.delete(label);
587
+ const ms = Number(process.hrtime.bigint() - start) / 1e6;
588
+ return logger.log(`${label}: ${ms < 1000 ? `${Math.round(ms * 100) / 100}ms` : require("./Time.js").formatDuration(ms, { units: 2 })}`);
589
+ },
590
+ table: (rows, tableOptions) => logger.log(require("./Cli.js").table(rows, { ...tableOptions, colors: faceOf("stdout").colors })),
591
+ box: (text, boxOptions) => logger.log(require("./Cli.js").box(text, { ...boxOptions, colors: faceOf("stdout").colors })),
592
+ divider: (title) => {
593
+ const columns = /** @type {any} */ (streamOf("stdout")).columns ?? 80;
594
+ const width = Math.max(10, Math.min(columns, 100) - (settings.timestamp ? 11 : 0) - groups.length * 2);
595
+ const text = title ? `── ${title} ${"─".repeat(Math.max(0, width - color.width(title) - 4))}` : "─".repeat(width);
596
+ return logger.log(`${settings.delimiter.open}gray ${text}${settings.delimiter.close}`);
597
+ },
598
+ child: (scope, childOptions = {}) =>
599
+ _create({
600
+ ...settings,
601
+ ...childOptions,
602
+ fields: { ...settings.fields, ...childOptions.fields },
603
+ scope: settings.scope ? `${settings.scope}:${scope}` : scope,
604
+ }),
605
+ setLevel: (level) => {
606
+ if (!Object.hasOwn(LEVELS, level)) throw new RangeError(`Unknown log level "${level}"`);
607
+ settings.level = level;
608
+ gate();
609
+ return logger;
610
+ },
611
+ getLevel: () => settings.level,
612
+ isLevelEnabled: (level) => threshold !== Infinity && (LEVELS[level] >= threshold || (debugScope && (level === "debug" || level === "trace"))),
613
+ configure: (next) => {
614
+ if (next.level !== undefined) logger.setLevel(next.level);
615
+ if (next.scope !== undefined) settings.scope = next.scope;
616
+ if (next.timestamp !== undefined) settings.timestamp = next.timestamp;
617
+ if (next.format !== undefined) settings.format = next.format;
618
+ if (next.delimiter !== undefined) settings.delimiter = { ...settings.delimiter, ...next.delimiter };
619
+ if (next.colors !== undefined) settings.colors = next.colors;
620
+ if (next.stdout !== undefined) settings.stdout = next.stdout;
621
+ if (next.stderr !== undefined) settings.stderr = next.stderr;
622
+ if (next.file !== undefined) settings.file = typeof next.file === "string" ? { path: next.file } : next.file;
623
+ if (next.fields !== undefined) settings.fields = next.fields;
624
+ if (next.redact !== undefined) settings.redact = next.redact;
625
+ if (next.onLog !== undefined) settings.onLog = next.onLog;
626
+ refresh();
627
+ return logger;
628
+ },
629
+ };
630
+ return logger;
631
+ };
632
+
633
+ const _default = _create({});
634
+
635
+ /**
636
+ * Creates a logger with its own settings.
637
+ *
638
+ * @example
639
+ * const log = nc.createLogger({ scope: "api", timestamp: "HH:mm:ss.SSS" });
640
+ * log.info("GET /users 200"); // [14:30:05.120] [api] ℹ INFO GET /users 200
641
+ * log.child("db").debug("query took 12ms");
642
+ *
643
+ * const prod = nc.createLogger({ format: "json", fields: { service: "billing" } });
644
+ * prod.error("charge failed", { orderId: 42 }, error);
645
+ *
646
+ * @param {LoggerOptions} [options]
647
+ * @returns {Logger}
648
+ */
649
+ function createLogger(options = {}) {
650
+ return _create(options);
651
+ }
652
+
653
+ /**
654
+ * Prints a message without a badge. Strings support markup, objects are
655
+ * pretty-printed, and several arguments are joined with spaces.
656
+ *
657
+ * @example
658
+ * nc.log("<% green ✔ Saved%> in <% bold 12ms%>");
659
+ * nc.log("user:", { id: 1, roles: ["admin"] });
660
+ *
661
+ * @param {...unknown} args
662
+ * @returns {typeof import("./Logger")}
663
+ */
664
+ function log(...args) {
665
+ _default.log(...args);
666
+ return module.exports;
667
+ }
668
+
669
+ /**
670
+ * Very detailed diagnostics, shown only at the `trace` level.
671
+ *
672
+ * @param {...unknown} args
673
+ * @returns {typeof import("./Logger")}
674
+ */
675
+ function trace(...args) {
676
+ _default.trace(...args);
677
+ return module.exports;
678
+ }
679
+
680
+ /**
681
+ * Diagnostics for developers, shown at the `debug` level (the default when
682
+ * `DEBUG` is set).
683
+ *
684
+ * @example
685
+ * nc.debug("cache miss", { key });
686
+ *
687
+ * @param {...unknown} args
688
+ * @returns {typeof import("./Logger")}
689
+ */
690
+ function debug(...args) {
691
+ _default.debug(...args);
692
+ return module.exports;
693
+ }
694
+
695
+ /**
696
+ * An informational message.
697
+ *
698
+ * @example
699
+ * nc.info("Server listening on http://localhost:3000");
700
+ *
701
+ * @param {...unknown} args
702
+ * @returns {typeof import("./Logger")}
703
+ */
704
+ function info(...args) {
705
+ _default.info(...args);
706
+ return module.exports;
707
+ }
708
+
709
+ /**
710
+ * A success message.
711
+ *
712
+ * @example
713
+ * nc.success("Database connected");
714
+ *
715
+ * @param {...unknown} args
716
+ * @returns {typeof import("./Logger")}
717
+ */
718
+ function success(...args) {
719
+ _default.success(...args);
720
+ return module.exports;
721
+ }
722
+
723
+ /**
724
+ * A warning, written to stderr.
725
+ *
726
+ * @param {...unknown} args
727
+ * @returns {typeof import("./Logger")}
728
+ */
729
+ function warn(...args) {
730
+ _default.warn(...args);
731
+ return module.exports;
732
+ }
733
+
734
+ /**
735
+ * An error, written to stderr. Error objects show their stack trace and
736
+ * their chain of causes.
737
+ *
738
+ * @example
739
+ * nc.error(new Error("Cannot save order", { cause: dbError }));
740
+ *
741
+ * @param {...unknown} args
742
+ * @returns {typeof import("./Logger")}
743
+ */
744
+ function error(...args) {
745
+ _default.error(...args);
746
+ return module.exports;
747
+ }
748
+
749
+ /**
750
+ * A fatal error, written to stderr. It doesn't stop the process; call
751
+ * `process.exit(1)` yourself if you mean to.
752
+ *
753
+ * @param {...unknown} args
754
+ * @returns {typeof import("./Logger")}
755
+ */
756
+ function fatal(...args) {
757
+ _default.fatal(...args);
758
+ return module.exports;
759
+ }
760
+
761
+ /**
762
+ * Prints a label and indents what follows, until `groupEnd()`. Groups nest.
763
+ *
764
+ * @example
765
+ * nc.group("Startup").info("Loading config");
766
+ * nc.group("Database").success("Connected").groupEnd();
767
+ * nc.groupEnd();
768
+ *
769
+ * @param {string} [label]
770
+ * @returns {typeof import("./Logger")}
771
+ */
772
+ function group(label) {
773
+ _default.group(label);
774
+ return module.exports;
775
+ }
776
+
777
+ /**
778
+ * Closes the current group.
779
+ *
780
+ * @returns {typeof import("./Logger")}
781
+ */
782
+ function groupEnd() {
783
+ _default.groupEnd();
784
+ return module.exports;
785
+ }
786
+
787
+ /**
788
+ * Starts a named timer. `timeEnd()` prints the elapsed time.
789
+ *
790
+ * @example
791
+ * nc.timeStart("import");
792
+ * await importProducts();
793
+ * nc.timeEnd("import"); // "import: 1s 240ms"
794
+ *
795
+ * @param {string} [label="default"]
796
+ * @returns {typeof import("./Logger")}
797
+ */
798
+ function timeStart(label) {
799
+ _default.timeStart(label);
800
+ return module.exports;
801
+ }
802
+
803
+ /**
804
+ * Prints the time since `timeStart()` with the same label.
805
+ *
806
+ * @param {string} [label="default"]
807
+ * @returns {typeof import("./Logger")}
808
+ */
809
+ function timeEnd(label) {
810
+ _default.timeEnd(label);
811
+ return module.exports;
812
+ }
813
+
814
+ /**
815
+ * Prints rows as a table.
816
+ *
817
+ * @example
818
+ * nc.table([{ name: "Ada", role: "admin" }, { name: "Bob", role: "user" }]);
819
+ *
820
+ * @param {Array<Record<string, unknown>> | unknown[][]} rows
821
+ * @param {import("./Cli").TableOptions} [options]
822
+ * @returns {typeof import("./Logger")}
823
+ */
824
+ function table(rows, options) {
825
+ _default.table(rows, options);
826
+ return module.exports;
827
+ }
828
+
829
+ /**
830
+ * Prints text in a box. Nice for startup banners.
831
+ *
832
+ * @example
833
+ * nc.box("Server ready\nhttp://localhost:3000", { title: "my-app", borderColor: "green" });
834
+ *
835
+ * @param {string} text
836
+ * @param {import("./Cli").BoxOptions} [options]
837
+ * @returns {typeof import("./Logger")}
838
+ */
839
+ function box(text, options) {
840
+ _default.box(text, options);
841
+ return module.exports;
842
+ }
843
+
844
+ /**
845
+ * Prints a horizontal line, with an optional title in it.
846
+ *
847
+ * @param {string} [title]
848
+ * @returns {typeof import("./Logger")}
849
+ */
850
+ function divider(title) {
851
+ _default.divider(title);
852
+ return module.exports;
853
+ }
854
+
855
+ /**
856
+ * Sets the minimum level.
857
+ *
858
+ * @example
859
+ * nc.setLevel("warn"); // warnings and errors only
860
+ * nc.setLevel("silent"); // nothing
861
+ *
862
+ * @param {LogLevel} level
863
+ * @returns {typeof import("./Logger")}
864
+ * @throws {RangeError} For an unknown level.
865
+ */
866
+ function setLevel(level) {
867
+ _default.setLevel(level);
868
+ return module.exports;
869
+ }
870
+
871
+ /**
872
+ * The current minimum level.
873
+ *
874
+ * @returns {LogLevel}
875
+ */
876
+ function getLevel() {
877
+ return _default.getLevel();
878
+ }
879
+
880
+ /**
881
+ * Would this level be printed? Use it to skip building expensive debug
882
+ * output.
883
+ *
884
+ * @example
885
+ * if (nc.isLevelEnabled("debug")) nc.debug(buildHugeReport());
886
+ *
887
+ * @param {LogLevel} level
888
+ * @returns {boolean}
889
+ */
890
+ function isLevelEnabled(level) {
891
+ return _default.isLevelEnabled(level);
892
+ }
893
+
894
+ /**
895
+ * Changes several settings at once.
896
+ *
897
+ * @example
898
+ * nc.configure({ level: "debug", file: "logs/app.log" });
899
+ * nc.configure({ format: "json", fields: { service: "api" } });
900
+ *
901
+ * @param {LoggerOptions} options
902
+ * @returns {typeof import("./Logger")}
903
+ */
904
+ function configure(options) {
905
+ _default.configure(options);
906
+ return module.exports;
907
+ }
908
+
909
+ /**
910
+ * Shows or hides timestamps, or sets their pattern.
911
+ *
912
+ * @example
913
+ * nc.setTimestamp(false);
914
+ * nc.setTimestamp("YYYY-MM-DD HH:mm:ss.SSS");
915
+ *
916
+ * @param {boolean | string} value
917
+ * @returns {typeof import("./Logger")}
918
+ */
919
+ function setTimestamp(value) {
920
+ _default.configure({ timestamp: value });
921
+ return module.exports;
922
+ }
923
+
924
+ /**
925
+ * Changes the markup delimiters, for when `<%` clashes with a template
926
+ * engine.
927
+ *
928
+ * @example
929
+ * nc.setDelimiter({ open: "{{", close: "}}" });
930
+ * nc.log("{{red Hello}} world");
931
+ *
932
+ * @param {{ open?: string, close?: string }} [options]
933
+ * @returns {typeof import("./Logger")}
934
+ */
935
+ function setDelimiter(options = {}) {
936
+ _default.configure({ delimiter: options });
937
+ return module.exports;
938
+ }
939
+
940
+ module.exports = {
941
+ log,
942
+ trace,
943
+ debug,
944
+ info,
945
+ success,
946
+ warn,
947
+ error,
948
+ fatal,
949
+ group,
950
+ groupEnd,
951
+ timeStart,
952
+ timeEnd,
953
+ table,
954
+ box,
955
+ divider,
956
+ setLevel,
957
+ getLevel,
958
+ isLevelEnabled,
959
+ configure,
960
+ setTimestamp,
961
+ setDelimiter,
962
+ createLogger,
963
+ };