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,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin System for Custom Detectors
|
|
3
|
+
* Allows users to create custom app detectors via plugins
|
|
4
|
+
* v1.5.0 Feature
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ArtifactPath } from '../types';
|
|
8
|
+
import { Logger } from '../utils/logger';
|
|
9
|
+
|
|
10
|
+
export interface AppDetectorPlugin {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
detect(appName: string): Promise<boolean>;
|
|
14
|
+
findArtifacts(appName: string): Promise<ArtifactPath[]>;
|
|
15
|
+
remove(appName: string): Promise<boolean>;
|
|
16
|
+
getInfo(): PluginInfo;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PluginInfo {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
description: string;
|
|
23
|
+
author: string;
|
|
24
|
+
website?: string;
|
|
25
|
+
supportedPlatforms: string[];
|
|
26
|
+
capabilities: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PluginConfig {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
settings?: Record<string, any>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class PluginSystem {
|
|
35
|
+
private plugins: Map<string, AppDetectorPlugin> = new Map();
|
|
36
|
+
private pluginConfigs: Map<string, PluginConfig> = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a plugin
|
|
40
|
+
*/
|
|
41
|
+
registerPlugin(plugin: AppDetectorPlugin, config?: PluginConfig): void {
|
|
42
|
+
this.plugins.set(plugin.name, plugin);
|
|
43
|
+
this.pluginConfigs.set(plugin.name, config || { enabled: true });
|
|
44
|
+
Logger.info(`Plugin registered: ${plugin.name} v${plugin.version}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Unregister a plugin
|
|
49
|
+
*/
|
|
50
|
+
unregisterPlugin(pluginName: string): void {
|
|
51
|
+
this.plugins.delete(pluginName);
|
|
52
|
+
this.pluginConfigs.delete(pluginName);
|
|
53
|
+
Logger.info(`Plugin unregistered: ${pluginName}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all registered plugins
|
|
58
|
+
*/
|
|
59
|
+
getPlugins(): AppDetectorPlugin[] {
|
|
60
|
+
return Array.from(this.plugins.values());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get plugin by name
|
|
65
|
+
*/
|
|
66
|
+
getPlugin(pluginName: string): AppDetectorPlugin | null {
|
|
67
|
+
return this.plugins.get(pluginName) || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute plugin detection
|
|
72
|
+
*/
|
|
73
|
+
async executeDetection(appName: string): Promise<Map<string, boolean>> {
|
|
74
|
+
const results = new Map<string, boolean>();
|
|
75
|
+
|
|
76
|
+
for (const [name, plugin] of this.plugins) {
|
|
77
|
+
const config = this.pluginConfigs.get(name);
|
|
78
|
+
if (config?.enabled) {
|
|
79
|
+
try {
|
|
80
|
+
const detected = await plugin.detect(appName);
|
|
81
|
+
results.set(name, detected);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
Logger.debug(`Plugin ${name} detection failed: ${(error as Error).message}`);
|
|
84
|
+
results.set(name, false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Execute plugin artifact finding
|
|
94
|
+
*/
|
|
95
|
+
async executeFindArtifacts(appName: string): Promise<Map<string, ArtifactPath[]>> {
|
|
96
|
+
const results = new Map<string, ArtifactPath[]>();
|
|
97
|
+
|
|
98
|
+
for (const [name, plugin] of this.plugins) {
|
|
99
|
+
const config = this.pluginConfigs.get(name);
|
|
100
|
+
if (config?.enabled) {
|
|
101
|
+
try {
|
|
102
|
+
const artifacts = await plugin.findArtifacts(appName);
|
|
103
|
+
results.set(name, artifacts);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
Logger.debug(`Plugin ${name} artifact finding failed: ${(error as Error).message}`);
|
|
106
|
+
results.set(name, []);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Execute plugin removal
|
|
116
|
+
*/
|
|
117
|
+
async executeRemoval(appName: string, pluginName?: string): Promise<Map<string, boolean>> {
|
|
118
|
+
const results = new Map<string, boolean>();
|
|
119
|
+
|
|
120
|
+
if (pluginName) {
|
|
121
|
+
const plugin = this.plugins.get(pluginName);
|
|
122
|
+
if (plugin) {
|
|
123
|
+
const config = this.pluginConfigs.get(pluginName);
|
|
124
|
+
if (config?.enabled) {
|
|
125
|
+
try {
|
|
126
|
+
const removed = await plugin.remove(appName);
|
|
127
|
+
results.set(pluginName, removed);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
Logger.debug(`Plugin ${pluginName} removal failed: ${(error as Error).message}`);
|
|
130
|
+
results.set(pluginName, false);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
for (const [name, plugin] of this.plugins) {
|
|
136
|
+
const config = this.pluginConfigs.get(name);
|
|
137
|
+
if (config?.enabled) {
|
|
138
|
+
try {
|
|
139
|
+
const removed = await plugin.remove(appName);
|
|
140
|
+
results.set(name, removed);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
Logger.debug(`Plugin ${name} removal failed: ${(error as Error).message}`);
|
|
143
|
+
results.set(name, false);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return results;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Enable/disable plugin
|
|
154
|
+
*/
|
|
155
|
+
setPluginEnabled(pluginName: string, enabled: boolean): void {
|
|
156
|
+
const config = this.pluginConfigs.get(pluginName);
|
|
157
|
+
if (config) {
|
|
158
|
+
config.enabled = enabled;
|
|
159
|
+
Logger.info(`Plugin ${pluginName} ${enabled ? 'enabled' : 'disabled'}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get plugin configuration
|
|
165
|
+
*/
|
|
166
|
+
getPluginConfig(pluginName: string): PluginConfig | null {
|
|
167
|
+
return this.pluginConfigs.get(pluginName) || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Update plugin configuration
|
|
172
|
+
*/
|
|
173
|
+
updatePluginConfig(pluginName: string, config: Partial<PluginConfig>): void {
|
|
174
|
+
const existing = this.pluginConfigs.get(pluginName);
|
|
175
|
+
if (existing) {
|
|
176
|
+
this.pluginConfigs.set(pluginName, { ...existing, ...config });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get plugin info
|
|
182
|
+
*/
|
|
183
|
+
getPluginInfo(pluginName: string): PluginInfo | null {
|
|
184
|
+
const plugin = this.plugins.get(pluginName);
|
|
185
|
+
return plugin?.getInfo() || null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* List all plugins with info
|
|
190
|
+
*/
|
|
191
|
+
listPlugins(): PluginInfo[] {
|
|
192
|
+
return Array.from(this.plugins.values()).map((p) => p.getInfo());
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getHomeDir } from '../utils/platform';
|
|
4
|
+
import { ArtifactPath } from '../types';
|
|
5
|
+
import { formatDate, formatBytes } from '../utils/logger';
|
|
6
|
+
|
|
7
|
+
export interface RemovalRecord {
|
|
8
|
+
timestamp: Date;
|
|
9
|
+
appName: string;
|
|
10
|
+
installMethod: string;
|
|
11
|
+
userConsent: boolean;
|
|
12
|
+
artifactsDeleted: DeletedArtifact[];
|
|
13
|
+
verificationStatus: 'verified_removed' | 'still_exists' | 'partial_removal' | 'unknown';
|
|
14
|
+
totalSpaceFreed: number;
|
|
15
|
+
completionStatus: 'success' | 'partial' | 'failed';
|
|
16
|
+
userNotes?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DeletedArtifact {
|
|
20
|
+
path: string;
|
|
21
|
+
type: string;
|
|
22
|
+
size: number;
|
|
23
|
+
status: 'deleted' | 'failed' | 'skipped';
|
|
24
|
+
errorMessage?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class RemovalRecorder {
|
|
28
|
+
private recordsDir: string;
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
const home = getHomeDir();
|
|
32
|
+
this.recordsDir = path.join(home, '.appclean-records');
|
|
33
|
+
|
|
34
|
+
// Create records directory if it doesn't exist
|
|
35
|
+
if (!fs.existsSync(this.recordsDir)) {
|
|
36
|
+
fs.mkdirSync(this.recordsDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new removal record
|
|
42
|
+
*/
|
|
43
|
+
createRecord(appName: string, installMethod: string, userConsent: boolean): RemovalRecord {
|
|
44
|
+
return {
|
|
45
|
+
timestamp: new Date(),
|
|
46
|
+
appName,
|
|
47
|
+
installMethod,
|
|
48
|
+
userConsent,
|
|
49
|
+
artifactsDeleted: [],
|
|
50
|
+
verificationStatus: 'unknown',
|
|
51
|
+
totalSpaceFreed: 0,
|
|
52
|
+
completionStatus: 'success',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add deleted artifact to record
|
|
58
|
+
*/
|
|
59
|
+
addDeletedArtifact(
|
|
60
|
+
record: RemovalRecord,
|
|
61
|
+
artifact: ArtifactPath,
|
|
62
|
+
status: 'deleted' | 'failed' | 'skipped',
|
|
63
|
+
error?: string
|
|
64
|
+
): void {
|
|
65
|
+
record.artifactsDeleted.push({
|
|
66
|
+
path: artifact.path,
|
|
67
|
+
type: artifact.type,
|
|
68
|
+
size: artifact.size,
|
|
69
|
+
status,
|
|
70
|
+
errorMessage: error,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (status === 'deleted') {
|
|
74
|
+
record.totalSpaceFreed += artifact.size;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Update verification status
|
|
80
|
+
*/
|
|
81
|
+
updateVerificationStatus(
|
|
82
|
+
record: RemovalRecord,
|
|
83
|
+
status: 'verified_removed' | 'still_exists' | 'partial_removal' | 'unknown'
|
|
84
|
+
): void {
|
|
85
|
+
record.verificationStatus = status;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update completion status
|
|
90
|
+
*/
|
|
91
|
+
updateCompletionStatus(
|
|
92
|
+
record: RemovalRecord,
|
|
93
|
+
status: 'success' | 'partial' | 'failed'
|
|
94
|
+
): void {
|
|
95
|
+
record.completionStatus = status;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Save record to file
|
|
100
|
+
*/
|
|
101
|
+
saveRecord(record: RemovalRecord): string {
|
|
102
|
+
const filename = `${record.appName}-${record.timestamp.getTime()}.json`;
|
|
103
|
+
const filepath = path.join(this.recordsDir, filename);
|
|
104
|
+
|
|
105
|
+
fs.writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf-8');
|
|
106
|
+
|
|
107
|
+
return filepath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get all removal records
|
|
112
|
+
*/
|
|
113
|
+
getAllRecords(): RemovalRecord[] {
|
|
114
|
+
const files = fs.readdirSync(this.recordsDir);
|
|
115
|
+
const records: RemovalRecord[] = [];
|
|
116
|
+
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
if (file.endsWith('.json')) {
|
|
119
|
+
try {
|
|
120
|
+
const content = fs.readFileSync(path.join(this.recordsDir, file), 'utf-8');
|
|
121
|
+
const record = JSON.parse(content);
|
|
122
|
+
record.timestamp = new Date(record.timestamp);
|
|
123
|
+
records.push(record);
|
|
124
|
+
} catch {
|
|
125
|
+
// Skip invalid files
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return records.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get records for a specific app
|
|
135
|
+
*/
|
|
136
|
+
getAppRecords(appName: string): RemovalRecord[] {
|
|
137
|
+
return this.getAllRecords().filter((r) => r.appName === appName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get records directory path
|
|
142
|
+
*/
|
|
143
|
+
getRecordsDirectory(): string {
|
|
144
|
+
return this.recordsDir;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { getHomeDir } from '../utils/platform';
|
|
4
|
+
import {
|
|
5
|
+
deleteFile,
|
|
6
|
+
deleteDirectory,
|
|
7
|
+
pathExists,
|
|
8
|
+
getDirectorySize,
|
|
9
|
+
} from '../utils/filesystem';
|
|
10
|
+
import { ArtifactPath, RemovalOptions, RemovalResult } from '../types';
|
|
11
|
+
import { Logger, formatBytes } from '../utils/logger';
|
|
12
|
+
import { NpmManager } from '../managers/npmManager';
|
|
13
|
+
import { BrewManager } from '../managers/brewManager';
|
|
14
|
+
import { LinuxManager } from '../managers/linuxManager';
|
|
15
|
+
import { PermissionHandler } from './permissionHandler';
|
|
16
|
+
import { ServiceFileDetector } from './serviceFileDetector';
|
|
17
|
+
import { RemovalRecorder, RemovalRecord } from './removalRecorder';
|
|
18
|
+
import { ReportGenerator } from './reportGenerator';
|
|
19
|
+
import { VerificationModule } from './verificationModule';
|
|
20
|
+
|
|
21
|
+
export class Remover {
|
|
22
|
+
async previewRemoval(artifacts: ArtifactPath[]): Promise<void> {
|
|
23
|
+
Logger.info('Files to be removed:');
|
|
24
|
+
Logger.space();
|
|
25
|
+
|
|
26
|
+
let totalSize = 0;
|
|
27
|
+
|
|
28
|
+
for (const artifact of artifacts) {
|
|
29
|
+
const size = artifact.size || 0;
|
|
30
|
+
totalSize += size;
|
|
31
|
+
|
|
32
|
+
console.log(` ${artifact.type.padEnd(8)} ${formatBytes(size).padEnd(10)} ${artifact.path}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Logger.space();
|
|
36
|
+
Logger.info(`Total space to be freed: ${formatBytes(totalSize)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async createBackup(appName: string, artifacts: ArtifactPath[]): Promise<string> {
|
|
40
|
+
const home = getHomeDir();
|
|
41
|
+
const backupDir = path.join(home, '.appclean-backups');
|
|
42
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
43
|
+
const backupPath = path.join(backupDir, `${appName}-${timestamp}.tar.gz`);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Create backup directory
|
|
47
|
+
execSync(`mkdir -p "${backupDir}"`);
|
|
48
|
+
|
|
49
|
+
// Create tar archive
|
|
50
|
+
const filePaths = artifacts.map((a) => `"${a.path}"`).join(' ');
|
|
51
|
+
execSync(`tar -czf "${backupPath}" ${filePaths} 2>/dev/null || true`);
|
|
52
|
+
|
|
53
|
+
Logger.success(`Backup created: ${backupPath}`);
|
|
54
|
+
return backupPath;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
Logger.warn(`Failed to create backup: ${(error as Error).message}`);
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async removeArtifacts(
|
|
62
|
+
artifacts: ArtifactPath[],
|
|
63
|
+
options: RemovalOptions
|
|
64
|
+
): Promise<{ success: number; failed: number; errors: string[] }> {
|
|
65
|
+
const errors: string[] = [];
|
|
66
|
+
let success = 0;
|
|
67
|
+
let failed = 0;
|
|
68
|
+
|
|
69
|
+
for (const artifact of artifacts) {
|
|
70
|
+
try {
|
|
71
|
+
if (!pathExists(artifact.path)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if it's a directory or file
|
|
76
|
+
const isDirectory = this.isDirectory(artifact.path);
|
|
77
|
+
|
|
78
|
+
if (isDirectory) {
|
|
79
|
+
const deleted = deleteDirectory(artifact.path);
|
|
80
|
+
if (deleted) {
|
|
81
|
+
success++;
|
|
82
|
+
} else {
|
|
83
|
+
failed++;
|
|
84
|
+
errors.push(`Failed to delete directory: ${artifact.path}`);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
const deleted = deleteFile(artifact.path);
|
|
88
|
+
if (deleted) {
|
|
89
|
+
success++;
|
|
90
|
+
} else {
|
|
91
|
+
failed++;
|
|
92
|
+
errors.push(`Failed to delete file: ${artifact.path}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
failed++;
|
|
97
|
+
errors.push(`Error removing ${artifact.path}: ${(error as Error).message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { success, failed, errors };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async removeApp(
|
|
105
|
+
appName: string,
|
|
106
|
+
installMethod: string,
|
|
107
|
+
artifacts: ArtifactPath[],
|
|
108
|
+
options: RemovalOptions = {}
|
|
109
|
+
): Promise<RemovalResult> {
|
|
110
|
+
const result: RemovalResult = {
|
|
111
|
+
success: false,
|
|
112
|
+
appName,
|
|
113
|
+
removedFiles: 0,
|
|
114
|
+
freedSpace: 0,
|
|
115
|
+
errors: [],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const recorder = new RemovalRecorder();
|
|
119
|
+
const verificationModule = new VerificationModule();
|
|
120
|
+
const serviceDetector = new ServiceFileDetector();
|
|
121
|
+
const reportGenerator = new ReportGenerator();
|
|
122
|
+
|
|
123
|
+
let removalRecord: RemovalRecord | null = null;
|
|
124
|
+
const artifactPaths = artifacts.map((a) => a.path);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Step 1: Check if elevation is required
|
|
128
|
+
const requiresElevation = PermissionHandler.installationRequiresElevation(installMethod, artifactPaths);
|
|
129
|
+
if (requiresElevation && !PermissionHandler.isElevated()) {
|
|
130
|
+
await PermissionHandler.requestElevatedPermissions();
|
|
131
|
+
Logger.warn('Please run this command with sudo for complete removal of system packages');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Step 2: Detect service files that need manual cleanup
|
|
135
|
+
const serviceFiles = await serviceDetector.findServiceFiles(appName);
|
|
136
|
+
if (serviceFiles.length > 0) {
|
|
137
|
+
Logger.info('\n⚠️ Manual Cleanup Required:');
|
|
138
|
+
Logger.info('The following service files require manual cleanup:');
|
|
139
|
+
serviceFiles.forEach((file, index) => {
|
|
140
|
+
console.log(`\n${index + 1}. ${file.type.toUpperCase()}`);
|
|
141
|
+
console.log(` Path: ${file.path}`);
|
|
142
|
+
console.log(` Command: ${file.manualCleanupInstructions}`);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Step 3: Dry run
|
|
147
|
+
if (options.dryRun) {
|
|
148
|
+
await this.previewRemoval(artifacts);
|
|
149
|
+
result.success = true;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Step 4: Create removal record and record user consent
|
|
154
|
+
removalRecord = recorder.createRecord(appName, installMethod, options.userConsent ?? true);
|
|
155
|
+
|
|
156
|
+
// Step 5: Create backup if requested
|
|
157
|
+
if (options.createBackup) {
|
|
158
|
+
result.backupPath = await this.createBackup(appName, artifacts);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Step 6: Remove using package manager first
|
|
162
|
+
const removed = await this.removeViaPackageManager(appName, installMethod);
|
|
163
|
+
|
|
164
|
+
if (!removed) {
|
|
165
|
+
Logger.warn(`Package manager removal failed, attempting manual file removal...`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Step 7: Manually remove remaining artifacts
|
|
169
|
+
const removalResult = await this.removeArtifacts(artifacts, options);
|
|
170
|
+
|
|
171
|
+
// Step 8: Record deleted artifacts
|
|
172
|
+
artifacts.forEach((artifact, index) => {
|
|
173
|
+
const status = index < removalResult.success ? 'deleted' : 'failed';
|
|
174
|
+
const errorMsg = removalResult.errors.find((e) => e.includes(artifact.path));
|
|
175
|
+
recorder.addDeletedArtifact(removalRecord!, artifact, status as 'deleted' | 'failed' | 'skipped', errorMsg);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
result.removedFiles = removalResult.success;
|
|
179
|
+
result.errors = removalResult.errors;
|
|
180
|
+
result.freedSpace = artifacts.reduce((total, a) => total + (a.size || 0), 0);
|
|
181
|
+
|
|
182
|
+
// Step 9: Verify removal
|
|
183
|
+
const verificationResult = await verificationModule.verifyRemoval(appName, artifactPaths);
|
|
184
|
+
recorder.updateVerificationStatus(removalRecord!, verificationResult.status);
|
|
185
|
+
Logger.info(`\n${verificationModule.getStatusMessage(verificationResult.status)}`);
|
|
186
|
+
|
|
187
|
+
if (removalResult.failed === 0) {
|
|
188
|
+
result.success = true;
|
|
189
|
+
recorder.updateCompletionStatus(removalRecord!, 'success');
|
|
190
|
+
Logger.success(`Successfully removed ${appName}`);
|
|
191
|
+
} else {
|
|
192
|
+
recorder.updateCompletionStatus(removalRecord!, removalResult.failed === removalResult.success + removalResult.failed ? 'partial' : 'failed');
|
|
193
|
+
Logger.warn(`Removed ${appName} with ${removalResult.failed} errors`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Step 10: Save removal record
|
|
197
|
+
if (removalRecord) {
|
|
198
|
+
const recordPath = recorder.saveRecord(removalRecord);
|
|
199
|
+
Logger.debug(`Removal record saved to: ${recordPath}`);
|
|
200
|
+
|
|
201
|
+
// Step 11: Generate and save report
|
|
202
|
+
const reportPath = ReportGenerator.saveReport(removalRecord, options.reportFormat ?? 'html');
|
|
203
|
+
Logger.success(`Report generated: ${reportPath}`);
|
|
204
|
+
|
|
205
|
+
// Display report summary
|
|
206
|
+
ReportGenerator.displayReport(removalRecord);
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
result.success = false;
|
|
210
|
+
result.errors?.push((error as Error).message);
|
|
211
|
+
Logger.error(`Failed to remove ${appName}: ${(error as Error).message}`);
|
|
212
|
+
|
|
213
|
+
if (removalRecord) {
|
|
214
|
+
recorder.updateCompletionStatus(removalRecord, 'failed');
|
|
215
|
+
const recordPath = recorder.saveRecord(removalRecord);
|
|
216
|
+
Logger.debug(`Failed removal record saved to: ${recordPath}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async removeViaPackageManager(
|
|
224
|
+
appName: string,
|
|
225
|
+
installMethod: string
|
|
226
|
+
): Promise<boolean> {
|
|
227
|
+
try {
|
|
228
|
+
switch (installMethod) {
|
|
229
|
+
case 'npm': {
|
|
230
|
+
const npm = new NpmManager();
|
|
231
|
+
return await npm.removePackage(appName);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'brew': {
|
|
235
|
+
const brew = new BrewManager();
|
|
236
|
+
return await brew.removePackage(appName);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'apt':
|
|
240
|
+
case 'yum':
|
|
241
|
+
case 'dnf': {
|
|
242
|
+
const linux = new LinuxManager();
|
|
243
|
+
return await linux.removePackage(appName);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
default:
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
Logger.debug(`Package manager removal failed: ${(error as Error).message}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private isDirectory(filePath: string): boolean {
|
|
256
|
+
try {
|
|
257
|
+
const fs = require('fs');
|
|
258
|
+
return fs.statSync(filePath).isDirectory();
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async rollback(backupPath: string): Promise<boolean> {
|
|
265
|
+
try {
|
|
266
|
+
if (!pathExists(backupPath)) {
|
|
267
|
+
Logger.error(`Backup file not found: ${backupPath}`);
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
Logger.info('Restoring from backup...');
|
|
272
|
+
execSync(`tar -xzf "${backupPath}" -C /`);
|
|
273
|
+
Logger.success('Backup restored successfully');
|
|
274
|
+
return true;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
Logger.error(`Failed to restore backup: ${(error as Error).message}`);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|