agileflow 2.89.2 → 2.90.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/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* table-formatter.js - Unified Table and List Formatting
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent, TTY-aware formatting for tables, key-value pairs,
|
|
5
|
+
* and lists across all CLI commands.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - TTY detection for smart formatting
|
|
9
|
+
* - Unicode box-drawing for borders
|
|
10
|
+
* - Auto-width calculation
|
|
11
|
+
* - Column alignment
|
|
12
|
+
* - Safe for piping (plain text when not TTY)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* const { formatTable, formatKeyValue, formatList } = require('./table-formatter');
|
|
16
|
+
*
|
|
17
|
+
* // Table with headers
|
|
18
|
+
* console.log(formatTable(
|
|
19
|
+
* ['Name', 'Status', 'Count'],
|
|
20
|
+
* [['Story 1', 'Done', '5'], ['Story 2', 'In Progress', '3']]
|
|
21
|
+
* ));
|
|
22
|
+
*
|
|
23
|
+
* // Key-value pairs
|
|
24
|
+
* console.log(formatKeyValue({ Version: '1.0.0', Status: 'Active' }));
|
|
25
|
+
*
|
|
26
|
+
* // Simple list
|
|
27
|
+
* console.log(formatList(['Item 1', 'Item 2', 'Item 3']));
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const { c } = require('./colors');
|
|
31
|
+
|
|
32
|
+
// Box-drawing characters for different styles
|
|
33
|
+
const BOX_CHARS = {
|
|
34
|
+
// Single-line box
|
|
35
|
+
single: {
|
|
36
|
+
topLeft: '┌',
|
|
37
|
+
topRight: '┐',
|
|
38
|
+
bottomLeft: '└',
|
|
39
|
+
bottomRight: '┘',
|
|
40
|
+
horizontal: '─',
|
|
41
|
+
vertical: '│',
|
|
42
|
+
cross: '┼',
|
|
43
|
+
topT: '┬',
|
|
44
|
+
bottomT: '┴',
|
|
45
|
+
leftT: '├',
|
|
46
|
+
rightT: '┤',
|
|
47
|
+
},
|
|
48
|
+
// Double-line box
|
|
49
|
+
double: {
|
|
50
|
+
topLeft: '╔',
|
|
51
|
+
topRight: '╗',
|
|
52
|
+
bottomLeft: '╚',
|
|
53
|
+
bottomRight: '╝',
|
|
54
|
+
horizontal: '═',
|
|
55
|
+
vertical: '║',
|
|
56
|
+
cross: '╬',
|
|
57
|
+
topT: '╦',
|
|
58
|
+
bottomT: '╩',
|
|
59
|
+
leftT: '╠',
|
|
60
|
+
rightT: '╣',
|
|
61
|
+
},
|
|
62
|
+
// Rounded corners
|
|
63
|
+
rounded: {
|
|
64
|
+
topLeft: '╭',
|
|
65
|
+
topRight: '╮',
|
|
66
|
+
bottomLeft: '╰',
|
|
67
|
+
bottomRight: '╯',
|
|
68
|
+
horizontal: '─',
|
|
69
|
+
vertical: '│',
|
|
70
|
+
cross: '┼',
|
|
71
|
+
topT: '┬',
|
|
72
|
+
bottomT: '┴',
|
|
73
|
+
leftT: '├',
|
|
74
|
+
rightT: '┤',
|
|
75
|
+
},
|
|
76
|
+
// ASCII fallback
|
|
77
|
+
ascii: {
|
|
78
|
+
topLeft: '+',
|
|
79
|
+
topRight: '+',
|
|
80
|
+
bottomLeft: '+',
|
|
81
|
+
bottomRight: '+',
|
|
82
|
+
horizontal: '-',
|
|
83
|
+
vertical: '|',
|
|
84
|
+
cross: '+',
|
|
85
|
+
topT: '+',
|
|
86
|
+
bottomT: '+',
|
|
87
|
+
leftT: '+',
|
|
88
|
+
rightT: '+',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// List bullet styles
|
|
93
|
+
const BULLETS = {
|
|
94
|
+
disc: '•',
|
|
95
|
+
circle: '○',
|
|
96
|
+
square: '■',
|
|
97
|
+
dash: '-',
|
|
98
|
+
arrow: '→',
|
|
99
|
+
check: '✓',
|
|
100
|
+
cross: '✗',
|
|
101
|
+
star: '★',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Strip ANSI escape codes from a string
|
|
106
|
+
* @param {string} str - String potentially containing ANSI codes
|
|
107
|
+
* @returns {string} Plain text without ANSI codes
|
|
108
|
+
*/
|
|
109
|
+
function stripAnsi(str) {
|
|
110
|
+
if (typeof str !== 'string') return String(str);
|
|
111
|
+
// eslint-disable-next-line no-control-regex
|
|
112
|
+
return str.replace(/\x1B\[[0-9;]*m/g, '');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get visible width of a string (excluding ANSI codes)
|
|
117
|
+
* @param {string} str - String to measure
|
|
118
|
+
* @returns {number} Visible character count
|
|
119
|
+
*/
|
|
120
|
+
function visibleWidth(str) {
|
|
121
|
+
return stripAnsi(str).length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Pad a string to a specified width (accounting for ANSI codes)
|
|
126
|
+
* @param {string} str - String to pad
|
|
127
|
+
* @param {number} width - Target width
|
|
128
|
+
* @param {string} align - Alignment ('left', 'right', 'center')
|
|
129
|
+
* @returns {string} Padded string
|
|
130
|
+
*/
|
|
131
|
+
function padString(str, width, align = 'left') {
|
|
132
|
+
const visible = visibleWidth(str);
|
|
133
|
+
const padding = Math.max(0, width - visible);
|
|
134
|
+
|
|
135
|
+
if (align === 'right') {
|
|
136
|
+
return ' '.repeat(padding) + str;
|
|
137
|
+
}
|
|
138
|
+
if (align === 'center') {
|
|
139
|
+
const leftPad = Math.floor(padding / 2);
|
|
140
|
+
const rightPad = padding - leftPad;
|
|
141
|
+
return ' '.repeat(leftPad) + str + ' '.repeat(rightPad);
|
|
142
|
+
}
|
|
143
|
+
// Default: left
|
|
144
|
+
return str + ' '.repeat(padding);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if output is going to a TTY
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
function isTTY() {
|
|
152
|
+
return process.stdout.isTTY === true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Format data as a table
|
|
157
|
+
*
|
|
158
|
+
* @param {string[]} headers - Column headers
|
|
159
|
+
* @param {(string|number)[][]} rows - Table rows (array of arrays)
|
|
160
|
+
* @param {Object} [options={}] - Formatting options
|
|
161
|
+
* @param {string} [options.style='rounded'] - Box style ('single', 'double', 'rounded', 'ascii', 'none')
|
|
162
|
+
* @param {string[]} [options.align] - Column alignments ('left', 'right', 'center')
|
|
163
|
+
* @param {number} [options.maxWidth] - Maximum table width
|
|
164
|
+
* @param {boolean} [options.compact=false] - Compact mode (no borders)
|
|
165
|
+
* @param {string} [options.indent=''] - Prefix each line with this string
|
|
166
|
+
* @returns {string} Formatted table
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* formatTable(
|
|
170
|
+
* ['Name', 'Status'],
|
|
171
|
+
* [['US-0001', 'Done'], ['US-0002', 'In Progress']],
|
|
172
|
+
* { style: 'rounded' }
|
|
173
|
+
* );
|
|
174
|
+
*/
|
|
175
|
+
function formatTable(headers, rows, options = {}) {
|
|
176
|
+
const {
|
|
177
|
+
style = 'rounded',
|
|
178
|
+
align = [],
|
|
179
|
+
maxWidth = process.stdout.columns || 80,
|
|
180
|
+
compact = false,
|
|
181
|
+
indent = '',
|
|
182
|
+
} = options;
|
|
183
|
+
|
|
184
|
+
// Convert all values to strings
|
|
185
|
+
const stringHeaders = headers.map(h => String(h ?? ''));
|
|
186
|
+
const stringRows = rows.map(row => row.map(cell => String(cell ?? '')));
|
|
187
|
+
|
|
188
|
+
// Calculate column widths
|
|
189
|
+
const colWidths = stringHeaders.map((h, i) => {
|
|
190
|
+
const headerWidth = visibleWidth(h);
|
|
191
|
+
const maxCellWidth = stringRows.reduce((max, row) => {
|
|
192
|
+
const cellWidth = visibleWidth(row[i] || '');
|
|
193
|
+
return Math.max(max, cellWidth);
|
|
194
|
+
}, 0);
|
|
195
|
+
return Math.max(headerWidth, maxCellWidth);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Use compact/no-border for non-TTY
|
|
199
|
+
const useCompact = compact || !isTTY();
|
|
200
|
+
const box = useCompact ? null : BOX_CHARS[style] || BOX_CHARS.ascii;
|
|
201
|
+
|
|
202
|
+
const lines = [];
|
|
203
|
+
|
|
204
|
+
if (useCompact) {
|
|
205
|
+
// Compact mode: tab-separated or space-padded
|
|
206
|
+
if (isTTY()) {
|
|
207
|
+
// Padded columns
|
|
208
|
+
const headerLine = stringHeaders
|
|
209
|
+
.map((h, i) => padString(h, colWidths[i], align[i] || 'left'))
|
|
210
|
+
.join(' ');
|
|
211
|
+
lines.push(indent + c.bold + headerLine + c.reset);
|
|
212
|
+
|
|
213
|
+
for (const row of stringRows) {
|
|
214
|
+
const rowLine = row.map((cell, i) => padString(cell, colWidths[i], align[i] || 'left')).join(' ');
|
|
215
|
+
lines.push(indent + rowLine);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
// Tab-separated for piping (still respect indent)
|
|
219
|
+
lines.push(indent + stringHeaders.join('\t'));
|
|
220
|
+
for (const row of stringRows) {
|
|
221
|
+
lines.push(indent + row.join('\t'));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// Bordered table
|
|
226
|
+
const padding = 1;
|
|
227
|
+
const paddedWidths = colWidths.map(w => w + padding * 2);
|
|
228
|
+
|
|
229
|
+
// Top border
|
|
230
|
+
const topBorder =
|
|
231
|
+
box.topLeft +
|
|
232
|
+
paddedWidths.map(w => box.horizontal.repeat(w)).join(box.topT) +
|
|
233
|
+
box.topRight;
|
|
234
|
+
lines.push(indent + c.dim + topBorder + c.reset);
|
|
235
|
+
|
|
236
|
+
// Header row
|
|
237
|
+
const headerCells = stringHeaders.map((h, i) => {
|
|
238
|
+
const padded = padString(h, colWidths[i], align[i] || 'left');
|
|
239
|
+
return ' ' + c.bold + padded + c.reset + ' ';
|
|
240
|
+
});
|
|
241
|
+
lines.push(indent + c.dim + box.vertical + c.reset + headerCells.join(c.dim + box.vertical + c.reset) + c.dim + box.vertical + c.reset);
|
|
242
|
+
|
|
243
|
+
// Header separator
|
|
244
|
+
const headerSep =
|
|
245
|
+
box.leftT +
|
|
246
|
+
paddedWidths.map(w => box.horizontal.repeat(w)).join(box.cross) +
|
|
247
|
+
box.rightT;
|
|
248
|
+
lines.push(indent + c.dim + headerSep + c.reset);
|
|
249
|
+
|
|
250
|
+
// Data rows
|
|
251
|
+
for (const row of stringRows) {
|
|
252
|
+
const cells = row.map((cell, i) => {
|
|
253
|
+
const padded = padString(cell, colWidths[i], align[i] || 'left');
|
|
254
|
+
return ' ' + padded + ' ';
|
|
255
|
+
});
|
|
256
|
+
lines.push(indent + c.dim + box.vertical + c.reset + cells.join(c.dim + box.vertical + c.reset) + c.dim + box.vertical + c.reset);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Bottom border
|
|
260
|
+
const bottomBorder =
|
|
261
|
+
box.bottomLeft +
|
|
262
|
+
paddedWidths.map(w => box.horizontal.repeat(w)).join(box.bottomT) +
|
|
263
|
+
box.bottomRight;
|
|
264
|
+
lines.push(indent + c.dim + bottomBorder + c.reset);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return lines.join('\n');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Format key-value pairs
|
|
272
|
+
*
|
|
273
|
+
* @param {Object|Map|Array<[string,any]>} data - Key-value data
|
|
274
|
+
* @param {Object} [options={}] - Formatting options
|
|
275
|
+
* @param {string} [options.separator=':'] - Key-value separator
|
|
276
|
+
* @param {boolean} [options.alignValues=true] - Align all values to same column
|
|
277
|
+
* @param {string} [options.indent=''] - Prefix each line
|
|
278
|
+
* @param {string} [options.keyColor] - Color code for keys
|
|
279
|
+
* @param {string} [options.valueColor] - Color code for values
|
|
280
|
+
* @returns {string} Formatted key-value pairs
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* formatKeyValue({
|
|
284
|
+
* Version: '2.0.0',
|
|
285
|
+
* Status: 'Active',
|
|
286
|
+
* 'Last Update': '2024-01-15'
|
|
287
|
+
* });
|
|
288
|
+
*/
|
|
289
|
+
function formatKeyValue(data, options = {}) {
|
|
290
|
+
const {
|
|
291
|
+
separator = ':',
|
|
292
|
+
alignValues = true,
|
|
293
|
+
indent = '',
|
|
294
|
+
keyColor = c.bold,
|
|
295
|
+
valueColor = '',
|
|
296
|
+
} = options;
|
|
297
|
+
|
|
298
|
+
// Convert to array of [key, value] pairs
|
|
299
|
+
let pairs;
|
|
300
|
+
if (data instanceof Map) {
|
|
301
|
+
pairs = Array.from(data.entries());
|
|
302
|
+
} else if (Array.isArray(data)) {
|
|
303
|
+
pairs = data;
|
|
304
|
+
} else {
|
|
305
|
+
pairs = Object.entries(data);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Convert values to strings
|
|
309
|
+
pairs = pairs.map(([k, v]) => [String(k), String(v ?? '')]);
|
|
310
|
+
|
|
311
|
+
if (!isTTY()) {
|
|
312
|
+
// Plain text for piping (still respect indent)
|
|
313
|
+
return pairs.map(([k, v]) => `${indent}${k}${separator} ${v}`).join('\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Calculate key width for alignment
|
|
317
|
+
const maxKeyWidth = alignValues
|
|
318
|
+
? Math.max(...pairs.map(([k]) => visibleWidth(k)))
|
|
319
|
+
: 0;
|
|
320
|
+
|
|
321
|
+
const lines = pairs.map(([key, value]) => {
|
|
322
|
+
const paddedKey = alignValues ? padString(key, maxKeyWidth, 'left') : key;
|
|
323
|
+
return `${indent}${keyColor}${paddedKey}${c.reset}${separator} ${valueColor}${value}${valueColor ? c.reset : ''}`;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return lines.join('\n');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Format a list of items
|
|
331
|
+
*
|
|
332
|
+
* @param {(string|{text:string,status?:string})[]} items - List items
|
|
333
|
+
* @param {Object} [options={}] - Formatting options
|
|
334
|
+
* @param {string} [options.bullet='disc'] - Bullet style ('disc', 'circle', 'square', 'dash', 'arrow', 'check', 'cross', 'star', 'number')
|
|
335
|
+
* @param {string} [options.indent=''] - Prefix each line
|
|
336
|
+
* @param {boolean} [options.numbered=false] - Use numbers instead of bullets
|
|
337
|
+
* @param {number} [options.startNumber=1] - Starting number for numbered lists
|
|
338
|
+
* @param {string} [options.itemColor] - Color code for items
|
|
339
|
+
* @returns {string} Formatted list
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* formatList(['Item 1', 'Item 2', 'Item 3'], { bullet: 'check' });
|
|
343
|
+
*
|
|
344
|
+
* // With status indicators
|
|
345
|
+
* formatList([
|
|
346
|
+
* { text: 'Task 1', status: 'done' },
|
|
347
|
+
* { text: 'Task 2', status: 'pending' }
|
|
348
|
+
* ]);
|
|
349
|
+
*/
|
|
350
|
+
function formatList(items, options = {}) {
|
|
351
|
+
const {
|
|
352
|
+
bullet = 'disc',
|
|
353
|
+
indent = '',
|
|
354
|
+
numbered = false,
|
|
355
|
+
startNumber = 1,
|
|
356
|
+
itemColor = '',
|
|
357
|
+
} = options;
|
|
358
|
+
|
|
359
|
+
if (!isTTY()) {
|
|
360
|
+
// Plain text for piping
|
|
361
|
+
return items
|
|
362
|
+
.map((item, i) => {
|
|
363
|
+
const text = typeof item === 'object' ? item.text : item;
|
|
364
|
+
const prefix = numbered ? `${startNumber + i}.` : '-';
|
|
365
|
+
return `${prefix} ${text}`;
|
|
366
|
+
})
|
|
367
|
+
.join('\n');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const bulletChar = BULLETS[bullet] || BULLETS.disc;
|
|
371
|
+
const numWidth = numbered ? String(startNumber + items.length - 1).length : 0;
|
|
372
|
+
|
|
373
|
+
const lines = items.map((item, i) => {
|
|
374
|
+
const text = typeof item === 'object' ? item.text : String(item);
|
|
375
|
+
const status = typeof item === 'object' ? item.status : null;
|
|
376
|
+
|
|
377
|
+
let prefix;
|
|
378
|
+
if (numbered) {
|
|
379
|
+
prefix = `${String(startNumber + i).padStart(numWidth)}.`;
|
|
380
|
+
} else if (status) {
|
|
381
|
+
// Status-based bullet
|
|
382
|
+
switch (status) {
|
|
383
|
+
case 'done':
|
|
384
|
+
case 'completed':
|
|
385
|
+
case 'success':
|
|
386
|
+
prefix = c.green + BULLETS.check + c.reset;
|
|
387
|
+
break;
|
|
388
|
+
case 'error':
|
|
389
|
+
case 'failed':
|
|
390
|
+
prefix = c.red + BULLETS.cross + c.reset;
|
|
391
|
+
break;
|
|
392
|
+
case 'pending':
|
|
393
|
+
case 'waiting':
|
|
394
|
+
prefix = c.yellow + BULLETS.circle + c.reset;
|
|
395
|
+
break;
|
|
396
|
+
case 'active':
|
|
397
|
+
case 'in_progress':
|
|
398
|
+
prefix = c.cyan + BULLETS.disc + c.reset;
|
|
399
|
+
break;
|
|
400
|
+
default:
|
|
401
|
+
prefix = c.dim + bulletChar + c.reset;
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
prefix = c.dim + bulletChar + c.reset;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const coloredText = itemColor ? `${itemColor}${text}${c.reset}` : text;
|
|
408
|
+
return `${indent}${prefix} ${coloredText}`;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return lines.join('\n');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Format a horizontal divider
|
|
416
|
+
*
|
|
417
|
+
* @param {Object} [options={}] - Formatting options
|
|
418
|
+
* @param {number} [options.width] - Divider width (default: terminal width)
|
|
419
|
+
* @param {string} [options.char='─'] - Character to use
|
|
420
|
+
* @param {string} [options.style] - Style ('single', 'double', 'dotted', 'bold')
|
|
421
|
+
* @returns {string} Formatted divider
|
|
422
|
+
*/
|
|
423
|
+
function formatDivider(options = {}) {
|
|
424
|
+
const { width = process.stdout.columns || 80, char, style = 'single' } = options;
|
|
425
|
+
|
|
426
|
+
if (!isTTY()) {
|
|
427
|
+
return '-'.repeat(Math.min(width, 80));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const chars = {
|
|
431
|
+
single: '─',
|
|
432
|
+
double: '═',
|
|
433
|
+
dotted: '┄',
|
|
434
|
+
bold: '━',
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const divChar = char || chars[style] || chars.single;
|
|
438
|
+
return c.dim + divChar.repeat(width) + c.reset;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Format a section header
|
|
443
|
+
*
|
|
444
|
+
* @param {string} title - Section title
|
|
445
|
+
* @param {Object} [options={}] - Formatting options
|
|
446
|
+
* @param {string} [options.subtitle] - Optional subtitle
|
|
447
|
+
* @param {string} [options.indent=''] - Prefix
|
|
448
|
+
* @returns {string} Formatted header
|
|
449
|
+
*/
|
|
450
|
+
function formatHeader(title, options = {}) {
|
|
451
|
+
const { subtitle, indent = '' } = options;
|
|
452
|
+
|
|
453
|
+
if (!isTTY()) {
|
|
454
|
+
let header = `\n${title}`;
|
|
455
|
+
if (subtitle) header += ` - ${subtitle}`;
|
|
456
|
+
header += '\n' + '='.repeat(Math.min(visibleWidth(title) + (subtitle ? subtitle.length + 3 : 0), 40));
|
|
457
|
+
return header;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let header = `\n${indent}${c.bold}${title}${c.reset}`;
|
|
461
|
+
if (subtitle) {
|
|
462
|
+
header += ` ${c.dim}${subtitle}${c.reset}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return header;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Truncate a string to fit within a width
|
|
470
|
+
*
|
|
471
|
+
* @param {string} str - String to truncate
|
|
472
|
+
* @param {number} maxWidth - Maximum width
|
|
473
|
+
* @param {string} [suffix='…'] - Suffix to add when truncated
|
|
474
|
+
* @returns {string} Truncated string
|
|
475
|
+
*/
|
|
476
|
+
function truncate(str, maxWidth, suffix = '…') {
|
|
477
|
+
if (typeof str !== 'string') str = String(str);
|
|
478
|
+
const width = visibleWidth(str);
|
|
479
|
+
if (width <= maxWidth) return str;
|
|
480
|
+
|
|
481
|
+
const suffixWidth = visibleWidth(suffix);
|
|
482
|
+
const targetWidth = maxWidth - suffixWidth;
|
|
483
|
+
|
|
484
|
+
// Need to handle ANSI codes carefully
|
|
485
|
+
const plain = stripAnsi(str);
|
|
486
|
+
if (plain.length <= maxWidth) return str;
|
|
487
|
+
|
|
488
|
+
return plain.slice(0, targetWidth) + suffix;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
module.exports = {
|
|
492
|
+
formatTable,
|
|
493
|
+
formatKeyValue,
|
|
494
|
+
formatList,
|
|
495
|
+
formatDivider,
|
|
496
|
+
formatHeader,
|
|
497
|
+
truncate,
|
|
498
|
+
stripAnsi,
|
|
499
|
+
visibleWidth,
|
|
500
|
+
padString,
|
|
501
|
+
isTTY,
|
|
502
|
+
BOX_CHARS,
|
|
503
|
+
BULLETS,
|
|
504
|
+
};
|