oh-my-node-modules 1.0.0

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.
@@ -0,0 +1,562 @@
1
+ /**
2
+ * Core type definitions for oh-my-node-modules
3
+ *
4
+ * These types represent the domain model for node_modules analysis.
5
+ * Each interface answers a specific question about the data structure.
6
+ */
7
+ /**
8
+ * Represents a discovered node_modules directory with all relevant metadata.
9
+ * This is the core data structure that flows through the entire application.
10
+ */
11
+ interface NodeModulesInfo {
12
+ /** Absolute path to the node_modules directory */
13
+ path: string;
14
+ /** Absolute path to the parent project directory (containing package.json) */
15
+ projectPath: string;
16
+ /** Project name from package.json, or directory name if no package.json */
17
+ projectName: string;
18
+ /** Project version from package.json, or undefined if not available */
19
+ projectVersion?: string;
20
+ /** Total size in bytes (recursive, includes all nested files) */
21
+ sizeBytes: number;
22
+ /** Human-readable size string (e.g., "1.2 GB", "456 MB") */
23
+ sizeFormatted: string;
24
+ /** Number of packages directly in node_modules (top-level) */
25
+ packageCount: number;
26
+ /** Total number of packages including nested dependencies */
27
+ totalPackageCount: number;
28
+ /** Last modification time of the node_modules directory */
29
+ lastModified: Date;
30
+ /** Formatted string showing how long ago (e.g., "30d ago", "2d ago") */
31
+ lastModifiedFormatted: string;
32
+ /** Whether this node_modules is currently selected for deletion */
33
+ selected: boolean;
34
+ /** Whether this project is marked as a favorite (never suggest deleting) */
35
+ isFavorite: boolean;
36
+ /** Age category for color coding */
37
+ ageCategory: AgeCategory;
38
+ /** Size category for color coding */
39
+ sizeCategory: SizeCategory;
40
+ }
41
+ /**
42
+ * Age categories determine visual styling in the TUI.
43
+ * Used to highlight stale/old node_modules that might be safe to delete.
44
+ */
45
+ type AgeCategory = 'fresh' | 'recent' | 'old' | 'stale';
46
+ /**
47
+ * Size categories determine visual styling and smart selection rules.
48
+ * Helps users quickly identify large disk space consumers.
49
+ */
50
+ type SizeCategory = 'small' | 'medium' | 'large' | 'huge';
51
+ /**
52
+ * Sort options for the TUI list view.
53
+ * Each option represents a different way to organize the results.
54
+ */
55
+ type SortOption = 'size-desc' | 'size-asc' | 'date-desc' | 'date-asc' | 'name-asc' | 'name-desc' | 'packages-desc' | 'packages-asc';
56
+ /**
57
+ * Configuration options for the scanning process.
58
+ * Controls what gets scanned and how results are filtered.
59
+ */
60
+ interface ScanOptions {
61
+ /** Root directory to start scanning from */
62
+ rootPath: string;
63
+ /** Maximum depth to scan (undefined = unlimited) */
64
+ maxDepth?: number;
65
+ /** Patterns to exclude (glob strings) */
66
+ excludePatterns: string[];
67
+ /** Whether to follow symbolic links */
68
+ followSymlinks: boolean;
69
+ /** Minimum size threshold in bytes (skip smaller node_modules) */
70
+ minSizeBytes?: number;
71
+ /** Maximum age in days (only include node_modules older than this) */
72
+ olderThanDays?: number;
73
+ }
74
+ /**
75
+ * Configuration options for the deletion operation.
76
+ * Controls safety checks and behavior during deletion.
77
+ */
78
+ interface DeleteOptions {
79
+ /** Whether to perform a dry run (don't actually delete) */
80
+ dryRun: boolean;
81
+ /** Whether to skip confirmation prompts */
82
+ yes: boolean;
83
+ /** Whether to check for running processes before deleting */
84
+ checkRunningProcesses: boolean;
85
+ /** Whether to show a progress bar during deletion */
86
+ showProgress: boolean;
87
+ }
88
+ /**
89
+ * Results of a deletion operation.
90
+ * Provides detailed feedback about what was deleted and any errors.
91
+ */
92
+ interface DeletionResult {
93
+ /** Total number of node_modules directories attempted */
94
+ totalAttempted: number;
95
+ /** Number successfully deleted */
96
+ successful: number;
97
+ /** Number that failed to delete */
98
+ failed: number;
99
+ /** Total bytes freed (or that would be freed in dry run) */
100
+ bytesFreed: number;
101
+ /** Human-readable formatted version of bytes freed */
102
+ formattedBytesFreed: string;
103
+ /** Detailed results for each deletion attempt */
104
+ details: DeletionDetail[];
105
+ }
106
+ /**
107
+ * Result of a single node_modules deletion attempt.
108
+ */
109
+ interface DeletionDetail {
110
+ /** The node_modules info that was targeted */
111
+ nodeModules: NodeModulesInfo;
112
+ /** Whether the deletion succeeded */
113
+ success: boolean;
114
+ /** Error message if deletion failed */
115
+ error?: string;
116
+ /** Time taken to delete in milliseconds */
117
+ durationMs: number;
118
+ }
119
+ /**
120
+ * Application state for the TUI.
121
+ * Centralized state management following the reactive pattern.
122
+ */
123
+ interface AppState {
124
+ /** All discovered node_modules directories */
125
+ nodeModules: NodeModulesInfo[];
126
+ /** Currently selected index in the list (for keyboard navigation) */
127
+ selectedIndex: number;
128
+ /** Current sort option */
129
+ sortBy: SortOption;
130
+ /** Current filter string (empty = no filter) */
131
+ filterQuery: string;
132
+ /** Whether the app is currently scanning */
133
+ isScanning: boolean;
134
+ /** Whether the app is currently deleting */
135
+ isDeleting: boolean;
136
+ /** Scan progress (0-100) */
137
+ scanProgress: number;
138
+ /** Total bytes reclaimed in this session */
139
+ sessionBytesReclaimed: number;
140
+ /** Whether to show the help overlay */
141
+ showHelp: boolean;
142
+ /** Error message to display (undefined = no error) */
143
+ errorMessage?: string;
144
+ }
145
+ /**
146
+ * CLI arguments parsed from command line.
147
+ * Used to configure the application behavior.
148
+ */
149
+ interface CliArgs {
150
+ /** Path to scan (default: current directory) */
151
+ path: string;
152
+ /** Quick scan mode (no TUI, just report) */
153
+ scan: boolean;
154
+ /** Auto-delete mode (no TUI, delete matching criteria) */
155
+ auto: boolean;
156
+ /** Dry run mode (don't actually delete) */
157
+ dryRun: boolean;
158
+ /** Skip confirmations */
159
+ yes: boolean;
160
+ /** Minimum size threshold for auto mode (e.g., "1gb", "500mb") */
161
+ minSize?: string;
162
+ /** Output as JSON */
163
+ json: boolean;
164
+ /** Show help message */
165
+ help: boolean;
166
+ /** Show version */
167
+ version: boolean;
168
+ }
169
+ /**
170
+ * Statistics calculated from a list of node_modules.
171
+ * Used for summary displays and overview headers.
172
+ */
173
+ interface ScanStatistics$1 {
174
+ /** Total number of projects found */
175
+ totalProjects: number;
176
+ /** Total number of node_modules directories */
177
+ totalNodeModules: number;
178
+ /** Total size of all node_modules in bytes */
179
+ totalSizeBytes: number;
180
+ /** Total size formatted as human-readable string */
181
+ totalSizeFormatted: string;
182
+ /** Number of selected node_modules */
183
+ selectedCount: number;
184
+ /** Total size of selected node_modules */
185
+ selectedSizeBytes: number;
186
+ /** Total size of selected formatted */
187
+ selectedSizeFormatted: string;
188
+ /** Average age in days */
189
+ averageAgeDays: number;
190
+ /** Number of stale node_modules (>30 days) */
191
+ staleCount: number;
192
+ }
193
+ /**
194
+ * Color configuration for terminal output.
195
+ * Provides semantic color mapping for different categories.
196
+ */
197
+ interface ColorConfig {
198
+ /** Color for huge directories (>1GB) */
199
+ huge: string;
200
+ /** Color for large directories (>500MB) */
201
+ large: string;
202
+ /** Color for small directories (<100MB) */
203
+ small: string;
204
+ /** Color for stale/old directories */
205
+ stale: string;
206
+ /** Color for fresh directories */
207
+ fresh: string;
208
+ /** Color for selected items */
209
+ selected: string;
210
+ /** Color for errors */
211
+ error: string;
212
+ /** Color for success */
213
+ success: string;
214
+ /** Color for warnings */
215
+ warning: string;
216
+ /** Color for info/primary text */
217
+ info: string;
218
+ }
219
+ /** Default color configuration using standard terminal colors */
220
+ declare const DEFAULT_COLORS: ColorConfig;
221
+ /**
222
+ * Thresholds for size categorization in bytes.
223
+ * Used to determine visual styling and smart selection rules.
224
+ */
225
+ declare const SIZE_THRESHOLDS: {
226
+ /** 100 MB - upper bound for "small" category */
227
+ readonly SMALL: number;
228
+ /** 500 MB - upper bound for "medium" category */
229
+ readonly MEDIUM: number;
230
+ /** 1 GB - upper bound for "large" category */
231
+ readonly LARGE: number;
232
+ };
233
+ /**
234
+ * Thresholds for age categorization in days.
235
+ * Used to identify stale node_modules that might be safe to delete.
236
+ */
237
+ declare const AGE_THRESHOLDS: {
238
+ /** 7 days - still considered fresh */
239
+ readonly FRESH: 7;
240
+ /** 30 days - warning threshold */
241
+ readonly RECENT: 30;
242
+ /** 90 days - stale threshold */
243
+ readonly OLD: 90;
244
+ };
245
+
246
+ /**
247
+ * Scanner module for discovering and analyzing node_modules directories
248
+ *
249
+ * This module handles the core scanning functionality:
250
+ * - Recursive directory traversal
251
+ * - Size calculation (recursive)
252
+ * - Package.json parsing
253
+ * - Metadata extraction
254
+ *
255
+ * All operations are async and non-blocking to keep the TUI responsive.
256
+ */
257
+
258
+ /**
259
+ * Result of a directory scan operation.
260
+ */
261
+ interface ScanResult {
262
+ /** Discovered node_modules entries */
263
+ nodeModules: NodeModulesInfo[];
264
+ /** Number of directories scanned */
265
+ directoriesScanned: number;
266
+ /** Any errors encountered during scanning */
267
+ errors: string[];
268
+ }
269
+ /**
270
+ * Recursively scan for node_modules directories starting from root path.
271
+ *
272
+ * This is the main entry point for discovery. It walks the directory tree,
273
+ * identifies node_modules folders, and collects metadata about each one.
274
+ *
275
+ * @param options - Scan configuration options
276
+ * @param onProgress - Optional callback for progress updates (0-100)
277
+ * @returns Scan results with all discovered node_modules
278
+ */
279
+ declare function scanForNodeModules(options: ScanOptions, onProgress?: (progress: number) => void): Promise<ScanResult>;
280
+ /**
281
+ * Analyze a specific node_modules directory and extract all metadata.
282
+ *
283
+ * This function performs the heavy lifting of:
284
+ * - Calculating total size (recursive)
285
+ * - Counting packages
286
+ * - Reading parent project info
287
+ * - Determining age and size categories
288
+ *
289
+ * @param nodeModulesPath - Path to node_modules directory
290
+ * @param projectPath - Path to parent project (containing package.json)
291
+ * @returns Complete metadata for the node_modules
292
+ */
293
+ declare function analyzeNodeModules(nodeModulesPath: string, projectPath: string): Promise<NodeModulesInfo>;
294
+ /**
295
+ * Load ignore patterns from .onmignore file.
296
+ *
297
+ * Looks for .onmignore in:
298
+ * 1. Current working directory
299
+ * 2. Home directory
300
+ *
301
+ * @returns Array of ignore patterns
302
+ */
303
+ declare function loadIgnorePatterns(): Promise<string[]>;
304
+ /**
305
+ * Load favorites list from .onmfavorites file.
306
+ *
307
+ * Favorites are projects that should never be suggested for deletion.
308
+ *
309
+ * @returns Set of favorite project paths
310
+ */
311
+ declare function loadFavorites(): Promise<Set<string>>;
312
+ /**
313
+ * Check if a node_modules directory is currently in use.
314
+ *
315
+ * This is a safety check to prevent deleting node_modules that
316
+ * might be actively being used by a running process.
317
+ *
318
+ * Note: This is a best-effort check and may not catch all cases.
319
+ *
320
+ * @param path - Path to node_modules
321
+ * @returns True if potentially in use
322
+ */
323
+ declare function isNodeModulesInUse(path: string): Promise<boolean>;
324
+ /**
325
+ * Quick scan mode - just report without full metadata.
326
+ *
327
+ * Faster than full scan when you just need a quick overview.
328
+ *
329
+ * @param rootPath - Root directory to scan
330
+ * @returns Basic info about found node_modules
331
+ */
332
+ declare function quickScan(rootPath: string): Promise<Array<{
333
+ path: string;
334
+ projectPath: string;
335
+ projectName: string;
336
+ }>>;
337
+
338
+ /**
339
+ * Deletion module for safely removing node_modules directories
340
+ *
341
+ * This module handles:
342
+ * - Safe deletion with confirmations
343
+ * - Dry run mode
344
+ * - Progress tracking
345
+ * - Error handling
346
+ * - In-use detection
347
+ *
348
+ * Safety is the priority - we never delete without explicit confirmation
349
+ * and we check for potential issues before proceeding.
350
+ */
351
+
352
+ /**
353
+ * Delete selected node_modules directories.
354
+ *
355
+ * This is the main entry point for deletion operations. It:
356
+ * 1. Filters to only selected items
357
+ * 2. Performs safety checks
358
+ * 3. Deletes each node_modules (or simulates in dry run)
359
+ * 4. Collects results and statistics
360
+ *
361
+ * @param nodeModules - List of all node_modules (selected ones will be deleted)
362
+ * @param options - Deletion options
363
+ * @param onProgress - Optional callback for progress updates
364
+ * @returns Deletion results with statistics
365
+ */
366
+ declare function deleteSelectedNodeModules(nodeModules: NodeModulesInfo[], options: DeleteOptions, onProgress?: (current: number, total: number, currentPath: string) => void): Promise<DeletionResult>;
367
+ /**
368
+ * Generate a preview report of what would be deleted.
369
+ *
370
+ * Used for dry run mode and confirmation prompts.
371
+ *
372
+ * @param nodeModules - All node_modules items
373
+ * @returns Formatted report string
374
+ */
375
+ declare function generateDeletionPreview(nodeModules: NodeModulesInfo[]): string;
376
+ /**
377
+ * Generate a JSON report of deletion results.
378
+ *
379
+ * @param result - Deletion result
380
+ * @returns JSON string
381
+ */
382
+ declare function generateJSONReport(result: DeletionResult): string;
383
+ /**
384
+ * Select node_modules by size criteria.
385
+ *
386
+ * Helper for "select all >500MB" functionality.
387
+ *
388
+ * @param nodeModules - All node_modules
389
+ * @param minSizeBytes - Minimum size in bytes
390
+ * @returns Updated array with selections
391
+ */
392
+ declare function selectBySize(nodeModules: NodeModulesInfo[], minSizeBytes: number): NodeModulesInfo[];
393
+ /**
394
+ * Select node_modules by age criteria.
395
+ *
396
+ * Helper for "select all older than X days" functionality.
397
+ *
398
+ * @param nodeModules - All node_modules
399
+ * @param minAgeDays - Minimum age in days
400
+ * @returns Updated array with selections
401
+ */
402
+ declare function selectByAge(nodeModules: NodeModulesInfo[], minAgeDays: number): NodeModulesInfo[];
403
+ /**
404
+ * Select all node_modules.
405
+ *
406
+ * @param nodeModules - All node_modules
407
+ * @param selected - Whether to select (true) or deselect (false)
408
+ * @returns Updated array
409
+ */
410
+ declare function selectAll(nodeModules: NodeModulesInfo[], selected: boolean): NodeModulesInfo[];
411
+ /**
412
+ * Invert selection.
413
+ *
414
+ * @param nodeModules - All node_modules
415
+ * @returns Updated array with inverted selections
416
+ */
417
+ declare function invertSelection(nodeModules: NodeModulesInfo[]): NodeModulesInfo[];
418
+
419
+ /**
420
+ * Utility functions for oh-my-node-modules
421
+ *
422
+ * This module provides pure functions for common operations like
423
+ * formatting bytes, parsing sizes, and date calculations.
424
+ * Pure functions make testing easier and reduce side effects.
425
+ */
426
+
427
+ /**
428
+ * Statistics calculated from a list of node_modules.
429
+ * Used for summary displays and overview headers.
430
+ */
431
+ interface ScanStatistics {
432
+ /** Total number of projects found */
433
+ totalProjects: number;
434
+ /** Total number of node_modules directories */
435
+ totalNodeModules: number;
436
+ /** Total size of all node_modules in bytes */
437
+ totalSizeBytes: number;
438
+ /** Total size formatted as human-readable string */
439
+ totalSizeFormatted: string;
440
+ /** Number of selected node_modules */
441
+ selectedCount: number;
442
+ /** Total size of selected node_modules */
443
+ selectedSizeBytes: number;
444
+ /** Total size of selected formatted */
445
+ selectedSizeFormatted: string;
446
+ /** Average age in days */
447
+ averageAgeDays: number;
448
+ /** Number of stale node_modules (>30 days) */
449
+ staleCount: number;
450
+ }
451
+ /**
452
+ * Format bytes into human-readable string.
453
+ * Uses binary units (MiB, GiB) for accuracy.
454
+ *
455
+ * @param bytes - Number of bytes to format
456
+ * @returns Formatted string like "1.2 GB" or "456 MB"
457
+ */
458
+ declare function formatBytes(bytes: number): string;
459
+ /**
460
+ * Parse human-readable size string into bytes.
461
+ * Supports formats like "1gb", "500MB", "10mb"
462
+ *
463
+ * @param sizeStr - Size string to parse
464
+ * @returns Size in bytes, or undefined if invalid
465
+ */
466
+ declare function parseSize(sizeStr: string): number | undefined;
467
+ /**
468
+ * Format a date into "X days ago" string.
469
+ * Provides more readable relative time than raw dates.
470
+ *
471
+ * @param date - Date to format
472
+ * @returns Formatted string like "30d ago" or "2d ago"
473
+ */
474
+ declare function formatRelativeTime(date: Date): string;
475
+ /**
476
+ * Determine size category based on bytes.
477
+ * Used for color coding and smart selection.
478
+ *
479
+ * @param bytes - Size in bytes
480
+ * @returns Size category
481
+ */
482
+ declare function getSizeCategory(bytes: number): SizeCategory;
483
+ /**
484
+ * Determine age category based on days since modification.
485
+ * Used to identify potentially stale node_modules.
486
+ *
487
+ * @param lastModified - Last modification date
488
+ * @returns Age category
489
+ */
490
+ declare function getAgeCategory(lastModified: Date): AgeCategory;
491
+ /**
492
+ * Sort node_modules by the specified option.
493
+ * Pure function that returns a new sorted array.
494
+ *
495
+ * @param items - Array to sort
496
+ * @param sortBy - Sort option
497
+ * @returns New sorted array
498
+ */
499
+ declare function sortNodeModules(items: NodeModulesInfo[], sortBy: SortOption): NodeModulesInfo[];
500
+ /**
501
+ * Filter node_modules by search query.
502
+ * Matches against project name and path.
503
+ *
504
+ * @param items - Array to filter
505
+ * @param query - Search query (case-insensitive)
506
+ * @returns Filtered array
507
+ */
508
+ declare function filterNodeModules(items: NodeModulesInfo[], query: string): NodeModulesInfo[];
509
+ /**
510
+ * Calculate statistics from node_modules list.
511
+ * Used for overview displays and summaries.
512
+ *
513
+ * @param items - Node modules to analyze
514
+ * @returns Calculated statistics
515
+ */
516
+ declare function calculateStatistics(items: NodeModulesInfo[]): ScanStatistics;
517
+ /**
518
+ * Toggle selection state for a node_modules item.
519
+ * Returns new array with toggled item (immutable update).
520
+ *
521
+ * @param items - Current items array
522
+ * @param index - Index of item to toggle
523
+ * @returns New array with toggled selection
524
+ */
525
+ declare function toggleSelection(items: NodeModulesInfo[], index: number): NodeModulesInfo[];
526
+ /**
527
+ * Select or deselect all items matching a predicate.
528
+ * Useful for "select all >500MB" or "select all stale" operations.
529
+ *
530
+ * @param items - Current items array
531
+ * @param predicate - Function to determine which items to select
532
+ * @param selected - Whether to select (true) or deselect (false)
533
+ * @returns New array with updated selections
534
+ */
535
+ declare function selectByPredicate(items: NodeModulesInfo[], predicate: (item: NodeModulesInfo) => boolean, selected: boolean): NodeModulesInfo[];
536
+
537
+ /**
538
+ * oh-my-node-modules - Public API
539
+ *
540
+ * This module exports the public API for programmatic use.
541
+ * Most users will use the CLI, but the API is available for
542
+ * integration with other tools.
543
+ *
544
+ * @example
545
+ * ```typescript
546
+ * import { scanForNodeModules, deleteSelectedNodeModules } from 'oh-my-node-modules';
547
+ *
548
+ * const result = await scanForNodeModules({
549
+ * rootPath: '/path/to/projects',
550
+ * excludePatterns: [],
551
+ * followSymlinks: false,
552
+ * });
553
+ *
554
+ * // ... process results ...
555
+ * ```
556
+ */
557
+
558
+ declare const VERSION = "1.0.0";
559
+
560
+ export { AGE_THRESHOLDS, DEFAULT_COLORS, SIZE_THRESHOLDS, VERSION, analyzeNodeModules, calculateStatistics, deleteSelectedNodeModules, filterNodeModules, formatBytes, formatRelativeTime, generateDeletionPreview, generateJSONReport, getAgeCategory, getSizeCategory, invertSelection, isNodeModulesInUse, loadFavorites, loadIgnorePatterns, parseSize, quickScan, scanForNodeModules, selectAll, selectByAge, selectByPredicate, selectBySize, sortNodeModules, toggleSelection };
561
+ export type { AgeCategory, AppState, CliArgs, ColorConfig, DeleteOptions, DeletionDetail, DeletionResult, NodeModulesInfo, ScanOptions, ScanStatistics$1 as ScanStatistics, SizeCategory, SortOption };
562
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sources":["../src/types.ts","../src/scanner.ts","../src/deletion.ts","../src/utils.ts","../src/index.ts"],"sourcesContent":["/**\n * Core type definitions for oh-my-node-modules\n * \n * These types represent the domain model for node_modules analysis.\n * Each interface answers a specific question about the data structure.\n */\n\n/** \n * Represents a discovered node_modules directory with all relevant metadata.\n * This is the core data structure that flows through the entire application.\n */\nexport interface NodeModulesInfo {\n /** Absolute path to the node_modules directory */\n path: string;\n \n /** Absolute path to the parent project directory (containing package.json) */\n projectPath: string;\n \n /** Project name from package.json, or directory name if no package.json */\n projectName: string;\n \n /** Project version from package.json, or undefined if not available */\n projectVersion?: string;\n \n /** Total size in bytes (recursive, includes all nested files) */\n sizeBytes: number;\n \n /** Human-readable size string (e.g., \"1.2 GB\", \"456 MB\") */\n sizeFormatted: string;\n \n /** Number of packages directly in node_modules (top-level) */\n packageCount: number;\n \n /** Total number of packages including nested dependencies */\n totalPackageCount: number;\n \n /** Last modification time of the node_modules directory */\n lastModified: Date;\n \n /** Formatted string showing how long ago (e.g., \"30d ago\", \"2d ago\") */\n lastModifiedFormatted: string;\n \n /** Whether this node_modules is currently selected for deletion */\n selected: boolean;\n \n /** Whether this project is marked as a favorite (never suggest deleting) */\n isFavorite: boolean;\n \n /** Age category for color coding */\n ageCategory: AgeCategory;\n \n /** Size category for color coding */\n sizeCategory: SizeCategory;\n}\n\n/** \n * Age categories determine visual styling in the TUI.\n * Used to highlight stale/old node_modules that might be safe to delete.\n */\nexport type AgeCategory = 'fresh' | 'recent' | 'old' | 'stale';\n\n/**\n * Size categories determine visual styling and smart selection rules.\n * Helps users quickly identify large disk space consumers.\n */\nexport type SizeCategory = 'small' | 'medium' | 'large' | 'huge';\n\n/**\n * Sort options for the TUI list view.\n * Each option represents a different way to organize the results.\n */\nexport type SortOption = \n | 'size-desc' // Largest first (default)\n | 'size-asc' // Smallest first\n | 'date-desc' // Most recently modified first\n | 'date-asc' // Oldest first\n | 'name-asc' // Alphabetical A-Z\n | 'name-desc' // Alphabetical Z-A\n | 'packages-desc' // Most packages first\n | 'packages-asc'; // Fewest packages first\n\n/**\n * Configuration options for the scanning process.\n * Controls what gets scanned and how results are filtered.\n */\nexport interface ScanOptions {\n /** Root directory to start scanning from */\n rootPath: string;\n \n /** Maximum depth to scan (undefined = unlimited) */\n maxDepth?: number;\n \n /** Patterns to exclude (glob strings) */\n excludePatterns: string[];\n \n /** Whether to follow symbolic links */\n followSymlinks: boolean;\n \n /** Minimum size threshold in bytes (skip smaller node_modules) */\n minSizeBytes?: number;\n \n /** Maximum age in days (only include node_modules older than this) */\n olderThanDays?: number;\n}\n\n/**\n * Configuration options for the deletion operation.\n * Controls safety checks and behavior during deletion.\n */\nexport interface DeleteOptions {\n /** Whether to perform a dry run (don't actually delete) */\n dryRun: boolean;\n \n /** Whether to skip confirmation prompts */\n yes: boolean;\n \n /** Whether to check for running processes before deleting */\n checkRunningProcesses: boolean;\n \n /** Whether to show a progress bar during deletion */\n showProgress: boolean;\n}\n\n/**\n * Results of a deletion operation.\n * Provides detailed feedback about what was deleted and any errors.\n */\nexport interface DeletionResult {\n /** Total number of node_modules directories attempted */\n totalAttempted: number;\n \n /** Number successfully deleted */\n successful: number;\n \n /** Number that failed to delete */\n failed: number;\n \n /** Total bytes freed (or that would be freed in dry run) */\n bytesFreed: number;\n \n /** Human-readable formatted version of bytes freed */\n formattedBytesFreed: string;\n \n /** Detailed results for each deletion attempt */\n details: DeletionDetail[];\n}\n\n/**\n * Result of a single node_modules deletion attempt.\n */\nexport interface DeletionDetail {\n /** The node_modules info that was targeted */\n nodeModules: NodeModulesInfo;\n \n /** Whether the deletion succeeded */\n success: boolean;\n \n /** Error message if deletion failed */\n error?: string;\n \n /** Time taken to delete in milliseconds */\n durationMs: number;\n}\n\n/**\n * Application state for the TUI.\n * Centralized state management following the reactive pattern.\n */\nexport interface AppState {\n /** All discovered node_modules directories */\n nodeModules: NodeModulesInfo[];\n \n /** Currently selected index in the list (for keyboard navigation) */\n selectedIndex: number;\n \n /** Current sort option */\n sortBy: SortOption;\n \n /** Current filter string (empty = no filter) */\n filterQuery: string;\n \n /** Whether the app is currently scanning */\n isScanning: boolean;\n \n /** Whether the app is currently deleting */\n isDeleting: boolean;\n \n /** Scan progress (0-100) */\n scanProgress: number;\n \n /** Total bytes reclaimed in this session */\n sessionBytesReclaimed: number;\n \n /** Whether to show the help overlay */\n showHelp: boolean;\n \n /** Error message to display (undefined = no error) */\n errorMessage?: string;\n}\n\n/**\n * Initial state factory function.\n * Creates a fresh application state with sensible defaults.\n */\nexport function createInitialState(): AppState {\n return {\n nodeModules: [],\n selectedIndex: 0,\n sortBy: 'size-desc',\n filterQuery: '',\n isScanning: false,\n isDeleting: false,\n scanProgress: 0,\n sessionBytesReclaimed: 0,\n showHelp: false,\n };\n}\n\n/**\n * CLI arguments parsed from command line.\n * Used to configure the application behavior.\n */\nexport interface CliArgs {\n /** Path to scan (default: current directory) */\n path: string;\n \n /** Quick scan mode (no TUI, just report) */\n scan: boolean;\n \n /** Auto-delete mode (no TUI, delete matching criteria) */\n auto: boolean;\n \n /** Dry run mode (don't actually delete) */\n dryRun: boolean;\n \n /** Skip confirmations */\n yes: boolean;\n \n /** Minimum size threshold for auto mode (e.g., \"1gb\", \"500mb\") */\n minSize?: string;\n \n /** Output as JSON */\n json: boolean;\n \n /** Show help message */\n help: boolean;\n \n /** Show version */\n version: boolean;\n}\n\n/**\n * Statistics calculated from a list of node_modules.\n * Used for summary displays and overview headers.\n */\nexport interface ScanStatistics {\n /** Total number of projects found */\n totalProjects: number;\n \n /** Total number of node_modules directories */\n totalNodeModules: number;\n \n /** Total size of all node_modules in bytes */\n totalSizeBytes: number;\n \n /** Total size formatted as human-readable string */\n totalSizeFormatted: string;\n \n /** Number of selected node_modules */\n selectedCount: number;\n \n /** Total size of selected node_modules */\n selectedSizeBytes: number;\n \n /** Total size of selected formatted */\n selectedSizeFormatted: string;\n \n /** Average age in days */\n averageAgeDays: number;\n \n /** Number of stale node_modules (>30 days) */\n staleCount: number;\n}\n\n/**\n * Color configuration for terminal output.\n * Provides semantic color mapping for different categories.\n */\nexport interface ColorConfig {\n /** Color for huge directories (>1GB) */\n huge: string;\n \n /** Color for large directories (>500MB) */\n large: string;\n \n /** Color for small directories (<100MB) */\n small: string;\n \n /** Color for stale/old directories */\n stale: string;\n \n /** Color for fresh directories */\n fresh: string;\n \n /** Color for selected items */\n selected: string;\n \n /** Color for errors */\n error: string;\n \n /** Color for success */\n success: string;\n \n /** Color for warnings */\n warning: string;\n \n /** Color for info/primary text */\n info: string;\n}\n\n/** Default color configuration using standard terminal colors */\nexport const DEFAULT_COLORS: ColorConfig = {\n huge: 'red',\n large: 'yellow',\n small: 'green',\n stale: 'gray',\n fresh: 'white',\n selected: 'cyan',\n error: 'red',\n success: 'green',\n warning: 'yellow',\n info: 'blue',\n};\n\n/**\n * Thresholds for size categorization in bytes.\n * Used to determine visual styling and smart selection rules.\n */\nexport const SIZE_THRESHOLDS = {\n /** 100 MB - upper bound for \"small\" category */\n SMALL: 100 * 1024 * 1024,\n /** 500 MB - upper bound for \"medium\" category */\n MEDIUM: 500 * 1024 * 1024,\n /** 1 GB - upper bound for \"large\" category */\n LARGE: 1024 * 1024 * 1024,\n} as const;\n\n/**\n * Thresholds for age categorization in days.\n * Used to identify stale node_modules that might be safe to delete.\n */\nexport const AGE_THRESHOLDS = {\n /** 7 days - still considered fresh */\n FRESH: 7,\n /** 30 days - warning threshold */\n RECENT: 30,\n /** 90 days - stale threshold */\n OLD: 90,\n} as const;\n","/**\n * Scanner module for discovering and analyzing node_modules directories\n * \n * This module handles the core scanning functionality:\n * - Recursive directory traversal\n * - Size calculation (recursive)\n * - Package.json parsing\n * - Metadata extraction\n * \n * All operations are async and non-blocking to keep the TUI responsive.\n */\n\nimport { promises as fs } from 'fs';\nimport { join, basename, dirname } from 'path';\nimport type { NodeModulesInfo, ScanOptions } from './types.js';\nimport {\n formatBytes,\n formatRelativeTime,\n getSizeCategory,\n getAgeCategory,\n readPackageJson,\n shouldExcludePath,\n fileExists,\n} from './utils.js';\n\n/**\n * Result of a directory scan operation.\n */\ninterface ScanResult {\n /** Discovered node_modules entries */\n nodeModules: NodeModulesInfo[];\n /** Number of directories scanned */\n directoriesScanned: number;\n /** Any errors encountered during scanning */\n errors: string[];\n}\n\n/**\n * Recursively scan for node_modules directories starting from root path.\n * \n * This is the main entry point for discovery. It walks the directory tree,\n * identifies node_modules folders, and collects metadata about each one.\n * \n * @param options - Scan configuration options\n * @param onProgress - Optional callback for progress updates (0-100)\n * @returns Scan results with all discovered node_modules\n */\nexport async function scanForNodeModules(\n options: ScanOptions,\n onProgress?: (progress: number) => void\n): Promise<ScanResult> {\n const result: ScanResult = {\n nodeModules: [],\n directoriesScanned: 0,\n errors: [],\n };\n\n const visitedPaths = new Set<string>();\n const pathsToScan: Array<{ path: string; depth: number }> = [\n { path: options.rootPath, depth: 0 },\n ];\n\n let processedCount = 0;\n let totalEstimate = 1; // Start with 1, will adjust as we discover\n\n while (pathsToScan.length > 0) {\n const { path: currentPath, depth } = pathsToScan.shift()!;\n\n // Skip if already visited or exceeds max depth\n if (visitedPaths.has(currentPath)) continue;\n if (options.maxDepth !== undefined && depth > options.maxDepth) continue;\n if (shouldExcludePath(currentPath, options.excludePatterns)) continue;\n\n visitedPaths.add(currentPath);\n result.directoriesScanned++;\n\n try {\n const entries = await fs.readdir(currentPath, { withFileTypes: true });\n \n // Check if current directory has node_modules\n const hasNodeModules = entries.some(\n entry => entry.isDirectory() && entry.name === 'node_modules'\n );\n\n if (hasNodeModules) {\n const nodeModulesPath = join(currentPath, 'node_modules');\n const info = await analyzeNodeModules(nodeModulesPath, currentPath);\n\n // Apply filters\n if (options.minSizeBytes && info.sizeBytes < options.minSizeBytes) {\n // Skip - too small\n } else if (\n options.olderThanDays &&\n getAgeInDays(info.lastModified) < options.olderThanDays\n ) {\n // Skip - too recent\n } else {\n result.nodeModules.push(info);\n }\n }\n\n // Add subdirectories to scan queue (excluding node_modules itself)\n for (const entry of entries) {\n if (\n entry.isDirectory() &&\n entry.name !== 'node_modules' &&\n !entry.name.startsWith('.')\n ) {\n const subPath = join(currentPath, entry.name);\n if (!shouldExcludePath(subPath, options.excludePatterns)) {\n pathsToScan.push({ path: subPath, depth: depth + 1 });\n totalEstimate++;\n }\n }\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n result.errors.push(`Error scanning ${currentPath}: ${errorMessage}`);\n }\n\n // Report progress\n processedCount++;\n if (onProgress) {\n const progress = Math.min(100, Math.round((processedCount / totalEstimate) * 100));\n onProgress(progress);\n }\n }\n\n // Ensure we report 100% at the end\n if (onProgress) {\n onProgress(100);\n }\n\n return result;\n}\n\n/**\n * Analyze a specific node_modules directory and extract all metadata.\n * \n * This function performs the heavy lifting of:\n * - Calculating total size (recursive)\n * - Counting packages\n * - Reading parent project info\n * - Determining age and size categories\n * \n * @param nodeModulesPath - Path to node_modules directory\n * @param projectPath - Path to parent project (containing package.json)\n * @returns Complete metadata for the node_modules\n */\nexport async function analyzeNodeModules(\n nodeModulesPath: string,\n projectPath: string\n): Promise<NodeModulesInfo> {\n // Get basic stats\n const stats = await fs.stat(nodeModulesPath);\n \n // Calculate size and count packages\n const { totalSize, packageCount, totalPackageCount } = await calculateDirectorySize(\n nodeModulesPath\n );\n\n // Read project info from package.json\n const packageJson = await readPackageJson(projectPath);\n const projectName = packageJson?.name || basename(projectPath);\n const projectVersion = packageJson?.version;\n\n // Determine categories\n const sizeCategory = getSizeCategory(totalSize);\n const ageCategory = getAgeCategory(stats.mtime);\n\n return {\n path: nodeModulesPath,\n projectPath,\n projectName,\n projectVersion,\n sizeBytes: totalSize,\n sizeFormatted: formatBytes(totalSize),\n packageCount,\n totalPackageCount,\n lastModified: stats.mtime,\n lastModifiedFormatted: formatRelativeTime(stats.mtime),\n selected: false,\n isFavorite: false,\n ageCategory,\n sizeCategory,\n };\n}\n\n/**\n * Recursively calculate directory size and package counts.\n * \n * This is an expensive operation for large node_modules directories.\n * We optimize by:\n * - Using iterative approach (avoid stack overflow)\n * - Counting only top-level packages for packageCount\n * - Counting all packages for totalPackageCount\n * \n * @param dirPath - Directory to analyze\n * @returns Size in bytes and package counts\n */\nasync function calculateDirectorySize(dirPath: string): Promise<{\n totalSize: number;\n packageCount: number;\n totalPackageCount: number;\n}> {\n let totalSize = 0;\n let packageCount = 0;\n let totalPackageCount = 0;\n let isTopLevel = true;\n\n const pathsToProcess: string[] = [dirPath];\n const processedPaths = new Set<string>();\n\n while (pathsToProcess.length > 0) {\n const currentPath = pathsToProcess.pop()!;\n \n if (processedPaths.has(currentPath)) continue;\n processedPaths.add(currentPath);\n\n try {\n const stats = await fs.stat(currentPath);\n \n if (stats.isFile()) {\n totalSize += stats.size;\n } else if (stats.isDirectory()) {\n // Add directory entry size (approximate)\n totalSize += 4096; // Typical directory entry size\n \n // Count packages at top level only\n if (isTopLevel && currentPath !== dirPath) {\n const entryName = basename(currentPath);\n // Skip hidden directories and special directories\n if (!entryName.startsWith('.') && entryName !== '.bin') {\n packageCount++;\n }\n }\n \n // Count all packages for total\n if (currentPath !== dirPath) {\n const entryName = basename(currentPath);\n if (!entryName.startsWith('.') && entryName !== '.bin') {\n totalPackageCount++;\n }\n }\n\n // Read directory contents\n try {\n const entries = await fs.readdir(currentPath, { withFileTypes: true });\n for (const entry of entries) {\n const entryPath = join(currentPath, entry.name);\n pathsToProcess.push(entryPath);\n }\n } catch {\n // Permission denied or other error - skip this directory\n }\n } else if (stats.isSymbolicLink()) {\n // Skip symbolic links to avoid cycles\n }\n } catch {\n // File not accessible - skip\n }\n\n if (currentPath === dirPath) {\n isTopLevel = false;\n }\n }\n\n return { totalSize, packageCount, totalPackageCount };\n}\n\n/**\n * Helper to get age in days from a date.\n */\nfunction getAgeInDays(date: Date): number {\n const now = new Date();\n return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));\n}\n\n/**\n * Load ignore patterns from .onmignore file.\n * \n * Looks for .onmignore in:\n * 1. Current working directory\n * 2. Home directory\n * \n * @returns Array of ignore patterns\n */\nexport async function loadIgnorePatterns(): Promise<string[]> {\n const patterns: string[] = [\n '**/node_modules/**', // Never scan inside node_modules\n '**/.git/**',\n '**/.*', // Hidden directories\n ];\n\n const ignoreFiles = [\n join(process.cwd(), '.onmignore'),\n join(process.env.HOME || process.cwd(), '.onmignore'),\n ];\n\n for (const ignoreFile of ignoreFiles) {\n try {\n if (await fileExists(ignoreFile)) {\n const content = await fs.readFile(ignoreFile, 'utf-8');\n const lines = content\n .split('\\n')\n .map(line => line.trim())\n .filter(line => line && !line.startsWith('#'));\n patterns.push(...lines);\n }\n } catch {\n // Ignore errors reading ignore files\n }\n }\n\n return patterns;\n}\n\n/**\n * Load favorites list from .onmfavorites file.\n * \n * Favorites are projects that should never be suggested for deletion.\n * \n * @returns Set of favorite project paths\n */\nexport async function loadFavorites(): Promise<Set<string>> {\n const favorites = new Set<string>();\n\n const favoritesFile = join(process.env.HOME || process.cwd(), '.onmfavorites');\n\n try {\n if (await fileExists(favoritesFile)) {\n const content = await fs.readFile(favoritesFile, 'utf-8');\n const lines = content\n .split('\\n')\n .map(line => line.trim())\n .filter(line => line && !line.startsWith('#'));\n \n for (const line of lines) {\n favorites.add(line);\n }\n }\n } catch {\n // Ignore errors reading favorites\n }\n\n return favorites;\n}\n\n/**\n * Check if a node_modules directory is currently in use.\n * \n * This is a safety check to prevent deleting node_modules that\n * might be actively being used by a running process.\n * \n * Note: This is a best-effort check and may not catch all cases.\n * \n * @param path - Path to node_modules\n * @returns True if potentially in use\n */\nexport async function isNodeModulesInUse(path: string): Promise<boolean> {\n // This is a simplified check - in production, you might want to:\n // 1. Check for lock files\n // 2. Check for running node processes using this path\n // 3. Check for open file handles\n \n try {\n const lockFiles = ['.package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];\n const projectPath = dirname(path);\n \n for (const lockFile of lockFiles) {\n const lockPath = join(projectPath, lockFile);\n try {\n const stats = await fs.stat(lockPath);\n // If lock file was modified in the last minute, might be in use\n const oneMinuteAgo = Date.now() - 60 * 1000;\n if (stats.mtime.getTime() > oneMinuteAgo) {\n return true;\n }\n } catch {\n // Lock file doesn't exist - that's fine\n }\n }\n } catch {\n // Error checking - assume not in use\n }\n\n return false;\n}\n\n/**\n * Quick scan mode - just report without full metadata.\n * \n * Faster than full scan when you just need a quick overview.\n * \n * @param rootPath - Root directory to scan\n * @returns Basic info about found node_modules\n */\nexport async function quickScan(rootPath: string): Promise<Array<{\n path: string;\n projectPath: string;\n projectName: string;\n}>> {\n const results: Array<{ path: string; projectPath: string; projectName: string }> = [];\n const visitedPaths = new Set<string>();\n const pathsToScan = [rootPath];\n\n while (pathsToScan.length > 0) {\n const currentPath = pathsToScan.pop()!;\n \n if (visitedPaths.has(currentPath)) continue;\n visitedPaths.add(currentPath);\n\n try {\n const entries = await fs.readdir(currentPath, { withFileTypes: true });\n \n const hasNodeModules = entries.some(\n entry => entry.isDirectory() && entry.name === 'node_modules'\n );\n\n if (hasNodeModules) {\n const projectPath = currentPath;\n const nodeModulesPath = join(currentPath, 'node_modules');\n const packageJson = await readPackageJson(projectPath);\n \n results.push({\n path: nodeModulesPath,\n projectPath,\n projectName: packageJson?.name || basename(projectPath),\n });\n }\n\n // Add subdirectories\n for (const entry of entries) {\n if (\n entry.isDirectory() &&\n entry.name !== 'node_modules' &&\n !entry.name.startsWith('.')\n ) {\n pathsToScan.push(join(currentPath, entry.name));\n }\n }\n } catch {\n // Skip directories we can't read\n }\n }\n\n return results;\n}\n","/**\n * Deletion module for safely removing node_modules directories\n * \n * This module handles:\n * - Safe deletion with confirmations\n * - Dry run mode\n * - Progress tracking\n * - Error handling\n * - In-use detection\n * \n * Safety is the priority - we never delete without explicit confirmation\n * and we check for potential issues before proceeding.\n */\n\nimport { promises as fs } from 'fs';\nimport { join } from 'path';\nimport type { NodeModulesInfo, DeleteOptions, DeletionResult, DeletionDetail } from './types.js';\nimport { formatBytes, fileExists } from './utils.js';\nimport { isNodeModulesInUse } from './scanner.js';\n\n/**\n * Delete selected node_modules directories.\n * \n * This is the main entry point for deletion operations. It:\n * 1. Filters to only selected items\n * 2. Performs safety checks\n * 3. Deletes each node_modules (or simulates in dry run)\n * 4. Collects results and statistics\n * \n * @param nodeModules - List of all node_modules (selected ones will be deleted)\n * @param options - Deletion options\n * @param onProgress - Optional callback for progress updates\n * @returns Deletion results with statistics\n */\nexport async function deleteSelectedNodeModules(\n nodeModules: NodeModulesInfo[],\n options: DeleteOptions,\n onProgress?: (current: number, total: number, currentPath: string) => void\n): Promise<DeletionResult> {\n const selected = nodeModules.filter(nm => nm.selected);\n \n const result: DeletionResult = {\n totalAttempted: selected.length,\n successful: 0,\n failed: 0,\n bytesFreed: 0,\n formattedBytesFreed: '0 B',\n details: [],\n };\n\n for (let i = 0; i < selected.length; i++) {\n const item = selected[i];\n \n if (onProgress) {\n onProgress(i + 1, selected.length, item.projectName);\n }\n\n const detail = await deleteNodeModules(item, options);\n result.details.push(detail);\n\n if (detail.success) {\n result.successful++;\n result.bytesFreed += item.sizeBytes;\n } else {\n result.failed++;\n }\n }\n\n result.formattedBytesFreed = formatBytes(result.bytesFreed);\n return result;\n}\n\n/**\n * Delete a single node_modules directory.\n * \n * Performs safety checks before deletion:\n * - Verifies it's actually a node_modules directory\n * - Checks if it's in use (if enabled)\n * - Verifies the path is valid\n * \n * @param nodeModules - NodeModulesInfo to delete\n * @param options - Deletion options\n * @returns Detailed result of the deletion\n */\nasync function deleteNodeModules(\n nodeModules: NodeModulesInfo,\n options: DeleteOptions\n): Promise<DeletionDetail> {\n const startTime = Date.now();\n \n const detail: DeletionDetail = {\n nodeModules,\n success: false,\n durationMs: 0,\n };\n\n try {\n // Safety check 1: Verify path ends with node_modules\n if (!nodeModules.path.endsWith('node_modules')) {\n detail.error = 'Path does not appear to be a node_modules directory';\n detail.durationMs = Date.now() - startTime;\n return detail;\n }\n\n // Safety check 2: Verify directory exists\n if (!(await fileExists(nodeModules.path))) {\n detail.error = 'Directory does not exist';\n detail.durationMs = Date.now() - startTime;\n return detail;\n }\n\n // Safety check 3: Check if in use\n if (options.checkRunningProcesses) {\n const inUse = await isNodeModulesInUse(nodeModules.path);\n if (inUse) {\n detail.error = 'Directory appears to be in use by a running process';\n detail.durationMs = Date.now() - startTime;\n return detail;\n }\n }\n\n // Safety check 4: Verify it looks like a real node_modules\n const isValidNodeModules = await verifyNodeModules(nodeModules.path);\n if (!isValidNodeModules) {\n detail.error = 'Directory does not appear to be a valid node_modules';\n detail.durationMs = Date.now() - startTime;\n return detail;\n }\n\n // Perform deletion (or simulate)\n if (options.dryRun) {\n // In dry run, just simulate success\n detail.success = true;\n } else {\n // Actually delete the directory\n await fs.rm(nodeModules.path, { recursive: true, force: true });\n detail.success = true;\n }\n\n detail.durationMs = Date.now() - startTime;\n } catch (error) {\n detail.error = error instanceof Error ? error.message : String(error);\n detail.durationMs = Date.now() - startTime;\n }\n\n return detail;\n}\n\n/**\n * Verify that a directory looks like a real node_modules.\n * \n * We check for:\n * - Directory name is exactly \"node_modules\"\n * - Contains at least one subdirectory (package)\n * - Parent directory contains package.json\n * \n * These checks prevent accidental deletion of similarly named directories.\n * \n * @param path - Path to verify\n * @returns True if it looks like a valid node_modules\n */\nasync function verifyNodeModules(path: string): Promise<boolean> {\n try {\n // Check name\n const parts = path.split('/');\n if (parts[parts.length - 1] !== 'node_modules') {\n return false;\n }\n\n // Check it has contents (not empty)\n const entries = await fs.readdir(path);\n const hasSubdirs = entries.some(async entry => {\n const entryPath = join(path, entry);\n const stats = await fs.stat(entryPath);\n return stats.isDirectory();\n });\n\n // Parent should have package.json\n const parentPath = path.replace(/\\/node_modules$/, '').replace(/\\\\node_modules$/, '');\n const hasPackageJson = await fileExists(join(parentPath, 'package.json'));\n\n return hasSubdirs || hasPackageJson;\n } catch {\n return false;\n }\n}\n\n/**\n * Generate a preview report of what would be deleted.\n * \n * Used for dry run mode and confirmation prompts.\n * \n * @param nodeModules - All node_modules items\n * @returns Formatted report string\n */\nexport function generateDeletionPreview(nodeModules: NodeModulesInfo[]): string {\n const selected = nodeModules.filter(nm => nm.selected);\n \n if (selected.length === 0) {\n return 'No node_modules selected for deletion.';\n }\n\n const totalBytes = selected.reduce((sum, nm) => sum + nm.sizeBytes, 0);\n \n let report = `\\n⚠️ You are about to delete ${selected.length} node_modules director${selected.length === 1 ? 'y' : 'ies'}:\\n\\n`;\n \n for (const nm of selected) {\n const shortPath = nm.path.replace(process.cwd(), '.');\n report += ` • ${shortPath} (${nm.sizeFormatted})\\n`;\n }\n \n report += `\\n Total space to reclaim: ${formatBytes(totalBytes)}\\n`;\n \n return report;\n}\n\n/**\n * Generate a JSON report of deletion results.\n * \n * @param result - Deletion result\n * @returns JSON string\n */\nexport function generateJSONReport(result: DeletionResult): string {\n return JSON.stringify({\n summary: {\n totalAttempted: result.totalAttempted,\n successful: result.successful,\n failed: result.failed,\n bytesFreed: result.bytesFreed,\n formattedBytesFreed: result.formattedBytesFreed,\n },\n details: result.details.map(d => ({\n path: d.nodeModules.path,\n projectName: d.nodeModules.projectName,\n sizeBytes: d.nodeModules.sizeBytes,\n sizeFormatted: d.nodeModules.sizeFormatted,\n success: d.success,\n error: d.error,\n durationMs: d.durationMs,\n })),\n }, null, 2);\n}\n\n/**\n * Select node_modules by size criteria.\n * \n * Helper for \"select all >500MB\" functionality.\n * \n * @param nodeModules - All node_modules\n * @param minSizeBytes - Minimum size in bytes\n * @returns Updated array with selections\n */\nexport function selectBySize(\n nodeModules: NodeModulesInfo[],\n minSizeBytes: number\n): NodeModulesInfo[] {\n return nodeModules.map(nm => \n nm.sizeBytes >= minSizeBytes ? { ...nm, selected: true } : nm\n );\n}\n\n/**\n * Select node_modules by age criteria.\n * \n * Helper for \"select all older than X days\" functionality.\n * \n * @param nodeModules - All node_modules\n * @param minAgeDays - Minimum age in days\n * @returns Updated array with selections\n */\nexport function selectByAge(\n nodeModules: NodeModulesInfo[],\n minAgeDays: number\n): NodeModulesInfo[] {\n const now = new Date();\n return nodeModules.map(nm => {\n const ageDays = Math.floor((now.getTime() - nm.lastModified.getTime()) / (1000 * 60 * 60 * 24));\n return ageDays >= minAgeDays ? { ...nm, selected: true } : nm;\n });\n}\n\n/**\n * Select all node_modules.\n * \n * @param nodeModules - All node_modules\n * @param selected - Whether to select (true) or deselect (false)\n * @returns Updated array\n */\nexport function selectAll(\n nodeModules: NodeModulesInfo[],\n selected: boolean\n): NodeModulesInfo[] {\n return nodeModules.map(nm => ({ ...nm, selected }));\n}\n\n/**\n * Invert selection.\n * \n * @param nodeModules - All node_modules\n * @returns Updated array with inverted selections\n */\nexport function invertSelection(nodeModules: NodeModulesInfo[]): NodeModulesInfo[] {\n return nodeModules.map(nm => ({ ...nm, selected: !nm.selected }));\n}\n","/**\n * Utility functions for oh-my-node-modules\n * \n * This module provides pure functions for common operations like\n * formatting bytes, parsing sizes, and date calculations.\n * Pure functions make testing easier and reduce side effects.\n */\n\nimport { promises as fs } from 'fs';\nimport { join } from 'path';\nimport type { \n NodeModulesInfo, \n AgeCategory, \n SizeCategory,\n SortOption \n} from './types.js';\nimport { SIZE_THRESHOLDS, AGE_THRESHOLDS } from './types.js';\n\n// Re-export for convenience\nexport { SIZE_THRESHOLDS, AGE_THRESHOLDS };\n\n/**\n * Statistics calculated from a list of node_modules.\n * Used for summary displays and overview headers.\n */\nexport interface ScanStatistics {\n /** Total number of projects found */\n totalProjects: number;\n \n /** Total number of node_modules directories */\n totalNodeModules: number;\n \n /** Total size of all node_modules in bytes */\n totalSizeBytes: number;\n \n /** Total size formatted as human-readable string */\n totalSizeFormatted: string;\n \n /** Number of selected node_modules */\n selectedCount: number;\n \n /** Total size of selected node_modules */\n selectedSizeBytes: number;\n \n /** Total size of selected formatted */\n selectedSizeFormatted: string;\n \n /** Average age in days */\n averageAgeDays: number;\n \n /** Number of stale node_modules (>30 days) */\n staleCount: number;\n}\n\n/**\n * Format bytes into human-readable string.\n * Uses binary units (MiB, GiB) for accuracy.\n * \n * @param bytes - Number of bytes to format\n * @returns Formatted string like \"1.2 GB\" or \"456 MB\"\n */\nexport function formatBytes(bytes: number): string {\n if (bytes === 0) return '0 B';\n \n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n const base = 1024;\n const exponent = Math.floor(Math.log(bytes) / Math.log(base));\n const unit = units[Math.min(exponent, units.length - 1)];\n const value = bytes / Math.pow(base, exponent);\n \n // Show 1 decimal place for MB and above, 0 for smaller\n const decimals = exponent >= 2 ? 1 : 0;\n return `${value.toFixed(decimals)} ${unit}`;\n}\n\n/**\n * Parse human-readable size string into bytes.\n * Supports formats like \"1gb\", \"500MB\", \"10mb\"\n * \n * @param sizeStr - Size string to parse\n * @returns Size in bytes, or undefined if invalid\n */\nexport function parseSize(sizeStr: string): number | undefined {\n const match = sizeStr.trim().toLowerCase().match(/^(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb|gb|tb)?$/);\n if (!match) return undefined;\n \n const value = parseFloat(match[1]);\n const unit = match[2] || 'b';\n \n const multipliers: Record<string, number> = {\n b: 1,\n kb: 1024,\n mb: 1024 * 1024,\n gb: 1024 * 1024 * 1024,\n tb: 1024 * 1024 * 1024 * 1024,\n };\n \n return Math.floor(value * (multipliers[unit] || 1));\n}\n\n/**\n * Format a date into \"X days ago\" string.\n * Provides more readable relative time than raw dates.\n * \n * @param date - Date to format\n * @returns Formatted string like \"30d ago\" or \"2d ago\"\n */\nexport function formatRelativeTime(date: Date): string {\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n \n if (diffDays === 0) {\n const diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n if (diffHours === 0) {\n const diffMinutes = Math.floor(diffMs / (1000 * 60));\n return diffMinutes <= 1 ? 'just now' : `${diffMinutes}m ago`;\n }\n return `${diffHours}h ago`;\n }\n \n if (diffDays < 30) return `${diffDays}d ago`;\n if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;\n return `${Math.floor(diffDays / 365)}y ago`;\n}\n\n/**\n * Determine size category based on bytes.\n * Used for color coding and smart selection.\n * \n * @param bytes - Size in bytes\n * @returns Size category\n */\nexport function getSizeCategory(bytes: number): SizeCategory {\n if (bytes > SIZE_THRESHOLDS.LARGE) return 'huge';\n if (bytes > SIZE_THRESHOLDS.MEDIUM) return 'large';\n if (bytes > SIZE_THRESHOLDS.SMALL) return 'medium';\n return 'small';\n}\n\n/**\n * Determine age category based on days since modification.\n * Used to identify potentially stale node_modules.\n * \n * @param lastModified - Last modification date\n * @returns Age category\n */\nexport function getAgeCategory(lastModified: Date): AgeCategory {\n const now = new Date();\n const diffDays = Math.floor((now.getTime() - lastModified.getTime()) / (1000 * 60 * 60 * 24));\n \n if (diffDays > AGE_THRESHOLDS.OLD) return 'stale';\n if (diffDays > AGE_THRESHOLDS.RECENT) return 'old';\n if (diffDays > AGE_THRESHOLDS.FRESH) return 'recent';\n return 'fresh';\n}\n\n/**\n * Calculate age in days from a date.\n * \n * @param date - Date to calculate age from\n * @returns Number of days\n */\nexport function getAgeInDays(date: Date): number {\n const now = new Date();\n return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));\n}\n\n/**\n * Sort node_modules by the specified option.\n * Pure function that returns a new sorted array.\n * \n * @param items - Array to sort\n * @param sortBy - Sort option\n * @returns New sorted array\n */\nexport function sortNodeModules(\n items: NodeModulesInfo[],\n sortBy: SortOption\n): NodeModulesInfo[] {\n const sorted = [...items]; // Create copy to avoid mutation\n \n switch (sortBy) {\n case 'size-desc':\n return sorted.sort((a, b) => b.sizeBytes - a.sizeBytes);\n case 'size-asc':\n return sorted.sort((a, b) => a.sizeBytes - b.sizeBytes);\n case 'date-desc':\n return sorted.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());\n case 'date-asc':\n return sorted.sort((a, b) => a.lastModified.getTime() - b.lastModified.getTime());\n case 'name-asc':\n return sorted.sort((a, b) => a.projectName.localeCompare(b.projectName));\n case 'name-desc':\n return sorted.sort((a, b) => b.projectName.localeCompare(a.projectName));\n case 'packages-desc':\n return sorted.sort((a, b) => b.totalPackageCount - a.totalPackageCount);\n case 'packages-asc':\n return sorted.sort((a, b) => a.totalPackageCount - b.totalPackageCount);\n default:\n return sorted;\n }\n}\n\n/**\n * Filter node_modules by search query.\n * Matches against project name and path.\n * \n * @param items - Array to filter\n * @param query - Search query (case-insensitive)\n * @returns Filtered array\n */\nexport function filterNodeModules(\n items: NodeModulesInfo[],\n query: string\n): NodeModulesInfo[] {\n if (!query.trim()) return items;\n \n const lowerQuery = query.toLowerCase();\n return items.filter(item => \n item.projectName.toLowerCase().includes(lowerQuery) ||\n item.path.toLowerCase().includes(lowerQuery)\n );\n}\n\n/**\n * Calculate statistics from node_modules list.\n * Used for overview displays and summaries.\n * \n * @param items - Node modules to analyze\n * @returns Calculated statistics\n */\nexport function calculateStatistics(items: NodeModulesInfo[]): ScanStatistics {\n const selectedItems = items.filter(item => item.selected);\n const totalSize = items.reduce((sum, item) => sum + item.sizeBytes, 0);\n const selectedSize = selectedItems.reduce((sum, item) => sum + item.sizeBytes, 0);\n \n const totalAge = items.reduce((sum, item) => {\n return sum + getAgeInDays(item.lastModified);\n }, 0);\n \n const staleCount = items.filter(item => item.ageCategory === 'stale').length;\n \n return {\n totalProjects: new Set(items.map(item => item.projectPath)).size,\n totalNodeModules: items.length,\n totalSizeBytes: totalSize,\n totalSizeFormatted: formatBytes(totalSize),\n selectedCount: selectedItems.length,\n selectedSizeBytes: selectedSize,\n selectedSizeFormatted: formatBytes(selectedSize),\n averageAgeDays: items.length > 0 ? Math.round(totalAge / items.length) : 0,\n staleCount,\n };\n}\n\n/**\n * Check if a path should be excluded based on patterns.\n * Supports glob-like patterns with * and ? wildcards.\n * \n * @param path - Path to check\n * @param patterns - Exclusion patterns\n * @returns True if path should be excluded\n */\nexport function shouldExcludePath(path: string, patterns: string[]): boolean {\n return patterns.some(pattern => {\n // Convert glob pattern to regex\n const regexPattern = pattern\n .replace(/\\*\\*/g, '{{GLOBSTAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/\\?/g, '.')\n .replace(/\\{\\{GLOBSTAR\\}\\}/g, '.*');\n \n const regex = new RegExp(regexPattern, 'i');\n return regex.test(path);\n });\n}\n\n/**\n * Toggle selection state for a node_modules item.\n * Returns new array with toggled item (immutable update).\n * \n * @param items - Current items array\n * @param index - Index of item to toggle\n * @returns New array with toggled selection\n */\nexport function toggleSelection(\n items: NodeModulesInfo[],\n index: number\n): NodeModulesInfo[] {\n if (index < 0 || index >= items.length) return items;\n \n return items.map((item, i) => \n i === index ? { ...item, selected: !item.selected } : item\n );\n}\n\n/**\n * Select or deselect all items matching a predicate.\n * Useful for \"select all >500MB\" or \"select all stale\" operations.\n * \n * @param items - Current items array\n * @param predicate - Function to determine which items to select\n * @param selected - Whether to select (true) or deselect (false)\n * @returns New array with updated selections\n */\nexport function selectByPredicate(\n items: NodeModulesInfo[],\n predicate: (item: NodeModulesInfo) => boolean,\n selected: boolean\n): NodeModulesInfo[] {\n return items.map(item => \n predicate(item) ? { ...item, selected } : item\n );\n}\n\n/**\n * Get a color for a size category.\n * Used for consistent visual feedback across the TUI.\n * \n * @param category - Size category\n * @returns Color name for Ink Text component\n */\nexport function getSizeColor(category: SizeCategory): string {\n switch (category) {\n case 'huge': return 'red';\n case 'large': return 'yellow';\n case 'medium': return 'cyan';\n case 'small': return 'green';\n default: return 'white';\n }\n}\n\n/**\n * Get a color for an age category.\n * \n * @param category - Age category\n * @returns Color name for Ink Text component\n */\nexport function getAgeColor(category: AgeCategory): string {\n switch (category) {\n case 'stale': return 'gray';\n case 'old': return 'yellow';\n case 'recent': return 'cyan';\n case 'fresh': return 'green';\n default: return 'white';\n }\n}\n\n/**\n * Truncate a string to fit within a maximum length.\n * Adds ellipsis if truncated.\n * \n * @param str - String to truncate\n * @param maxLength - Maximum length\n * @returns Truncated string\n */\nexport function truncate(str: string, maxLength: number): string {\n if (str.length <= maxLength) return str;\n if (maxLength <= 3) return str.slice(0, maxLength);\n return str.slice(0, maxLength - 3) + '...';\n}\n\n/**\n * Safe file existence check that doesn't throw.\n * Useful for checking if package.json exists before parsing.\n * \n * @param path - Path to check\n * @returns True if file exists\n */\nexport async function fileExists(path: string): Promise<boolean> {\n try {\n await fs.access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Read and parse package.json safely.\n * Returns undefined if file doesn't exist or is invalid.\n * \n * @param projectPath - Path to project directory\n * @returns Parsed package.json or undefined\n */\nexport async function readPackageJson(\n projectPath: string\n): Promise<{ name?: string; version?: string } | undefined> {\n const packagePath = join(projectPath, 'package.json');\n \n try {\n const content = await fs.readFile(packagePath, 'utf-8');\n const parsed = JSON.parse(content) as { name?: string; version?: string };\n return parsed;\n } catch {\n return undefined;\n }\n}\n","/**\n * oh-my-node-modules - Public API\n * \n * This module exports the public API for programmatic use.\n * Most users will use the CLI, but the API is available for\n * integration with other tools.\n * \n * @example\n * ```typescript\n * import { scanForNodeModules, deleteSelectedNodeModules } from 'oh-my-node-modules';\n * \n * const result = await scanForNodeModules({\n * rootPath: '/path/to/projects',\n * excludePatterns: [],\n * followSymlinks: false,\n * });\n * \n * // ... process results ...\n * ```\n */\n\n// Core types\nexport type {\n NodeModulesInfo,\n AgeCategory,\n SizeCategory,\n SortOption,\n ScanOptions,\n DeleteOptions,\n DeletionResult,\n DeletionDetail,\n AppState,\n CliArgs,\n ScanStatistics,\n ColorConfig,\n} from './types.js';\n\n// Core functions\nexport { scanForNodeModules, analyzeNodeModules, quickScan, loadIgnorePatterns, loadFavorites, isNodeModulesInUse } from './scanner.js';\nexport { deleteSelectedNodeModules, generateDeletionPreview, generateJSONReport, selectBySize, selectByAge, selectAll, invertSelection } from './deletion.js';\nexport {\n formatBytes,\n parseSize,\n formatRelativeTime,\n getSizeCategory,\n getAgeCategory,\n sortNodeModules,\n filterNodeModules,\n calculateStatistics,\n toggleSelection,\n selectByPredicate,\n} from './utils.js';\n\n// Constants\nexport { SIZE_THRESHOLDS, AGE_THRESHOLDS, DEFAULT_COLORS } from './types.js';\n\n// Version\nexport const VERSION = '1.0.0';\n"],"names":["ScanStatistics"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,IAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,WAAA;AACA;AACA,kBAAA,YAAA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,WAAA;AACP;AACA;AACA;AACA;AACO,KAAA,YAAA;AACP;AACA;AACA;AACA;AACO,KAAA,UAAA;AACP;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,aAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,cAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAA,cAAA;AACA;AACA;AACA;AACA;AACO,UAAA,cAAA;AACP;AACA,iBAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,QAAA;AACP;AACA,iBAAA,eAAA;AACA;AACA;AACA;AACA,YAAA,UAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAMA;AACA;AACA;AACA;AACO,UAAA,OAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,cAAA,EAAA,WAAA;AACP;AACA;AACA;AACA;AACO,cAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,cAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;;ACxPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,UAAA,UAAA;AACA;AACA,iBAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,kBAAA,UAAA,WAAA,4CAAA,OAAA,CAAA,UAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,kBAAA,gDAAA,OAAA,CAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,kBAAA,IAAA,OAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,aAAA,IAAA,OAAA,CAAA,GAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,kBAAA,gBAAA,OAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,SAAA,oBAAA,OAAA,CAAA,KAAA;AACP;AACA;AACA;AACA;;AC1FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,yBAAA,cAAA,eAAA,aAAA,aAAA,+EAAA,OAAA,CAAA,cAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,uBAAA,cAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,kBAAA,SAAA,cAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,YAAA,cAAA,eAAA,2BAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,WAAA,cAAA,eAAA,yBAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,SAAA,cAAA,eAAA,wBAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,eAAA,cAAA,eAAA,KAAA,eAAA;;AC/EP;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA;AACA;AACA;AACA;AACO,UAAA,cAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,SAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,kBAAA,OAAA,IAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,eAAA,iBAAA,YAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,cAAA,eAAA,IAAA,GAAA,WAAA;AAQP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,eAAA,QAAA,eAAA,YAAA,UAAA,GAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,iBAAA,QAAA,eAAA,oBAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,mBAAA,QAAA,eAAA,KAAA,cAAA;AAUP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,eAAA,QAAA,eAAA,oBAAA,eAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,iBAAA,QAAA,eAAA,sBAAA,eAAA,kCAAA,eAAA;;ACtIP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAMO,cAAA,OAAA;;;;"}