@jspg-ai/coding-bb 0.0.2-beta.28 → 0.0.2-beta.29

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.
@@ -23,6 +23,21 @@ const yaml = require('js-yaml');
23
23
  // 异步执行外部命令:长耗时 git 操作不阻塞事件循环,进度行才能持续刷新
24
24
  const execFileAsync = promisify(execFile);
25
25
 
26
+ /**
27
+ * 跨平台异步执行命令(不阻塞事件循环)。
28
+ * Windows 上 npm / openspec 均为 .cmd:Node 直接 spawn .cmd 会报 EINVAL,
29
+ * `shell: true` 又会触发 DEP0190 弃用警告;故统一经 cmd.exe /d /s /c 执行。
30
+ * 仅用于本文件内常量命令(不接受用户输入拼接)。
31
+ */
32
+ function execCmd(file, args, opts = {}) {
33
+ if (process.platform === 'win32') {
34
+ const line = [file, ...args].join(' ');
35
+ return execFileAsync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', line],
36
+ { windowsHide: true, ...opts });
37
+ }
38
+ return execFileAsync(file, args, opts);
39
+ }
40
+
26
41
  const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
27
42
 
28
43
  // 目标项目根目录:默认取当前 cwd,init() / initWorkspace() 可在调用前覆盖。
@@ -211,7 +226,7 @@ function ask(question) {
211
226
  output: process.stdout,
212
227
  });
213
228
  return new Promise(resolve => {
214
- rl.question(question, answer => {
229
+ rl.question(' ' + question, answer => {
215
230
  rl.close();
216
231
  resolve(answer.trim());
217
232
  });
@@ -623,22 +638,29 @@ const OPENSPEC_REGISTRY = 'https://registry.npmmirror.com';
623
638
  /**
624
639
  * 确保 OpenSpec CLI 已安装且为最新版本。
625
640
  * 未安装或版本非最新时自动执行 npm install -g,无需用户确认。
641
+ * 命令异步执行(execFile)避免阻塞事件循环——否则长耗时的 npm 查询/安装
642
+ * 会让进度行冻结、看起来像卡死;传入 onProgress 时由调用方渲染实时进度,
643
+ * 否则退化为安装/升级前的一行灰字提示。
626
644
  */
627
- async function ensureOpenSpecCli() {
645
+ async function ensureOpenSpecCli(onProgress = null) {
646
+ const report = text => { if (onProgress) onProgress(text); };
647
+
628
648
  // 1. 获取当前版本(未安装时 currentVersion = null)
629
649
  let currentVersion = null;
650
+ report('检查 OpenSpec CLI');
630
651
  try {
631
- currentVersion = execSync('openspec --version', { stdio: 'pipe', timeout: 10000 })
632
- .toString().trim().replace(/^v/, '');
652
+ const { stdout } = await execCmd('openspec', ['--version'], { timeout: 10000 });
653
+ currentVersion = stdout.toString().trim().replace(/^v/, '');
633
654
  } catch (_) {}
634
655
 
635
656
  // 2. 查询最新版本(网络失败则跳过检测,视为无法判断)
636
657
  let latestVersion = null;
637
658
  try {
638
- latestVersion = execSync(
639
- `npm view ${OPENSPEC_PKG} version --registry ${OPENSPEC_REGISTRY}`,
640
- { stdio: 'pipe', timeout: 15000 }
641
- ).toString().trim();
659
+ const { stdout } = await execCmd(
660
+ 'npm', ['view', OPENSPEC_PKG, 'version', '--registry', OPENSPEC_REGISTRY],
661
+ { timeout: 15000 }
662
+ );
663
+ latestVersion = stdout.toString().trim();
642
664
  } catch (_) {}
643
665
 
644
666
  // 3. 判断是否需要安装/升级
@@ -650,17 +672,20 @@ async function ensureOpenSpecCli() {
650
672
  return { state: 'ok', version: currentVersion };
651
673
  }
652
674
 
653
- // 4. 执行安装/升级(耗时操作,打印进行中提示)
675
+ // 4. 执行安装/升级(耗时操作:由调用方进度行或灰字提示覆盖全程)
654
676
  const action = needInstall ? '安装' : '升级';
655
677
  const detail = needInstall
656
678
  ? `→ v${latestVersion || 'latest'}`
657
679
  : `v${currentVersion} → v${latestVersion}`;
658
- console.log(` ${c('gray', '⏳')} ${c('gray', `${action} OpenSpec CLI(${detail})...`)}`);
680
+ report(`${action} OpenSpec CLI(${detail})`);
681
+ if (!onProgress) {
682
+ console.log(` ${c('gray', '⏳')} ${c('gray', `${action} OpenSpec CLI(${detail})...`)}`);
683
+ }
659
684
 
660
685
  try {
661
- execSync(
662
- `npm install -g ${OPENSPEC_PKG}@latest --registry ${OPENSPEC_REGISTRY}`,
663
- { stdio: 'pipe', timeout: 120000 }
686
+ await execCmd(
687
+ 'npm', ['install', '-g', `${OPENSPEC_PKG}@latest`, '--registry', OPENSPEC_REGISTRY],
688
+ { timeout: 120000 }
664
689
  );
665
690
  return { state: needInstall ? 'installed' : 'upgraded', version: latestVersion || currentVersion };
666
691
  } catch (err) {
@@ -799,7 +824,12 @@ async function init(targetDir, options = {}) {
799
824
  const selectedAdapters = isNonInteractive ? adapters : await selectTools(isUpgrade);
800
825
 
801
826
  // 前置依赖检测(放在选适配器之后,只检测与所选工具相关的依赖)
802
- await ensureOpenSpecCli();
827
+ const depProgress = progress('依赖');
828
+ try {
829
+ await ensureOpenSpecCli(t => depProgress.update(t));
830
+ } finally {
831
+ depProgress.done();
832
+ }
803
833
 
804
834
  // 分发核心规则到各工具目录
805
835
  console.log(`\n 📦 ${action}核心编码规范...`);
@@ -943,7 +973,7 @@ async function init(targetDir, options = {}) {
943
973
  if (isUpgrade) {
944
974
  const oldPaths = pathsToDelete(managedPaths, newPaths);
945
975
  if (oldPaths.length > 0) {
946
- section('🧹', `清理上一版本残留(${oldPaths.length} 个)`);
976
+ section('!', `清理上一版本残留(${oldPaths.length} 个)`);
947
977
  // 先全部删除
948
978
  oldPaths.forEach(oldPath => {
949
979
  const targetPath = path.join(TARGET_DIR, oldPath);
@@ -959,7 +989,7 @@ async function init(targetDir, options = {}) {
959
989
 
960
990
  const kept = protectedKeptPaths(managedPaths, newPaths);
961
991
  if (kept.length > 0) {
962
- section('🛡️', '保留用户文档');
992
+ section('!', '保留用户文档');
963
993
  kept.forEach(k => {
964
994
  log(` ${k}(本包已不再管理,文件可能含用户内容,未删除)`, 'warn');
965
995
  });
@@ -1058,8 +1088,6 @@ async function installWorkspaceConfig(targetDir, options = {}) {
1058
1088
  selectedAdapters = await selectTools(isUpgrade);
1059
1089
  }
1060
1090
  }
1061
- const openspecCli = await ensureOpenSpecCli();
1062
-
1063
1091
  // 只装 worktree 生命周期管理 Skills(init/close/push)
1064
1092
  const skillsAdapters = selectedAdapters.filter(a => a.skillsDir);
1065
1093
  const commandsAdapters = selectedAdapters.filter(a => a.worktreeCommandsDir);
@@ -1067,48 +1095,63 @@ async function installWorkspaceConfig(targetDir, options = {}) {
1067
1095
  let cmdCount = 0;
1068
1096
  let adapterRoots = [];
1069
1097
 
1070
- if (skillsAdapters.length > 0) {
1071
- const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
1072
- const osExtFilter = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
1073
- skillsAdapters.forEach(adapter => {
1074
- const r = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, osExtFilter);
1075
- if (r.count > 0) {
1076
- skillCount = Math.max(skillCount, r.count);
1077
- r.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
1078
- }
1079
- });
1080
- adapterRoots = skillsAdapters.map(a => a.skillsDir.split('/')[0]);
1081
- }
1098
+ // 「AI 配置」阶段心跳:检查依赖 部署 skills/commands → 写入 openspec 配置 → 生成导航。
1099
+ // 其中 npm 查询/安装最长可达分钟级,必须让用户看到进程仍活着;任何异常先清掉进度行。
1100
+ const cfgProgress = progress('AI 配置');
1101
+ let agentsMdState = 'exists';
1102
+ let openspecRes;
1103
+ try {
1104
+ await ensureOpenSpecCli(t => cfgProgress.update(t));
1082
1105
 
1083
- // 只装 worktree 管理 Commands(cbb:worktree-init/close/push)
1084
- if (commandsAdapters.length > 0) {
1085
- const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
1086
- const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
1087
- commandsAdapters.forEach(adapter => {
1088
- const r = deployWorktreeCommands(adapter, commandsExtendsSrc, cmdFilter);
1089
- if (r.count > 0) {
1090
- cmdCount = Math.max(cmdCount, r.count);
1091
- r.names.forEach(name => newPaths.add(`${adapter.worktreeCommandsDir}/${name}`));
1092
- }
1093
- });
1094
- if (adapterRoots.length === 0) adapterRoots = commandsAdapters.map(a => a.worktreeCommandsDir.split('/')[0]);
1095
- }
1106
+ if (skillsAdapters.length > 0) {
1107
+ cfgProgress.update('部署 skills');
1108
+ const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
1109
+ const osExtFilter = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
1110
+ skillsAdapters.forEach(adapter => {
1111
+ const r = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, osExtFilter);
1112
+ if (r.count > 0) {
1113
+ skillCount = Math.max(skillCount, r.count);
1114
+ r.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
1115
+ }
1116
+ });
1117
+ adapterRoots = skillsAdapters.map(a => a.skillsDir.split('/')[0]);
1118
+ }
1096
1119
 
1097
- // 设置 OpenSpec 工作流(config.yaml + schemas
1098
- const schemasSrcs = [
1099
- path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
1100
- ];
1101
- schemasSrcs.forEach(src => {
1102
- if (fs.existsSync(src)) {
1103
- fs.readdirSync(src).filter(f2 => fs.statSync(path.join(src, f2)).isDirectory())
1104
- .forEach(name => newPaths.add(`openspec/schemas/${name}`));
1120
+ // 只装 worktree 管理 Commands(cbb:worktree-init/close/push
1121
+ if (commandsAdapters.length > 0) {
1122
+ cfgProgress.update('部署 worktree 命令');
1123
+ const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
1124
+ const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
1125
+ commandsAdapters.forEach(adapter => {
1126
+ const r = deployWorktreeCommands(adapter, commandsExtendsSrc, cmdFilter);
1127
+ if (r.count > 0) {
1128
+ cmdCount = Math.max(cmdCount, r.count);
1129
+ r.names.forEach(name => newPaths.add(`${adapter.worktreeCommandsDir}/${name}`));
1130
+ }
1131
+ });
1132
+ if (adapterRoots.length === 0) adapterRoots = commandsAdapters.map(a => a.worktreeCommandsDir.split('/')[0]);
1105
1133
  }
1106
- });
1107
- const openspecRes = setupOpenSpec(isUpgrade, managedPaths);
1108
- newPaths.add('openspec/config.yaml');
1109
1134
 
1110
- // 空间导航文件 AGENTS.md:仅缺失时生成,永不覆盖(不登记清单,卸载不删)
1111
- const agentsMdState = workspaces.ensureWorkspaceAgentsMd(TARGET_DIR);
1135
+ // 设置 OpenSpec 工作流(config.yaml + schemas)
1136
+ cfgProgress.update('写入 openspec 配置');
1137
+ const schemasSrcs = [
1138
+ path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
1139
+ ];
1140
+ schemasSrcs.forEach(src => {
1141
+ if (fs.existsSync(src)) {
1142
+ fs.readdirSync(src).filter(f2 => fs.statSync(path.join(src, f2)).isDirectory())
1143
+ .forEach(name => newPaths.add(`openspec/schemas/${name}`));
1144
+ }
1145
+ });
1146
+ openspecRes = setupOpenSpec(isUpgrade, managedPaths);
1147
+ newPaths.add('openspec/config.yaml');
1148
+
1149
+ // 空间导航文件 AGENTS.md:仅缺失时生成,永不覆盖(不登记清单,卸载不删)
1150
+ cfgProgress.update('生成空间导航');
1151
+ agentsMdState = workspaces.ensureWorkspaceAgentsMd(TARGET_DIR);
1152
+ } finally {
1153
+ cfgProgress.done();
1154
+ }
1112
1155
 
1113
1156
  // 升级时清理旧路径(workspace 曾装过的 base 工作流等不属于此子集,删除)
1114
1157
  const cleaned = [];
@@ -1149,11 +1192,11 @@ async function installWorkspaceConfig(targetDir, options = {}) {
1149
1192
  if (cleaned.length > 0) {
1150
1193
  const MAX_DISPLAY = 3;
1151
1194
  const preview = cleaned.slice(0, MAX_DISPLAY).join('、');
1152
- step('🧹', '清理残留', `${cleaned.length} 个(${preview}${cleaned.length > MAX_DISPLAY ? ' 等' : ''})`,
1195
+ step('!', '清理残留', `${cleaned.length} 个(${preview}${cleaned.length > MAX_DISPLAY ? ' 等' : ''})`,
1153
1196
  { iconColor: 'yellow' });
1154
1197
  }
1155
1198
  if (keptDocs.length > 0) {
1156
- step('🛡️', '保留文档', `${keptDocs.join('、')}(本包已不再管理)`, { iconColor: 'yellow' });
1199
+ step('!', '保留文档', `${keptDocs.join('、')}(本包已不再管理)`, { iconColor: 'yellow' });
1157
1200
  }
1158
1201
  }
1159
1202
 
@@ -1341,9 +1384,8 @@ async function syncWorkspaceRepo(targetDir, onProgress = () => {}) {
1341
1384
  if (currentBranch !== defaultBranch) {
1342
1385
  log(`本地当前分支 ${currentBranch} ≠ 远程默认分支 ${defaultBranch},正在切换...`, 'warn');
1343
1386
  try {
1344
- execSync(`git checkout -q "${defaultBranch}"`, {
1387
+ await execFileAsync('git', ['checkout', '-q', defaultBranch], {
1345
1388
  cwd: targetDir,
1346
- stdio: 'pipe',
1347
1389
  timeout: 30000,
1348
1390
  });
1349
1391
  } catch (e) {
@@ -1355,9 +1397,8 @@ async function syncWorkspaceRepo(targetDir, onProgress = () => {}) {
1355
1397
 
1356
1398
  // 4. fast-forward merge 到 origin/<默认分支>
1357
1399
  try {
1358
- execSync(`git merge --ff-only -q "origin/${defaultBranch}"`, {
1400
+ await execFileAsync('git', ['merge', '--ff-only', '-q', `origin/${defaultBranch}`], {
1359
1401
  cwd: targetDir,
1360
- stdio: 'pipe',
1361
1402
  timeout: 60000,
1362
1403
  });
1363
1404
  } catch (e) {
@@ -1515,7 +1556,7 @@ function printBatchSummary(results) {
1515
1556
  }
1516
1557
  step('!', '完成', `成功 ${ok.length}/${results.length},失败项见下`, { iconColor: 'yellow', valueColor: 'yellow' });
1517
1558
  failed.forEach(r => {
1518
- step('✗', r.w.name, describeBatchError(r), { iconColor: 'red', labelColor: 'red', valueColor: 'red' });
1559
+ step('✗', '失败', `${r.w.name}:${describeBatchError(r)}`, { iconColor: 'red', labelColor: 'red', valueColor: 'red' });
1519
1560
  });
1520
1561
  }
1521
1562
 
@@ -1609,7 +1650,6 @@ async function setupWorkspace() {
1609
1650
  const result = await initSingleWorkspace(w, { selectedAdapters });
1610
1651
  results.push({ w, ...result });
1611
1652
  }
1612
- console.log('');
1613
1653
  printBatchSummary(results);
1614
1654
  }
1615
1655
 
@@ -1620,11 +1660,11 @@ function printSetupSummary(selected, selectedAdapters, parentDir) {
1620
1660
  console.log(' ' + c('bold', 'cbb setup') + ' ' + c('gray', `v${pkg.version}`));
1621
1661
  console.log('');
1622
1662
  selected.forEach(w => {
1623
- console.log(` ${c('yellow', '[' + w.businessLine + ']')} ${w.name}`);
1624
- console.log(` ${c('gray', w.repo)}`);
1663
+ console.log(` ${c('yellow', '[' + w.businessLine + ']')} ${w.name}`);
1664
+ console.log(` ${c('gray', w.repo)}`);
1625
1665
  });
1626
- console.log(' ' + c('gray', padVisual('目标目录', 10)) + ' ' + c('cyan', parentDir));
1627
- console.log(' ' + c('gray', padVisual('AI 工具', 10)) + ' ' + selectedAdapters.map(a => a.displayName).join('、'));
1666
+ console.log(' ' + c('gray', padVisual('目标目录', 10)) + ' ' + c('cyan', parentDir));
1667
+ console.log(' ' + c('gray', padVisual('AI 工具', 10)) + ' ' + selectedAdapters.map(a => a.displayName).join('、'));
1628
1668
  }
1629
1669
 
1630
1670
  // ── 用户级组件 ────────────────────────────────────────────
@@ -1822,7 +1862,7 @@ async function uninstall() {
1822
1862
  const managedPaths = readManifest();
1823
1863
 
1824
1864
  // 确认卸载
1825
- console.log('\n🗑 卸载 @jspg-ai/coding-bb...\n');
1865
+ console.log('\n 🗑 卸载 @jspg-ai/coding-bb...\n');
1826
1866
  const answer = await ask('确认卸载?此操作将移除本包管理的所有文件 (y/N): ');
1827
1867
  if (answer.toLowerCase() !== 'y') {
1828
1868
  log('已取消卸载');
@@ -87,8 +87,8 @@ function renderLines(state) {
87
87
  const lines = [];
88
88
 
89
89
  if (header) {
90
- lines.push(c('bold', header));
91
- lines.push(c('gray', '─'.repeat(48)));
90
+ lines.push(' ' + c('bold', header));
91
+ lines.push(' ' + c('gray', '─'.repeat(48)));
92
92
  }
93
93
 
94
94
  if (items.length === 0) {
@@ -99,21 +99,21 @@ function renderLines(state) {
99
99
  const isSelected = selected.has(i);
100
100
  const cursorMark = isCursor ? c('cyan', '❯') : ' ';
101
101
  const checkMark = isSelected ? c('green', '☑') : c('gray', '☐');
102
- const raw = ` ${cursorMark} ${checkMark} ${item.label}`;
102
+ const raw = ` ${cursorMark} ${checkMark} ${item.label}`;
103
103
  // 当前行整体加粗 + 反转视频:浅色终端也能识别
104
104
  const line = isCursor && COLOR_ENABLED
105
105
  ? `${COLOR.bold}${COLOR.reverse}${raw}${COLOR.reset}`
106
106
  : (isCursor ? c('bold', raw) : raw);
107
107
  lines.push(line);
108
108
  if (item.hint) {
109
- lines.push(c('gray', ' ' + item.hint));
109
+ lines.push(c('gray', ' ' + item.hint));
110
110
  }
111
111
  });
112
112
  }
113
113
 
114
114
  if (footer) {
115
115
  lines.push('');
116
- lines.push(c('gray', footer));
116
+ lines.push(' ' + c('gray', footer));
117
117
  }
118
118
 
119
119
  return lines;
@@ -325,21 +325,21 @@ function promptCheckboxFallback({ items, header, footer, preSelected }) {
325
325
  };
326
326
 
327
327
  // 输出列表(无光标、无高亮,逐项标号)
328
- console.log(c('bold', header) || '');
329
- console.log(c('gray', '─'.repeat(48)));
328
+ console.log(' ' + (c('bold', header) || ''));
329
+ console.log(' ' + c('gray', '─'.repeat(48)));
330
330
  items.forEach((item, i) => {
331
- console.log(` ${c('cyan', String(i + 1) + '.')} ${item.label}`);
332
- if (item.hint) console.log(c('gray', ' ' + item.hint));
331
+ console.log(` ${c('cyan', String(i + 1) + '.')} ${item.label}`);
332
+ if (item.hint) console.log(c('gray', ' ' + item.hint));
333
333
  });
334
334
  console.log('');
335
- if (footer) console.log(c('gray', footer));
335
+ if (footer) console.log(' ' + c('gray', footer));
336
336
 
337
337
  const rl = readline.createInterface({
338
338
  input: process.stdin,
339
339
  output: process.stdout,
340
340
  });
341
341
 
342
- rl.question(`请输入编号(多选用逗号分隔): `, (answer) => {
342
+ rl.question(` 请输入编号(多选用逗号分隔): `, (answer) => {
343
343
  rl.close();
344
344
  const selected = parseBulkInput(answer, items.length);
345
345
  resolve(selected);
@@ -37,34 +37,51 @@ const LOG_STYLES = {
37
37
 
38
38
  function log(msg, type = 'info') {
39
39
  const s = LOG_STYLES[type] || LOG_STYLES.info;
40
- const text = `${s.icon} ${msg}`;
40
+ const text = ` ${s.icon} ${msg}`;
41
41
  console.log(s.color ? c(s.color, text) : text);
42
42
  }
43
43
 
44
44
  /** 输出一行分节标题(带图标 + 颜色),紧跟一行灰色分隔线 */
45
45
  function section(icon, title) {
46
46
  console.log('');
47
- console.log(c('bold', `${icon} ${title}`));
48
- console.log(c('gray', '─'.repeat(48)));
47
+ console.log(' ' + c('bold', `${icon} ${title}`));
48
+ console.log(' ' + c('gray', '─'.repeat(48)));
49
+ }
50
+
51
+ /** 单字符视觉宽度:CJK / 全角按 2 列计,其余按 1 列 */
52
+ function charWidth(ch) {
53
+ const cp = ch.codePointAt(0);
54
+ const wide =
55
+ (cp >= 0x1100 && cp <= 0x115f) ||
56
+ cp === 0x2329 || cp === 0x232a ||
57
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
58
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
59
+ (cp >= 0xf900 && cp <= 0xfaff) ||
60
+ (cp >= 0xfe30 && cp <= 0xfe6f) ||
61
+ (cp >= 0xff00 && cp <= 0xff60) ||
62
+ (cp >= 0xffe0 && cp <= 0xffe6);
63
+ return wide ? 2 : 1;
49
64
  }
50
65
 
51
66
  /** 视觉宽度:CJK / 全角字符按 2 列计,其余按 1 列(终端对齐用) */
52
67
  function visualWidth(text) {
53
68
  let w = 0;
69
+ for (const ch of String(text)) w += charWidth(ch);
70
+ return w;
71
+ }
72
+
73
+ /** 按视觉宽度截断文本(超出补 …),用于防止进度行折行残留 */
74
+ function sliceVisual(text, maxWidth) {
75
+ if (visualWidth(text) <= maxWidth) return text;
76
+ let w = 0;
77
+ let out = '';
54
78
  for (const ch of String(text)) {
55
- const cp = ch.codePointAt(0);
56
- const wide =
57
- (cp >= 0x1100 && cp <= 0x115f) ||
58
- cp === 0x2329 || cp === 0x232a ||
59
- (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
60
- (cp >= 0xac00 && cp <= 0xd7a3) ||
61
- (cp >= 0xf900 && cp <= 0xfaff) ||
62
- (cp >= 0xfe30 && cp <= 0xfe6f) ||
63
- (cp >= 0xff00 && cp <= 0xff60) ||
64
- (cp >= 0xffe0 && cp <= 0xffe6);
65
- w += wide ? 2 : 1;
79
+ const cw = charWidth(ch);
80
+ if (w + cw > maxWidth - 1) return out + '…';
81
+ out += ch;
82
+ w += cw;
66
83
  }
67
- return w;
84
+ return out;
68
85
  }
69
86
 
70
87
  /** 按视觉宽度右补空格(中文标签对齐用) */
@@ -96,7 +113,11 @@ function progress(label, opts = {}) {
96
113
 
97
114
  const render = () => {
98
115
  const sec = elapsed();
99
- const value = sec > 0 ? `${text} ${c('gray', sec + 's')}` : text;
116
+ // 按终端宽度截断内容,防止折行后 \x1b[2K 清不净留下残影
117
+ const cols = stream.columns || 0;
118
+ const maxText = cols > 0 ? Math.max(16, cols - 22) : 0;
119
+ const shown = maxText > 0 ? sliceVisual(text, maxText) : text;
120
+ const value = sec > 0 ? `${shown} ${c('gray', sec + 's')}` : shown;
100
121
  stream.write(`\r\x1b[2K${head(SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length])} ${value}`);
101
122
  };
102
123
 
@@ -146,6 +167,7 @@ module.exports = {
146
167
  log,
147
168
  section,
148
169
  visualWidth,
170
+ sliceVisual,
149
171
  padVisual,
150
172
  progress,
151
173
  step,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jspg-ai/coding-bb",
3
- "version": "0.0.2-beta.28",
3
+ "version": "0.0.2-beta.29",
4
4
  "description": "整合业界热门且高价值的工具、框架与技能,为 AI CODING AGENT 提供统一的行为准则与工作流,辅助开发者将需求高效落地为符合规范的代码",
5
5
  "main": "cbb/lib/install/init.js",
6
6
  "bin": {