@xdxer/dingtalk-agent 0.1.5-beta.5 → 0.1.5-beta.6

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.
@@ -0,0 +1,157 @@
1
+ // Managed Agents Platform(MAP)注册表与工作区归属解析。
2
+ //
3
+ // 一个 Agent 工作区归属于至多一个托管平台;归属解析顺序固定:
4
+ // DTA_MAP 环境变量 > dingtalk-agent.json#map > .dingtalk-agent/map.json > 按 Provider 声明推断。
5
+ // 平台相关的方法学不写进 CLI——它们随对应平台的 Skill 包分发,只在用户选择该平台时安装。
6
+ // 平台命令(如 deploy/promote/observe)必须先通过归属门禁;coming-soon 平台一律 fail-closed。
7
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { dirname, join, parse, resolve } from 'node:path';
9
+ import { findAgentProjectRoot } from './development-workspace.js';
10
+ import { installGlobalSkill } from './skill-manager.js';
11
+ export const MANAGED_AGENT_PLATFORMS = [
12
+ {
13
+ name: 'multica-dingtalk',
14
+ label: 'Multica (DingTalk)',
15
+ status: 'supported',
16
+ description: '钉钉 Multica 托管平台:受控部署、晋级与观测回流。',
17
+ skills: ['dingtalk-agent-deploy', 'dingtalk-agent-boot-multica'],
18
+ commands: ['deploy', 'promote', 'observe'],
19
+ },
20
+ {
21
+ name: 'deap',
22
+ label: 'DEAP',
23
+ status: 'coming-soon',
24
+ description: '敬请期待。',
25
+ skills: [],
26
+ commands: [],
27
+ },
28
+ ];
29
+ export const MAP_FILE = '.dingtalk-agent/map.json';
30
+ const PROJECT_FILE = 'dingtalk-agent.json';
31
+ export function mapDefinition(name) {
32
+ return MANAGED_AGENT_PLATFORMS.find((item) => item.name === name) || null;
33
+ }
34
+ export function resolveMap(start = process.cwd(), env = process.env) {
35
+ const fromEnv = String(env.DTA_MAP || '').trim();
36
+ if (fromEnv)
37
+ return resolution(fromEnv, 'env', '');
38
+ const projectRoot = findAgentProjectRoot(start);
39
+ if (projectRoot) {
40
+ const manifestPath = join(projectRoot, PROJECT_FILE);
41
+ if (existsSync(manifestPath) && !lstatSync(manifestPath).isSymbolicLink()) {
42
+ const manifest = readJsonOrNull(manifestPath);
43
+ const declared = typeof manifest?.map === 'string' ? manifest.map.trim() : '';
44
+ if (declared)
45
+ return resolution(declared, 'project-manifest', manifestPath);
46
+ }
47
+ }
48
+ const mapPath = findUp(start, MAP_FILE);
49
+ if (mapPath) {
50
+ const stored = readJsonOrNull(mapPath);
51
+ const declared = typeof stored?.map === 'string' ? stored.map.trim() : '';
52
+ if (declared)
53
+ return resolution(declared, 'map-file', mapPath);
54
+ }
55
+ if (projectRoot) {
56
+ const manifest = readJsonOrNull(join(projectRoot, PROJECT_FILE));
57
+ const providers = Object.values(manifest?.workspaces || {})
58
+ .map((workspace) => workspace?.provider?.kind);
59
+ if (providers.includes('multica')) {
60
+ return resolution('multica-dingtalk', 'inferred', join(projectRoot, PROJECT_FILE));
61
+ }
62
+ }
63
+ return resolution(null, 'none', '');
64
+ }
65
+ export function useMap(start, name, options) {
66
+ const definition = mapDefinition(name);
67
+ if (!definition) {
68
+ throw new Error(`未知 managed agents platform: ${name}\n` +
69
+ `可用: ${MANAGED_AGENT_PLATFORMS.map((item) => `${item.name}(${statusLabel(item.status)})`).join('、')}`);
70
+ }
71
+ if (definition.status !== 'supported') {
72
+ throw new Error(`${definition.label} 敬请期待——当前版本尚未开放该平台。`);
73
+ }
74
+ const projectRoot = findAgentProjectRoot(start);
75
+ let written;
76
+ if (projectRoot && existsSync(join(projectRoot, PROJECT_FILE))) {
77
+ const manifestPath = join(projectRoot, PROJECT_FILE);
78
+ if (lstatSync(manifestPath).isSymbolicLink()) {
79
+ throw new Error(`${PROJECT_FILE} 不能是符号链接`);
80
+ }
81
+ const manifest = readJsonOrNull(manifestPath);
82
+ if (!manifest || typeof manifest !== 'object') {
83
+ throw new Error(`${PROJECT_FILE} 不是合法 JSON,无法写入 map 归属`);
84
+ }
85
+ manifest.map = definition.name;
86
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
87
+ written = manifestPath;
88
+ }
89
+ else {
90
+ const target = join(resolve(start), MAP_FILE);
91
+ mkdirSync(dirname(target), { recursive: true });
92
+ writeFileSync(target, JSON.stringify({ $schema: 'dingtalk-agent/map@1', map: definition.name }, null, 2) + '\n');
93
+ written = target;
94
+ }
95
+ const skillInstalls = [];
96
+ if (options.installSkills !== false) {
97
+ for (const skill of definition.skills) {
98
+ skillInstalls.push(installGlobalSkill(options.packageRoot, {
99
+ name: skill, home: options.home, env: options.env,
100
+ }));
101
+ }
102
+ }
103
+ return { ...resolveMap(start, options.env || process.env), written, skillInstalls };
104
+ }
105
+ /** deploy/promote/observe 等平台命令的归属门禁:未归属、未知或 coming-soon 一律 fail-closed。 */
106
+ export function requireSupportedMap(command, start = process.cwd(), env = process.env) {
107
+ const resolved = resolveMap(start, env);
108
+ if (!resolved.map) {
109
+ throw new Error(`当前工作区未声明 managed agents platform,无法执行 \`${command}\`。\n` +
110
+ '先运行 `dta map use multica-dingtalk`,或设置 DTA_MAP 环境变量。');
111
+ }
112
+ if (!resolved.definition) {
113
+ throw new Error(`工作区声明了未知 platform: ${resolved.map}(来源 ${resolved.source})。\n` +
114
+ `可用: ${MANAGED_AGENT_PLATFORMS.map((item) => item.name).join('、')}`);
115
+ }
116
+ if (resolved.definition.status !== 'supported') {
117
+ throw new Error(`${resolved.definition.label} 敬请期待——\`${command}\` 尚未在该平台开放。`);
118
+ }
119
+ if (!resolved.definition.commands.includes(command)) {
120
+ throw new Error(`${resolved.definition.label} 不承接命令 \`${command}\`。`);
121
+ }
122
+ return resolved;
123
+ }
124
+ export function statusLabel(status) {
125
+ return status === 'supported' ? '已支持' : '敬请期待';
126
+ }
127
+ function resolution(name, source, file) {
128
+ return {
129
+ $schema: 'dingtalk-agent/map-resolution@1',
130
+ map: name,
131
+ source,
132
+ file,
133
+ definition: name ? mapDefinition(name) : null,
134
+ platforms: MANAGED_AGENT_PLATFORMS,
135
+ };
136
+ }
137
+ function readJsonOrNull(path) {
138
+ try {
139
+ return JSON.parse(readFileSync(path, 'utf8'));
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ function findUp(start, relativePath) {
146
+ let dir = resolve(start);
147
+ while (true) {
148
+ const candidate = join(dir, relativePath);
149
+ if (existsSync(candidate) && !lstatSync(candidate).isSymbolicLink())
150
+ return candidate;
151
+ const parent = dirname(dir);
152
+ if (parent === dir || dir === parse(dir).root)
153
+ return null;
154
+ dir = parent;
155
+ }
156
+ }
157
+ //# sourceMappingURL=map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/map.ts"],"names":[],"mappings":"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","sourcesContent":["// Managed Agents Platform(MAP)注册表与工作区归属解析。\n//\n// 一个 Agent 工作区归属于至多一个托管平台;归属解析顺序固定:\n// DTA_MAP 环境变量 > dingtalk-agent.json#map > .dingtalk-agent/map.json > 按 Provider 声明推断。\n// 平台相关的方法学不写进 CLI——它们随对应平台的 Skill 包分发,只在用户选择该平台时安装。\n// 平台命令(如 deploy/promote/observe)必须先通过归属门禁;coming-soon 平台一律 fail-closed。\n\nimport { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join, parse, resolve } from 'node:path'\n\nimport { findAgentProjectRoot } from './development-workspace.js'\nimport { installGlobalSkill, type GlobalSkillStatus } from './skill-manager.js'\n\nexport type MapStatus = 'supported' | 'coming-soon'\n\nexport interface MapDefinition {\n name: string\n label: string\n status: MapStatus\n description: string\n skills: string[]\n commands: string[]\n}\n\nexport const MANAGED_AGENT_PLATFORMS: MapDefinition[] = [\n {\n name: 'multica-dingtalk',\n label: 'Multica (DingTalk)',\n status: 'supported',\n description: '钉钉 Multica 托管平台:受控部署、晋级与观测回流。',\n skills: ['dingtalk-agent-deploy', 'dingtalk-agent-boot-multica'],\n commands: ['deploy', 'promote', 'observe'],\n },\n {\n name: 'deap',\n label: 'DEAP',\n status: 'coming-soon',\n description: '敬请期待。',\n skills: [],\n commands: [],\n },\n]\n\nexport const MAP_FILE = '.dingtalk-agent/map.json'\nconst PROJECT_FILE = 'dingtalk-agent.json'\n\nexport type MapSource = 'env' | 'project-manifest' | 'map-file' | 'inferred' | 'none'\n\nexport interface MapResolution {\n $schema: 'dingtalk-agent/map-resolution@1'\n map: string | null\n source: MapSource\n file: string\n definition: MapDefinition | null\n platforms: MapDefinition[]\n}\n\nexport interface MapUseResult extends MapResolution {\n written: string\n skillInstalls: GlobalSkillStatus[]\n}\n\nexport function mapDefinition(name: string): MapDefinition | null {\n return MANAGED_AGENT_PLATFORMS.find((item) => item.name === name) || null\n}\n\nexport function resolveMap(start = process.cwd(), env: NodeJS.ProcessEnv = process.env): MapResolution {\n const fromEnv = String(env.DTA_MAP || '').trim()\n if (fromEnv) return resolution(fromEnv, 'env', '')\n\n const projectRoot = findAgentProjectRoot(start)\n if (projectRoot) {\n const manifestPath = join(projectRoot, PROJECT_FILE)\n if (existsSync(manifestPath) && !lstatSync(manifestPath).isSymbolicLink()) {\n const manifest = readJsonOrNull(manifestPath)\n const declared = typeof manifest?.map === 'string' ? manifest.map.trim() : ''\n if (declared) return resolution(declared, 'project-manifest', manifestPath)\n }\n }\n\n const mapPath = findUp(start, MAP_FILE)\n if (mapPath) {\n const stored = readJsonOrNull(mapPath)\n const declared = typeof stored?.map === 'string' ? stored.map.trim() : ''\n if (declared) return resolution(declared, 'map-file', mapPath)\n }\n\n if (projectRoot) {\n const manifest = readJsonOrNull(join(projectRoot, PROJECT_FILE))\n const providers = Object.values(manifest?.workspaces || {})\n .map((workspace: any) => workspace?.provider?.kind)\n if (providers.includes('multica')) {\n return resolution('multica-dingtalk', 'inferred', join(projectRoot, PROJECT_FILE))\n }\n }\n\n return resolution(null, 'none', '')\n}\n\nexport interface MapUseOptions {\n packageRoot: string\n home?: string\n env?: NodeJS.ProcessEnv\n installSkills?: boolean\n}\n\nexport function useMap(start: string, name: string, options: MapUseOptions): MapUseResult {\n const definition = mapDefinition(name)\n if (!definition) {\n throw new Error(\n `未知 managed agents platform: ${name}\\n` +\n `可用: ${MANAGED_AGENT_PLATFORMS.map((item) => `${item.name}(${statusLabel(item.status)})`).join('、')}`,\n )\n }\n if (definition.status !== 'supported') {\n throw new Error(`${definition.label} 敬请期待——当前版本尚未开放该平台。`)\n }\n\n const projectRoot = findAgentProjectRoot(start)\n let written: string\n if (projectRoot && existsSync(join(projectRoot, PROJECT_FILE))) {\n const manifestPath = join(projectRoot, PROJECT_FILE)\n if (lstatSync(manifestPath).isSymbolicLink()) {\n throw new Error(`${PROJECT_FILE} 不能是符号链接`)\n }\n const manifest = readJsonOrNull(manifestPath)\n if (!manifest || typeof manifest !== 'object') {\n throw new Error(`${PROJECT_FILE} 不是合法 JSON,无法写入 map 归属`)\n }\n manifest.map = definition.name\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\\n')\n written = manifestPath\n } else {\n const target = join(resolve(start), MAP_FILE)\n mkdirSync(dirname(target), { recursive: true })\n writeFileSync(target, JSON.stringify(\n { $schema: 'dingtalk-agent/map@1', map: definition.name }, null, 2,\n ) + '\\n')\n written = target\n }\n\n const skillInstalls: GlobalSkillStatus[] = []\n if (options.installSkills !== false) {\n for (const skill of definition.skills) {\n skillInstalls.push(installGlobalSkill(options.packageRoot, {\n name: skill, home: options.home, env: options.env,\n }))\n }\n }\n\n return { ...resolveMap(start, options.env || process.env), written, skillInstalls }\n}\n\n/** deploy/promote/observe 等平台命令的归属门禁:未归属、未知或 coming-soon 一律 fail-closed。 */\nexport function requireSupportedMap(\n command: string, start = process.cwd(), env: NodeJS.ProcessEnv = process.env,\n): MapResolution {\n const resolved = resolveMap(start, env)\n if (!resolved.map) {\n throw new Error(\n `当前工作区未声明 managed agents platform,无法执行 \\`${command}\\`。\\n` +\n '先运行 `dta map use multica-dingtalk`,或设置 DTA_MAP 环境变量。',\n )\n }\n if (!resolved.definition) {\n throw new Error(\n `工作区声明了未知 platform: ${resolved.map}(来源 ${resolved.source})。\\n` +\n `可用: ${MANAGED_AGENT_PLATFORMS.map((item) => item.name).join('、')}`,\n )\n }\n if (resolved.definition.status !== 'supported') {\n throw new Error(`${resolved.definition.label} 敬请期待——\\`${command}\\` 尚未在该平台开放。`)\n }\n if (!resolved.definition.commands.includes(command)) {\n throw new Error(`${resolved.definition.label} 不承接命令 \\`${command}\\`。`)\n }\n return resolved\n}\n\nexport function statusLabel(status: MapStatus): string {\n return status === 'supported' ? '已支持' : '敬请期待'\n}\n\nfunction resolution(name: string | null, source: MapSource, file: string): MapResolution {\n return {\n $schema: 'dingtalk-agent/map-resolution@1',\n map: name,\n source,\n file,\n definition: name ? mapDefinition(name) : null,\n platforms: MANAGED_AGENT_PLATFORMS,\n }\n}\n\nfunction readJsonOrNull(path: string): any {\n try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return null }\n}\n\nfunction findUp(start: string, relativePath: string): string | null {\n let dir = resolve(start)\n while (true) {\n const candidate = join(dir, relativePath)\n if (existsSync(candidate) && !lstatSync(candidate).isSymbolicLink()) return candidate\n const parent = dirname(dir)\n if (parent === dir || dir === parse(dir).root) return null\n dir = parent\n }\n}\n"]}
@@ -0,0 +1,369 @@
1
+ // dta 的输出层:同一份内容,人看一套,机器读另一套。
2
+ //
3
+ // 三条规则,实现时不要绕过:
4
+ // 1. **颜色轴与动效轴是两个独立开关。** NO_COLOR 只关颜色;\r 重绘、光标控制、
5
+ // 列对齐一律只由 stream.isTTY 决定。半数 spinner 库正是在这里搞错的。
6
+ // 2. **颜色永远是可丢弃的装饰。** 剥掉之后语义必须完整——靠行首状态词和
7
+ // [n/N] 编号承载,绝不出现"红色那行是错误"这种只有颜色能表达的语义。
8
+ // 3. **环境变量只能关色,不能开色。** 管道里想上色只有命令行显式 --color 一条路,
9
+ // 这样"不显式要就没有 ANSI"是硬保证,而不是"取决于 harness 有没有预置
10
+ // FORCE_COLOR"。--json 另外还结构性绕开本模块,双重保险。
11
+ import { createInterface } from 'node:readline/promises';
12
+ // ── 判定层 ────────────────────────────────────────────────────────────
13
+ const OFF = new Set(['0', 'false', 'no', 'off']);
14
+ /**
15
+ * 颜色轴:**环境变量只能关色,不能开色。**
16
+ *
17
+ * 优先级 NO_COLOR / NODE_DISABLE_COLORS > FORCE_COLOR=0 等显式关 > CLICOLOR=0
18
+ * > TERM=dumb > isTTY。想在管道里上色只有一条路:命令行显式 `--color`。
19
+ *
20
+ * 这一条刻意偏离 npm 生态惯例(那边 FORCE_COLOR=1 能在管道里强制上色)。
21
+ * 理由是 dta 的非 TTY 读者主要是 Agent——它通过 Bash 工具捕获 stdout 喂给
22
+ * 模型,一旦 harness 预置了 FORCE_COLOR,转义序列就会变成模型读到的噪声,
23
+ * 而这个泄漏在有色终端里肉眼完全看不出来。把"不显式要就没有 ANSI"做成
24
+ * 硬保证,比跟惯例对齐更值钱;CI 里真想要彩色日志,用 `--color` 即可。
25
+ *
26
+ * TERM / COLORTERM 同理不作为开色依据:Agent 调用时它们会被原样继承
27
+ * (实测 Claude Code 下 stdout 非 TTY 但 COLORTERM=truecolor 仍在)。
28
+ */
29
+ function colorFromEnv(stream, env) {
30
+ // no-color.org:变量存在且非空即禁用,与值无关(NO_COLOR=0 也禁)
31
+ if (env.NO_COLOR)
32
+ return false;
33
+ if (env.NODE_DISABLE_COLORS)
34
+ return false;
35
+ // 关色方向仍然尊重;开色方向一律无效,落到 isTTY
36
+ const force = env.CLICOLOR_FORCE ?? env.FORCE_COLOR;
37
+ if (force !== undefined && force !== '' && OFF.has(force.toLowerCase()))
38
+ return false;
39
+ if (env.CLICOLOR !== undefined && OFF.has(env.CLICOLOR.toLowerCase()))
40
+ return false;
41
+ if (env.TERM === 'dumb')
42
+ return false;
43
+ return Boolean(stream && stream.isTTY);
44
+ }
45
+ export function decide(options = {}) {
46
+ const stream = options.stream || process.stdout;
47
+ const flag = options.flag || 'auto';
48
+ const env = options.env || process.env;
49
+ const color = flag === 'always' ? true : flag === 'never' ? false : colorFromEnv(stream, env);
50
+ // 动效轴只看流本身,永远不看环境变量
51
+ const rich = Boolean(stream && stream.isTTY);
52
+ // columns 报 0 是真实存在的坑:据此算重绘宽度会得到负数或零长度
53
+ const cols = stream && stream.columns > 0 ? stream.columns : 80;
54
+ return { color, rich, cols };
55
+ }
56
+ // ── 渲染层 ────────────────────────────────────────────────────────────
57
+ // 只用 16 色基础子集:任何终端主题下都不会失效,也不需要探测色深。
58
+ const SGR = {
59
+ dim: ['\x1b[2m', '\x1b[22m'],
60
+ bold: ['\x1b[1m', '\x1b[22m'],
61
+ success: ['\x1b[32;1m', '\x1b[39;22m'],
62
+ danger: ['\x1b[31;1m', '\x1b[39;22m'],
63
+ warn: ['\x1b[33;1m', '\x1b[39;22m'],
64
+ noun: ['\x1b[36m', '\x1b[39m'],
65
+ };
66
+ /** 两套 Theme 必须共用它,否则同一份数据在有色/无色终端会显示成不同内容。 */
67
+ function normalize(value) {
68
+ return String(value ?? '');
69
+ }
70
+ const PLAIN = {
71
+ dim: normalize, bold: normalize, success: normalize,
72
+ danger: normalize, warn: normalize, noun: normalize,
73
+ };
74
+ export function makeTheme(ctx) {
75
+ if (!ctx.color)
76
+ return PLAIN;
77
+ const paint = (kind) => (value) => {
78
+ const text = normalize(value);
79
+ const [open, close] = SGR[kind];
80
+ // bold 与 dim 共用 22 复位,嵌套时内层的 close 会把外层也关掉;补回外层。
81
+ return open + text.split(close).join(close + open) + close;
82
+ };
83
+ return {
84
+ dim: paint('dim'), bold: paint('bold'), success: paint('success'),
85
+ danger: paint('danger'), warn: paint('warn'), noun: paint('noun'),
86
+ };
87
+ }
88
+ // ── 显示宽度 ──────────────────────────────────────────────────────────
89
+ // CSI(私有参数 / 冒号参数 / intermediate bytes 全覆盖,按 ECMA-48 的形状)与
90
+ // OSC(BEL 或 ST 收尾)。未终止的序列吃到串尾——宁可少算也不把裸转义算进宽度。
91
+ // 注意:本函数只承诺处理 CLI 自己产生的文本,不是通用终端流解析器。
92
+ const ANSI = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][\s\S]*?(?:\x07|\x1b\\|$)|\x1b[@-Z\\-_]/g;
93
+ const SEGMENTER = new Intl.Segmenter('zh', { granularity: 'grapheme' });
94
+ // 占两列的码点区间。**由 string-width@8 逐码点扫描 0x0..0x3FFFF 生成**,
95
+ // 并剔除了 Emoji_Presentation(那部分下面用正则判,放进表里只会让表白白变大)。
96
+ // 手写区间表是这套东西最容易出错的地方:上一版把 [0x3041,0x33ff] 整段当 Wide,
97
+ // 混进了 U+3248..U+324F 这些 Ambiguous 字符,同时又漏掉西夏、假名补充、
98
+ // 女书、圈起表意补充等真正的 Wide 区段。
99
+ const WIDE = [
100
+ [0x1100, 0x115e], [0x2329, 0x232a], [0x2630, 0x2637], [0x268a, 0x268f],
101
+ [0x2e80, 0x2e99], [0x2e9b, 0x2ef3], [0x2f00, 0x2fd5], [0x2ff0, 0x3029],
102
+ [0x302e, 0x303e], [0x3041, 0x3096], [0x309b, 0x30ff], [0x3105, 0x312f],
103
+ [0x3131, 0x3163], [0x3165, 0x318e], [0x3190, 0x31e5], [0x31ef, 0x321e],
104
+ [0x3220, 0x3247], [0x3250, 0xa48c], [0xa490, 0xa4c6], [0xa960, 0xa97c],
105
+ [0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe10, 0xfe19], [0xfe30, 0xfe52],
106
+ [0xfe54, 0xfe66], [0xfe68, 0xfe6b], [0xff01, 0xff60], [0xffe0, 0xffe6],
107
+ [0x16fe0, 0x16fe3], [0x16ff0, 0x16ff6], [0x17000, 0x18cd5], [0x18cff, 0x18d1e],
108
+ [0x18d80, 0x18df2], [0x1aff0, 0x1aff3], [0x1aff5, 0x1affb], [0x1affd, 0x1affe],
109
+ [0x1b000, 0x1b122], [0x1b132, 0x1b132], [0x1b150, 0x1b152], [0x1b155, 0x1b155],
110
+ [0x1b164, 0x1b167], [0x1b170, 0x1b2fb], [0x1d300, 0x1d356], [0x1d360, 0x1d376],
111
+ [0x1f200, 0x1f200], [0x1f202, 0x1f202], [0x1f210, 0x1f219], [0x1f21b, 0x1f22e],
112
+ [0x1f230, 0x1f231], [0x1f237, 0x1f237], [0x1f23b, 0x1f23b], [0x1f240, 0x1f248],
113
+ [0x1f260, 0x1f265], [0x1f6d8, 0x1f6d8], [0x1fa8a, 0x1fa8a], [0x1fa8e, 0x1fa8e],
114
+ [0x1fac8, 0x1fac8], [0x1facd, 0x1facd], [0x1faea, 0x1faea], [0x1faef, 0x1faef],
115
+ [0x20000, 0x2fffd], [0x30000, 0x3fffd]
116
+ ];
117
+ function isWide(codePoint) {
118
+ let low = 0;
119
+ let high = WIDE.length - 1;
120
+ while (low <= high) {
121
+ const mid = (low + high) >> 1;
122
+ if (codePoint < WIDE[mid][0])
123
+ high = mid - 1;
124
+ else if (codePoint > WIDE[mid][1])
125
+ low = mid + 1;
126
+ else
127
+ return true;
128
+ }
129
+ return false;
130
+ }
131
+ const VS16 = '\ufe0f';
132
+ const ZWJ = '\u200d';
133
+ // 只有真的含图形 emoji 的字素簇才按 2 列算。"含 ZWJ 就是 emoji" 是错的:
134
+ // a + ZWJ + b 会被切成两簇,按旧规则算出 3 列。
135
+ const EXTENDED_PICTOGRAPHIC = /\p{Extended_Pictographic}/u;
136
+ const EMOJI_PRESENTATION = /\p{Emoji_Presentation}/u;
137
+ // 肤色修饰符单独出现时也占 2 列(附着在基字符上时会与基字符同属一簇)
138
+ const EMOJI_MODIFIER = /\p{Emoji_Modifier}/u;
139
+ const LONE_REGIONAL_INDICATOR = /^\p{Regional_Indicator}$/u;
140
+ // 整簇只由非间距/包围标记、格式字符、默认可忽略码点组成时不占列:
141
+ // 单独的 U+0301、软连字符 U+00AD、阿拉伯数字标记 U+0600 都属于这一类。
142
+ // **只能用 Mn/Me,不能用 Mark**:Mc(间距组合标记,如梵文 U+0903)实际占 1 列。
143
+ const ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}\p{Default_Ignorable_Code_Point}]+$/u;
144
+ /**
145
+ * 终端显示宽度。先剥 ANSI,再按字素簇计算——不是 String.length。
146
+ *
147
+ * 四类都会翻车,缺一不可:
148
+ * 中文 '新版本可用:' length=6 但占 11 列
149
+ * 变体符 ✔(U+2714) 占 1 列,✔️(U+2714 U+FE0F) 占 2 列
150
+ * emoji ✅ 不在 EAW Wide 区间内,靠 Emoji_Presentation 才判得出 2 列
151
+ * 组合标记 单独出现的 U+0301 不占列
152
+ */
153
+ export function displayWidth(input) {
154
+ const text = normalize(input).replace(ANSI, '');
155
+ let width = 0;
156
+ for (const { segment } of SEGMENTER.segment(text)) {
157
+ const codePoint = segment.codePointAt(0);
158
+ if (codePoint === undefined)
159
+ continue;
160
+ // 控制字符不占列
161
+ if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0))
162
+ continue;
163
+ // 落单的地区指示符只占 1 列;成对时(🇨🇳)两个码点属于同一簇,走下面的 emoji 分支占 2 列。
164
+ if (LONE_REGIONAL_INDICATOR.test(segment)) {
165
+ width += 1;
166
+ continue;
167
+ }
168
+ // 按 emoji 呈现的簇占 2 列。三个条件缺一不可,而且都不能靠
169
+ // Extended_Pictographic 兜底——肤色修饰符 U+1F3FB 是 Emoji_Presentation
170
+ // 但 **不是** Extended_Pictographic,拿后者当外层 gate 会直接漏掉。
171
+ const asEmoji = EMOJI_PRESENTATION.test(segment) || EMOJI_MODIFIER.test(segment) ||
172
+ (EXTENDED_PICTOGRAPHIC.test(segment) &&
173
+ (segment.includes(VS16) || segment.includes(ZWJ)));
174
+ if (asEmoji) {
175
+ width += 2;
176
+ continue;
177
+ }
178
+ // 文字呈现的图形符不能直接判 1:八卦符 U+2630 这类本身就是 EAW Wide,
179
+ // 要落回下面的区间表,否则会把 2 列算成 1 列。
180
+ // 零宽簇(附着在基字符上时会和基字符同属一簇,走下面的分支)
181
+ if (ZERO_WIDTH.test(segment))
182
+ continue;
183
+ width += isWide(codePoint) ? 2 : 1;
184
+ }
185
+ return width;
186
+ }
187
+ /** 按显示宽度截断;宁可少一列也不把宽字符劈成半个。 */
188
+ export function truncateToWidth(text, limit) {
189
+ if (limit <= 0)
190
+ return '';
191
+ if (displayWidth(text) <= limit)
192
+ return text;
193
+ let out = '';
194
+ let width = 0;
195
+ for (const { segment } of SEGMENTER.segment(normalize(text))) {
196
+ const next = displayWidth(segment);
197
+ if (width + next > limit - 1)
198
+ break;
199
+ out += segment;
200
+ width += next;
201
+ }
202
+ return `${out}…`;
203
+ }
204
+ /** 按显示宽度右填充;字符串里的 ANSI 不计入宽度。 */
205
+ export function padRight(text, width) {
206
+ const pad = width - displayWidth(text);
207
+ return pad > 0 ? text + ' '.repeat(pad) : text;
208
+ }
209
+ // ── 进程级配置 ────────────────────────────────────────────────────────
210
+ let flagOverride = 'auto';
211
+ let outCache = null;
212
+ let errCache = null;
213
+ /** 由 bin 在解析完 argv 后调用一次,把 --color/--no-color 交给整个进程。 */
214
+ export function configureTui(flag) {
215
+ flagOverride = flag;
216
+ outCache = null;
217
+ errCache = null;
218
+ }
219
+ /** 人类报告通道(stdout)。--json 分支不得经过这里。 */
220
+ export function out() {
221
+ if (!outCache) {
222
+ const ctx = decide({ stream: process.stdout, flag: flagOverride });
223
+ outCache = { ctx, theme: makeTheme(ctx) };
224
+ }
225
+ return outCache;
226
+ }
227
+ /** 进度与诊断通道(stderr)。 */
228
+ export function err() {
229
+ if (!errCache) {
230
+ const ctx = decide({ stream: process.stderr, flag: flagOverride });
231
+ errCache = { ctx, theme: makeTheme(ctx) };
232
+ }
233
+ return errCache;
234
+ }
235
+ // ── 分步进度 ──────────────────────────────────────────────────────────
236
+ const MARK_OK = '✔';
237
+ const MARK_FAIL = '✗';
238
+ /**
239
+ * 同一个事件流,两套 renderer:
240
+ * TTY —— 先打进行态,完成后原地重绘塌缩成一行终态。
241
+ * 非 TTY —— 进行态完全不打,只在终态输出一行;行首状态词可 grep,零控制码。
242
+ *
243
+ * 进度一律走 stderr,stdout 留给数据(AGENTS.md:--json 输出不得混入日志)。
244
+ */
245
+ export class StepReporter {
246
+ stream;
247
+ ctx;
248
+ theme;
249
+ total;
250
+ index = 0;
251
+ label = '';
252
+ startedAt = 0;
253
+ /** 只有 running 时才允许 end();防止漏 begin 打出 [0/N] 和从 epoch 起算的耗时 */
254
+ running = false;
255
+ /** 进行态实际写出的显示宽度,决定要擦几行 */
256
+ drawnWidth = 0;
257
+ constructor(options) {
258
+ this.stream = options.stream || process.stderr;
259
+ this.total = options.total;
260
+ this.ctx = decide({ stream: this.stream, flag: options.flag || flagOverride });
261
+ this.theme = makeTheme(this.ctx);
262
+ }
263
+ get rich() { return this.ctx.rich; }
264
+ begin(label) {
265
+ if (this.running)
266
+ this.end(true, '未收尾');
267
+ this.index += 1;
268
+ this.label = label;
269
+ this.startedAt = Date.now();
270
+ this.running = true;
271
+ this.drawnWidth = 0;
272
+ if (this.ctx.rich) {
273
+ const head = ` [${this.index}/${this.total}] `;
274
+ // 截断到单行:进行态一旦折行,end() 的 \r 擦除只能覆盖最后一个物理行,
275
+ // 剩下的会留成残影。安装源可以由环境变量拉得很长,这不是理论问题。
276
+ const body = truncateToWidth(`${label}...`, Math.max(0, this.ctx.cols - displayWidth(head) - 1));
277
+ this.drawnWidth = displayWidth(head) + displayWidth(body);
278
+ this.stream.write(` ${this.theme.dim(`[${this.index}/${this.total}]`)} ${body}`);
279
+ }
280
+ return this;
281
+ }
282
+ /** meta 例如 '2.1MB, 0.3s';不传时自动补耗时。 */
283
+ end(ok = true, meta = '') {
284
+ // 没 begin 过就 end 是调用方的 bug;静默吞掉比打出 [0/N] 和 1974 万秒好
285
+ if (!this.running)
286
+ return this;
287
+ this.running = false;
288
+ const seconds = ((Date.now() - this.startedAt) / 1000).toFixed(1);
289
+ const info = meta || `${seconds}s`;
290
+ const counter = `[${this.index}/${this.total}]`;
291
+ if (this.ctx.rich) {
292
+ // 擦掉进行态:\r 回行首 + 覆盖实际写过的宽度 + \r。
293
+ // 不用 \e[K 是为了在窄窗口下行为更可预期。
294
+ this.stream.write(`\r${' '.repeat(Math.min(this.ctx.cols - 1, Math.max(0, this.drawnWidth)))}\r`);
295
+ const mark = ok ? this.theme.success(MARK_OK) : this.theme.danger(MARK_FAIL);
296
+ this.stream.write(` ${this.theme.dim(counter)} ${this.label} ${mark} ${this.theme.dim(`(${info})`)}\n`);
297
+ }
298
+ else {
299
+ this.stream.write(`${counter} ${ok ? 'OK ' : 'FAIL'} ${this.label} (${info})\n`);
300
+ }
301
+ return this;
302
+ }
303
+ /** 挂在当前步骤下的子结果。 */
304
+ detail(text) {
305
+ if (this.ctx.rich) {
306
+ this.stream.write(` ${this.theme.dim('→')} ${this.theme.noun(text)}\n`);
307
+ }
308
+ else {
309
+ this.stream.write(`[${this.index}/${this.total}] ${text}\n`);
310
+ }
311
+ return this;
312
+ }
313
+ /** 一行旁白,两种形态都输出,只有颜色不同。 */
314
+ note(text) {
315
+ this.stream.write(this.ctx.rich ? ` ${this.theme.dim(text)}\n` : `${text}\n`);
316
+ return this;
317
+ }
318
+ }
319
+ /**
320
+ * 标签值对齐。**对齐是给人的渲染变换,不是数据结构**——非 TTY 下退回
321
+ * `key: value` 单空格,Agent 读到的没有一个多余空格。
322
+ */
323
+ export function renderPairs(rows) {
324
+ const { ctx, theme } = err();
325
+ if (!ctx.rich)
326
+ return rows.map(([key, value]) => `${key}: ${value}`);
327
+ const width = Math.max(...rows.map(([key]) => displayWidth(`${key}:`)));
328
+ return rows.map(([key, value]) => ` ${theme.bold(padRight(`${key}:`, width))} ${value}`);
329
+ }
330
+ // ── 确认 ──────────────────────────────────────────────────────────────
331
+ /**
332
+ * 非交互环境下**绝不能挂起**——Agent 调用时表现为超时而不是报错,极难排查。
333
+ * stdin 不是终端时直接返回 fallback,一个字节都不读。
334
+ */
335
+ export async function confirm(question, options = {}) {
336
+ if (options.yes)
337
+ return true;
338
+ const stdin = options.stdin || process.stdin;
339
+ const stream = options.stream || process.stderr;
340
+ const fallback = options.fallback ?? false;
341
+ if (!stdin.isTTY)
342
+ return fallback;
343
+ const rl = createInterface({ input: stdin, output: stream });
344
+ try {
345
+ const answered = rl.question(`${question} [y/N] `);
346
+ // 光靠 try/catch 不够:输入流结束(而不是 Ctrl+D 中断)时 question() 的
347
+ // Promise **永不 settle**,进程会在事件循环排空后静默退出——没有报错、
348
+ // 没有退出码线索,是最难排查的一类失败。必须同时盯着 close 事件。
349
+ const closed = new Promise((resolve) => rl.once('close', () => resolve(null)));
350
+ answered.catch(() => { }); // 竞速落定后迟到的 reject 不该变成 unhandled
351
+ const answer = await Promise.race([answered, closed]);
352
+ if (answer === null) {
353
+ stream.write('\n');
354
+ return false;
355
+ }
356
+ return /^y(es)?$/i.test(String(answer).trim());
357
+ }
358
+ catch {
359
+ // Ctrl+D 会让 readline 抛错。在确认提示上这是"拒绝",不是故障——
360
+ // 冒到顶层只会变成一条 `错误: Aborted`,既吓人又没信息。
361
+ // 这里返回 false 而不是 fallback:人明确中断了,不能当成默认同意。
362
+ stream.write('\n');
363
+ return false;
364
+ }
365
+ finally {
366
+ rl.close();
367
+ }
368
+ }
369
+ //# sourceMappingURL=tui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui.js","sourceRoot":"","sources":["../../src/tui.ts"],"names":[],"mappings":"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E,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;IACtE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;IACtE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC9E,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC9E,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC9E,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC9E,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC9E,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC9E,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC9E,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC9E,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAA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","sourcesContent":["// dta 的输出层:同一份内容,人看一套,机器读另一套。\n//\n// 三条规则,实现时不要绕过:\n// 1. **颜色轴与动效轴是两个独立开关。** NO_COLOR 只关颜色;\\r 重绘、光标控制、\n// 列对齐一律只由 stream.isTTY 决定。半数 spinner 库正是在这里搞错的。\n// 2. **颜色永远是可丢弃的装饰。** 剥掉之后语义必须完整——靠行首状态词和\n// [n/N] 编号承载,绝不出现\"红色那行是错误\"这种只有颜色能表达的语义。\n// 3. **环境变量只能关色,不能开色。** 管道里想上色只有命令行显式 --color 一条路,\n// 这样\"不显式要就没有 ANSI\"是硬保证,而不是\"取决于 harness 有没有预置\n// FORCE_COLOR\"。--json 另外还结构性绕开本模块,双重保险。\n\nimport { createInterface } from 'node:readline/promises'\n\nexport type ColorFlag = 'auto' | 'always' | 'never'\n\nexport interface TuiContext {\n /** 允许发 SGR 颜色 */\n color: boolean\n /** 允许发 \\r 重绘、光标控制、列对齐——只看 isTTY */\n rich: boolean\n /** 终端宽度;columns 报 0 时兜底 80 */\n cols: number\n}\n\nexport interface Theme {\n dim(value: unknown): string\n bold(value: unknown): string\n success(value: unknown): string\n danger(value: unknown): string\n warn(value: unknown): string\n /** 可复制的名词:路径、版本号、ID、命令 */\n noun(value: unknown): string\n}\n\n// ── 判定层 ────────────────────────────────────────────────────────────\n\nconst OFF = new Set(['0', 'false', 'no', 'off'])\n\n/**\n * 颜色轴:**环境变量只能关色,不能开色。**\n *\n * 优先级 NO_COLOR / NODE_DISABLE_COLORS > FORCE_COLOR=0 等显式关 > CLICOLOR=0\n * > TERM=dumb > isTTY。想在管道里上色只有一条路:命令行显式 `--color`。\n *\n * 这一条刻意偏离 npm 生态惯例(那边 FORCE_COLOR=1 能在管道里强制上色)。\n * 理由是 dta 的非 TTY 读者主要是 Agent——它通过 Bash 工具捕获 stdout 喂给\n * 模型,一旦 harness 预置了 FORCE_COLOR,转义序列就会变成模型读到的噪声,\n * 而这个泄漏在有色终端里肉眼完全看不出来。把\"不显式要就没有 ANSI\"做成\n * 硬保证,比跟惯例对齐更值钱;CI 里真想要彩色日志,用 `--color` 即可。\n *\n * TERM / COLORTERM 同理不作为开色依据:Agent 调用时它们会被原样继承\n * (实测 Claude Code 下 stdout 非 TTY 但 COLORTERM=truecolor 仍在)。\n */\nfunction colorFromEnv(stream: NodeJS.WriteStream | undefined, env: NodeJS.ProcessEnv): boolean {\n // no-color.org:变量存在且非空即禁用,与值无关(NO_COLOR=0 也禁)\n if (env.NO_COLOR) return false\n if (env.NODE_DISABLE_COLORS) return false\n\n // 关色方向仍然尊重;开色方向一律无效,落到 isTTY\n const force = env.CLICOLOR_FORCE ?? env.FORCE_COLOR\n if (force !== undefined && force !== '' && OFF.has(force.toLowerCase())) return false\n if (env.CLICOLOR !== undefined && OFF.has(env.CLICOLOR.toLowerCase())) return false\n if (env.TERM === 'dumb') return false\n\n return Boolean(stream && stream.isTTY)\n}\n\nexport function decide(options: {\n stream?: NodeJS.WriteStream\n flag?: ColorFlag\n env?: NodeJS.ProcessEnv\n} = {}): TuiContext {\n const stream = options.stream || process.stdout\n const flag = options.flag || 'auto'\n const env = options.env || process.env\n const color = flag === 'always' ? true : flag === 'never' ? false : colorFromEnv(stream, env)\n // 动效轴只看流本身,永远不看环境变量\n const rich = Boolean(stream && stream.isTTY)\n // columns 报 0 是真实存在的坑:据此算重绘宽度会得到负数或零长度\n const cols = stream && stream.columns > 0 ? stream.columns : 80\n return { color, rich, cols }\n}\n\n// ── 渲染层 ────────────────────────────────────────────────────────────\n\n// 只用 16 色基础子集:任何终端主题下都不会失效,也不需要探测色深。\nconst SGR: Record<keyof Theme, [string, string]> = {\n dim: ['\\x1b[2m', '\\x1b[22m'],\n bold: ['\\x1b[1m', '\\x1b[22m'],\n success: ['\\x1b[32;1m', '\\x1b[39;22m'],\n danger: ['\\x1b[31;1m', '\\x1b[39;22m'],\n warn: ['\\x1b[33;1m', '\\x1b[39;22m'],\n noun: ['\\x1b[36m', '\\x1b[39m'],\n}\n\n/** 两套 Theme 必须共用它,否则同一份数据在有色/无色终端会显示成不同内容。 */\nfunction normalize(value: unknown): string {\n return String(value ?? '')\n}\n\nconst PLAIN: Theme = {\n dim: normalize, bold: normalize, success: normalize,\n danger: normalize, warn: normalize, noun: normalize,\n}\n\nexport function makeTheme(ctx: TuiContext): Theme {\n if (!ctx.color) return PLAIN\n const paint = (kind: keyof Theme) => (value: unknown): string => {\n const text = normalize(value)\n const [open, close] = SGR[kind]\n // bold 与 dim 共用 22 复位,嵌套时内层的 close 会把外层也关掉;补回外层。\n return open + text.split(close).join(close + open) + close\n }\n return {\n dim: paint('dim'), bold: paint('bold'), success: paint('success'),\n danger: paint('danger'), warn: paint('warn'), noun: paint('noun'),\n }\n}\n\n// ── 显示宽度 ──────────────────────────────────────────────────────────\n\n// CSI(私有参数 / 冒号参数 / intermediate bytes 全覆盖,按 ECMA-48 的形状)与\n// OSC(BEL 或 ST 收尾)。未终止的序列吃到串尾——宁可少算也不把裸转义算进宽度。\n// 注意:本函数只承诺处理 CLI 自己产生的文本,不是通用终端流解析器。\nconst ANSI = /\\x1b\\[[0-?]*[ -/]*[@-~]|\\x1b\\][\\s\\S]*?(?:\\x07|\\x1b\\\\|$)|\\x1b[@-Z\\\\-_]/g\n\nconst SEGMENTER = new Intl.Segmenter('zh', { granularity: 'grapheme' })\n\n// 占两列的码点区间。**由 string-width@8 逐码点扫描 0x0..0x3FFFF 生成**,\n// 并剔除了 Emoji_Presentation(那部分下面用正则判,放进表里只会让表白白变大)。\n// 手写区间表是这套东西最容易出错的地方:上一版把 [0x3041,0x33ff] 整段当 Wide,\n// 混进了 U+3248..U+324F 这些 Ambiguous 字符,同时又漏掉西夏、假名补充、\n// 女书、圈起表意补充等真正的 Wide 区段。\nconst WIDE: ReadonlyArray<readonly [number, number]> = [\n [0x1100, 0x115e], [0x2329, 0x232a], [0x2630, 0x2637], [0x268a, 0x268f],\n [0x2e80, 0x2e99], [0x2e9b, 0x2ef3], [0x2f00, 0x2fd5], [0x2ff0, 0x3029],\n [0x302e, 0x303e], [0x3041, 0x3096], [0x309b, 0x30ff], [0x3105, 0x312f],\n [0x3131, 0x3163], [0x3165, 0x318e], [0x3190, 0x31e5], [0x31ef, 0x321e],\n [0x3220, 0x3247], [0x3250, 0xa48c], [0xa490, 0xa4c6], [0xa960, 0xa97c],\n [0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe10, 0xfe19], [0xfe30, 0xfe52],\n [0xfe54, 0xfe66], [0xfe68, 0xfe6b], [0xff01, 0xff60], [0xffe0, 0xffe6],\n [0x16fe0, 0x16fe3], [0x16ff0, 0x16ff6], [0x17000, 0x18cd5], [0x18cff, 0x18d1e],\n [0x18d80, 0x18df2], [0x1aff0, 0x1aff3], [0x1aff5, 0x1affb], [0x1affd, 0x1affe],\n [0x1b000, 0x1b122], [0x1b132, 0x1b132], [0x1b150, 0x1b152], [0x1b155, 0x1b155],\n [0x1b164, 0x1b167], [0x1b170, 0x1b2fb], [0x1d300, 0x1d356], [0x1d360, 0x1d376],\n [0x1f200, 0x1f200], [0x1f202, 0x1f202], [0x1f210, 0x1f219], [0x1f21b, 0x1f22e],\n [0x1f230, 0x1f231], [0x1f237, 0x1f237], [0x1f23b, 0x1f23b], [0x1f240, 0x1f248],\n [0x1f260, 0x1f265], [0x1f6d8, 0x1f6d8], [0x1fa8a, 0x1fa8a], [0x1fa8e, 0x1fa8e],\n [0x1fac8, 0x1fac8], [0x1facd, 0x1facd], [0x1faea, 0x1faea], [0x1faef, 0x1faef],\n [0x20000, 0x2fffd], [0x30000, 0x3fffd]\n]\n\nfunction isWide(codePoint: number): boolean {\n let low = 0\n let high = WIDE.length - 1\n while (low <= high) {\n const mid = (low + high) >> 1\n if (codePoint < WIDE[mid][0]) high = mid - 1\n else if (codePoint > WIDE[mid][1]) low = mid + 1\n else return true\n }\n return false\n}\n\nconst VS16 = '\\ufe0f'\nconst ZWJ = '\\u200d'\n// 只有真的含图形 emoji 的字素簇才按 2 列算。\"含 ZWJ 就是 emoji\" 是错的:\n// a + ZWJ + b 会被切成两簇,按旧规则算出 3 列。\nconst EXTENDED_PICTOGRAPHIC = /\\p{Extended_Pictographic}/u\nconst EMOJI_PRESENTATION = /\\p{Emoji_Presentation}/u\n// 肤色修饰符单独出现时也占 2 列(附着在基字符上时会与基字符同属一簇)\nconst EMOJI_MODIFIER = /\\p{Emoji_Modifier}/u\nconst LONE_REGIONAL_INDICATOR = /^\\p{Regional_Indicator}$/u\n// 整簇只由非间距/包围标记、格式字符、默认可忽略码点组成时不占列:\n// 单独的 U+0301、软连字符 U+00AD、阿拉伯数字标记 U+0600 都属于这一类。\n// **只能用 Mn/Me,不能用 Mark**:Mc(间距组合标记,如梵文 U+0903)实际占 1 列。\nconst ZERO_WIDTH = /^[\\p{Mn}\\p{Me}\\p{Cf}\\p{Default_Ignorable_Code_Point}]+$/u\n\n/**\n * 终端显示宽度。先剥 ANSI,再按字素簇计算——不是 String.length。\n *\n * 四类都会翻车,缺一不可:\n * 中文 '新版本可用:' length=6 但占 11 列\n * 变体符 ✔(U+2714) 占 1 列,✔️(U+2714 U+FE0F) 占 2 列\n * emoji ✅ 不在 EAW Wide 区间内,靠 Emoji_Presentation 才判得出 2 列\n * 组合标记 单独出现的 U+0301 不占列\n */\nexport function displayWidth(input: unknown): number {\n const text = normalize(input).replace(ANSI, '')\n let width = 0\n for (const { segment } of SEGMENTER.segment(text)) {\n const codePoint = segment.codePointAt(0)\n if (codePoint === undefined) continue\n // 控制字符不占列\n if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0)) continue\n // 落单的地区指示符只占 1 列;成对时(🇨🇳)两个码点属于同一簇,走下面的 emoji 分支占 2 列。\n if (LONE_REGIONAL_INDICATOR.test(segment)) { width += 1; continue }\n // 按 emoji 呈现的簇占 2 列。三个条件缺一不可,而且都不能靠\n // Extended_Pictographic 兜底——肤色修饰符 U+1F3FB 是 Emoji_Presentation\n // 但 **不是** Extended_Pictographic,拿后者当外层 gate 会直接漏掉。\n const asEmoji = EMOJI_PRESENTATION.test(segment) || EMOJI_MODIFIER.test(segment) ||\n (EXTENDED_PICTOGRAPHIC.test(segment) &&\n (segment.includes(VS16) || segment.includes(ZWJ)))\n if (asEmoji) { width += 2; continue }\n // 文字呈现的图形符不能直接判 1:八卦符 U+2630 这类本身就是 EAW Wide,\n // 要落回下面的区间表,否则会把 2 列算成 1 列。\n // 零宽簇(附着在基字符上时会和基字符同属一簇,走下面的分支)\n if (ZERO_WIDTH.test(segment)) continue\n width += isWide(codePoint) ? 2 : 1\n }\n return width\n}\n\n/** 按显示宽度截断;宁可少一列也不把宽字符劈成半个。 */\nexport function truncateToWidth(text: string, limit: number): string {\n if (limit <= 0) return ''\n if (displayWidth(text) <= limit) return text\n let out = ''\n let width = 0\n for (const { segment } of SEGMENTER.segment(normalize(text))) {\n const next = displayWidth(segment)\n if (width + next > limit - 1) break\n out += segment\n width += next\n }\n return `${out}…`\n}\n\n/** 按显示宽度右填充;字符串里的 ANSI 不计入宽度。 */\nexport function padRight(text: string, width: number): string {\n const pad = width - displayWidth(text)\n return pad > 0 ? text + ' '.repeat(pad) : text\n}\n\n// ── 进程级配置 ────────────────────────────────────────────────────────\n\nlet flagOverride: ColorFlag = 'auto'\nlet outCache: { ctx: TuiContext; theme: Theme } | null = null\nlet errCache: { ctx: TuiContext; theme: Theme } | null = null\n\n/** 由 bin 在解析完 argv 后调用一次,把 --color/--no-color 交给整个进程。 */\nexport function configureTui(flag: ColorFlag): void {\n flagOverride = flag\n outCache = null\n errCache = null\n}\n\n/** 人类报告通道(stdout)。--json 分支不得经过这里。 */\nexport function out(): { ctx: TuiContext; theme: Theme } {\n if (!outCache) {\n const ctx = decide({ stream: process.stdout, flag: flagOverride })\n outCache = { ctx, theme: makeTheme(ctx) }\n }\n return outCache\n}\n\n/** 进度与诊断通道(stderr)。 */\nexport function err(): { ctx: TuiContext; theme: Theme } {\n if (!errCache) {\n const ctx = decide({ stream: process.stderr, flag: flagOverride })\n errCache = { ctx, theme: makeTheme(ctx) }\n }\n return errCache\n}\n\n// ── 分步进度 ──────────────────────────────────────────────────────────\n\nconst MARK_OK = '✔'\nconst MARK_FAIL = '✗'\n\n/**\n * 同一个事件流,两套 renderer:\n * TTY —— 先打进行态,完成后原地重绘塌缩成一行终态。\n * 非 TTY —— 进行态完全不打,只在终态输出一行;行首状态词可 grep,零控制码。\n *\n * 进度一律走 stderr,stdout 留给数据(AGENTS.md:--json 输出不得混入日志)。\n */\nexport class StepReporter {\n private readonly stream: NodeJS.WriteStream\n private readonly ctx: TuiContext\n private readonly theme: Theme\n private readonly total: number\n private index = 0\n private label = ''\n private startedAt = 0\n /** 只有 running 时才允许 end();防止漏 begin 打出 [0/N] 和从 epoch 起算的耗时 */\n private running = false\n /** 进行态实际写出的显示宽度,决定要擦几行 */\n private drawnWidth = 0\n\n constructor(options: { total: number; stream?: NodeJS.WriteStream; flag?: ColorFlag }) {\n this.stream = options.stream || process.stderr\n this.total = options.total\n this.ctx = decide({ stream: this.stream, flag: options.flag || flagOverride })\n this.theme = makeTheme(this.ctx)\n }\n\n get rich(): boolean { return this.ctx.rich }\n\n begin(label: string): this {\n if (this.running) this.end(true, '未收尾')\n this.index += 1\n this.label = label\n this.startedAt = Date.now()\n this.running = true\n this.drawnWidth = 0\n if (this.ctx.rich) {\n const head = ` [${this.index}/${this.total}] `\n // 截断到单行:进行态一旦折行,end() 的 \\r 擦除只能覆盖最后一个物理行,\n // 剩下的会留成残影。安装源可以由环境变量拉得很长,这不是理论问题。\n const body = truncateToWidth(`${label}...`, Math.max(0, this.ctx.cols - displayWidth(head) - 1))\n this.drawnWidth = displayWidth(head) + displayWidth(body)\n this.stream.write(` ${this.theme.dim(`[${this.index}/${this.total}]`)} ${body}`)\n }\n return this\n }\n\n /** meta 例如 '2.1MB, 0.3s';不传时自动补耗时。 */\n end(ok = true, meta = ''): this {\n // 没 begin 过就 end 是调用方的 bug;静默吞掉比打出 [0/N] 和 1974 万秒好\n if (!this.running) return this\n this.running = false\n const seconds = ((Date.now() - this.startedAt) / 1000).toFixed(1)\n const info = meta || `${seconds}s`\n const counter = `[${this.index}/${this.total}]`\n if (this.ctx.rich) {\n // 擦掉进行态:\\r 回行首 + 覆盖实际写过的宽度 + \\r。\n // 不用 \\e[K 是为了在窄窗口下行为更可预期。\n this.stream.write(`\\r${' '.repeat(Math.min(this.ctx.cols - 1, Math.max(0, this.drawnWidth)))}\\r`)\n const mark = ok ? this.theme.success(MARK_OK) : this.theme.danger(MARK_FAIL)\n this.stream.write(` ${this.theme.dim(counter)} ${this.label} ${mark} ${this.theme.dim(`(${info})`)}\\n`)\n } else {\n this.stream.write(`${counter} ${ok ? 'OK ' : 'FAIL'} ${this.label} (${info})\\n`)\n }\n return this\n }\n\n /** 挂在当前步骤下的子结果。 */\n detail(text: string): this {\n if (this.ctx.rich) {\n this.stream.write(` ${this.theme.dim('→')} ${this.theme.noun(text)}\\n`)\n } else {\n this.stream.write(`[${this.index}/${this.total}] ${text}\\n`)\n }\n return this\n }\n\n /** 一行旁白,两种形态都输出,只有颜色不同。 */\n note(text: string): this {\n this.stream.write(this.ctx.rich ? ` ${this.theme.dim(text)}\\n` : `${text}\\n`)\n return this\n }\n}\n\n/**\n * 标签值对齐。**对齐是给人的渲染变换,不是数据结构**——非 TTY 下退回\n * `key: value` 单空格,Agent 读到的没有一个多余空格。\n */\nexport function renderPairs(rows: ReadonlyArray<readonly [string, string]>): string[] {\n const { ctx, theme } = err()\n if (!ctx.rich) return rows.map(([key, value]) => `${key}: ${value}`)\n const width = Math.max(...rows.map(([key]) => displayWidth(`${key}:`)))\n return rows.map(([key, value]) => ` ${theme.bold(padRight(`${key}:`, width))} ${value}`)\n}\n\n// ── 确认 ──────────────────────────────────────────────────────────────\n\n/**\n * 非交互环境下**绝不能挂起**——Agent 调用时表现为超时而不是报错,极难排查。\n * stdin 不是终端时直接返回 fallback,一个字节都不读。\n */\nexport async function confirm(question: string, options: {\n yes?: boolean\n /** 非交互时的取值;upgrade 传 true 以保持既有的\"敲下去就装\"行为。 */\n fallback?: boolean\n stdin?: NodeJS.ReadStream\n stream?: NodeJS.WriteStream\n} = {}): Promise<boolean> {\n if (options.yes) return true\n const stdin = options.stdin || process.stdin\n const stream = options.stream || process.stderr\n const fallback = options.fallback ?? false\n if (!stdin.isTTY) return fallback\n\n const rl = createInterface({ input: stdin, output: stream })\n try {\n const answered = rl.question(`${question} [y/N] `)\n // 光靠 try/catch 不够:输入流结束(而不是 Ctrl+D 中断)时 question() 的\n // Promise **永不 settle**,进程会在事件循环排空后静默退出——没有报错、\n // 没有退出码线索,是最难排查的一类失败。必须同时盯着 close 事件。\n const closed = new Promise<null>((resolve) => rl.once('close', () => resolve(null)))\n answered.catch(() => {}) // 竞速落定后迟到的 reject 不该变成 unhandled\n const answer = await Promise.race([answered, closed])\n if (answer === null) { stream.write('\\n'); return false }\n return /^y(es)?$/i.test(String(answer).trim())\n } catch {\n // Ctrl+D 会让 readline 抛错。在确认提示上这是\"拒绝\",不是故障——\n // 冒到顶层只会变成一条 `错误: Aborted`,既吓人又没信息。\n // 这里返回 false 而不是 fallback:人明确中断了,不能当成默认同意。\n stream.write('\\n')\n return false\n } finally {\n rl.close()\n }\n}\n"]}