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