@sooink/ai-session-tidy 0.1.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/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.ko.md +171 -0
- package/README.md +169 -0
- package/assets/demo-interactive.gif +0 -0
- package/assets/demo.gif +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1917 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +29 -0
- package/package.json +54 -0
- package/src/cli.ts +21 -0
- package/src/commands/clean.ts +335 -0
- package/src/commands/config.ts +144 -0
- package/src/commands/list.ts +86 -0
- package/src/commands/scan.ts +200 -0
- package/src/commands/watch.ts +359 -0
- package/src/core/cleaner.test.ts +125 -0
- package/src/core/cleaner.ts +181 -0
- package/src/core/service.ts +236 -0
- package/src/core/trash.test.ts +100 -0
- package/src/core/trash.ts +40 -0
- package/src/core/watcher.test.ts +210 -0
- package/src/core/watcher.ts +194 -0
- package/src/index.ts +5 -0
- package/src/scanners/claude-code.test.ts +112 -0
- package/src/scanners/claude-code.ts +452 -0
- package/src/scanners/cursor.test.ts +140 -0
- package/src/scanners/cursor.ts +133 -0
- package/src/scanners/index.ts +39 -0
- package/src/scanners/types.ts +34 -0
- package/src/utils/config.ts +132 -0
- package/src/utils/logger.ts +29 -0
- package/src/utils/paths.test.ts +95 -0
- package/src/utils/paths.ts +92 -0
- package/src/utils/size.test.ts +80 -0
- package/src/utils/size.ts +50 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { formatSize } from '../utils/size.js';
|
|
10
|
+
import { tildify } from '../utils/paths.js';
|
|
11
|
+
import {
|
|
12
|
+
createAllScanners,
|
|
13
|
+
getAvailableScanners,
|
|
14
|
+
runAllScanners,
|
|
15
|
+
} from '../scanners/index.js';
|
|
16
|
+
import { Cleaner } from '../core/cleaner.js';
|
|
17
|
+
import type { OrphanedSession } from '../scanners/types.js';
|
|
18
|
+
|
|
19
|
+
interface CleanOptions {
|
|
20
|
+
force?: boolean;
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
noTrash?: boolean;
|
|
23
|
+
verbose?: boolean;
|
|
24
|
+
interactive?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatSessionChoice(session: OrphanedSession): string {
|
|
28
|
+
const projectName = basename(session.projectPath);
|
|
29
|
+
const isConfig = session.type === 'config';
|
|
30
|
+
const toolTag = isConfig
|
|
31
|
+
? chalk.yellow('[config]')
|
|
32
|
+
: chalk.cyan(`[${session.toolName}]`);
|
|
33
|
+
const name = chalk.white(projectName);
|
|
34
|
+
const size = isConfig ? '' : chalk.dim(`(${formatSize(session.size)})`);
|
|
35
|
+
const path = chalk.dim(`→ ${session.projectPath}`);
|
|
36
|
+
return `${toolTag} ${name} ${size}\n ${path}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const cleanCommand = new Command('clean')
|
|
40
|
+
.description('Remove orphaned session data')
|
|
41
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
42
|
+
.option('-n, --dry-run', 'Show what would be deleted without deleting')
|
|
43
|
+
.option('-i, --interactive', 'Select sessions to delete interactively')
|
|
44
|
+
.option('--no-trash', 'Permanently delete instead of moving to trash')
|
|
45
|
+
.option('-v, --verbose', 'Enable verbose output')
|
|
46
|
+
.action(async (options: CleanOptions) => {
|
|
47
|
+
const spinner = ora('Scanning for orphaned sessions...').start();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Scan
|
|
51
|
+
const allScanners = createAllScanners();
|
|
52
|
+
const availableScanners = await getAvailableScanners(allScanners);
|
|
53
|
+
|
|
54
|
+
if (availableScanners.length === 0) {
|
|
55
|
+
spinner.warn('No AI coding tools detected on this system.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const results = await runAllScanners(availableScanners);
|
|
60
|
+
const allSessions = results.flatMap((r) => r.sessions);
|
|
61
|
+
const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
|
|
62
|
+
|
|
63
|
+
spinner.stop();
|
|
64
|
+
|
|
65
|
+
if (allSessions.length === 0) {
|
|
66
|
+
logger.success('No orphaned sessions found.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Separate session folders, config entries, and auto-cleanup targets
|
|
71
|
+
const folderSessions = allSessions.filter(
|
|
72
|
+
(s) => s.type === 'session' || s.type === undefined
|
|
73
|
+
);
|
|
74
|
+
const configEntries = allSessions.filter((s) => s.type === 'config');
|
|
75
|
+
const sessionEnvEntries = allSessions.filter(
|
|
76
|
+
(s) => s.type === 'session-env'
|
|
77
|
+
);
|
|
78
|
+
const todosEntries = allSessions.filter((s) => s.type === 'todos');
|
|
79
|
+
const fileHistoryEntries = allSessions.filter(
|
|
80
|
+
(s) => s.type === 'file-history'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Auto-cleanup targets (session-env, todos, file-history)
|
|
84
|
+
const autoCleanEntries = [
|
|
85
|
+
...sessionEnvEntries,
|
|
86
|
+
...todosEntries,
|
|
87
|
+
...fileHistoryEntries,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// Interactive selection targets (excluding auto-cleanup targets)
|
|
91
|
+
const selectableSessions = allSessions.filter(
|
|
92
|
+
(s) =>
|
|
93
|
+
s.type !== 'session-env' &&
|
|
94
|
+
s.type !== 'todos' &&
|
|
95
|
+
s.type !== 'file-history'
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Output summary
|
|
99
|
+
console.log();
|
|
100
|
+
const parts: string[] = [];
|
|
101
|
+
if (folderSessions.length > 0) {
|
|
102
|
+
parts.push(`${folderSessions.length} session folder(s)`);
|
|
103
|
+
}
|
|
104
|
+
if (configEntries.length > 0) {
|
|
105
|
+
parts.push(`${configEntries.length} config entry(ies)`);
|
|
106
|
+
}
|
|
107
|
+
if (sessionEnvEntries.length > 0) {
|
|
108
|
+
parts.push(`${sessionEnvEntries.length} session-env folder(s)`);
|
|
109
|
+
}
|
|
110
|
+
if (todosEntries.length > 0) {
|
|
111
|
+
parts.push(`${todosEntries.length} todos file(s)`);
|
|
112
|
+
}
|
|
113
|
+
if (fileHistoryEntries.length > 0) {
|
|
114
|
+
parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
|
|
115
|
+
}
|
|
116
|
+
logger.warn(`Found ${parts.join(' + ')} (${formatSize(totalSize)})`);
|
|
117
|
+
|
|
118
|
+
if (options.verbose && !options.interactive) {
|
|
119
|
+
console.log();
|
|
120
|
+
// Exclude auto-cleanup targets from detailed list
|
|
121
|
+
for (const session of selectableSessions) {
|
|
122
|
+
console.log(
|
|
123
|
+
chalk.dim(` ${session.toolName}: ${session.projectPath}`)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Interactive mode: session selection (excluding auto-cleanup targets)
|
|
129
|
+
let sessionsToClean = allSessions;
|
|
130
|
+
if (options.interactive) {
|
|
131
|
+
if (selectableSessions.length === 0) {
|
|
132
|
+
// Only auto-cleanup targets exist
|
|
133
|
+
if (autoCleanEntries.length > 0) {
|
|
134
|
+
sessionsToClean = autoCleanEntries;
|
|
135
|
+
logger.info(
|
|
136
|
+
`Only ${autoCleanEntries.length} auto-cleanup target(s) found`
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
logger.info('No selectable sessions found.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
console.log();
|
|
144
|
+
const { selected } = await inquirer.prompt<{
|
|
145
|
+
selected: OrphanedSession[];
|
|
146
|
+
}>([
|
|
147
|
+
{
|
|
148
|
+
type: 'checkbox',
|
|
149
|
+
name: 'selected',
|
|
150
|
+
message: 'Select sessions to delete:',
|
|
151
|
+
choices: selectableSessions.map((session) => ({
|
|
152
|
+
name: formatSessionChoice(session),
|
|
153
|
+
value: session,
|
|
154
|
+
checked: false,
|
|
155
|
+
})),
|
|
156
|
+
pageSize: 15,
|
|
157
|
+
loop: false,
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
if (selected.length === 0) {
|
|
162
|
+
logger.info('No sessions selected. Cancelled.');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Include selected sessions + auto-cleanup targets
|
|
167
|
+
sessionsToClean = [...selected, ...autoCleanEntries];
|
|
168
|
+
const selectedSize = selected.reduce((sum, s) => sum + s.size, 0);
|
|
169
|
+
console.log();
|
|
170
|
+
if (selected.length > 0) {
|
|
171
|
+
logger.info(
|
|
172
|
+
`Selected: ${selected.length} session(s) (${formatSize(selectedSize)})`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (autoCleanEntries.length > 0) {
|
|
176
|
+
const autoSize = autoCleanEntries.reduce((sum, s) => sum + s.size, 0);
|
|
177
|
+
logger.info(
|
|
178
|
+
`+ ${autoCleanEntries.length} auto-cleanup target(s) (${formatSize(autoSize)})`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
|
|
185
|
+
|
|
186
|
+
// Dry-run mode
|
|
187
|
+
if (options.dryRun) {
|
|
188
|
+
console.log();
|
|
189
|
+
logger.info(
|
|
190
|
+
chalk.yellow('Dry-run mode: No files will be deleted.')
|
|
191
|
+
);
|
|
192
|
+
console.log();
|
|
193
|
+
|
|
194
|
+
const dryRunFolders = sessionsToClean.filter(
|
|
195
|
+
(s) => s.type === 'session' || s.type === undefined
|
|
196
|
+
);
|
|
197
|
+
const dryRunConfigs = sessionsToClean.filter((s) => s.type === 'config');
|
|
198
|
+
const dryRunEnvs = sessionsToClean.filter(
|
|
199
|
+
(s) => s.type === 'session-env'
|
|
200
|
+
);
|
|
201
|
+
const dryRunTodos = sessionsToClean.filter((s) => s.type === 'todos');
|
|
202
|
+
const dryRunHistories = sessionsToClean.filter(
|
|
203
|
+
(s) => s.type === 'file-history'
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
for (const session of dryRunFolders) {
|
|
207
|
+
console.log(
|
|
208
|
+
` ${chalk.red('Would delete:')} ${session.sessionPath} (${formatSize(session.size)})`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (dryRunConfigs.length > 0) {
|
|
213
|
+
console.log();
|
|
214
|
+
console.log(
|
|
215
|
+
` ${chalk.yellow('Would remove from ~/.claude.json:')}`
|
|
216
|
+
);
|
|
217
|
+
for (const config of dryRunConfigs) {
|
|
218
|
+
console.log(` - ${config.projectPath}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Auto-cleanup targets summary
|
|
223
|
+
const autoCleanParts: string[] = [];
|
|
224
|
+
if (dryRunEnvs.length > 0) {
|
|
225
|
+
autoCleanParts.push(`${dryRunEnvs.length} session-env`);
|
|
226
|
+
}
|
|
227
|
+
if (dryRunTodos.length > 0) {
|
|
228
|
+
autoCleanParts.push(`${dryRunTodos.length} todos`);
|
|
229
|
+
}
|
|
230
|
+
if (dryRunHistories.length > 0) {
|
|
231
|
+
autoCleanParts.push(`${dryRunHistories.length} file-history`);
|
|
232
|
+
}
|
|
233
|
+
if (autoCleanParts.length > 0) {
|
|
234
|
+
const autoSize =
|
|
235
|
+
dryRunEnvs.reduce((sum, s) => sum + s.size, 0) +
|
|
236
|
+
dryRunTodos.reduce((sum, s) => sum + s.size, 0) +
|
|
237
|
+
dryRunHistories.reduce((sum, s) => sum + s.size, 0);
|
|
238
|
+
console.log();
|
|
239
|
+
console.log(
|
|
240
|
+
` ${chalk.dim(`Would auto-delete: ${autoCleanParts.join(' + ')} (${formatSize(autoSize)})`)}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Confirmation prompt (also in interactive mode)
|
|
247
|
+
if (!options.force) {
|
|
248
|
+
console.log();
|
|
249
|
+
const action = options.noTrash ? 'permanently delete' : 'move to trash';
|
|
250
|
+
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
|
|
251
|
+
{
|
|
252
|
+
type: 'confirm',
|
|
253
|
+
name: 'confirmed',
|
|
254
|
+
message: `${action} ${sessionsToClean.length} session(s) (${formatSize(cleanSize)})?`,
|
|
255
|
+
default: false,
|
|
256
|
+
},
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
if (!confirmed) {
|
|
260
|
+
logger.info('Cancelled.');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Execute cleanup
|
|
266
|
+
const cleanSpinner = ora('Cleaning orphaned sessions...').start();
|
|
267
|
+
|
|
268
|
+
const cleaner = new Cleaner();
|
|
269
|
+
const cleanResult = await cleaner.clean(sessionsToClean, {
|
|
270
|
+
dryRun: false,
|
|
271
|
+
useTrash: !options.noTrash,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
cleanSpinner.stop();
|
|
275
|
+
|
|
276
|
+
// Output results
|
|
277
|
+
console.log();
|
|
278
|
+
if (cleanResult.deletedCount > 0) {
|
|
279
|
+
const action = options.noTrash ? 'Deleted' : 'Moved to trash';
|
|
280
|
+
const parts: string[] = [];
|
|
281
|
+
const { deletedByType } = cleanResult;
|
|
282
|
+
|
|
283
|
+
if (deletedByType.session > 0) {
|
|
284
|
+
parts.push(`${deletedByType.session} session`);
|
|
285
|
+
}
|
|
286
|
+
if (deletedByType.sessionEnv > 0) {
|
|
287
|
+
parts.push(`${deletedByType.sessionEnv} session-env`);
|
|
288
|
+
}
|
|
289
|
+
if (deletedByType.todos > 0) {
|
|
290
|
+
parts.push(`${deletedByType.todos} todos`);
|
|
291
|
+
}
|
|
292
|
+
if (deletedByType.fileHistory > 0) {
|
|
293
|
+
parts.push(`${deletedByType.fileHistory} file-history`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const summary =
|
|
297
|
+
parts.length > 0
|
|
298
|
+
? parts.join(' + ')
|
|
299
|
+
: `${cleanResult.deletedCount} item(s)`;
|
|
300
|
+
logger.success(
|
|
301
|
+
`${action}: ${summary} (${formatSize(cleanResult.totalSizeDeleted)})`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (cleanResult.alreadyGoneCount > 0 && options.verbose) {
|
|
306
|
+
logger.info(
|
|
307
|
+
`Skipped ${cleanResult.alreadyGoneCount} already-deleted item(s)`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (cleanResult.configEntriesRemoved > 0) {
|
|
312
|
+
logger.success(
|
|
313
|
+
`Removed ${cleanResult.configEntriesRemoved} config entry(ies) from ~/.claude.json`
|
|
314
|
+
);
|
|
315
|
+
if (cleanResult.backupPath) {
|
|
316
|
+
logger.info(`Backup saved to: ${tildify(cleanResult.backupPath)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (cleanResult.errors.length > 0) {
|
|
321
|
+
logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
|
|
322
|
+
if (options.verbose) {
|
|
323
|
+
for (const err of cleanResult.errors) {
|
|
324
|
+
console.log(chalk.red(` ${err.sessionPath}: ${err.error.message}`));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
spinner.fail('Clean failed');
|
|
330
|
+
logger.error(
|
|
331
|
+
error instanceof Error ? error.message : 'Unknown error occurred'
|
|
332
|
+
);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import {
|
|
6
|
+
loadConfig,
|
|
7
|
+
addWatchPath,
|
|
8
|
+
removeWatchPath,
|
|
9
|
+
getWatchPaths,
|
|
10
|
+
getWatchDelay,
|
|
11
|
+
setWatchDelay,
|
|
12
|
+
getWatchDepth,
|
|
13
|
+
setWatchDepth,
|
|
14
|
+
resetConfig,
|
|
15
|
+
} from '../utils/config.js';
|
|
16
|
+
|
|
17
|
+
export const configCommand = new Command('config').description(
|
|
18
|
+
'Manage configuration'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const pathsCommand = new Command('paths').description('Manage watch paths');
|
|
22
|
+
|
|
23
|
+
pathsCommand
|
|
24
|
+
.command('add <path>')
|
|
25
|
+
.description('Add a watch path')
|
|
26
|
+
.action((path: string) => {
|
|
27
|
+
addWatchPath(path);
|
|
28
|
+
logger.success(`Added: ${path}`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
pathsCommand
|
|
32
|
+
.command('remove <path>')
|
|
33
|
+
.description('Remove a watch path')
|
|
34
|
+
.action((path: string) => {
|
|
35
|
+
const removed = removeWatchPath(path);
|
|
36
|
+
if (removed) {
|
|
37
|
+
logger.success(`Removed: ${path}`);
|
|
38
|
+
} else {
|
|
39
|
+
logger.warn(`Path not found: ${path}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
pathsCommand
|
|
44
|
+
.command('list')
|
|
45
|
+
.description('List watch paths')
|
|
46
|
+
.action(() => {
|
|
47
|
+
const paths = getWatchPaths();
|
|
48
|
+
if (!paths || paths.length === 0) {
|
|
49
|
+
logger.info('No watch paths configured.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
for (const p of paths) {
|
|
54
|
+
console.log(` ${p}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
configCommand.addCommand(pathsCommand);
|
|
59
|
+
|
|
60
|
+
const DEFAULT_DELAY_MINUTES = 5;
|
|
61
|
+
const MAX_DELAY_MINUTES = 10;
|
|
62
|
+
|
|
63
|
+
configCommand
|
|
64
|
+
.command('delay [minutes]')
|
|
65
|
+
.description(`Get or set watch delay in minutes (default: ${DEFAULT_DELAY_MINUTES}, max: ${MAX_DELAY_MINUTES})`)
|
|
66
|
+
.action((minutes?: string) => {
|
|
67
|
+
if (minutes === undefined) {
|
|
68
|
+
// Get current delay
|
|
69
|
+
const delay = getWatchDelay() ?? DEFAULT_DELAY_MINUTES;
|
|
70
|
+
console.log(`Watch delay: ${String(delay)} minute(s)`);
|
|
71
|
+
} else {
|
|
72
|
+
// Set delay
|
|
73
|
+
const value = parseInt(minutes, 10);
|
|
74
|
+
if (isNaN(value) || value < 1) {
|
|
75
|
+
logger.error('Invalid delay value. Must be a positive number.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (value > MAX_DELAY_MINUTES) {
|
|
79
|
+
logger.warn(`Maximum delay is ${String(MAX_DELAY_MINUTES)} minutes. Setting to ${String(MAX_DELAY_MINUTES)}.`);
|
|
80
|
+
setWatchDelay(MAX_DELAY_MINUTES);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
setWatchDelay(value);
|
|
84
|
+
logger.success(`Watch delay set to ${String(value)} minute(s)`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const DEFAULT_DEPTH = 3;
|
|
89
|
+
const MAX_DEPTH = 5;
|
|
90
|
+
|
|
91
|
+
configCommand
|
|
92
|
+
.command('depth [level]')
|
|
93
|
+
.description('Get or set watch depth (default: 3, max: 5)')
|
|
94
|
+
.action((level?: string) => {
|
|
95
|
+
if (level === undefined) {
|
|
96
|
+
const depth = getWatchDepth() ?? DEFAULT_DEPTH;
|
|
97
|
+
console.log(`Watch depth: ${String(depth)}`);
|
|
98
|
+
} else {
|
|
99
|
+
const value = parseInt(level, 10);
|
|
100
|
+
if (isNaN(value) || value < 1) {
|
|
101
|
+
logger.error('Invalid depth value. Must be a positive number.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (value > MAX_DEPTH) {
|
|
105
|
+
logger.warn(`Maximum depth is ${String(MAX_DEPTH)}. Setting to ${String(MAX_DEPTH)}.`);
|
|
106
|
+
}
|
|
107
|
+
setWatchDepth(value);
|
|
108
|
+
const actualValue = Math.min(value, MAX_DEPTH);
|
|
109
|
+
logger.success(`Watch depth set to ${String(actualValue)}`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
configCommand
|
|
114
|
+
.command('show')
|
|
115
|
+
.description('Show all configuration')
|
|
116
|
+
.action(() => {
|
|
117
|
+
const config = loadConfig();
|
|
118
|
+
console.log(JSON.stringify(config, null, 2));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
configCommand
|
|
122
|
+
.command('reset')
|
|
123
|
+
.description('Reset configuration to defaults')
|
|
124
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
125
|
+
.action(async (options: { force?: boolean }) => {
|
|
126
|
+
if (!options.force) {
|
|
127
|
+
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
|
|
128
|
+
{
|
|
129
|
+
type: 'confirm',
|
|
130
|
+
name: 'confirmed',
|
|
131
|
+
message: 'Reset all configuration to defaults?',
|
|
132
|
+
default: false,
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
if (!confirmed) {
|
|
137
|
+
logger.info('Cancelled.');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
resetConfig();
|
|
143
|
+
logger.success('Configuration reset to defaults.');
|
|
144
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { formatSize } from '../utils/size.js';
|
|
7
|
+
import {
|
|
8
|
+
createAllScanners,
|
|
9
|
+
getAvailableScanners,
|
|
10
|
+
} from '../scanners/index.js';
|
|
11
|
+
import {
|
|
12
|
+
getClaudeProjectsDir,
|
|
13
|
+
getCursorWorkspaceDir,
|
|
14
|
+
} from '../utils/paths.js';
|
|
15
|
+
|
|
16
|
+
interface ListOptions {
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const listCommand = new Command('list')
|
|
21
|
+
.description('List detected AI coding tools and their data locations')
|
|
22
|
+
.option('-v, --verbose', 'Show detailed information')
|
|
23
|
+
.action(async (options: ListOptions) => {
|
|
24
|
+
const allScanners = createAllScanners();
|
|
25
|
+
const availableScanners = await getAvailableScanners(allScanners);
|
|
26
|
+
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(chalk.bold('AI Coding Tools Status:'));
|
|
29
|
+
console.log();
|
|
30
|
+
|
|
31
|
+
const table = new Table({
|
|
32
|
+
head: [
|
|
33
|
+
chalk.cyan('Tool'),
|
|
34
|
+
chalk.cyan('Status'),
|
|
35
|
+
chalk.cyan('Data Location'),
|
|
36
|
+
],
|
|
37
|
+
style: { head: [] },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const toolLocations: Record<string, string> = {
|
|
41
|
+
'claude-code': getClaudeProjectsDir(),
|
|
42
|
+
cursor: getCursorWorkspaceDir(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const scanner of allScanners) {
|
|
46
|
+
const isAvailable = availableScanners.some((s) => s.name === scanner.name);
|
|
47
|
+
const status = isAvailable
|
|
48
|
+
? chalk.green('Available')
|
|
49
|
+
: chalk.dim('Not found');
|
|
50
|
+
const location = toolLocations[scanner.name] || 'Unknown';
|
|
51
|
+
|
|
52
|
+
table.push([scanner.name, status, isAvailable ? location : chalk.dim(location)]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(table.toString());
|
|
56
|
+
|
|
57
|
+
// Additional info (verbose)
|
|
58
|
+
if (options.verbose && availableScanners.length > 0) {
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(chalk.bold('Tool Details:'));
|
|
61
|
+
console.log();
|
|
62
|
+
|
|
63
|
+
for (const scanner of availableScanners) {
|
|
64
|
+
console.log(chalk.cyan(`${scanner.name}:`));
|
|
65
|
+
|
|
66
|
+
switch (scanner.name) {
|
|
67
|
+
case 'claude-code':
|
|
68
|
+
console.log(' Session format: Encoded project path directories');
|
|
69
|
+
console.log(' Path encoding: /path/to/project → -path-to-project');
|
|
70
|
+
break;
|
|
71
|
+
case 'cursor':
|
|
72
|
+
console.log(' Session format: Hash-named directories with workspace.json');
|
|
73
|
+
console.log(' Project info: Stored in workspace.json "folder" field');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Summary
|
|
81
|
+
console.log(
|
|
82
|
+
chalk.dim(
|
|
83
|
+
`${availableScanners.length} of ${allScanners.length} tools detected on this system.`
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
});
|