padrone 1.5.0 → 1.7.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 +44 -0
- package/README.md +15 -11
- package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.mjs +4 -4
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +4 -4
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +10 -12
- package/dist/docs/index.mjs.map +1 -1
- package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
- package/dist/errors-DA4KzK1M.mjs.map +1 -0
- package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
- package/dist/help-BtxLgrF_.mjs.map +1 -0
- package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
- package/dist/index-D6-7dz0l.d.mts.map +1 -0
- package/dist/index.d.mts +869 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3884 -1699
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
- package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
- package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
- package/dist/serve-YVTPzBCl.mjs.map +1 -0
- package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
- package/dist/stream-DC4H8YTx.mjs.map +1 -0
- package/dist/test.d.mts +5 -8
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +2 -13
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +2 -2
- package/dist/zod.d.mts.map +1 -1
- package/dist/zod.mjs +2 -2
- package/dist/zod.mjs.map +1 -1
- package/package.json +15 -12
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -10
- package/src/cli/doctor.ts +22 -18
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +10 -7
- package/src/cli/link.ts +20 -16
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/schema-to-code.ts +2 -2
- package/src/{args.ts → core/args.ts} +32 -225
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +301 -0
- package/src/core/default-runtime.ts +239 -0
- package/src/{errors.ts → core/errors.ts} +22 -0
- package/src/core/exec.ts +259 -0
- package/src/core/interceptors.ts +302 -0
- package/src/{parse.ts → core/parse.ts} +36 -89
- package/src/core/program-methods.ts +301 -0
- package/src/core/results.ts +229 -0
- package/src/core/runtime.ts +246 -0
- package/src/core/validate.ts +247 -0
- package/src/docs/index.ts +12 -13
- package/src/extension/auto-output.ts +146 -0
- package/src/extension/color.ts +38 -0
- package/src/extension/completion.ts +49 -0
- package/src/extension/config.ts +262 -0
- package/src/extension/env.ts +101 -0
- package/src/extension/help.ts +192 -0
- package/src/extension/index.ts +44 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +262 -0
- package/src/extension/man.ts +51 -0
- package/src/extension/mcp.ts +52 -0
- package/src/extension/progress-renderer.ts +338 -0
- package/src/extension/progress.ts +299 -0
- package/src/extension/repl.ts +94 -0
- package/src/extension/serve.ts +48 -0
- package/src/extension/signal.ts +87 -0
- package/src/extension/stdin.ts +62 -0
- package/src/extension/suggestions.ts +114 -0
- package/src/extension/timing.ts +81 -0
- package/src/extension/tracing.ts +175 -0
- package/src/extension/update-check.ts +77 -0
- package/src/extension/utils.ts +51 -0
- package/src/extension/version.ts +63 -0
- package/src/{completion.ts → feature/completion.ts} +12 -12
- package/src/{interactive.ts → feature/interactive.ts} +4 -4
- package/src/{mcp.ts → feature/mcp.ts} +12 -15
- package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
- package/src/{serve.ts → feature/serve.ts} +11 -15
- package/src/feature/test.ts +262 -0
- package/src/{update-check.ts → feature/update-check.ts} +16 -16
- package/src/{wrap.ts → feature/wrap.ts} +10 -8
- package/src/index.ts +115 -30
- package/src/{formatter.ts → output/formatter.ts} +124 -176
- package/src/{help.ts → output/help.ts} +22 -8
- package/src/output/output-indicator.ts +87 -0
- package/src/output/primitives.ts +335 -0
- package/src/output/styling.ts +221 -0
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -276
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +718 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +60 -0
- package/src/types/interceptor.ts +296 -0
- package/src/types/preferences.ts +83 -0
- package/src/types/result.ts +71 -0
- package/src/types/schema.ts +19 -0
- package/src/util/dotenv.ts +244 -0
- package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
- package/src/{stream.ts → util/stream.ts} +27 -1
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +71 -33
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -50
- package/dist/args-D5PNDyNu.mjs.map +0 -1
- package/dist/chunk-CjcI7cDX.mjs +0 -15
- package/dist/command-utils-B1D-HqCd.mjs +0 -1117
- package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/errors-BiVrBgi6.mjs.map +0 -1
- package/dist/formatter-DtHzbP22.d.mts.map +0 -1
- package/dist/help-bbmu9-qd.mjs.map +0 -1
- package/dist/mcp-mLWIdUIu.mjs.map +0 -1
- package/dist/serve-B0u43DK7.mjs.map +0 -1
- package/dist/stream-BcC146Ud.mjs.map +0 -1
- package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
- package/dist/update-check-CFX1FV3v.mjs.map +0 -1
- package/src/command-utils.ts +0 -882
- package/src/create.ts +0 -1829
- package/src/runtime.ts +0 -497
- package/src/types.ts +0 -1291
- package/src/utils.ts +0 -140
- /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { PadroneActionContext } from '../types/index.ts';
|
|
2
|
+
import { type ColorConfig, type ColorTheme, createColorizer } from './colorizer.ts';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TERMINAL_WIDTH = 80;
|
|
5
|
+
|
|
6
|
+
export function wrapText(text: string, maxWidth: number): string[] {
|
|
7
|
+
if (maxWidth <= 0 || text.length <= maxWidth) return [text];
|
|
8
|
+
const words = text.split(' ');
|
|
9
|
+
const lines: string[] = [];
|
|
10
|
+
let current = '';
|
|
11
|
+
for (const word of words) {
|
|
12
|
+
if (current && current.length + 1 + word.length > maxWidth) {
|
|
13
|
+
lines.push(current);
|
|
14
|
+
current = word;
|
|
15
|
+
} else {
|
|
16
|
+
current = current ? `${current} ${word}` : word;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (current) lines.push(current);
|
|
20
|
+
return lines.length > 0 ? lines : [text];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function escapeHtml(text: string): string {
|
|
24
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Styler ──────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Styling functions for semantic text roles.
|
|
31
|
+
* Used by formatters to apply visual styles (ANSI, HTML, Markdown, etc.)
|
|
32
|
+
* to different types of content.
|
|
33
|
+
*/
|
|
34
|
+
export type Styler = {
|
|
35
|
+
command: (text: string) => string;
|
|
36
|
+
arg: (text: string) => string;
|
|
37
|
+
type: (text: string) => string;
|
|
38
|
+
description: (text: string) => string;
|
|
39
|
+
label: (text: string) => string;
|
|
40
|
+
section: (text: string) => string;
|
|
41
|
+
meta: (text: string) => string;
|
|
42
|
+
example: (text: string) => string;
|
|
43
|
+
exampleValue: (text: string) => string;
|
|
44
|
+
deprecated: (text: string) => string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Layout configuration for formatters.
|
|
49
|
+
*/
|
|
50
|
+
export type LayoutConfig = {
|
|
51
|
+
newline: string;
|
|
52
|
+
indent: (level: number) => string;
|
|
53
|
+
join: (parts: string[]) => string;
|
|
54
|
+
wrapDocument?: (content: string) => string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ── Styler Factories ────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function createTextStyler(): Styler {
|
|
60
|
+
return {
|
|
61
|
+
command: (text) => text,
|
|
62
|
+
arg: (text) => text,
|
|
63
|
+
type: (text) => text,
|
|
64
|
+
description: (text) => text,
|
|
65
|
+
label: (text) => text,
|
|
66
|
+
section: (text) => text,
|
|
67
|
+
meta: (text) => text,
|
|
68
|
+
example: (text) => text,
|
|
69
|
+
exampleValue: (text) => text,
|
|
70
|
+
deprecated: (text) => text,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createAnsiStyler(theme?: ColorTheme | ColorConfig): Styler {
|
|
75
|
+
const colorizer = createColorizer(theme);
|
|
76
|
+
return {
|
|
77
|
+
command: colorizer.command,
|
|
78
|
+
arg: colorizer.arg,
|
|
79
|
+
type: colorizer.type,
|
|
80
|
+
description: colorizer.description,
|
|
81
|
+
label: colorizer.label,
|
|
82
|
+
section: colorizer.label,
|
|
83
|
+
meta: colorizer.meta,
|
|
84
|
+
example: colorizer.example,
|
|
85
|
+
exampleValue: colorizer.exampleValue,
|
|
86
|
+
deprecated: colorizer.deprecated,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createConsoleStyler(theme?: ColorTheme | ColorConfig): Styler {
|
|
91
|
+
return createAnsiStyler(theme);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createMarkdownStyler(): Styler {
|
|
95
|
+
return {
|
|
96
|
+
command: (text) => `**${text}**`,
|
|
97
|
+
arg: (text) => `\`${text}\``,
|
|
98
|
+
type: (text) => `\`${text}\``,
|
|
99
|
+
description: (text) => text,
|
|
100
|
+
label: (text) => `**${text}**`,
|
|
101
|
+
section: (text) => `### ${text}`,
|
|
102
|
+
meta: (text) => `*${text}*`,
|
|
103
|
+
example: (text) => `**${text}**`,
|
|
104
|
+
exampleValue: (text) => `\`${text}\``,
|
|
105
|
+
deprecated: (text) => `~~${text}~~`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createHtmlStyler(): Styler {
|
|
110
|
+
return {
|
|
111
|
+
command: (text) => `<strong style="color: #00bcd4;">${escapeHtml(text)}</strong>`,
|
|
112
|
+
arg: (text) => `<code style="color: #4caf50;">${escapeHtml(text)}</code>`,
|
|
113
|
+
type: (text) => `<code style="color: #ff9800;">${escapeHtml(text)}</code>`,
|
|
114
|
+
description: (text) => `<span style="color: #666;">${escapeHtml(text)}</span>`,
|
|
115
|
+
label: (text) => `<strong>${escapeHtml(text)}</strong>`,
|
|
116
|
+
section: (text) => `<h3>${escapeHtml(text)}</h3>`,
|
|
117
|
+
meta: (text) => `<span style="color: #999;">${escapeHtml(text)}</span>`,
|
|
118
|
+
example: (text) => `<strong style="text-decoration: underline;">${escapeHtml(text)}</strong>`,
|
|
119
|
+
exampleValue: (text) => `<em>${escapeHtml(text)}</em>`,
|
|
120
|
+
deprecated: (text) => `<del style="color: #999;">${escapeHtml(text)}</del>`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Layout Factories ────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export function createTextLayout(): LayoutConfig {
|
|
127
|
+
return {
|
|
128
|
+
newline: '\n',
|
|
129
|
+
indent: (level) => ' '.repeat(level),
|
|
130
|
+
join: (parts) => parts.filter(Boolean).join(' '),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createMarkdownLayout(): LayoutConfig {
|
|
135
|
+
return {
|
|
136
|
+
newline: '\n\n',
|
|
137
|
+
indent: (level) => {
|
|
138
|
+
if (level === 0) return '';
|
|
139
|
+
if (level === 1) return ' ';
|
|
140
|
+
return ' ';
|
|
141
|
+
},
|
|
142
|
+
join: (parts) => parts.filter(Boolean).join(' '),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createHtmlLayout(): LayoutConfig {
|
|
147
|
+
return {
|
|
148
|
+
newline: '<br>',
|
|
149
|
+
indent: (level) => ' '.repeat(level),
|
|
150
|
+
join: (parts) => parts.filter(Boolean).join(' '),
|
|
151
|
+
wrapDocument: (content) => `<div style="font-family: monospace; line-height: 1.6;">${content}</div>`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Format Detection ────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export function shouldUseAnsi(env?: Record<string, string | undefined>, isTTY?: boolean): boolean {
|
|
158
|
+
if (env?.NO_COLOR) return false;
|
|
159
|
+
if (env?.CI) return false;
|
|
160
|
+
if (typeof isTTY === 'boolean') return isTTY;
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Output Context ──────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/** Resolved formatting context used by output primitives. */
|
|
167
|
+
export type OutputFormat = 'text' | 'ansi' | 'json' | 'markdown' | 'html';
|
|
168
|
+
|
|
169
|
+
export type OutputContext = {
|
|
170
|
+
format: OutputFormat;
|
|
171
|
+
styler: Styler;
|
|
172
|
+
layout: LayoutConfig;
|
|
173
|
+
terminalWidth?: number;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** Resolve the output format from the runtime and caller context. */
|
|
177
|
+
export function resolveOutputFormat(
|
|
178
|
+
runtime: {
|
|
179
|
+
format?: string;
|
|
180
|
+
theme?: ColorTheme | ColorConfig;
|
|
181
|
+
terminal?: { columns?: number; isTTY?: boolean };
|
|
182
|
+
env: () => Record<string, string | undefined>;
|
|
183
|
+
},
|
|
184
|
+
caller?: PadroneActionContext['caller'],
|
|
185
|
+
): OutputContext {
|
|
186
|
+
let format: OutputFormat;
|
|
187
|
+
|
|
188
|
+
if (caller === 'serve' || caller === 'mcp' || caller === 'tool') {
|
|
189
|
+
format = 'json';
|
|
190
|
+
} else if (runtime.format && runtime.format !== 'auto' && runtime.format !== 'console') {
|
|
191
|
+
format = runtime.format as OutputFormat;
|
|
192
|
+
} else {
|
|
193
|
+
format = shouldUseAnsi(runtime.env(), runtime.terminal?.isTTY) ? 'ansi' : 'text';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const terminalWidth = format === 'markdown' || format === 'html' ? undefined : (runtime.terminal?.columns ?? DEFAULT_TERMINAL_WIDTH);
|
|
197
|
+
|
|
198
|
+
let styler: Styler;
|
|
199
|
+
let layout: LayoutConfig;
|
|
200
|
+
|
|
201
|
+
switch (format) {
|
|
202
|
+
case 'ansi':
|
|
203
|
+
styler = createAnsiStyler(runtime.theme);
|
|
204
|
+
layout = createTextLayout();
|
|
205
|
+
break;
|
|
206
|
+
case 'markdown':
|
|
207
|
+
styler = createMarkdownStyler();
|
|
208
|
+
layout = createMarkdownLayout();
|
|
209
|
+
break;
|
|
210
|
+
case 'html':
|
|
211
|
+
styler = createHtmlStyler();
|
|
212
|
+
layout = createHtmlLayout();
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
styler = createTextStyler();
|
|
216
|
+
layout = createTextLayout();
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { format, styler, layout, terminalWidth };
|
|
221
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import type { PadroneSchema } from '../types/index.ts';
|
|
3
|
+
import { asyncStream } from '../util/stream.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a Zod schema for an async stream field, ready to use in `.arguments()`.
|
|
7
|
+
* Wraps `z.custom<AsyncIterable<T>>()` with the `asyncStream()` metadata automatically.
|
|
8
|
+
*
|
|
9
|
+
* @param itemSchema - Optional item schema for per-item validation.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { zodAsyncStream } from 'padrone/zod';
|
|
14
|
+
*
|
|
15
|
+
* // String lines
|
|
16
|
+
* z.object({ lines: zodAsyncStream() })
|
|
17
|
+
*
|
|
18
|
+
* // Typed items — each line JSON.parse'd and validated
|
|
19
|
+
* const recordSchema = z.object({ name: z.string(), age: z.number() });
|
|
20
|
+
* z.object({ records: zodAsyncStream(jsonCodec(recordSchema)) })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function zodAsyncStream<T = string>(itemSchema?: PadroneSchema<unknown, T>) {
|
|
24
|
+
return z.custom<AsyncIterable<T>>().meta(asyncStream(itemSchema));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* JSON codec for Zod schemas
|
|
29
|
+
* @see https://zod.dev/codecs?id=jsonschema
|
|
30
|
+
* Unlike the example in the docs, this codec also handles the case where the input is already an object
|
|
31
|
+
*/
|
|
32
|
+
export const jsonCodec = <T extends z.ZodType>(schema: T) =>
|
|
33
|
+
z.codec(z.union([z.string(), z.unknown()]), schema, {
|
|
34
|
+
decode: (jsonString, ctx) => {
|
|
35
|
+
try {
|
|
36
|
+
// HACK: in some cases the object is already deserialized, we just need to validate it
|
|
37
|
+
if (typeof jsonString !== 'string') return jsonString as z.input<T>;
|
|
38
|
+
return JSON.parse(jsonString) as z.input<T>;
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
ctx.issues.push({
|
|
41
|
+
code: 'invalid_format',
|
|
42
|
+
format: 'json',
|
|
43
|
+
input: typeof jsonString === 'string' ? jsonString : JSON.stringify(jsonString),
|
|
44
|
+
message: err.message,
|
|
45
|
+
});
|
|
46
|
+
return z.NEVER;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
encode: (value) => JSON.stringify(value),
|
|
50
|
+
});
|