lcch-cli 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,3015 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * LCCH - Claude Code Helper
5
+ * 管理 Claude Code 配置的 CLI 工具
6
+ *
7
+ * 使用方式: lcch <command> [options]
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ var __importDefault = (this && this.__importDefault) || function (mod) {
43
+ return (mod && mod.__esModule) ? mod : { "default": mod };
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ const commander_1 = require("commander");
47
+ const chalk_1 = __importDefault(require("chalk"));
48
+ const inquirer_1 = __importDefault(require("inquirer"));
49
+ const prompts_1 = require("@inquirer/prompts");
50
+ const path = __importStar(require("path"));
51
+ const fs = __importStar(require("fs-extra"));
52
+ const os = __importStar(require("os"));
53
+ const scanner_1 = require("./scanner");
54
+ const backup_1 = require("./backup");
55
+ const export_1 = require("./export");
56
+ /**
57
+ * 简单掩码函数 - 保留前4位,其余用 **** 替代
58
+ */
59
+ function maskSecret(secret) {
60
+ if (secret.length <= 8) {
61
+ return secret.substring(0, Math.min(4, secret.length)) + '****';
62
+ }
63
+ return secret.substring(0, 4) + '****';
64
+ }
65
+ /**
66
+ * 打印项目会话历史(按有效/无效分类)
67
+ */
68
+ async function printProjectSessions(config, result, filter = 'all') {
69
+ const sessions = await (0, scanner_1.readSessionHistory)();
70
+ const sessionsByProject = (0, scanner_1.groupSessionsByProject)(sessions);
71
+ // 扫描孤立会话(有session但没有project)
72
+ const allSessionProjects = new Set(sessionsByProject.keys());
73
+ const configProjects = new Set([...result.validProjects, ...result.invalidProjects]);
74
+ const orphanedProjects = [];
75
+ for (const projectPath of allSessionProjects) {
76
+ if (!configProjects.has(projectPath)) {
77
+ orphanedProjects.push(projectPath);
78
+ }
79
+ }
80
+ // 按有效/无效分类
81
+ const validSet = new Set(result.validProjects);
82
+ // 打印有效项目的会话
83
+ if ((filter === 'all' || filter === 'valid') && result.validProjects.length > 0) {
84
+ console.log(chalk_1.default.green.bold(`\n✓ 有效项目 (${result.validProjects.length} 个):\n`));
85
+ const sortedValid = [...result.validProjects].sort();
86
+ for (const projectPath of sortedValid) {
87
+ const projectSessions = sessionsByProject.get(projectPath) || [];
88
+ printProjectSessionDetail(projectPath, true, projectSessions);
89
+ }
90
+ }
91
+ // 打印无效项目的会话
92
+ if ((filter === 'all' || filter === 'invalid') && result.invalidProjects.length > 0) {
93
+ console.log(chalk_1.default.red.bold(`\n✗ 无效项目 (${result.invalidProjects.length} 个):\n`));
94
+ const sortedInvalid = [...result.invalidProjects].sort();
95
+ for (const projectPath of sortedInvalid) {
96
+ const projectSessions = sessionsByProject.get(projectPath) || [];
97
+ printProjectSessionDetail(projectPath, false, projectSessions);
98
+ }
99
+ }
100
+ // 打印孤立会话
101
+ if ((filter === 'all' || filter === 'orphaned') && orphanedProjects.length > 0) {
102
+ console.log(chalk_1.default.yellow.bold(`\n⚠ 孤立会话 (${orphanedProjects.length} 个 - 有记录但无配置):\n`));
103
+ const sortedOrphaned = orphanedProjects.sort();
104
+ for (const projectPath of sortedOrphaned) {
105
+ const projectSessions = sessionsByProject.get(projectPath) || [];
106
+ printProjectSessionDetail(projectPath, null, projectSessions);
107
+ }
108
+ }
109
+ // 汇总(只显示符合条件的)
110
+ console.log(chalk_1.default.bold('\n会话汇总:'));
111
+ if (filter === 'all' || filter === 'valid') {
112
+ console.log(` ${chalk_1.default.green('有效项目')}: ${result.validProjects.length}`);
113
+ }
114
+ if (filter === 'all' || filter === 'invalid') {
115
+ console.log(` ${chalk_1.default.red('无效项目')}: ${result.invalidProjects.length}`);
116
+ }
117
+ if (filter === 'all' || filter === 'orphaned') {
118
+ console.log(` ${chalk_1.default.yellow('孤立会话')}: ${orphanedProjects.length}`);
119
+ }
120
+ if (filter === 'all') {
121
+ console.log(` 总计: ${result.totalCount + orphanedProjects.length}`);
122
+ }
123
+ }
124
+ /**
125
+ * 打印单个项目的会话详情
126
+ */
127
+ function printProjectSessionDetail(projectPath, isValid, projectSessions) {
128
+ // 项目标题
129
+ let statusIcon;
130
+ let pathColor;
131
+ if (isValid === true) {
132
+ statusIcon = chalk_1.default.green('✓');
133
+ pathColor = chalk_1.default.white;
134
+ }
135
+ else if (isValid === false) {
136
+ statusIcon = chalk_1.default.red('✗');
137
+ pathColor = chalk_1.default.gray;
138
+ }
139
+ else {
140
+ statusIcon = chalk_1.default.yellow('⚠');
141
+ pathColor = chalk_1.default.yellow;
142
+ }
143
+ const sessionCount = projectSessions.length;
144
+ const countStr = sessionCount > 0 ? chalk_1.default.gray(`(${sessionCount} 条记录)`) : chalk_1.default.gray('(无记录)');
145
+ console.log(`${statusIcon} ${pathColor(chalk_1.default.bold(projectPath))} ${countStr}`);
146
+ // 显示会话记录(最多显示最近10条)
147
+ if (projectSessions.length > 0) {
148
+ const displaySessions = projectSessions.slice(0, 10);
149
+ for (let i = 0; i < displaySessions.length; i++) {
150
+ const session = displaySessions[i];
151
+ const isLast = i === displaySessions.length - 1;
152
+ const connector = isLast ? '└── ' : '├── ';
153
+ const timeStr = chalk_1.default.gray(`[${(0, scanner_1.formatRelativeTime)(session.timestamp)}]`);
154
+ const sessionIdStr = chalk_1.default.cyan(`[${session.sessionId}]`);
155
+ const content = (0, scanner_1.truncateText)(session.display || '(空)', 50);
156
+ console.log(` ${connector}${timeStr} ${sessionIdStr} ${content}`);
157
+ }
158
+ // 如果还有更多,显示省略提示
159
+ if (projectSessions.length > 10) {
160
+ console.log(` └── ${chalk_1.default.gray(`... 还有 ${projectSessions.length - 10} 条记录`)}`);
161
+ }
162
+ }
163
+ console.log();
164
+ }
165
+ /**
166
+ * 打印所有项目的 resume 历史(按有效/无效分类)
167
+ */
168
+ async function printProjectResumes(config, result, allowDelete = false, filter = 'all') {
169
+ const validSet = new Set(result.validProjects);
170
+ const allConfigProjects = new Set([...result.validProjects, ...result.invalidProjects]);
171
+ // 收集所有 config 中项目的 resume 信息(包括有 resume 和没有 resume 的)
172
+ const allProjects = [];
173
+ for (const projectPath of allConfigProjects) {
174
+ const resumes = await (0, scanner_1.getProjectResumeInfo)(projectPath);
175
+ allProjects.push({
176
+ projectPath,
177
+ isValid: validSet.has(projectPath),
178
+ resumes,
179
+ });
180
+ }
181
+ // 扫描孤立的 resume(不在 config 中的项目目录)
182
+ const orphanedResumes = await (0, scanner_1.getOrphanedResumes)(allConfigProjects);
183
+ if (allProjects.length === 0 && orphanedResumes.length === 0) {
184
+ console.log(chalk_1.default.yellow('\n没有找到任何项目'));
185
+ return;
186
+ }
187
+ // 按有效/无效/孤立分类
188
+ const validProjects = allProjects.filter(p => p.isValid);
189
+ const invalidProjects = allProjects.filter(p => !p.isValid);
190
+ const validWithResume = validProjects.filter(p => p.resumes.length > 0);
191
+ const validNoResume = validProjects.filter(p => p.resumes.length === 0);
192
+ const invalidWithResume = invalidProjects.filter(p => p.resumes.length > 0);
193
+ const invalidNoResume = invalidProjects.filter(p => p.resumes.length === 0);
194
+ const allResumeChoices = [];
195
+ // 详细显示 resume(仅在非删除模式下显示)
196
+ if (!allowDelete) {
197
+ // 打印有效项目(有 resume)
198
+ if ((filter === 'all' || filter === 'valid') && validWithResume.length > 0) {
199
+ console.log(chalk_1.default.green.bold(`\n✓ 有效项目 - 有 resume (${validWithResume.length} 个):\n`));
200
+ for (const { projectPath, resumes } of validWithResume) {
201
+ console.log(`${chalk_1.default.green('✓')} ${chalk_1.default.bold(projectPath)} ${chalk_1.default.gray(`(${resumes.length} 个 resume)`)}`);
202
+ resumes.forEach((resume, index) => {
203
+ const isLast = index === resumes.length - 1;
204
+ const connector = isLast ? '└── ' : '├── ';
205
+ // 一行格式: [session-id] 创建时间→最后活动时间 | 消息数 | 磁盘大小 | 压缩次数
206
+ const createdTime = (0, scanner_1.formatRelativeTime)(resume.createdAt.getTime());
207
+ const lastActivityTime = (0, scanner_1.formatRelativeTime)(resume.lastActivity.getTime());
208
+ const timeRange = `${createdTime} → ${lastActivityTime}`;
209
+ const messagesStr = `${resume.messageCount}条`;
210
+ const sizeStr = (0, backup_1.formatFileSize)(resume.fileSize);
211
+ const compactStr = resume.compactCount > 0 ? ` | ${resume.compactCount}次压缩` : '';
212
+ console.log(` ${connector}${chalk_1.default.cyan(`[${resume.sessionId}]`)} ${chalk_1.default.gray(timeRange)} | ${chalk_1.default.yellow(messagesStr)} | ${chalk_1.default.gray(sizeStr)}${chalk_1.default.magenta(compactStr)}`);
213
+ // 添加到删除选项
214
+ allResumeChoices.push({
215
+ name: `${projectPath} - [${resume.sessionId}] (${(0, scanner_1.formatRelativeTime)(resume.lastActivity.getTime())})`,
216
+ value: { projectPath, sessionId: resume.sessionId },
217
+ checked: false,
218
+ });
219
+ });
220
+ console.log();
221
+ }
222
+ }
223
+ // 打印有效项目(无 resume)
224
+ if ((filter === 'all' || filter === 'valid') && validNoResume.length > 0) {
225
+ console.log(chalk_1.default.green.bold(`\n✓ 有效项目 - 无 resume (${validNoResume.length} 个):\n`));
226
+ for (const { projectPath } of validNoResume) {
227
+ console.log(`${chalk_1.default.green('✓')} ${chalk_1.default.bold(projectPath)} ${chalk_1.default.gray('(无 resume)')}`);
228
+ }
229
+ console.log();
230
+ }
231
+ // 打印无效项目(有 resume)
232
+ if ((filter === 'all' || filter === 'invalid') && invalidWithResume.length > 0) {
233
+ console.log(chalk_1.default.red.bold(`\n✗ 无效项目 - 有 resume (${invalidWithResume.length} 个):\n`));
234
+ for (const { projectPath, resumes } of invalidWithResume) {
235
+ console.log(`${chalk_1.default.red('✗')} ${chalk_1.default.gray(chalk_1.default.bold(projectPath))} ${chalk_1.default.gray(`(${resumes.length} 个 resume)`)}`);
236
+ resumes.forEach((resume, index) => {
237
+ const isLast = index === resumes.length - 1;
238
+ const connector = isLast ? '└── ' : '├── ';
239
+ // 一行格式: [session-id] 创建时间→最后活动时间 | 消息数 | 磁盘大小 | 压缩次数
240
+ const createdTime = (0, scanner_1.formatRelativeTime)(resume.createdAt.getTime());
241
+ const lastActivityTime = (0, scanner_1.formatRelativeTime)(resume.lastActivity.getTime());
242
+ const timeRange = `${createdTime} → ${lastActivityTime}`;
243
+ const messagesStr = `${resume.messageCount}条`;
244
+ const sizeStr = (0, backup_1.formatFileSize)(resume.fileSize);
245
+ const compactStr = resume.compactCount > 0 ? ` | ${resume.compactCount}次压缩` : '';
246
+ console.log(` ${connector}${chalk_1.default.cyan(`[${resume.sessionId}]`)} ${chalk_1.default.gray(timeRange)} | ${chalk_1.default.yellow(messagesStr)} | ${chalk_1.default.gray(sizeStr)}${chalk_1.default.magenta(compactStr)}`);
247
+ // 添加到删除选项(默认选中无效项目的 resume)
248
+ allResumeChoices.push({
249
+ name: `${chalk_1.default.gray(projectPath)} - [${resume.sessionId}] (${(0, scanner_1.formatRelativeTime)(resume.lastActivity.getTime())})`,
250
+ value: { projectPath, sessionId: resume.sessionId },
251
+ checked: true,
252
+ });
253
+ });
254
+ console.log();
255
+ }
256
+ }
257
+ // 打印无效项目(无 resume)
258
+ if ((filter === 'all' || filter === 'invalid') && invalidNoResume.length > 0) {
259
+ console.log(chalk_1.default.red.bold(`\n✗ 无效项目 - 无 resume (${invalidNoResume.length} 个):\n`));
260
+ for (const { projectPath } of invalidNoResume) {
261
+ console.log(`${chalk_1.default.red('✗')} ${chalk_1.default.gray(projectPath)} ${chalk_1.default.gray('(无 resume)')}`);
262
+ }
263
+ console.log();
264
+ }
265
+ // 打印孤立 resume(项目已删除但 resume 文件仍存在)
266
+ if ((filter === 'all' || filter === 'orphaned') && orphanedResumes.length > 0) {
267
+ console.log(chalk_1.default.yellow.bold(`\n⚠ 孤立 resume (${orphanedResumes.length} 个 - 项目已删除):\n`));
268
+ for (const { normalizedPath, resumes } of orphanedResumes) {
269
+ // 将 normalized path 转换回原始路径格式用于显示
270
+ const displayPath = normalizedPath.replace(/^-/, '/').replace(/-/g, '/');
271
+ console.log(`${chalk_1.default.yellow('⚠')} ${chalk_1.default.yellow.bold(displayPath)} ${chalk_1.default.gray(`(${resumes.length} 个 resume)`)}`);
272
+ resumes.forEach((resume, index) => {
273
+ const isLast = index === resumes.length - 1;
274
+ const connector = isLast ? '└── ' : '├── ';
275
+ // 一行格式: [session-id] 创建时间→最后活动时间 | 消息数 | 磁盘大小 | 压缩次数
276
+ const createdTime = (0, scanner_1.formatRelativeTime)(resume.createdAt.getTime());
277
+ const lastActivityTime = (0, scanner_1.formatRelativeTime)(resume.lastActivity.getTime());
278
+ const timeRange = `${createdTime} → ${lastActivityTime}`;
279
+ const messagesStr = `${resume.messageCount}条`;
280
+ const sizeStr = (0, backup_1.formatFileSize)(resume.fileSize);
281
+ const compactStr = resume.compactCount > 0 ? ` | ${resume.compactCount}次压缩` : '';
282
+ console.log(` ${connector}${chalk_1.default.cyan(`[${resume.sessionId}]`)} ${chalk_1.default.gray(timeRange)} | ${chalk_1.default.yellow(messagesStr)} | ${chalk_1.default.gray(sizeStr)}${chalk_1.default.magenta(compactStr)}`);
283
+ // 添加到删除选项(默认选中孤立 resume)
284
+ allResumeChoices.push({
285
+ name: `${chalk_1.default.yellow(displayPath)} - [${resume.sessionId}] (${(0, scanner_1.formatRelativeTime)(resume.lastActivity.getTime())})`,
286
+ value: { projectPath: normalizedPath, sessionId: resume.sessionId, isOrphaned: true },
287
+ checked: true,
288
+ });
289
+ });
290
+ console.log();
291
+ }
292
+ }
293
+ }
294
+ // 汇总(只显示符合条件的)
295
+ const totalResumes = allProjects.reduce((sum, p) => sum + p.resumes.length, 0);
296
+ const totalOrphanedResumes = orphanedResumes.reduce((sum, r) => sum + r.resumes.length, 0);
297
+ const validResumeCount = validWithResume.reduce((sum, p) => sum + p.resumes.length, 0);
298
+ const invalidResumeCount = invalidWithResume.reduce((sum, p) => sum + p.resumes.length, 0);
299
+ console.log(chalk_1.default.bold('\nResume 汇总:'));
300
+ if (filter === 'all' || filter === 'valid') {
301
+ console.log(` ${chalk_1.default.green('有效项目')}: ${validProjects.length} 个项目 (${validWithResume.length} 个有 resume, ${validNoResume.length} 个无 resume)`);
302
+ }
303
+ if (filter === 'all' || filter === 'invalid') {
304
+ console.log(` ${chalk_1.default.red('无效项目')}: ${invalidProjects.length} 个项目 (${invalidWithResume.length} 个有 resume, ${invalidNoResume.length} 个无 resume)`);
305
+ }
306
+ if (filter === 'all' || filter === 'orphaned') {
307
+ console.log(` ${chalk_1.default.yellow('孤立 resume')}: ${orphanedResumes.length} 个项目, ${totalOrphanedResumes} 个 resume`);
308
+ }
309
+ if (filter === 'all') {
310
+ console.log(` 总计: ${totalResumes + totalOrphanedResumes} 个 resume`);
311
+ }
312
+ // 如果允许删除,进入删除流程
313
+ // 注意:在删除模式下,allResumeChoices 可能为空(因为跳过了详细显示),需要单独检查
314
+ const hasResumesToDelete = validWithResume.length > 0 || invalidWithResume.length > 0 || orphanedResumes.length > 0;
315
+ if (allowDelete && hasResumesToDelete) {
316
+ // 按有效/无效/孤立分类显示项目列表(不含 resume 明细)
317
+ const projectChoices = [];
318
+ // 有效项目(默认不选中)
319
+ for (const { projectPath, resumes } of validWithResume) {
320
+ // 转换为标准化路径格式(与 .claude/projects 目录一致)
321
+ const normalizedPath = projectPath.replace(/\//g, '-');
322
+ projectChoices.push({
323
+ name: `${projectPath} (${resumes.length} 个 resume)`,
324
+ value: { projectPath: normalizedPath, isValid: true, isOrphaned: false, resumes },
325
+ checked: false,
326
+ });
327
+ }
328
+ // 无效项目(默认不选中,因为路径不存在但可能是误配置)
329
+ for (const { projectPath, resumes } of invalidWithResume) {
330
+ // 转换为标准化路径格式
331
+ const normalizedPath = projectPath.replace(/\//g, '-');
332
+ projectChoices.push({
333
+ name: `${projectPath} (${resumes.length} 个 resume)`,
334
+ value: { projectPath: normalizedPath, isValid: false, isOrphaned: false, resumes },
335
+ checked: false,
336
+ });
337
+ }
338
+ // 孤立项目(默认全选,因为项目已删除,resume 没有意义)
339
+ for (const { normalizedPath, resumes } of orphanedResumes) {
340
+ // 使用 .claude/projects 路径格式
341
+ const displayPath = `~/.claude/projects/${normalizedPath}`;
342
+ projectChoices.push({
343
+ name: `${displayPath} (${resumes.length} 个 resume)`,
344
+ value: { projectPath: normalizedPath, isValid: false, isOrphaned: true, resumes },
345
+ checked: true,
346
+ });
347
+ }
348
+ if (projectChoices.length === 0) {
349
+ console.log(chalk_1.default.yellow('\n没有包含 resume 的项目'));
350
+ return;
351
+ }
352
+ // 根据 filter 筛选可选项目
353
+ const filteredProjectChoices = projectChoices.filter(choice => {
354
+ if (filter === 'all')
355
+ return true;
356
+ if (filter === 'valid')
357
+ return choice.value.isValid && !choice.value.isOrphaned;
358
+ if (filter === 'invalid')
359
+ return !choice.value.isValid && !choice.value.isOrphaned;
360
+ if (filter === 'orphaned')
361
+ return choice.value.isOrphaned;
362
+ return true;
363
+ });
364
+ // 第一步:选择项目
365
+ console.log(chalk_1.default.bold('\n请选择要删除 resume 的项目:\n'));
366
+ // 根据项目类型生成带颜色的选项名称
367
+ const getProjectChoiceName = (c) => {
368
+ const statusIcon = c.value.isOrphaned ? '⚠' : c.value.isValid ? '✓' : '✗';
369
+ const statusColor = c.value.isOrphaned ? chalk_1.default.yellow : c.value.isValid ? chalk_1.default.green : chalk_1.default.red;
370
+ return `${statusColor(statusIcon)} ${c.name}`;
371
+ };
372
+ const selectedProjects = await (0, prompts_1.checkbox)({
373
+ message: '选择项目(空格选择,回车确认):',
374
+ choices: filteredProjectChoices.map(c => ({
375
+ name: getProjectChoiceName(c),
376
+ value: c.value,
377
+ checked: c.checked,
378
+ })),
379
+ pageSize: 15,
380
+ theme: {
381
+ style: {
382
+ keysHelpTip: () => undefined,
383
+ },
384
+ },
385
+ });
386
+ if (selectedProjects.length === 0) {
387
+ console.log(chalk_1.default.yellow('未选择任何项目,操作已取消'));
388
+ return;
389
+ }
390
+ // 收集所有选中项目的 resume
391
+ const allSelectedResumes = [];
392
+ for (const project of selectedProjects) {
393
+ for (const resume of project.resumes) {
394
+ allSelectedResumes.push({
395
+ name: '', // 动态生成
396
+ value: {
397
+ projectPath: project.projectPath,
398
+ sessionId: resume.sessionId,
399
+ isOrphaned: project.isOrphaned,
400
+ lastActivity: resume.lastActivity,
401
+ messageCount: resume.messageCount,
402
+ fileSize: resume.fileSize,
403
+ },
404
+ checked: true, // 默认全选
405
+ });
406
+ }
407
+ }
408
+ // 第二步:选择要删除的 resume
409
+ console.log(chalk_1.default.bold(`\n已选择 ${selectedProjects.length} 个项目,共 ${allSelectedResumes.length} 个 resume,请选择要删除的:\n`));
410
+ // 根据 resume 所属项目的类型生成带颜色的选项名称(完整路径 + 详细信息)
411
+ const getResumeChoiceName = (c) => {
412
+ const statusIcon = c.value.isOrphaned ? '⚠' : '✓';
413
+ const statusColor = c.value.isOrphaned ? chalk_1.default.yellow : chalk_1.default.green;
414
+ // 统一使用 .claude/projects 路径格式
415
+ const displayPath = `~/.claude/projects/${c.value.projectPath}`;
416
+ // 显示详细信息
417
+ const timeStr = c.value.lastActivity ? (0, scanner_1.formatRelativeTime)(c.value.lastActivity.getTime()) : 'N/A';
418
+ const msgStr = c.value.messageCount !== undefined ? `${c.value.messageCount}条` : 'N/A';
419
+ const sizeStr = c.value.fileSize ? (0, backup_1.formatFileSize)(c.value.fileSize) : 'N/A';
420
+ // 在一行内显示完整信息,不截断
421
+ return `${statusColor(statusIcon)} ${displayPath} | ${chalk_1.default.cyan(c.value.sessionId)} | ${timeStr} | ${msgStr} | ${sizeStr}`;
422
+ };
423
+ const selectedResumes = await (0, prompts_1.checkbox)({
424
+ message: '选择要删除的 resume(空格选择,回车确认,全选请按 a):',
425
+ choices: allSelectedResumes.map(c => ({
426
+ name: getResumeChoiceName(c),
427
+ value: c.value,
428
+ checked: c.checked,
429
+ })),
430
+ pageSize: 15,
431
+ theme: {
432
+ style: {
433
+ keysHelpTip: () => undefined,
434
+ },
435
+ },
436
+ });
437
+ if (selectedResumes.length === 0) {
438
+ console.log(chalk_1.default.yellow('未选择任何 resume,操作已取消'));
439
+ return;
440
+ }
441
+ // 第三步:确认删除
442
+ const confirmed = await (0, prompts_1.confirm)({
443
+ message: chalk_1.default.red(`确认删除选中的 ${selectedResumes.length} 个 resume?`),
444
+ default: false,
445
+ });
446
+ if (!confirmed) {
447
+ console.log(chalk_1.default.yellow('操作已取消'));
448
+ return;
449
+ }
450
+ // 执行删除
451
+ const deleteByProject = new Map();
452
+ for (const { projectPath, sessionId } of selectedResumes) {
453
+ const set = deleteByProject.get(projectPath) || new Set();
454
+ set.add(sessionId);
455
+ deleteByProject.set(projectPath, set);
456
+ }
457
+ let totalDeleted = 0;
458
+ for (const [projectPath, sessionIds] of deleteByProject) {
459
+ const deleted = await (0, scanner_1.deleteProjectResumes)(projectPath, sessionIds);
460
+ totalDeleted += deleted;
461
+ }
462
+ console.log(chalk_1.default.green(`\n✓ 成功删除 ${totalDeleted} 个 resume!`));
463
+ }
464
+ }
465
+ const version = '1.0.0';
466
+ // ============================================================
467
+ // 主程序配置
468
+ // ============================================================
469
+ commander_1.program
470
+ .name('lcch')
471
+ .description('Claude Code Helper - 管理 Claude Code 配置的 CLI 工具')
472
+ .version(version, '--version', '显示版本号')
473
+ .usage('<command> [options]')
474
+ .addHelpText('after', `
475
+ ${chalk_1.default.bold('常用命令:')}
476
+
477
+ ${chalk_1.default.bold('项目管理:')}
478
+ ${chalk_1.default.cyan('lcch projects list')} 列出所有项目
479
+ ${chalk_1.default.cyan('lcch projects list --tree')} 树形结构显示项目
480
+ ${chalk_1.default.cyan('lcch projects list --verbose')} 显示详细统计信息
481
+ ${chalk_1.default.cyan('lcch projects list --sessions')} 显示会话历史
482
+ ${chalk_1.default.cyan('lcch projects list --resume')} 显示 resume 历史
483
+ ${chalk_1.default.cyan('lcch projects clean')} 交互式清理无效项目
484
+ ${chalk_1.default.cyan('lcch projects clean --force')} 强制清理无效项目
485
+ ${chalk_1.default.cyan('lcch project <path>')} 查看单个项目信息
486
+ ${chalk_1.default.cyan('lcch project <path> --resume')} 查看项目 resume 历史
487
+
488
+ ${chalk_1.default.bold('备份管理:')}
489
+ ${chalk_1.default.cyan('lcch backup_config list')} 列出所有备份(按类型分组)
490
+ ${chalk_1.default.cyan('lcch backup_config create')} 交互式创建备份(默认备份全部)
491
+ ${chalk_1.default.cyan('lcch backup_config restore')} 恢复备份
492
+ ${chalk_1.default.cyan('lcch backup_config delete')} 删除备份
493
+
494
+ ${chalk_1.default.bold('缓存和安全:')}
495
+ ${chalk_1.default.cyan('lcch cache')} 查看缓存信息
496
+ ${chalk_1.default.cyan('lcch cache --json')} 以 JSON 格式输出
497
+ ${chalk_1.default.cyan('lcch scan-secrets')} 扫描缓存中的密钥
498
+ ${chalk_1.default.cyan('lcch scan-secrets --show')} 显示完整密钥
499
+ ${chalk_1.default.cyan('lcch scan-secrets --mask')} 扫描并掩码项目中的敏感信息
500
+ ${chalk_1.default.cyan('lcch scan-secrets --mask --restore <项目目录>')} 恢复项目所有备份
501
+
502
+ ${chalk_1.default.bold('配置信息:')}
503
+ ${chalk_1.default.cyan('lcch config path')} 显示配置文件路径
504
+ ${chalk_1.default.cyan('lcch config stats')} 显示详细统计信息
505
+
506
+ ${chalk_1.default.bold('MCP 和 Hooks:')}
507
+ ${chalk_1.default.cyan('lcch mcp list')} 列出 MCP 服务器
508
+ ${chalk_1.default.cyan('lcch mcp info')} 查看 MCP 服务器详细配置(交互式)
509
+ ${chalk_1.default.cyan('lcch hooks list')} 列出 hooks
510
+
511
+ ${chalk_1.default.bold('查看子命令帮助:')}
512
+ ${chalk_1.default.cyan('lcch projects -h')} 查看 projects 命令帮助
513
+ ${chalk_1.default.cyan('lcch projects list -h')} 查看 list 子命令帮助
514
+ ${chalk_1.default.cyan('lcch project -h')} 查看 project 命令帮助
515
+ ${chalk_1.default.cyan('lcch backup_config -h')} 查看 backup_config 命令帮助
516
+ ${chalk_1.default.cyan('lcch cache -h')} 查看 cache 命令帮助
517
+ `);
518
+ // ============================================================
519
+ // projects 子命令 - 项目管理
520
+ // ============================================================
521
+ const projectsCmd = commander_1.program
522
+ .command('projects')
523
+ .description('管理 Claude Code 项目')
524
+ .addHelpText('after', `
525
+ ${chalk_1.default.bold('子命令:')}
526
+ ${chalk_1.default.cyan('list [options]')} 列出所有项目
527
+ ${chalk_1.default.cyan('clean [options]')} 清理无效项目
528
+ ${chalk_1.default.cyan('export [options]')} 导出项目会话
529
+
530
+ ${chalk_1.default.bold('常用示例:')}
531
+ ${chalk_1.default.cyan('lcch projects list')} 列出所有项目
532
+ ${chalk_1.default.cyan('lcch projects list --tree')} 树形结构显示
533
+ ${chalk_1.default.cyan('lcch projects list --tree-verbose')} 树形显示详细统计
534
+ ${chalk_1.default.cyan('lcch projects list --verbose')} 列表显示详细统计
535
+ ${chalk_1.default.cyan('lcch projects list --sessions')} 显示会话历史
536
+ ${chalk_1.default.cyan('lcch projects list --sessions --filter orphaned')} 只显示孤立会话
537
+ ${chalk_1.default.cyan('lcch projects list --resume')} 显示 resume 历史
538
+ ${chalk_1.default.cyan('lcch projects list --resume --filter invalid')} 只显示无效项目的 resume
539
+ ${chalk_1.default.cyan('lcch projects list --resume --delete-resumes')} 删除 resume
540
+ ${chalk_1.default.cyan('lcch projects clean')} 交互式清理
541
+ ${chalk_1.default.cyan('lcch projects clean --all')} 清理所有无效项目
542
+ ${chalk_1.default.cyan('lcch projects clean --force')} 强制清理(不确认)
543
+ ${chalk_1.default.cyan('lcch projects export')} 交互式导出会话
544
+ ${chalk_1.default.cyan('lcch projects export --format json')} 导出为 JSON
545
+ ${chalk_1.default.cyan('lcch projects export --format markdown')} 导出为 Markdown
546
+ ${chalk_1.default.cyan('lcch projects export --format html')} 导出为 HTML 交互式
547
+ ${chalk_1.default.cyan('lcch projects export --output-dir ./exports')} 指定输出目录
548
+ `);
549
+ // lcch projects list - 列出所有项目
550
+ projectsCmd
551
+ .command('list')
552
+ .description('列出所有项目')
553
+ .option('--tree', '以树形结构显示', false)
554
+ .option('--sessions', '显示每个项目的会话历史', false)
555
+ .option('--resume', '显示每个项目的 resume 历史', false)
556
+ .option('--delete-resumes', '删除选中的 resume(需配合 --resume 使用)', false)
557
+ .option('--tree-verbose', '以树形结构显示,并显示详细统计信息', false)
558
+ .option('--verbose', '以列表形式显示详细统计信息', false)
559
+ .option('--filter <type>', '过滤类型:valid(有效)|invalid(无效)|orphaned(孤立)|all(全部)', 'all')
560
+ .action(async (options) => {
561
+ try {
562
+ const config = await (0, scanner_1.readConfig)();
563
+ const result = await (0, scanner_1.scanProjects)(config);
564
+ // 验证 filter 参数
565
+ const validFilters = ['valid', 'invalid', 'orphaned', 'all'];
566
+ if (!validFilters.includes(options.filter)) {
567
+ console.error(chalk_1.default.red(`错误: --filter 参数必须是其中之一: ${validFilters.join(', ')}`));
568
+ process.exit(1);
569
+ }
570
+ if (options.sessions) {
571
+ // 显示会话历史
572
+ await printProjectSessions(config, result, options.filter);
573
+ return;
574
+ }
575
+ if (options.resume) {
576
+ // 显示 resume 历史
577
+ await printProjectResumes(config, result, options.deleteResumes, options.filter);
578
+ return;
579
+ }
580
+ if (options.tree) {
581
+ // 树形结构显示
582
+ console.log(chalk_1.default.bold(`\n共 ${result.totalCount} 个项目:\n`));
583
+ const allProjects = [...result.validProjects, ...result.invalidProjects];
584
+ const tree = (0, scanner_1.buildProjectTree)(allProjects);
585
+ const validSet = new Set(result.validProjects);
586
+ const invalidSet = new Set(result.invalidProjects);
587
+ (0, scanner_1.printProjectTree)(tree, '', true, validSet, invalidSet);
588
+ console.log();
589
+ printSummary(result);
590
+ return;
591
+ }
592
+ if (options.treeVerbose) {
593
+ // 树形结构显示(详细信息)
594
+ await printProjectTreeVerbose(config, result);
595
+ return;
596
+ }
597
+ if (options.verbose) {
598
+ // 列表显示(详细信息)
599
+ await printProjectListVerbose(config, result);
600
+ return;
601
+ }
602
+ console.log(chalk_1.default.bold(`\n共 ${result.totalCount} 个项目:\n`));
603
+ // 显示有效项目
604
+ if (result.validProjects.length > 0) {
605
+ console.log(chalk_1.default.green.bold('有效项目:'));
606
+ for (const projectPath of result.validProjects) {
607
+ const info = (0, scanner_1.getProjectInfo)(config, projectPath);
608
+ console.log(` ${chalk_1.default.green('✓')} ${projectPath}`);
609
+ }
610
+ console.log();
611
+ }
612
+ // 显示无效项目
613
+ if (result.invalidProjects.length > 0) {
614
+ console.log(chalk_1.default.red.bold('无效项目(路径不存在):'));
615
+ for (const projectPath of result.invalidProjects) {
616
+ const info = (0, scanner_1.getProjectInfo)(config, projectPath);
617
+ console.log(` ${chalk_1.default.red('✗')} ${projectPath}`);
618
+ }
619
+ console.log();
620
+ }
621
+ printSummary(result);
622
+ }
623
+ catch (error) {
624
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
625
+ process.exit(1);
626
+ }
627
+ });
628
+ // lcch projects clean - 清理无效项目
629
+ projectsCmd
630
+ .command('clean')
631
+ .description('清理无效项目')
632
+ .option('--force', '强制清理,不提示确认', false)
633
+ .option('--all', '清理所有无效项目(默认行为)', false)
634
+ .action(async (options) => {
635
+ try {
636
+ const config = await (0, scanner_1.readConfig)();
637
+ const result = await (0, scanner_1.scanProjects)(config);
638
+ if (result.invalidProjects.length === 0) {
639
+ console.log(chalk_1.default.green('没有发现无效项目,无需清理!'));
640
+ return;
641
+ }
642
+ printScanResult(result);
643
+ let projectsToDelete;
644
+ if (options.force) {
645
+ // 强制模式:直接删除所有无效项目
646
+ projectsToDelete = result.invalidProjects;
647
+ }
648
+ else if (options.all) {
649
+ // 删除所有,但需要确认
650
+ projectsToDelete = result.invalidProjects;
651
+ const { confirm } = await inquirer_1.default.prompt([{
652
+ type: 'confirm',
653
+ name: 'confirm',
654
+ message: chalk_1.default.red(`确认删除所有 ${projectsToDelete.length} 个无效项目?`),
655
+ default: false,
656
+ }]);
657
+ if (!confirm) {
658
+ console.log(chalk_1.default.yellow('操作已取消'));
659
+ return;
660
+ }
661
+ }
662
+ else {
663
+ // 交互式选择要删除的项目
664
+ const selectedProjects = await (0, prompts_1.checkbox)({
665
+ message: '选择要删除的无效项目(空格选择,回车确认,a 全选):',
666
+ choices: result.invalidProjects.map(p => ({
667
+ name: p,
668
+ value: p,
669
+ checked: false,
670
+ })),
671
+ pageSize: 20,
672
+ theme: {
673
+ style: {
674
+ keysHelpTip: () => undefined,
675
+ },
676
+ },
677
+ });
678
+ if (selectedProjects.length === 0) {
679
+ console.log(chalk_1.default.yellow('未选择任何项目,操作已取消'));
680
+ return;
681
+ }
682
+ projectsToDelete = selectedProjects;
683
+ // 确认删除
684
+ const { confirm } = await inquirer_1.default.prompt([{
685
+ type: 'confirm',
686
+ name: 'confirm',
687
+ message: chalk_1.default.red(`确认删除选中的 ${projectsToDelete.length} 个项目?`),
688
+ default: false,
689
+ }]);
690
+ if (!confirm) {
691
+ console.log(chalk_1.default.yellow('操作已取消'));
692
+ return;
693
+ }
694
+ }
695
+ // 创建备份
696
+ const backupPath = await (0, backup_1.createBackup)('config');
697
+ console.log(chalk_1.default.green(`\n✓ 已创建备份: ${path.basename(backupPath)}`));
698
+ console.log(chalk_1.default.gray(` 恢复命令: lcch backup_config restore ${path.basename(backupPath)}`));
699
+ // 执行删除
700
+ (0, scanner_1.removeProjects)(config, projectsToDelete);
701
+ await (0, scanner_1.writeConfig)(config);
702
+ console.log(chalk_1.default.green(`✓ 成功清理 ${projectsToDelete.length} 个项目!`));
703
+ console.log(chalk_1.default.gray(` 恢复命令: lcch backup_config restore ${path.basename(backupPath)}`));
704
+ console.log(chalk_1.default.gray(` 剩余 ${result.totalCount - projectsToDelete.length} 个项目`));
705
+ }
706
+ catch (error) {
707
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
708
+ process.exit(1);
709
+ }
710
+ });
711
+ // lcch projects export - 导出项目会话
712
+ projectsCmd
713
+ .command('export')
714
+ .description('导出项目会话')
715
+ .option('--format <format>', '导出格式 (json|markdown|html)', '')
716
+ .option('--output-dir <dir>', '输出目录', '')
717
+ .action(async (options) => {
718
+ try {
719
+ const config = await (0, scanner_1.readConfig)();
720
+ const result = await (0, scanner_1.scanProjects)(config);
721
+ if (result.validProjects.length === 0) {
722
+ console.log(chalk_1.default.yellow('没有发现有效项目'));
723
+ return;
724
+ }
725
+ // 检查哪些项目有 resume 数据
726
+ const projectsWithSessions = [];
727
+ for (const projectPath of result.validProjects) {
728
+ const sessions = await (0, export_1.readProjectSessions)(projectPath);
729
+ if (sessions.length > 0) {
730
+ projectsWithSessions.push(projectPath);
731
+ }
732
+ }
733
+ if (projectsWithSessions.length === 0) {
734
+ console.log(chalk_1.default.yellow('没有找到任何项目的会话数据'));
735
+ return;
736
+ }
737
+ console.log(chalk_1.default.bold(`\n发现 ${projectsWithSessions.length} 个项目有会话数据:\n`));
738
+ // 选择要导出的项目
739
+ const selectedProjects = await (0, prompts_1.checkbox)({
740
+ message: '选择要导出会话的项目(空格选择,回车确认):',
741
+ choices: projectsWithSessions.map(p => ({
742
+ name: `${path.basename(p)} ${chalk_1.default.gray(p)}`,
743
+ value: p,
744
+ checked: false,
745
+ })),
746
+ pageSize: 20,
747
+ theme: {
748
+ style: {
749
+ keysHelpTip: () => undefined,
750
+ },
751
+ },
752
+ });
753
+ if (selectedProjects.length === 0) {
754
+ console.log(chalk_1.default.yellow('\n未选择任何项目,操作已取消'));
755
+ return;
756
+ }
757
+ // 保存项目到会话ID列表的映射
758
+ const projectSessionsMap = new Map();
759
+ const allSessions = [];
760
+ for (const projectPath of selectedProjects) {
761
+ const resumeInfos = await (0, scanner_1.getProjectResumeInfo)(projectPath);
762
+ const sessionIds = [];
763
+ for (const info of resumeInfos) {
764
+ sessionIds.push(info.sessionId);
765
+ allSessions.push({
766
+ projectPath,
767
+ sessionId: info.sessionId,
768
+ label: `${path.basename(projectPath)} | ${info.sessionId.substring(0, 8)}... | ${info.messageCount} 条消息 | ${(0, scanner_1.formatRelativeTime)(info.lastActivity.getTime())}`,
769
+ messageCount: info.messageCount,
770
+ });
771
+ }
772
+ projectSessionsMap.set(projectPath, sessionIds);
773
+ }
774
+ // 选择要导出的会话
775
+ let exportOptions = { sessions: [], roles: [], contentTypes: [] };
776
+ if (allSessions.length > 0) {
777
+ console.log(chalk_1.default.bold(`\n共发现 ${allSessions.length} 个会话\n`));
778
+ const selectedSessions = await (0, prompts_1.checkbox)({
779
+ message: '选择要导出的会话(空格选择,回车确认):',
780
+ choices: allSessions.map(s => ({
781
+ name: s.label,
782
+ value: `${s.projectPath}|${s.sessionId}`, // 使用项目路径+会话ID作为唯一值
783
+ checked: true, // 默认全选
784
+ })),
785
+ pageSize: 20,
786
+ theme: {
787
+ style: {
788
+ keysHelpTip: () => undefined,
789
+ },
790
+ },
791
+ });
792
+ // 解析选中的会话,按项目分组
793
+ const selectedProjectSessions = new Map();
794
+ for (const value of selectedSessions) {
795
+ const [projectPath, sessionId] = value.split('|');
796
+ if (!selectedProjectSessions.has(projectPath)) {
797
+ selectedProjectSessions.set(projectPath, []);
798
+ }
799
+ selectedProjectSessions.get(projectPath).push(sessionId);
800
+ }
801
+ // 保存到导出选项中(后续会根据项目路径取对应的会话ID)
802
+ // 这里我们直接存储到临时变量中,稍后在导出时使用
803
+ exportOptions.projectSessionsMap = selectedProjectSessions;
804
+ }
805
+ // 选择要导出的角色
806
+ const selectedRoles = await (0, prompts_1.checkbox)({
807
+ message: '选择要导出的角色:',
808
+ choices: [
809
+ { name: '用户 (user)', value: 'user', checked: true },
810
+ { name: '助手 (assistant)', value: 'assistant', checked: true },
811
+ ],
812
+ pageSize: 10,
813
+ theme: {
814
+ style: {
815
+ keysHelpTip: () => undefined,
816
+ },
817
+ },
818
+ });
819
+ exportOptions.roles = selectedRoles;
820
+ // 选择要导出的内容类型
821
+ const contentTypeChoices = [
822
+ { name: '文本内容', value: 'text', checked: true },
823
+ { name: '工具调用请求 (tool_use)', value: 'tool_use', checked: true },
824
+ { name: '工具执行结果 (tool_result)', value: 'tool_result', checked: true },
825
+ { name: '思考过程 (thinking)', value: 'thinking', checked: true },
826
+ ];
827
+ const selectedContentTypes = await (0, prompts_1.checkbox)({
828
+ message: '选择要导出的内容类型:',
829
+ choices: contentTypeChoices,
830
+ pageSize: 10,
831
+ theme: {
832
+ style: {
833
+ keysHelpTip: () => undefined,
834
+ },
835
+ },
836
+ });
837
+ exportOptions.contentTypes = selectedContentTypes;
838
+ // 选择导出格式
839
+ let exportFormat;
840
+ const validFormats = ['json', 'markdown', 'html', 'text'];
841
+ if (validFormats.includes(options.format)) {
842
+ exportFormat = options.format;
843
+ }
844
+ else {
845
+ const { format } = await inquirer_1.default.prompt([{
846
+ type: 'list',
847
+ name: 'format',
848
+ message: '选择导出格式:',
849
+ choices: [
850
+ { name: 'JSON', value: 'json' },
851
+ { name: 'Markdown', value: 'markdown' },
852
+ { name: 'HTML 交互式', value: 'html' },
853
+ { name: '纯文本', value: 'text' },
854
+ ],
855
+ }]);
856
+ exportFormat = format;
857
+ }
858
+ // 选择输出目录
859
+ let outputDir;
860
+ if (options.outputDir) {
861
+ outputDir = options.outputDir;
862
+ }
863
+ else {
864
+ const defaultDir = (0, export_1.getDefaultOutputDir)();
865
+ const { dir } = await inquirer_1.default.prompt([{
866
+ type: 'input',
867
+ name: 'dir',
868
+ message: '输入输出目录:',
869
+ default: defaultDir,
870
+ }]);
871
+ outputDir = dir;
872
+ }
873
+ // 确保输出目录存在
874
+ await fs.ensureDir(outputDir);
875
+ // 确认导出
876
+ const formatNameMap = {
877
+ json: 'JSON',
878
+ markdown: 'Markdown',
879
+ html: 'HTML 交互式',
880
+ };
881
+ const formatName = formatNameMap[exportFormat] || exportFormat;
882
+ console.log(chalk_1.default.bold(`\n导出信息:`));
883
+ console.log(` 选中项目: ${selectedProjects.length} 个`);
884
+ console.log(` 导出格式: ${formatName}`);
885
+ console.log(` 输出目录: ${outputDir}`);
886
+ const { confirm } = await inquirer_1.default.prompt([{
887
+ type: 'confirm',
888
+ name: 'confirm',
889
+ message: '\n确认导出?',
890
+ default: true,
891
+ }]);
892
+ if (!confirm) {
893
+ console.log(chalk_1.default.yellow('操作已取消'));
894
+ return;
895
+ }
896
+ // 执行导出
897
+ console.log(chalk_1.default.bold('\n正在导出...\n'));
898
+ const exportResults = [];
899
+ const usedNames = new Set();
900
+ for (const projectPath of selectedProjects) {
901
+ // 为当前项目获取对应的会话ID
902
+ let projectSessionIds = [];
903
+ // 检查是否选择了会话
904
+ const projectSessionsMap = exportOptions.projectSessionsMap;
905
+ if (projectSessionsMap && projectSessionsMap.has(projectPath)) {
906
+ // 用户选择了具体会话
907
+ projectSessionIds = projectSessionsMap.get(projectPath) || [];
908
+ }
909
+ // 如果 projectSessionIds 为空数组,则表示导出全部会话(由 filterSessions 函数处理)
910
+ const result = await (0, export_1.exportProjectSessions)(projectPath, exportFormat, outputDir, usedNames, { ...exportOptions, sessions: projectSessionIds });
911
+ if (result) {
912
+ exportResults.push(result);
913
+ console.log(`${chalk_1.default.green('✓')} ${path.basename(projectPath)} → ${chalk_1.default.cyan(result.fileName)}`);
914
+ console.log(` ${chalk_1.default.gray(`${result.sessionCount} 个会话, ${result.messageCount} 条消息`)}`);
915
+ }
916
+ else {
917
+ console.log(`${chalk_1.default.yellow('⚠')} ${path.basename(projectPath)} ${chalk_1.default.gray('(无会话数据)')}`);
918
+ }
919
+ }
920
+ // 显示导出结果
921
+ if (exportResults.length > 0) {
922
+ console.log(chalk_1.default.green(`\n✓ 成功导出 ${exportResults.length} 个项目!\n`));
923
+ console.log(chalk_1.default.bold('导出文件:'));
924
+ for (const result of exportResults) {
925
+ console.log(` ${chalk_1.default.cyan(result.fileName)}`);
926
+ console.log(` ${chalk_1.default.gray(result.projectPath)}`);
927
+ }
928
+ console.log(chalk_1.default.gray(`\n输出目录: ${outputDir}`));
929
+ }
930
+ else {
931
+ console.log(chalk_1.default.yellow('\n没有成功导出任何项目'));
932
+ }
933
+ }
934
+ catch (error) {
935
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
936
+ process.exit(1);
937
+ }
938
+ });
939
+ // ============================================================
940
+ // backup 子命令 - 备份管理
941
+ // ============================================================
942
+ const backupCmd = commander_1.program
943
+ .command('backup_config')
944
+ .description('管理 Claude Code 配置备份')
945
+ .addHelpText('after', `
946
+ ${chalk_1.default.bold('子命令:')}
947
+ ${chalk_1.default.cyan('list')} 列出所有备份文件(按类型分组)
948
+ ${chalk_1.default.cyan('create')} 交互式创建配置备份(默认备份 .claude.json 和 settings.json)
949
+ ${chalk_1.default.cyan('restore [file]')} 恢复配置备份
950
+ ${chalk_1.default.cyan('delete [file]')} 删除备份文件
951
+
952
+ ${chalk_1.default.bold('常用示例:')}
953
+ ${chalk_1.default.cyan('lcch backup_config list')} 列出所有备份
954
+ ${chalk_1.default.cyan('lcch backup_config create')} 交互式选择创建备份
955
+ ${chalk_1.default.cyan('lcch backup_config create --target config')} 只备份 .claude.json
956
+ ${chalk_1.default.cyan('lcch backup_config restore')} 交互式选择恢复
957
+ ${chalk_1.default.cyan('lcch backup_config restore <file>')} 直接恢复指定备份
958
+ ${chalk_1.default.cyan('lcch backup_config delete')} 交互式选择删除
959
+ ${chalk_1.default.cyan('lcch backup_config delete <file>')} 直接删除指定备份
960
+ `);
961
+ // lcch backup_config list - 列出备份
962
+ backupCmd
963
+ .command('list')
964
+ .description('列出所有备份文件')
965
+ .option('--target <target>', '只显示指定类型的备份 (config|settings)')
966
+ .action(async (options) => {
967
+ try {
968
+ let backups = await (0, backup_1.listBackups)();
969
+ if (options.target) {
970
+ if (options.target !== 'config' && options.target !== 'settings') {
971
+ console.error(chalk_1.default.red('错误: --target 必须是 config 或 settings'));
972
+ process.exit(1);
973
+ }
974
+ backups = backups.filter(b => b.target === options.target);
975
+ }
976
+ if (backups.length === 0) {
977
+ console.log(chalk_1.default.yellow('没有找到备份文件'));
978
+ return;
979
+ }
980
+ // 按类型分组
981
+ const configBackups = backups.filter(b => b.target === 'config');
982
+ const settingsBackups = backups.filter(b => b.target === 'settings');
983
+ if (configBackups.length > 0) {
984
+ console.log(chalk_1.default.bold(`\n.claude.json 备份 (${configBackups.length}个):`));
985
+ configBackups.forEach((backup, index) => {
986
+ const isAuto = backup.filename.includes('backup_auto_before_restore');
987
+ console.log(` ${index + 1}. ${chalk_1.default.cyan(backup.filename)}${isAuto ? chalk_1.default.gray(' (自动备份)') : ''}`);
988
+ console.log(` ${chalk_1.default.gray('时间:')} ${(0, scanner_1.formatDate)(backup.createdAt)} ${chalk_1.default.gray('大小:')} ${(0, backup_1.formatFileSize)(backup.size)}`);
989
+ });
990
+ }
991
+ if (settingsBackups.length > 0) {
992
+ console.log(chalk_1.default.bold(`\nsettings.json 备份 (${settingsBackups.length}个):`));
993
+ settingsBackups.forEach((backup, index) => {
994
+ const isAuto = backup.filename.includes('backup_auto_before_restore');
995
+ console.log(` ${index + 1}. ${chalk_1.default.cyan(backup.filename)}${isAuto ? chalk_1.default.gray(' (自动备份)') : ''}`);
996
+ console.log(` ${chalk_1.default.gray('时间:')} ${(0, scanner_1.formatDate)(backup.createdAt)} ${chalk_1.default.gray('大小:')} ${(0, backup_1.formatFileSize)(backup.size)}`);
997
+ });
998
+ }
999
+ console.log();
1000
+ }
1001
+ catch (error) {
1002
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1003
+ process.exit(1);
1004
+ }
1005
+ });
1006
+ // lcch backup_config create - 创建备份
1007
+ backupCmd
1008
+ .command('create')
1009
+ .description('手动创建配置备份')
1010
+ .option('--target <target>', '指定备份目标 (config|settings)')
1011
+ .action(async (options) => {
1012
+ try {
1013
+ let targets = [];
1014
+ if (options.target) {
1015
+ if (options.target !== 'config' && options.target !== 'settings') {
1016
+ console.error(chalk_1.default.red('错误: --target 必须是 config 或 settings'));
1017
+ process.exit(1);
1018
+ }
1019
+ targets = [options.target];
1020
+ }
1021
+ else {
1022
+ // 交互式多选
1023
+ const { selectedTargets } = await inquirer_1.default.prompt([{
1024
+ type: 'checkbox',
1025
+ name: 'selectedTargets',
1026
+ message: '选择要备份的文件(空格选择,回车确认):',
1027
+ choices: [
1028
+ { name: '~/.claude.json', value: 'config', checked: true },
1029
+ { name: '~/.claude/settings.json', value: 'settings', checked: true },
1030
+ ],
1031
+ }]);
1032
+ if (selectedTargets.length === 0) {
1033
+ console.log(chalk_1.default.yellow('未选择任何文件,操作已取消'));
1034
+ return;
1035
+ }
1036
+ targets = selectedTargets;
1037
+ }
1038
+ for (const target of targets) {
1039
+ const label = target === 'config' ? '.claude.json' : 'settings.json';
1040
+ const spinner = createSpinner(`正在备份 ${label}...`);
1041
+ spinner.start();
1042
+ const backupPath = await (0, backup_1.createBackup)(target);
1043
+ spinner.stop();
1044
+ console.log(chalk_1.default.green(`✓ 备份已创建: ${path.basename(backupPath)}`));
1045
+ }
1046
+ }
1047
+ catch (error) {
1048
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1049
+ process.exit(1);
1050
+ }
1051
+ });
1052
+ // lcch backup_config restore - 恢复备份
1053
+ backupCmd
1054
+ .command('restore [file]')
1055
+ .description('恢复配置备份')
1056
+ .option('--force', '强制恢复,不提示确认', false)
1057
+ .action(async (file, options) => {
1058
+ try {
1059
+ let backupPath;
1060
+ if (file) {
1061
+ // 使用指定的文件
1062
+ if (path.isAbsolute(file)) {
1063
+ backupPath = file;
1064
+ }
1065
+ else {
1066
+ // 通过文件名前缀推断目录
1067
+ let backupDir;
1068
+ if (file.startsWith('.claude.json.')) {
1069
+ backupDir = path.join(os.homedir(), '.lcch', 'backups', 'config');
1070
+ }
1071
+ else if (file.startsWith('settings.json.')) {
1072
+ backupDir = path.join(os.homedir(), '.lcch', 'backups', 'settings');
1073
+ }
1074
+ else {
1075
+ console.error(chalk_1.default.red('错误: 无法识别的备份文件名'));
1076
+ process.exit(1);
1077
+ }
1078
+ backupPath = path.join(backupDir, file);
1079
+ }
1080
+ }
1081
+ else {
1082
+ // 交互式选择
1083
+ const backups = await (0, backup_1.listBackups)();
1084
+ if (backups.length === 0) {
1085
+ console.log(chalk_1.default.yellow('没有找到备份文件'));
1086
+ return;
1087
+ }
1088
+ const { selectedBackup } = await inquirer_1.default.prompt([{
1089
+ type: 'list',
1090
+ name: 'selectedBackup',
1091
+ message: '选择要恢复的备份:',
1092
+ choices: backups.map(b => ({
1093
+ name: `${b.filename} (${b.target === 'config' ? '主配置' : 'Settings'}, ${(0, scanner_1.formatDate)(b.createdAt)}, ${(0, backup_1.formatFileSize)(b.size)})`,
1094
+ value: b.path,
1095
+ })),
1096
+ }]);
1097
+ backupPath = selectedBackup;
1098
+ }
1099
+ if (!await fs.pathExists(backupPath)) {
1100
+ console.error(chalk_1.default.red(`备份文件不存在: ${backupPath}`));
1101
+ process.exit(1);
1102
+ }
1103
+ if (!options.force) {
1104
+ const { confirmRestore } = await inquirer_1.default.prompt([{
1105
+ type: 'confirm',
1106
+ name: 'confirmRestore',
1107
+ message: chalk_1.default.yellow(`确认从 ${path.basename(backupPath)} 恢复配置?`),
1108
+ default: false,
1109
+ }]);
1110
+ if (!confirmRestore) {
1111
+ console.log(chalk_1.default.yellow('操作已取消'));
1112
+ return;
1113
+ }
1114
+ }
1115
+ await (0, backup_1.restoreBackup)(backupPath);
1116
+ const filename = path.basename(backupPath);
1117
+ const targetLabel = filename.startsWith('.claude.json.') ? '.claude.json' : 'settings.json';
1118
+ console.log(chalk_1.default.green(`✓ ${targetLabel} 配置已成功恢复!`));
1119
+ console.log(chalk_1.default.gray(` 恢复前已创建自动备份(覆盖旧备份)`));
1120
+ }
1121
+ catch (error) {
1122
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1123
+ process.exit(1);
1124
+ }
1125
+ });
1126
+ // lcch backup_config delete - 删除备份
1127
+ backupCmd
1128
+ .command('delete [file]')
1129
+ .description('删除备份文件')
1130
+ .action(async (file) => {
1131
+ try {
1132
+ if (file) {
1133
+ let backupPath;
1134
+ if (path.isAbsolute(file)) {
1135
+ backupPath = file;
1136
+ }
1137
+ else {
1138
+ // 通过文件名前缀推断目录
1139
+ let backupDir;
1140
+ if (file.startsWith('.claude.json.')) {
1141
+ backupDir = path.join(os.homedir(), '.lcch', 'backups', 'config');
1142
+ }
1143
+ else if (file.startsWith('settings.json.')) {
1144
+ backupDir = path.join(os.homedir(), '.lcch', 'backups', 'settings');
1145
+ }
1146
+ else {
1147
+ console.error(chalk_1.default.red('错误: 无法识别的备份文件名'));
1148
+ process.exit(1);
1149
+ }
1150
+ backupPath = path.join(backupDir, file);
1151
+ }
1152
+ const { confirmDelete } = await inquirer_1.default.prompt([{
1153
+ type: 'confirm',
1154
+ name: 'confirmDelete',
1155
+ message: `确认删除 ${path.basename(backupPath)}?`,
1156
+ default: false,
1157
+ }]);
1158
+ if (!confirmDelete) {
1159
+ console.log(chalk_1.default.yellow('操作已取消'));
1160
+ return;
1161
+ }
1162
+ await (0, backup_1.deleteBackup)(backupPath);
1163
+ console.log(chalk_1.default.green('✓ 备份已删除'));
1164
+ }
1165
+ else {
1166
+ // 交互式选择多个,按类型分组
1167
+ const backups = await (0, backup_1.listBackups)();
1168
+ if (backups.length === 0) {
1169
+ console.log(chalk_1.default.yellow('没有找到备份文件'));
1170
+ return;
1171
+ }
1172
+ const configBackups = backups.filter(b => b.target === 'config');
1173
+ const settingsBackups = backups.filter(b => b.target === 'settings');
1174
+ const choices = [];
1175
+ if (configBackups.length > 0) {
1176
+ choices.push(new prompts_1.Separator('.claude.json 备份:'));
1177
+ choices.push(...configBackups.map(b => ({
1178
+ name: `${b.filename} (${(0, scanner_1.formatDate)(b.createdAt)})`,
1179
+ value: b.path,
1180
+ })));
1181
+ }
1182
+ if (settingsBackups.length > 0) {
1183
+ choices.push(new prompts_1.Separator('settings.json 备份:'));
1184
+ choices.push(...settingsBackups.map(b => ({
1185
+ name: `${b.filename} (${(0, scanner_1.formatDate)(b.createdAt)})`,
1186
+ value: b.path,
1187
+ })));
1188
+ }
1189
+ const selectedBackups = await (0, prompts_1.checkbox)({
1190
+ message: '选择要删除的备份(空格选择,回车确认):',
1191
+ choices,
1192
+ theme: {
1193
+ style: {
1194
+ keysHelpTip: () => undefined,
1195
+ },
1196
+ },
1197
+ });
1198
+ if (selectedBackups.length === 0) {
1199
+ console.log(chalk_1.default.yellow('未选择任何备份'));
1200
+ return;
1201
+ }
1202
+ const confirmedDelete = await (0, prompts_1.confirm)({
1203
+ message: chalk_1.default.red(`确认删除选中的 ${selectedBackups.length} 个备份?`),
1204
+ default: false,
1205
+ });
1206
+ if (!confirmedDelete) {
1207
+ console.log(chalk_1.default.yellow('操作已取消'));
1208
+ return;
1209
+ }
1210
+ for (const bp of selectedBackups) {
1211
+ await (0, backup_1.deleteBackup)(bp);
1212
+ }
1213
+ console.log(chalk_1.default.green(`✓ 已删除 ${selectedBackups.length} 个备份`));
1214
+ }
1215
+ }
1216
+ catch (error) {
1217
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1218
+ process.exit(1);
1219
+ }
1220
+ });
1221
+ // ============================================================
1222
+ // hooks 子命令 - Hooks 管理
1223
+ // ============================================================
1224
+ const hooksCmd = commander_1.program
1225
+ .command('hooks')
1226
+ .description('管理 Claude Code Hooks')
1227
+ .addHelpText('after', `
1228
+ ${chalk_1.default.bold('子命令:')}
1229
+ ${chalk_1.default.cyan('list')} 列出所有 hooks
1230
+
1231
+ ${chalk_1.default.bold('示例:')}
1232
+ ${chalk_1.default.cyan('lcch hooks list')} 列出所有 hooks(按全局/项目级分类)
1233
+ `);
1234
+ // lcch hooks list - 列出所有 hooks
1235
+ hooksCmd
1236
+ .command('list')
1237
+ .description('列出所有 hooks')
1238
+ .action(async () => {
1239
+ try {
1240
+ const config = await (0, scanner_1.readConfig)();
1241
+ const hooks = await (0, scanner_1.getAllHooksWithScope)(config);
1242
+ if (hooks.length === 0) {
1243
+ console.log(chalk_1.default.yellow('\n没有找到任何 hooks'));
1244
+ return;
1245
+ }
1246
+ const { global, project } = (0, scanner_1.groupHooksByScope)(hooks);
1247
+ console.log(chalk_1.default.bold(`\n共 ${hooks.length} 个 hooks:\n`));
1248
+ // 显示全局 hooks
1249
+ if (global.length > 0) {
1250
+ const globalByEvent = new Map();
1251
+ for (const hook of global) {
1252
+ const list = globalByEvent.get(hook.event) || [];
1253
+ list.push(hook);
1254
+ globalByEvent.set(hook.event, list);
1255
+ }
1256
+ console.log(chalk_1.default.blue.bold('🌍 全局 Hooks:\n'));
1257
+ for (const [event, eventHooks] of globalByEvent) {
1258
+ console.log(chalk_1.default.blue.bold(` 📌 ${event}`));
1259
+ console.log(chalk_1.default.gray(` (${eventHooks.length} 个 hook)\n`));
1260
+ for (let i = 0; i < eventHooks.length; i++) {
1261
+ const hook = eventHooks[i];
1262
+ const isLast = i === eventHooks.length - 1;
1263
+ const connector = isLast ? '└── ' : '├── ';
1264
+ console.log(` ${connector}${chalk_1.default.cyan(`[${hook.type}]`)}`);
1265
+ if (hook.matcher) {
1266
+ console.log(` ${chalk_1.default.gray('匹配器:')} ${hook.matcher}`);
1267
+ }
1268
+ if (hook.command) {
1269
+ console.log(` ${chalk_1.default.gray('命令:')} ${hook.command}`);
1270
+ }
1271
+ }
1272
+ console.log();
1273
+ }
1274
+ }
1275
+ // 显示项目级 hooks
1276
+ if (project.size > 0) {
1277
+ console.log(chalk_1.default.green.bold('📁 项目级 Hooks:\n'));
1278
+ for (const [projectPath, projectHooks] of project) {
1279
+ const projectHooksByEvent = new Map();
1280
+ for (const hook of projectHooks) {
1281
+ const list = projectHooksByEvent.get(hook.event) || [];
1282
+ list.push(hook);
1283
+ projectHooksByEvent.set(hook.event, list);
1284
+ }
1285
+ console.log(chalk_1.default.green(` ✓ ${chalk_1.default.bold(projectPath)}`));
1286
+ console.log(chalk_1.default.gray(` (${projectHooks.length} 个 hook)\n`));
1287
+ for (const [event, eventHooks] of projectHooksByEvent) {
1288
+ console.log(chalk_1.default.blue.bold(` 📌 ${event}`));
1289
+ console.log(chalk_1.default.gray(` (${eventHooks.length} 个 hook)\n`));
1290
+ for (let i = 0; i < eventHooks.length; i++) {
1291
+ const hook = eventHooks[i];
1292
+ const isLast = i === eventHooks.length - 1;
1293
+ const connector = isLast ? '└── ' : '├── ';
1294
+ console.log(` ${connector}${chalk_1.default.cyan(`[${hook.type}]`)}`);
1295
+ if (hook.matcher) {
1296
+ console.log(` ${chalk_1.default.gray('匹配器:')} ${hook.matcher}`);
1297
+ }
1298
+ if (hook.command) {
1299
+ console.log(` ${chalk_1.default.gray('命令:')} ${hook.command}`);
1300
+ }
1301
+ }
1302
+ console.log();
1303
+ }
1304
+ }
1305
+ }
1306
+ // 汇总
1307
+ console.log(chalk_1.default.bold('汇总:'));
1308
+ console.log(` ${chalk_1.default.blue('全局:')} ${global.length} 个`);
1309
+ console.log(` ${chalk_1.default.green('项目级:')} ${project.size} 个项目,${hooks.length - global.length} 个 hooks`);
1310
+ console.log();
1311
+ }
1312
+ catch (error) {
1313
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1314
+ process.exit(1);
1315
+ }
1316
+ });
1317
+ // ============================================================
1318
+ // config 子命令 - 配置管理
1319
+ // ============================================================
1320
+ const configCmd = commander_1.program
1321
+ .command('config')
1322
+ .description('查看 Claude Code 配置信息')
1323
+ .addHelpText('after', `
1324
+ ${chalk_1.default.bold('子命令:')}
1325
+ ${chalk_1.default.cyan('path')} 显示配置文件路径
1326
+ ${chalk_1.default.cyan('stats')} 显示配置统计信息
1327
+
1328
+ ${chalk_1.default.bold('示例:')}
1329
+ ${chalk_1.default.cyan('lcch config path')} 显示配置文件路径
1330
+ ${chalk_1.default.cyan('lcch config stats')} 显示详细统计(项目、会话、费用、Token等)
1331
+ `);
1332
+ // lcch config path - 显示配置文件路径
1333
+ configCmd
1334
+ .command('path')
1335
+ .description('显示配置文件路径')
1336
+ .action(() => {
1337
+ const configPath = (0, scanner_1.getConfigPath)();
1338
+ console.log(chalk_1.default.bold('配置文件路径:'));
1339
+ console.log(` ${configPath}`);
1340
+ if (fs.existsSync(configPath)) {
1341
+ const stats = fs.statSync(configPath);
1342
+ console.log(chalk_1.default.gray(`\n文件大小: ${(0, backup_1.formatFileSize)(stats.size)}`));
1343
+ console.log(chalk_1.default.gray(`修改时间: ${(0, scanner_1.formatDate)(stats.mtime)}`));
1344
+ }
1345
+ else {
1346
+ console.log(chalk_1.default.yellow('\n配置文件不存在'));
1347
+ }
1348
+ });
1349
+ // lcch config stats - 显示配置统计
1350
+ configCmd
1351
+ .command('stats')
1352
+ .description('显示配置统计信息')
1353
+ .action(async () => {
1354
+ try {
1355
+ const config = await (0, scanner_1.readConfig)();
1356
+ const result = await (0, scanner_1.scanProjects)(config);
1357
+ const stats = await (0, scanner_1.calculateGlobalStats)(config);
1358
+ console.log(chalk_1.default.bold('\n📊 配置统计\n'));
1359
+ // 项目统计
1360
+ console.log(chalk_1.default.bold('📁 项目:'));
1361
+ console.log(` 总项目数: ${result.totalCount}`);
1362
+ console.log(chalk_1.default.green(` 有效项目: ${result.validCount}`));
1363
+ console.log(chalk_1.default.red(` 无效项目: ${result.invalidCount}`));
1364
+ console.log(` MCP 服务器: ${Object.keys(config.mcpServers || {}).length}`);
1365
+ console.log();
1366
+ // 会话统计
1367
+ console.log(chalk_1.default.bold('💬 会话:'));
1368
+ console.log(` 历史会话总计: ${chalk_1.default.cyan(stats.totalSessionHistory.toString())} 条`);
1369
+ console.log(` ${chalk_1.default.green('✓ 有效项目:')} ${formatNumber(stats.sessionHistoryByCategory.valid)} 条`);
1370
+ console.log(` ${chalk_1.default.red('✗ 无效项目:')} ${formatNumber(stats.sessionHistoryByCategory.invalid)} 条`);
1371
+ console.log(` ${chalk_1.default.yellow('⚠ 无头项目:')} ${formatNumber(stats.sessionHistoryByCategory.orphaned)} 条`);
1372
+ console.log();
1373
+ console.log(` Resume 总计: ${chalk_1.default.cyan(stats.totalResumeSessions.toString())} 个`);
1374
+ console.log(` ${chalk_1.default.green('✓ 有效项目:')} ${formatNumber(stats.resumeSessionsByCategory.valid)} 个`);
1375
+ console.log(` ${chalk_1.default.red('✗ 无效项目:')} ${formatNumber(stats.resumeSessionsByCategory.invalid)} 个`);
1376
+ console.log(` ${chalk_1.default.yellow('⚠ 无头项目:')} ${formatNumber(stats.resumeSessionsByCategory.orphaned)} 个`);
1377
+ console.log(` 启动次数: ${config.numStartups || 0}`);
1378
+ console.log();
1379
+ // 费用统计
1380
+ console.log(chalk_1.default.bold('💰 费用:'));
1381
+ console.log(` 总花费: ${chalk_1.default.yellow('$' + stats.totalCost.toFixed(4))}`);
1382
+ if (result.totalCount > 0) {
1383
+ console.log(` 平均/项目: ${chalk_1.default.gray('$' + stats.avgCostPerProject.toFixed(4))}`);
1384
+ }
1385
+ console.log(chalk_1.default.gray(' 按项目分类:'));
1386
+ console.log(` ${chalk_1.default.green('✓ 有效项目:')} $${stats.costByCategory.valid.toFixed(4)}`);
1387
+ console.log(` ${chalk_1.default.red('✗ 无效项目:')} $${stats.costByCategory.invalid.toFixed(4)}`);
1388
+ console.log();
1389
+ console.log();
1390
+ // Token 统计
1391
+ console.log(chalk_1.default.bold('🔢 Token 使用:'));
1392
+ console.log(` 输入 Tokens: ${chalk_1.default.cyan(formatNumber(stats.totalInputTokens))}`);
1393
+ console.log(` 输出 Tokens: ${chalk_1.default.cyan(formatNumber(stats.totalOutputTokens))}`);
1394
+ console.log(` 缓存创建 Tokens: ${chalk_1.default.cyan(formatNumber(stats.totalCacheCreationTokens))}`);
1395
+ console.log(` 缓存读取 Tokens: ${chalk_1.default.cyan(formatNumber(stats.totalCacheReadTokens))}`);
1396
+ console.log();
1397
+ // 工具使用统计
1398
+ console.log(chalk_1.default.bold('🛠️ 工具使用:'));
1399
+ console.log(` 工具调用总计: ${chalk_1.default.cyan(formatNumber(stats.totalToolUsage))} 次`);
1400
+ if (stats.toolDetails.length > 0) {
1401
+ console.log(chalk_1.default.gray(' 详细列表:'));
1402
+ stats.toolDetails.slice(0, 10).forEach((tool, index) => {
1403
+ const isLast = index === Math.min(stats.toolDetails.length - 1, 9);
1404
+ const connector = isLast ? '└── ' : '├── ';
1405
+ const name = tool.name;
1406
+ const count = chalk_1.default.cyan(`${formatNumber(tool.count)} 次`);
1407
+ const lastUsed = chalk_1.default.gray(`[${(0, scanner_1.formatRelativeTime)(tool.lastUsedAt)}]`);
1408
+ console.log(` ${connector}${name} ${count} ${lastUsed}`);
1409
+ });
1410
+ if (stats.toolDetails.length > 10) {
1411
+ console.log(` └── ${chalk_1.default.gray(`... 还有 ${stats.toolDetails.length - 10} 个工具`)}`);
1412
+ }
1413
+ }
1414
+ console.log();
1415
+ // Skill 使用统计
1416
+ console.log(chalk_1.default.bold('✨ Skill 使用:'));
1417
+ console.log(` Skill 调用总计: ${chalk_1.default.cyan(formatNumber(stats.totalSkillUsage))} 次`);
1418
+ if (stats.skillDetails.length > 0) {
1419
+ console.log(chalk_1.default.gray(' 详细列表:'));
1420
+ stats.skillDetails.slice(0, 10).forEach((skill, index) => {
1421
+ const isLast = index === Math.min(stats.skillDetails.length - 1, 9);
1422
+ const connector = isLast ? '└── ' : '├── ';
1423
+ const name = skill.name;
1424
+ const count = chalk_1.default.cyan(`${formatNumber(skill.count)} 次`);
1425
+ const lastUsed = chalk_1.default.gray(`[${(0, scanner_1.formatRelativeTime)(skill.lastUsedAt)}]`);
1426
+ console.log(` ${connector}${name} ${count} ${lastUsed}`);
1427
+ });
1428
+ if (stats.skillDetails.length > 10) {
1429
+ console.log(` └── ${chalk_1.default.gray(`... 还有 ${stats.skillDetails.length - 10} 个 Skill`)}`);
1430
+ }
1431
+ }
1432
+ console.log();
1433
+ // GitHub 统计
1434
+ console.log(chalk_1.default.bold('🔗 GitHub:'));
1435
+ console.log(` 关联仓库: ${chalk_1.default.cyan(stats.totalGithubRepos.toString())} 个`);
1436
+ console.log();
1437
+ // 时间统计
1438
+ console.log(chalk_1.default.bold('⏱️ 时间:'));
1439
+ console.log(` 使用天数: ${chalk_1.default.cyan(stats.usageDays.toString())} 天`);
1440
+ if (config.firstStartTime) {
1441
+ console.log(` 首次启动: ${(0, scanner_1.formatDate)(new Date(config.firstStartTime))}`);
1442
+ }
1443
+ if (stats.lastActivity) {
1444
+ console.log(` 最后活动: ${(0, scanner_1.formatRelativeTime)(stats.lastActivity.getTime())}`);
1445
+ }
1446
+ console.log();
1447
+ // 用户信息
1448
+ console.log(chalk_1.default.bold('👤 用户:'));
1449
+ console.log(` 用户 ID: ${config.userID || 'N/A'}`);
1450
+ console.log(` 安装方式: ${config.installMethod || 'N/A'}`);
1451
+ // AI 伴侣信息
1452
+ if (stats.companionInfo) {
1453
+ console.log();
1454
+ console.log(chalk_1.default.bold('🥚 AI 伴侣:'));
1455
+ console.log(` 名字: ${chalk_1.default.magenta(stats.companionInfo.name)}`);
1456
+ console.log(` 性格: ${stats.companionInfo.personality}`);
1457
+ console.log(` 孵化时间: ${(0, scanner_1.formatRelativeTime)(stats.companionInfo.hatchedAt.getTime())}`);
1458
+ }
1459
+ console.log();
1460
+ }
1461
+ catch (error) {
1462
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1463
+ process.exit(1);
1464
+ }
1465
+ });
1466
+ // ============================================================
1467
+ // mcp 子命令 - MCP 服务器管理
1468
+ // ============================================================
1469
+ const mcpCmd = commander_1.program
1470
+ .command('mcp')
1471
+ .description('管理 MCP 服务器')
1472
+ .addHelpText('after', `
1473
+ ${chalk_1.default.bold('子命令:')}
1474
+ ${chalk_1.default.cyan('list')} 列出所有 MCP 服务器
1475
+ ${chalk_1.default.cyan('info')} 查看 MCP 服务器详细配置(交互式选择)
1476
+
1477
+ ${chalk_1.default.bold('示例:')}
1478
+ ${chalk_1.default.cyan('lcch mcp list')} 列出所有 MCP 服务器(按全局/项目级分类)
1479
+ ${chalk_1.default.cyan('lcch mcp info')} 交互式选择并查看 MCP 服务器详情
1480
+ ${chalk_1.default.cyan('lcch mcp info --show')} 交互式选择并显示敏感信息(环境变量等)
1481
+ `);
1482
+ // lcch mcp list - 列出所有 MCP 服务器
1483
+ mcpCmd
1484
+ .command('list')
1485
+ .description('列出所有 MCP 服务器')
1486
+ .action(async () => {
1487
+ try {
1488
+ const config = await (0, scanner_1.readConfig)();
1489
+ const mcpServers = (0, scanner_1.getAllMcpServers)(config);
1490
+ const { global, project } = (0, scanner_1.groupMcpServersByScope)(mcpServers);
1491
+ if (mcpServers.length === 0) {
1492
+ console.log(chalk_1.default.yellow('\n没有找到 MCP 服务器'));
1493
+ return;
1494
+ }
1495
+ console.log(chalk_1.default.bold(`\n共 ${mcpServers.length} 个 MCP 服务器:\n`));
1496
+ // 显示全局 MCP 服务器
1497
+ if (global.length > 0) {
1498
+ console.log(chalk_1.default.magenta.bold('全局 MCP 服务器:'));
1499
+ for (const server of global) {
1500
+ console.log(` ${chalk_1.default.cyan('•')} ${chalk_1.default.bold(server.name)}`);
1501
+ if (server.config.type) {
1502
+ console.log(` ${chalk_1.default.gray('类型:')} ${server.config.type}`);
1503
+ }
1504
+ if (server.config.command) {
1505
+ console.log(` ${chalk_1.default.gray('命令:')} ${server.config.command}`);
1506
+ }
1507
+ }
1508
+ console.log();
1509
+ }
1510
+ // 显示项目级 MCP 服务器
1511
+ if (project.size > 0) {
1512
+ console.log(chalk_1.default.green.bold('项目级 MCP 服务器:'));
1513
+ for (const [projectPath, servers] of project) {
1514
+ console.log(` ${chalk_1.default.green('✓')} ${chalk_1.default.bold(projectPath)} ${chalk_1.default.gray(`(${servers.length} 个)`)}`);
1515
+ for (const server of servers) {
1516
+ console.log(` ${chalk_1.default.cyan('•')} ${server.name}`);
1517
+ if (server.config.type) {
1518
+ console.log(` ${chalk_1.default.gray('类型:')} ${server.config.type}`);
1519
+ }
1520
+ if (server.config.command) {
1521
+ console.log(` ${chalk_1.default.gray('命令:')} ${server.config.command}`);
1522
+ }
1523
+ }
1524
+ }
1525
+ console.log();
1526
+ }
1527
+ // 汇总
1528
+ console.log(chalk_1.default.bold('汇总:'));
1529
+ console.log(` ${chalk_1.default.magenta('全局:')} ${global.length} 个`);
1530
+ console.log(` ${chalk_1.default.green('项目级:')} ${project.size} 个项目,${mcpServers.length - global.length} 个服务器`);
1531
+ console.log();
1532
+ }
1533
+ catch (error) {
1534
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1535
+ process.exit(1);
1536
+ }
1537
+ });
1538
+ // lcch mcp info - 查看 MCP 服务器详细信息
1539
+ mcpCmd
1540
+ .command('info')
1541
+ .description('查看 MCP 服务器详细配置(交互式选择)')
1542
+ .option('--show', '显示敏感信息(环境变量中的密钥等)', false)
1543
+ .action(async (options) => {
1544
+ try {
1545
+ const config = await (0, scanner_1.readConfig)();
1546
+ const mcpServers = (0, scanner_1.getAllMcpServers)(config);
1547
+ if (mcpServers.length === 0) {
1548
+ console.log(chalk_1.default.yellow('\n没有找到 MCP 服务器'));
1549
+ return;
1550
+ }
1551
+ // 交互式选择
1552
+ const choices = mcpServers.map(s => {
1553
+ const scopeLabel = s.scope === 'global' ? chalk_1.default.magenta('[全局]') : chalk_1.default.green(`[项目: ${s.projectPath}]`);
1554
+ return {
1555
+ name: `${s.name} ${scopeLabel}`,
1556
+ value: s,
1557
+ };
1558
+ });
1559
+ const { server } = await inquirer_1.default.prompt([{
1560
+ type: 'list',
1561
+ name: 'server',
1562
+ message: '选择要查看的 MCP 服务器:',
1563
+ choices,
1564
+ pageSize: 20,
1565
+ }]);
1566
+ const selectedServer = server;
1567
+ if (!selectedServer) {
1568
+ console.log(chalk_1.default.yellow('未选择任何服务器'));
1569
+ return;
1570
+ }
1571
+ // 显示详细信息
1572
+ console.log(chalk_1.default.bold(`\n🔌 MCP 服务器详情\n`));
1573
+ console.log(`${chalk_1.default.bold('名称:')} ${chalk_1.default.cyan(selectedServer.name)}`);
1574
+ console.log(`${chalk_1.default.bold('作用范围:')} ${selectedServer.scope === 'global' ? chalk_1.default.magenta('全局') : chalk_1.default.green('项目级')}`);
1575
+ if (selectedServer.projectPath) {
1576
+ console.log(`${chalk_1.default.bold('项目路径:')} ${selectedServer.projectPath}`);
1577
+ }
1578
+ console.log();
1579
+ // 显示配置详情
1580
+ console.log(chalk_1.default.bold('配置详情:'));
1581
+ console.log(chalk_1.default.gray('─'.repeat(50)));
1582
+ const serverConfig = selectedServer.config;
1583
+ // 类型
1584
+ if (serverConfig.type) {
1585
+ console.log(`${chalk_1.default.bold('类型:')} ${chalk_1.default.yellow(serverConfig.type)}`);
1586
+ }
1587
+ // 命令
1588
+ if (serverConfig.command) {
1589
+ console.log(`${chalk_1.default.bold('命令:')} ${chalk_1.default.cyan(serverConfig.command)}`);
1590
+ }
1591
+ // 参数
1592
+ if (serverConfig.args && Array.isArray(serverConfig.args) && serverConfig.args.length > 0) {
1593
+ console.log(`${chalk_1.default.bold('参数:')}`);
1594
+ serverConfig.args.forEach((arg, index) => {
1595
+ console.log(` [${index}] ${chalk_1.default.cyan(arg)}`);
1596
+ });
1597
+ }
1598
+ // 环境变量
1599
+ if (serverConfig.env && typeof serverConfig.env === 'object' && Object.keys(serverConfig.env).length > 0) {
1600
+ console.log(`${chalk_1.default.bold('环境变量:')}`);
1601
+ for (const [key, value] of Object.entries(serverConfig.env)) {
1602
+ // 检测敏感信息:包含 key、token、secret、password、auth、authorization、apikey、api-key、access_key、secret_key 或以 _pass 结尾的键
1603
+ const isSecretKey = /key|token|secret|password|auth|authorization|apikey|api-key|access_key|secret_key|_pass$/i.test(key);
1604
+ // 检测值是否包含敏感模式(如 Bearer token、Basic auth 等)
1605
+ const strValue = String(value);
1606
+ const isSecretValue = /^(Bearer\s+|Basic\s+|ApiKey\s+|Token\s+)/i.test(strValue);
1607
+ let displayValue;
1608
+ if ((isSecretKey || isSecretValue) && value && !options.show) {
1609
+ // 模糊化显示:保留前4个字符,其余用 * 替代
1610
+ displayValue = strValue.length > 4 ? `${strValue.substring(0, 4)}****` : '****';
1611
+ }
1612
+ else {
1613
+ displayValue = strValue;
1614
+ }
1615
+ console.log(` ${chalk_1.default.gray(key)}=${chalk_1.default.cyan(displayValue)}`);
1616
+ }
1617
+ }
1618
+ // URL(对于 HTTP SSE 类型)
1619
+ if (serverConfig.url) {
1620
+ console.log(`${chalk_1.default.bold('URL:')} ${chalk_1.default.cyan(serverConfig.url)}`);
1621
+ }
1622
+ // 超时设置
1623
+ if (serverConfig.timeout !== undefined) {
1624
+ console.log(`${chalk_1.default.bold('超时:')} ${serverConfig.timeout}ms`);
1625
+ }
1626
+ // 自动批准
1627
+ if (serverConfig.autoApprove !== undefined) {
1628
+ console.log(`${chalk_1.default.bold('自动批准:')} ${serverConfig.autoApprove ? chalk_1.default.green('是') : chalk_1.default.red('否')}`);
1629
+ }
1630
+ // 禁用状态
1631
+ if (serverConfig.disabled !== undefined) {
1632
+ console.log(`${chalk_1.default.bold('状态:')} ${serverConfig.disabled ? chalk_1.default.red('已禁用') : chalk_1.default.green('已启用')}`);
1633
+ }
1634
+ // 其他配置项
1635
+ const knownKeys = ['type', 'command', 'args', 'env', 'url', 'timeout', 'autoApprove', 'disabled'];
1636
+ const otherKeys = Object.keys(serverConfig).filter(k => !knownKeys.includes(k));
1637
+ if (otherKeys.length > 0) {
1638
+ console.log();
1639
+ console.log(`${chalk_1.default.bold('其他配置:')}`);
1640
+ for (const key of otherKeys) {
1641
+ const value = serverConfig[key];
1642
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
1643
+ console.log(` ${chalk_1.default.gray(key)}: ${chalk_1.default.cyan(String(value))}`);
1644
+ }
1645
+ else if (value !== null && value !== undefined) {
1646
+ console.log(` ${chalk_1.default.gray(key)}: ${chalk_1.default.cyan(JSON.stringify(value))}`);
1647
+ }
1648
+ }
1649
+ }
1650
+ console.log(chalk_1.default.gray('─'.repeat(50)));
1651
+ console.log();
1652
+ }
1653
+ catch (error) {
1654
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1655
+ process.exit(1);
1656
+ }
1657
+ });
1658
+ // ============================================================
1659
+ // cache 子命令 - 缓存管理
1660
+ // ============================================================
1661
+ const cacheCmd = commander_1.program
1662
+ .command('cache')
1663
+ .description('查看 Claude Code 缓存信息')
1664
+ .option('--json', '以 JSON 格式输出', false)
1665
+ .addHelpText('after', `
1666
+ ${chalk_1.default.bold('子命令:')}
1667
+ ${chalk_1.default.cyan('scan-secrets')} 扫描缓存中的敏感信息
1668
+
1669
+ ${chalk_1.default.bold('示例:')}
1670
+ ${chalk_1.default.cyan('lcch cache')} 查看所有缓存信息
1671
+ ${chalk_1.default.cyan('lcch cache --json')} 以 JSON 格式输出
1672
+ ${chalk_1.default.cyan('lcch scan-secrets')} 扫描缓存中的密钥(独立命令)
1673
+ `);
1674
+ // lcch cache - 查看缓存信息
1675
+ cacheCmd
1676
+ .action(async (options) => {
1677
+ try {
1678
+ const caches = await (0, scanner_1.getCacheInfo)();
1679
+ const promptCacheStats = await (0, scanner_1.getPromptCacheStats)();
1680
+ if (options.json) {
1681
+ // JSON 格式输出
1682
+ console.log(JSON.stringify({
1683
+ caches: caches.map(c => ({
1684
+ name: c.name,
1685
+ description: c.description,
1686
+ path: c.path,
1687
+ fileCount: c.fileCount,
1688
+ totalSize: c.totalSize,
1689
+ lastModified: c.lastModified?.toISOString(),
1690
+ })),
1691
+ promptCacheStats: promptCacheStats || undefined,
1692
+ }, null, 2));
1693
+ return;
1694
+ }
1695
+ // 表格形式输出
1696
+ console.log(chalk_1.default.bold('\n📦 Claude Code 缓存信息\n'));
1697
+ // 计算总缓存大小
1698
+ const totalCacheSize = caches.reduce((sum, c) => sum + c.totalSize, 0);
1699
+ // 显示各类缓存
1700
+ if (caches.length === 0) {
1701
+ console.log(chalk_1.default.yellow('没有发现缓存文件'));
1702
+ }
1703
+ else {
1704
+ for (const cache of caches) {
1705
+ const sizeStr = (0, backup_1.formatFileSize)(cache.totalSize);
1706
+ const countStr = cache.fileCount === 1
1707
+ ? '1 个文件'
1708
+ : `${cache.fileCount} 个文件`;
1709
+ const dateStr = cache.lastModified
1710
+ ? chalk_1.default.gray(`最后更新: ${(0, scanner_1.formatRelativeTime)(cache.lastModified.getTime())}`)
1711
+ : '';
1712
+ console.log(`${chalk_1.default.cyan('•')} ${chalk_1.default.bold(cache.name)}`);
1713
+ console.log(` ${chalk_1.default.gray(cache.description)}`);
1714
+ console.log(` ${chalk_1.default.gray('路径:')} ${cache.path}`);
1715
+ console.log(` ${chalk_1.default.gray(`大小: ${sizeStr} | ${countStr}`)} ${dateStr}`);
1716
+ console.log();
1717
+ }
1718
+ // 汇总
1719
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
1720
+ console.log(chalk_1.default.bold(' 缓存汇总'));
1721
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
1722
+ console.log(` 缓存类型: ${caches.length} 种`);
1723
+ console.log(` 总大小: ${chalk_1.default.yellow((0, backup_1.formatFileSize)(totalCacheSize))}`);
1724
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
1725
+ }
1726
+ // 显示 API Prompt 缓存统计
1727
+ if (promptCacheStats) {
1728
+ console.log(chalk_1.default.bold('\n\n🔄 API Prompt 缓存统计\n'));
1729
+ for (const [model, stats] of Object.entries(promptCacheStats.modelUsage)) {
1730
+ console.log(chalk_1.default.blue.bold(` 模型: ${model}`));
1731
+ console.log(` 输入 Tokens: ${chalk_1.default.cyan(formatNumber(stats.inputTokens))}`);
1732
+ console.log(` 输出 Tokens: ${chalk_1.default.cyan(formatNumber(stats.outputTokens))}`);
1733
+ console.log(` ${chalk_1.default.green('缓存读取 Tokens:')} ${chalk_1.default.green(formatNumber(stats.cacheReadInputTokens))}`);
1734
+ console.log(` ${chalk_1.default.yellow('缓存创建 Tokens:')} ${chalk_1.default.yellow(formatNumber(stats.cacheCreationInputTokens))}`);
1735
+ console.log(` 费用: ${chalk_1.default.yellow('$' + stats.costUSD.toFixed(4))}`);
1736
+ // 计算缓存命中率
1737
+ const totalInput = stats.inputTokens + stats.cacheReadInputTokens;
1738
+ if (totalInput > 0) {
1739
+ const cacheHitRate = (stats.cacheReadInputTokens / totalInput * 100).toFixed(1);
1740
+ console.log(` 缓存命中率: ${chalk_1.default.cyan(cacheHitRate + '%')}`);
1741
+ }
1742
+ console.log();
1743
+ }
1744
+ }
1745
+ console.log();
1746
+ }
1747
+ catch (error) {
1748
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
1749
+ process.exit(1);
1750
+ }
1751
+ });
1752
+ // ============================================================
1753
+ // 恢复备份相关函数
1754
+ // ============================================================
1755
+ /**
1756
+ * 恢复单个备份文件
1757
+ */
1758
+ async function restoreSingleBackup(backupPath) {
1759
+ if (!await fs.pathExists(backupPath)) {
1760
+ console.error(chalk_1.default.red(`错误: 备份不存在: ${backupPath}`));
1761
+ process.exit(1);
1762
+ }
1763
+ // 从备份文件名推断原文件路径
1764
+ // 备份格式: /path/to/file.json.backup.1234567890
1765
+ const backupMatch = backupPath.match(/^(.+)\.backup\.\d+$/);
1766
+ if (!backupMatch) {
1767
+ console.error(chalk_1.default.red(`错误: 无效的备份文件路径格式: ${backupPath}`));
1768
+ console.error(chalk_1.default.gray(' 备份文件格式应为: <原文件路径>.backup.<时间戳>'));
1769
+ process.exit(1);
1770
+ }
1771
+ const originalPath = backupMatch[1];
1772
+ const { confirm } = await inquirer_1.default.prompt([{
1773
+ type: 'confirm',
1774
+ name: 'confirm',
1775
+ message: chalk_1.default.yellow(`确认从 ${path.basename(backupPath)} 恢复文件?原文件将被覆盖。`),
1776
+ default: false,
1777
+ }]);
1778
+ if (!confirm) {
1779
+ console.log(chalk_1.default.yellow('操作已取消'));
1780
+ return;
1781
+ }
1782
+ // 恢复单个文件
1783
+ await fs.copy(backupPath, originalPath);
1784
+ console.log(chalk_1.default.green(`✓ 文件已成功恢复: ${originalPath}`));
1785
+ // 删除备份文件
1786
+ await fs.remove(backupPath);
1787
+ console.log(chalk_1.default.gray(` 备份文件已删除: ${path.basename(backupPath)}`));
1788
+ }
1789
+ /**
1790
+ * 恢复项目下的所有备份文件
1791
+ */
1792
+ async function restoreProjectBackups(projectDir) {
1793
+ if (!await fs.pathExists(projectDir)) {
1794
+ console.error(chalk_1.default.red(`错误: 项目目录不存在: ${projectDir}`));
1795
+ process.exit(1);
1796
+ }
1797
+ // 查找项目目录下的所有备份文件
1798
+ const allFiles = await fs.readdir(projectDir, { recursive: true });
1799
+ const backupFiles = [];
1800
+ for (const file of allFiles) {
1801
+ const fullPath = path.join(projectDir, file);
1802
+ const stat = await fs.stat(fullPath);
1803
+ if (stat.isFile() && file.match(/\.backup\.\d+$/)) {
1804
+ backupFiles.push(fullPath);
1805
+ }
1806
+ }
1807
+ if (backupFiles.length === 0) {
1808
+ console.log(chalk_1.default.yellow(`项目目录下没有找到备份文件: ${projectDir}`));
1809
+ return;
1810
+ }
1811
+ // 按原文件路径分组备份
1812
+ const backupGroups = new Map();
1813
+ for (const backupPath of backupFiles) {
1814
+ const match = backupPath.match(/^(.+)\.backup\.\d+$/);
1815
+ if (match) {
1816
+ const originalPath = match[1];
1817
+ const list = backupGroups.get(originalPath) || [];
1818
+ list.push(backupPath);
1819
+ backupGroups.set(originalPath, list);
1820
+ }
1821
+ }
1822
+ // 显示找到的备份
1823
+ console.log(chalk_1.default.bold(`\n📁 找到 ${backupFiles.length} 个备份文件(涉及 ${backupGroups.size} 个原文件)\n`));
1824
+ for (const [originalPath, backups] of backupGroups) {
1825
+ const relativeOriginal = originalPath.replace(projectDir, '.');
1826
+ console.log(` ${chalk_1.default.cyan(relativeOriginal)}:`);
1827
+ for (const backup of backups.sort()) {
1828
+ console.log(` ${chalk_1.default.gray('→')} ${path.basename(backup)}`);
1829
+ }
1830
+ }
1831
+ console.log();
1832
+ // 确认恢复
1833
+ const { confirm } = await inquirer_1.default.prompt([{
1834
+ type: 'confirm',
1835
+ name: 'confirm',
1836
+ message: chalk_1.default.yellow(`确认恢复所有 ${backupFiles.length} 个备份文件?原文件将被覆盖。`),
1837
+ default: false,
1838
+ }]);
1839
+ if (!confirm) {
1840
+ console.log(chalk_1.default.yellow('操作已取消'));
1841
+ return;
1842
+ }
1843
+ // 执行恢复
1844
+ console.log(chalk_1.default.bold('\n📝 正在恢复备份文件...\n'));
1845
+ let restoredCount = 0;
1846
+ let deletedCount = 0;
1847
+ const failedRestores = [];
1848
+ for (const [originalPath, backups] of backupGroups) {
1849
+ // 使用最新的备份(按时间戳排序)
1850
+ const latestBackup = backups.sort().pop();
1851
+ const relativeOriginal = originalPath.replace(projectDir, '.');
1852
+ try {
1853
+ // 恢复文件
1854
+ await fs.copy(latestBackup, originalPath);
1855
+ console.log(` ${chalk_1.default.green('✓')} ${relativeOriginal} ${chalk_1.default.gray(`← ${path.basename(latestBackup)}`)}`);
1856
+ restoredCount++;
1857
+ // 删除该原文件的所有备份
1858
+ for (const backup of backups) {
1859
+ await fs.remove(backup);
1860
+ deletedCount++;
1861
+ }
1862
+ // 删除最新备份
1863
+ await fs.remove(latestBackup);
1864
+ deletedCount++;
1865
+ }
1866
+ catch (error) {
1867
+ console.error(` ${chalk_1.default.red('✗')} ${relativeOriginal} - ${error.message}`);
1868
+ failedRestores.push(relativeOriginal);
1869
+ }
1870
+ }
1871
+ console.log();
1872
+ console.log(chalk_1.default.green('✓ 恢复完成'));
1873
+ console.log(` ${chalk_1.default.gray('成功恢复:')} ${restoredCount} 个文件`);
1874
+ console.log(` ${chalk_1.default.gray('删除备份:')} ${deletedCount} 个`);
1875
+ if (failedRestores.length > 0) {
1876
+ console.log(` ${chalk_1.default.red('失败:')} ${failedRestores.length} 个文件`);
1877
+ for (const failed of failedRestores) {
1878
+ console.log(` ${chalk_1.default.red('•')} ${failed}`);
1879
+ }
1880
+ }
1881
+ }
1882
+ /**
1883
+ * 列出所有掩码备份
1884
+ */
1885
+ async function listMaskBackups() {
1886
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
1887
+ if (!await fs.pathExists(projectsDir)) {
1888
+ console.log(chalk_1.default.yellow('没有找到项目缓存目录'));
1889
+ return;
1890
+ }
1891
+ // 读取所有项目目录
1892
+ const projectDirs = await fs.readdir(projectsDir, { withFileTypes: true });
1893
+ const projectsWithBackups = [];
1894
+ for (const dir of projectDirs) {
1895
+ if (!dir.isDirectory())
1896
+ continue;
1897
+ const projectPath = path.join(projectsDir, dir.name);
1898
+ const allFiles = await fs.readdir(projectPath, { recursive: true });
1899
+ const backupInfos = [];
1900
+ for (const file of allFiles) {
1901
+ const fullPath = path.join(projectPath, file);
1902
+ const stat = await fs.stat(fullPath).catch(() => null);
1903
+ if (!stat || !stat.isFile())
1904
+ continue;
1905
+ // 匹配备份文件格式: filename.backup.timestamp
1906
+ const backupMatch = file.match(/^(.*)\.backup\.(\d+)$/);
1907
+ if (backupMatch) {
1908
+ const originalName = backupMatch[1];
1909
+ const timestamp = parseInt(backupMatch[2], 10);
1910
+ const originalPath = path.join(projectPath, file.replace(/\.backup\.\d+$/, ''));
1911
+ backupInfos.push({
1912
+ projectName: dir.name,
1913
+ projectPath,
1914
+ originalPath: originalPath.replace(projectPath, '.'),
1915
+ backupPath: fullPath,
1916
+ backupTime: new Date(timestamp),
1917
+ size: stat.size,
1918
+ });
1919
+ }
1920
+ }
1921
+ if (backupInfos.length > 0) {
1922
+ // 尝试获取真实项目路径
1923
+ const realPath = dir.name.replace(/-/g, '/');
1924
+ const fullRealPath = path.resolve('/', realPath);
1925
+ projectsWithBackups.push({
1926
+ normalizedPath: dir.name,
1927
+ realPath: await fs.pathExists(fullRealPath) ? fullRealPath : undefined,
1928
+ backups: backupInfos,
1929
+ });
1930
+ }
1931
+ }
1932
+ if (projectsWithBackups.length === 0) {
1933
+ console.log(chalk_1.default.green('✓ 没有发现任何备份文件'));
1934
+ return;
1935
+ }
1936
+ // 计算统计信息
1937
+ let totalBackups = 0;
1938
+ let totalSize = 0;
1939
+ for (const project of projectsWithBackups) {
1940
+ totalBackups += project.backups.length;
1941
+ for (const backup of project.backups) {
1942
+ totalSize += backup.size;
1943
+ }
1944
+ }
1945
+ // 显示总览
1946
+ console.log(chalk_1.default.bold('\n📦 掩码备份统计\n'));
1947
+ console.log(` ${chalk_1.default.gray('涉及项目:')} ${projectsWithBackups.length} 个`);
1948
+ console.log(` ${chalk_1.default.gray('备份文件:')} ${totalBackups} 个`);
1949
+ console.log(` ${chalk_1.default.gray('总大小:')} ${(0, backup_1.formatFileSize)(totalSize)}`);
1950
+ console.log();
1951
+ // 按项目显示详细信息
1952
+ for (const project of projectsWithBackups.sort((a, b) => a.normalizedPath.localeCompare(b.normalizedPath))) {
1953
+ // 判断项目状态
1954
+ const isValid = project.realPath && await fs.pathExists(project.realPath);
1955
+ const statusIcon = isValid ? chalk_1.default.green('✓') : chalk_1.default.yellow('⚠');
1956
+ const statusColor = isValid ? chalk_1.default.green : chalk_1.default.yellow;
1957
+ console.log(statusColor(` ${statusIcon} ${project.normalizedPath}`));
1958
+ console.log(` ${chalk_1.default.gray('备份数:')} ${project.backups.length}`);
1959
+ if (project.realPath) {
1960
+ console.log(` ${chalk_1.default.gray('路径:')} ${project.realPath.replace(os.homedir(), '~')}`);
1961
+ }
1962
+ // 按原文件分组显示备份
1963
+ const byOriginalFile = new Map();
1964
+ for (const backup of project.backups) {
1965
+ const list = byOriginalFile.get(backup.originalPath) || [];
1966
+ list.push(backup);
1967
+ byOriginalFile.set(backup.originalPath, list);
1968
+ }
1969
+ for (const [originalPath, backups] of byOriginalFile) {
1970
+ console.log(` ${chalk_1.default.cyan('•')} ${originalPath}`);
1971
+ for (const backup of backups.sort((a, b) => b.backupTime.getTime() - a.backupTime.getTime())) {
1972
+ const timeStr = backup.backupTime.toLocaleString('zh-CN');
1973
+ const sizeStr = (0, backup_1.formatFileSize)(backup.size);
1974
+ console.log(` ${chalk_1.default.gray('→')} ${chalk_1.default.yellow(path.basename(backup.backupPath))} ${chalk_1.default.gray(`(${timeStr}, ${sizeStr})`)}`);
1975
+ }
1976
+ }
1977
+ console.log();
1978
+ }
1979
+ // 显示恢复提示
1980
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
1981
+ console.log(chalk_1.default.gray('恢复命令:'));
1982
+ console.log(` lcch scan-secrets --mask --restore ${chalk_1.default.cyan('<项目目录>')}`);
1983
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
1984
+ console.log();
1985
+ }
1986
+ // ============================================================
1987
+ // scan-secrets 子命令 - 扫描缓存中的密钥
1988
+ // ============================================================
1989
+ const scanSecretsCmd = commander_1.program
1990
+ .command('scan-secrets')
1991
+ .description('扫描 Claude Code 缓存或项目中的敏感信息')
1992
+ .option('--show', '显示完整密钥(不安全,仅供调试)', false)
1993
+ .option('--mask', '扫描项目并交互式掩码敏感信息', false)
1994
+ .option('--restore <path>', '恢复指定的备份文件或项目目录(需配合 --mask)', '')
1995
+ .option('--backup', '列出所有掩码备份信息', false)
1996
+ .addHelpText('after', `
1997
+ ${chalk_1.default.bold('说明:')}
1998
+ 扫描 Claude Code 缓存目录或项目中的敏感信息,包括:
1999
+ - API 密钥(Anthropic、OpenAI、GitHub 等)
2000
+ - 数据库连接字符串
2001
+ - JWT 令牌
2002
+ - 私钥文件
2003
+ - MCP 服务器认证信息
2004
+
2005
+ 扫描位置:
2006
+ - ~/.claude/settings.json
2007
+ - ~/.claude/projects/
2008
+ - ~/.claude/history.jsonl
2009
+ - ~/.claude/session-env/
2010
+
2011
+ ${chalk_1.default.bold('示例:')}
2012
+ ${chalk_1.default.cyan('lcch scan-secrets')} 扫描缓存中的密钥
2013
+ ${chalk_1.default.cyan('lcch scan-secrets --show')} 显示完整密钥
2014
+ ${chalk_1.default.cyan('lcch scan-secrets --mask')} 扫描项目并交互式掩码敏感信息
2015
+ ${chalk_1.default.cyan('lcch scan-secrets --mask --restore <项目目录>')} 恢复项目所有备份
2016
+ ${chalk_1.default.cyan('lcch scan-secrets --backup')} 列出所有掩码备份
2017
+ `);
2018
+ // lcch cache scan-secrets - 扫描密钥
2019
+ scanSecretsCmd
2020
+ .action(async (options) => {
2021
+ try {
2022
+ // 处理 --backup 模式:列出所有备份
2023
+ if (options.backup) {
2024
+ await listMaskBackups();
2025
+ return;
2026
+ }
2027
+ // 处理 --mask 模式下的恢复备份
2028
+ if (options.mask && options.restore) {
2029
+ const restorePath = options.restore;
2030
+ // 检查是否是项目目录(包含备份文件)
2031
+ const isProjectDir = await fs.pathExists(path.join(restorePath, '..', '..', '.claude')) ||
2032
+ restorePath.includes('.claude/projects');
2033
+ if (isProjectDir || (await fs.stat(restorePath)).isDirectory()) {
2034
+ // 按项目恢复所有备份
2035
+ await restoreProjectBackups(restorePath);
2036
+ }
2037
+ else {
2038
+ // 恢复单个备份文件(旧方式,保持兼容)
2039
+ await restoreSingleBackup(restorePath);
2040
+ }
2041
+ return;
2042
+ }
2043
+ // 处理 --mask 模式
2044
+ if (options.mask) {
2045
+ // 获取配置和项目状态
2046
+ const config = await (0, scanner_1.readConfig)();
2047
+ const result = await (0, scanner_1.scanProjects)(config);
2048
+ // 构建项目状态映射
2049
+ const validProjectsSet = new Set(result.validProjects);
2050
+ const invalidProjectsSet = new Set(result.invalidProjects);
2051
+ const allConfigProjects = new Set([...result.validProjects, ...result.invalidProjects]);
2052
+ // 构建 normalizedPath -> projectPath 的映射
2053
+ const normalizedToProjectMap = new Map();
2054
+ for (const configPath of allConfigProjects) {
2055
+ const configNormalizedPath = configPath.replace(/\//g, '-');
2056
+ normalizedToProjectMap.set(configNormalizedPath, configPath);
2057
+ }
2058
+ // 获取所有项目缓存目录
2059
+ const allCacheDirs = await (0, scanner_1.getAllProjectCacheDirs)();
2060
+ if (allCacheDirs.length === 0) {
2061
+ console.log(chalk_1.default.yellow('没有发现项目缓存目录'));
2062
+ return;
2063
+ }
2064
+ // 扫描所有项目缓存,收集包含敏感信息的项目
2065
+ console.log(chalk_1.default.bold('\n🔍 正在扫描所有项目缓存中的敏感信息...\n'));
2066
+ const projectsWithSecrets = [];
2067
+ for (const normalizedPath of allCacheDirs) {
2068
+ const findings = await (0, scanner_1.scanProjectCacheSecrets)(normalizedPath);
2069
+ if (findings.length > 0) {
2070
+ const realPath = normalizedToProjectMap.get(normalizedPath);
2071
+ let status;
2072
+ if (realPath) {
2073
+ if (validProjectsSet.has(realPath)) {
2074
+ status = 'valid';
2075
+ }
2076
+ else {
2077
+ status = 'invalid';
2078
+ }
2079
+ }
2080
+ else {
2081
+ status = 'orphaned';
2082
+ }
2083
+ projectsWithSecrets.push({
2084
+ normalizedPath,
2085
+ realPath,
2086
+ status,
2087
+ findings,
2088
+ });
2089
+ }
2090
+ }
2091
+ if (projectsWithSecrets.length === 0) {
2092
+ console.log(chalk_1.default.green('✓ 所有项目均未发现敏感信息'));
2093
+ return;
2094
+ }
2095
+ // 按状态分组
2096
+ const validProjects = projectsWithSecrets.filter(p => p.status === 'valid');
2097
+ const invalidProjects = projectsWithSecrets.filter(p => p.status === 'invalid');
2098
+ const orphanedProjects = projectsWithSecrets.filter(p => p.status === 'orphaned');
2099
+ console.log(chalk_1.default.yellow(`\n发现 ${projectsWithSecrets.length} 个项目包含敏感信息\n`));
2100
+ // 构建交互选择列表(按状态分组)
2101
+ const choices = [];
2102
+ // 有效项目组
2103
+ if (validProjects.length > 0) {
2104
+ choices.push({
2105
+ name: chalk_1.default.green('━━ 有效项目 ━━'),
2106
+ value: '---valid---',
2107
+ short: '有效项目',
2108
+ });
2109
+ for (const project of validProjects.sort((a, b) => (a.realPath || '').localeCompare(b.realPath || ''))) {
2110
+ const shortPath = (project.realPath || '').replace(os.homedir(), '~');
2111
+ choices.push({
2112
+ name: `${chalk_1.default.green(shortPath)} ${chalk_1.default.gray(`(${project.findings.length} 处敏感信息)`)}`,
2113
+ value: project.normalizedPath,
2114
+ short: shortPath,
2115
+ });
2116
+ }
2117
+ }
2118
+ // 无效项目组
2119
+ if (invalidProjects.length > 0) {
2120
+ choices.push({
2121
+ name: chalk_1.default.red('━━ 无效项目 ━━'),
2122
+ value: '---invalid---',
2123
+ short: '无效项目',
2124
+ });
2125
+ for (const project of invalidProjects.sort((a, b) => (a.realPath || '').localeCompare(b.realPath || ''))) {
2126
+ const shortPath = (project.realPath || '').replace(os.homedir(), '~');
2127
+ choices.push({
2128
+ name: `${chalk_1.default.magenta(shortPath)} ${chalk_1.default.gray(`(${project.findings.length} 处敏感信息)`)}`,
2129
+ value: project.normalizedPath,
2130
+ short: shortPath,
2131
+ });
2132
+ }
2133
+ }
2134
+ // 孤立项目组
2135
+ if (orphanedProjects.length > 0) {
2136
+ choices.push({
2137
+ name: chalk_1.default.yellow('━━ 孤立项目 ━━'),
2138
+ value: '---orphaned---',
2139
+ short: '孤立项目',
2140
+ });
2141
+ for (const project of orphanedProjects.sort((a, b) => a.normalizedPath.localeCompare(b.normalizedPath))) {
2142
+ choices.push({
2143
+ name: `${chalk_1.default.yellow(project.normalizedPath)} ${chalk_1.default.gray(`(${project.findings.length} 处敏感信息)`)}`,
2144
+ value: project.normalizedPath,
2145
+ short: project.normalizedPath,
2146
+ });
2147
+ }
2148
+ }
2149
+ // 交互选择项目(支持重新选择分隔项)
2150
+ let selectedNormalizedPath;
2151
+ while (true) {
2152
+ const result = await inquirer_1.default.prompt([{
2153
+ type: 'list',
2154
+ name: 'selectedNormalizedPath',
2155
+ message: '选择要处理的项目:',
2156
+ choices,
2157
+ pageSize: 20,
2158
+ }]);
2159
+ selectedNormalizedPath = result.selectedNormalizedPath;
2160
+ // 检查是否选择了分隔项
2161
+ if (!selectedNormalizedPath.startsWith('---')) {
2162
+ break;
2163
+ }
2164
+ // 如果选择了分隔项,显示提示并重新选择
2165
+ console.log(chalk_1.default.gray('请选择一个项目,而不是分组标题'));
2166
+ }
2167
+ // 获取选中项目的扫描结果
2168
+ const selectedProjectData = projectsWithSecrets.find(p => p.normalizedPath === selectedNormalizedPath);
2169
+ if (!selectedProjectData) {
2170
+ console.log(chalk_1.default.red('错误: 无法找到选中的项目数据'));
2171
+ return;
2172
+ }
2173
+ const findings = selectedProjectData.findings;
2174
+ const projectDir = path.join(os.homedir(), '.claude', 'projects', selectedNormalizedPath);
2175
+ // 显示项目标题
2176
+ console.log();
2177
+ if (selectedProjectData.status === 'valid' && selectedProjectData.realPath) {
2178
+ const shortPath = selectedProjectData.realPath.replace(os.homedir(), '~');
2179
+ console.log(chalk_1.default.bold(`\n🔍 项目: ${chalk_1.default.green(shortPath)}`));
2180
+ console.log(chalk_1.default.gray(` ~/.claude/projects/${selectedNormalizedPath}`));
2181
+ }
2182
+ else if (selectedProjectData.status === 'invalid' && selectedProjectData.realPath) {
2183
+ const shortPath = selectedProjectData.realPath.replace(os.homedir(), '~');
2184
+ console.log(chalk_1.default.bold(`\n🔍 项目: ${chalk_1.default.magenta(shortPath)}`));
2185
+ console.log(chalk_1.default.gray(` ~/.claude/projects/${selectedNormalizedPath}`));
2186
+ }
2187
+ else {
2188
+ console.log(chalk_1.default.bold(`\n🔍 项目: ${chalk_1.default.yellow(selectedNormalizedPath)}`));
2189
+ console.log(chalk_1.default.gray(` ~/.claude/projects/${selectedNormalizedPath}`));
2190
+ }
2191
+ console.log();
2192
+ // 按类型统计敏感信息
2193
+ const byType = new Map();
2194
+ for (const finding of findings) {
2195
+ const list = byType.get(finding.type) || [];
2196
+ list.push(finding);
2197
+ byType.set(finding.type, list);
2198
+ }
2199
+ // 按文件统计,并收集每文件的敏感信息类型
2200
+ const byFile = new Map();
2201
+ for (const finding of findings) {
2202
+ const relativePath = finding.path.replace(projectDir, '.');
2203
+ if (!byFile.has(relativePath)) {
2204
+ byFile.set(relativePath, new Map());
2205
+ }
2206
+ const typeMap = byFile.get(relativePath);
2207
+ const list = typeMap.get(finding.type) || [];
2208
+ list.push(finding);
2209
+ typeMap.set(finding.type, list);
2210
+ }
2211
+ // 显示统计概览
2212
+ console.log(chalk_1.default.red.bold(`⚠️ 发现 ${findings.length} 处敏感信息:\n`));
2213
+ console.log(chalk_1.default.bold(' 🔑 按类型分布:'));
2214
+ for (const [type, typeFindings] of byType) {
2215
+ console.log(` ${chalk_1.default.cyan('•')} ${type}: ${chalk_1.default.yellow(String(typeFindings.length))} 处`);
2216
+ }
2217
+ console.log();
2218
+ const fileGroups = [];
2219
+ for (const [relativePath, typeMap] of byFile) {
2220
+ let totalCount = 0;
2221
+ for (const list of typeMap.values()) {
2222
+ totalCount += list.length;
2223
+ }
2224
+ fileGroups.push({ relativePath, typeMap, totalCount });
2225
+ }
2226
+ // 按文件路径排序
2227
+ fileGroups.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
2228
+ console.log(chalk_1.default.bold(' 📁 涉及文件列表:'));
2229
+ for (const group of fileGroups) {
2230
+ const typeNames = Array.from(group.typeMap.keys()).join(', ');
2231
+ console.log(` ${chalk_1.default.cyan('•')} ${group.relativePath}: ${chalk_1.default.yellow(String(group.totalCount))} 处 ${chalk_1.default.gray(`(${typeNames})`)}`);
2232
+ }
2233
+ console.log();
2234
+ // 收集所有敏感信息(所有文件)
2235
+ const allFindings = [];
2236
+ for (const group of fileGroups) {
2237
+ for (const [type, findingsList] of group.typeMap) {
2238
+ for (const f of findingsList) {
2239
+ allFindings.push({
2240
+ type: f.type,
2241
+ description: f.description,
2242
+ path: f.path,
2243
+ line: f.line,
2244
+ context: f.context,
2245
+ masked: f.masked,
2246
+ originalSecret: f.originalSecret,
2247
+ fullMatch: f.fullMatch,
2248
+ replacement: maskSecret(f.originalSecret),
2249
+ });
2250
+ }
2251
+ }
2252
+ }
2253
+ // 二次确认
2254
+ const confirmedMask = await (0, prompts_1.confirm)({
2255
+ message: chalk_1.default.red(`确认创建备份并替换所有 ${allFindings.length} 处敏感信息?`),
2256
+ default: false,
2257
+ });
2258
+ if (!confirmedMask) {
2259
+ console.log(chalk_1.default.yellow('操作已取消'));
2260
+ return;
2261
+ }
2262
+ // 执行掩码
2263
+ console.log(chalk_1.default.bold('\n📝 正在创建备份并替换敏感信息...\n'));
2264
+ const maskResult = await (0, scanner_1.maskProjectSecrets)(projectDir, allFindings);
2265
+ // 显示结果
2266
+ console.log(chalk_1.default.green('✓ 备份已创建'));
2267
+ console.log(` ${chalk_1.default.gray('备份文件数:')} ${maskResult.backups.length}`);
2268
+ for (const backup of maskResult.backups) {
2269
+ const relativeOriginal = backup.originalPath.replace(projectDir, '.');
2270
+ const backupFilename = path.basename(backup.backupPath);
2271
+ console.log(` ${chalk_1.default.cyan(relativeOriginal)} ${chalk_1.default.gray('→')} ${chalk_1.default.yellow(backupFilename)}`);
2272
+ }
2273
+ console.log();
2274
+ console.log(chalk_1.default.green(`✓ 掩码替换完成`));
2275
+ console.log(` ${chalk_1.default.gray('修改文件数:')} ${maskResult.filesModified.length}`);
2276
+ console.log();
2277
+ if (maskResult.filesModified.length > 0) {
2278
+ console.log(chalk_1.default.bold('修改的文件:'));
2279
+ for (const file of maskResult.filesModified) {
2280
+ const relativePath = file.replace(projectDir, '.');
2281
+ console.log(` ${chalk_1.default.cyan(relativePath)}`);
2282
+ }
2283
+ }
2284
+ console.log();
2285
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
2286
+ console.log(chalk_1.default.yellow('如需恢复原始文件,请运行以下命令:'));
2287
+ console.log(chalk_1.default.gray(' # 恢复项目所有文件'));
2288
+ console.log(` lcch scan-secrets --mask --restore ${projectDir}`);
2289
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
2290
+ console.log();
2291
+ return;
2292
+ }
2293
+ // 普通模式:扫描缓存
2294
+ console.log(chalk_1.default.bold('\n🔍 正在扫描缓存中的敏感信息...\n'));
2295
+ const findings = await (0, scanner_1.scanCacheSecrets)();
2296
+ // 表格形式输出
2297
+ if (findings.length === 0) {
2298
+ console.log(chalk_1.default.green('✓ 未发现敏感信息'));
2299
+ console.log(chalk_1.default.gray(' 扫描了以下位置:'));
2300
+ console.log(chalk_1.default.gray(' - ~/.claude/settings.json'));
2301
+ console.log(chalk_1.default.gray(' - ~/.claude/projects/'));
2302
+ console.log(chalk_1.default.gray(' - ~/.claude/history.jsonl'));
2303
+ console.log(chalk_1.default.gray(' - ~/.claude/session-env/'));
2304
+ }
2305
+ else {
2306
+ // 读取配置并扫描项目状态
2307
+ const config = await (0, scanner_1.readConfig)();
2308
+ const scanResult = await (0, scanner_1.scanProjects)(config);
2309
+ // 构建项目状态映射
2310
+ const validProjectsSet = new Set(scanResult.validProjects);
2311
+ const invalidProjectsSet = new Set(scanResult.invalidProjects);
2312
+ const allConfigProjects = new Set([...scanResult.validProjects, ...scanResult.invalidProjects]);
2313
+ // 构建 normalizedPath -> projectPath 的映射
2314
+ const normalizedToProjectMap = new Map();
2315
+ for (const configPath of allConfigProjects) {
2316
+ const configNormalizedPath = configPath.replace(/\//g, '-');
2317
+ normalizedToProjectMap.set(configNormalizedPath, configPath);
2318
+ }
2319
+ // 按状态分类收集涉及的项目
2320
+ const validWithSecrets = [];
2321
+ const invalidWithSecrets = [];
2322
+ const orphanedWithSecrets = [];
2323
+ // 收集所有涉及的 normalizedPath
2324
+ const involvedNormalizedPaths = new Set();
2325
+ for (const finding of findings) {
2326
+ const projectsMatch = finding.path.match(/\.claude\/projects\/([^/]+)/);
2327
+ if (projectsMatch) {
2328
+ involvedNormalizedPaths.add(projectsMatch[1]);
2329
+ }
2330
+ }
2331
+ // 分类项目
2332
+ for (const normalizedPath of involvedNormalizedPaths) {
2333
+ const projectPath = normalizedToProjectMap.get(normalizedPath);
2334
+ if (projectPath) {
2335
+ // 在配置中
2336
+ if (validProjectsSet.has(projectPath)) {
2337
+ validWithSecrets.push(projectPath);
2338
+ }
2339
+ else {
2340
+ invalidWithSecrets.push(projectPath);
2341
+ }
2342
+ }
2343
+ else {
2344
+ // 不在配置中(孤立项目)
2345
+ orphanedWithSecrets.push(normalizedPath);
2346
+ }
2347
+ }
2348
+ const totalProjects = validWithSecrets.length + invalidWithSecrets.length + orphanedWithSecrets.length;
2349
+ console.log(chalk_1.default.red.bold(`\n⚠️ 发现 ${findings.length} 处敏感信息(涉及 ${totalProjects} 个项目)\n`));
2350
+ // 显示涉及的项目列表(按状态分组)
2351
+ if (totalProjects > 0) {
2352
+ console.log(chalk_1.default.bold('📁 涉及项目列表:'));
2353
+ // 有效项目
2354
+ if (validWithSecrets.length > 0) {
2355
+ console.log(chalk_1.default.green(`\n ✓ 有效项目 (${validWithSecrets.length} 个):`));
2356
+ for (const projectPath of validWithSecrets.sort()) {
2357
+ const shortPath = projectPath.replace(os.homedir(), '~');
2358
+ const normalizedPath = projectPath.replace(/\//g, '-');
2359
+ console.log(` ${chalk_1.default.green('•')} ${chalk_1.default.cyan(shortPath)}`);
2360
+ console.log(` ${chalk_1.default.gray('~/.claude/projects/' + normalizedPath)}`);
2361
+ }
2362
+ }
2363
+ // 无效项目
2364
+ if (invalidWithSecrets.length > 0) {
2365
+ console.log(chalk_1.default.red(`\n ✗ 无效项目 (${invalidWithSecrets.length} 个):`));
2366
+ for (const projectPath of invalidWithSecrets.sort()) {
2367
+ const shortPath = projectPath.replace(os.homedir(), '~');
2368
+ const normalizedPath = projectPath.replace(/\//g, '-');
2369
+ console.log(` ${chalk_1.default.red('•')} ${chalk_1.default.magenta(shortPath)}`);
2370
+ console.log(` ${chalk_1.default.gray('~/.claude/projects/' + normalizedPath)}`);
2371
+ }
2372
+ }
2373
+ // 孤立项目
2374
+ if (orphanedWithSecrets.length > 0) {
2375
+ console.log(chalk_1.default.yellow(`\n ⚠ 孤立项目 (${orphanedWithSecrets.length} 个 - 已删除但数据仍存在):`));
2376
+ for (const normalizedPath of orphanedWithSecrets.sort()) {
2377
+ console.log(` ${chalk_1.default.yellow('•')} ${chalk_1.default.yellow(normalizedPath)}`);
2378
+ }
2379
+ }
2380
+ console.log();
2381
+ }
2382
+ // 按类型分组显示敏感信息明细
2383
+ const byType = new Map();
2384
+ for (const finding of findings) {
2385
+ const list = byType.get(finding.type) || [];
2386
+ list.push(finding);
2387
+ byType.set(finding.type, list);
2388
+ }
2389
+ for (const [type, typeFindings] of byType) {
2390
+ console.log(chalk_1.default.red.bold(` 🔑 ${type} (${typeFindings.length} 处)`));
2391
+ // 按项目分组(有效/无效/孤立)
2392
+ const validFindings = typeFindings.filter(f => {
2393
+ const match = f.path.match(/\.claude\/projects\/([^/]+)/);
2394
+ if (!match)
2395
+ return false;
2396
+ const projectPath = normalizedToProjectMap.get(match[1]);
2397
+ return projectPath && validProjectsSet.has(projectPath);
2398
+ });
2399
+ const invalidFindings = typeFindings.filter(f => {
2400
+ const match = f.path.match(/\.claude\/projects\/([^/]+)/);
2401
+ if (!match)
2402
+ return false;
2403
+ const projectPath = normalizedToProjectMap.get(match[1]);
2404
+ return projectPath && invalidProjectsSet.has(projectPath);
2405
+ });
2406
+ const orphanedFindings = typeFindings.filter(f => {
2407
+ const match = f.path.match(/\.claude\/projects\/([^/]+)/);
2408
+ if (!match)
2409
+ return false;
2410
+ return !normalizedToProjectMap.has(match[1]);
2411
+ });
2412
+ // 按文件路径分组,合并相同的敏感信息
2413
+ const byFile = new Map();
2414
+ for (const finding of typeFindings) {
2415
+ const relativePath = finding.path.replace(os.homedir(), '~');
2416
+ if (!byFile.has(relativePath)) {
2417
+ byFile.set(relativePath, new Map());
2418
+ }
2419
+ const fileFindings = byFile.get(relativePath);
2420
+ // 使用 masked 值作为唯一标识来合并相同的敏感信息
2421
+ const key = finding.masked;
2422
+ if (!fileFindings.has(key)) {
2423
+ fileFindings.set(key, {
2424
+ lines: [],
2425
+ masked: finding.masked,
2426
+ fullMatch: finding.fullMatch,
2427
+ });
2428
+ }
2429
+ const entry = fileFindings.get(key);
2430
+ if (finding.line && !entry.lines.includes(finding.line)) {
2431
+ entry.lines.push(finding.line);
2432
+ }
2433
+ }
2434
+ // 输出合并后的结果
2435
+ for (const [relativePath, fileFindings] of byFile) {
2436
+ // 确定文件所属的项目状态
2437
+ const projectsMatch = relativePath.match(/\.claude\/projects\/([^/]+)/);
2438
+ let statusIndicator = chalk_1.default.gray('📄');
2439
+ let pathColor;
2440
+ let statusLabel;
2441
+ if (projectsMatch) {
2442
+ const normalizedPath = projectsMatch[1];
2443
+ const projectPath = normalizedToProjectMap.get(normalizedPath);
2444
+ if (projectPath) {
2445
+ if (validProjectsSet.has(projectPath)) {
2446
+ statusIndicator = chalk_1.default.green('✓');
2447
+ pathColor = chalk_1.default.green; // 有效项目用绿色
2448
+ statusLabel = 'valid';
2449
+ }
2450
+ else {
2451
+ statusIndicator = chalk_1.default.red('✗');
2452
+ pathColor = chalk_1.default.magenta; // 无效项目用洋红色
2453
+ statusLabel = 'invalid';
2454
+ }
2455
+ }
2456
+ else {
2457
+ statusIndicator = chalk_1.default.yellow('⚠');
2458
+ pathColor = chalk_1.default.yellow; // 孤立项目用黄色
2459
+ statusLabel = 'orphaned';
2460
+ }
2461
+ }
2462
+ else {
2463
+ // 非项目文件(如 settings.json)
2464
+ pathColor = chalk_1.default.cyan;
2465
+ statusLabel = 'other';
2466
+ }
2467
+ // 每个文件显示一次路径,根据项目状态使用不同颜色
2468
+ const pathParts = relativePath.split('/');
2469
+ const coloredPath = pathParts.map((part, index) => {
2470
+ if (index === pathParts.length - 1) {
2471
+ // 文件名用白色加粗
2472
+ return chalk_1.default.white.bold(part);
2473
+ }
2474
+ else if (part.startsWith('.')) {
2475
+ // 隐藏目录用灰色
2476
+ return chalk_1.default.gray(part);
2477
+ }
2478
+ else {
2479
+ // 根据项目状态使用不同颜色
2480
+ return pathColor(part);
2481
+ }
2482
+ }).join(chalk_1.default.gray('/'));
2483
+ console.log(` ${statusIndicator} ${coloredPath}`);
2484
+ if (options.show) {
2485
+ // --show 模式:去掉序号,每项信息换行,纵向对齐
2486
+ const entries = Array.from(fileFindings.entries());
2487
+ entries.forEach(([key, entry], index) => {
2488
+ const lineCount = entry.lines.length;
2489
+ const sortedLines = entry.lines.sort((a, b) => a - b);
2490
+ const isLast = index === entries.length - 1;
2491
+ const connector = isLast ? '└─' : '├─';
2492
+ console.log(` ${chalk_1.default.gray(`${connector} 匹配:`)} ${chalk_1.default.yellow(entry.fullMatch)}`);
2493
+ console.log(` ${chalk_1.default.gray(isLast ? ' ' : '│ ')} ${chalk_1.default.gray('条数:')} ${chalk_1.default.cyan(lineCount.toString())}`);
2494
+ if (sortedLines.length > 0) {
2495
+ console.log(` ${chalk_1.default.gray(isLast ? ' ' : '│ ')} ${chalk_1.default.gray('行号:')} ${chalk_1.default.cyan(sortedLines.join(', '))}`);
2496
+ }
2497
+ });
2498
+ }
2499
+ else {
2500
+ // 普通模式:每项信息换行,纵向对齐
2501
+ const entries = Array.from(fileFindings.entries());
2502
+ entries.forEach(([key, entry], index) => {
2503
+ const lineCount = entry.lines.length;
2504
+ const sortedLines = entry.lines.sort((a, b) => a - b);
2505
+ const isLast = index === entries.length - 1;
2506
+ const connector = isLast ? '└─' : '├─';
2507
+ console.log(` ${chalk_1.default.gray(`${connector} 匹配:`)} ${chalk_1.default.yellow(entry.masked)}`);
2508
+ console.log(` ${chalk_1.default.gray(isLast ? ' ' : '│ ')} ${chalk_1.default.gray('条数:')} ${chalk_1.default.cyan(lineCount.toString())}`);
2509
+ if (sortedLines.length > 0) {
2510
+ console.log(` ${chalk_1.default.gray(isLast ? ' ' : '│ ')} ${chalk_1.default.gray('行号:')} ${chalk_1.default.cyan(sortedLines.join(', '))}`);
2511
+ }
2512
+ });
2513
+ }
2514
+ console.log(); // 每个文件后空一行
2515
+ }
2516
+ }
2517
+ console.log(chalk_1.default.bold('\n─────────────────────────────────────'));
2518
+ console.log(chalk_1.default.bold(' 安全建议'));
2519
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
2520
+ console.log(chalk_1.default.yellow(' 1. 定期清理会话历史和 resume 数据'));
2521
+ console.log(chalk_1.default.yellow(' 2. 定期轮换 API 密钥'));
2522
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
2523
+ }
2524
+ console.log();
2525
+ }
2526
+ catch (error) {
2527
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
2528
+ process.exit(1);
2529
+ }
2530
+ });
2531
+ // ============================================================
2532
+ // project 子命令 - 单个项目管理
2533
+ // ============================================================
2534
+ const projectCmd = commander_1.program
2535
+ .command('project <path>')
2536
+ .description('管理单个 Claude Code 项目')
2537
+ .option('--sessions', '显示该项目的所有会话', false)
2538
+ .option('--delete-sessions', '删除选中的会话(需配合 --sessions 使用)', false)
2539
+ .option('--resume', '显示该项目的 resume 历史记录', false)
2540
+ .addHelpText('after', `
2541
+ ${chalk_1.default.bold('参数:')}
2542
+ ${chalk_1.default.cyan('<path>')} 项目路径(支持相对路径和 ~ 符号)
2543
+
2544
+ ${chalk_1.default.bold('选项:')}
2545
+ ${chalk_1.default.cyan('--sessions')} 显示该项目的所有会话
2546
+ ${chalk_1.default.cyan('--delete-sessions')} 删除选中的会话(需配合 --sessions 使用)
2547
+ ${chalk_1.default.cyan('--resume')} 显示该项目的 resume 历史记录
2548
+
2549
+ ${chalk_1.default.bold('常用示例:')}
2550
+ ${chalk_1.default.cyan('lcch project /path/to/project')} 查看项目基本信息
2551
+ ${chalk_1.default.cyan('lcch project ~/workspace/my-project')} 使用 ~ 符号
2552
+ ${chalk_1.default.cyan('lcch project ./my-project')} 使用相对路径
2553
+ ${chalk_1.default.cyan('lcch project /path/to/project --sessions')} 查看会话历史
2554
+ ${chalk_1.default.cyan('lcch project /path/to/project --sessions --delete-sessions')} 删除会话
2555
+ ${chalk_1.default.cyan('lcch project /path/to/project --resume')} 查看 resume 历史
2556
+ ${chalk_1.default.cyan('lcch project /path/to/project --resume --sessions')} 同时查看 resume 和会话
2557
+ `)
2558
+ .action(async (projectPath, options) => {
2559
+ try {
2560
+ const config = await (0, scanner_1.readConfig)();
2561
+ // 标准化路径(处理 ~ 符号)
2562
+ const expandedPath = projectPath.startsWith('~')
2563
+ ? path.join(os.homedir(), projectPath.slice(1))
2564
+ : projectPath;
2565
+ const absolutePath = path.resolve(expandedPath);
2566
+ // 检查路径是否存在
2567
+ const isValid = await fs.pathExists(absolutePath);
2568
+ // 获取项目的配置信息(如果存在)
2569
+ const projectConfig = config.projects[absolutePath];
2570
+ // 显示项目基本信息
2571
+ console.log(chalk_1.default.bold('\n项目信息:'));
2572
+ console.log(` 路径: ${absolutePath}`);
2573
+ console.log(` 状态: ${isValid ? chalk_1.default.green('有效') : chalk_1.default.red('无效')}`);
2574
+ if (projectConfig) {
2575
+ console.log(` 最后会话: ${projectConfig.lastSessionId || 'N/A'}`);
2576
+ console.log(` 花费: $${projectConfig.lastCost?.toFixed(4) || 0}`);
2577
+ }
2578
+ else {
2579
+ console.log(` 配置状态: ${chalk_1.default.yellow('未在 claude.json 中注册')}`);
2580
+ }
2581
+ // 如果指定了 --resume,显示 resume 历史
2582
+ if (options.resume) {
2583
+ const resumeInfos = await (0, scanner_1.getProjectResumeInfo)(absolutePath);
2584
+ console.log(chalk_1.default.bold(`\nResume 历史 (${resumeInfos.length} 个会话):\n`));
2585
+ if (resumeInfos.length === 0) {
2586
+ console.log(chalk_1.default.gray(' 无 resume 记录'));
2587
+ }
2588
+ else {
2589
+ resumeInfos.forEach((info, index) => {
2590
+ const isLast = index === resumeInfos.length - 1;
2591
+ const connector = isLast ? '└── ' : '├── ';
2592
+ const sessionIdStr = chalk_1.default.cyan(`[${info.sessionId}]`);
2593
+ const createdStr = chalk_1.default.gray(`创建: ${(0, scanner_1.formatRelativeTime)(info.createdAt.getTime())}`);
2594
+ const lastActivityStr = chalk_1.default.gray(`最后活动: ${(0, scanner_1.formatRelativeTime)(info.lastActivity.getTime())}`);
2595
+ const messagesStr = chalk_1.default.yellow(`${info.messageCount} 条消息`);
2596
+ const compactStr = info.compactCount > 0 ? chalk_1.default.magenta(` (${info.compactCount} 次压缩)`) : '';
2597
+ const sizeStr = chalk_1.default.gray((0, backup_1.formatFileSize)(info.fileSize));
2598
+ console.log(` ${connector}${sessionIdStr}`);
2599
+ console.log(` ${createdStr}`);
2600
+ console.log(` ${lastActivityStr}`);
2601
+ console.log(` ${messagesStr}${compactStr} | ${sizeStr}`);
2602
+ });
2603
+ }
2604
+ console.log();
2605
+ }
2606
+ // 如果指定了 --sessions,显示会话历史
2607
+ if (options.sessions) {
2608
+ const sessions = await (0, scanner_1.readSessionHistory)();
2609
+ const sessionsByProject = (0, scanner_1.groupSessionsByProject)(sessions);
2610
+ const projectSessions = sessionsByProject.get(absolutePath) || [];
2611
+ console.log(chalk_1.default.bold(`\n会话历史 (${projectSessions.length} 条):\n`));
2612
+ if (projectSessions.length === 0) {
2613
+ console.log(chalk_1.default.gray(' 无会话记录'));
2614
+ return;
2615
+ }
2616
+ // 显示所有会话
2617
+ projectSessions.forEach((session, index) => {
2618
+ const isLast = index === projectSessions.length - 1;
2619
+ const connector = isLast ? '└── ' : '├── ';
2620
+ const timeStr = chalk_1.default.gray(`[${(0, scanner_1.formatRelativeTime)(session.timestamp)}]`);
2621
+ const sessionIdStr = chalk_1.default.cyan(`[${session.sessionId}]`);
2622
+ const content = (0, scanner_1.truncateText)(session.display || '(空)', 50);
2623
+ console.log(` ${connector}${timeStr} ${sessionIdStr} ${content}`);
2624
+ });
2625
+ // 如果指定了 --delete-sessions,删除选中的会话
2626
+ if (options.deleteSessions) {
2627
+ console.log();
2628
+ const selectedSessions = await (0, prompts_1.checkbox)({
2629
+ message: '选择要删除的会话(空格选择,回车确认):',
2630
+ choices: projectSessions.map(s => ({
2631
+ name: `[${(0, scanner_1.formatRelativeTime)(s.timestamp)}] ${(0, scanner_1.truncateText)(s.display || '(空)', 40)}`,
2632
+ value: s.sessionId,
2633
+ })),
2634
+ pageSize: 20,
2635
+ theme: {
2636
+ style: {
2637
+ keysHelpTip: () => undefined,
2638
+ },
2639
+ },
2640
+ });
2641
+ if (selectedSessions.length === 0) {
2642
+ console.log(chalk_1.default.yellow('未选择任何会话,操作已取消'));
2643
+ return;
2644
+ }
2645
+ const confirmedDeleteSessions = await (0, prompts_1.confirm)({
2646
+ message: chalk_1.default.red(`确认删除选中的 ${selectedSessions.length} 条会话记录?`),
2647
+ default: false,
2648
+ });
2649
+ if (!confirmedDeleteSessions) {
2650
+ console.log(chalk_1.default.yellow('操作已取消'));
2651
+ return;
2652
+ }
2653
+ const deletedCount = await (0, scanner_1.deleteSessions)(new Set(selectedSessions));
2654
+ console.log(chalk_1.default.green(`✓ 成功删除 ${deletedCount} 条会话记录!`));
2655
+ }
2656
+ }
2657
+ console.log();
2658
+ }
2659
+ catch (error) {
2660
+ console.error(chalk_1.default.red(`错误: ${error.message}`));
2661
+ process.exit(1);
2662
+ }
2663
+ });
2664
+ function createSpinner(text) {
2665
+ let spinner = null;
2666
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
2667
+ let index = 0;
2668
+ return {
2669
+ start: () => {
2670
+ process.stdout.write(`${frames[0]} ${text}`);
2671
+ spinner = setInterval(() => {
2672
+ index = (index + 1) % frames.length;
2673
+ process.stdout.write(`\r${frames[index]} ${text}`);
2674
+ }, 80);
2675
+ },
2676
+ stop: () => {
2677
+ if (spinner) {
2678
+ clearInterval(spinner);
2679
+ process.stdout.write(`\r✓ ${text}\n`);
2680
+ }
2681
+ },
2682
+ };
2683
+ }
2684
+ function printScanResult(result) {
2685
+ console.log(chalk_1.default.bold('\n扫描结果:'));
2686
+ console.log(` 总项目数: ${result.totalCount}`);
2687
+ console.log(chalk_1.default.green(` 有效项目: ${result.validCount}`));
2688
+ console.log(chalk_1.default.red(` 无效项目: ${result.invalidCount}`));
2689
+ if (result.invalidProjects.length > 0) {
2690
+ console.log(chalk_1.default.red.bold('\n无效项目列表:'));
2691
+ result.invalidProjects.forEach((project, index) => {
2692
+ console.log(` ${index + 1}. ${chalk_1.default.red(project)}`);
2693
+ });
2694
+ }
2695
+ }
2696
+ function printSummary(result) {
2697
+ console.log(chalk_1.default.bold('\n汇总:'));
2698
+ console.log(` ${chalk_1.default.green('✓')} 有效: ${result.validCount}`);
2699
+ console.log(` ${chalk_1.default.red('✗')} 无效: ${result.invalidCount}`);
2700
+ console.log(` 总计: ${result.totalCount}`);
2701
+ if (result.invalidCount > 0) {
2702
+ console.log(chalk_1.default.yellow(`\n提示: 使用 ${chalk_1.default.cyan('lcch projects clean')} 清理无效项目`));
2703
+ }
2704
+ }
2705
+ /**
2706
+ * 获取单个项目的详细统计信息
2707
+ */
2708
+ async function getProjectStats(config, projectPath, isValid) {
2709
+ const projectConfig = config.projects[projectPath];
2710
+ // 基础信息
2711
+ const stats = {
2712
+ projectPath,
2713
+ isValid,
2714
+ cost: projectConfig?.lastCost || 0,
2715
+ resumes: 0,
2716
+ sessions: 0,
2717
+ mcpServers: projectConfig?.mcpServers ? Object.keys(projectConfig.mcpServers).length : 0,
2718
+ skills: 0,
2719
+ hooks: 0,
2720
+ diskUsage: 0,
2721
+ inputTokens: projectConfig?.lastTotalInputTokens || 0,
2722
+ outputTokens: projectConfig?.lastTotalOutputTokens || 0,
2723
+ };
2724
+ // 获取 resume 数量和磁盘占用
2725
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
2726
+ const normalizedPath = projectPath.replace(/\//g, '-');
2727
+ const projectDir = path.join(projectsDir, normalizedPath);
2728
+ if (await fs.pathExists(projectDir)) {
2729
+ const entries = await fs.readdir(projectDir, { withFileTypes: true });
2730
+ stats.resumes = entries.filter(e => e.isFile() && e.name.endsWith('.jsonl')).length;
2731
+ // 计算磁盘占用
2732
+ let totalSize = 0;
2733
+ for (const entry of entries) {
2734
+ if (entry.isFile()) {
2735
+ const filePath = path.join(projectDir, entry.name);
2736
+ const fileStat = await fs.stat(filePath);
2737
+ totalSize += fileStat.size;
2738
+ }
2739
+ }
2740
+ stats.diskUsage = totalSize;
2741
+ }
2742
+ // 获取会话数量
2743
+ const historyPath = path.join(os.homedir(), '.claude', 'history.jsonl');
2744
+ if (await fs.pathExists(historyPath)) {
2745
+ const content = await fs.readFile(historyPath, 'utf-8');
2746
+ const lines = content.trim().split('\n').filter(line => line.trim());
2747
+ for (const line of lines) {
2748
+ try {
2749
+ const record = JSON.parse(line);
2750
+ if (record.project === projectPath) {
2751
+ stats.sessions++;
2752
+ }
2753
+ }
2754
+ catch {
2755
+ // 忽略
2756
+ }
2757
+ }
2758
+ }
2759
+ // 获取 skills 数量(从项目级 settings)
2760
+ const projectSettingsPath = path.join(projectPath, '.claude', 'settings.json');
2761
+ if (await fs.pathExists(projectSettingsPath)) {
2762
+ try {
2763
+ const settingsContent = await fs.readFile(projectSettingsPath, 'utf-8');
2764
+ const settings = JSON.parse(settingsContent);
2765
+ if (settings.skills) {
2766
+ stats.skills = Object.keys(settings.skills).length;
2767
+ }
2768
+ // 获取 hooks 数量
2769
+ if (settings.hooks) {
2770
+ let hookCount = 0;
2771
+ for (const eventHooks of Object.values(settings.hooks)) {
2772
+ if (Array.isArray(eventHooks)) {
2773
+ for (const hookGroup of eventHooks) {
2774
+ if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) {
2775
+ hookCount += hookGroup.hooks.length;
2776
+ }
2777
+ }
2778
+ }
2779
+ }
2780
+ stats.hooks = hookCount;
2781
+ }
2782
+ }
2783
+ catch {
2784
+ // 忽略
2785
+ }
2786
+ }
2787
+ return stats;
2788
+ }
2789
+ /**
2790
+ * 打印项目树(详细信息)
2791
+ */
2792
+ async function printProjectTreeVerbose(config, result) {
2793
+ const allProjects = [...result.validProjects, ...result.invalidProjects];
2794
+ if (allProjects.length === 0) {
2795
+ console.log(chalk_1.default.yellow('没有项目'));
2796
+ return;
2797
+ }
2798
+ console.log(chalk_1.default.bold(`\n共 ${result.totalCount} 个项目(详细信息):\n`));
2799
+ // 收集所有项目的统计信息
2800
+ const projectStatsMap = new Map();
2801
+ const validSet = new Set(result.validProjects);
2802
+ const invalidSet = new Set(result.invalidProjects);
2803
+ // 批量收集统计信息
2804
+ for (const projectPath of allProjects) {
2805
+ const isValid = validSet.has(projectPath);
2806
+ const stats = await getProjectStats(config, projectPath, isValid);
2807
+ projectStatsMap.set(projectPath, stats);
2808
+ }
2809
+ // 构建树结构
2810
+ const tree = (0, scanner_1.buildProjectTree)(allProjects);
2811
+ // 打印树形结构
2812
+ printProjectTreeVerboseRecursive(tree, '', true, projectStatsMap, validSet, invalidSet);
2813
+ // 打印汇总
2814
+ printTreeVerboseSummary(projectStatsMap, result);
2815
+ }
2816
+ /**
2817
+ * 递归打印项目树(详细信息)
2818
+ */
2819
+ function printProjectTreeVerboseRecursive(tree, prefix = '', isLast = true, projectStatsMap, validProjects, invalidProjects) {
2820
+ const entries = Array.from(tree.entries());
2821
+ for (let i = 0; i < entries.length; i++) {
2822
+ const [name, value] = entries[i];
2823
+ const isLastItem = i === entries.length - 1;
2824
+ const connector = isLastItem ? '└── ' : '├── ';
2825
+ const childPrefix = isLastItem ? ' ' : '│ ';
2826
+ if (value && typeof value === 'object' && value._isLeaf) {
2827
+ // 叶子节点(项目)
2828
+ const fullPath = value._fullPath;
2829
+ const stats = projectStatsMap.get(fullPath);
2830
+ if (stats) {
2831
+ const statusIcon = stats.isValid ? chalk_1.default.green('✓') : chalk_1.default.red('✗');
2832
+ // 构建统计信息字符串(使用管道分隔)
2833
+ const statParts = [];
2834
+ // 花费
2835
+ const costStr = stats.cost > 0
2836
+ ? chalk_1.default.yellow(`花费: $${stats.cost.toFixed(2)}`)
2837
+ : chalk_1.default.gray('花费: $0.00');
2838
+ statParts.push(costStr);
2839
+ // Resume
2840
+ const resumeStr = stats.resumes > 0
2841
+ ? chalk_1.default.cyan(`Resume: ${stats.resumes}`)
2842
+ : chalk_1.default.gray('Resume: 0');
2843
+ statParts.push(resumeStr);
2844
+ // 会话
2845
+ const sessionStr = stats.sessions > 0
2846
+ ? chalk_1.default.cyan(`会话: ${stats.sessions}`)
2847
+ : chalk_1.default.gray('会话: 0');
2848
+ statParts.push(sessionStr);
2849
+ // MCP
2850
+ const mcpStr = stats.mcpServers > 0
2851
+ ? chalk_1.default.magenta(`MCP: ${stats.mcpServers}`)
2852
+ : chalk_1.default.gray('MCP: 0');
2853
+ statParts.push(mcpStr);
2854
+ // Skills
2855
+ const skillStr = stats.skills > 0
2856
+ ? chalk_1.default.green(`Skills: ${stats.skills}`)
2857
+ : chalk_1.default.gray('Skills: 0');
2858
+ statParts.push(skillStr);
2859
+ // Hooks
2860
+ const hooksStr = stats.hooks > 0
2861
+ ? chalk_1.default.magenta(`Hooks: ${stats.hooks}`)
2862
+ : chalk_1.default.gray('Hooks: 0');
2863
+ statParts.push(hooksStr);
2864
+ // 磁盘
2865
+ const diskStr = stats.diskUsage > 0
2866
+ ? chalk_1.default.yellow(`磁盘: ${(0, backup_1.formatFileSize)(stats.diskUsage)}`)
2867
+ : chalk_1.default.gray('磁盘: 0 B');
2868
+ statParts.push(diskStr);
2869
+ console.log(`${prefix}${connector}${name} ${statusIcon}`);
2870
+ console.log(`${prefix}${childPrefix} ${statParts.join(' | ')}`);
2871
+ }
2872
+ }
2873
+ else if (value instanceof Map) {
2874
+ // 目录节点
2875
+ console.log(`${prefix}${connector}${chalk_1.default.white(name)}`);
2876
+ printProjectTreeVerboseRecursive(value, prefix + childPrefix, isLastItem, projectStatsMap, validProjects, invalidProjects);
2877
+ }
2878
+ }
2879
+ }
2880
+ /**
2881
+ * 打印树形详细信息的汇总
2882
+ */
2883
+ function printTreeVerboseSummary(projectStatsMap, result) {
2884
+ let totalCost = 0;
2885
+ let totalResumes = 0;
2886
+ let totalSessions = 0;
2887
+ let totalMcp = 0;
2888
+ let totalSkills = 0;
2889
+ let totalHooks = 0;
2890
+ let totalDisk = 0;
2891
+ for (const stats of projectStatsMap.values()) {
2892
+ totalCost += stats.cost;
2893
+ totalResumes += stats.resumes;
2894
+ totalSessions += stats.sessions;
2895
+ totalMcp += stats.mcpServers;
2896
+ totalSkills += stats.skills;
2897
+ totalHooks += stats.hooks;
2898
+ totalDisk += stats.diskUsage;
2899
+ }
2900
+ console.log();
2901
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
2902
+ console.log(chalk_1.default.bold(' 汇总'));
2903
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
2904
+ console.log(` ${chalk_1.default.green('✓')} 有效项目: ${result.validCount}`);
2905
+ console.log(` ${chalk_1.default.red('✗')} 无效项目: ${result.invalidCount}`);
2906
+ console.log(` ${chalk_1.default.yellow('花费:')} $${totalCost.toFixed(4)}`);
2907
+ console.log(` ${chalk_1.default.cyan('Resume:')} ${totalResumes}`);
2908
+ console.log(` ${chalk_1.default.cyan('会话:')} ${totalSessions}`);
2909
+ console.log(` ${chalk_1.default.magenta('MCP:')} ${totalMcp}`);
2910
+ console.log(` ${chalk_1.default.green('Skills:')} ${totalSkills}`);
2911
+ console.log(` ${chalk_1.default.magenta('Hooks:')} ${totalHooks}`);
2912
+ console.log(` ${chalk_1.default.yellow('磁盘:')} ${(0, backup_1.formatFileSize)(totalDisk)}`);
2913
+ console.log(chalk_1.default.bold('─────────────────────────────────────'));
2914
+ console.log();
2915
+ }
2916
+ /**
2917
+ * 打印项目列表(详细信息)
2918
+ */
2919
+ async function printProjectListVerbose(config, result) {
2920
+ const allProjects = [...result.validProjects, ...result.invalidProjects];
2921
+ if (allProjects.length === 0) {
2922
+ console.log(chalk_1.default.yellow('没有项目'));
2923
+ return;
2924
+ }
2925
+ console.log(chalk_1.default.bold(`\n共 ${result.totalCount} 个项目(详细信息):\n`));
2926
+ // 收集所有项目的统计信息
2927
+ const projectStatsMap = new Map();
2928
+ const validSet = new Set(result.validProjects);
2929
+ const invalidSet = new Set(result.invalidProjects);
2930
+ // 批量收集统计信息
2931
+ for (const projectPath of allProjects) {
2932
+ const isValid = validSet.has(projectPath);
2933
+ const stats = await getProjectStats(config, projectPath, isValid);
2934
+ projectStatsMap.set(projectPath, stats);
2935
+ }
2936
+ // 按花费排序
2937
+ const sortedProjects = [...projectStatsMap.values()].sort((a, b) => b.cost - a.cost);
2938
+ // 按状态分组
2939
+ const validProjects = sortedProjects.filter(p => p.isValid);
2940
+ const invalidProjects = sortedProjects.filter(p => !p.isValid);
2941
+ // 打印有效项目
2942
+ if (validProjects.length > 0) {
2943
+ console.log(chalk_1.default.green.bold(`✓ 有效项目 (${validProjects.length}):`));
2944
+ for (const stats of validProjects) {
2945
+ const costStr = stats.cost > 0
2946
+ ? chalk_1.default.yellow(`花费: $${stats.cost.toFixed(2)}`)
2947
+ : chalk_1.default.gray('花费: $0.00');
2948
+ const resumeStr = stats.resumes > 0
2949
+ ? chalk_1.default.cyan(`Resume: ${stats.resumes}`)
2950
+ : chalk_1.default.gray('Resume: 0');
2951
+ const sessionStr = stats.sessions > 0
2952
+ ? chalk_1.default.cyan(`会话: ${stats.sessions}`)
2953
+ : chalk_1.default.gray('会话: 0');
2954
+ const mcpStr = stats.mcpServers > 0
2955
+ ? chalk_1.default.magenta(`MCP: ${stats.mcpServers}`)
2956
+ : chalk_1.default.gray('MCP: 0');
2957
+ const skillStr = stats.skills > 0
2958
+ ? chalk_1.default.green(`Skills: ${stats.skills}`)
2959
+ : chalk_1.default.gray('Skills: 0');
2960
+ const hooksStr = stats.hooks > 0
2961
+ ? chalk_1.default.magenta(`Hooks: ${stats.hooks}`)
2962
+ : chalk_1.default.gray('Hooks: 0');
2963
+ const diskStr = stats.diskUsage > 0
2964
+ ? chalk_1.default.yellow(`磁盘: ${(0, backup_1.formatFileSize)(stats.diskUsage)}`)
2965
+ : chalk_1.default.gray('磁盘: 0 B');
2966
+ console.log(` ${chalk_1.default.green('✓')} ${chalk_1.default.white(stats.projectPath)}`);
2967
+ console.log(` ${[costStr, resumeStr, sessionStr, mcpStr, skillStr, hooksStr, diskStr].join(' | ')}`);
2968
+ }
2969
+ console.log();
2970
+ }
2971
+ // 打印无效项目
2972
+ if (invalidProjects.length > 0) {
2973
+ console.log(chalk_1.default.red.bold(`✗ 无效项目 (${invalidProjects.length}):`));
2974
+ for (const stats of invalidProjects) {
2975
+ const costStr = stats.cost > 0
2976
+ ? chalk_1.default.yellow(`花费: $${stats.cost.toFixed(2)}`)
2977
+ : chalk_1.default.gray('花费: $0.00');
2978
+ const resumeStr = stats.resumes > 0
2979
+ ? chalk_1.default.cyan(`Resume: ${stats.resumes}`)
2980
+ : chalk_1.default.gray('Resume: 0');
2981
+ const sessionStr = stats.sessions > 0
2982
+ ? chalk_1.default.cyan(`会话: ${stats.sessions}`)
2983
+ : chalk_1.default.gray('会话: 0');
2984
+ const mcpStr = stats.mcpServers > 0
2985
+ ? chalk_1.default.magenta(`MCP: ${stats.mcpServers}`)
2986
+ : chalk_1.default.gray('MCP: 0');
2987
+ const skillStr = stats.skills > 0
2988
+ ? chalk_1.default.green(`Skills: ${stats.skills}`)
2989
+ : chalk_1.default.gray('Skills: 0');
2990
+ const hooksStr = stats.hooks > 0
2991
+ ? chalk_1.default.magenta(`Hooks: ${stats.hooks}`)
2992
+ : chalk_1.default.gray('Hooks: 0');
2993
+ const diskStr = stats.diskUsage > 0
2994
+ ? chalk_1.default.yellow(`磁盘: ${(0, backup_1.formatFileSize)(stats.diskUsage)}`)
2995
+ : chalk_1.default.gray('磁盘: 0 B');
2996
+ console.log(` ${chalk_1.default.red('✗')} ${chalk_1.default.gray(stats.projectPath)}`);
2997
+ console.log(` ${[costStr, resumeStr, sessionStr, mcpStr, skillStr, hooksStr, diskStr].join(' | ')}`);
2998
+ }
2999
+ console.log();
3000
+ }
3001
+ // 打印汇总
3002
+ printTreeVerboseSummary(projectStatsMap, result);
3003
+ }
3004
+ /**
3005
+ * 格式化数字(添加千分位分隔符)
3006
+ */
3007
+ function formatNumber(num) {
3008
+ if (num === 0)
3009
+ return '0';
3010
+ if (num < 1000)
3011
+ return num.toString();
3012
+ return num.toLocaleString('zh-CN');
3013
+ }
3014
+ // 运行程序
3015
+ commander_1.program.parse();