@sooink/ai-session-tidy 0.1.2 → 0.1.3
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/README.ko.md +45 -8
- package/README.md +45 -8
- package/assets/demo-interactive.gif +0 -0
- package/assets/demo.gif +0 -0
- package/dist/index.js +237 -108
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/clean.ts +161 -78
- package/src/commands/config.ts +7 -2
- package/src/commands/scan.ts +51 -2
- package/src/commands/watch.ts +6 -5
- package/src/core/constants.ts +10 -10
- package/src/core/service.ts +11 -0
- package/src/utils/paths.ts +36 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sooink/ai-session-tidy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI tool that detects and cleans orphaned session data from AI coding tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -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,33 @@ 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';
|
|
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
|
+
};
|
|
51
|
+
const colors: Record<string, (s: string) => string> = {
|
|
52
|
+
'session-env': chalk.green,
|
|
53
|
+
'todos': chalk.magenta,
|
|
54
|
+
'file-history': chalk.blue,
|
|
55
|
+
};
|
|
56
|
+
const label = labels[group.type] || group.type;
|
|
57
|
+
const count = group.sessions.length;
|
|
58
|
+
const plural = count > 1 ? 's' : '';
|
|
59
|
+
const color = colors[group.type] || chalk.white;
|
|
60
|
+
return `${color(`[${group.type}]`)} ${chalk.white(`${count} ${label}${plural}`)} ${chalk.dim(`(${formatSize(group.totalSize)})`)}`;
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
export const cleanCommand = new Command('clean')
|
|
@@ -67,7 +91,7 @@ export const cleanCommand = new Command('clean')
|
|
|
67
91
|
return;
|
|
68
92
|
}
|
|
69
93
|
|
|
70
|
-
// Separate
|
|
94
|
+
// Separate by type
|
|
71
95
|
const folderSessions = allSessions.filter(
|
|
72
96
|
(s) => s.type === 'session' || s.type === undefined
|
|
73
97
|
);
|
|
@@ -80,21 +104,38 @@ export const cleanCommand = new Command('clean')
|
|
|
80
104
|
(s) => s.type === 'file-history'
|
|
81
105
|
);
|
|
82
106
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
...sessionEnvEntries,
|
|
86
|
-
...todosEntries,
|
|
87
|
-
...fileHistoryEntries,
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
// Interactive selection targets (excluding auto-cleanup targets)
|
|
91
|
-
const selectableSessions = allSessions.filter(
|
|
107
|
+
// Individual selection targets (session folders and config entries)
|
|
108
|
+
const individualSessions = allSessions.filter(
|
|
92
109
|
(s) =>
|
|
93
110
|
s.type !== 'session-env' &&
|
|
94
111
|
s.type !== 'todos' &&
|
|
95
112
|
s.type !== 'file-history'
|
|
96
113
|
);
|
|
97
114
|
|
|
115
|
+
// Group selection targets (session-env, todos, file-history)
|
|
116
|
+
const groupChoices: GroupChoice[] = [];
|
|
117
|
+
if (sessionEnvEntries.length > 0) {
|
|
118
|
+
groupChoices.push({
|
|
119
|
+
type: 'session-env',
|
|
120
|
+
sessions: sessionEnvEntries,
|
|
121
|
+
totalSize: sessionEnvEntries.reduce((sum, s) => sum + s.size, 0),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (todosEntries.length > 0) {
|
|
125
|
+
groupChoices.push({
|
|
126
|
+
type: 'todos',
|
|
127
|
+
sessions: todosEntries,
|
|
128
|
+
totalSize: todosEntries.reduce((sum, s) => sum + s.size, 0),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (fileHistoryEntries.length > 0) {
|
|
132
|
+
groupChoices.push({
|
|
133
|
+
type: 'file-history',
|
|
134
|
+
sessions: fileHistoryEntries,
|
|
135
|
+
totalSize: fileHistoryEntries.reduce((sum, s) => sum + s.size, 0),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
98
139
|
// Output summary
|
|
99
140
|
console.log();
|
|
100
141
|
const parts: string[] = [];
|
|
@@ -117,68 +158,108 @@ export const cleanCommand = new Command('clean')
|
|
|
117
158
|
|
|
118
159
|
if (options.verbose && !options.interactive) {
|
|
119
160
|
console.log();
|
|
120
|
-
|
|
121
|
-
for (const session of selectableSessions) {
|
|
161
|
+
for (const session of individualSessions) {
|
|
122
162
|
console.log(
|
|
123
|
-
chalk.dim(` ${session.toolName}: ${session.projectPath}`)
|
|
163
|
+
chalk.dim(` ${session.toolName}: ${tildify(session.projectPath)}`)
|
|
124
164
|
);
|
|
125
165
|
}
|
|
126
166
|
}
|
|
127
167
|
|
|
128
|
-
// Interactive mode: session selection
|
|
168
|
+
// Interactive mode: session selection
|
|
129
169
|
let sessionsToClean = allSessions;
|
|
130
170
|
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
|
-
|
|
171
|
+
if (!process.stdout.isTTY) {
|
|
172
|
+
logger.error('Interactive mode requires a TTY. Omit -i or use -f to skip confirmation.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Build choices: individual sessions + group choices
|
|
176
|
+
const choices: Array<{
|
|
177
|
+
name: string;
|
|
178
|
+
value: { type: 'individual'; session: OrphanedSession } | { type: 'group'; group: GroupChoice };
|
|
179
|
+
checked: boolean;
|
|
180
|
+
}> = [];
|
|
181
|
+
|
|
182
|
+
// Add individual sessions
|
|
183
|
+
for (const session of individualSessions) {
|
|
184
|
+
choices.push({
|
|
185
|
+
name: formatSessionChoice(session),
|
|
186
|
+
value: { type: 'individual', session },
|
|
187
|
+
checked: false,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Add group choices
|
|
192
|
+
for (const group of groupChoices) {
|
|
193
|
+
choices.push({
|
|
194
|
+
name: formatGroupChoice(group),
|
|
195
|
+
value: { type: 'group', group },
|
|
196
|
+
checked: false,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (choices.length === 0) {
|
|
201
|
+
logger.info('No selectable sessions found.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log();
|
|
206
|
+
const { selected } = await inquirer.prompt<{
|
|
207
|
+
selected: Array<{ type: 'individual'; session: OrphanedSession } | { type: 'group'; group: GroupChoice }>;
|
|
208
|
+
}>([
|
|
209
|
+
{
|
|
210
|
+
type: 'checkbox',
|
|
211
|
+
name: 'selected',
|
|
212
|
+
message: 'Select items to delete:',
|
|
213
|
+
choices,
|
|
214
|
+
pageSize: 15,
|
|
215
|
+
loop: false,
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
if (selected.length === 0) {
|
|
220
|
+
logger.info('No items selected. Cancelled.');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Clear inquirer's multi-line output (approximately N+1 lines for N selected items)
|
|
225
|
+
if (process.stdout.isTTY) {
|
|
226
|
+
const linesToClear = selected.length + 1;
|
|
227
|
+
for (let i = 0; i < linesToClear; i++) {
|
|
228
|
+
process.stdout.write('\x1B[1A\x1B[2K');
|
|
164
229
|
}
|
|
230
|
+
}
|
|
165
231
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
232
|
+
// Display selected items in a clean list
|
|
233
|
+
console.log(chalk.green('✔') + ' ' + chalk.bold('Selected items:'));
|
|
234
|
+
for (const item of selected) {
|
|
235
|
+
if (item.type === 'individual') {
|
|
236
|
+
const s = item.session;
|
|
237
|
+
const tag = s.type === 'config'
|
|
238
|
+
? chalk.yellow('[config]')
|
|
239
|
+
: chalk.cyan(`[${s.toolName}]`);
|
|
240
|
+
const size = s.type === 'config' ? '' : ` ${chalk.dim(`(${formatSize(s.size)})`)}`;
|
|
241
|
+
console.log(` ${tag} ${basename(s.projectPath)}${size}`);
|
|
242
|
+
console.log(` ${chalk.dim(`→ ${tildify(s.projectPath)}`)}`);
|
|
243
|
+
} else {
|
|
244
|
+
console.log(` ${formatGroupChoice(item.group)}`);
|
|
174
245
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Flatten selected items
|
|
249
|
+
sessionsToClean = [];
|
|
250
|
+
for (const item of selected) {
|
|
251
|
+
if (item.type === 'individual') {
|
|
252
|
+
sessionsToClean.push(item.session);
|
|
253
|
+
} else {
|
|
254
|
+
sessionsToClean.push(...item.group.sessions);
|
|
180
255
|
}
|
|
181
256
|
}
|
|
257
|
+
|
|
258
|
+
const selectedSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
|
|
259
|
+
console.log();
|
|
260
|
+
logger.info(
|
|
261
|
+
`Selected: ${sessionsToClean.length} item(s) (${formatSize(selectedSize)})`
|
|
262
|
+
);
|
|
182
263
|
}
|
|
183
264
|
|
|
184
265
|
const cleanSize = sessionsToClean.reduce((sum, s) => sum + s.size, 0);
|
|
@@ -205,7 +286,7 @@ export const cleanCommand = new Command('clean')
|
|
|
205
286
|
|
|
206
287
|
for (const session of dryRunFolders) {
|
|
207
288
|
console.log(
|
|
208
|
-
` ${chalk.red('Would delete:')} ${session.sessionPath} (${formatSize(session.size)})`
|
|
289
|
+
` ${chalk.red('Would delete:')} ${tildify(session.sessionPath)} (${formatSize(session.size)})`
|
|
209
290
|
);
|
|
210
291
|
}
|
|
211
292
|
|
|
@@ -215,29 +296,27 @@ export const cleanCommand = new Command('clean')
|
|
|
215
296
|
` ${chalk.yellow('Would remove from ~/.claude.json:')}`
|
|
216
297
|
);
|
|
217
298
|
for (const config of dryRunConfigs) {
|
|
218
|
-
console.log(` - ${config.projectPath}`);
|
|
299
|
+
console.log(` - ${tildify(config.projectPath)}`);
|
|
219
300
|
}
|
|
220
301
|
}
|
|
221
302
|
|
|
222
|
-
//
|
|
223
|
-
const autoCleanParts: string[] = [];
|
|
303
|
+
// Group items summary
|
|
224
304
|
if (dryRunEnvs.length > 0) {
|
|
225
|
-
|
|
305
|
+
console.log();
|
|
306
|
+
console.log(
|
|
307
|
+
` ${chalk.green('Would delete:')} ${dryRunEnvs.length} session-env folder(s)`
|
|
308
|
+
);
|
|
226
309
|
}
|
|
227
310
|
if (dryRunTodos.length > 0) {
|
|
228
|
-
|
|
311
|
+
console.log();
|
|
312
|
+
console.log(
|
|
313
|
+
` ${chalk.magenta('Would delete:')} ${dryRunTodos.length} todos file(s) (${formatSize(dryRunTodos.reduce((sum, s) => sum + s.size, 0))})`
|
|
314
|
+
);
|
|
229
315
|
}
|
|
230
316
|
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
317
|
console.log();
|
|
239
318
|
console.log(
|
|
240
|
-
` ${chalk.
|
|
319
|
+
` ${chalk.blue('Would delete:')} ${dryRunHistories.length} file-history folder(s) (${formatSize(dryRunHistories.reduce((sum, s) => sum + s.size, 0))})`
|
|
241
320
|
);
|
|
242
321
|
}
|
|
243
322
|
return;
|
|
@@ -245,6 +324,10 @@ export const cleanCommand = new Command('clean')
|
|
|
245
324
|
|
|
246
325
|
// Confirmation prompt (also in interactive mode)
|
|
247
326
|
if (!options.force) {
|
|
327
|
+
if (!process.stdout.isTTY) {
|
|
328
|
+
logger.error('Confirmation requires a TTY. Use -f to skip confirmation.');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
248
331
|
console.log();
|
|
249
332
|
const action = options.noTrash ? 'permanently delete' : 'move to trash';
|
|
250
333
|
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
|
|
@@ -321,7 +404,7 @@ export const cleanCommand = new Command('clean')
|
|
|
321
404
|
logger.error(`Failed to delete ${cleanResult.errors.length} item(s)`);
|
|
322
405
|
if (options.verbose) {
|
|
323
406
|
for (const err of cleanResult.errors) {
|
|
324
|
-
console.log(chalk.red(` ${err.sessionPath}: ${err.error.message}`));
|
|
407
|
+
console.log(chalk.red(` ${tildify(err.sessionPath)}: ${err.error.message}`));
|
|
325
408
|
}
|
|
326
409
|
}
|
|
327
410
|
}
|
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,
|
|
@@ -163,7 +164,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
163
164
|
console.log(
|
|
164
165
|
` ${chalk.cyan(`[${session.toolName}]`)} ${chalk.white(projectName)} ${chalk.dim(`(${formatSize(session.size)})`)}`
|
|
165
166
|
);
|
|
166
|
-
console.log(` ${chalk.dim('→')} ${session.projectPath}`);
|
|
167
|
+
console.log(` ${chalk.dim('→')} ${tildify(session.projectPath)}`);
|
|
167
168
|
console.log(` ${chalk.dim('Modified:')} ${session.lastModified.toLocaleDateString()}`);
|
|
168
169
|
console.log();
|
|
169
170
|
}
|
|
@@ -180,7 +181,7 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
180
181
|
console.log(
|
|
181
182
|
` ${chalk.yellow('[config]')} ${chalk.white(projectName)}`
|
|
182
183
|
);
|
|
183
|
-
console.log(` ${chalk.dim('→')} ${entry.projectPath}`);
|
|
184
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.projectPath)}`);
|
|
184
185
|
if (entry.configStats?.lastCost) {
|
|
185
186
|
const cost = `$${entry.configStats.lastCost.toFixed(2)}`;
|
|
186
187
|
const inTokens = formatTokens(entry.configStats.lastTotalInputTokens);
|
|
@@ -191,6 +192,54 @@ function outputTable(results: ScanResult[], verbose?: boolean): void {
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
// Session Env (empty folders)
|
|
196
|
+
if (sessionEnvEntries.length > 0) {
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(chalk.bold('Empty Session Env Folders:'));
|
|
199
|
+
console.log();
|
|
200
|
+
|
|
201
|
+
for (const entry of sessionEnvEntries) {
|
|
202
|
+
const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
|
|
203
|
+
console.log(
|
|
204
|
+
` ${chalk.green('[session-env]')} ${chalk.white(folderName)} ${chalk.dim('(empty)')}`
|
|
205
|
+
);
|
|
206
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
|
|
207
|
+
console.log();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Todos
|
|
212
|
+
if (todosEntries.length > 0) {
|
|
213
|
+
console.log();
|
|
214
|
+
console.log(chalk.bold('Orphaned Todos:'));
|
|
215
|
+
console.log();
|
|
216
|
+
|
|
217
|
+
for (const entry of todosEntries) {
|
|
218
|
+
const fileName = entry.sessionPath.split('/').pop() || entry.sessionPath;
|
|
219
|
+
console.log(
|
|
220
|
+
` ${chalk.magenta('[todos]')} ${chalk.white(fileName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
|
|
221
|
+
);
|
|
222
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
|
|
223
|
+
console.log();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// File history
|
|
228
|
+
if (fileHistoryEntries.length > 0) {
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(chalk.bold('Orphaned File History:'));
|
|
231
|
+
console.log();
|
|
232
|
+
|
|
233
|
+
for (const entry of fileHistoryEntries) {
|
|
234
|
+
const folderName = entry.sessionPath.split('/').pop() || entry.sessionPath;
|
|
235
|
+
console.log(
|
|
236
|
+
` ${chalk.blue('[file-history]')} ${chalk.white(folderName)} ${chalk.dim(`(${formatSize(entry.size)})`)}`
|
|
237
|
+
);
|
|
238
|
+
console.log(` ${chalk.dim('→')} ${tildify(entry.sessionPath)}`);
|
|
239
|
+
console.log();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
194
243
|
}
|
|
195
244
|
|
|
196
245
|
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) {
|
|
@@ -268,12 +269,12 @@ async function runWatcher(options: RunOptions): Promise<void> {
|
|
|
268
269
|
onDelete: async (events) => {
|
|
269
270
|
// Log batch events
|
|
270
271
|
if (events.length === 1) {
|
|
271
|
-
logger.info(`Detected deletion: ${events[0].path}`);
|
|
272
|
+
logger.info(`Detected deletion: ${tildify(events[0].path)}`);
|
|
272
273
|
} else {
|
|
273
274
|
logger.info(`Detected ${events.length} deletions (debounced)`);
|
|
274
275
|
if (options.verbose) {
|
|
275
276
|
for (const event of events) {
|
|
276
|
-
logger.debug(` - ${event.path}`);
|
|
277
|
+
logger.debug(` - ${tildify(event.path)}`);
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
}
|
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
|
];
|
package/src/core/service.ts
CHANGED
|
@@ -131,6 +131,17 @@ export class ServiceManager {
|
|
|
131
131
|
throw new Error('Service not installed. Run "watch start" to install and start.');
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Clear old log files before starting
|
|
135
|
+
const logDir = join(homedir(), '.ai-session-tidy');
|
|
136
|
+
const stdoutPath = join(logDir, 'watcher.log');
|
|
137
|
+
const stderrPath = join(logDir, 'watcher.error.log');
|
|
138
|
+
if (existsSync(stdoutPath)) {
|
|
139
|
+
await writeFile(stdoutPath, '', 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
if (existsSync(stderrPath)) {
|
|
142
|
+
await writeFile(stderrPath, '', 'utf-8');
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
try {
|
|
135
146
|
execSync(`launchctl load "${this.plistPath}"`, { stdio: 'pipe' });
|
|
136
147
|
} catch (error) {
|
package/src/utils/paths.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
1
2
|
import { homedir } from 'os';
|
|
2
3
|
import { join } from 'path';
|
|
3
4
|
|
|
@@ -14,13 +15,45 @@ export function encodePath(path: string): string {
|
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Decode Claude Code encoded path to Unix path
|
|
17
|
-
*
|
|
18
|
+
* Handles hyphenated folder names by checking filesystem
|
|
19
|
+
*
|
|
20
|
+
* -Users-kory-my-project → /Users/kory/my-project (if exists)
|
|
21
|
+
* -Users-kory-my-project → /Users/kory/my/project (fallback)
|
|
18
22
|
*/
|
|
19
23
|
export function decodePath(encoded: string): string {
|
|
20
24
|
if (encoded === '') return '';
|
|
21
|
-
// Treat as Unix encoding if it starts with a dash
|
|
22
25
|
if (!encoded.startsWith('-')) return encoded;
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
// Split by dash (remove leading dash first)
|
|
28
|
+
const parts = encoded.slice(1).split('-');
|
|
29
|
+
|
|
30
|
+
// Try to reconstruct path by checking filesystem
|
|
31
|
+
return reconstructPath(parts, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively reconstruct path by trying different combinations
|
|
36
|
+
*/
|
|
37
|
+
function reconstructPath(parts: string[], currentPath: string): string {
|
|
38
|
+
if (parts.length === 0) return currentPath;
|
|
39
|
+
|
|
40
|
+
// Try combining segments (longest first to prefer hyphenated names)
|
|
41
|
+
for (let len = parts.length; len >= 1; len--) {
|
|
42
|
+
const segment = parts.slice(0, len).join('-');
|
|
43
|
+
const testPath = currentPath + '/' + segment;
|
|
44
|
+
|
|
45
|
+
if (existsSync(testPath)) {
|
|
46
|
+
// Found existing path, continue with remaining parts
|
|
47
|
+
const result = reconstructPath(parts.slice(len), testPath);
|
|
48
|
+
// Verify the final path exists (or is the best we can do)
|
|
49
|
+
if (existsSync(result) || parts.slice(len).length === 0) {
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No existing path found, use simple decode for remaining parts
|
|
56
|
+
return currentPath + '/' + parts.join('/');
|
|
24
57
|
}
|
|
25
58
|
|
|
26
59
|
/**
|