@ix-xs/node-comfort 1.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.
package/core/Logger.js ADDED
@@ -0,0 +1,321 @@
1
+ const _x = "\x1B";
2
+ const _style = {
3
+ normal: 0,
4
+ bold: 1,
5
+ italic: 3,
6
+ underline: 4,
7
+ overline: 9,
8
+ };
9
+ const _color = {
10
+ fg: {
11
+ black: 30,
12
+ red: 31,
13
+ green: 32,
14
+ yellow: 33,
15
+ blue: 34,
16
+ magenta: 35,
17
+ cyan: 36,
18
+ white: 37,
19
+ gray: 90,
20
+ redBright: 91,
21
+ greenBright: 92,
22
+ yellowBright: 93,
23
+ blueBright: 94,
24
+ magentaBright: 95,
25
+ cyanBright: 96,
26
+ whiteBright: 97,
27
+ rgb: 38,
28
+ },
29
+ bg: {
30
+ black: 40,
31
+ red: 41,
32
+ green: 42,
33
+ yellow: 43,
34
+ blue: 44,
35
+ magenta: 45,
36
+ cyan: 46,
37
+ white: 47,
38
+ gray: 100,
39
+ redBright: 101,
40
+ greenBright: 102,
41
+ yellowBright: 103,
42
+ blueBright: 104,
43
+ magentaBright: 105,
44
+ cyanBright: 106,
45
+ whiteBright: 107,
46
+ rgb: 48,
47
+ },
48
+ };
49
+ const _options = {
50
+ delimiters: {
51
+ start: "<%",
52
+ end: "%>",
53
+ },
54
+ timestamp: true,
55
+ };
56
+ const _groups = [];
57
+
58
+ /**
59
+ * Gets ANSI escape code for style/color.
60
+ * @private
61
+ * @param {string} name - Style or color name.
62
+ * @returns {number|null} ANSI code or null.
63
+ */
64
+ const _getCode = (name) => {
65
+ if (_style[name] !== undefined) return _style[name];
66
+ if (_color.fg[name] !== undefined) return _color.fg[name];
67
+ if (name.startsWith("bg")) {
68
+ const bg = name.charAt(2).toLowerCase() + name.slice(3);
69
+ if (_color.bg[bg] !== undefined) return _color.bg[bg];
70
+ }
71
+ return null;
72
+ };
73
+
74
+ /**
75
+ * Parses template tokens into styles and text.
76
+ * @private
77
+ * @param {string} input - Template string.
78
+ * @returns {{codes: number[], text: string}} Parsed result.
79
+ */
80
+ const _parse = (input) => {
81
+ const tokens = input.trim().split(/\s+/);
82
+ const styles = [];
83
+ const fgs = [];
84
+ const bgs = [];
85
+ const txts = [];
86
+
87
+ for (const token of tokens) {
88
+ const code = _getCode(token);
89
+ if (code === null) {
90
+ txts.push(token);
91
+ } else {
92
+ if (_style[token] !== undefined) {
93
+ styles.push(code);
94
+ } else if (token.startsWith("bg")) {
95
+ bgs.push(code);
96
+ } else {
97
+ fgs.push(code);
98
+ }
99
+ }
100
+ }
101
+
102
+ const codes = [...styles, ...bgs, ...fgs];
103
+ const text = input
104
+ .replace(
105
+ /^\s*((redBright|greenBright|yellowBright|blueBright|magentaBright|cyanBright|whiteBright|red|green|yellow|blue|magenta|cyan|white|gray|black|bold|italic|underline|overline|bg\w+|\s+)*)/gi,
106
+ "",
107
+ )
108
+ .trimStart();
109
+ return { codes, text };
110
+ };
111
+
112
+ /**
113
+ * Builds ANSI escape sequence from codes.
114
+ * @private
115
+ * @param {number[]} codes - ANSI codes.
116
+ * @returns {string} Escape sequence.
117
+ */
118
+ const _build = (codes) => {
119
+ if (codes.length === 0) return "";
120
+ return `${_x}[${codes.join(";")}m`;
121
+ };
122
+
123
+ const _apply = (str) => {
124
+ const esc_start = _options.delimiters.start.replace(
125
+ /[.*+?^${}()|[\]\\]/g,
126
+ "\\$&",
127
+ );
128
+ const esc_end = _options.delimiters.end.replace(
129
+ /[.*+?^${}()|[\]\\]/g,
130
+ "\\$&",
131
+ );
132
+
133
+ return str.replace(
134
+ new RegExp(`${esc_start}([\\s\\S]*?)${esc_end}`, "g"),
135
+ (match, content) => {
136
+ const { codes, text } = _parse(content);
137
+ if (codes.length === 0 || text === "") return text;
138
+
139
+ if (text.includes("\n")) {
140
+ return text
141
+ .split(/\\r?\\n/)
142
+ .filter((line) => line.trim())
143
+ .map((line) => `${_build(codes)}${line}${_x}[0m`)
144
+ .join("\\n");
145
+ }
146
+
147
+ return `${_build(codes)}${text}${_x}[0m`;
148
+ },
149
+ );
150
+ };
151
+
152
+ /**
153
+ * @private
154
+ * @returns {string}
155
+ */
156
+ const _timestamp = () => {
157
+ if (!_options.timestamp) return "";
158
+ return _apply(
159
+ `<%gray [${new Date().toLocaleTimeString(undefined, { hour12: false })}]%>`,
160
+ );
161
+ };
162
+
163
+ /**
164
+ * Generates indentation based on group depth.
165
+ * @private
166
+ * @returns {string} Indentation spaces.
167
+ */
168
+ const _indent = () => {
169
+ return " ".repeat(0 + _groups.length);
170
+ };
171
+
172
+ /**
173
+ * @module logger
174
+ * @description
175
+ * Beautiful ANSI-colored console logger with grouping, timestamps, and template syntax.
176
+ *
177
+ * Supports 16+ colors, styles (bold, italic, underline), background colors,
178
+ * nested groups with indentation, and customizable delimiters.
179
+ *
180
+ * ## Syntax
181
+ * ```
182
+ * <%red bold This is red and bold%>
183
+ * <%bgBlue whiteBright ERROR%>
184
+ * <%yellowBright [WARN]%>
185
+ * ```
186
+ *
187
+ * Available styles: `normal`, `bold`, `italic`, `underline`, `overline`
188
+ * Colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`
189
+ * Bright colors: `redBright`, `greenBright`, `yellowBright`, `blueBright`, `magentaBright`, `cyanBright`, `whiteBright`
190
+ * Backgrounds: `bgBlack`, `bgRed`, `bgGreen`, etc.
191
+ *
192
+ * @example
193
+ * const nodeComfort = require("@ix-xs/node-comfort");
194
+ *
195
+ * nodeComfort.log("<%greenBright ✓ Success%> Operation completed!");
196
+ * nodeComfort.group("<%blue Processing files%>").log("File 1").groupEnd();
197
+ * const log = nodeComfort.logify({ users: 42 }); // → ANSI string
198
+ */
199
+ module.exports = {
200
+ /**
201
+ * Logs content with colors, timestamp, and group indentation.
202
+ * Supports strings, objects (pretty JSON), and chaining.
203
+ *
204
+ * @param {any} content - Content to log (string, object, or primitive).
205
+ * @returns {this} Chainable instance.
206
+ * @example
207
+ * nodeComfort.log("<%greenBright ✓ Done%>");
208
+ * nodeComfort.log({ users: 42, active: true });
209
+ * nodeComfort.group("Files").log("Processing...").groupEnd();
210
+ */
211
+ log(content) {
212
+ let str;
213
+ if (typeof content === "string") {
214
+ str = content;
215
+ } else if (typeof content === "object") {
216
+ str = JSON.stringify(content, null, 2);
217
+ } else {
218
+ str = String(content);
219
+ }
220
+
221
+ const prefix = `${_timestamp()}${_indent()}`;
222
+ const fullStr = prefix ? `${prefix} ${str}` : str;
223
+
224
+ const formatted = _apply(fullStr);
225
+
226
+ if (formatted.includes("\n")) {
227
+ const lines = formatted.split("\n");
228
+
229
+ if (lines[0].trim()) console.log(lines[0]);
230
+
231
+ const indentOnly = _indent();
232
+ for (let i = 1; i < lines.length; i++) {
233
+ if (lines[i].trim()) {
234
+ console.log(`${indentOnly}${lines[i]}`);
235
+ }
236
+ }
237
+ } else {
238
+ console.log(formatted);
239
+ }
240
+
241
+ return this;
242
+ },
243
+
244
+ /**
245
+ * Formats content with colors, timestamp, and group indentation (no console output).
246
+ *
247
+ * @param {any} content - Content to format.
248
+ * @returns {string} Formatted string.
249
+ * @example
250
+ * const msg = nodeComfort.logify("<%redBright ERROR%> Invalid input");
251
+ * console.log(msg);
252
+ */
253
+ logify(content) {
254
+ let str;
255
+ if (typeof content === "string") {
256
+ str = content;
257
+ } else if (typeof content === "object") {
258
+ str = JSON.stringify(content, null, 2);
259
+ } else {
260
+ str = String(content);
261
+ }
262
+
263
+ const prefix = `${_timestamp()}${_indent()}`;
264
+ return _apply(prefix ? `${prefix} ${str}` : str);
265
+ },
266
+
267
+ /**
268
+ * Starts a new log group with ▼ indicator and increased indentation.
269
+ *
270
+ * @param {string} label - Group label (supports colors).
271
+ * @returns {this} Chainable instance.
272
+ * @example
273
+ * nodeComfort.group("<%yellow Processing%>").log("Step 1").groupEnd();
274
+ */
275
+ group(label) {
276
+ _groups.push(label);
277
+ const prefix = `${_timestamp()}${_indent()}`;
278
+ console.log(prefix ? `${prefix}▼ ${label}` : `▼ ${label}`);
279
+ return this;
280
+ },
281
+
282
+ /**
283
+ * Ends current group and reduces indentation.
284
+ *
285
+ * @returns {this} Chainable instance.
286
+ * @example
287
+ * nodeComfort.group("Work").groupEnd();
288
+ */
289
+ groupEnd() {
290
+ if (_groups.length > 0) _groups.pop();
291
+ return this;
292
+ },
293
+
294
+ /**
295
+ * Configures logger options (delimiters and timestamp).
296
+ *
297
+ * @param {object} [options] - Logger options.
298
+ * @param {object} [options.delimiters] - Template delimiters.
299
+ * @param {string} [options.delimiters.start="<%="] - Start delimiter.
300
+ * @param {string} [options.delimiters.end="%>"] - End delimiter.
301
+ * @param {boolean} [options.timestamp=true] - Show timestamps.
302
+ * @returns {this} Chainable instance.
303
+ * @example
304
+ * nodeComfort.setLogger({
305
+ * delimiters: { start: "{{", end: "}}" },
306
+ * timestamp: false
307
+ * }).log("{{ bgGreen new delimiters }}");
308
+ */
309
+ setLogger(options = _options) {
310
+ options = {
311
+ delimiters: {
312
+ start: options?.delimiters?.start ?? _options.delimiters.start,
313
+ end: options?.delimiters?.end ?? _options.delimiters.end,
314
+ },
315
+ timestamp: options?.timestamp ?? _options.timestamp,
316
+ };
317
+ _options.delimiters = options.delimiters;
318
+ _options.timestamp = options.timestamp;
319
+ return this;
320
+ },
321
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @module Stepper
3
+ * @description
4
+ * Fluent step controller for sequential task execution with navigation, skipping, and indexing.
5
+ *
6
+ * Perfect for CLI wizards, build pipelines, tutorials, or any sequential workflow.
7
+ * Supports forward/backward navigation, jumping to specific steps, ignoring steps, and reset.
8
+ *
9
+ * @example
10
+ * const nodeComfort = require("@ix-xs/node-comfort");
11
+ *
12
+ * // Simple 3-step process
13
+ * nodeComfort.step([
14
+ * () => console.log("Step 1: Validate config"),
15
+ * () => console.log("Step 2: Build assets"),
16
+ * () => console.log("Step 3: Deploy")
17
+ * ])
18
+ * .next().next().next(); // Execute all steps
19
+ *
20
+ * // Interactive navigation
21
+ * const wizard = nodeComfort.step([
22
+ * () => nodeComfort.log("<%yellow 1%> Enter name"),
23
+ * () => nodeComfort.log("<%yellow 2%> Enter email"),
24
+ * () => nodeComfort.log("<%green ✓%> Complete!")
25
+ * ]);
26
+ *
27
+ * wizard.next().back().ignore().next(); // 1 → back to 0 → skip 1 → execute 2
28
+ */
29
+ module.exports = {
30
+ /**
31
+ * Creates a step controller for sequential function execution.
32
+ *
33
+ * Returns fluent API with `next()`, `back()`, `at()`, `ignore()`, `reset()` methods.
34
+ *
35
+ * @param {Array<() => void>} steps - Array of step functions to execute.
36
+ *
37
+ * @example
38
+ * const steps = nodeComfort.step([
39
+ * () => console.log("Install dependencies"),
40
+ * () => console.log("Compile TypeScript"),
41
+ * () => console.log("Run tests")
42
+ * ]);
43
+ *
44
+ * steps.next().next(); // Execute steps 0, 1
45
+ * steps.back(); // Go back to step 0
46
+ * steps.at(2); // Jump to step 2
47
+ */
48
+ step(steps) {
49
+ let i = -1;
50
+
51
+ const _ = {
52
+ currentIndex: i,
53
+ currentStep: steps[i],
54
+ /**
55
+ * Advances to next step and executes it (if exists).
56
+ */
57
+ next() {
58
+ if (i < steps.length - 1) {
59
+ i++;
60
+ if (typeof steps[i] === "function") steps[i]();
61
+ }
62
+ return _;
63
+ },
64
+ /**
65
+ * Goes back to previous step and re-executes it (if exists).
66
+ */
67
+ back() {
68
+ if (i > 0) {
69
+ i--;
70
+ if (typeof steps[i] === "function") steps[i]();
71
+ }
72
+ return _;
73
+ },
74
+ /**
75
+ * Jumps to specific step index and executes it.
76
+ * @param {number} index - Step index (0 to steps.length-1).
77
+ * @example
78
+ * steps.at(2); // Jump to 3rd step
79
+ */
80
+ at(index) {
81
+ if (index >= 0 && index < steps.length) {
82
+ i = index;
83
+ if (typeof steps[i] === "function") steps[i]();
84
+ }
85
+ return _;
86
+ },
87
+ /**
88
+ * Advances index without executing current step.
89
+ * @example
90
+ * steps.next().ignore().next(); // Execute 0, skip 1, execute 2
91
+ */
92
+ ignore() {
93
+ if (i < steps.length - 1) {
94
+ i++;
95
+ }
96
+ return _;
97
+ },
98
+ /**
99
+ * Resets to initial state (index: -1).
100
+ */
101
+ reset() {
102
+ i = -1;
103
+ return _;
104
+ },
105
+ all() {
106
+ while (i < steps.length - 1) {
107
+ this.next();
108
+ }
109
+ return _;
110
+ },
111
+ };
112
+
113
+ return _;
114
+ },
115
+ };