padrone 1.6.0 → 1.7.1
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 +14 -0
- package/dist/docs/index.mjs +1 -1
- package/dist/{errors-CL63UOzt.mjs → errors-DA4KzK1M.mjs} +1 -1
- package/dist/{errors-CL63UOzt.mjs.map → errors-DA4KzK1M.mjs.map} +1 -1
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -1
- package/dist/{help-B5Kk83of.mjs → help-BtxLgrF_.mjs} +88 -55
- package/dist/help-BtxLgrF_.mjs.map +1 -0
- package/dist/{index-BaU3X6dY.d.mts → index-C0Tab27T.d.mts} +25 -4
- package/dist/{index-BaU3X6dY.d.mts.map → index-C0Tab27T.d.mts.map} +1 -1
- package/dist/index.d.mts +143 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +363 -24
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-BM-d0nZi.mjs → mcp-6-Jw4Bpq.mjs} +2 -2
- package/dist/{mcp-BM-d0nZi.mjs.map → mcp-6-Jw4Bpq.mjs.map} +1 -1
- package/dist/{serve-Bk0JUlCj.mjs → serve-YVTPzBCl.mjs} +3 -3
- package/dist/{serve-Bk0JUlCj.mjs.map → serve-YVTPzBCl.mjs.map} +1 -1
- package/dist/test.d.mts +1 -1
- package/dist/zod.d.mts +1 -1
- package/package.json +1 -1
- package/src/core/create.ts +33 -0
- package/src/extension/auto-output.ts +72 -21
- package/src/extension/config.ts +1 -1
- package/src/extension/index.ts +1 -0
- package/src/extension/logger.ts +49 -1
- package/src/index.ts +5 -1
- package/src/output/formatter.ts +16 -168
- package/src/output/output-indicator.ts +87 -0
- package/src/output/primitives.ts +335 -0
- package/src/output/styling.ts +221 -0
- package/src/types/builder.ts +87 -37
- package/src/types/index.ts +1 -0
- package/dist/help-B5Kk83of.mjs.map +0 -1
package/src/output/formatter.ts
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
import { camelToKebab } from '../util/shell-utils.ts';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
if (current) lines.push(current);
|
|
20
|
-
return lines.length > 0 ? lines : [text];
|
|
21
|
-
}
|
|
2
|
+
import type { ColorConfig, ColorTheme } from './colorizer.ts';
|
|
3
|
+
import {
|
|
4
|
+
createAnsiStyler,
|
|
5
|
+
createConsoleStyler,
|
|
6
|
+
createHtmlLayout,
|
|
7
|
+
createHtmlStyler,
|
|
8
|
+
createMarkdownLayout,
|
|
9
|
+
createMarkdownStyler,
|
|
10
|
+
createTextLayout,
|
|
11
|
+
createTextStyler,
|
|
12
|
+
DEFAULT_TERMINAL_WIDTH,
|
|
13
|
+
type LayoutConfig,
|
|
14
|
+
type Styler,
|
|
15
|
+
shouldUseAnsi,
|
|
16
|
+
wrapText,
|
|
17
|
+
} from './styling.ts';
|
|
22
18
|
|
|
23
19
|
export type HelpFormat = 'text' | 'ansi' | 'console' | 'markdown' | 'html' | 'json';
|
|
24
20
|
export type HelpDetail = 'minimal' | 'standard' | 'full';
|
|
@@ -144,143 +140,6 @@ export type Formatter = {
|
|
|
144
140
|
format: (info: HelpInfo) => string;
|
|
145
141
|
};
|
|
146
142
|
|
|
147
|
-
// ============================================================================
|
|
148
|
-
// Internal Styling Types
|
|
149
|
-
// ============================================================================
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Internal styling functions used by formatters.
|
|
153
|
-
* These handle the visual styling of individual text elements.
|
|
154
|
-
*/
|
|
155
|
-
type Styler = {
|
|
156
|
-
command: (text: string) => string;
|
|
157
|
-
arg: (text: string) => string;
|
|
158
|
-
type: (text: string) => string;
|
|
159
|
-
description: (text: string) => string;
|
|
160
|
-
label: (text: string) => string;
|
|
161
|
-
section: (text: string) => string;
|
|
162
|
-
meta: (text: string) => string;
|
|
163
|
-
example: (text: string) => string;
|
|
164
|
-
exampleValue: (text: string) => string;
|
|
165
|
-
deprecated: (text: string) => string;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Layout configuration for formatters.
|
|
170
|
-
*/
|
|
171
|
-
type LayoutConfig = {
|
|
172
|
-
newline: string;
|
|
173
|
-
indent: (level: number) => string;
|
|
174
|
-
join: (parts: string[]) => string;
|
|
175
|
-
wrapDocument?: (content: string) => string;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// ============================================================================
|
|
179
|
-
// Styler Factories
|
|
180
|
-
// ============================================================================
|
|
181
|
-
|
|
182
|
-
function createTextStyler(): Styler {
|
|
183
|
-
return {
|
|
184
|
-
command: (text) => text,
|
|
185
|
-
arg: (text) => text,
|
|
186
|
-
type: (text) => text,
|
|
187
|
-
description: (text) => text,
|
|
188
|
-
label: (text) => text,
|
|
189
|
-
section: (text) => text,
|
|
190
|
-
meta: (text) => text,
|
|
191
|
-
example: (text) => text,
|
|
192
|
-
exampleValue: (text) => text,
|
|
193
|
-
deprecated: (text) => text,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function createAnsiStyler(theme?: ColorTheme | ColorConfig): Styler {
|
|
198
|
-
const colorizer = createColorizer(theme);
|
|
199
|
-
return {
|
|
200
|
-
command: colorizer.command,
|
|
201
|
-
arg: colorizer.arg,
|
|
202
|
-
type: colorizer.type,
|
|
203
|
-
description: colorizer.description,
|
|
204
|
-
label: colorizer.label,
|
|
205
|
-
section: colorizer.label,
|
|
206
|
-
meta: colorizer.meta,
|
|
207
|
-
example: colorizer.example,
|
|
208
|
-
exampleValue: colorizer.exampleValue,
|
|
209
|
-
deprecated: colorizer.deprecated,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function createConsoleStyler(theme?: ColorTheme | ColorConfig): Styler {
|
|
214
|
-
return createAnsiStyler(theme);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function createMarkdownStyler(): Styler {
|
|
218
|
-
return {
|
|
219
|
-
command: (text) => `**${text}**`,
|
|
220
|
-
arg: (text) => `\`${text}\``,
|
|
221
|
-
type: (text) => `\`${text}\``,
|
|
222
|
-
description: (text) => text,
|
|
223
|
-
label: (text) => `**${text}**`,
|
|
224
|
-
section: (text) => `### ${text}`,
|
|
225
|
-
meta: (text) => `*${text}*`,
|
|
226
|
-
example: (text) => `**${text}**`,
|
|
227
|
-
exampleValue: (text) => `\`${text}\``,
|
|
228
|
-
deprecated: (text) => `~~${text}~~`,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function escapeHtml(text: string): string {
|
|
233
|
-
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function createHtmlStyler(): Styler {
|
|
237
|
-
return {
|
|
238
|
-
command: (text) => `<strong style="color: #00bcd4;">${escapeHtml(text)}</strong>`,
|
|
239
|
-
arg: (text) => `<code style="color: #4caf50;">${escapeHtml(text)}</code>`,
|
|
240
|
-
type: (text) => `<code style="color: #ff9800;">${escapeHtml(text)}</code>`,
|
|
241
|
-
description: (text) => `<span style="color: #666;">${escapeHtml(text)}</span>`,
|
|
242
|
-
label: (text) => `<strong>${escapeHtml(text)}</strong>`,
|
|
243
|
-
section: (text) => `<h3>${escapeHtml(text)}</h3>`,
|
|
244
|
-
meta: (text) => `<span style="color: #999;">${escapeHtml(text)}</span>`,
|
|
245
|
-
example: (text) => `<strong style="text-decoration: underline;">${escapeHtml(text)}</strong>`,
|
|
246
|
-
exampleValue: (text) => `<em>${escapeHtml(text)}</em>`,
|
|
247
|
-
deprecated: (text) => `<del style="color: #999;">${escapeHtml(text)}</del>`,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ============================================================================
|
|
252
|
-
// Layout Configurations
|
|
253
|
-
// ============================================================================
|
|
254
|
-
|
|
255
|
-
function createTextLayout(): LayoutConfig {
|
|
256
|
-
return {
|
|
257
|
-
newline: '\n',
|
|
258
|
-
indent: (level) => ' '.repeat(level),
|
|
259
|
-
join: (parts) => parts.filter(Boolean).join(' '),
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function createMarkdownLayout(): LayoutConfig {
|
|
264
|
-
return {
|
|
265
|
-
newline: '\n\n',
|
|
266
|
-
indent: (level) => {
|
|
267
|
-
if (level === 0) return '';
|
|
268
|
-
if (level === 1) return ' ';
|
|
269
|
-
return ' ';
|
|
270
|
-
},
|
|
271
|
-
join: (parts) => parts.filter(Boolean).join(' '),
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function createHtmlLayout(): LayoutConfig {
|
|
276
|
-
return {
|
|
277
|
-
newline: '<br>',
|
|
278
|
-
indent: (level) => ' '.repeat(level),
|
|
279
|
-
join: (parts) => parts.filter(Boolean).join(' '),
|
|
280
|
-
wrapDocument: (content) => `<div style="font-family: monospace; line-height: 1.6;">${content}</div>`,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
143
|
// ============================================================================
|
|
285
144
|
// Generic Formatter Implementation
|
|
286
145
|
// ============================================================================
|
|
@@ -729,17 +588,6 @@ function createJsonFormatter(): Formatter {
|
|
|
729
588
|
};
|
|
730
589
|
}
|
|
731
590
|
|
|
732
|
-
// ============================================================================
|
|
733
|
-
// Formatter Factory
|
|
734
|
-
// ============================================================================
|
|
735
|
-
|
|
736
|
-
function shouldUseAnsi(env?: Record<string, string | undefined>, isTTY?: boolean): boolean {
|
|
737
|
-
if (env?.NO_COLOR) return false;
|
|
738
|
-
if (env?.CI) return false;
|
|
739
|
-
if (typeof isTTY === 'boolean') return isTTY;
|
|
740
|
-
return false;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
591
|
// ============================================================================
|
|
744
592
|
// Minimal Formatter
|
|
745
593
|
// ============================================================================
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { KeyValueOptions, ListItem, ListOptions, TableOptions, TreeNode, TreeOptions } from './primitives.ts';
|
|
2
|
+
import { renderKeyValue, renderList, renderTable, renderTree } from './primitives.ts';
|
|
3
|
+
import type { OutputContext } from './styling.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runtime output helper injected into action context as `ctx.context.output`.
|
|
7
|
+
* Provides format-aware output primitives (table, tree, list, key-value).
|
|
8
|
+
*
|
|
9
|
+
* Each method renders data using the resolved format (ANSI, text, JSON, markdown, HTML)
|
|
10
|
+
* and writes it to the runtime's output function.
|
|
11
|
+
*/
|
|
12
|
+
export type PadroneOutputIndicator = {
|
|
13
|
+
/** Render data as a table. */
|
|
14
|
+
table(data: Record<string, unknown>[], options?: TableOptions): void;
|
|
15
|
+
/** Render data as a tree. */
|
|
16
|
+
tree(data: TreeNode | TreeNode[], options?: TreeOptions): void;
|
|
17
|
+
/** Render data as a list. */
|
|
18
|
+
list(data: ListItem[], options?: ListOptions): void;
|
|
19
|
+
/** Render data as aligned key-value pairs. */
|
|
20
|
+
kv(data: Record<string, unknown>, options?: KeyValueOptions): void;
|
|
21
|
+
/** Write raw output (same as runtime.output but sets the "already called" flag). */
|
|
22
|
+
raw(...args: unknown[]): void;
|
|
23
|
+
/** Whether any output method has been called. */
|
|
24
|
+
readonly called: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Declarative output configuration for a command. */
|
|
28
|
+
export type OutputPrimitiveType = 'table' | 'tree' | 'list' | 'kv' | 'json';
|
|
29
|
+
export type OutputConfig =
|
|
30
|
+
| OutputPrimitiveType
|
|
31
|
+
| { type: OutputPrimitiveType; options?: TableOptions | TreeOptions | ListOptions | KeyValueOptions };
|
|
32
|
+
|
|
33
|
+
/** Create an output indicator that renders through the given output function and format context. */
|
|
34
|
+
export function createOutputIndicator(outputFn: (...args: unknown[]) => void, ctx: OutputContext): PadroneOutputIndicator {
|
|
35
|
+
let _called = false;
|
|
36
|
+
|
|
37
|
+
const emit = (rendered: string) => {
|
|
38
|
+
_called = true;
|
|
39
|
+
outputFn(rendered);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
table(data, options) {
|
|
44
|
+
emit(renderTable(data, options, ctx));
|
|
45
|
+
},
|
|
46
|
+
tree(data, options) {
|
|
47
|
+
emit(renderTree(data, options, ctx));
|
|
48
|
+
},
|
|
49
|
+
list(data, options) {
|
|
50
|
+
emit(renderList(data, options, ctx));
|
|
51
|
+
},
|
|
52
|
+
kv(data, options) {
|
|
53
|
+
emit(renderKeyValue(data, options, ctx));
|
|
54
|
+
},
|
|
55
|
+
raw(...args) {
|
|
56
|
+
_called = true;
|
|
57
|
+
outputFn(...args);
|
|
58
|
+
},
|
|
59
|
+
get called() {
|
|
60
|
+
return _called;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Format a return value using a declarative output config. */
|
|
66
|
+
export function formatDeclarativeOutput(value: unknown, config: OutputConfig, ctx: OutputContext): string | undefined {
|
|
67
|
+
const type = typeof config === 'string' ? config : config.type;
|
|
68
|
+
const options = typeof config === 'object' ? config.options : undefined;
|
|
69
|
+
|
|
70
|
+
switch (type) {
|
|
71
|
+
case 'table':
|
|
72
|
+
if (!Array.isArray(value)) return undefined;
|
|
73
|
+
return renderTable(value as Record<string, unknown>[], options as TableOptions | undefined, ctx);
|
|
74
|
+
case 'tree':
|
|
75
|
+
return renderTree(value as TreeNode | TreeNode[], options as TreeOptions | undefined, ctx);
|
|
76
|
+
case 'list':
|
|
77
|
+
if (!Array.isArray(value)) return undefined;
|
|
78
|
+
return renderList(value as ListItem[], options as ListOptions | undefined, ctx);
|
|
79
|
+
case 'kv':
|
|
80
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
|
|
81
|
+
return renderKeyValue(value as Record<string, unknown>, options as KeyValueOptions | undefined, ctx);
|
|
82
|
+
case 'json':
|
|
83
|
+
return JSON.stringify(value, null, 2);
|
|
84
|
+
default:
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { escapeHtml, type OutputContext } from './styling.ts';
|
|
2
|
+
|
|
3
|
+
// ── Table ───────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type TableOptions = {
|
|
6
|
+
/** Explicit column keys to display (default: infer from first row's keys). */
|
|
7
|
+
columns?: string[];
|
|
8
|
+
/** Column key → display header name mapping. */
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
/** Column key → text alignment. */
|
|
11
|
+
align?: Record<string, 'left' | 'right' | 'center'>;
|
|
12
|
+
/** Maximum column width before truncation. */
|
|
13
|
+
maxColumnWidth?: number;
|
|
14
|
+
/** Show borders (default: true for ansi/text, false for others). */
|
|
15
|
+
border?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function stringifyCell(value: unknown): string {
|
|
19
|
+
if (value === undefined || value === null) return '';
|
|
20
|
+
if (typeof value === 'string') return value;
|
|
21
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function truncate(text: string, max: number): string {
|
|
26
|
+
if (max <= 0 || text.length <= max) return text;
|
|
27
|
+
return max <= 1 ? '…' : `${text.slice(0, max - 1)}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function padCell(text: string, width: number, alignment: 'left' | 'right' | 'center' = 'left'): string {
|
|
31
|
+
const pad = width - text.length;
|
|
32
|
+
if (pad <= 0) return text;
|
|
33
|
+
if (alignment === 'right') return ' '.repeat(pad) + text;
|
|
34
|
+
if (alignment === 'center') {
|
|
35
|
+
const left = Math.floor(pad / 2);
|
|
36
|
+
return ' '.repeat(left) + text + ' '.repeat(pad - left);
|
|
37
|
+
}
|
|
38
|
+
return text + ' '.repeat(pad);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function renderTable(data: Record<string, unknown>[], options: TableOptions | undefined, ctx: OutputContext): string {
|
|
42
|
+
if (data.length === 0) return '';
|
|
43
|
+
if (ctx.format === 'json') return JSON.stringify(data, null, 2);
|
|
44
|
+
|
|
45
|
+
const columns = options?.columns ?? Object.keys(data[0]!);
|
|
46
|
+
if (columns.length === 0) return '';
|
|
47
|
+
|
|
48
|
+
const headers = columns.map((col) => options?.headers?.[col] ?? col);
|
|
49
|
+
const maxCol = options?.maxColumnWidth;
|
|
50
|
+
|
|
51
|
+
const rows = data.map((row) =>
|
|
52
|
+
columns.map((col) => {
|
|
53
|
+
const text = stringifyCell(row[col]);
|
|
54
|
+
return maxCol ? truncate(text, maxCol) : text;
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const colWidths = columns.map((_, i) => {
|
|
59
|
+
const headerWidth = headers[i]!.length;
|
|
60
|
+
const maxCellWidth = rows.reduce((max, row) => Math.max(max, row[i]!.length), 0);
|
|
61
|
+
return Math.max(headerWidth, maxCellWidth);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const getAlign = (i: number): 'left' | 'right' | 'center' => options?.align?.[columns[i]!] ?? 'left';
|
|
65
|
+
|
|
66
|
+
if (ctx.format === 'markdown') return renderTableMarkdown(headers, rows, colWidths, getAlign);
|
|
67
|
+
if (ctx.format === 'html') return renderTableHtml(columns, headers, rows, data, getAlign);
|
|
68
|
+
return renderTableText(headers, rows, colWidths, getAlign, options?.border !== false, ctx);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderTableText(
|
|
72
|
+
headers: string[],
|
|
73
|
+
rows: string[][],
|
|
74
|
+
colWidths: number[],
|
|
75
|
+
getAlign: (i: number) => 'left' | 'right' | 'center',
|
|
76
|
+
border: boolean,
|
|
77
|
+
ctx: OutputContext,
|
|
78
|
+
): string {
|
|
79
|
+
const { styler } = ctx;
|
|
80
|
+
const formatRow = (cells: string[], style?: (s: string) => string) =>
|
|
81
|
+
cells.map((cell, i) => {
|
|
82
|
+
const padded = padCell(cell, colWidths[i]!, getAlign(i));
|
|
83
|
+
return style ? style(padded) : padded;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (border) {
|
|
87
|
+
const sep = ctx.styler.meta('─');
|
|
88
|
+
const divider = colWidths.map((w) => sep.repeat(w + 2)).join(styler.meta('┼'));
|
|
89
|
+
const row = (cells: string[]) => cells.map((c, i) => ` ${padCell(c, colWidths[i]!, getAlign(i))} `).join(styler.meta('│'));
|
|
90
|
+
const headerRow = row(headers.map((h) => styler.label(h)));
|
|
91
|
+
const dataRows = rows.map((r) => row(r.map((c) => styler.description(c))));
|
|
92
|
+
return [headerRow, styler.meta('─') + divider + styler.meta('─'), ...dataRows].join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const headerCells = formatRow(headers, styler.label);
|
|
96
|
+
const dataCells = rows.map((r) => formatRow(r, styler.description));
|
|
97
|
+
const gap = ' ';
|
|
98
|
+
return [headerCells.join(gap), ...dataCells.map((r) => r.join(gap))].join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderTableMarkdown(
|
|
102
|
+
headers: string[],
|
|
103
|
+
rows: string[][],
|
|
104
|
+
colWidths: number[],
|
|
105
|
+
getAlign: (i: number) => 'left' | 'right' | 'center',
|
|
106
|
+
): string {
|
|
107
|
+
const headerLine = `| ${headers.map((h, i) => padCell(h, colWidths[i]!, 'left')).join(' | ')} |`;
|
|
108
|
+
const separatorLine =
|
|
109
|
+
'| ' +
|
|
110
|
+
colWidths
|
|
111
|
+
.map((w, i) => {
|
|
112
|
+
const a = getAlign(i);
|
|
113
|
+
const dashes = '─'.repeat(Math.max(w, 3));
|
|
114
|
+
if (a === 'center') return `:${dashes}:`;
|
|
115
|
+
if (a === 'right') return `${dashes}:`;
|
|
116
|
+
return dashes;
|
|
117
|
+
})
|
|
118
|
+
.join(' | ') +
|
|
119
|
+
' |';
|
|
120
|
+
const dataLines = rows.map((r) => `| ${r.map((c, i) => padCell(c, colWidths[i]!, 'left')).join(' | ')} |`);
|
|
121
|
+
return [headerLine, separatorLine, ...dataLines].join('\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderTableHtml(
|
|
125
|
+
columns: string[],
|
|
126
|
+
headers: string[],
|
|
127
|
+
_rows: string[][],
|
|
128
|
+
data: Record<string, unknown>[],
|
|
129
|
+
getAlign: (i: number) => 'left' | 'right' | 'center',
|
|
130
|
+
): string {
|
|
131
|
+
const ths = headers.map((h, i) => {
|
|
132
|
+
const a = getAlign(i);
|
|
133
|
+
const style = a !== 'left' ? ` style="text-align: ${a};"` : '';
|
|
134
|
+
return `<th${style}>${escapeHtml(h)}</th>`;
|
|
135
|
+
});
|
|
136
|
+
const trs = data.map(
|
|
137
|
+
(row) =>
|
|
138
|
+
'<tr>' +
|
|
139
|
+
columns
|
|
140
|
+
.map((col, i) => {
|
|
141
|
+
const a = getAlign(i);
|
|
142
|
+
const style = a !== 'left' ? ` style="text-align: ${a};"` : '';
|
|
143
|
+
return `<td${style}>${escapeHtml(stringifyCell(row[col]))}</td>`;
|
|
144
|
+
})
|
|
145
|
+
.join('') +
|
|
146
|
+
'</tr>',
|
|
147
|
+
);
|
|
148
|
+
return `<table><thead><tr>${ths.join('')}</tr></thead><tbody>${trs.join('')}</tbody></table>`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Tree ────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export type TreeNode = {
|
|
154
|
+
label: string;
|
|
155
|
+
children?: TreeNode[];
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type TreeOptions = {
|
|
159
|
+
/** Characters per indent level (default: 2). */
|
|
160
|
+
indent?: number;
|
|
161
|
+
/** Show tree guide lines (default: true for ansi/text). */
|
|
162
|
+
guides?: boolean;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export function renderTree(data: TreeNode | TreeNode[], options: TreeOptions | undefined, ctx: OutputContext): string {
|
|
166
|
+
const nodes = Array.isArray(data) ? data : [data];
|
|
167
|
+
if (nodes.length === 0) return '';
|
|
168
|
+
if (ctx.format === 'json') return JSON.stringify(nodes, null, 2);
|
|
169
|
+
if (ctx.format === 'markdown') return renderTreeMarkdown(nodes, 0);
|
|
170
|
+
if (ctx.format === 'html') return renderTreeHtml(nodes);
|
|
171
|
+
|
|
172
|
+
const guides = options?.guides !== false;
|
|
173
|
+
return renderTreeText(nodes, '', guides, ctx).join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderTreeText(nodes: TreeNode[], prefix: string, guides: boolean, ctx: OutputContext): string[] {
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
179
|
+
const node = nodes[i]!;
|
|
180
|
+
const isLast = i === nodes.length - 1;
|
|
181
|
+
if (guides) {
|
|
182
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
183
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
184
|
+
lines.push(prefix + ctx.styler.meta(connector) + ctx.styler.label(node.label));
|
|
185
|
+
if (node.children?.length) lines.push(...renderTreeText(node.children, prefix + ctx.styler.meta(childPrefix), guides, ctx));
|
|
186
|
+
} else {
|
|
187
|
+
const indent = prefix ? `${prefix} ` : '';
|
|
188
|
+
lines.push(indent + ctx.styler.label(node.label));
|
|
189
|
+
if (node.children?.length) lines.push(...renderTreeText(node.children, indent, guides, ctx));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderTreeMarkdown(nodes: TreeNode[], depth: number): string {
|
|
196
|
+
return nodes
|
|
197
|
+
.map((node) => {
|
|
198
|
+
const indent = ' '.repeat(depth);
|
|
199
|
+
const line = `${indent}- ${node.label}`;
|
|
200
|
+
if (!node.children?.length) return line;
|
|
201
|
+
return `${line}\n${renderTreeMarkdown(node.children, depth + 1)}`;
|
|
202
|
+
})
|
|
203
|
+
.join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function renderTreeHtml(nodes: TreeNode[]): string {
|
|
207
|
+
const items = nodes
|
|
208
|
+
.map((node) => {
|
|
209
|
+
const label = escapeHtml(node.label);
|
|
210
|
+
if (!node.children?.length) return `<li>${label}</li>`;
|
|
211
|
+
return `<li>${label}${renderTreeHtml(node.children)}</li>`;
|
|
212
|
+
})
|
|
213
|
+
.join('');
|
|
214
|
+
return `<ul>${items}</ul>`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── List ────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
export type ListItem = string | { label: string; description?: string };
|
|
220
|
+
|
|
221
|
+
export type ListOptions = {
|
|
222
|
+
/** Bullet character (default: '•' for ansi, '-' for text). */
|
|
223
|
+
bullet?: string;
|
|
224
|
+
/** Use numbered list instead of bullets. */
|
|
225
|
+
numbered?: boolean;
|
|
226
|
+
/** Indent level (default: 0). */
|
|
227
|
+
indent?: number;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export function renderList(data: ListItem[], options: ListOptions | undefined, ctx: OutputContext): string {
|
|
231
|
+
if (data.length === 0) return '';
|
|
232
|
+
if (ctx.format === 'json') {
|
|
233
|
+
const normalized = data.map((item) => (typeof item === 'string' ? { label: item } : item));
|
|
234
|
+
return JSON.stringify(normalized, null, 2);
|
|
235
|
+
}
|
|
236
|
+
if (ctx.format === 'markdown') return renderListMarkdown(data, options);
|
|
237
|
+
if (ctx.format === 'html') return renderListHtml(data, options);
|
|
238
|
+
return renderListText(data, options, ctx);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderListText(data: ListItem[], options: ListOptions | undefined, ctx: OutputContext): string {
|
|
242
|
+
const { styler } = ctx;
|
|
243
|
+
const numbered = options?.numbered ?? false;
|
|
244
|
+
const bullet = options?.bullet ?? (ctx.format === 'ansi' ? '•' : '-');
|
|
245
|
+
const baseIndent = ' '.repeat(options?.indent ?? 0);
|
|
246
|
+
|
|
247
|
+
return data
|
|
248
|
+
.map((item, i) => {
|
|
249
|
+
const prefix = numbered ? `${i + 1}.` : bullet;
|
|
250
|
+
const label = typeof item === 'string' ? item : item.label;
|
|
251
|
+
const desc = typeof item === 'object' && item.description ? item.description : undefined;
|
|
252
|
+
const line = `${baseIndent}${styler.meta(prefix)} ${styler.label(label)}`;
|
|
253
|
+
if (!desc) return line;
|
|
254
|
+
return `${line} ${styler.description(desc)}`;
|
|
255
|
+
})
|
|
256
|
+
.join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function renderListMarkdown(data: ListItem[], options: ListOptions | undefined): string {
|
|
260
|
+
const numbered = options?.numbered ?? false;
|
|
261
|
+
return data
|
|
262
|
+
.map((item, i) => {
|
|
263
|
+
const prefix = numbered ? `${i + 1}.` : '-';
|
|
264
|
+
const label = typeof item === 'string' ? item : item.label;
|
|
265
|
+
const desc = typeof item === 'object' && item.description ? item.description : undefined;
|
|
266
|
+
if (!desc) return `${prefix} ${label}`;
|
|
267
|
+
return `${prefix} **${label}** — ${desc}`;
|
|
268
|
+
})
|
|
269
|
+
.join('\n');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function renderListHtml(data: ListItem[], options: ListOptions | undefined): string {
|
|
273
|
+
const tag = options?.numbered ? 'ol' : 'ul';
|
|
274
|
+
const items = data
|
|
275
|
+
.map((item) => {
|
|
276
|
+
const label = typeof item === 'string' ? item : item.label;
|
|
277
|
+
const desc = typeof item === 'object' && item.description ? item.description : undefined;
|
|
278
|
+
if (!desc) return `<li>${escapeHtml(label)}</li>`;
|
|
279
|
+
return `<li><strong>${escapeHtml(label)}</strong> — ${escapeHtml(desc)}</li>`;
|
|
280
|
+
})
|
|
281
|
+
.join('');
|
|
282
|
+
return `<${tag}>${items}</${tag}>`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Key-Value ───────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
export type KeyValueOptions = {
|
|
288
|
+
/** Separator between key and value (default: ': '). */
|
|
289
|
+
separator?: string;
|
|
290
|
+
/** Align values by padding keys to the same width. */
|
|
291
|
+
align?: boolean;
|
|
292
|
+
/** Key → display label mapping. */
|
|
293
|
+
labels?: Record<string, string>;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export function renderKeyValue(data: Record<string, unknown>, options: KeyValueOptions | undefined, ctx: OutputContext): string {
|
|
297
|
+
const entries = Object.entries(data);
|
|
298
|
+
if (entries.length === 0) return '';
|
|
299
|
+
if (ctx.format === 'json') return JSON.stringify(data, null, 2);
|
|
300
|
+
if (ctx.format === 'markdown') return renderKeyValueMarkdown(entries, options);
|
|
301
|
+
if (ctx.format === 'html') return renderKeyValueHtml(entries, options);
|
|
302
|
+
return renderKeyValueText(entries, options, ctx);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function getLabel(key: string, labels?: Record<string, string>): string {
|
|
306
|
+
return labels?.[key] ?? key;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderKeyValueText(entries: [string, unknown][], options: KeyValueOptions | undefined, ctx: OutputContext): string {
|
|
310
|
+
const { styler } = ctx;
|
|
311
|
+
const sep = options?.separator ?? ': ';
|
|
312
|
+
const shouldAlign = options?.align !== false;
|
|
313
|
+
|
|
314
|
+
const displayLabels = entries.map(([k]) => getLabel(k, options?.labels));
|
|
315
|
+
const maxWidth = shouldAlign ? Math.max(...displayLabels.map((l) => l.length)) : 0;
|
|
316
|
+
|
|
317
|
+
return entries
|
|
318
|
+
.map(([_key, value], i) => {
|
|
319
|
+
const label = displayLabels[i]!;
|
|
320
|
+
const paddedLabel = shouldAlign ? label + ' '.repeat(maxWidth - label.length) : label;
|
|
321
|
+
return `${styler.label(paddedLabel)}${styler.meta(sep)}${styler.description(stringifyCell(value))}`;
|
|
322
|
+
})
|
|
323
|
+
.join('\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function renderKeyValueMarkdown(entries: [string, unknown][], options: KeyValueOptions | undefined): string {
|
|
327
|
+
return entries.map(([key, value]) => `- **${getLabel(key, options?.labels)}**: ${stringifyCell(value)}`).join('\n');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function renderKeyValueHtml(entries: [string, unknown][], options: KeyValueOptions | undefined): string {
|
|
331
|
+
const items = entries
|
|
332
|
+
.map(([key, value]) => `<dt>${escapeHtml(getLabel(key, options?.labels))}</dt><dd>${escapeHtml(stringifyCell(value))}</dd>`)
|
|
333
|
+
.join('');
|
|
334
|
+
return `<dl>${items}</dl>`;
|
|
335
|
+
}
|