@sooink/ai-session-tidy 0.1.2 → 0.1.4
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/.github/workflows/release.yml +57 -0
- package/CHANGELOG.md +32 -0
- package/README.md +50 -11
- package/assets/demo-interactive.gif +0 -0
- package/assets/demo.gif +0 -0
- package/dist/index.js +369 -116
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/clean.ts +187 -78
- package/src/commands/config.ts +7 -2
- package/src/commands/scan.ts +74 -2
- package/src/commands/watch.ts +12 -7
- package/src/core/cleaner.ts +5 -0
- package/src/core/constants.ts +10 -10
- package/src/core/service.ts +64 -10
- package/src/scanners/claude-code.ts +55 -0
- package/src/scanners/types.ts +1 -1
- package/src/utils/paths.ts +43 -3
- package/README.ko.md +0 -176
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sooink/ai-session-tidy",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI tool that detects and cleans orphaned session data from AI coding tools",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "CLI tool that automatically detects and cleans orphaned session data from AI coding tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ai-session-tidy": "./dist/index.js"
|
|
@@ -51,4 +51,4 @@
|
|
|
51
51
|
"typescript-eslint": "^8.21.0",
|
|
52
52
|
"vitest": "^3.0.4"
|
|
53
53
|
}
|
|
54
|
-
}
|
|
54
|
+
}
|
package/src/commands/clean.ts
CHANGED
|
@@ -31,9 +31,35 @@ function formatSessionChoice(session: OrphanedSession): string {
|
|
|
31
31
|
? chalk.yellow('[config]')
|
|
32
32
|
: chalk.cyan(`[${session.toolName}]`);
|
|
33
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}
|
|
34
|
+
const size = isConfig ? '' : ` ${chalk.dim(`(${formatSize(session.size)})`)}`;
|
|
35
|
+
const path = chalk.dim(`→ ${tildify(session.projectPath)}`);
|
|
36
|
+
return `${toolTag} ${name}${size}\n ${path}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface GroupChoice {
|
|
40
|
+
type: 'session-env' | 'todos' | 'file-history' | 'tasks';
|
|
41
|
+
sessions: OrphanedSession[];
|
|
42
|
+
totalSize: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatGroupChoice(group: GroupChoice): string {
|
|
46
|
+
const labels: Record<string, string> = {
|
|
47
|
+
'session-env': 'empty session-env folder',
|
|
48
|
+
'todos': 'orphaned todos file',
|
|
49
|
+
'file-history': 'orphaned file-history folder',
|
|
50
|
+
'tasks': 'orphaned tasks folder',
|
|
51
|
+
};
|
|
52
|
+
const colors: Record<string, (s: string) => string> = {
|
|
53
|
+
'session-env': chalk.green,
|
|
54
|
+
'todos': chalk.magenta,
|
|
55
|
+
'file-history': chalk.blue,
|
|
56
|
+
'tasks': chalk.cyan,
|
|
57
|
+
};
|
|
58
|
+
const label = labels[group.type] || group.type;
|
|
59
|
+
const count = group.sessions.length;
|
|
60
|
+
const plural = count > 1 ? 's' : '';
|
|
61
|
+
const color = colors[group.type] || chalk.white;
|
|
62
|
+
return `${color(`[${group.type}]`)} ${chalk.white(`${count} ${label}${plural}`)} ${chalk.dim(`(${formatSize(group.totalSize)})`)}`;
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
export const cleanCommand = new Command('clean')
|
|
@@ -67,7 +93,7 @@ export const cleanCommand = new Command('clean')
|
|
|
67
93
|
return;
|
|
68
94
|
}
|
|
69
95
|
|
|
70
|
-
// Separate
|
|
96
|
+
// Separate by type
|
|
71
97
|
const folderSessions = allSessions.filter(
|
|
72
98
|
(s) => s.type === 'session' || s.type === undefined
|
|
73
99
|
);
|
|
@@ -79,22 +105,48 @@ export const cleanCommand = new Command('clean')
|
|
|
79
105
|
const fileHistoryEntries = allSessions.filter(
|
|
80
106
|
(s) => s.type === 'file-history'
|
|
81
107
|
);
|
|
108
|
+
const tasksEntries = allSessions.filter((s) => s.type === 'tasks');
|
|
82
109
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
...sessionEnvEntries,
|
|
86
|
-
...todosEntries,
|
|
87
|
-
...fileHistoryEntries,
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
// Interactive selection targets (excluding auto-cleanup targets)
|
|
91
|
-
const selectableSessions = allSessions.filter(
|
|
110
|
+
// Individual selection targets (session folders and config entries)
|
|
111
|
+
const individualSessions = allSessions.filter(
|
|
92
112
|
(s) =>
|
|
93
113
|
s.type !== 'session-env' &&
|
|
94
114
|
s.type !== 'todos' &&
|
|
95
|
-
s.type !== 'file-history'
|
|
115
|
+
s.type !== 'file-history' &&
|
|
116
|
+
s.type !== 'tasks'
|
|
96
117
|
);
|
|
97
118
|
|
|
119
|
+
// Group selection targets (session-env, todos, file-history, tasks)
|
|
120
|
+
const groupChoices: GroupChoice[] = [];
|
|
121
|
+
if (sessionEnvEntries.length > 0) {
|
|
122
|
+
groupChoices.push({
|
|
123
|
+
type: 'session-env',
|
|
124
|
+
sessions: sessionEnvEntries,
|
|
125
|
+
totalSize: sessionEnvEntries.reduce((sum, s) => sum + s.size, 0),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (todosEntries.length > 0) {
|
|
129
|
+
groupChoices.push({
|
|
130
|
+
type: 'todos',
|
|
131
|
+
sessions: todosEntries,
|
|
132
|
+
totalSize: todosEntries.reduce((sum, s) => sum + s.size, 0),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (fileHistoryEntries.length > 0) {
|
|
136
|
+
groupChoices.push({
|
|
137
|
+
type: 'file-history',
|
|
138
|
+
sessions: fileHistoryEntries,
|
|
139
|
+
totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (tasksEntries.length > 0) {
|
|
143
|
+
groupChoices.push({
|
|
144
|
+
type: 'tasks',
|
|
145
|
+
sessions: tasksEntries,
|
|
146
|
+
totalSize: tasksEntries.reduce((sum, s) => sum + s.size, 0),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
98
150
|
// Output summary
|
|
99
151
|
console.log();
|
|
100
152
|
const parts: string[] = [];
|
|
@@ -113,72 +165,115 @@ export const cleanCommand = new Command('clean')
|
|
|
113
165
|
if (fileHistoryEntries.length > 0) {
|
|
114
166
|
parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
|
|
115
167
|
}
|
|
168
|
+
if (tasksEntries.length > 0) {
|
|
169
|
+
parts.push(`${tasksEntries.length} tasks folder(s)`);
|
|
170
|
+
}
|
|
116
171
|
logger.warn(`Found ${parts.join(' + ')} (${formatSize(totalSize)})`);
|
|
117
172
|
|
|
118
173
|
if (options.verbose && !options.interactive) {
|
|
119
174
|
console.log();
|
|
120
|
-
|
|
121
|
-
for (const session of selectableSessions) {
|
|
175
|
+
for (const session of individualSessions) {
|
|
122
176
|
console.log(
|
|
123
|
-
chalk.dim(` ${session.toolName}: ${session.projectPath}`)
|
|
177
|
+
chalk.dim(` ${session.toolName}: ${tildify(session.projectPath)}`)
|
|
124
178
|
);
|
|
125
179
|
}
|
|
126
180
|
}
|
|
127
181
|
|
|
128
|
-
// Interactive mode: session selection
|
|
182
|
+
// Interactive mode: session selection
|
|
129
183
|
let sessionsToClean = allSessions;
|
|
130
184
|
if (options.interactive) {
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
if (!process.stdout.isTTY) {
|
|
186
|
+
logger.error('Interactive mode requires a TTY. Omit -i or use -f to skip confirmation.');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Build choices: individual sessions + group choices
|
|
190
|
+
const choices: Array<{
|
|
191
|
+
name: string;
|
|
192
|
+
value: { type: 'individual'; session: OrphanedSession } | { type: 'group'; group: GroupChoice };
|
|
193
|
+
checked: boolean;
|
|
194
|
+
}> = [];
|
|
195
|
+
|
|
196
|
+
// Add individual sessions
|
|
197
|
+
for (const session of individualSessions) {
|
|
198
|
+
choices.push({
|
|
199
|
+
name: formatSessionChoice(session),
|
|
200
|
+
value: { type: 'individual', session },
|
|
201
|
+
checked: false,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add group choices
|
|
206
|
+
for (const group of groupChoices) {
|
|
207
|
+
choices.push({
|
|
208
|
+
name: formatGroupChoice(group),
|
|
209
|
+
value: { type: 'group', group },
|
|
210
|
+
checked: false,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (choices.length === 0) {
|
|
215
|
+
logger.info('No selectable sessions found.');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log();
|
|
220
|
+
const { selected } = await inquirer.prompt<{
|
|
221
|
+
selected: Array<{ type: 'individual'; session: OrphanedSession } | { type: 'group'; group: GroupChoice }>;
|
|
222
|
+
}>([
|
|
223
|
+
{
|
|
224
|
+
type: 'checkbox',
|
|
225
|
+
name: 'selected',
|
|
226
|
+
message: 'Select items to delete:',
|
|
227
|
+
choices,
|
|
228
|
+
pageSize: 15,
|
|
229
|
+
loop: false,
|
|
230
|
+
},
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
if (selected.length === 0) {
|
|
234
|
+
logger.info('No items selected. Cancelled.');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Clear inquirer's multi-line output (approximately N+1 lines for N selected items)
|
|
239
|
+
if (process.stdout.isTTY) {
|
|
240
|
+
const linesToClear = selected.length + 1;
|
|
241
|
+
for (let i = 0; i < linesToClear; i++) {
|
|
242
|
+
process.stdout.write('\x1B[1A\x1B[2K');
|
|
164
243
|
}
|
|
244
|
+
}
|
|
165
245
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
246
|
+
// Display selected items in a clean list
|
|
247
|
+
console.log(chalk.green('✔') + ' ' + chalk.bold('Selected items:'));
|
|
248
|
+
for (const item of selected) {
|
|
249
|
+
if (item.type === 'individual') {
|
|
250
|
+
const s = item.session;
|
|
251
|
+
const tag = s.type === 'config'
|
|
252
|
+
? chalk.yellow('[config]')
|
|
253
|
+
: chalk.cyan(`[${s.toolName}]`);
|
|
254
|
+
const size = s.type === 'config' ? '' : ` ${chalk.dim(`(${formatSize(s.size)})`)}`;
|
|
255
|
+
console.log(` ${tag} ${basename(s.projectPath)}${size}`);
|
|
256
|
+
console.log(` ${chalk.dim(`→ ${tildify(s.projectPath)}`)}`);
|
|
257
|
+
} else {
|
|
258
|
+
console.log(` ${formatGroupChoice(item.group)}`);
|
|
174
259
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Flatten selected items
|
|
263
|
+
sessionsToClean = [];
|
|
264
|
+
for (const item of selected) {
|
|
265
|
+
if (item.type === 'individual') {
|
|
266
|
+
sessionsToClean.push(item.session);
|
|
267
|
+
} else {
|
|
268
|
+
sessionsToClean.push(...item.group.sessions);
|
|
180
269
|
}
|
|
181
270
|
}
|
|
271
|
+
|
|
272
|
+
const selectedSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
|
|
273
|
+
console.log();
|
|
274
|
+
logger.info(
|
|
275
|
+
`Selected: ${sessionsToClean.length} item(s) (${formatSize(selectedSize)})`
|
|
276
|
+
);
|
|
182
277
|
}
|
|
183
278
|
|
|
184
279
|
const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
|
|
@@ -202,10 +297,13 @@ export const cleanCommand = new Command('clean')
|
|
|
202
297
|
const dryRunHistories = sessionsToClean.filter(
|
|
203
298
|
(s) => s.type === 'file-history'
|
|
204
299
|
);
|
|
300
|
+
const dryRunTasks = sessionsToClean.filter(
|
|
301
|
+
(s) => s.type === 'tasks'
|
|
302
|
+
);
|
|
205
303
|
|
|
206
304
|
for (const session of dryRunFolders) {
|
|
207
305
|
console.log(
|
|
208
|
-
` ${chalk.red('Would delete:')} ${session.sessionPath} (${formatSize(session.size)})`
|
|
306
|
+
` ${chalk.red('Would delete:')} ${tildify(session.sessionPath)} (${formatSize(session.size)})`
|
|
209
307
|
);
|
|
210
308
|
}
|
|
211
309
|
|
|
@@ -215,29 +313,33 @@ export const cleanCommand = new Command('clean')
|
|
|
215
313
|
` ${chalk.yellow('Would remove from ~/.claude.json:')}`
|
|
216
314
|
);
|
|
217
315
|
for (const config of dryRunConfigs) {
|
|
218
|
-
console.log(` - ${config.projectPath}`);
|
|
316
|
+
console.log(` - ${tildify(config.projectPath)}`);
|
|
219
317
|
}
|
|
220
318
|
}
|
|
221
319
|
|
|
222
|
-
//
|
|
223
|
-
const autoCleanParts: string[] = [];
|
|
320
|
+
// Group items summary
|
|
224
321
|
if (dryRunEnvs.length > 0) {
|
|
225
|
-
|
|
322
|
+
console.log();
|
|
323
|
+
console.log(
|
|
324
|
+
` ${chalk.green('Would delete:')} ${dryRunEnvs.length} session-env folder(s)`
|
|
325
|
+
);
|
|
226
326
|
}
|
|
227
327
|
if (dryRunTodos.length > 0) {
|
|
228
|
-
|
|
328
|
+
console.log();
|
|
329
|
+
console.log(
|
|
330
|
+
` ${chalk.magenta('Would delete:')} ${dryRunTodos.length} todos file(s) (${formatSize(dryRunTodos.reduce((sum, s) => sum + s.size, 0))})`
|
|
331
|
+
);
|
|
229
332
|
}
|
|
230
333
|
if (dryRunHistories.length > 0) {
|
|
231
|
-
|
|
334
|
+
console.log();
|
|
335
|
+
console.log(
|
|
336
|
+
` ${chalk.blue('Would delete:')} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
|
|
337
|
+
);
|
|
232
338
|
}
|
|
233
|
-
if (
|
|
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);
|
|
339
|
+
if (dryRunTasks.length > 0) {
|
|
238
340
|
console.log();
|
|
239
341
|
console.log(
|
|
240
|
-
` ${chalk.
|
|
342
|
+
` ${chalk.cyan('Would delete:')} ${dryRunTasks.length} tasks folder(s) (${formatSize(dryRunTasks.reduce((sum, s) => sum + s.size, 0))})`
|
|
241
343
|
);
|
|
242
344
|
}
|
|
243
345
|
return;
|
|
@@ -245,6 +347,10 @@ export const cleanCommand = new Command('clean')
|
|
|
245
347
|
|
|
246
348
|
// Confirmation prompt (also in interactive mode)
|
|
247
349
|
if (!options.force) {
|
|
350
|
+
if (!process.stdout.isTTY) {
|
|
351
|
+
logger.error('Confirmation requires a TTY. Use -f to skip confirmation.');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
248
354
|
console.log();
|
|
249
355
|
const action = options.noTrash ? 'permanently delete' : 'move to trash';
|
|
250
356
|
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
|
|
@@ -292,6 +398,9 @@ export const cleanCommand = new Command('clean')
|
|
|
292
398
|
if (deletedByType.fileHistory > 0) {
|
|
293
399
|
parts.push(`${deletedByType.fileHistory} file-history`);
|
|
294
400
|
}
|
|
401
|
+
if (deletedByType.tasks > 0) {
|
|
402
|
+
parts.push(`${deletedByType.tasks} tasks`);
|
|
403
|
+
}
|
|
295
404
|
|
|
296
405
|
const summary =
|
|
297
406
|
parts.length > 0
|
|
@@ -321,7 +430,7 @@ export const cleanCommand = new Command('clean')
|
|
|
321
430
|
logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
|
|
322
431
|
if (options.verbose) {
|
|
323
432
|
for (const err of cleanResult.errors) {
|
|
324
|
-
console.log(chalk.red(` ${err.sessionPath}: ${err.error.message}`));
|
|
433
|
+
console.log(chalk.red(` ${tildify(err.sessionPath)}: ${err.error.message}`));
|
|
325
434
|
}
|
|
326
435
|
}
|
|
327
436
|
}
|
package/src/commands/config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from 'os';
|
|
|
4
4
|
import { resolve } from 'path';
|
|
5
5
|
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { tildify } from '../utils/paths.js';
|
|
7
8
|
import {
|
|
8
9
|
loadConfig,
|
|
9
10
|
addWatchPath,
|
|
@@ -67,7 +68,7 @@ pathCommand
|
|
|
67
68
|
}
|
|
68
69
|
console.log();
|
|
69
70
|
for (const p of paths) {
|
|
70
|
-
console.log(` ${p}`);
|
|
71
|
+
console.log(` ${tildify(p)}`);
|
|
71
72
|
}
|
|
72
73
|
});
|
|
73
74
|
|
|
@@ -106,7 +107,7 @@ ignoreCommand
|
|
|
106
107
|
}
|
|
107
108
|
console.log();
|
|
108
109
|
for (const p of paths) {
|
|
109
|
-
console.log(` ${p}`);
|
|
110
|
+
console.log(` ${tildify(p)}`);
|
|
110
111
|
}
|
|
111
112
|
});
|
|
112
113
|
|
|
@@ -179,6 +180,10 @@ configCommand
|
|
|
179
180
|
.option('-f, --force', 'Skip confirmation prompt')
|
|
180
181
|
.action(async (options: { force?: boolean }) => {
|
|
181
182
|
if (!options.force) {
|
|
183
|
+
if (!process.stdout.isTTY) {
|
|
184
|
+
logger.error('Confirmation requires a TTY. Use -f to skip confirmation.');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
182
187
|
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
|
|
183
188
|
{
|
|
184
189
|
type: 'confirm',
|
package/src/commands/scan.ts
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
import { formatSize } from '../utils/size.js';
|
|
8
|
+
import { tildify } from '../utils/paths.js';
|
|
8
9
|
import {
|
|
9
10
|
createAllScanners,
|
|
10
11
|
getAvailableScanners,
|
|
@@ -84,6 +85,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
84
85
|
const sessionEnvEntries = allSessions.filter((s) => s.type === 'session-env');
|
|
85
86
|
const todosEntries = allSessions.filter((s) => s.type === 'todos');
|
|
86
87
|
const fileHistoryEntries = allSessions.filter((s) => s.type === 'file-history');
|
|
88
|
+
const tasksEntries = allSessions.filter((s) => s.type === 'tasks');
|
|
87
89
|
const totalSize = results.reduce((sum, r) => sum + r.totalSize, 0);
|
|
88
90
|
|
|
89
91
|
if (allSessions.length === 0) {
|
|
@@ -110,6 +112,9 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
110
112
|
if (fileHistoryEntries.length > 0) {
|
|
111
113
|
parts.push(`${fileHistoryEntries.length} file-history folder(s)`);
|
|
112
114
|
}
|
|
115
|
+
if (tasksEntries.length > 0) {
|
|
116
|
+
parts.push(`${tasksEntries.length} tasks folder(s)`);
|
|
117
|
+
}
|
|
113
118
|
logger.warn(`Found ${parts.join(' + ')} (${formatSize(totalSize)})`);
|
|
114
119
|
console.log();
|
|
115
120
|
|
|
@@ -122,6 +127,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
122
127
|
chalk.cyan('Env'),
|
|
123
128
|
chalk.cyan('Todos'),
|
|
124
129
|
chalk.cyan('History'),
|
|
130
|
+
chalk.cyan('Tasks'),
|
|
125
131
|
chalk.cyan('Size'),
|
|
126
132
|
chalk.cyan('Scan Time'),
|
|
127
133
|
],
|
|
@@ -135,6 +141,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
135
141
|
const envs = result.sessions.filter((s) => s.type === 'session-env').length;
|
|
136
142
|
const todos = result.sessions.filter((s) => s.type === 'todos').length;
|
|
137
143
|
const histories = result.sessions.filter((s) => s.type === 'file-history').length;
|
|
144
|
+
const tasks = result.sessions.filter((s) => s.type === 'tasks').length;
|
|
138
145
|
summaryTable.push([
|
|
139
146
|
result.toolName,
|
|
140
147
|
folders > 0 ? String(folders) : '-',
|
|
@@ -142,6 +149,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
142
149
|
envs > 0 ? String(envs) : '-',
|
|
143
150
|
todos > 0 ? String(todos) : '-',
|
|
144
151
|
histories > 0 ? String(histories) : '-',
|
|
152
|
+
tasks > 0 ? String(tasks) : '-',
|
|
145
153
|
formatSize(result.totalSize),
|
|
146
154
|
`${result.scanDuration.toFixed(0)}ms`,
|
|
147
155
|
]);
|
|
@@ -163,7 +171,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
163
171
|
console.log(
|
|
164
172
|
` ${chalk.cyan(`[${session.toolName}]`)} ${chalk.white(projectName)} ${chalk.dim(`(${formatSize(session.size)})`)}`
|
|
165
173
|
);
|
|
166
|
-
console.log(` ${chalk.dim('→')} ${session.projectPath}`);
|
|
174
|
+
console.log(` ${chalk.dim('→')} ${tildify(session.projectPath)}`);
|
|
167
175
|
console.log(` ${chalk.dim('Modified:')} ${session.lastModified.toLocaleDateString()}`);
|
|
168
176
|
console.log();
|
|
169
177
|
}
|
|
@@ -180,7 +188,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
180
188
|
console.log(
|
|
181
189
|
` ${chalk.yellow('[config]')} ${chalk.white(projectName)}`
|
|
182
190
|
);
|
|
183
|
-
console.log(` ${chalk.dim('→')} ${entry.projectPath}`);
|
|
191
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.projectPath)}`);
|
|
184
192
|
if (entry.configStats?.lastCost) {
|
|
185
193
|
const cost = `$${entry.configStats.lastCost.toFixed(2)}`;
|
|
186
194
|
const inTokens = formatTokens(entry.configStats.lastTotalInputTokens);
|
|
@@ -191,6 +199,70 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
191
199
|
}
|
|
192
200
|
}
|
|
193
201
|
|
|
202
|
+
// Session Env (empty folders)
|
|
203
|
+
if (sessionEnvEntries.length > 0) {
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(chalk.bold('Empty Session Env Folders:'));
|
|
206
|
+
console.log();
|
|
207
|
+
|
|
208
|
+
for (const entry of sessionEnvEntries) {
|
|
209
|
+
const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
|
|
210
|
+
console.log(
|
|
211
|
+
` ${chalk.green('[session-env]')} ${chalk.white(folderName)} ${chalk.dim('(empty)')}`
|
|
212
|
+
);
|
|
213
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
|
|
214
|
+
console.log();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Todos
|
|
219
|
+
if (todosEntries.length > 0) {
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(chalk.bold('Orphaned Todos:'));
|
|
222
|
+
console.log();
|
|
223
|
+
|
|
224
|
+
for (const entry of todosEntries) {
|
|
225
|
+
const fileName = entry.sessionPath.split('/').pop() || entry.sessionPath;
|
|
226
|
+
console.log(
|
|
227
|
+
` ${chalk.magenta('[todos]')} ${chalk.white(fileName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
|
|
228
|
+
);
|
|
229
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
|
|
230
|
+
console.log();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// File history
|
|
235
|
+
if (fileHistoryEntries.length > 0) {
|
|
236
|
+
console.log();
|
|
237
|
+
console.log(chalk.bold('Orphaned File History:'));
|
|
238
|
+
console.log();
|
|
239
|
+
|
|
240
|
+
for (const entry of fileHistoryEntries) {
|
|
241
|
+
const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
|
|
242
|
+
console.log(
|
|
243
|
+
` ${chalk.blue('[file-history]')} ${chalk.white(folderName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
|
|
244
|
+
);
|
|
245
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
|
|
246
|
+
console.log();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Tasks
|
|
251
|
+
if (tasksEntries.length > 0) {
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(chalk.bold('Orphaned Tasks:'));
|
|
254
|
+
console.log();
|
|
255
|
+
|
|
256
|
+
for (const entry of tasksEntries) {
|
|
257
|
+
const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
|
|
258
|
+
console.log(
|
|
259
|
+
` ${chalk.cyan('[tasks]')} ${chalk.white(folderName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
|
|
260
|
+
);
|
|
261
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
|
|
262
|
+
console.log();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
194
266
|
}
|
|
195
267
|
|
|
196
268
|
console.log();
|
package/src/commands/watch.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { join, resolve } from 'path';
|
|
|
6
6
|
|
|
7
7
|
import { logger } from '../utils/logger.js';
|
|
8
8
|
import { formatSize } from '../utils/size.js';
|
|
9
|
+
import { tildify } from '../utils/paths.js';
|
|
9
10
|
import { getWatchPaths as getConfigWatchPaths, setWatchPaths, getWatchDelay, getWatchDepth, getIgnorePaths } from '../utils/config.js';
|
|
10
11
|
import {
|
|
11
12
|
createAllScanners,
|
|
@@ -140,7 +141,7 @@ const statusCommand = new Command('status')
|
|
|
140
141
|
if (status.pid) {
|
|
141
142
|
console.log(`PID: ${status.pid}`);
|
|
142
143
|
}
|
|
143
|
-
console.log(`Plist: ${status.plistPath}`);
|
|
144
|
+
console.log(`Plist: ${tildify(status.plistPath)}`);
|
|
144
145
|
console.log();
|
|
145
146
|
|
|
146
147
|
if (options.logs) {
|
|
@@ -233,7 +234,7 @@ async function runWatcher(options: RunOptions): Promise<void> {
|
|
|
233
234
|
|
|
234
235
|
if (validPaths.length < watchPaths.length) {
|
|
235
236
|
const invalidPaths = watchPaths.filter((p) => !existsSync(p));
|
|
236
|
-
logger.warn(`Skipping non-existent paths: ${invalidPaths.join(', ')}`);
|
|
237
|
+
logger.warn(`Skipping non-existent paths: ${invalidPaths.map(tildify).join(', ')}`);
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
// Check scanners
|
|
@@ -250,7 +251,7 @@ async function runWatcher(options: RunOptions): Promise<void> {
|
|
|
250
251
|
logger.info(
|
|
251
252
|
`Watching for project deletions (${availableScanners.map((s) => s.name).join(', ')})`
|
|
252
253
|
);
|
|
253
|
-
logger.info(`Watch paths: ${validPaths.join(', ')}`);
|
|
254
|
+
logger.info(`Watch paths: ${validPaths.map(tildify).join(', ')}`);
|
|
254
255
|
logger.info(`Cleanup delay: ${String(delayMinutes)} minute(s)`);
|
|
255
256
|
logger.info(`Watch depth: ${String(depth)}`);
|
|
256
257
|
if (process.stdout.isTTY) {
|
|
@@ -264,16 +265,17 @@ async function runWatcher(options: RunOptions): Promise<void> {
|
|
|
264
265
|
watchPaths: validPaths,
|
|
265
266
|
delayMs,
|
|
266
267
|
depth,
|
|
267
|
-
ignorePaths: getIgnorePaths(),
|
|
268
|
+
ignorePaths: getIgnorePaths() ?? [],
|
|
268
269
|
onDelete: async (events) => {
|
|
269
270
|
// Log batch events
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
const firstEvent = events[0];
|
|
272
|
+
if (events.length === 1 && firstEvent) {
|
|
273
|
+
logger.info(`Detected deletion: ${tildify(firstEvent.path)}`);
|
|
272
274
|
} else {
|
|
273
275
|
logger.info(`Detected ${events.length} deletions (debounced)`);
|
|
274
276
|
if (options.verbose) {
|
|
275
277
|
for (const event of events) {
|
|
276
|
-
logger.debug(` - ${event.path}`);
|
|
278
|
+
logger.debug(` - ${tildify(event.path)}`);
|
|
277
279
|
}
|
|
278
280
|
}
|
|
279
281
|
}
|
|
@@ -312,6 +314,9 @@ async function runWatcher(options: RunOptions): Promise<void> {
|
|
|
312
314
|
if (deletedByType.fileHistory > 0) {
|
|
313
315
|
parts.push(`${deletedByType.fileHistory} file-history`);
|
|
314
316
|
}
|
|
317
|
+
if (deletedByType.tasks > 0) {
|
|
318
|
+
parts.push(`${deletedByType.tasks} tasks`);
|
|
319
|
+
}
|
|
315
320
|
|
|
316
321
|
const summary = parts.length > 0 ? parts.join(' + ') : `${cleanResult.deletedCount} item(s)`;
|
|
317
322
|
logger.success(
|
package/src/core/cleaner.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface CleanCountByType {
|
|
|
21
21
|
sessionEnv: number;
|
|
22
22
|
todos: number;
|
|
23
23
|
fileHistory: number;
|
|
24
|
+
tasks: number;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface CleanResult {
|
|
@@ -55,6 +56,7 @@ export class Cleaner {
|
|
|
55
56
|
sessionEnv: 0,
|
|
56
57
|
todos: 0,
|
|
57
58
|
fileHistory: 0,
|
|
59
|
+
tasks: 0,
|
|
58
60
|
},
|
|
59
61
|
skippedCount: 0,
|
|
60
62
|
alreadyGoneCount: 0,
|
|
@@ -96,6 +98,9 @@ export class Cleaner {
|
|
|
96
98
|
case 'file-history':
|
|
97
99
|
result.deletedByType.fileHistory++;
|
|
98
100
|
break;
|
|
101
|
+
case 'tasks':
|
|
102
|
+
result.deletedByType.tasks++;
|
|
103
|
+
break;
|
|
99
104
|
default:
|
|
100
105
|
result.deletedByType.session++;
|
|
101
106
|
}
|
package/src/core/constants.ts
CHANGED
|
@@ -7,16 +7,16 @@ export const IGNORED_SYSTEM_PATTERNS: RegExp[] = [
|
|
|
7
7
|
/(^|[/\\])\../,
|
|
8
8
|
|
|
9
9
|
// macOS user folders (not project directories)
|
|
10
|
-
/\/Library
|
|
11
|
-
/\/Applications
|
|
12
|
-
/\/Music
|
|
13
|
-
/\/Movies
|
|
14
|
-
/\/Pictures
|
|
15
|
-
/\/Downloads
|
|
16
|
-
/\/Documents
|
|
17
|
-
/\/Desktop
|
|
18
|
-
/\/Public
|
|
10
|
+
/\/Library(\/|$)/,
|
|
11
|
+
/\/Applications(\/|$)/,
|
|
12
|
+
/\/Music(\/|$)/,
|
|
13
|
+
/\/Movies(\/|$)/,
|
|
14
|
+
/\/Pictures(\/|$)/,
|
|
15
|
+
/\/Downloads(\/|$)/,
|
|
16
|
+
/\/Documents(\/|$)/,
|
|
17
|
+
/\/Desktop(\/|$)/,
|
|
18
|
+
/\/Public(\/|$)/,
|
|
19
19
|
|
|
20
20
|
// Development folders
|
|
21
|
-
/node_modules/,
|
|
21
|
+
/node_modules(\/|$)/,
|
|
22
22
|
];
|