@mks2508/better-logger 3.1.0 → 5.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 (106) hide show
  1. package/CHANGELOG.json +45 -1
  2. package/dist/Logger.d.ts +129 -4
  3. package/dist/Logger.d.ts.map +1 -1
  4. package/dist/ScopedLogger.d.ts +17 -0
  5. package/dist/ScopedLogger.d.ts.map +1 -1
  6. package/dist/chunks/{Logger-C086w3EM.js → Logger-BZ7B7OXE.js} +258 -16
  7. package/dist/chunks/Logger-BZ7B7OXE.js.map +1 -0
  8. package/dist/chunks/Logger-Cbi1f10o.js +2 -0
  9. package/dist/chunks/Logger-Cbi1f10o.js.map +1 -0
  10. package/dist/chunks/cli-module-CadqgZ1Z.js +2 -0
  11. package/dist/chunks/cli-module-CadqgZ1Z.js.map +1 -0
  12. package/dist/chunks/cli-module-ClIJmaT8.js +383 -0
  13. package/dist/chunks/cli-module-ClIJmaT8.js.map +1 -0
  14. package/dist/chunks/color-converter-CCSQRztd.js +2 -0
  15. package/dist/chunks/color-converter-CCSQRztd.js.map +1 -0
  16. package/dist/chunks/color-converter-_Xdmsy7E.js +462 -0
  17. package/dist/chunks/color-converter-_Xdmsy7E.js.map +1 -0
  18. package/dist/chunks/{environment-DXHouGD4.js → environment-C1xxvc8l.js} +28 -478
  19. package/dist/chunks/environment-C1xxvc8l.js.map +1 -0
  20. package/dist/chunks/environment-Cprkw3g9.js +4 -0
  21. package/dist/chunks/environment-Cprkw3g9.js.map +1 -0
  22. package/dist/chunks/{formatting-BNnVUPcw.js → formatting-DMJxYq9o.js} +18 -18
  23. package/dist/chunks/{formatting-BNnVUPcw.js.map → formatting-DMJxYq9o.js.map} +1 -1
  24. package/dist/chunks/{formatting-Cg5YhB9Y.js → formatting-DpKxCsXq.js} +2 -2
  25. package/dist/chunks/{formatting-Cg5YhB9Y.js.map → formatting-DpKxCsXq.js.map} +1 -1
  26. package/dist/cli-module.d.ts +15 -0
  27. package/dist/cli-module.d.ts.map +1 -0
  28. package/dist/cli-primitives/box.d.ts +11 -0
  29. package/dist/cli-primitives/box.d.ts.map +1 -0
  30. package/dist/cli-primitives/cli-table.d.ts +11 -0
  31. package/dist/cli-primitives/cli-table.d.ts.map +1 -0
  32. package/dist/cli-primitives/divider.d.ts +11 -0
  33. package/dist/cli-primitives/divider.d.ts.map +1 -0
  34. package/dist/cli-primitives/header.d.ts +12 -0
  35. package/dist/cli-primitives/header.d.ts.map +1 -0
  36. package/dist/cli-primitives/index.d.ts +13 -0
  37. package/dist/cli-primitives/index.d.ts.map +1 -0
  38. package/dist/cli-primitives/server-fallback.d.ts +25 -0
  39. package/dist/cli-primitives/server-fallback.d.ts.map +1 -0
  40. package/dist/cli-primitives/spinner.d.ts +47 -0
  41. package/dist/cli-primitives/spinner.d.ts.map +1 -0
  42. package/dist/cli-primitives/step.d.ts +11 -0
  43. package/dist/cli-primitives/step.d.ts.map +1 -0
  44. package/dist/cli.cjs +2 -0
  45. package/dist/cli.cjs.map +1 -0
  46. package/dist/cli.js +12 -0
  47. package/dist/cli.js.map +1 -0
  48. package/dist/constants.d.ts +9 -1
  49. package/dist/constants.d.ts.map +1 -1
  50. package/dist/core.cjs +1 -1
  51. package/dist/core.js +2 -2
  52. package/dist/exports-module.d.ts +1 -1
  53. package/dist/exports-module.d.ts.map +1 -1
  54. package/dist/exports.cjs +1 -1
  55. package/dist/exports.js +2 -2
  56. package/dist/index.cjs +1 -1
  57. package/dist/index.cjs.map +1 -1
  58. package/dist/index.d.ts +13 -3
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +162 -195
  61. package/dist/index.js.map +1 -1
  62. package/dist/styling-module.d.ts +1 -1
  63. package/dist/styling-module.d.ts.map +1 -1
  64. package/dist/styling.cjs +1 -1
  65. package/dist/styling.js +3 -3
  66. package/dist/types/core.d.ts +91 -0
  67. package/dist/types/core.d.ts.map +1 -1
  68. package/dist/types/index.d.ts +1 -1
  69. package/dist/types/index.d.ts.map +1 -1
  70. package/dist/writers/BufferWriter.d.ts +96 -0
  71. package/dist/writers/BufferWriter.d.ts.map +1 -0
  72. package/dist/writers/index.d.ts +6 -0
  73. package/dist/writers/index.d.ts.map +1 -0
  74. package/package.json +28 -3
  75. package/packages/core/package.json +1 -1
  76. package/playground/demo-all.ts +95 -0
  77. package/playground/demo-box.ts +85 -0
  78. package/playground/demo-levels.ts +56 -0
  79. package/playground/demo-real-world.ts +98 -0
  80. package/playground/demo-spinner.ts +77 -0
  81. package/playground/demo-steps.ts +72 -0
  82. package/playground/demo-table.ts +83 -0
  83. package/src/Logger.ts +263 -16
  84. package/src/ScopedLogger.ts +43 -1
  85. package/src/cli-module.ts +21 -0
  86. package/src/cli-primitives/box.ts +86 -0
  87. package/src/cli-primitives/cli-table.ts +62 -0
  88. package/src/cli-primitives/divider.ts +17 -0
  89. package/src/cli-primitives/header.ts +18 -0
  90. package/src/cli-primitives/index.ts +13 -0
  91. package/src/cli-primitives/server-fallback.ts +54 -0
  92. package/src/cli-primitives/spinner.ts +133 -0
  93. package/src/cli-primitives/step.ts +22 -0
  94. package/src/constants.ts +14 -2
  95. package/src/index.ts +23 -2
  96. package/src/types/core.ts +99 -0
  97. package/src/types/index.ts +4 -0
  98. package/src/writers/BufferWriter.ts +157 -0
  99. package/src/writers/index.ts +6 -0
  100. package/vite.config.ts +33 -0
  101. package/dist/chunks/Logger-BV0v1AyC.js +0 -2
  102. package/dist/chunks/Logger-BV0v1AyC.js.map +0 -1
  103. package/dist/chunks/Logger-C086w3EM.js.map +0 -1
  104. package/dist/chunks/environment-C_8J-zQ_.js +0 -4
  105. package/dist/chunks/environment-C_8J-zQ_.js.map +0 -1
  106. package/dist/chunks/environment-DXHouGD4.js.map +0 -1
package/src/Logger.ts CHANGED
@@ -36,7 +36,8 @@ import { TransportManager } from './transports/index.js';
36
36
  import { parseStackTrace } from './utils/stackTrace.js';
37
37
  import { formatTimestamp } from './utils/timestamps.js';
38
38
  import { createStyledOutput, setupThemeChangeListener } from './utils/output.js';
39
- import { getEnvironment } from './utils/environment-detector.js';
39
+ import { getEnvironment, getColorCapability, isRunningInTerminal } from './utils/environment-detector.js';
40
+ import { formatBadge } from './terminal/formatter.js';
40
41
 
41
42
  // Styling imports
42
43
  import {
@@ -61,7 +62,17 @@ import { ExportLogHandler } from './handlers/index.js';
61
62
  import { createDefaultCLI, type CommandProcessor } from './cli/index.js';
62
63
 
63
64
  // Constants
64
- import { DEFAULT_CONFIG } from './constants.js';
65
+ import { DEFAULT_CONFIG, CLI_LEVEL_MAP } from './constants.js';
66
+
67
+ // CLI Primitives
68
+ import type { CLILogLevel, ISpinnerHandle, IBoxOptions, ITableOptions } from './types/index.js';
69
+ import { renderStep } from './cli-primitives/step.js';
70
+ import { renderHeader } from './cli-primitives/header.js';
71
+ import { renderDivider } from './cli-primitives/divider.js';
72
+ import { renderBox } from './cli-primitives/box.js';
73
+ import { renderTable } from './cli-primitives/cli-table.js';
74
+ import { SpinnerManager, NoopSpinner } from './cli-primitives/spinner.js';
75
+ import { ServerFallback } from './cli-primitives/server-fallback.js';
65
76
 
66
77
  /**
67
78
  * Estilos del tema activo actual
@@ -115,6 +126,11 @@ export class Logger {
115
126
  private hookManager: HookManager;
116
127
  private transportManager?: TransportManager;
117
128
 
129
+ /** Whether CLI primitives (step, box, header, etc.) should be shown @since 5.0.0 */
130
+ private _showPrimitives = true;
131
+ /** Server-mode fallback for non-TTY environments @since 5.0.0 */
132
+ private _serverFallback?: ServerFallback;
133
+
118
134
  /**
119
135
  * Crea una nueva instancia del Logger
120
136
  *
@@ -942,12 +958,8 @@ export class Logger {
942
958
  const groupIndent = ' '.repeat(this.groupDepth);
943
959
  const finalFormat = groupIndent + format;
944
960
 
945
- // Output to console
946
- if (additionalArgs.length > 0) {
947
- console.log(finalFormat, ...styles, ...additionalArgs);
948
- } else {
949
- console.log(finalFormat, ...styles);
950
- }
961
+ // Output via configured writer (console by default)
962
+ this.writeOutput(finalFormat, level, styles, additionalArgs);
951
963
 
952
964
  // Update export handler group info
953
965
  if (this.exportHandler) {
@@ -999,11 +1011,13 @@ export class Logger {
999
1011
  if (!this.shouldLog(level)) return;
1000
1012
 
1001
1013
  let prefix = '';
1014
+ const colorCapability = getColorCapability();
1015
+
1002
1016
  if (bindings.badges?.length) {
1003
- prefix += bindings.badges.map(b => `[${b}]`).join('');
1017
+ prefix += bindings.badges.map(b => formatBadge(b, 'pill', colorCapability, '#00ff88')).join(' ') + ' ';
1004
1018
  }
1005
1019
  if (bindings.scope) {
1006
- prefix += `[${bindings.scope}] `;
1020
+ prefix += formatBadge(bindings.scope, 'pill', colorCapability, '#00ffff') + ' ';
1007
1021
  }
1008
1022
 
1009
1023
  if (prefix && args.length > 0) {
@@ -1293,21 +1307,22 @@ export class Logger {
1293
1307
 
1294
1308
  /**
1295
1309
  * Finaliza un temporizador y muestra el tiempo transcurrido
1296
- *
1310
+ *
1297
1311
  * @param {string} label - Etiqueta del temporizador a finalizar
1298
- *
1312
+ * @returns {number} Elapsed milliseconds, or -1 if timer not found
1313
+ *
1299
1314
  * @example
1300
1315
  * logger.time('consulta-db');
1301
1316
  * await consultarBaseDatos();
1302
- * logger.timeEnd('consulta-db'); // ⏱️ Timer ended: consulta-db - 234.56ms
1303
- *
1317
+ * const elapsed = logger.timeEnd('consulta-db'); // ⏱️ Timer ended: consulta-db - 234.56ms
1318
+ *
1304
1319
  * @since 0.3.0
1305
1320
  */
1306
- timeEnd(label: string): void {
1321
+ timeEnd(label: string): number {
1307
1322
  const timer = this.timers.get(label);
1308
1323
  if (!timer) {
1309
1324
  this.warn(`Timer '${label}' does not exist`);
1310
- return;
1325
+ return -1;
1311
1326
  }
1312
1327
 
1313
1328
  const elapsed = performance.now() - timer.startTime;
@@ -1315,6 +1330,8 @@ export class Logger {
1315
1330
 
1316
1331
  const timerStyle = StylePresets.success().build();
1317
1332
  console.log(`%c⏱️ Timer ended: ${label} - ${elapsed.toFixed(2)}ms`, timerStyle);
1333
+
1334
+ return elapsed;
1318
1335
  }
1319
1336
 
1320
1337
  // ===== ADVANCED VISUAL FEATURES =====
@@ -1463,6 +1480,236 @@ export class Logger {
1463
1480
  }
1464
1481
  }
1465
1482
 
1483
+ // ===== CLI PRIMITIVES (v5.0) =====
1484
+
1485
+ /**
1486
+ * Displays a step progress indicator in the terminal
1487
+ *
1488
+ * @param {number} current - Current step number
1489
+ * @param {number} total - Total number of steps
1490
+ * @param {string} message - Step description
1491
+ *
1492
+ * @example
1493
+ * logger.step(1, 5, 'Analyzing repository...');
1494
+ * logger.step(2, 5, 'Generating commit message...');
1495
+ *
1496
+ * @since 5.0.0
1497
+ */
1498
+ step(current: number, total: number, message: string): void {
1499
+ if (!this._showPrimitives) return;
1500
+ if (!isRunningInTerminal()) {
1501
+ this.getServerFallback().step(current, total, message);
1502
+ return;
1503
+ }
1504
+ const colorCap = getColorCapability();
1505
+ const output = renderStep(current, total, message, colorCap);
1506
+ process.stderr.write(output + '\n');
1507
+ }
1508
+
1509
+ /**
1510
+ * Displays a styled header with optional subtitle
1511
+ *
1512
+ * @param {string} title - Main title text
1513
+ * @param {string} subtitle - Optional subtitle (rendered dimmed)
1514
+ *
1515
+ * @example
1516
+ * logger.header('Commit Wizard', 'v2.0.0');
1517
+ *
1518
+ * @since 5.0.0
1519
+ */
1520
+ header(title: string, subtitle?: string): void {
1521
+ if (!this._showPrimitives) return;
1522
+ if (!isRunningInTerminal()) {
1523
+ this.getServerFallback().header(title, subtitle);
1524
+ return;
1525
+ }
1526
+ const output = renderHeader(title, subtitle);
1527
+ process.stderr.write(output + '\n');
1528
+ }
1529
+
1530
+ /**
1531
+ * Displays a horizontal divider line
1532
+ *
1533
+ * @example
1534
+ * logger.divider();
1535
+ *
1536
+ * @since 5.0.0
1537
+ */
1538
+ divider(): void {
1539
+ if (!this._showPrimitives) return;
1540
+ if (!isRunningInTerminal()) {
1541
+ this.getServerFallback().divider();
1542
+ return;
1543
+ }
1544
+ const output = renderDivider();
1545
+ process.stderr.write(output + '\n');
1546
+ }
1547
+
1548
+ /**
1549
+ * Outputs a blank line
1550
+ *
1551
+ * @example
1552
+ * logger.blank();
1553
+ *
1554
+ * @since 5.0.0
1555
+ */
1556
+ blank(): void {
1557
+ if (!this._showPrimitives) return;
1558
+ if (!isRunningInTerminal()) {
1559
+ this.getServerFallback().blank();
1560
+ return;
1561
+ }
1562
+ process.stderr.write('\n');
1563
+ }
1564
+
1565
+ /**
1566
+ * Renders content inside a bordered box
1567
+ *
1568
+ * @param {string} content - Content string (may contain newlines)
1569
+ * @param {IBoxOptions} options - Box rendering options
1570
+ *
1571
+ * @example
1572
+ * logger.box('3 commits generated\nProvider: Groq', { title: 'Done', borderColor: '#00ff00' });
1573
+ *
1574
+ * @since 5.0.0
1575
+ */
1576
+ box(content: string, options?: IBoxOptions): void {
1577
+ if (!this._showPrimitives) return;
1578
+ if (!isRunningInTerminal()) {
1579
+ this.getServerFallback().box(content, options);
1580
+ return;
1581
+ }
1582
+ const colorCap = getColorCapability();
1583
+ const output = renderBox(content, options, colorCap);
1584
+ process.stderr.write(output + '\n');
1585
+ }
1586
+
1587
+ /**
1588
+ * Renders an array of objects as a formatted ASCII table.
1589
+ * Note: This is distinct from the existing table() method which uses console.table.
1590
+ *
1591
+ * @param {Record<string, unknown>[]} rows - Array of row objects
1592
+ * @param {ITableOptions} options - Table rendering options
1593
+ *
1594
+ * @example
1595
+ * logger.cliTable([
1596
+ * { provider: 'Groq', status: 'Available', model: 'llama-3.3-70b' },
1597
+ * { provider: 'Gemini', status: 'Configured', model: 'gemini-2.5-flash' },
1598
+ * ]);
1599
+ *
1600
+ * @since 5.0.0
1601
+ */
1602
+ cliTable(rows: Record<string, unknown>[], options?: ITableOptions): void {
1603
+ if (!this._showPrimitives) return;
1604
+ if (!isRunningInTerminal()) {
1605
+ this.getServerFallback().cliTable(rows, options);
1606
+ return;
1607
+ }
1608
+ const colorCap = getColorCapability();
1609
+ const output = renderTable(rows, options, colorCap);
1610
+ process.stderr.write(output + '\n');
1611
+ }
1612
+
1613
+ /**
1614
+ * Creates a spinner handle for showing progress during async operations.
1615
+ * Returns a NoopSpinner in non-TTY environments.
1616
+ *
1617
+ * @param {string} message - Initial spinner text
1618
+ * @returns {ISpinnerHandle} Spinner controller
1619
+ *
1620
+ * @example
1621
+ * const s = logger.spinner('Analyzing repository...');
1622
+ * s.start();
1623
+ * await analyzeRepo();
1624
+ * s.succeed('Analysis complete (1.2s)');
1625
+ *
1626
+ * @since 5.0.0
1627
+ */
1628
+ spinner(message: string): ISpinnerHandle {
1629
+ if (!isRunningInTerminal() || this.config.outputMode === 'silent') {
1630
+ return new NoopSpinner(message, this);
1631
+ }
1632
+ return new SpinnerManager(message, this.config, this);
1633
+ }
1634
+
1635
+ /**
1636
+ * Sets the CLI verbosity level, controlling both log verbosity and primitive visibility
1637
+ *
1638
+ * @param {CLILogLevel} level - CLI log level
1639
+ *
1640
+ * @example
1641
+ * logger.setCLILevel('quiet'); // Only errors, no CLI primitives
1642
+ * logger.setCLILevel('verbose'); // Debug logs + all CLI primitives
1643
+ *
1644
+ * @since 5.0.0
1645
+ */
1646
+ setCLILevel(level: CLILogLevel): void {
1647
+ const mapping = CLI_LEVEL_MAP[level];
1648
+ this.setVerbosity(mapping.verbosity);
1649
+ this._showPrimitives = mapping.showPrimitives;
1650
+ this.config.cliLevel = level;
1651
+ }
1652
+
1653
+ /**
1654
+ * Returns the current CLI log level
1655
+ * @returns {CLILogLevel} Current CLI log level
1656
+ * @since 5.0.0
1657
+ */
1658
+ get cliLevel(): CLILogLevel {
1659
+ return this.config.cliLevel ?? 'normal';
1660
+ }
1661
+
1662
+ /** Lazily creates the server fallback instance @private */
1663
+ private getServerFallback(): ServerFallback {
1664
+ if (!this._serverFallback) {
1665
+ this._serverFallback = new ServerFallback(this);
1666
+ }
1667
+ return this._serverFallback;
1668
+ }
1669
+
1670
+ // ===== OUTPUT WRITER SYSTEM =====
1671
+
1672
+ /**
1673
+ * Writes formatted output to the configured destination.
1674
+ * Respects outputMode configuration for console, silent, or custom output.
1675
+ *
1676
+ * @private
1677
+ * @param {string} message - Formatted log message
1678
+ * @param {LogLevel} level - Log level
1679
+ * @param {string[]} styles - CSS styles for browser console
1680
+ * @param {any[]} additionalArgs - Additional arguments to log
1681
+ * @since 4.0.0
1682
+ */
1683
+ private writeOutput(
1684
+ message: string,
1685
+ level: LogLevel,
1686
+ styles: string[],
1687
+ additionalArgs: any[]
1688
+ ): void {
1689
+ const mode = this.config.outputMode ?? 'console';
1690
+
1691
+ // Silent mode: no output
1692
+ if (mode === 'silent') {
1693
+ return;
1694
+ }
1695
+
1696
+ // Custom mode: use configured writer
1697
+ if (mode === 'custom' && this.config.outputWriter) {
1698
+ const fullMessage = additionalArgs.length > 0
1699
+ ? `${message} ${additionalArgs.map(a => String(a)).join(' ')}`
1700
+ : message;
1701
+ this.config.outputWriter.write(fullMessage, level, styles);
1702
+ return;
1703
+ }
1704
+
1705
+ // Default: console output
1706
+ if (additionalArgs.length > 0) {
1707
+ console.log(message, ...styles, ...additionalArgs);
1708
+ } else {
1709
+ console.log(message, ...styles);
1710
+ }
1711
+ }
1712
+
1466
1713
  // ===== CLI SYSTEM =====
1467
1714
 
1468
1715
  /**
@@ -1,5 +1,5 @@
1
1
  import { Logger } from './Logger.js';
2
- import type { TimerEntry, Bindings } from './types/index.js';
2
+ import type { TimerEntry, Bindings, ISpinnerHandle, IBoxOptions, ITableOptions, CLILogLevel } from './types/index.js';
3
3
 
4
4
  export class ScopedLogger {
5
5
  private readonly parent: Logger;
@@ -105,6 +105,48 @@ export class ScopedLogger {
105
105
  console.trace(`[${this.scopeName}]`);
106
106
  }
107
107
 
108
+ // ===== CLI PRIMITIVES (v5.0 delegation) =====
109
+
110
+ /** @see Logger.step */
111
+ step(current: number, total: number, message: string): void {
112
+ this.parent.step(current, total, message);
113
+ }
114
+
115
+ /** @see Logger.header */
116
+ header(title: string, subtitle?: string): void {
117
+ this.parent.header(title, subtitle);
118
+ }
119
+
120
+ /** @see Logger.divider */
121
+ divider(): void {
122
+ this.parent.divider();
123
+ }
124
+
125
+ /** @see Logger.blank */
126
+ blank(): void {
127
+ this.parent.blank();
128
+ }
129
+
130
+ /** @see Logger.box */
131
+ box(content: string, options?: IBoxOptions): void {
132
+ this.parent.box(content, options);
133
+ }
134
+
135
+ /** @see Logger.cliTable */
136
+ cliTable(rows: Record<string, unknown>[], options?: ITableOptions): void {
137
+ this.parent.cliTable(rows, options);
138
+ }
139
+
140
+ /** @see Logger.spinner */
141
+ spinner(message: string): ISpinnerHandle {
142
+ return this.parent.spinner(message);
143
+ }
144
+
145
+ /** @see Logger.setCLILevel */
146
+ setCLILevel(level: CLILogLevel): void {
147
+ this.parent.setCLILevel(level);
148
+ }
149
+
108
150
  _pushContext(context: string): void {
109
151
  this.contextStack.push(context);
110
152
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @fileoverview CLI-only entry point for @mks2508/better-logger/cli
3
+ * Exports only CLI primitives for minimal bundle size in CLI tools.
4
+ * @version 5.0.0
5
+ * @since 5.0.0
6
+ */
7
+
8
+ export { renderStep } from './cli-primitives/step.js';
9
+ export { renderHeader } from './cli-primitives/header.js';
10
+ export { renderDivider } from './cli-primitives/divider.js';
11
+ export { renderBox } from './cli-primitives/box.js';
12
+ export { renderTable } from './cli-primitives/cli-table.js';
13
+ export { SpinnerManager, NoopSpinner } from './cli-primitives/spinner.js';
14
+ export { ServerFallback } from './cli-primitives/server-fallback.js';
15
+
16
+ export type {
17
+ CLILogLevel,
18
+ ISpinnerHandle,
19
+ IBoxOptions,
20
+ ITableOptions,
21
+ } from './types/index.js';
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @fileoverview Box renderer for CLI primitives
3
+ * @since 5.0.0
4
+ */
5
+
6
+ import type { IBoxOptions } from '../types/core.js';
7
+ import type { ColorCapability } from '../terminal/color-converter.js';
8
+ import { getANSIForeground, ANSI } from '../terminal/color-converter.js';
9
+ import { getTerminalWidth } from '../utils/environment-detector.js';
10
+ import { stripAnsi, getVisibleLength } from '../terminal/formatter.js';
11
+
12
+ /**
13
+ * Border character sets for different styles
14
+ */
15
+ const BORDER_CHARS = {
16
+ single: { tl: '\u250c', tr: '\u2510', bl: '\u2514', br: '\u2518', h: '\u2500', v: '\u2502' },
17
+ rounded: { tl: '\u256d', tr: '\u256e', bl: '\u2570', br: '\u256f', h: '\u2500', v: '\u2502' },
18
+ double: { tl: '\u2554', tr: '\u2557', bl: '\u255a', br: '\u255d', h: '\u2550', v: '\u2551' },
19
+ bold: { tl: '\u250f', tr: '\u2513', bl: '\u2517', br: '\u251b', h: '\u2501', v: '\u2503' },
20
+ } as const;
21
+
22
+ /**
23
+ * Renders content inside a bordered box
24
+ * @param content - Content string (may contain newlines)
25
+ * @param options - Box rendering options
26
+ * @param colorCap - Terminal color capability
27
+ * @returns Formatted box string with border
28
+ */
29
+ export function renderBox(content: string, options: IBoxOptions = {}, colorCap: ColorCapability = 'full'): string {
30
+ const {
31
+ title,
32
+ borderColor,
33
+ borderStyle = 'rounded',
34
+ padding = 0,
35
+ } = options;
36
+
37
+ const chars = BORDER_CHARS[borderStyle] ?? BORDER_CHARS.rounded;
38
+ const lines = content.split('\n');
39
+ const maxTermWidth = Math.min(getTerminalWidth() - 4, 80);
40
+
41
+ // Calculate content width from visible text
42
+ const contentWidths = lines.map(l => getVisibleLength(l));
43
+ const titleWidth = title ? stripAnsi(title).length + 2 : 0; // +2 for spaces around title
44
+ const maxContentWidth = Math.max(...contentWidths, titleWidth);
45
+ const innerWidth = Math.min(maxContentWidth + 2, maxTermWidth); // +2 for horizontal padding
46
+
47
+ // Color wrapper for border chars
48
+ const bc = borderColor && colorCap !== 'none'
49
+ ? getANSIForeground(borderColor, colorCap)
50
+ : '';
51
+ const reset = bc ? ANSI.reset : '';
52
+
53
+ const wrap = (char: string) => `${bc}${char}${reset}`;
54
+
55
+ // Build top border (with optional title)
56
+ let topBorder: string;
57
+ if (title) {
58
+ const titleStr = ` ${title} `;
59
+ const afterTitle = innerWidth - stripAnsi(titleStr).length;
60
+ topBorder = ` ${wrap(chars.tl)}${wrap(chars.h)}${wrap(titleStr)}${wrap(chars.h.repeat(Math.max(0, afterTitle - 1)))}${wrap(chars.tr)}`;
61
+ } else {
62
+ topBorder = ` ${wrap(chars.tl)}${wrap(chars.h.repeat(innerWidth))}${wrap(chars.tr)}`;
63
+ }
64
+
65
+ // Build bottom border
66
+ const bottomBorder = ` ${wrap(chars.bl)}${wrap(chars.h.repeat(innerWidth))}${wrap(chars.br)}`;
67
+
68
+ // Build padding lines
69
+ const emptyLine = ` ${wrap(chars.v)}${' '.repeat(innerWidth)}${wrap(chars.v)}`;
70
+ const paddingLines = padding > 0 ? Array(padding).fill(emptyLine) : [];
71
+
72
+ // Build content lines
73
+ const contentLines = lines.map(line => {
74
+ const visible = getVisibleLength(line);
75
+ const pad = innerWidth - visible - 1; // -1 for left space
76
+ return ` ${wrap(chars.v)} ${line}${' '.repeat(Math.max(0, pad))}${wrap(chars.v)}`;
77
+ });
78
+
79
+ return [
80
+ topBorder,
81
+ ...paddingLines,
82
+ ...contentLines,
83
+ ...paddingLines,
84
+ bottomBorder,
85
+ ].join('\n');
86
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @fileoverview CLI table renderer for CLI primitives
3
+ * @since 5.0.0
4
+ */
5
+
6
+ import type { ITableOptions } from '../types/core.js';
7
+ import type { ColorCapability } from '../terminal/color-converter.js';
8
+ import { getANSIForeground, ANSI } from '../terminal/color-converter.js';
9
+ import { getVisibleLength, padToWidth } from '../terminal/formatter.js';
10
+
11
+ /**
12
+ * Renders an array of objects as a formatted ASCII table
13
+ * @param rows - Array of row objects
14
+ * @param options - Table rendering options
15
+ * @param colorCap - Terminal color capability
16
+ * @returns Formatted table string
17
+ */
18
+ export function renderTable(
19
+ rows: Record<string, unknown>[],
20
+ options: ITableOptions = {},
21
+ colorCap: ColorCapability = 'full'
22
+ ): string {
23
+ if (rows.length === 0) return '';
24
+
25
+ // Determine columns from options or first row's keys
26
+ const columns = options.columns ?? Object.keys(rows[0]!);
27
+ const headers = options.head ?? columns;
28
+
29
+ // Calculate column widths (max of header and all values)
30
+ const colWidths = columns.map((col, i) => {
31
+ const headerLen = getVisibleLength(headers[i] ?? col);
32
+ const maxValueLen = rows.reduce((max, row) => {
33
+ const val = String(row[col] ?? '');
34
+ return Math.max(max, getVisibleLength(val));
35
+ }, 0);
36
+ return Math.max(headerLen, maxValueLen) + 2; // +2 for padding
37
+ });
38
+
39
+ // Color helpers
40
+ const cyan = colorCap !== 'none' ? getANSIForeground('#00bcd4', colorCap) : '';
41
+ const dim = colorCap !== 'none' ? ANSI.dim : '';
42
+ const reset = colorCap !== 'none' ? ANSI.reset : '';
43
+
44
+ // Build header row
45
+ const headerRow = ' ' + columns.map((col, i) => {
46
+ const label = headers[i] ?? col;
47
+ return cyan + ANSI.bold + padToWidth(` ${label}`, colWidths[i]!) + reset;
48
+ }).join('');
49
+
50
+ // Build separator
51
+ const separator = ' ' + dim + colWidths.map(w => '\u2500'.repeat(w)).join('\u2500') + reset;
52
+
53
+ // Build data rows
54
+ const dataRows = rows.map(row => {
55
+ return ' ' + columns.map((col, i) => {
56
+ const val = String(row[col] ?? '');
57
+ return padToWidth(` ${val}`, colWidths[i]!);
58
+ }).join('');
59
+ });
60
+
61
+ return [headerRow, separator, ...dataRows].join('\n');
62
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @fileoverview Divider renderer for CLI primitives
3
+ * @since 5.0.0
4
+ */
5
+
6
+ import { ANSI } from '../terminal/color-converter.js';
7
+ import { getTerminalWidth } from '../utils/environment-detector.js';
8
+
9
+ /**
10
+ * Renders a horizontal divider line
11
+ * @param width - Optional explicit width (defaults to terminal width capped at 60)
12
+ * @returns Formatted divider string
13
+ */
14
+ export function renderDivider(width?: number): string {
15
+ const w = width ?? Math.min(getTerminalWidth() - 4, 60);
16
+ return ANSI.dim + ' ' + '\u2500'.repeat(w) + ANSI.reset;
17
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @fileoverview Header renderer for CLI primitives
3
+ * @since 5.0.0
4
+ */
5
+
6
+ import { ANSI } from '../terminal/color-converter.js';
7
+
8
+ /**
9
+ * Renders a styled header with optional subtitle
10
+ * @param title - Main title text
11
+ * @param subtitle - Optional subtitle (rendered dimmed)
12
+ * @returns Formatted header string
13
+ */
14
+ export function renderHeader(title: string, subtitle?: string): string {
15
+ const t = ANSI.bold + title + ANSI.reset;
16
+ const s = subtitle ? ANSI.dim + ` ${subtitle}` + ANSI.reset : '';
17
+ return ` ${t}${s}`;
18
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @fileoverview CLI Primitives - Built-in terminal UI components for better-logger
3
+ * @version 5.0.0
4
+ * @since 5.0.0
5
+ */
6
+
7
+ export { renderStep } from './step.js';
8
+ export { renderHeader } from './header.js';
9
+ export { renderDivider } from './divider.js';
10
+ export { renderBox } from './box.js';
11
+ export { renderTable } from './cli-table.js';
12
+ export { SpinnerManager, NoopSpinner } from './spinner.js';
13
+ export { ServerFallback } from './server-fallback.js';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @fileoverview Server/JSON fallback for CLI primitives in non-TTY environments
3
+ * @since 5.0.0
4
+ */
5
+
6
+ import type { IBoxOptions, ITableOptions } from '../types/core.js';
7
+ import type { Logger } from '../Logger.js';
8
+
9
+ /**
10
+ * Server-mode fallback: outputs CLI primitives as plain logger calls
11
+ * when not running in an interactive terminal.
12
+ *
13
+ * @since 5.0.0
14
+ */
15
+ export class ServerFallback {
16
+ private logger: Logger;
17
+
18
+ constructor(logger: Logger) {
19
+ this.logger = logger;
20
+ }
21
+
22
+ /** Render step as plain info log */
23
+ step(current: number, total: number, msg: string): void {
24
+ this.logger.info(`[${current}/${total}] ${msg}`);
25
+ }
26
+
27
+ /** Render header as plain info log */
28
+ header(title: string, subtitle?: string): void {
29
+ const text = subtitle ? `${title} ${subtitle}` : title;
30
+ this.logger.info(text);
31
+ }
32
+
33
+ /** Divider is a no-op in server mode */
34
+ divider(): void {
35
+ // No-op in server/JSON mode
36
+ }
37
+
38
+ /** Blank line is a no-op in server mode */
39
+ blank(): void {
40
+ // No-op in server/JSON mode
41
+ }
42
+
43
+ /** Render box content as plain info log */
44
+ box(content: string, _options?: IBoxOptions): void {
45
+ this.logger.info(content);
46
+ }
47
+
48
+ /** Render table rows as plain info logs */
49
+ cliTable(rows: Record<string, unknown>[], _options?: ITableOptions): void {
50
+ for (const row of rows) {
51
+ this.logger.info(JSON.stringify(row));
52
+ }
53
+ }
54
+ }