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,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
+ }