dgxay-code-cli 0.0.0-stage → 1.1.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 (46) hide show
  1. package/LICENSE +674 -0
  2. package/README-zh.md +242 -0
  3. package/README.md +223 -2
  4. package/bin/dgxay.js +42 -0
  5. package/package.json +47 -4
  6. package/src/cli/ansi.js +539 -0
  7. package/src/cli/approval.js +186 -0
  8. package/src/cli/args.js +162 -0
  9. package/src/cli/command-table.js +52 -0
  10. package/src/cli/editor.js +828 -0
  11. package/src/cli/i18n.js +276 -0
  12. package/src/cli/index.js +228 -0
  13. package/src/cli/print.js +148 -0
  14. package/src/cli/render.js +395 -0
  15. package/src/cli/repl-state.js +85 -0
  16. package/src/cli/repl.js +1231 -0
  17. package/src/cli/slash.js +75 -0
  18. package/src/cli/ui.js +52 -0
  19. package/src/commands/_shared.js +20 -0
  20. package/src/commands/chat.js +212 -0
  21. package/src/commands/config.js +363 -0
  22. package/src/commands/context.js +43 -0
  23. package/src/commands/doctor.js +192 -0
  24. package/src/commands/export.js +102 -0
  25. package/src/commands/mcp.js +257 -0
  26. package/src/commands/model.js +223 -0
  27. package/src/commands/oauth.js +246 -0
  28. package/src/commands/permission.js +108 -0
  29. package/src/commands/provider.js +332 -0
  30. package/src/commands/run.js +172 -0
  31. package/src/commands/skill.js +94 -0
  32. package/src/commands/workspace.js +100 -0
  33. package/src/core/agent.js +420 -0
  34. package/src/core/llm.js +709 -0
  35. package/src/core/mcp.js +271 -0
  36. package/src/core/oauth.js +737 -0
  37. package/src/core/prompt.js +269 -0
  38. package/src/core/sessions.js +208 -0
  39. package/src/core/tools.js +391 -0
  40. package/src/services/config.js +182 -0
  41. package/src/services/input.js +91 -0
  42. package/src/services/session-bus.js +104 -0
  43. package/src/services/settings.js +340 -0
  44. package/src/services/skills.js +125 -0
  45. package/src/services/terminal.js +124 -0
  46. package/src/services/version.js +25 -0
@@ -0,0 +1,539 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 终端渲染:颜色、Markdown → ANSI、状态栏、上下文环。
5
+ *
6
+ * 规则:
7
+ * - 非 TTY 或设置了 NO_COLOR / TERM=dumb 时自动降级为纯文本;
8
+ * - DGXAY_COLOR=always 可强制开启颜色(测试与重定向时用);
9
+ * - 统一蓝色强调色 #2563EB,与桌面版视觉一致(256 色 27 号 / 真彩 #2563EB)。
10
+ */
11
+
12
+ const ANSI = {
13
+ reset: '\x1b[0m',
14
+ bold: '\x1b[1m',
15
+ dim: '\x1b[2m',
16
+ italic: '\x1b[3m',
17
+ underline: '\x1b[4m',
18
+ strike: '\x1b[9m'
19
+ };
20
+
21
+ let colorLevel = detectColor();
22
+
23
+ function detectColor() {
24
+ const forced = String(process.env.DGXAY_COLOR || '').toLowerCase();
25
+ if (forced === 'always' || forced === '1' || forced === 'true') return 'truecolor';
26
+ if (forced === 'never' || forced === '0' || forced === 'false') return 'none';
27
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') return 'none';
28
+ if (process.env.TERM === 'dumb') return 'none';
29
+ if (!process.stdout.isTTY) return 'none';
30
+ if (/truecolor|24bit/i.test(process.env.COLORTERM || '')) return 'truecolor';
31
+ return 'ansi256';
32
+ }
33
+
34
+ function setColorMode(mode) {
35
+ if (['none', 'ansi256', 'truecolor'].includes(mode)) colorLevel = mode;
36
+ return colorLevel;
37
+ }
38
+
39
+ function colorEnabled() {
40
+ return colorLevel !== 'none';
41
+ }
42
+
43
+ /** 蓝色强调色 */
44
+ function accent(text) {
45
+ if (!colorEnabled()) return String(text);
46
+ return colorLevel === 'truecolor' ? `\x1b[38;2;37;99;235m${text}${ANSI.reset}` : `\x1b[38;5;27m${text}${ANSI.reset}`;
47
+ }
48
+
49
+ function paint(code, text) {
50
+ if (!colorEnabled()) return String(text);
51
+ return `${code}${text}${ANSI.reset}`;
52
+ }
53
+
54
+ const rgb = (r, g, b, text) => {
55
+ if (!colorEnabled()) return String(text);
56
+ if (colorLevel === 'truecolor') return `\x1b[38;2;${r};${g};${b}m${text}${ANSI.reset}`;
57
+ return paint(`\x1b[38;5;${to256(r, g, b)}m`, text);
58
+ };
59
+
60
+ function to256(r, g, b) {
61
+ if (r === g && g === b) {
62
+ if (r < 8) return 16;
63
+ if (r > 248) return 231;
64
+ return Math.round(((r - 8) / 247) * 24) + 232;
65
+ }
66
+ return 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
67
+ }
68
+
69
+ const COLORS = {
70
+ dim: (t) => paint('\x1b[38;5;245m', t),
71
+ faint: (t) => paint('\x1b[38;5;242m', t),
72
+ text: (t) => paint('\x1b[38;5;252m', t),
73
+ strong: (t) => paint(`\x1b[1m\x1b[38;5;255m`, t),
74
+ green: (t) => paint('\x1b[38;5;70m', t),
75
+ red: (t) => paint('\x1b[38;5;167m', t),
76
+ yellow: (t) => paint('\x1b[38;5;179m', t),
77
+ cyan: (t) => paint('\x1b[38;5;73m', t),
78
+ magenta: (t) => paint('\x1b[38;5;140m', t),
79
+ accent,
80
+ bold: (t) => paint(ANSI.bold, t),
81
+ underline: (t) => paint(ANSI.underline, t)
82
+ };
83
+
84
+ /* ------------------------------ 终端能力 ------------------------------ */
85
+
86
+ function isInteractive() {
87
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
88
+ }
89
+
90
+ function termWidth(stream = process.stdout) {
91
+ const width = Number(stream.columns) || 0;
92
+ if (width > 0) return Math.max(20, width);
93
+ return 80;
94
+ }
95
+
96
+ /**
97
+ * 终端能否安全输出 Unicode 框线字符。
98
+ * Windows Terminal / VSCode / 现代 shell 都支持;只有老的 conhost 需要降级成 ASCII。
99
+ */
100
+ function useUnicode(stream = process.stdout) {
101
+ if (process.env.DGXAY_ASCII === '1') return false;
102
+ if (process.platform !== 'win32') return true;
103
+ if (process.env.WT_SESSION || process.env.TERM_PROGRAM || process.env.ConEmuANSI) return true;
104
+ if (/UTF-?8/i.test(process.env.LANG || process.env.LC_ALL || '')) return true;
105
+ // 能拿到列数说明是真实交互终端;Windows 10+ 的 PowerShell 用 Consolas 渲染框线没有问题
106
+ return Boolean(stream.columns);
107
+ }
108
+
109
+ function clearLine() {
110
+ if (process.stdout.isTTY) process.stdout.write('\r\x1b[2K');
111
+ }
112
+
113
+ /* --- 光标与屏幕控制(供输入框 / 区域重绘使用) --- */
114
+
115
+ /** 上移 n 行(n <= 0 时返回空串) */
116
+ function cursorUp(n = 1) {
117
+ return n > 0 ? `\x1b[${n}A` : '';
118
+ }
119
+
120
+ /** 下移 n 行 */
121
+ function cursorDown(n = 1) {
122
+ return n > 0 ? `\x1b[${n}B` : '';
123
+ }
124
+
125
+ /** 清整行(光标不动) */
126
+ function eraseLine() {
127
+ return '\x1b[2K';
128
+ }
129
+
130
+ /** 清屏并把光标移到左上角 */
131
+ function clearScreen(stream = process.stdout) {
132
+ stream.write('\x1b[2J\x1b[H');
133
+ }
134
+
135
+ function hideCursor() {
136
+ if (process.stdout.isTTY) process.stdout.write('\x1b[?25l');
137
+ }
138
+
139
+ function showCursor() {
140
+ if (process.stdout.isTTY) process.stdout.write('\x1b[?25h');
141
+ }
142
+
143
+ /* ------------------------------ 文本测量 ------------------------------ */
144
+
145
+ const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
146
+
147
+ function stripAnsi(text) {
148
+ return String(text).replace(ANSI_RE, '');
149
+ }
150
+
151
+ /** 显示宽度:CJK / 全角算 2,组合字符算 0 */
152
+ function charWidth(codePoint) {
153
+ if (codePoint === 0x200b || codePoint === 0xfeff) return 0;
154
+ if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0)) return 0;
155
+ if (
156
+ (codePoint >= 0x0300 && codePoint <= 0x036f) ||
157
+ (codePoint >= 0x200b && codePoint <= 0x200f) ||
158
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
159
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f)
160
+ ) {
161
+ return 0;
162
+ }
163
+ if (
164
+ (codePoint >= 0x1100 && codePoint <= 0x115f) ||
165
+ (codePoint >= 0x2e80 && codePoint <= 0x303e) ||
166
+ (codePoint >= 0x3041 && codePoint <= 0x33ff) ||
167
+ (codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
168
+ (codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
169
+ (codePoint >= 0xa000 && codePoint <= 0xa4cf) ||
170
+ (codePoint >= 0xa960 && codePoint <= 0xa97f) ||
171
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
172
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
173
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
174
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
175
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
176
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
177
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
178
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
179
+ (codePoint >= 0x1f680 && codePoint <= 0x1f6ff) ||
180
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)
181
+ ) {
182
+ return 2;
183
+ }
184
+ return 1;
185
+ }
186
+
187
+ function displayWidth(text) {
188
+ let width = 0;
189
+ for (const char of stripAnsi(text)) width += charWidth(char.codePointAt(0));
190
+ return width;
191
+ }
192
+
193
+ /** 按显示宽度截断,超出补省略号 */
194
+ function truncate(text, max) {
195
+ const raw = String(text ?? '');
196
+ if (max <= 0) return '';
197
+ if (displayWidth(raw) <= max) return raw;
198
+ const ellipsis = max >= 2 ? (useUnicode() ? '…' : '.') : '';
199
+ const budget = max - displayWidth(ellipsis);
200
+ let out = '';
201
+ let width = 0;
202
+ for (const char of raw) {
203
+ const w = charWidth(char.codePointAt(0));
204
+ if (width + w > budget) break;
205
+ out += char;
206
+ width += w;
207
+ }
208
+ return out + ellipsis;
209
+ }
210
+
211
+ /** 按显示宽度右对齐补空格 */
212
+ function padEnd(text, width) {
213
+ const diff = width - displayWidth(text);
214
+ return diff > 0 ? `${text}${' '.repeat(diff)}` : String(text);
215
+ }
216
+
217
+ function wrapText(text, width) {
218
+ const lines = [];
219
+ let current = '';
220
+ let currentWidth = 0;
221
+ for (const char of String(text ?? '')) {
222
+ if (char === '\n') {
223
+ lines.push(current);
224
+ current = '';
225
+ currentWidth = 0;
226
+ continue;
227
+ }
228
+ const w = charWidth(char.codePointAt(0));
229
+ if (current && currentWidth + w > width) {
230
+ lines.push(current);
231
+ current = '';
232
+ currentWidth = 0;
233
+ }
234
+ current += char;
235
+ currentWidth += w;
236
+ }
237
+ if (current) lines.push(current);
238
+ return lines;
239
+ }
240
+
241
+ /* ------------------------------ Markdown → ANSI ------------------------------ */
242
+
243
+ function escapeHtml(text) {
244
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
245
+ }
246
+
247
+ /**
248
+ * 中西文混排留白:中文旁边紧跟英文/数字时补一个空格,读起来更松弛。
249
+ * 只在渲染层做,不改原文;已有空格的地方不重复补。
250
+ */
251
+ const CJK = '\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff';
252
+ const LATIN = 'A-Za-z0-9@#$%&_+=/\\\\';
253
+ function spacing(text) {
254
+ let out = String(text ?? '');
255
+ out = out.replace(new RegExp(`([${CJK}])([${LATIN}])`, 'g'), '$1 $2');
256
+ out = out.replace(new RegExp(`([${LATIN}])([${CJK}])`, 'g'), '$1 $2');
257
+ return out;
258
+ }
259
+
260
+ function renderInline(source) {
261
+ const tokens = [];
262
+ const hold = (text) => {
263
+ tokens.push(text);
264
+ return `\u0000${tokens.length - 1}\u0000`;
265
+ };
266
+
267
+ // 中西文留白在“标记解析前”做,只处理纯文本部分(占位符不会被误伤)
268
+ let text = spacing(String(source ?? ''));
269
+
270
+ // 行内代码优先,内部不再解析其他标记
271
+ text = text.replace(/`+([^`\n]+?)`+/g, (m, code) => hold(COLORS.cyan(code)));
272
+ text = text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (m, alt, url) => hold(`🖼 ${alt || url}`));
273
+ text = text.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (m, label, url) =>
274
+ hold(`${label}${COLORS.faint(` (${url})`)}`)
275
+ );
276
+ text = text.replace(/<(https?:\/\/[^>\s]+)>/g, (m, url) => hold(url));
277
+ text = text.replace(/\*\*([^*\n]+)\*\*/g, (m, inner) => hold(COLORS.bold(inner)));
278
+ text = text.replace(/__([^_\n]+)__/g, (m, inner) => hold(COLORS.bold(inner)));
279
+ text = text.replace(/(^|[^\w*])\*([^*\n]+)\*(?!\w)/g, (m, pre, inner) => `${pre}${hold(COLORS.italic(inner))}`);
280
+ text = text.replace(/(^|[^\w_])_([^_\n]+)_(?!\w)/g, (m, pre, inner) => `${pre}${hold(COLORS.italic(inner))}`);
281
+ text = text.replace(/~~([^~\n]+)~~/g, (m, inner) => hold(paint(ANSI.strike, inner)));
282
+
283
+ // 占位符替换:重复执行直到没有占位符(替换结果本身可能含占位符形态)
284
+ let guard = 0;
285
+ while (guard < 6) {
286
+ let replaced = false;
287
+ text = text.replace(/\u0000(\d+)\u0000/g, (m, index) => {
288
+ const value = tokens[Number(index)];
289
+ if (value === undefined) return '';
290
+ replaced = true;
291
+ return value;
292
+ });
293
+ if (!replaced) break;
294
+ guard += 1;
295
+ }
296
+ return text.replace(/\u0000\d+\u0000/g, '');
297
+ }
298
+
299
+ /** 把 Markdown 渲染成适合终端的多行 ANSI 文本(保留块内空行,末尾只补一个换行) */
300
+ function renderMarkdown(source, options = {}) {
301
+ const body = renderMarkdownBody(source, options);
302
+ return body ? `${body}\n` : '';
303
+ }
304
+
305
+ /** 渲染但不强制补齐结尾换行;表格需要“往前看一行”的能力,因此返回已渲染的行数组 */
306
+ function renderMarkdownBody(source, options = {}) {
307
+ const width = Math.max(24, (options.width || termWidth()) - 2);
308
+ const lines = String(source ?? '').replace(/\r\n/g, '\n').split('\n');
309
+ const out = [];
310
+ let index = 0;
311
+ let inCode = false;
312
+ let codeLang = '';
313
+ let codeLines = [];
314
+ const unicode = useUnicode();
315
+
316
+ const flushCode = () => {
317
+ const gutter = unicode ? '│ ' : '| ';
318
+ if (codeLang) out.push(COLORS.faint(` ${codeLang}`));
319
+ for (const line of codeLines) out.push(`${COLORS.faint(gutter)}${COLORS.green(line)}`);
320
+ if (!codeLines.length) out.push(COLORS.faint(gutter));
321
+ codeLines = [];
322
+ codeLang = '';
323
+ };
324
+
325
+ while (index < lines.length) {
326
+ const line = lines[index];
327
+
328
+ if (/^\s*(```|~~~)/.test(line)) {
329
+ if (inCode) {
330
+ flushCode();
331
+ inCode = false;
332
+ } else {
333
+ inCode = true;
334
+ codeLang = line.replace(/^\s*(```|~~~)\s*/, '').trim();
335
+ }
336
+ index += 1;
337
+ continue;
338
+ }
339
+
340
+ if (inCode) {
341
+ codeLines.push(line);
342
+ index += 1;
343
+ continue;
344
+ }
345
+
346
+ if (!line.trim()) {
347
+ if (out.length && out[out.length - 1] !== '') out.push('');
348
+ index += 1;
349
+ continue;
350
+ }
351
+
352
+ // 标题
353
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
354
+ if (heading) {
355
+ const level = heading[1].length;
356
+ const text = renderInline(heading[2].replace(/\s+#+\s*$/, ''));
357
+ if (level === 1) out.push('', COLORS.bold(text), COLORS.accent(unicode ? '━'.repeat(Math.min(width, 40)) : '='.repeat(Math.min(width, 40))));
358
+ else if (level === 2) out.push(COLORS.bold(text), COLORS.faint(unicode ? '─'.repeat(Math.min(width, 32)) : '-'.repeat(Math.min(width, 32))));
359
+ else out.push(COLORS.bold(`${level >= 4 ? '›' : '▸'} ${text}`));
360
+ index += 1;
361
+ continue;
362
+ }
363
+
364
+ // 分隔线
365
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
366
+ out.push(COLORS.faint((unicode ? '─' : '-').repeat(Math.min(width, 40))));
367
+ index += 1;
368
+ continue;
369
+ }
370
+
371
+ // 引用(可嵌套)
372
+ const quote = line.match(/^(\s*(?:>\s*)+)(.*)$/);
373
+ if (quote) {
374
+ const depth = (quote[1].match(/>/g) || []).length;
375
+ const prefix = `${' '.repeat(Math.max(0, depth - 1))}${COLORS.accent(unicode ? '▎' : '|')} `;
376
+ for (const piece of wrapText(quote[2], width - depth * 2 - 2)) {
377
+ out.push(`${prefix}${COLORS.dim(renderInline(piece))}`);
378
+ }
379
+ index += 1;
380
+ continue;
381
+ }
382
+
383
+ // 表格(表头行 + 分隔行);必须是当前行是分隔行、上一行是表头
384
+ const isSeparatorRow = (text) =>
385
+ /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(text) && text.includes('-');
386
+ if (index > 0 && isSeparatorRow(line) && lines[index - 1].includes('|') && out.length) {
387
+ // 表头在上一轮已经作为段落输出,把它取回来重新排版
388
+ out.pop();
389
+ const splitRow = (row) =>
390
+ row
391
+ .replace(/^\s*\|/, '')
392
+ .replace(/\|\s*$/, '')
393
+ .split('|')
394
+ .map((cell) => cell.trim());
395
+ const header = splitRow(lines[index - 1]);
396
+ const align = splitRow(line).map((cell) => {
397
+ if (/^:-+:$/.test(cell)) return 'center';
398
+ if (/-+:$/.test(cell)) return 'right';
399
+ return 'left';
400
+ });
401
+ index += 1;
402
+ const rows = [];
403
+ while (index < lines.length && /\|/.test(lines[index]) && lines[index].trim()) {
404
+ rows.push(splitRow(lines[index]));
405
+ index += 1;
406
+ }
407
+ const columns = Math.max(header.length, ...rows.map((row) => row.length));
408
+ const widths = [];
409
+ for (let col = 0; col < columns; col += 1) {
410
+ const cells = [header[col] || '', ...rows.map((row) => row[col] || '')];
411
+ widths[col] = Math.min(40, Math.max(...cells.map((cell) => displayWidth(cell))));
412
+ }
413
+ const renderRow = (cells, bold) =>
414
+ cells
415
+ .map((cell, col) => {
416
+ const clipped = truncate(cell, widths[col]);
417
+ const gap = Math.max(0, widths[col] - displayWidth(clipped));
418
+ const mode = align[col] || 'left';
419
+ const body =
420
+ mode === 'right'
421
+ ? `${' '.repeat(gap)}${clipped}`
422
+ : mode === 'center'
423
+ ? `${' '.repeat(Math.floor(gap / 2))}${clipped}${' '.repeat(Math.ceil(gap / 2))}`
424
+ : `${clipped}${' '.repeat(gap)}`;
425
+ return bold ? COLORS.bold(body) : body;
426
+ })
427
+ .join(COLORS.faint(' │ '));
428
+ out.push(`${COLORS.faint('│ ')}${renderRow(header, true)}${COLORS.faint(' │')}`);
429
+ out.push(COLORS.faint(` ${widths.map((w) => '─'.repeat(w)).join('─┼─')}`));
430
+ for (const row of rows) {
431
+ const padded = [];
432
+ for (let col = 0; col < columns; col += 1) padded.push(row[col] || '');
433
+ out.push(`${COLORS.faint('│ ')}${renderRow(padded, false)}${COLORS.faint(' │')}`);
434
+ }
435
+ continue;
436
+ }
437
+
438
+ // 列表(支持缩进与复选框)
439
+ const list = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
440
+ if (list) {
441
+ const indent = Math.floor(list[1].replace(/\t/g, ' ').length / 2);
442
+ const marker = /^\d/.test(list[2]) ? list[2] : unicode ? '•' : '-';
443
+ let content = list[3];
444
+ let checkbox = '';
445
+ const task = content.match(/^\[([ xX])\]\s+(.*)$/);
446
+ if (task) {
447
+ checkbox = task[1].trim() ? COLORS.green('[✓] ') : COLORS.dim('[ ] ');
448
+ content = task[2];
449
+ }
450
+ const prefix = `${' '.repeat(indent)}${COLORS.accent(marker)} ${checkbox}`;
451
+ const hang = displayWidth(prefix);
452
+ const pieces = wrapText(content, Math.max(12, width - hang));
453
+ pieces.forEach((piece, i) => {
454
+ out.push(`${i === 0 ? prefix : ' '.repeat(hang)}${renderInline(piece)}`);
455
+ });
456
+ index += 1;
457
+ continue;
458
+ }
459
+
460
+ // 普通段落:软换行合并(遇到任何块级结构就停下,表格分隔行走专用分支)
461
+ let paragraph = line;
462
+ let lookahead = index + 1;
463
+ const stopsParagraph = (text) =>
464
+ /^\s*(```|~~~|#{1,6}\s|>)/.test(text) ||
465
+ /^\s*([-*+]|\d+[.)])\s/.test(text) ||
466
+ /^\s*([-*_])(\s*\1){2,}\s*$/.test(text) ||
467
+ isSeparatorRow(text);
468
+ while (lookahead < lines.length && lines[lookahead].trim() && !stopsParagraph(lines[lookahead])) {
469
+ paragraph += ` ${lines[lookahead].trim()}`;
470
+ lookahead += 1;
471
+ }
472
+ for (const piece of wrapText(paragraph, width)) if (piece) out.push(renderInline(piece));
473
+ index = lookahead;
474
+ }
475
+
476
+ if (inCode) flushCode();
477
+
478
+ while (out.length && out[0] === '') out.shift();
479
+ // 注意:结尾的空行由调用方决定(流式输出逐行渲染时需要保留块内空行)
480
+ return out.join('\n');
481
+ }
482
+
483
+ /* ------------------------------ 组件 ------------------------------ */
484
+
485
+ function ring(ratio) {
486
+ if (!colorEnabled()) {
487
+ return `${Math.round(Math.min(1, ratio) * 100)}%`;
488
+ }
489
+ const segments = 8;
490
+ const filled = Math.round(Math.min(1, Math.max(0, ratio)) * segments);
491
+ return COLORS.accent('●'.repeat(filled)) + COLORS.faint('○'.repeat(segments - filled));
492
+ }
493
+
494
+ function badge(text, kind = 'accent') {
495
+ const palette = {
496
+ accent,
497
+ green: COLORS.green,
498
+ red: COLORS.red,
499
+ yellow: COLORS.yellow,
500
+ dim: COLORS.dim
501
+ };
502
+ return (palette[kind] || accent)(`[${text}]`);
503
+ }
504
+
505
+ /** 状态符号(自动降级为 ASCII) */
506
+ const SIGN = useUnicode()
507
+ ? { ok: '✓', fail: '✗', run: '⠿', dot: '·', arrow: '›', bullet: '•', ellipse: '…', check: '✓' }
508
+ : { ok: 'ok', fail: 'x', run: '*', dot: '.', arrow: '>', bullet: '-', ellipse: '...', check: 'ok' };
509
+
510
+ module.exports = {
511
+ COLORS,
512
+ SIGN,
513
+ accent,
514
+ paint,
515
+ rgb,
516
+ colorEnabled,
517
+ setColorMode,
518
+ isInteractive,
519
+ termWidth,
520
+ useUnicode,
521
+ clearLine,
522
+ eraseLine,
523
+ clearScreen,
524
+ cursorUp,
525
+ cursorDown,
526
+ hideCursor,
527
+ showCursor,
528
+ stripAnsi,
529
+ displayWidth,
530
+ charWidth,
531
+ truncate,
532
+ padEnd,
533
+ wrapText,
534
+ renderMarkdown,
535
+ renderInline,
536
+ spacing,
537
+ escapeHtml,
538
+ ring
539
+ };