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.
Files changed (141) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +15 -11
  3. package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.mjs +4 -4
  6. package/dist/codegen/index.mjs.map +1 -1
  7. package/dist/commands-B_gufyR9.mjs +514 -0
  8. package/dist/commands-B_gufyR9.mjs.map +1 -0
  9. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
  10. package/dist/completion-BEuflbDO.mjs.map +1 -0
  11. package/dist/docs/index.d.mts +4 -4
  12. package/dist/docs/index.d.mts.map +1 -1
  13. package/dist/docs/index.mjs +10 -12
  14. package/dist/docs/index.mjs.map +1 -1
  15. package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
  16. package/dist/errors-DA4KzK1M.mjs.map +1 -0
  17. package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
  18. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  19. package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
  20. package/dist/help-BtxLgrF_.mjs.map +1 -0
  21. package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
  22. package/dist/index-D6-7dz0l.d.mts.map +1 -0
  23. package/dist/index.d.mts +869 -36
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +3884 -1699
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
  28. package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
  29. package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
  30. package/dist/serve-YVTPzBCl.mjs.map +1 -0
  31. package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
  32. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  33. package/dist/test.d.mts +5 -8
  34. package/dist/test.d.mts.map +1 -1
  35. package/dist/test.mjs +2 -13
  36. package/dist/test.mjs.map +1 -1
  37. package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  38. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  39. package/dist/zod.d.mts +2 -2
  40. package/dist/zod.d.mts.map +1 -1
  41. package/dist/zod.mjs +2 -2
  42. package/dist/zod.mjs.map +1 -1
  43. package/package.json +15 -12
  44. package/src/cli/completions.ts +14 -11
  45. package/src/cli/docs.ts +13 -10
  46. package/src/cli/doctor.ts +22 -18
  47. package/src/cli/index.ts +28 -82
  48. package/src/cli/init.ts +10 -7
  49. package/src/cli/link.ts +20 -16
  50. package/src/cli/wrap.ts +14 -11
  51. package/src/codegen/schema-to-code.ts +2 -2
  52. package/src/{args.ts → core/args.ts} +32 -225
  53. package/src/core/commands.ts +373 -0
  54. package/src/core/create.ts +301 -0
  55. package/src/core/default-runtime.ts +239 -0
  56. package/src/{errors.ts → core/errors.ts} +22 -0
  57. package/src/core/exec.ts +259 -0
  58. package/src/core/interceptors.ts +302 -0
  59. package/src/{parse.ts → core/parse.ts} +36 -89
  60. package/src/core/program-methods.ts +301 -0
  61. package/src/core/results.ts +229 -0
  62. package/src/core/runtime.ts +246 -0
  63. package/src/core/validate.ts +247 -0
  64. package/src/docs/index.ts +12 -13
  65. package/src/extension/auto-output.ts +146 -0
  66. package/src/extension/color.ts +38 -0
  67. package/src/extension/completion.ts +49 -0
  68. package/src/extension/config.ts +262 -0
  69. package/src/extension/env.ts +101 -0
  70. package/src/extension/help.ts +192 -0
  71. package/src/extension/index.ts +44 -0
  72. package/src/extension/ink.ts +93 -0
  73. package/src/extension/interactive.ts +106 -0
  74. package/src/extension/logger.ts +262 -0
  75. package/src/extension/man.ts +51 -0
  76. package/src/extension/mcp.ts +52 -0
  77. package/src/extension/progress-renderer.ts +338 -0
  78. package/src/extension/progress.ts +299 -0
  79. package/src/extension/repl.ts +94 -0
  80. package/src/extension/serve.ts +48 -0
  81. package/src/extension/signal.ts +87 -0
  82. package/src/extension/stdin.ts +62 -0
  83. package/src/extension/suggestions.ts +114 -0
  84. package/src/extension/timing.ts +81 -0
  85. package/src/extension/tracing.ts +175 -0
  86. package/src/extension/update-check.ts +77 -0
  87. package/src/extension/utils.ts +51 -0
  88. package/src/extension/version.ts +63 -0
  89. package/src/{completion.ts → feature/completion.ts} +12 -12
  90. package/src/{interactive.ts → feature/interactive.ts} +4 -4
  91. package/src/{mcp.ts → feature/mcp.ts} +12 -15
  92. package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
  93. package/src/{serve.ts → feature/serve.ts} +11 -15
  94. package/src/feature/test.ts +262 -0
  95. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  96. package/src/{wrap.ts → feature/wrap.ts} +10 -8
  97. package/src/index.ts +115 -30
  98. package/src/{formatter.ts → output/formatter.ts} +124 -176
  99. package/src/{help.ts → output/help.ts} +22 -8
  100. package/src/output/output-indicator.ts +87 -0
  101. package/src/output/primitives.ts +335 -0
  102. package/src/output/styling.ts +221 -0
  103. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  104. package/src/schema/zod.ts +50 -0
  105. package/src/test.ts +2 -276
  106. package/src/types/args-meta.ts +151 -0
  107. package/src/types/builder.ts +718 -0
  108. package/src/types/command.ts +157 -0
  109. package/src/types/index.ts +60 -0
  110. package/src/types/interceptor.ts +296 -0
  111. package/src/types/preferences.ts +83 -0
  112. package/src/types/result.ts +71 -0
  113. package/src/types/schema.ts +19 -0
  114. package/src/util/dotenv.ts +244 -0
  115. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  116. package/src/{stream.ts → util/stream.ts} +27 -1
  117. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  118. package/src/{type-utils.ts → util/type-utils.ts} +71 -33
  119. package/src/util/utils.ts +51 -0
  120. package/src/zod.ts +1 -50
  121. package/dist/args-D5PNDyNu.mjs.map +0 -1
  122. package/dist/chunk-CjcI7cDX.mjs +0 -15
  123. package/dist/command-utils-B1D-HqCd.mjs +0 -1117
  124. package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
  125. package/dist/completion.d.mts +0 -64
  126. package/dist/completion.d.mts.map +0 -1
  127. package/dist/completion.mjs.map +0 -1
  128. package/dist/errors-BiVrBgi6.mjs.map +0 -1
  129. package/dist/formatter-DtHzbP22.d.mts.map +0 -1
  130. package/dist/help-bbmu9-qd.mjs.map +0 -1
  131. package/dist/mcp-mLWIdUIu.mjs.map +0 -1
  132. package/dist/serve-B0u43DK7.mjs.map +0 -1
  133. package/dist/stream-BcC146Ud.mjs.map +0 -1
  134. package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
  135. package/dist/update-check-CFX1FV3v.mjs.map +0 -1
  136. package/src/command-utils.ts +0 -882
  137. package/src/create.ts +0 -1829
  138. package/src/runtime.ts +0 -497
  139. package/src/types.ts +0 -1291
  140. package/src/utils.ts +0 -140
  141. /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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
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) => '&nbsp;&nbsp;'.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
+ }
@@ -1,4 +1,4 @@
1
- import type { PadroneFieldMeta } from './args.ts';
1
+ import type { PadroneFieldMeta } from '../core/args.ts';
2
2
 
3
3
  declare module 'zod/v4/core' {
4
4
  export interface GlobalMeta extends PadroneFieldMeta {}
@@ -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
+ });