@adversity/coding-tool-x 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +8 -0
- package/dist/web/assets/{index-dhun1bYQ.js → index-Bu1oPcKu.js} +1172 -718
- package/dist/web/assets/{index-hHb7DAda.css → index-XSok7-mN.css} +3 -3
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/config/default.js +1 -1
- package/src/server/api/config-export.js +122 -0
- package/src/server/api/config-templates.js +12 -6
- package/src/server/api/health-check.js +1 -89
- package/src/server/api/permissions.js +92 -69
- package/src/server/index.js +4 -11
- package/src/server/services/config-export-service.js +209 -0
- package/src/server/services/config-templates-service.js +61 -38
- package/src/server/services/health-check.js +1 -315
- package/src/server/services/permission-templates-service.js +339 -0
- package/src/server/services/workspace-service.js +8 -66
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置导出/导入服务
|
|
3
|
+
* 支持权限模板、配置模板、频道配置的导出与导入
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const permissionTemplatesService = require('./permission-templates-service');
|
|
9
|
+
const configTemplatesService = require('./config-templates-service');
|
|
10
|
+
const channelsService = require('./channels');
|
|
11
|
+
|
|
12
|
+
const CONFIG_VERSION = '1.0.0';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 导出所有配置为JSON
|
|
16
|
+
* @returns {Object} 配置导出对象
|
|
17
|
+
*/
|
|
18
|
+
function exportAllConfigs() {
|
|
19
|
+
try {
|
|
20
|
+
// 获取所有权限模板(只导出自定义模板)
|
|
21
|
+
const allPermissionTemplates = permissionTemplatesService.getAllTemplates();
|
|
22
|
+
const customPermissionTemplates = allPermissionTemplates.filter(t => !t.isBuiltin);
|
|
23
|
+
|
|
24
|
+
// 获取所有配置模板(只导出自定义模板)
|
|
25
|
+
const allConfigTemplates = configTemplatesService.getAllTemplates();
|
|
26
|
+
const customConfigTemplates = allConfigTemplates.filter(t => !t.isBuiltin);
|
|
27
|
+
|
|
28
|
+
// 获取所有频道配置
|
|
29
|
+
const channelsData = channelsService.getAllChannels();
|
|
30
|
+
const channels = channelsData?.channels || [];
|
|
31
|
+
|
|
32
|
+
const exportData = {
|
|
33
|
+
version: CONFIG_VERSION,
|
|
34
|
+
exportedAt: new Date().toISOString(),
|
|
35
|
+
data: {
|
|
36
|
+
permissionTemplates: customPermissionTemplates,
|
|
37
|
+
configTemplates: customConfigTemplates,
|
|
38
|
+
channels: channels || []
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
data: exportData
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('[ConfigExport] 导出配置失败:', error);
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
message: error.message
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 导入配置
|
|
57
|
+
* @param {Object} importData - 导入的配置对象
|
|
58
|
+
* @param {Object} options - 导入选项 { overwrite: boolean }
|
|
59
|
+
* @returns {Object} 导入结果
|
|
60
|
+
*/
|
|
61
|
+
function importConfigs(importData, options = {}) {
|
|
62
|
+
const { overwrite = false } = options;
|
|
63
|
+
const results = {
|
|
64
|
+
permissionTemplates: { success: 0, failed: 0, skipped: 0 },
|
|
65
|
+
configTemplates: { success: 0, failed: 0, skipped: 0 },
|
|
66
|
+
channels: { success: 0, failed: 0, skipped: 0 }
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// 验证导入数据格式
|
|
71
|
+
if (!importData || !importData.data) {
|
|
72
|
+
throw new Error('无效的导入数据格式');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { permissionTemplates = [], configTemplates = [], channels = [] } = importData.data;
|
|
76
|
+
|
|
77
|
+
// 导入权限模板
|
|
78
|
+
for (const template of permissionTemplates) {
|
|
79
|
+
try {
|
|
80
|
+
const existing = permissionTemplatesService.getTemplateById(template.id);
|
|
81
|
+
|
|
82
|
+
if (existing && !overwrite) {
|
|
83
|
+
results.permissionTemplates.skipped++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (existing && overwrite) {
|
|
88
|
+
permissionTemplatesService.updateTemplate(template.id, template);
|
|
89
|
+
} else {
|
|
90
|
+
// 创建新模板(使用原ID)
|
|
91
|
+
const newTemplate = {
|
|
92
|
+
...template,
|
|
93
|
+
isBuiltin: false,
|
|
94
|
+
importedAt: new Date().toISOString()
|
|
95
|
+
};
|
|
96
|
+
permissionTemplatesService.createTemplate(newTemplate);
|
|
97
|
+
}
|
|
98
|
+
results.permissionTemplates.success++;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`[ConfigImport] 导入权限模板失败: ${template.name}`, err);
|
|
101
|
+
results.permissionTemplates.failed++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 导入配置模板
|
|
106
|
+
for (const template of configTemplates) {
|
|
107
|
+
try {
|
|
108
|
+
const existing = configTemplatesService.getTemplateById(template.id);
|
|
109
|
+
|
|
110
|
+
if (existing && !overwrite) {
|
|
111
|
+
results.configTemplates.skipped++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (existing && overwrite) {
|
|
116
|
+
configTemplatesService.updateTemplate(template.id, template);
|
|
117
|
+
} else {
|
|
118
|
+
const newTemplate = {
|
|
119
|
+
...template,
|
|
120
|
+
isBuiltin: false,
|
|
121
|
+
importedAt: new Date().toISOString()
|
|
122
|
+
};
|
|
123
|
+
configTemplatesService.createTemplate(newTemplate);
|
|
124
|
+
}
|
|
125
|
+
results.configTemplates.success++;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`[ConfigImport] 导入配置模板失败: ${template.name}`, err);
|
|
128
|
+
results.configTemplates.failed++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 导入频道配置
|
|
133
|
+
for (const channel of channels) {
|
|
134
|
+
try {
|
|
135
|
+
const channelsData = channelsService.getAllChannels();
|
|
136
|
+
const existingChannels = channelsData?.channels || [];
|
|
137
|
+
const existing = existingChannels.find(c => c.id === channel.id);
|
|
138
|
+
|
|
139
|
+
if (existing && !overwrite) {
|
|
140
|
+
results.channels.skipped++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (existing && overwrite) {
|
|
145
|
+
channelsService.updateChannel(channel.id, channel);
|
|
146
|
+
} else {
|
|
147
|
+
// createChannel 需要单独的参数,不是一个对象
|
|
148
|
+
const { name, baseUrl, apiKey, websiteUrl, ...extraConfig } = channel;
|
|
149
|
+
channelsService.createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig);
|
|
150
|
+
}
|
|
151
|
+
results.channels.success++;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error(`[ConfigImport] 导入频道失败: ${channel.name}`, err);
|
|
154
|
+
results.channels.failed++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
results,
|
|
161
|
+
message: generateImportSummary(results)
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('[ConfigImport] 导入配置失败:', error);
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
message: error.message,
|
|
168
|
+
results
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 生成导入摘要消息
|
|
175
|
+
*/
|
|
176
|
+
function generateImportSummary(results) {
|
|
177
|
+
const parts = [];
|
|
178
|
+
|
|
179
|
+
if (results.permissionTemplates.success > 0) {
|
|
180
|
+
parts.push(`权限模板: ${results.permissionTemplates.success}成功`);
|
|
181
|
+
}
|
|
182
|
+
if (results.configTemplates.success > 0) {
|
|
183
|
+
parts.push(`配置模板: ${results.configTemplates.success}成功`);
|
|
184
|
+
}
|
|
185
|
+
if (results.channels.success > 0) {
|
|
186
|
+
parts.push(`频道: ${results.channels.success}成功`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const totalSkipped = results.permissionTemplates.skipped +
|
|
190
|
+
results.configTemplates.skipped +
|
|
191
|
+
results.channels.skipped;
|
|
192
|
+
if (totalSkipped > 0) {
|
|
193
|
+
parts.push(`${totalSkipped}项已跳过`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const totalFailed = results.permissionTemplates.failed +
|
|
197
|
+
results.configTemplates.failed +
|
|
198
|
+
results.channels.failed;
|
|
199
|
+
if (totalFailed > 0) {
|
|
200
|
+
parts.push(`${totalFailed}项失败`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return parts.join(', ');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
exportAllConfigs,
|
|
208
|
+
importConfigs
|
|
209
|
+
};
|
|
@@ -802,7 +802,8 @@ function generateRuleContent(rule) {
|
|
|
802
802
|
* @param {string} targetDir - 目标项目目录
|
|
803
803
|
* @param {string} templateId - 模板 ID
|
|
804
804
|
* @param {object} options - 可选配置
|
|
805
|
-
* @param {string} options.
|
|
805
|
+
* @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini']
|
|
806
|
+
* @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
|
|
806
807
|
*/
|
|
807
808
|
function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
808
809
|
const template = getTemplateById(templateId);
|
|
@@ -813,7 +814,7 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
813
814
|
ensureDir(targetDir);
|
|
814
815
|
|
|
815
816
|
const results = {
|
|
816
|
-
|
|
817
|
+
aiConfigs: [], // 改为数组存储多个 AI 配置结果
|
|
817
818
|
skills: { applied: template.skills?.length || 0, items: template.skills?.map(s => s.directory || s.name) || [] },
|
|
818
819
|
agents: { applied: 0, files: [] },
|
|
819
820
|
commands: { applied: 0, files: [] },
|
|
@@ -822,27 +823,37 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
822
823
|
};
|
|
823
824
|
|
|
824
825
|
// 1. 写入 AI 配置文件(支持多 AI 类型选择)
|
|
825
|
-
|
|
826
|
+
// 兼容旧版单值参数
|
|
827
|
+
let aiConfigTypes = options.aiConfigTypes;
|
|
828
|
+
if (!aiConfigTypes) {
|
|
829
|
+
aiConfigTypes = options.aiConfigType ? [options.aiConfigType] : ['claude'];
|
|
830
|
+
}
|
|
831
|
+
if (!Array.isArray(aiConfigTypes)) {
|
|
832
|
+
aiConfigTypes = [aiConfigTypes];
|
|
833
|
+
}
|
|
834
|
+
|
|
826
835
|
const aiConfigMap = {
|
|
827
836
|
claude: { fileName: 'CLAUDE.md', name: 'Claude' },
|
|
828
|
-
codex: { fileName: '
|
|
837
|
+
codex: { fileName: 'AGENTS.md', name: 'Codex' },
|
|
829
838
|
gemini: { fileName: 'GEMINI.md', name: 'Gemini' }
|
|
830
839
|
};
|
|
831
840
|
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
841
|
+
// 遍历所有选中的 AI 配置类型
|
|
842
|
+
for (const aiConfigType of aiConfigTypes) {
|
|
843
|
+
let aiConfig = null;
|
|
844
|
+
if (template.aiConfigs && template.aiConfigs[aiConfigType]) {
|
|
845
|
+
aiConfig = template.aiConfigs[aiConfigType];
|
|
846
|
+
} else if (aiConfigType === 'claude' && template.claudeMd) {
|
|
847
|
+
// 兼容旧的 claudeMd 字段
|
|
848
|
+
aiConfig = template.claudeMd;
|
|
849
|
+
}
|
|
840
850
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
851
|
+
if (aiConfig?.enabled && aiConfig?.content) {
|
|
852
|
+
const configInfo = aiConfigMap[aiConfigType];
|
|
853
|
+
const configPath = path.join(targetDir, configInfo.fileName);
|
|
854
|
+
fs.writeFileSync(configPath, aiConfig.content, 'utf-8');
|
|
855
|
+
results.aiConfigs.push({ applied: true, path: configInfo.fileName, type: configInfo.name, key: aiConfigType });
|
|
856
|
+
}
|
|
846
857
|
}
|
|
847
858
|
|
|
848
859
|
// 2. 写入 Agents
|
|
@@ -932,8 +943,8 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
932
943
|
templateId: template.id,
|
|
933
944
|
templateName: template.name,
|
|
934
945
|
appliedAt: new Date().toISOString(),
|
|
935
|
-
|
|
936
|
-
|
|
946
|
+
aiConfigTypes: aiConfigTypes,
|
|
947
|
+
aiConfigPaths: results.aiConfigs.map(c => c.path),
|
|
937
948
|
skills: template.skills?.map(s => s.directory || s.name) || [],
|
|
938
949
|
agents: template.agents?.map(a => a.fileName || a.name) || [],
|
|
939
950
|
commands: template.commands?.map(c => c.name) || [],
|
|
@@ -955,7 +966,8 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
955
966
|
* @param {string} targetDir - 目标项目目录
|
|
956
967
|
* @param {string} templateId - 模板 ID
|
|
957
968
|
* @param {object} options - 可选配置
|
|
958
|
-
* @param {string} options.
|
|
969
|
+
* @param {string|string[]} options.aiConfigTypes - 选择的 AI 配置类型数组: ['claude', 'codex', 'gemini']
|
|
970
|
+
* @param {string} options.aiConfigType - (兼容旧版) 单个 AI 配置类型
|
|
959
971
|
*/
|
|
960
972
|
function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
961
973
|
const template = getTemplateById(templateId);
|
|
@@ -967,7 +979,7 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
|
967
979
|
willCreate: [],
|
|
968
980
|
willOverwrite: [],
|
|
969
981
|
summary: {
|
|
970
|
-
|
|
982
|
+
aiConfigs: [], // 改为数组
|
|
971
983
|
skills: 0,
|
|
972
984
|
agents: 0,
|
|
973
985
|
commands: 0,
|
|
@@ -976,30 +988,41 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
|
976
988
|
}
|
|
977
989
|
};
|
|
978
990
|
|
|
979
|
-
// 检查 AI
|
|
980
|
-
|
|
991
|
+
// 检查 AI 配置文件(支持多选)
|
|
992
|
+
// 兼容旧版单值参数
|
|
993
|
+
let aiConfigTypes = options.aiConfigTypes;
|
|
994
|
+
if (!aiConfigTypes) {
|
|
995
|
+
aiConfigTypes = options.aiConfigType ? [options.aiConfigType] : ['claude'];
|
|
996
|
+
}
|
|
997
|
+
if (!Array.isArray(aiConfigTypes)) {
|
|
998
|
+
aiConfigTypes = [aiConfigTypes];
|
|
999
|
+
}
|
|
1000
|
+
|
|
981
1001
|
const aiConfigMap = {
|
|
982
1002
|
claude: { fileName: 'CLAUDE.md', name: 'Claude' },
|
|
983
|
-
codex: { fileName: '
|
|
1003
|
+
codex: { fileName: 'AGENTS.md', name: 'Codex' },
|
|
984
1004
|
gemini: { fileName: 'GEMINI.md', name: 'Gemini' }
|
|
985
1005
|
};
|
|
986
1006
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
aiConfig =
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1007
|
+
// 遍历所有选中的 AI 配置类型
|
|
1008
|
+
for (const aiConfigType of aiConfigTypes) {
|
|
1009
|
+
let aiConfig = null;
|
|
1010
|
+
if (template.aiConfigs && template.aiConfigs[aiConfigType]) {
|
|
1011
|
+
aiConfig = template.aiConfigs[aiConfigType];
|
|
1012
|
+
} else if (aiConfigType === 'claude' && template.claudeMd) {
|
|
1013
|
+
aiConfig = template.claudeMd;
|
|
1014
|
+
}
|
|
993
1015
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1016
|
+
if (aiConfig?.enabled && aiConfig?.content) {
|
|
1017
|
+
const configInfo = aiConfigMap[aiConfigType];
|
|
1018
|
+
const configPath = path.join(targetDir, configInfo.fileName);
|
|
1019
|
+
if (fs.existsSync(configPath)) {
|
|
1020
|
+
preview.willOverwrite.push(configInfo.fileName);
|
|
1021
|
+
} else {
|
|
1022
|
+
preview.willCreate.push(configInfo.fileName);
|
|
1023
|
+
}
|
|
1024
|
+
preview.summary.aiConfigs.push({ type: aiConfigType, fileName: configInfo.fileName, name: configInfo.name });
|
|
1001
1025
|
}
|
|
1002
|
-
preview.summary.aiConfig = { type: aiConfigType, fileName: configInfo.fileName, name: configInfo.name };
|
|
1003
1026
|
}
|
|
1004
1027
|
|
|
1005
1028
|
// Skills 摘要
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const readline = require('readline');
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* 健康检查:确保项目的 .claude/sessions 目录存在
|
|
@@ -81,319 +79,7 @@ function healthCheckAllProjects(projects) {
|
|
|
81
79
|
};
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
/**
|
|
85
|
-
* 从会话文件中提取 cwd
|
|
86
|
-
* @param {string} sessionFilePath - 会话文件路径
|
|
87
|
-
* @returns {string|null} cwd 或 null
|
|
88
|
-
*/
|
|
89
|
-
function extractCwdFromSession(sessionFilePath) {
|
|
90
|
-
try {
|
|
91
|
-
const content = fs.readFileSync(sessionFilePath, 'utf8');
|
|
92
|
-
const firstLine = content.split('\n')[0];
|
|
93
|
-
if (firstLine) {
|
|
94
|
-
const json = JSON.parse(firstLine);
|
|
95
|
-
return json.cwd || null;
|
|
96
|
-
}
|
|
97
|
-
} catch (err) {
|
|
98
|
-
// Ignore parse errors
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 扫描旧的全局目录中的会话文件
|
|
105
|
-
* @returns {Object} 扫描结果
|
|
106
|
-
*/
|
|
107
|
-
function scanLegacySessionFiles() {
|
|
108
|
-
try {
|
|
109
|
-
const legacyProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
110
|
-
|
|
111
|
-
if (!fs.existsSync(legacyProjectsDir)) {
|
|
112
|
-
return {
|
|
113
|
-
found: false,
|
|
114
|
-
message: 'Legacy directory not found (nothing to clean)',
|
|
115
|
-
legacyDir: legacyProjectsDir
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const projects = [];
|
|
120
|
-
const entries = fs.readdirSync(legacyProjectsDir, { withFileTypes: true });
|
|
121
|
-
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
if (!entry.isDirectory()) continue;
|
|
124
|
-
|
|
125
|
-
const projectName = entry.name;
|
|
126
|
-
const projectDir = path.join(legacyProjectsDir, projectName);
|
|
127
|
-
const files = fs.readdirSync(projectDir)
|
|
128
|
-
.filter(f => f.endsWith('.jsonl') || f.endsWith('.json'));
|
|
129
|
-
|
|
130
|
-
if (files.length > 0) {
|
|
131
|
-
let totalSize = 0;
|
|
132
|
-
for (const file of files) {
|
|
133
|
-
const filePath = path.join(projectDir, file);
|
|
134
|
-
const stats = fs.statSync(filePath);
|
|
135
|
-
totalSize += stats.size;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
projects.push({
|
|
139
|
-
projectName,
|
|
140
|
-
projectDir,
|
|
141
|
-
fileCount: files.length,
|
|
142
|
-
totalSize,
|
|
143
|
-
files: files.slice(0, 5) // 只显示前5个文件名
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
found: true,
|
|
150
|
-
legacyDir: legacyProjectsDir,
|
|
151
|
-
projectCount: projects.length,
|
|
152
|
-
projects
|
|
153
|
-
};
|
|
154
|
-
} catch (err) {
|
|
155
|
-
return {
|
|
156
|
-
found: false,
|
|
157
|
-
error: err.message
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 迁移会话文件到正确的位置
|
|
164
|
-
* @param {Object} options - 迁移选项
|
|
165
|
-
* @returns {Object} 迁移结果
|
|
166
|
-
*/
|
|
167
|
-
function migrateSessionFiles(options = {}) {
|
|
168
|
-
const {
|
|
169
|
-
dryRun = false, // 是否只是预演
|
|
170
|
-
projectNames = null // 指定要迁移的项目
|
|
171
|
-
} = options;
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
const legacyProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
175
|
-
|
|
176
|
-
if (!fs.existsSync(legacyProjectsDir)) {
|
|
177
|
-
return {
|
|
178
|
-
success: true,
|
|
179
|
-
message: 'No legacy directory found',
|
|
180
|
-
migrated: 0
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const results = {
|
|
185
|
-
dryRun,
|
|
186
|
-
migrated: [],
|
|
187
|
-
skipped: [],
|
|
188
|
-
errors: []
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const entries = fs.readdirSync(legacyProjectsDir, { withFileTypes: true });
|
|
192
|
-
|
|
193
|
-
for (const entry of entries) {
|
|
194
|
-
if (!entry.isDirectory()) continue;
|
|
195
|
-
|
|
196
|
-
const projectName = entry.name;
|
|
197
|
-
|
|
198
|
-
// 如果指定了项目列表,只迁移列表中的项目
|
|
199
|
-
if (projectNames && !projectNames.includes(projectName)) {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const projectDir = path.join(legacyProjectsDir, projectName);
|
|
204
|
-
const files = fs.readdirSync(projectDir)
|
|
205
|
-
.filter(f => f.endsWith('.jsonl'));
|
|
206
|
-
|
|
207
|
-
for (const file of files) {
|
|
208
|
-
const oldPath = path.join(projectDir, file);
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
// 从会话文件提取 cwd
|
|
212
|
-
const cwd = extractCwdFromSession(oldPath);
|
|
213
|
-
|
|
214
|
-
if (!cwd || !fs.existsSync(cwd)) {
|
|
215
|
-
results.skipped.push({
|
|
216
|
-
file,
|
|
217
|
-
reason: 'Invalid or missing cwd',
|
|
218
|
-
oldPath
|
|
219
|
-
});
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// 目标路径: {cwd}/.claude/sessions/{file}
|
|
224
|
-
const targetDir = path.join(cwd, '.claude', 'sessions');
|
|
225
|
-
const newPath = path.join(targetDir, file);
|
|
226
|
-
|
|
227
|
-
// 如果文件已存在,跳过
|
|
228
|
-
if (fs.existsSync(newPath)) {
|
|
229
|
-
results.skipped.push({
|
|
230
|
-
file,
|
|
231
|
-
reason: 'Already exists at target',
|
|
232
|
-
oldPath,
|
|
233
|
-
newPath
|
|
234
|
-
});
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (!dryRun) {
|
|
239
|
-
// 确保目标目录存在
|
|
240
|
-
if (!fs.existsSync(targetDir)) {
|
|
241
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 复制文件(保留原文件)
|
|
245
|
-
fs.copyFileSync(oldPath, newPath);
|
|
246
|
-
console.log(`[Migration] Migrated: ${file} -> ${newPath}`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
results.migrated.push({
|
|
250
|
-
file,
|
|
251
|
-
oldPath,
|
|
252
|
-
newPath,
|
|
253
|
-
cwd
|
|
254
|
-
});
|
|
255
|
-
} catch (err) {
|
|
256
|
-
results.errors.push({
|
|
257
|
-
file,
|
|
258
|
-
oldPath,
|
|
259
|
-
error: err.message
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
success: true,
|
|
267
|
-
dryRun,
|
|
268
|
-
migratedCount: results.migrated.length,
|
|
269
|
-
skippedCount: results.skipped.length,
|
|
270
|
-
errorCount: results.errors.length,
|
|
271
|
-
results
|
|
272
|
-
};
|
|
273
|
-
} catch (err) {
|
|
274
|
-
return {
|
|
275
|
-
success: false,
|
|
276
|
-
error: err.message
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* 清理旧的全局目录中的会话文件
|
|
283
|
-
* @param {Object} options - 清理选项
|
|
284
|
-
* @returns {Object} 清理结果
|
|
285
|
-
*/
|
|
286
|
-
function cleanLegacySessionFiles(options = {}) {
|
|
287
|
-
const {
|
|
288
|
-
dryRun = false, // 是否只是预演,不实际删除
|
|
289
|
-
projectNames = null // 指定要清理的项目名称列表,null 表示全部
|
|
290
|
-
} = options;
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
const legacyProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
294
|
-
|
|
295
|
-
if (!fs.existsSync(legacyProjectsDir)) {
|
|
296
|
-
return {
|
|
297
|
-
success: true,
|
|
298
|
-
message: 'No legacy directory found',
|
|
299
|
-
deleted: 0
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const results = {
|
|
304
|
-
dryRun,
|
|
305
|
-
deleted: [],
|
|
306
|
-
errors: [],
|
|
307
|
-
totalSize: 0
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const entries = fs.readdirSync(legacyProjectsDir, { withFileTypes: true });
|
|
311
|
-
|
|
312
|
-
for (const entry of entries) {
|
|
313
|
-
if (!entry.isDirectory()) continue;
|
|
314
|
-
|
|
315
|
-
const projectName = entry.name;
|
|
316
|
-
|
|
317
|
-
// 如果指定了项目列表,只清理列表中的项目
|
|
318
|
-
if (projectNames && !projectNames.includes(projectName)) {
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const projectDir = path.join(legacyProjectsDir, projectName);
|
|
323
|
-
const files = fs.readdirSync(projectDir);
|
|
324
|
-
|
|
325
|
-
for (const file of files) {
|
|
326
|
-
const filePath = path.join(projectDir, file);
|
|
327
|
-
|
|
328
|
-
try {
|
|
329
|
-
const stats = fs.statSync(filePath);
|
|
330
|
-
results.totalSize += stats.size;
|
|
331
|
-
|
|
332
|
-
if (!dryRun) {
|
|
333
|
-
fs.unlinkSync(filePath);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
results.deleted.push({
|
|
337
|
-
projectName,
|
|
338
|
-
file,
|
|
339
|
-
size: stats.size
|
|
340
|
-
});
|
|
341
|
-
} catch (err) {
|
|
342
|
-
results.errors.push({
|
|
343
|
-
projectName,
|
|
344
|
-
file,
|
|
345
|
-
error: err.message
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// 如果项目目录为空,删除目录
|
|
351
|
-
if (!dryRun) {
|
|
352
|
-
try {
|
|
353
|
-
const remainingFiles = fs.readdirSync(projectDir);
|
|
354
|
-
if (remainingFiles.length === 0) {
|
|
355
|
-
fs.rmdirSync(projectDir);
|
|
356
|
-
console.log(`[Cleanup] Removed empty directory: ${projectDir}`);
|
|
357
|
-
}
|
|
358
|
-
} catch (err) {
|
|
359
|
-
console.warn(`[Cleanup] Failed to remove directory ${projectDir}:`, err.message);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// 如果 projects 目录为空,删除它
|
|
365
|
-
if (!dryRun) {
|
|
366
|
-
try {
|
|
367
|
-
const remainingProjects = fs.readdirSync(legacyProjectsDir);
|
|
368
|
-
if (remainingProjects.length === 0) {
|
|
369
|
-
fs.rmdirSync(legacyProjectsDir);
|
|
370
|
-
console.log(`[Cleanup] Removed empty legacy directory: ${legacyProjectsDir}`);
|
|
371
|
-
}
|
|
372
|
-
} catch (err) {
|
|
373
|
-
console.warn(`[Cleanup] Failed to remove legacy directory:`, err.message);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return {
|
|
378
|
-
success: true,
|
|
379
|
-
dryRun,
|
|
380
|
-
deletedCount: results.deleted.length,
|
|
381
|
-
errorCount: results.errors.length,
|
|
382
|
-
totalSize: results.totalSize,
|
|
383
|
-
results
|
|
384
|
-
};
|
|
385
|
-
} catch (err) {
|
|
386
|
-
return {
|
|
387
|
-
success: false,
|
|
388
|
-
error: err.message
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
82
|
module.exports = {
|
|
394
83
|
ensureProjectClaudeDir,
|
|
395
|
-
healthCheckAllProjects
|
|
396
|
-
scanLegacySessionFiles,
|
|
397
|
-
migrateSessionFiles,
|
|
398
|
-
cleanLegacySessionFiles
|
|
84
|
+
healthCheckAllProjects
|
|
399
85
|
};
|