claude-coder 1.8.4 → 1.9.1

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 (78) hide show
  1. package/README.md +214 -167
  2. package/bin/cli.js +155 -172
  3. package/package.json +55 -53
  4. package/recipes/_shared/roles/developer.md +11 -11
  5. package/recipes/_shared/roles/product.md +12 -12
  6. package/recipes/_shared/roles/tester.md +12 -12
  7. package/recipes/_shared/test/report-format.md +86 -86
  8. package/recipes/backend/base.md +27 -27
  9. package/recipes/backend/components/auth.md +18 -18
  10. package/recipes/backend/components/crud-api.md +18 -18
  11. package/recipes/backend/components/file-service.md +15 -15
  12. package/recipes/backend/manifest.json +20 -20
  13. package/recipes/backend/test/api-test.md +25 -25
  14. package/recipes/console/base.md +37 -37
  15. package/recipes/console/components/modal-form.md +20 -20
  16. package/recipes/console/components/pagination.md +17 -17
  17. package/recipes/console/components/search.md +17 -17
  18. package/recipes/console/components/table-list.md +18 -18
  19. package/recipes/console/components/tabs.md +14 -14
  20. package/recipes/console/components/tree.md +15 -15
  21. package/recipes/console/components/upload.md +15 -15
  22. package/recipes/console/manifest.json +24 -24
  23. package/recipes/console/test/crud-e2e.md +47 -47
  24. package/recipes/h5/base.md +26 -26
  25. package/recipes/h5/components/animation.md +11 -11
  26. package/recipes/h5/components/countdown.md +11 -11
  27. package/recipes/h5/components/share.md +11 -11
  28. package/recipes/h5/components/swiper.md +11 -11
  29. package/recipes/h5/manifest.json +21 -21
  30. package/recipes/h5/test/h5-e2e.md +20 -20
  31. package/src/commands/auth.js +362 -290
  32. package/src/commands/setup-modules/helpers.js +100 -99
  33. package/src/commands/setup-modules/index.js +25 -25
  34. package/src/commands/setup-modules/mcp.js +115 -95
  35. package/src/commands/setup-modules/provider.js +260 -260
  36. package/src/commands/setup-modules/safety.js +47 -61
  37. package/src/commands/setup-modules/simplify.js +52 -52
  38. package/src/commands/setup.js +172 -172
  39. package/src/common/assets.js +245 -236
  40. package/src/common/config.js +125 -125
  41. package/src/common/constants.js +55 -55
  42. package/src/common/indicator.js +260 -222
  43. package/src/common/interaction.js +170 -170
  44. package/src/common/logging.js +77 -77
  45. package/src/common/sdk.js +50 -50
  46. package/src/common/tasks.js +88 -88
  47. package/src/common/utils.js +213 -161
  48. package/src/core/coding.js +33 -55
  49. package/src/core/go.js +264 -310
  50. package/src/core/hooks.js +500 -533
  51. package/src/core/init.js +166 -171
  52. package/src/core/plan.js +188 -325
  53. package/src/core/prompts.js +247 -227
  54. package/src/core/repair.js +36 -46
  55. package/src/core/runner.js +458 -195
  56. package/src/core/scan.js +93 -89
  57. package/src/core/session.js +271 -57
  58. package/src/core/simplify.js +74 -53
  59. package/src/core/state.js +105 -0
  60. package/src/index.js +76 -0
  61. package/templates/bash-process.md +12 -12
  62. package/templates/codingSystem.md +65 -65
  63. package/templates/codingUser.md +17 -17
  64. package/templates/coreProtocol.md +29 -29
  65. package/templates/goSystem.md +130 -130
  66. package/templates/guidance.json +72 -53
  67. package/templates/planSystem.md +78 -78
  68. package/templates/planUser.md +8 -8
  69. package/templates/requirements.example.md +57 -57
  70. package/templates/scanSystem.md +120 -120
  71. package/templates/scanUser.md +10 -10
  72. package/templates/test_rule.md +194 -194
  73. package/templates/web-testing.md +17 -0
  74. package/types/index.d.ts +217 -0
  75. package/src/core/context.js +0 -117
  76. package/src/core/harness.js +0 -484
  77. package/src/core/query.js +0 -50
  78. package/templates/playwright.md +0 -17
@@ -1,222 +1,260 @@
1
- 'use strict';
2
-
3
- const { COLOR } = require('./config');
4
- const { localTimestamp, truncatePath } = require('./utils');
5
-
6
- const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
7
-
8
- class Indicator {
9
- constructor() {
10
- this.phase = 'thinking';
11
- this.step = '';
12
- this.toolTarget = '';
13
- this.spinnerIndex = 0;
14
- this.timer = null;
15
- this.lastActivity = '';
16
- this.lastToolTime = Date.now();
17
- this.lastActivityTime = Date.now();
18
- this.sessionNum = 0;
19
- this.startTime = Date.now();
20
- this.stallTimeoutMin = 30;
21
- this.completionTimeoutMin = null;
22
- this.toolRunning = false;
23
- this.toolStartTime = 0;
24
- this.currentToolName = '';
25
- this._paused = false;
26
- }
27
-
28
- start(sessionNum, stallTimeoutMin) {
29
- this.sessionNum = sessionNum;
30
- this.startTime = Date.now();
31
- this.lastActivityTime = Date.now();
32
- if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
33
- this.timer = setInterval(() => this._render(), 1000);
34
- }
35
-
36
- stop() {
37
- if (this.timer) {
38
- clearInterval(this.timer);
39
- this.timer = null;
40
- }
41
- process.stderr.write('\r\x1b[K');
42
- }
43
-
44
- updatePhase(phase) {
45
- this.phase = phase;
46
- }
47
-
48
- updateStep(step) {
49
- this.step = step;
50
- }
51
-
52
- appendActivity(toolName, summary) {
53
- this.lastActivity = `${toolName}: ${summary}`;
54
- }
55
-
56
- setCompletionDetected(timeoutMin) {
57
- this.completionTimeoutMin = timeoutMin;
58
- }
59
-
60
- updateActivity() {
61
- this.lastActivityTime = Date.now();
62
- }
63
-
64
- startTool(name) {
65
- this.toolRunning = true;
66
- this.toolStartTime = Date.now();
67
- this.currentToolName = name;
68
- this.lastActivityTime = Date.now();
69
- }
70
-
71
- endTool() {
72
- if (!this.toolRunning) return;
73
- this.toolRunning = false;
74
- this.lastActivityTime = Date.now();
75
- }
76
-
77
- pauseRendering() { this._paused = true; }
78
- resumeRendering() { this._paused = false; }
79
-
80
- getStatusLine() {
81
- const clock = localTimestamp();
82
- const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
83
- const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
84
- const ss = String(elapsed % 60).padStart(2, '0');
85
- const spinner = SPINNERS[this.spinnerIndex % SPINNERS.length];
86
-
87
- const phaseLabel = this.phase === 'thinking'
88
- ? `${COLOR.yellow}思考中${COLOR.reset}`
89
- : `${COLOR.green}编码中${COLOR.reset}`;
90
-
91
- const idleMs = Date.now() - this.lastActivityTime;
92
- const idleMin = Math.floor(idleMs / 60000);
93
-
94
- let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
95
- if (idleMin >= 2) {
96
- if (this.toolRunning) {
97
- const toolSec = Math.floor((Date.now() - this.toolStartTime) / 1000);
98
- const toolMm = Math.floor(toolSec / 60);
99
- const toolSs = toolSec % 60;
100
- line += ` | ${COLOR.yellow}工具执行中 ${toolMm}:${String(toolSs).padStart(2, '0')}${COLOR.reset}`;
101
- } else if (this.completionTimeoutMin) {
102
- line += ` | ${COLOR.red}${idleMin}分无响应(session_result 已写入, ${this.completionTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
103
- } else {
104
- line += ` | ${COLOR.red}${idleMin}分无响应(等待模型响应, ${this.stallTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
105
- }
106
- }
107
- if (this.step) {
108
- line += ` | ${this.step}`;
109
- if (this.toolTarget) {
110
- // 动态获取终端宽度,默认 120 适配现代终端
111
- const cols = process.stderr.columns || 120;
112
- const usedWidth = line.replace(/\x1b\[[^m]*m/g, '').length;
113
- const availWidth = Math.max(20, cols - usedWidth - 4);
114
- const target = truncatePath(this.toolTarget, availWidth);
115
- line += `: ${target}`;
116
- }
117
- }
118
- return line;
119
- }
120
-
121
- _render() {
122
- if (this._paused) return;
123
- this.spinnerIndex++;
124
- process.stderr.write(`\r\x1b[K${this.getStatusLine()}`);
125
- }
126
- }
127
-
128
- function extractFileTarget(toolInput) {
129
- const raw = typeof toolInput === 'object'
130
- ? (toolInput.file_path || toolInput.path || '')
131
- : '';
132
- if (!raw) return '';
133
- return raw.split('/').slice(-2).join('/');
134
- }
135
-
136
- function extractBashLabel(cmd) {
137
- if (cmd.includes('git ')) return 'Git 操作';
138
- if (cmd.includes('npm ') || cmd.includes('pip ') || cmd.includes('pnpm ') || cmd.includes('yarn ')) return '安装依赖';
139
- if (/\b(sleep|Start-Sleep|timeout\s+\/t)\b/i.test(cmd)) return '等待就绪';
140
- if (cmd.includes('curl')) return '网络请求';
141
- if (cmd.includes('pytest') || cmd.includes('jest') || /\btest\b/.test(cmd)) return '测试验证';
142
- if (cmd.includes('python ') || cmd.includes('node ')) return '执行脚本';
143
- return '执行命令';
144
- }
145
-
146
- function extractMcpTarget(toolInput) {
147
- if (!toolInput || typeof toolInput !== 'object') return '';
148
- return String(toolInput.url || toolInput.text || toolInput.element || '').slice(0, 60);
149
- }
150
-
151
- /**
152
- * 提取 Bash 命令的主体部分(移除管道、重定向等)
153
- * 正确处理引号内的内容,不会错误分割引号内的分隔符
154
- */
155
- function extractBashTarget(cmd) {
156
- // 移除开头的 cd xxx && 部分
157
- let clean = cmd.replace(/^(?:cd\s+\S+\s*&&\s*)+/g, '').trim();
158
-
159
- // 临时替换引号内的分隔符为占位符
160
- const unescape = (s) => s.replace(/\x00/g, ';');
161
- clean = clean.replace(/"[^"]*"/g, m => m.replace(/[;|&]/g, '\x00'));
162
- clean = clean.replace(/'[^']*'/g, m => m.replace(/[;|&]/g, '\x00'));
163
-
164
- // 分割并取第一部分
165
- clean = clean.split(/\s*(?:\|\|?|;|&&|2>&1|2>\/dev\/null|>\s*\/dev\/null)\s*/)[0];
166
-
167
- // 还原占位符
168
- return unescape(clean).trim();
169
- }
170
-
171
- function inferPhaseStep(indicator, toolName, toolInput) {
172
- const name = (toolName || '').toLowerCase();
173
-
174
- indicator.startTool(toolName);
175
-
176
- if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
177
- indicator.updatePhase('coding');
178
- indicator.updateStep('编辑文件');
179
- indicator.toolTarget = extractFileTarget(toolInput);
180
- } else if (name === 'bash' || name === 'shell') {
181
- const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
182
- const label = extractBashLabel(cmd);
183
- indicator.updateStep(label);
184
- indicator.toolTarget = extractBashTarget(cmd);
185
- if (label === '测试验证' || label === '执行脚本' || label === '执行命令') {
186
- indicator.updatePhase('coding');
187
- }
188
- } else if (name === 'read' || name === 'glob' || name === 'grep' || name === 'ls') {
189
- indicator.updatePhase('thinking');
190
- indicator.updateStep('读取文件');
191
- indicator.toolTarget = extractFileTarget(toolInput);
192
- } else if (name === 'task') {
193
- indicator.updatePhase('thinking');
194
- indicator.updateStep('子 Agent 搜索');
195
- indicator.toolTarget = '';
196
- } else if (name === 'websearch' || name === 'webfetch') {
197
- indicator.updatePhase('thinking');
198
- indicator.updateStep('查阅文档');
199
- indicator.toolTarget = '';
200
- } else if (name.startsWith('mcp__')) {
201
- indicator.updatePhase('coding');
202
- const action = name.split('__').pop() || name;
203
- indicator.updateStep(`浏览器: ${action}`);
204
- indicator.toolTarget = extractMcpTarget(toolInput);
205
- } else {
206
- indicator.updateStep('工具调用');
207
- indicator.toolTarget = '';
208
- }
209
-
210
- let summary;
211
- if (typeof toolInput === 'object') {
212
- const target = toolInput.file_path || toolInput.path || '';
213
- const cmd = toolInput.command || '';
214
- const pattern = toolInput.pattern || '';
215
- summary = target || (cmd ? cmd.slice(0, 200) : '') || (pattern ? `pattern: ${pattern}` : JSON.stringify(toolInput).slice(0, 200));
216
- } else {
217
- summary = String(toolInput || '').slice(0, 200);
218
- }
219
- indicator.appendActivity(toolName, summary);
220
- }
221
-
222
- module.exports = { Indicator, inferPhaseStep };
1
+ 'use strict';
2
+
3
+ const { COLOR } = require('./config');
4
+ const { localTimestamp, truncateMiddle } = require('./utils');
5
+
6
+ const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
7
+
8
+ function termCols() {
9
+ return process.stderr.columns
10
+ || process.stdout.columns
11
+ || parseInt(process.env.COLUMNS, 10)
12
+ || 70;
13
+ }
14
+
15
+ class Indicator {
16
+ constructor() {
17
+ this.phase = 'thinking';
18
+ this.spinnerIndex = 0;
19
+ this.timer = null;
20
+ this.lastActivityTime = Date.now();
21
+ this.sessionNum = 0;
22
+ this.startTime = Date.now();
23
+ this.stallTimeoutMin = 30;
24
+ this.toolRunning = false;
25
+ this.toolStartTime = 0;
26
+ this._paused = false;
27
+ this.projectRoot = '';
28
+ }
29
+
30
+ start(sessionNum, stallTimeoutMin, projectRoot) {
31
+ this.sessionNum = sessionNum;
32
+ this.startTime = Date.now();
33
+ this.lastActivityTime = Date.now();
34
+ if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
35
+ if (projectRoot) this.projectRoot = projectRoot;
36
+ this.timer = setInterval(() => this._render(), 1000);
37
+ }
38
+
39
+ stop() {
40
+ if (this.timer) { clearInterval(this.timer); this.timer = null; }
41
+ process.stderr.write('\r\x1b[K');
42
+ }
43
+
44
+ updatePhase(phase) { this.phase = phase; }
45
+ updateActivity() { this.lastActivityTime = Date.now(); }
46
+
47
+ startTool() {
48
+ this.toolRunning = true;
49
+ this.toolStartTime = Date.now();
50
+ this.lastActivityTime = Date.now();
51
+ }
52
+
53
+ endTool() {
54
+ if (!this.toolRunning) return;
55
+ this.toolRunning = false;
56
+ this.lastActivityTime = Date.now();
57
+ }
58
+
59
+ pauseRendering() { this._paused = true; }
60
+ resumeRendering() { this._paused = false; }
61
+
62
+ _render() {
63
+ if (this._paused) return;
64
+ this.spinnerIndex++;
65
+ const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
66
+ const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
67
+ const ss = String(elapsed % 60).padStart(2, '0');
68
+ const spinner = SPINNERS[this.spinnerIndex % SPINNERS.length];
69
+ const phaseLabel = this.phase === 'thinking'
70
+ ? `${COLOR.yellow}思考中${COLOR.reset}`
71
+ : `${COLOR.green}编码中${COLOR.reset}`;
72
+
73
+ const idleMs = Date.now() - this.lastActivityTime;
74
+ const idleMin = Math.floor(idleMs / 60000);
75
+
76
+ let line = `${spinner} S${this.sessionNum} ${mm}:${ss} ${phaseLabel}`;
77
+ if (idleMin >= 2) {
78
+ if (this.toolRunning) {
79
+ const sec = Math.floor((Date.now() - this.toolStartTime) / 1000);
80
+ line += ` ${COLOR.yellow}工具执行中 ${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}${COLOR.reset}`;
81
+ } else {
82
+ line += ` ${COLOR.red}${idleMin}分无响应${COLOR.reset}`;
83
+ }
84
+ }
85
+ process.stderr.write(`\r\x1b[K${line}`);
86
+ }
87
+ }
88
+
89
+ // ─── Path helpers ────────────────────────────────────────
90
+
91
+ function normalizePath(raw, projectRoot) {
92
+ if (!raw) return '';
93
+ if (projectRoot && raw.startsWith(projectRoot)) {
94
+ const rel = raw.slice(projectRoot.length);
95
+ return (rel[0] === '/' || rel[0] === '\\') ? rel.slice(1) : rel;
96
+ }
97
+ const home = process.env.HOME || process.env.USERPROFILE || '';
98
+ if (home && raw.startsWith(home)) return '~' + raw.slice(home.length);
99
+ const parts = raw.split(/[/\\]/).filter(Boolean);
100
+ return parts.length > 3 ? '.../' + parts.slice(-3).join('/') : raw;
101
+ }
102
+
103
+ function stripAbsolutePaths(str, projectRoot) {
104
+ let result = str;
105
+ if (projectRoot) {
106
+ result = result.replace(new RegExp(projectRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[/\\\\]?', 'g'), './');
107
+ }
108
+ const home = process.env.HOME || process.env.USERPROFILE || '';
109
+ if (home) {
110
+ result = result.replace(new RegExp(home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[/\\\\]?', 'g'), '~/');
111
+ }
112
+ return result;
113
+ }
114
+
115
+ function extractTarget(input, projectRoot) {
116
+ if (!input || typeof input !== 'object') return '';
117
+ const filePath = input.file_path || input.path || '';
118
+ if (filePath) return normalizePath(filePath, projectRoot);
119
+ const cmd = input.command || '';
120
+ if (cmd) return stripAbsolutePaths(extractBashCore(cmd), projectRoot);
121
+ const pattern = input.pattern || '';
122
+ if (pattern) return `pattern: ${pattern}`;
123
+ return '';
124
+ }
125
+
126
+ // ─── Bash helpers ────────────────────────────────────────
127
+
128
+ function extractBashLabel(cmd) {
129
+ if (cmd.includes('git ')) return 'Git';
130
+ if (/\b(npm|pnpm|yarn|pip)\b/.test(cmd)) return cmd.match(/\b(npm|pnpm|yarn|pip)\b/)[0];
131
+ if (/\b(sleep|Start-Sleep|timeout\s+\/t)\b/i.test(cmd)) return '等待';
132
+ if (cmd.includes('curl')) return '网络';
133
+ if (/\b(pytest|jest|test)\b/.test(cmd)) return '测试';
134
+ if (/\b(python|node)\s/.test(cmd)) return '执行';
135
+ return '执行';
136
+ }
137
+
138
+ function extractCurlUrl(cmd) {
139
+ const m = cmd.match(/https?:\/\/\S+/);
140
+ return m ? m[0].replace(/['";)}\]>]+$/, '') : null;
141
+ }
142
+
143
+ function extractBashCore(cmd) {
144
+ let clean = cmd.replace(/^(?:(?:cd|source|export)\s+\S+\s*&&\s*)+/g, '').trim();
145
+ clean = clean.replace(/"[^"]*"/g, m => m.replace(/[;|&]/g, '\x00'));
146
+ clean = clean.replace(/'[^']*'/g, m => m.replace(/[;|&]/g, '\x00'));
147
+ clean = clean.split(/\s*(?:\|\|?|;|&&|2>&1|2>\/dev\/null|>\s*\/dev\/null)\s*/)[0];
148
+ clean = clean.replace(/\x00/g, ';').trim();
149
+ clean = clean.replace(/\s*<<\s*['"]?\w+['"]?\s*$/, '');
150
+ return clean;
151
+ }
152
+
153
+ // ─── inferPhaseStep: 输出永久工具行 ─────────────────────
154
+
155
+ function formatElapsed(indicator) {
156
+ const elapsed = Math.floor((Date.now() - indicator.startTime) / 1000);
157
+ const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
158
+ const ss = String(elapsed % 60).padStart(2, '0');
159
+ return `${mm}:${ss}`;
160
+ }
161
+
162
+ const CODING_TOOLS = /^(write|edit|multiedit|str_replace_editor|strreplace)$/;
163
+ const READ_TOOLS = /^(read|glob|grep|ls)$/;
164
+
165
+ function inferPhaseStep(indicator, toolName, toolInput) {
166
+ const name = (toolName || '').toLowerCase();
167
+ const displayName = toolName || name;
168
+ const pr = indicator.projectRoot || '';
169
+ const cols = termCols();
170
+
171
+ indicator.startTool();
172
+
173
+ let step, target;
174
+
175
+ if (CODING_TOOLS.test(name)) {
176
+ indicator.updatePhase('coding');
177
+ step = displayName;
178
+ target = normalizePath(
179
+ (typeof toolInput === 'object' ? (toolInput.file_path || toolInput.path || '') : ''), pr
180
+ );
181
+ } else if (name === 'bash' || name === 'shell') {
182
+ const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
183
+ const label = extractBashLabel(cmd);
184
+ step = displayName;
185
+ const url = (label === '网络') ? extractCurlUrl(cmd) : null;
186
+ target = url || stripAbsolutePaths(extractBashCore(cmd), pr);
187
+ if (['测试', '执行'].includes(label)) indicator.updatePhase('coding');
188
+ } else if (READ_TOOLS.test(name)) {
189
+ indicator.updatePhase('thinking');
190
+ step = displayName;
191
+ target = extractTarget(toolInput, pr);
192
+ } else if (name === 'task') {
193
+ indicator.updatePhase('thinking');
194
+ step = displayName;
195
+ target = '';
196
+ } else if (name === 'websearch' || name === 'webfetch') {
197
+ indicator.updatePhase('thinking');
198
+ step = displayName;
199
+ target = '';
200
+ } else if (name.startsWith('mcp__')) {
201
+ indicator.updatePhase('coding');
202
+ step = name.split('__').pop() || displayName;
203
+ target = typeof toolInput === 'object'
204
+ ? String(toolInput.url || toolInput.text || toolInput.element || '').slice(0, 60)
205
+ : '';
206
+ } else {
207
+ step = displayName;
208
+ target = '';
209
+ }
210
+
211
+ const time = localTimestamp();
212
+ const el = formatElapsed(indicator);
213
+ let line = ` ${COLOR.dim}${time}${COLOR.reset} ${COLOR.dim}${el}${COLOR.reset} ${step}`;
214
+ if (target) {
215
+ const maxTarget = Math.max(10, cols - displayWidth(stripAnsi(line)) - 3);
216
+ line += ` ${truncateMiddle(target, maxTarget)}`;
217
+ }
218
+ process.stderr.write(`\r\x1b[K${clampLine(line, cols)}\n`);
219
+ }
220
+
221
+ // ─── Terminal width helpers ──────────────────────────────
222
+
223
+ function stripAnsi(str) {
224
+ return str.replace(/\x1b\[[^m]*m/g, '');
225
+ }
226
+
227
+ function isWideChar(cp) {
228
+ return (cp >= 0x4E00 && cp <= 0x9FFF)
229
+ || (cp >= 0x3400 && cp <= 0x4DBF)
230
+ || (cp >= 0x3000 && cp <= 0x30FF)
231
+ || (cp >= 0xF900 && cp <= 0xFAFF)
232
+ || (cp >= 0xFF01 && cp <= 0xFF60)
233
+ || (cp >= 0xFFE0 && cp <= 0xFFE6)
234
+ || (cp >= 0xAC00 && cp <= 0xD7AF)
235
+ || (cp >= 0x20000 && cp <= 0x2FA1F);
236
+ }
237
+
238
+ function displayWidth(str) {
239
+ let w = 0;
240
+ for (const ch of str) {
241
+ w += isWideChar(ch.codePointAt(0)) ? 2 : 1;
242
+ }
243
+ return w;
244
+ }
245
+
246
+ function clampLine(line, cols) {
247
+ const max = cols - 1;
248
+ if (displayWidth(stripAnsi(line)) <= max) return line;
249
+ let w = 0, cut = 0, esc = false;
250
+ for (let i = 0; i < line.length; i++) {
251
+ if (line[i] === '\x1b') esc = true;
252
+ if (esc) { if (line[i] === 'm') esc = false; continue; }
253
+ const cw = isWideChar(line.codePointAt(i)) ? 2 : 1;
254
+ if (w + cw >= max) { cut = i; break; }
255
+ w += cw;
256
+ }
257
+ return line.slice(0, cut) + '…' + COLOR.reset;
258
+ }
259
+
260
+ module.exports = { Indicator, inferPhaseStep };