@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sooink/ai-session-tidy",
3
- "version": "0.1.2",
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
+ }
@@ -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} ${size}\n ${path}`;
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 session folders, config entries, and auto-cleanup targets
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
- // 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(
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
- // Exclude auto-cleanup targets from detailed list
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 (excluding auto-cleanup targets)
168
+ // Interactive mode: session selection
129
169
  let sessionsToClean = allSessions;
130
170
  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;
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
- // 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
- );
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
- 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
- );
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
- // Auto-cleanup targets summary
223
- const autoCleanParts: string[] = [];
303
+ // Group items summary
224
304
  if (dryRunEnvs.length > 0) {
225
- autoCleanParts.push(`${dryRunEnvs.length} session-env`);
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
- autoCleanParts.push(`${dryRunTodos.length} todos`);
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.dim(`Would auto-delete: ${autoCleanParts.join(' + ')} (${formatSize(autoSize)})`)}`
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
  }
@@ -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',
@@ -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();
@@ -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
  }
@@ -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
  ];
@@ -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) {
@@ -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
- * -home-user-project /home/user/project
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
- return encoded.replace(/-/g, '/');
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
  /**