appclean 1.8.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.
Files changed (154) hide show
  1. package/.github/workflows/publish.yml +41 -0
  2. package/.github/workflows/test.yml +37 -0
  3. package/ACTION_CHECKLIST.md +342 -0
  4. package/APPCLEAN_SUMMARY.md +309 -0
  5. package/CHANGELOG.md +205 -0
  6. package/CODE_OF_CONDUCT.md +49 -0
  7. package/CODE_REVIEW_REPORT.md +447 -0
  8. package/COMMUNITY_POSTS.md +307 -0
  9. package/CONTRIBUTING.md +121 -0
  10. package/DEPLOYMENT_GUIDE.md +345 -0
  11. package/DEPLOYMENT_STATUS.md +182 -0
  12. package/EXECUTIVE_REPORT.md +393 -0
  13. package/GITHUB_OPTIMIZATION.md +383 -0
  14. package/INDEX.md +165 -0
  15. package/LICENSE +21 -0
  16. package/MARKETING_SUMMARY.md +352 -0
  17. package/NPM_PACKAGE_OPTIMIZATION.md +281 -0
  18. package/NPM_PUBLISH.md +116 -0
  19. package/PROJECT_SUMMARY.txt +249 -0
  20. package/QUICKSTART.md +219 -0
  21. package/README.md +548 -0
  22. package/SECURITY.md +104 -0
  23. package/SETUP_GITHUB.md +237 -0
  24. package/TESTING_SUMMARY.md +379 -0
  25. package/dist/core/appUpdateChecker.d.ts +23 -0
  26. package/dist/core/appUpdateChecker.d.ts.map +1 -0
  27. package/dist/core/appUpdateChecker.js +159 -0
  28. package/dist/core/appUpdateChecker.js.map +1 -0
  29. package/dist/core/detector.d.ts +13 -0
  30. package/dist/core/detector.d.ts.map +1 -0
  31. package/dist/core/detector.js +99 -0
  32. package/dist/core/detector.js.map +1 -0
  33. package/dist/core/duplicateFileFinder.d.ts +14 -0
  34. package/dist/core/duplicateFileFinder.d.ts.map +1 -0
  35. package/dist/core/duplicateFileFinder.js +80 -0
  36. package/dist/core/duplicateFileFinder.js.map +1 -0
  37. package/dist/core/orphanedDependencyDetector.d.ts +19 -0
  38. package/dist/core/orphanedDependencyDetector.d.ts.map +1 -0
  39. package/dist/core/orphanedDependencyDetector.js +148 -0
  40. package/dist/core/orphanedDependencyDetector.js.map +1 -0
  41. package/dist/core/performanceOptimizer.d.ts +37 -0
  42. package/dist/core/performanceOptimizer.d.ts.map +1 -0
  43. package/dist/core/performanceOptimizer.js +128 -0
  44. package/dist/core/performanceOptimizer.js.map +1 -0
  45. package/dist/core/permissionHandler.d.ts +9 -0
  46. package/dist/core/permissionHandler.d.ts.map +1 -0
  47. package/dist/core/permissionHandler.js +89 -0
  48. package/dist/core/permissionHandler.js.map +1 -0
  49. package/dist/core/pluginSystem.d.ts +39 -0
  50. package/dist/core/pluginSystem.d.ts.map +1 -0
  51. package/dist/core/pluginSystem.js +120 -0
  52. package/dist/core/pluginSystem.js.map +1 -0
  53. package/dist/core/removalRecorder.d.ts +32 -0
  54. package/dist/core/removalRecorder.d.ts.map +1 -0
  55. package/dist/core/removalRecorder.js +79 -0
  56. package/dist/core/removalRecorder.js.map +1 -0
  57. package/dist/core/remover.d.ts +15 -0
  58. package/dist/core/remover.d.ts.map +1 -0
  59. package/dist/core/remover.js +225 -0
  60. package/dist/core/remover.js.map +1 -0
  61. package/dist/core/reportGenerator.d.ts +9 -0
  62. package/dist/core/reportGenerator.d.ts.map +1 -0
  63. package/dist/core/reportGenerator.js +328 -0
  64. package/dist/core/reportGenerator.js.map +1 -0
  65. package/dist/core/scheduledCleanup.d.ts +38 -0
  66. package/dist/core/scheduledCleanup.d.ts.map +1 -0
  67. package/dist/core/scheduledCleanup.js +127 -0
  68. package/dist/core/scheduledCleanup.js.map +1 -0
  69. package/dist/core/serviceFileDetector.d.ts +18 -0
  70. package/dist/core/serviceFileDetector.d.ts.map +1 -0
  71. package/dist/core/serviceFileDetector.js +136 -0
  72. package/dist/core/serviceFileDetector.js.map +1 -0
  73. package/dist/core/verificationModule.d.ts +14 -0
  74. package/dist/core/verificationModule.d.ts.map +1 -0
  75. package/dist/core/verificationModule.js +102 -0
  76. package/dist/core/verificationModule.js.map +1 -0
  77. package/dist/index.d.ts +3 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +333 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/managers/brewManager.d.ts +10 -0
  82. package/dist/managers/brewManager.d.ts.map +1 -0
  83. package/dist/managers/brewManager.js +130 -0
  84. package/dist/managers/brewManager.js.map +1 -0
  85. package/dist/managers/customManager.d.ts +8 -0
  86. package/dist/managers/customManager.d.ts.map +1 -0
  87. package/dist/managers/customManager.js +139 -0
  88. package/dist/managers/customManager.js.map +1 -0
  89. package/dist/managers/linuxManager.d.ts +10 -0
  90. package/dist/managers/linuxManager.d.ts.map +1 -0
  91. package/dist/managers/linuxManager.js +191 -0
  92. package/dist/managers/linuxManager.js.map +1 -0
  93. package/dist/managers/npmManager.d.ts +10 -0
  94. package/dist/managers/npmManager.d.ts.map +1 -0
  95. package/dist/managers/npmManager.js +119 -0
  96. package/dist/managers/npmManager.js.map +1 -0
  97. package/dist/types/index.d.ts +44 -0
  98. package/dist/types/index.d.ts.map +1 -0
  99. package/dist/types/index.js +3 -0
  100. package/dist/types/index.js.map +1 -0
  101. package/dist/ui/guiServer.d.ts +10 -0
  102. package/dist/ui/guiServer.d.ts.map +1 -0
  103. package/dist/ui/guiServer.js +134 -0
  104. package/dist/ui/guiServer.js.map +1 -0
  105. package/dist/ui/menu.d.ts +6 -0
  106. package/dist/ui/menu.d.ts.map +1 -0
  107. package/dist/ui/menu.js +93 -0
  108. package/dist/ui/menu.js.map +1 -0
  109. package/dist/ui/prompts.d.ts +13 -0
  110. package/dist/ui/prompts.d.ts.map +1 -0
  111. package/dist/ui/prompts.js +161 -0
  112. package/dist/ui/prompts.js.map +1 -0
  113. package/dist/utils/filesystem.d.ts +13 -0
  114. package/dist/utils/filesystem.d.ts.map +1 -0
  115. package/dist/utils/filesystem.js +152 -0
  116. package/dist/utils/filesystem.js.map +1 -0
  117. package/dist/utils/logger.d.ts +12 -0
  118. package/dist/utils/logger.d.ts.map +1 -0
  119. package/dist/utils/logger.js +49 -0
  120. package/dist/utils/logger.js.map +1 -0
  121. package/dist/utils/platform.d.ts +9 -0
  122. package/dist/utils/platform.d.ts.map +1 -0
  123. package/dist/utils/platform.js +75 -0
  124. package/dist/utils/platform.js.map +1 -0
  125. package/jest.config.js +20 -0
  126. package/logo.svg +60 -0
  127. package/package.json +55 -0
  128. package/setup-github.sh +125 -0
  129. package/src/core/appUpdateChecker.ts +220 -0
  130. package/src/core/detector.ts +133 -0
  131. package/src/core/duplicateFileFinder.ts +113 -0
  132. package/src/core/orphanedDependencyDetector.ts +195 -0
  133. package/src/core/performanceOptimizer.ts +209 -0
  134. package/src/core/permissionHandler.ts +121 -0
  135. package/src/core/pluginSystem.ts +194 -0
  136. package/src/core/removalRecorder.ts +146 -0
  137. package/src/core/remover.ts +280 -0
  138. package/src/core/reportGenerator.ts +354 -0
  139. package/src/core/scheduledCleanup.ts +204 -0
  140. package/src/core/serviceFileDetector.ts +181 -0
  141. package/src/core/verificationModule.ts +140 -0
  142. package/src/index.ts +449 -0
  143. package/src/managers/brewManager.ts +149 -0
  144. package/src/managers/customManager.ts +167 -0
  145. package/src/managers/linuxManager.ts +210 -0
  146. package/src/managers/npmManager.ts +137 -0
  147. package/src/types/index.ts +59 -0
  148. package/src/ui/guiServer.ts +155 -0
  149. package/src/ui/menu.ts +100 -0
  150. package/src/ui/prompts.ts +177 -0
  151. package/src/utils/filesystem.ts +145 -0
  152. package/src/utils/logger.ts +48 -0
  153. package/src/utils/platform.ts +75 -0
  154. package/tsconfig.json +20 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Duplicate File Finder
3
+ * Scans system for duplicate files by hash and size
4
+ * v1.3.0 Feature
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import crypto from 'crypto';
10
+ import { Logger } from '../utils/logger';
11
+
12
+ export interface DuplicateFile {
13
+ hash: string;
14
+ size: number;
15
+ paths: string[];
16
+ potentialSavings: number;
17
+ }
18
+
19
+ export class DuplicateFileFinder {
20
+ /**
21
+ * Find duplicate files in specified directories
22
+ */
23
+ async findDuplicates(directories: string[]): Promise<DuplicateFile[]> {
24
+ Logger.info('Scanning for duplicate files...');
25
+
26
+ const fileHashes = new Map<string, { size: number; paths: string[] }>();
27
+
28
+ for (const directory of directories) {
29
+ await this.scanDirectory(directory, fileHashes);
30
+ }
31
+
32
+ // Filter duplicates
33
+ const duplicates: DuplicateFile[] = [];
34
+ for (const [hash, data] of fileHashes.entries()) {
35
+ if (data.paths.length > 1) {
36
+ duplicates.push({
37
+ hash,
38
+ size: data.size,
39
+ paths: data.paths,
40
+ potentialSavings: data.size * (data.paths.length - 1),
41
+ });
42
+ }
43
+ }
44
+
45
+ return duplicates.sort((a, b) => b.potentialSavings - a.potentialSavings);
46
+ }
47
+
48
+ /**
49
+ * Calculate file hash
50
+ */
51
+ private async calculateHash(filePath: string): Promise<string> {
52
+ return new Promise((resolve, reject) => {
53
+ const hash = crypto.createHash('sha256');
54
+ const stream = fs.createReadStream(filePath);
55
+
56
+ stream.on('data', (data) => hash.update(data));
57
+ stream.on('end', () => resolve(hash.digest('hex')));
58
+ stream.on('error', reject);
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Recursively scan directory
64
+ */
65
+ private async scanDirectory(
66
+ directory: string,
67
+ fileHashes: Map<string, { size: number; paths: string[] }>
68
+ ): Promise<void> {
69
+ try {
70
+ const entries = fs.readdirSync(directory);
71
+
72
+ for (const entry of entries) {
73
+ const fullPath = path.join(directory, entry);
74
+ const stats = fs.statSync(fullPath);
75
+
76
+ if (stats.isDirectory()) {
77
+ await this.scanDirectory(fullPath, fileHashes);
78
+ } else if (stats.isFile()) {
79
+ const hash = await this.calculateHash(fullPath);
80
+
81
+ if (!fileHashes.has(hash)) {
82
+ fileHashes.set(hash, { size: stats.size, paths: [] });
83
+ }
84
+
85
+ fileHashes.get(hash)!.paths.push(fullPath);
86
+ }
87
+ }
88
+ } catch (error) {
89
+ Logger.debug(`Error scanning directory ${directory}: ${(error as Error).message}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get duplicate summary
95
+ */
96
+ getSummary(duplicates: DuplicateFile[]): string {
97
+ const totalSize = duplicates.reduce((sum, d) => sum + d.potentialSavings, 0);
98
+ const totalFiles = duplicates.reduce((sum, d) => sum + (d.paths.length - 1), 0);
99
+
100
+ return `
101
+ Found ${duplicates.length} duplicate groups with ${totalFiles} duplicate files
102
+ Potential space savings: ${this.formatBytes(totalSize)}
103
+ `;
104
+ }
105
+
106
+ private formatBytes(bytes: number): string {
107
+ if (bytes === 0) return '0 B';
108
+ const k = 1024;
109
+ const sizes = ['B', 'KB', 'MB', 'GB'];
110
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
111
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
112
+ }
113
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Orphaned Dependency Detector
3
+ * Identifies npm packages that are no longer referenced by any project
4
+ * v1.4.0 Feature
5
+ */
6
+
7
+ import { execSync } from 'child_process';
8
+ import { getHomeDir } from '../utils/platform';
9
+ import path from 'path';
10
+ import fs from 'fs';
11
+ import { Logger } from '../utils/logger';
12
+
13
+ export interface OrphanedPackage {
14
+ name: string;
15
+ version: string;
16
+ location: string;
17
+ size: number;
18
+ lastModified: Date;
19
+ isOrphaned: boolean;
20
+ dependedBy: string[];
21
+ }
22
+
23
+ export class OrphanedDependencyDetector {
24
+ /**
25
+ * Detect orphaned npm packages
26
+ */
27
+ async detectOrphanedPackages(): Promise<OrphanedPackage[]> {
28
+ Logger.info('Scanning for orphaned dependencies...');
29
+
30
+ try {
31
+ const globalPackages = this.getGlobalPackages();
32
+ const projectPackages = this.getProjectPackages();
33
+
34
+ const orphaned: OrphanedPackage[] = [];
35
+
36
+ for (const pkg of globalPackages) {
37
+ const dependedBy = this.checkDependencies(pkg.name, projectPackages);
38
+
39
+ if (dependedBy.length === 0) {
40
+ orphaned.push({
41
+ ...pkg,
42
+ isOrphaned: true,
43
+ dependedBy: [],
44
+ });
45
+ } else {
46
+ orphaned.push({
47
+ ...pkg,
48
+ isOrphaned: false,
49
+ dependedBy,
50
+ });
51
+ }
52
+ }
53
+
54
+ return orphaned.filter((p) => p.isOrphaned);
55
+ } catch (error) {
56
+ Logger.error(`Error detecting orphaned packages: ${(error as Error).message}`);
57
+ return [];
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get globally installed packages
63
+ */
64
+ private getGlobalPackages(): OrphanedPackage[] {
65
+ try {
66
+ const output = execSync('npm list -g --json 2>/dev/null || echo "{}"').toString();
67
+ const data = JSON.parse(output);
68
+
69
+ const packages: OrphanedPackage[] = [];
70
+
71
+ if (data.dependencies) {
72
+ for (const [name, pkg] of Object.entries(data.dependencies)) {
73
+ const location = this.findPackageLocation(name);
74
+ if (location) {
75
+ const stats = fs.statSync(location);
76
+ packages.push({
77
+ name,
78
+ version: (pkg as any).version || 'unknown',
79
+ location,
80
+ size: this.getDirectorySize(location),
81
+ lastModified: stats.mtime,
82
+ isOrphaned: false,
83
+ dependedBy: [],
84
+ });
85
+ }
86
+ }
87
+ }
88
+
89
+ return packages;
90
+ } catch {
91
+ return [];
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get project packages from package.json files
97
+ */
98
+ private getProjectPackages(): Set<string> {
99
+ const packages = new Set<string>();
100
+
101
+ try {
102
+ const home = getHomeDir();
103
+ const searchPaths = [home, '/usr/local'];
104
+
105
+ for (const searchPath of searchPaths) {
106
+ this.findPackageJsonFiles(searchPath, packages);
107
+ }
108
+ } catch {
109
+ // Ignore errors during project scanning
110
+ }
111
+
112
+ return packages;
113
+ }
114
+
115
+ /**
116
+ * Find package.json files recursively
117
+ */
118
+ private findPackageJsonFiles(dir: string, packages: Set<string>, depth: number = 0): void {
119
+ if (depth > 3) return; // Limit depth to avoid scanning too deep
120
+
121
+ try {
122
+ const entries = fs.readdirSync(dir);
123
+
124
+ for (const entry of entries) {
125
+ if (entry === 'package.json') {
126
+ try {
127
+ const content = fs.readFileSync(path.join(dir, entry), 'utf-8');
128
+ const json = JSON.parse(content);
129
+
130
+ if (json.dependencies) {
131
+ Object.keys(json.dependencies).forEach((dep) => packages.add(dep));
132
+ }
133
+ if (json.devDependencies) {
134
+ Object.keys(json.devDependencies).forEach((dep) => packages.add(dep));
135
+ }
136
+ } catch {
137
+ // Skip invalid package.json
138
+ }
139
+ }
140
+
141
+ const fullPath = path.join(dir, entry);
142
+ if (fs.statSync(fullPath).isDirectory() && !entry.startsWith('.')) {
143
+ this.findPackageJsonFiles(fullPath, packages, depth + 1);
144
+ }
145
+ }
146
+ } catch {
147
+ // Ignore read errors
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Check if package is depended by any project
153
+ */
154
+ private checkDependencies(packageName: string, projectPackages: Set<string>): string[] {
155
+ return Array.from(projectPackages).filter((p) => p === packageName);
156
+ }
157
+
158
+ /**
159
+ * Find package location
160
+ */
161
+ private findPackageLocation(packageName: string): string | null {
162
+ try {
163
+ const result = execSync(`npm list -g ${packageName} --json 2>/dev/null || echo "{}"`).toString();
164
+ const data = JSON.parse(result);
165
+ return data.dependencies?.[packageName]?.resolved || null;
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get directory size
173
+ */
174
+ private getDirectorySize(dir: string): number {
175
+ try {
176
+ let size = 0;
177
+ const entries = fs.readdirSync(dir);
178
+
179
+ for (const entry of entries) {
180
+ const fullPath = path.join(dir, entry);
181
+ const stats = fs.statSync(fullPath);
182
+
183
+ if (stats.isDirectory()) {
184
+ size += this.getDirectorySize(fullPath);
185
+ } else {
186
+ size += stats.size;
187
+ }
188
+ }
189
+
190
+ return size;
191
+ } catch {
192
+ return 0;
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Performance Optimizer
3
+ * Optimizes scanning performance for large systems
4
+ * v1.8.0 Feature
5
+ */
6
+
7
+ import { Logger } from '../utils/logger';
8
+
9
+ export interface ScanMetrics {
10
+ startTime: Date;
11
+ endTime?: Date;
12
+ filesScanned: number;
13
+ directoriesScanned: number;
14
+ totalSize: number;
15
+ duration?: number; // milliseconds
16
+ averageFileSize: number;
17
+ scanSpeed: number; // files per second
18
+ }
19
+
20
+ export interface OptimizationSettings {
21
+ parallelScanThreads: number;
22
+ cacheResults: boolean;
23
+ skipSymlinks: boolean;
24
+ maxDirectoryDepth: number;
25
+ excludePatterns: string[];
26
+ indexSystemDirectories: boolean;
27
+ }
28
+
29
+ export class PerformanceOptimizer {
30
+ private metrics: Map<string, ScanMetrics> = new Map();
31
+ private cache: Map<string, any> = new Map();
32
+ private settings: OptimizationSettings;
33
+
34
+ constructor(settings?: Partial<OptimizationSettings>) {
35
+ this.settings = {
36
+ parallelScanThreads: 4,
37
+ cacheResults: true,
38
+ skipSymlinks: true,
39
+ maxDirectoryDepth: 10,
40
+ excludePatterns: ['.git', 'node_modules', '.cache'],
41
+ indexSystemDirectories: true,
42
+ ...settings,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Start performance metrics tracking
48
+ */
49
+ startMetrics(scanId: string): ScanMetrics {
50
+ const metrics: ScanMetrics = {
51
+ startTime: new Date(),
52
+ filesScanned: 0,
53
+ directoriesScanned: 0,
54
+ totalSize: 0,
55
+ averageFileSize: 0,
56
+ scanSpeed: 0,
57
+ };
58
+
59
+ this.metrics.set(scanId, metrics);
60
+ Logger.debug(`Performance metrics started: ${scanId}`);
61
+ return metrics;
62
+ }
63
+
64
+ /**
65
+ * End performance metrics tracking
66
+ */
67
+ endMetrics(scanId: string): ScanMetrics | null {
68
+ const metrics = this.metrics.get(scanId);
69
+ if (metrics) {
70
+ metrics.endTime = new Date();
71
+ metrics.duration = metrics.endTime.getTime() - metrics.startTime.getTime();
72
+ metrics.averageFileSize = metrics.filesScanned > 0 ? metrics.totalSize / metrics.filesScanned : 0;
73
+ metrics.scanSpeed = metrics.duration > 0 ? (metrics.filesScanned / metrics.duration) * 1000 : 0;
74
+
75
+ Logger.info(`
76
+ Performance Report: ${scanId}
77
+ Duration: ${(metrics.duration / 1000).toFixed(2)}s
78
+ Files scanned: ${metrics.filesScanned}
79
+ Directories scanned: ${metrics.directoriesScanned}
80
+ Total size: ${this.formatBytes(metrics.totalSize)}
81
+ Scan speed: ${metrics.scanSpeed.toFixed(2)} files/sec
82
+ `);
83
+
84
+ return metrics;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Get metrics for a scan
91
+ */
92
+ getMetrics(scanId: string): ScanMetrics | null {
93
+ return this.metrics.get(scanId) || null;
94
+ }
95
+
96
+ /**
97
+ * Update metrics during scan
98
+ */
99
+ updateMetrics(scanId: string, data: Partial<ScanMetrics>): void {
100
+ const metrics = this.metrics.get(scanId);
101
+ if (metrics) {
102
+ Object.assign(metrics, data);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Cache scan result
108
+ */
109
+ cacheResult(key: string, value: any, ttl?: number): void {
110
+ if (this.settings.cacheResults) {
111
+ this.cache.set(key, {
112
+ value,
113
+ timestamp: Date.now(),
114
+ ttl: ttl || 3600000, // 1 hour default
115
+ });
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get cached result
121
+ */
122
+ getCachedResult(key: string): any | null {
123
+ const cached = this.cache.get(key);
124
+ if (cached) {
125
+ const age = Date.now() - cached.timestamp;
126
+ if (age < cached.ttl) {
127
+ return cached.value;
128
+ }
129
+ // Expired, delete it
130
+ this.cache.delete(key);
131
+ }
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * Clear cache
137
+ */
138
+ clearCache(): void {
139
+ this.cache.clear();
140
+ Logger.debug('Performance cache cleared');
141
+ }
142
+
143
+ /**
144
+ * Update optimization settings
145
+ */
146
+ updateSettings(settings: Partial<OptimizationSettings>): void {
147
+ Object.assign(this.settings, settings);
148
+ Logger.debug('Performance optimization settings updated');
149
+ }
150
+
151
+ /**
152
+ * Get optimization settings
153
+ */
154
+ getSettings(): OptimizationSettings {
155
+ return { ...this.settings };
156
+ }
157
+
158
+ /**
159
+ * Check if path should be scanned
160
+ */
161
+ shouldScanPath(path: string, depth: number): boolean {
162
+ // Check max depth
163
+ if (depth > this.settings.maxDirectoryDepth) {
164
+ return false;
165
+ }
166
+
167
+ // Check exclusions
168
+ for (const pattern of this.settings.excludePatterns) {
169
+ if (path.includes(pattern)) {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ return true;
175
+ }
176
+
177
+ /**
178
+ * Get optimization recommendations
179
+ */
180
+ getRecommendations(metrics: ScanMetrics): string[] {
181
+ const recommendations: string[] = [];
182
+
183
+ if (!metrics.duration || metrics.duration > 30000) {
184
+ recommendations.push('Consider increasing parallel scan threads for faster scans');
185
+ }
186
+
187
+ if (metrics.averageFileSize > 10000000) {
188
+ recommendations.push('Large average file size detected, consider excluding large files');
189
+ }
190
+
191
+ if (metrics.filesScanned > 100000) {
192
+ recommendations.push('Large scan detected, consider using caching to speed up subsequent scans');
193
+ }
194
+
195
+ if (metrics.directoriesScanned > 1000 && this.settings.maxDirectoryDepth > 8) {
196
+ recommendations.push('Consider reducing max directory depth to improve performance');
197
+ }
198
+
199
+ return recommendations;
200
+ }
201
+
202
+ private formatBytes(bytes: number): string {
203
+ if (bytes === 0) return '0 B';
204
+ const k = 1024;
205
+ const sizes = ['B', 'KB', 'MB', 'GB'];
206
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
207
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
208
+ }
209
+ }
@@ -0,0 +1,121 @@
1
+ import { execSync } from 'child_process';
2
+ import { isMacOS, isLinux, isWindows } from '../utils/platform';
3
+ import { Logger } from '../utils/logger';
4
+
5
+ export class PermissionHandler {
6
+ /**
7
+ * Check if current process has elevated privileges
8
+ */
9
+ static isElevated(): boolean {
10
+ try {
11
+ if (isMacOS() || isLinux()) {
12
+ // Check if running as root
13
+ const uid = execSync('id -u').toString().trim();
14
+ return uid === '0';
15
+ }
16
+
17
+ if (isWindows()) {
18
+ // Check if running with admin privileges
19
+ const output = execSync('net session').toString().trim();
20
+ return output.length > 0;
21
+ }
22
+
23
+ return false;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Request elevated permissions using sudo or UAC
31
+ */
32
+ static async requestElevatedPermissions(): Promise<boolean> {
33
+ try {
34
+ if (isMacOS() || isLinux()) {
35
+ Logger.warn('This operation requires elevated permissions');
36
+ Logger.info('You may be prompted for your password (sudo)');
37
+ return true;
38
+ }
39
+
40
+ if (isWindows()) {
41
+ Logger.warn('This operation requires administrator privileges');
42
+ Logger.info('Please run AppClean as Administrator');
43
+ return false;
44
+ }
45
+
46
+ return false;
47
+ } catch (error) {
48
+ Logger.error(`Permission request failed: ${(error as Error).message}`);
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Check if path requires elevated access
55
+ */
56
+ static pathRequiresElevation(path: string): boolean {
57
+ const systemPaths = [
58
+ '/etc',
59
+ '/sys',
60
+ '/var',
61
+ '/usr/bin',
62
+ '/usr/local/bin',
63
+ '/Library',
64
+ 'C:\\Program Files',
65
+ 'C:\\Windows',
66
+ 'C:\\ProgramData',
67
+ ];
68
+
69
+ return systemPaths.some((sysPath) => path.startsWith(sysPath));
70
+ }
71
+
72
+ /**
73
+ * Check if app installation requires elevated permissions for removal
74
+ */
75
+ static installationRequiresElevation(installMethod: string, paths: string[]): boolean {
76
+ if (installMethod === 'brew' || installMethod === 'apt' || installMethod === 'yum' || installMethod === 'dnf') {
77
+ return true;
78
+ }
79
+
80
+ // Check if any path requires elevation
81
+ return paths.some((path) => this.pathRequiresElevation(path));
82
+ }
83
+
84
+ /**
85
+ * Get the appropriate sudo command based on platform
86
+ */
87
+ static getSudoCommand(): string {
88
+ if (isMacOS() || isLinux()) {
89
+ return 'sudo';
90
+ }
91
+
92
+ if (isWindows()) {
93
+ return 'runas /user:Administrator';
94
+ }
95
+
96
+ return '';
97
+ }
98
+
99
+ /**
100
+ * Execute command with elevated permissions
101
+ */
102
+ static executeWithElevation(command: string): string {
103
+ try {
104
+ if (isMacOS() || isLinux()) {
105
+ // For sudo, we need to use -S to read password from stdin
106
+ // This is more complex in practice, so we'll ask user to run with sudo
107
+ return '';
108
+ }
109
+
110
+ if (isWindows()) {
111
+ // Windows requires special handling with runas
112
+ return execSync(`runas /noprofile /user:Administrator "${command}"`).toString();
113
+ }
114
+
115
+ return '';
116
+ } catch (error) {
117
+ Logger.error(`Elevated execution failed: ${(error as Error).message}`);
118
+ return '';
119
+ }
120
+ }
121
+ }