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.
- package/.github/workflows/publish.yml +41 -0
- package/.github/workflows/test.yml +37 -0
- package/ACTION_CHECKLIST.md +342 -0
- package/APPCLEAN_SUMMARY.md +309 -0
- package/CHANGELOG.md +205 -0
- package/CODE_OF_CONDUCT.md +49 -0
- package/CODE_REVIEW_REPORT.md +447 -0
- package/COMMUNITY_POSTS.md +307 -0
- package/CONTRIBUTING.md +121 -0
- package/DEPLOYMENT_GUIDE.md +345 -0
- package/DEPLOYMENT_STATUS.md +182 -0
- package/EXECUTIVE_REPORT.md +393 -0
- package/GITHUB_OPTIMIZATION.md +383 -0
- package/INDEX.md +165 -0
- package/LICENSE +21 -0
- package/MARKETING_SUMMARY.md +352 -0
- package/NPM_PACKAGE_OPTIMIZATION.md +281 -0
- package/NPM_PUBLISH.md +116 -0
- package/PROJECT_SUMMARY.txt +249 -0
- package/QUICKSTART.md +219 -0
- package/README.md +548 -0
- package/SECURITY.md +104 -0
- package/SETUP_GITHUB.md +237 -0
- package/TESTING_SUMMARY.md +379 -0
- package/dist/core/appUpdateChecker.d.ts +23 -0
- package/dist/core/appUpdateChecker.d.ts.map +1 -0
- package/dist/core/appUpdateChecker.js +159 -0
- package/dist/core/appUpdateChecker.js.map +1 -0
- package/dist/core/detector.d.ts +13 -0
- package/dist/core/detector.d.ts.map +1 -0
- package/dist/core/detector.js +99 -0
- package/dist/core/detector.js.map +1 -0
- package/dist/core/duplicateFileFinder.d.ts +14 -0
- package/dist/core/duplicateFileFinder.d.ts.map +1 -0
- package/dist/core/duplicateFileFinder.js +80 -0
- package/dist/core/duplicateFileFinder.js.map +1 -0
- package/dist/core/orphanedDependencyDetector.d.ts +19 -0
- package/dist/core/orphanedDependencyDetector.d.ts.map +1 -0
- package/dist/core/orphanedDependencyDetector.js +148 -0
- package/dist/core/orphanedDependencyDetector.js.map +1 -0
- package/dist/core/performanceOptimizer.d.ts +37 -0
- package/dist/core/performanceOptimizer.d.ts.map +1 -0
- package/dist/core/performanceOptimizer.js +128 -0
- package/dist/core/performanceOptimizer.js.map +1 -0
- package/dist/core/permissionHandler.d.ts +9 -0
- package/dist/core/permissionHandler.d.ts.map +1 -0
- package/dist/core/permissionHandler.js +89 -0
- package/dist/core/permissionHandler.js.map +1 -0
- package/dist/core/pluginSystem.d.ts +39 -0
- package/dist/core/pluginSystem.d.ts.map +1 -0
- package/dist/core/pluginSystem.js +120 -0
- package/dist/core/pluginSystem.js.map +1 -0
- package/dist/core/removalRecorder.d.ts +32 -0
- package/dist/core/removalRecorder.d.ts.map +1 -0
- package/dist/core/removalRecorder.js +79 -0
- package/dist/core/removalRecorder.js.map +1 -0
- package/dist/core/remover.d.ts +15 -0
- package/dist/core/remover.d.ts.map +1 -0
- package/dist/core/remover.js +225 -0
- package/dist/core/remover.js.map +1 -0
- package/dist/core/reportGenerator.d.ts +9 -0
- package/dist/core/reportGenerator.d.ts.map +1 -0
- package/dist/core/reportGenerator.js +328 -0
- package/dist/core/reportGenerator.js.map +1 -0
- package/dist/core/scheduledCleanup.d.ts +38 -0
- package/dist/core/scheduledCleanup.d.ts.map +1 -0
- package/dist/core/scheduledCleanup.js +127 -0
- package/dist/core/scheduledCleanup.js.map +1 -0
- package/dist/core/serviceFileDetector.d.ts +18 -0
- package/dist/core/serviceFileDetector.d.ts.map +1 -0
- package/dist/core/serviceFileDetector.js +136 -0
- package/dist/core/serviceFileDetector.js.map +1 -0
- package/dist/core/verificationModule.d.ts +14 -0
- package/dist/core/verificationModule.d.ts.map +1 -0
- package/dist/core/verificationModule.js +102 -0
- package/dist/core/verificationModule.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +333 -0
- package/dist/index.js.map +1 -0
- package/dist/managers/brewManager.d.ts +10 -0
- package/dist/managers/brewManager.d.ts.map +1 -0
- package/dist/managers/brewManager.js +130 -0
- package/dist/managers/brewManager.js.map +1 -0
- package/dist/managers/customManager.d.ts +8 -0
- package/dist/managers/customManager.d.ts.map +1 -0
- package/dist/managers/customManager.js +139 -0
- package/dist/managers/customManager.js.map +1 -0
- package/dist/managers/linuxManager.d.ts +10 -0
- package/dist/managers/linuxManager.d.ts.map +1 -0
- package/dist/managers/linuxManager.js +191 -0
- package/dist/managers/linuxManager.js.map +1 -0
- package/dist/managers/npmManager.d.ts +10 -0
- package/dist/managers/npmManager.d.ts.map +1 -0
- package/dist/managers/npmManager.js +119 -0
- package/dist/managers/npmManager.js.map +1 -0
- package/dist/types/index.d.ts +44 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/guiServer.d.ts +10 -0
- package/dist/ui/guiServer.d.ts.map +1 -0
- package/dist/ui/guiServer.js +134 -0
- package/dist/ui/guiServer.js.map +1 -0
- package/dist/ui/menu.d.ts +6 -0
- package/dist/ui/menu.d.ts.map +1 -0
- package/dist/ui/menu.js +93 -0
- package/dist/ui/menu.js.map +1 -0
- package/dist/ui/prompts.d.ts +13 -0
- package/dist/ui/prompts.d.ts.map +1 -0
- package/dist/ui/prompts.js +161 -0
- package/dist/ui/prompts.js.map +1 -0
- package/dist/utils/filesystem.d.ts +13 -0
- package/dist/utils/filesystem.d.ts.map +1 -0
- package/dist/utils/filesystem.js +152 -0
- package/dist/utils/filesystem.js.map +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +49 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/platform.d.ts +9 -0
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/utils/platform.js +75 -0
- package/dist/utils/platform.js.map +1 -0
- package/jest.config.js +20 -0
- package/logo.svg +60 -0
- package/package.json +55 -0
- package/setup-github.sh +125 -0
- package/src/core/appUpdateChecker.ts +220 -0
- package/src/core/detector.ts +133 -0
- package/src/core/duplicateFileFinder.ts +113 -0
- package/src/core/orphanedDependencyDetector.ts +195 -0
- package/src/core/performanceOptimizer.ts +209 -0
- package/src/core/permissionHandler.ts +121 -0
- package/src/core/pluginSystem.ts +194 -0
- package/src/core/removalRecorder.ts +146 -0
- package/src/core/remover.ts +280 -0
- package/src/core/reportGenerator.ts +354 -0
- package/src/core/scheduledCleanup.ts +204 -0
- package/src/core/serviceFileDetector.ts +181 -0
- package/src/core/verificationModule.ts +140 -0
- package/src/index.ts +449 -0
- package/src/managers/brewManager.ts +149 -0
- package/src/managers/customManager.ts +167 -0
- package/src/managers/linuxManager.ts +210 -0
- package/src/managers/npmManager.ts +137 -0
- package/src/types/index.ts +59 -0
- package/src/ui/guiServer.ts +155 -0
- package/src/ui/menu.ts +100 -0
- package/src/ui/prompts.ts +177 -0
- package/src/utils/filesystem.ts +145 -0
- package/src/utils/logger.ts +48 -0
- package/src/utils/platform.ts +75 -0
- 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
|
+
}
|