@adversity/coding-tool-x 2.3.0 → 2.4.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 (35) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +8 -0
  3. package/dist/web/assets/icons-Dom8a0SN.js +1 -0
  4. package/dist/web/assets/index-CQeUIH7E.css +41 -0
  5. package/dist/web/assets/index-YrjlFzC4.js +14 -0
  6. package/dist/web/assets/naive-ui-BjMHakwv.js +1 -0
  7. package/dist/web/assets/vendors-DtJKdpSj.js +7 -0
  8. package/dist/web/assets/vue-vendor-VFuFB5f4.js +44 -0
  9. package/dist/web/index.html +6 -2
  10. package/package.json +2 -2
  11. package/src/commands/export-config.js +205 -0
  12. package/src/config/default.js +1 -1
  13. package/src/server/api/config-export.js +122 -0
  14. package/src/server/api/config-sync.js +155 -0
  15. package/src/server/api/config-templates.js +12 -6
  16. package/src/server/api/health-check.js +1 -89
  17. package/src/server/api/permissions.js +92 -69
  18. package/src/server/api/projects.js +2 -2
  19. package/src/server/api/sessions.js +70 -70
  20. package/src/server/api/skills.js +206 -0
  21. package/src/server/api/terminal.js +26 -0
  22. package/src/server/index.js +7 -11
  23. package/src/server/services/config-export-service.js +415 -0
  24. package/src/server/services/config-sync-service.js +515 -0
  25. package/src/server/services/config-templates-service.js +61 -38
  26. package/src/server/services/enhanced-cache.js +196 -0
  27. package/src/server/services/health-check.js +1 -315
  28. package/src/server/services/permission-templates-service.js +339 -0
  29. package/src/server/services/pty-manager.js +35 -1
  30. package/src/server/services/sessions.js +122 -44
  31. package/src/server/services/skill-service.js +252 -2
  32. package/src/server/services/workspace-service.js +44 -84
  33. package/src/server/websocket-server.js +4 -1
  34. package/dist/web/assets/index-dhun1bYQ.js +0 -3555
  35. package/dist/web/assets/index-hHb7DAda.css +0 -41
@@ -847,14 +847,14 @@ class SkillService {
847
847
 
848
848
  request.on('error', (err) => {
849
849
  file.close();
850
- fs.unlink(dest, () => {});
850
+ fs.unlink(dest, () => { });
851
851
  reject(err);
852
852
  });
853
853
 
854
854
  request.on('timeout', () => {
855
855
  request.destroy();
856
856
  file.close();
857
- fs.unlink(dest, () => {});
857
+ fs.unlink(dest, () => { });
858
858
  reject(new Error('Download timeout'));
859
859
  });
860
860
  });
@@ -912,6 +912,256 @@ ${content}
912
912
  return { success: true, message: '技能创建成功', directory };
913
913
  }
914
914
 
915
+ /**
916
+ * 创建带多文件的技能
917
+ * @param {string} directory - 技能目录名
918
+ * @param {Array<{path: string, content: string}>} files - 文件数组
919
+ * @returns {Object} 创建结果
920
+ */
921
+ createSkillWithFiles({ directory, files }) {
922
+ const dest = path.join(this.installDir, directory);
923
+
924
+ // 检查是否已存在
925
+ if (fs.existsSync(dest)) {
926
+ throw new Error(`技能目录 "${directory}" 已存在`);
927
+ }
928
+
929
+ // 验证必须包含 SKILL.md
930
+ const hasSkillMd = files.some(f =>
931
+ f.path === 'SKILL.md' || f.path.endsWith('/SKILL.md')
932
+ );
933
+ if (!hasSkillMd) {
934
+ throw new Error('技能必须包含 SKILL.md 文件');
935
+ }
936
+
937
+ // 创建目录
938
+ fs.mkdirSync(dest, { recursive: true });
939
+
940
+ // 写入所有文件
941
+ for (const file of files) {
942
+ const filePath = path.join(dest, file.path);
943
+ const fileDir = path.dirname(filePath);
944
+
945
+ // 确保父目录存在
946
+ if (!fs.existsSync(fileDir)) {
947
+ fs.mkdirSync(fileDir, { recursive: true });
948
+ }
949
+
950
+ // 写入文件内容
951
+ if (file.isBase64) {
952
+ // 二进制文件使用 base64 编码
953
+ fs.writeFileSync(filePath, Buffer.from(file.content, 'base64'));
954
+ } else {
955
+ fs.writeFileSync(filePath, file.content, 'utf-8');
956
+ }
957
+ }
958
+
959
+ // 清除缓存
960
+ this.skillsCache = null;
961
+ this.cacheTime = 0;
962
+
963
+ return {
964
+ success: true,
965
+ message: '技能创建成功',
966
+ directory,
967
+ fileCount: files.length
968
+ };
969
+ }
970
+
971
+ /**
972
+ * 获取技能目录下所有文件列表
973
+ * @param {string} directory - 技能目录名
974
+ * @returns {Array<{path: string, size: number, isDirectory: boolean}>}
975
+ */
976
+ getSkillFiles(directory) {
977
+ const skillPath = path.join(this.installDir, directory);
978
+
979
+ if (!fs.existsSync(skillPath)) {
980
+ throw new Error(`技能 "${directory}" 不存在`);
981
+ }
982
+
983
+ const files = [];
984
+ this._scanFilesRecursive(skillPath, skillPath, files);
985
+ return files;
986
+ }
987
+
988
+ /**
989
+ * 递归扫描目录获取文件列表
990
+ */
991
+ _scanFilesRecursive(currentDir, baseDir, files) {
992
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
993
+
994
+ for (const entry of entries) {
995
+ const fullPath = path.join(currentDir, entry.name);
996
+ const relativePath = path.relative(baseDir, fullPath);
997
+
998
+ if (entry.isDirectory()) {
999
+ files.push({
1000
+ path: relativePath,
1001
+ size: 0,
1002
+ isDirectory: true
1003
+ });
1004
+ this._scanFilesRecursive(fullPath, baseDir, files);
1005
+ } else {
1006
+ const stats = fs.statSync(fullPath);
1007
+ files.push({
1008
+ path: relativePath,
1009
+ size: stats.size,
1010
+ isDirectory: false
1011
+ });
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * 获取技能文件内容
1018
+ * @param {string} directory - 技能目录名
1019
+ * @param {string} filePath - 文件相对路径
1020
+ * @returns {Object} 文件内容
1021
+ */
1022
+ getSkillFileContent(directory, filePath) {
1023
+ const fullPath = path.join(this.installDir, directory, filePath);
1024
+
1025
+ if (!fs.existsSync(fullPath)) {
1026
+ throw new Error(`文件 "${filePath}" 不存在`);
1027
+ }
1028
+
1029
+ const stats = fs.statSync(fullPath);
1030
+ if (stats.isDirectory()) {
1031
+ throw new Error(`"${filePath}" 是目录,不是文件`);
1032
+ }
1033
+
1034
+ // 判断是否是文本文件
1035
+ const textExtensions = ['.md', '.txt', '.json', '.js', '.ts', '.py', '.sh', '.yaml', '.yml', '.toml', '.xml', '.html', '.css'];
1036
+ const ext = path.extname(filePath).toLowerCase();
1037
+ const isText = textExtensions.includes(ext);
1038
+
1039
+ if (isText) {
1040
+ return {
1041
+ path: filePath,
1042
+ content: fs.readFileSync(fullPath, 'utf-8'),
1043
+ isBase64: false,
1044
+ size: stats.size
1045
+ };
1046
+ } else {
1047
+ return {
1048
+ path: filePath,
1049
+ content: fs.readFileSync(fullPath).toString('base64'),
1050
+ isBase64: true,
1051
+ size: stats.size
1052
+ };
1053
+ }
1054
+ }
1055
+
1056
+ /**
1057
+ * 添加文件到现有技能
1058
+ * @param {string} directory - 技能目录名
1059
+ * @param {Array<{path: string, content: string, isBase64?: boolean}>} files - 文件数组
1060
+ */
1061
+ addSkillFiles(directory, files) {
1062
+ const skillPath = path.join(this.installDir, directory);
1063
+
1064
+ if (!fs.existsSync(skillPath)) {
1065
+ throw new Error(`技能 "${directory}" 不存在`);
1066
+ }
1067
+
1068
+ const added = [];
1069
+ for (const file of files) {
1070
+ const filePath = path.join(skillPath, file.path);
1071
+ const fileDir = path.dirname(filePath);
1072
+
1073
+ // 确保父目录存在
1074
+ if (!fs.existsSync(fileDir)) {
1075
+ fs.mkdirSync(fileDir, { recursive: true });
1076
+ }
1077
+
1078
+ // 写入文件
1079
+ if (file.isBase64) {
1080
+ fs.writeFileSync(filePath, Buffer.from(file.content, 'base64'));
1081
+ } else {
1082
+ fs.writeFileSync(filePath, file.content, 'utf-8');
1083
+ }
1084
+ added.push(file.path);
1085
+ }
1086
+
1087
+ // 清除缓存
1088
+ this.skillsCache = null;
1089
+ this.cacheTime = 0;
1090
+
1091
+ return { success: true, added };
1092
+ }
1093
+
1094
+ /**
1095
+ * 删除技能中的文件
1096
+ * @param {string} directory - 技能目录名
1097
+ * @param {string} filePath - 文件相对路径
1098
+ */
1099
+ deleteSkillFile(directory, filePath) {
1100
+ const skillPath = path.join(this.installDir, directory);
1101
+
1102
+ if (!fs.existsSync(skillPath)) {
1103
+ throw new Error(`技能 "${directory}" 不存在`);
1104
+ }
1105
+
1106
+ // 不允许删除 SKILL.md
1107
+ if (filePath === 'SKILL.md') {
1108
+ throw new Error('不能删除 SKILL.md 文件');
1109
+ }
1110
+
1111
+ const fullPath = path.join(skillPath, filePath);
1112
+
1113
+ if (!fs.existsSync(fullPath)) {
1114
+ throw new Error(`文件 "${filePath}" 不存在`);
1115
+ }
1116
+
1117
+ const stats = fs.statSync(fullPath);
1118
+ if (stats.isDirectory()) {
1119
+ fs.rmSync(fullPath, { recursive: true, force: true });
1120
+ } else {
1121
+ fs.unlinkSync(fullPath);
1122
+ }
1123
+
1124
+ // 清除缓存
1125
+ this.skillsCache = null;
1126
+ this.cacheTime = 0;
1127
+
1128
+ return { success: true, deleted: filePath };
1129
+ }
1130
+
1131
+ /**
1132
+ * 更新技能文件内容
1133
+ * @param {string} directory - 技能目录名
1134
+ * @param {string} filePath - 文件相对路径
1135
+ * @param {string} content - 新内容
1136
+ * @param {boolean} isBase64 - 是否为 base64 编码
1137
+ */
1138
+ updateSkillFile(directory, filePath, content, isBase64 = false) {
1139
+ const skillPath = path.join(this.installDir, directory);
1140
+
1141
+ if (!fs.existsSync(skillPath)) {
1142
+ throw new Error(`技能 "${directory}" 不存在`);
1143
+ }
1144
+
1145
+ const fullPath = path.join(skillPath, filePath);
1146
+
1147
+ if (!fs.existsSync(fullPath)) {
1148
+ throw new Error(`文件 "${filePath}" 不存在`);
1149
+ }
1150
+
1151
+ if (isBase64) {
1152
+ fs.writeFileSync(fullPath, Buffer.from(content, 'base64'));
1153
+ } else {
1154
+ fs.writeFileSync(fullPath, content, 'utf-8');
1155
+ }
1156
+
1157
+ // 清除缓存
1158
+ this.skillsCache = null;
1159
+ this.cacheTime = 0;
1160
+
1161
+ return { success: true, updated: filePath };
1162
+ }
1163
+
1164
+
915
1165
  /**
916
1166
  * 卸载技能
917
1167
  */
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const { execSync } = require('child_process');
5
5
  const { PATHS } = require('../../config/paths');
6
6
  const configTemplatesService = require('./config-templates-service');
7
+ const permissionTemplatesService = require('./permission-templates-service');
7
8
 
8
9
  // 工作区配置文件路径
9
10
  const WORKSPACES_CONFIG = path.join(PATHS.base, 'workspaces.json');
@@ -299,73 +300,14 @@ function createWorkspace(options) {
299
300
  }
300
301
  }
301
302
 
302
- // 应用权限模板(如果指定且不是 'none')
303
+ // 应用权限模板(如果指定)
303
304
  let permissionInfo = null;
304
- if (permissionTemplate && permissionTemplate !== 'none') {
305
+ if (permissionTemplate) {
305
306
  try {
306
- const permissionTemplates = {
307
- safe: {
308
- allow: [
309
- 'Bash(cat:*)',
310
- 'Bash(ls:*)',
311
- 'Bash(pwd)',
312
- 'Bash(echo:*)',
313
- 'Bash(head:*)',
314
- 'Bash(tail:*)',
315
- 'Bash(grep:*)',
316
- 'Read(*)'
317
- ],
318
- deny: [
319
- 'Bash(rm:*)',
320
- 'Bash(sudo:*)',
321
- 'Bash(git push:*)',
322
- 'Bash(git reset --hard:*)',
323
- 'Bash(chmod:*)',
324
- 'Bash(chown:*)',
325
- 'Edit(*)'
326
- ]
327
- },
328
- balanced: {
329
- allow: [
330
- 'Bash(cat:*)',
331
- 'Bash(ls:*)',
332
- 'Bash(pwd)',
333
- 'Bash(echo:*)',
334
- 'Bash(head:*)',
335
- 'Bash(tail:*)',
336
- 'Bash(grep:*)',
337
- 'Bash(find:*)',
338
- 'Bash(git status)',
339
- 'Bash(git diff:*)',
340
- 'Bash(git log:*)',
341
- 'Bash(npm run:*)',
342
- 'Bash(pnpm:*)',
343
- 'Bash(yarn:*)',
344
- 'Read(*)',
345
- 'Edit(*)'
346
- ],
347
- deny: [
348
- 'Bash(rm -rf:*)',
349
- 'Bash(sudo:*)',
350
- 'Bash(git push --force:*)',
351
- 'Bash(git reset --hard:*)'
352
- ]
353
- },
354
- permissive: {
355
- allow: [
356
- 'Bash(*)',
357
- 'Read(*)',
358
- 'Edit(*)'
359
- ],
360
- deny: [
361
- 'Bash(rm -rf /*)',
362
- 'Bash(sudo rm -rf:*)'
363
- ]
364
- }
365
- };
307
+ // 从权限模板服务获取模板
308
+ const template = permissionTemplatesService.getTemplateById(permissionTemplate);
366
309
 
367
- const permSettings = permissionTemplates[permissionTemplate];
368
- if (permSettings) {
310
+ if (template && template.permissions) {
369
311
  // 为工作区中的每个项目应用权限设置
370
312
  for (const proj of workspaceProjects) {
371
313
  const projSettingsDir = path.join(proj.targetPath, '.claude');
@@ -388,8 +330,8 @@ function createWorkspace(options) {
388
330
 
389
331
  // 更新权限设置
390
332
  settings.permissions = {
391
- allow: permSettings.allow,
392
- deny: permSettings.deny
333
+ allow: template.permissions.allow || [],
334
+ deny: template.permissions.deny || []
393
335
  };
394
336
 
395
337
  // 保存设置
@@ -454,27 +396,44 @@ function deleteWorkspace(id, removeFiles = false) {
454
396
 
455
397
  const workspace = data.workspaces[index];
456
398
 
457
- // 如果需要删除物理文件
458
- if (removeFiles && fs.existsSync(workspace.path)) {
459
- try {
460
- // 清理 worktrees
461
- for (const proj of workspace.projects) {
462
- if (proj.worktrees && proj.worktrees.length > 0) {
463
- for (const wt of proj.worktrees) {
464
- if (fs.existsSync(wt.path)) {
465
- try {
466
- execSync(`git worktree remove "${wt.path}" --force`, {
467
- cwd: proj.sourcePath,
468
- stdio: 'pipe'
469
- });
470
- } catch (error) {
471
- console.error(`删除 worktree 失败: ${wt.path}`, error.message);
399
+ // 清理 worktrees (无论是否删除工作区目录,都应该清理 worktree)
400
+ for (const proj of workspace.projects) {
401
+ if (proj.isGitRepo && proj.sourcePath && fs.existsSync(proj.sourcePath)) {
402
+ try {
403
+ // 重新扫描实际的 worktrees,确保获取最新状态
404
+ const actualWorktrees = getGitWorktrees(proj.sourcePath);
405
+ for (const wt of actualWorktrees) {
406
+ // 只删除属于这个工作区的 worktree (通过 -ws- 标识符识别)
407
+ if (wt.path && wt.path.includes('-ws-')) {
408
+ try {
409
+ console.log(`清理 worktree: ${wt.path}`);
410
+ execSync(`git worktree remove "${wt.path}" --force`, {
411
+ cwd: proj.sourcePath,
412
+ stdio: 'pipe'
413
+ });
414
+ } catch (error) {
415
+ console.error(`删除 worktree 失败: ${wt.path}`, error.message);
416
+ // 如果 git worktree remove 失败,尝试手动删除目录
417
+ if (fs.existsSync(wt.path)) {
418
+ try {
419
+ fs.rmSync(wt.path, { recursive: true, force: true });
420
+ console.log(`手动删除 worktree 目录: ${wt.path}`);
421
+ } catch (rmError) {
422
+ console.error(`手动删除 worktree 目录失败: ${wt.path}`, rmError.message);
423
+ }
472
424
  }
473
425
  }
474
426
  }
475
427
  }
428
+ } catch (error) {
429
+ console.error(`扫描 worktree 失败: ${proj.sourcePath}`, error.message);
476
430
  }
431
+ }
432
+ }
477
433
 
434
+ // 如果需要删除物理文件
435
+ if (removeFiles && fs.existsSync(workspace.path)) {
436
+ try {
478
437
  // 删除工作区目录
479
438
  fs.rmSync(workspace.path, { recursive: true, force: true });
480
439
  } catch (error) {
@@ -678,10 +637,11 @@ function getAllAvailableProjects() {
678
637
  const projects = sessionsService.getProjectsWithStats(config, { force: true });
679
638
 
680
639
  for (const proj of projects) {
681
- // 使用 fullPath 去重
640
+ // 使用 fullPath + channel 组合去重,允许同一项目在不同渠道显示
682
641
  const projectPath = proj.fullPath;
683
- if (projectPath && !seenPaths.has(projectPath)) {
684
- seenPaths.add(projectPath);
642
+ const projectKey = `${projectPath}:${channel.name}`;
643
+ if (projectPath && !seenPaths.has(projectKey)) {
644
+ seenPaths.add(projectKey);
685
645
  allProjects.push({
686
646
  name: proj.name,
687
647
  displayName: proj.displayName,
@@ -175,7 +175,10 @@ function startWebSocketServer(httpServer) {
175
175
  console.log(`✅ WebSocket server started on ws://127.0.0.1:${port}/ws`);
176
176
  }
177
177
 
178
- wss.on('connection', (ws) => {
178
+ wss.on('connection', (ws, req) => {
179
+ const clientIp = req.socket.remoteAddress;
180
+ console.log(`[WebSocket] New connection from ${clientIp}`);
181
+
179
182
  wsClients.add(ws);
180
183
 
181
184
  // 标记客户端存活