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,354 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { RemovalRecord, DeletedArtifact } from './removalRecorder';
5
+ import { formatBytes, formatDate } from '../utils/logger';
6
+
7
+ export const REMOVAL_DISCLAIMER = `
8
+ ╔════════════════════════════════════════════════════════════════════════════╗
9
+ ║ ║
10
+ ║ ⚠️ IMPORTANT DISCLAIMER ⚠️ ║
11
+ ║ ║
12
+ ║ AppClean is provided "AS IS" without any warranties or representations. ║
13
+ ║ ║
14
+ ║ By using this tool, you acknowledge and agree that: ║
15
+ ║ ║
16
+ ║ 1. AppClean provides functionality to detect and remove applications ║
17
+ ║ and their associated files from your system. ║
18
+ ║ ║
19
+ ║ 2. Data deletion is PERMANENT and IRREVERSIBLE. Please ensure you have ║
20
+ ║ created backups of any important data before using this tool. ║
21
+ ║ ║
22
+ ║ 3. While AppClean aims to be accurate, it may not detect all files ║
23
+ ║ or may encounter errors during deletion. Some files may remain ║
24
+ ║ orphaned on your system despite successful removal. ║
25
+ ║ ║
26
+ ║ 4. AppClean developers and maintainers assume NO LIABILITY or ║
27
+ ║ RESPONSIBILITY for any data loss, system damage, or any other ║
28
+ ║ consequences arising from the use of this tool. ║
29
+ ║ ║
30
+ ║ 5. You are solely responsible for verifying the safety of removal ║
31
+ ║ operations before execution and for maintaining your own backups. ║
32
+ ║ ║
33
+ ║ 6. By proceeding with app removal, you confirm that you understand ║
34
+ ║ and accept these terms and conditions. ║
35
+ ║ ║
36
+ ╚════════════════════════════════════════════════════════════════════════════╝
37
+ `;
38
+
39
+ export class ReportGenerator {
40
+ /**
41
+ * Generate HTML report
42
+ */
43
+ static generateHTMLReport(record: RemovalRecord): string {
44
+ const timestamp = formatDate(record.timestamp);
45
+ const totalFreed = formatBytes(record.totalSpaceFreed);
46
+
47
+ let html = `
48
+ <!DOCTYPE html>
49
+ <html lang="en">
50
+ <head>
51
+ <meta charset="UTF-8">
52
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
53
+ <title>AppClean Removal Report - ${record.appName}</title>
54
+ <style>
55
+ body {
56
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
57
+ margin: 0;
58
+ padding: 20px;
59
+ background: #f5f5f5;
60
+ color: #333;
61
+ }
62
+ .container {
63
+ max-width: 900px;
64
+ margin: 0 auto;
65
+ background: white;
66
+ padding: 30px;
67
+ border-radius: 8px;
68
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
69
+ }
70
+ h1 {
71
+ color: #2c3e50;
72
+ border-bottom: 3px solid #3b82f6;
73
+ padding-bottom: 10px;
74
+ }
75
+ h2 {
76
+ color: #34495e;
77
+ margin-top: 30px;
78
+ }
79
+ .summary {
80
+ background: #f8f9fa;
81
+ padding: 20px;
82
+ border-left: 4px solid #3b82f6;
83
+ margin: 20px 0;
84
+ border-radius: 4px;
85
+ }
86
+ .summary-item {
87
+ margin: 10px 0;
88
+ }
89
+ .summary-label {
90
+ font-weight: bold;
91
+ color: #2c3e50;
92
+ }
93
+ table {
94
+ width: 100%;
95
+ border-collapse: collapse;
96
+ margin: 20px 0;
97
+ }
98
+ th {
99
+ background: #3b82f6;
100
+ color: white;
101
+ padding: 12px;
102
+ text-align: left;
103
+ font-weight: 600;
104
+ }
105
+ td {
106
+ padding: 12px;
107
+ border-bottom: 1px solid #e0e0e0;
108
+ }
109
+ tr:hover {
110
+ background: #f5f5f5;
111
+ }
112
+ .status-deleted {
113
+ background: #d4edda;
114
+ color: #155724;
115
+ padding: 4px 8px;
116
+ border-radius: 4px;
117
+ }
118
+ .status-failed {
119
+ background: #f8d7da;
120
+ color: #721c24;
121
+ padding: 4px 8px;
122
+ border-radius: 4px;
123
+ }
124
+ .status-skipped {
125
+ background: #fff3cd;
126
+ color: #856404;
127
+ padding: 4px 8px;
128
+ border-radius: 4px;
129
+ }
130
+ .disclaimer {
131
+ background: #fff3cd;
132
+ border: 2px solid #ff6b6b;
133
+ padding: 20px;
134
+ margin: 20px 0;
135
+ border-radius: 4px;
136
+ color: #721c24;
137
+ }
138
+ .disclaimer h3 {
139
+ margin-top: 0;
140
+ color: #721c24;
141
+ }
142
+ .footer {
143
+ text-align: center;
144
+ color: #7f8c8d;
145
+ font-size: 12px;
146
+ margin-top: 40px;
147
+ padding-top: 20px;
148
+ border-top: 1px solid #e0e0e0;
149
+ }
150
+ .success {
151
+ color: #27ae60;
152
+ }
153
+ .warning {
154
+ color: #e67e22;
155
+ }
156
+ .error {
157
+ color: #e74c3c;
158
+ }
159
+ </style>
160
+ </head>
161
+ <body>
162
+ <div class="container">
163
+ <h1>📋 AppClean Removal Report</h1>
164
+
165
+ <div class="summary">
166
+ <div class="summary-item">
167
+ <span class="summary-label">Application:</span> ${record.appName}
168
+ </div>
169
+ <div class="summary-item">
170
+ <span class="summary-label">Installation Method:</span> ${record.installMethod}
171
+ </div>
172
+ <div class="summary-item">
173
+ <span class="summary-label">Removal Date:</span> ${timestamp}
174
+ </div>
175
+ <div class="summary-item">
176
+ <span class="summary-label">User Consent:</span>
177
+ <span class="${record.userConsent ? 'success' : 'error'}">
178
+ ${record.userConsent ? '✓ Yes' : '✗ No'}
179
+ </span>
180
+ </div>
181
+ <div class="summary-item">
182
+ <span class="summary-label">Completion Status:</span>
183
+ <span class="${record.completionStatus === 'success' ? 'success' : record.completionStatus === 'partial' ? 'warning' : 'error'}">
184
+ ${record.completionStatus.toUpperCase()}
185
+ </span>
186
+ </div>
187
+ <div class="summary-item">
188
+ <span class="summary-label">Verification Status:</span>
189
+ <span class="${record.verificationStatus === 'verified_removed' ? 'success' : 'warning'}">
190
+ ${record.verificationStatus.replace(/_/g, ' ').toUpperCase()}
191
+ </span>
192
+ </div>
193
+ <div class="summary-item">
194
+ <span class="summary-label">Total Space Freed:</span>
195
+ <span class="success">${totalFreed}</span>
196
+ </div>
197
+ </div>
198
+
199
+ <h2>Deletion Details</h2>
200
+ <table>
201
+ <thead>
202
+ <tr>
203
+ <th>File/Folder</th>
204
+ <th>Type</th>
205
+ <th>Size</th>
206
+ <th>Status</th>
207
+ </tr>
208
+ </thead>
209
+ <tbody>
210
+ ${record.artifactsDeleted
211
+ .map(
212
+ (artifact) => `
213
+ <tr>
214
+ <td><code>${artifact.path}</code></td>
215
+ <td>${artifact.type}</td>
216
+ <td>${formatBytes(artifact.size)}</td>
217
+ <td>
218
+ <span class="status-${artifact.status}">
219
+ ${artifact.status.toUpperCase()}
220
+ ${artifact.errorMessage ? ` - ${artifact.errorMessage}` : ''}
221
+ </span>
222
+ </td>
223
+ </tr>
224
+ `
225
+ )
226
+ .join('')}
227
+ </tbody>
228
+ </table>
229
+
230
+ <div class="disclaimer">
231
+ <h3>⚠️ DISCLAIMER & NO LIABILITY NOTICE</h3>
232
+ <p>
233
+ <strong>IMPORTANT:</strong> AppClean is provided "AS IS" without any warranties or representations.
234
+ This report documents the data removal operations performed by AppClean.
235
+ </p>
236
+ <p>
237
+ <strong>By using AppClean, you acknowledge that:</strong>
238
+ </p>
239
+ <ol>
240
+ <li>Data deletion is PERMANENT and IRREVERSIBLE</li>
241
+ <li>AppClean developers assume NO LIABILITY for any data loss</li>
242
+ <li>You are responsible for maintaining your own backups</li>
243
+ <li>Some files may remain orphaned despite successful removal</li>
244
+ <li>You have reviewed and consented to all removal operations</li>
245
+ </ol>
246
+ <p>
247
+ <strong>AppClean is NOT responsible for any consequences arising from the use of this tool.</strong>
248
+ </p>
249
+ </div>
250
+
251
+ <div class="footer">
252
+ <p>Generated by AppClean v1.1.0 | Report timestamp: ${new Date().toISOString()}</p>
253
+ <p>For support, visit: https://github.com/praveenkay/AppClean</p>
254
+ </div>
255
+ </div>
256
+ </body>
257
+ </html>
258
+ `;
259
+
260
+ return html;
261
+ }
262
+
263
+ /**
264
+ * Generate text report
265
+ */
266
+ static generateTextReport(record: RemovalRecord): string {
267
+ const timestamp = formatDate(record.timestamp);
268
+ const totalFreed = formatBytes(record.totalSpaceFreed);
269
+
270
+ let report = `
271
+ ═══════════════════════════════════════════════════════════════════════════════
272
+ 📋 APPCLEAN REMOVAL REPORT
273
+ ═══════════════════════════════════════════════════════════════════════════════
274
+
275
+ SUMMARY
276
+ ──────────────────────────────────────────────────────────────────────────────
277
+ Application: ${record.appName}
278
+ Installation Method: ${record.installMethod}
279
+ Removal Date: ${timestamp}
280
+ User Consent: ${record.userConsent ? '✓ Yes' : '✗ No'}
281
+ Completion Status: ${record.completionStatus.toUpperCase()}
282
+ Verification Status: ${record.verificationStatus.replace(/_/g, ' ').toUpperCase()}
283
+ Total Space Freed: ${totalFreed}
284
+
285
+ DELETED FILES & FOLDERS
286
+ ──────────────────────────────────────────────────────────────────────────────
287
+ `;
288
+
289
+ record.artifactsDeleted.forEach((artifact, index) => {
290
+ report += `\n${index + 1}. ${artifact.path}
291
+ Type: ${artifact.type}
292
+ Size: ${formatBytes(artifact.size)}
293
+ Status: ${artifact.status.toUpperCase()}`;
294
+
295
+ if (artifact.errorMessage) {
296
+ report += `\n Error: ${artifact.errorMessage}`;
297
+ }
298
+ });
299
+
300
+ report += `\n\n${REMOVAL_DISCLAIMER}`;
301
+
302
+ report += `
303
+
304
+ ═══════════════════════════════════════════════════════════════════════════════
305
+ END OF REPORT
306
+ ═══════════════════════════════════════════════════════════════════════════════
307
+
308
+ Generated by AppClean v1.1.0
309
+ Timestamp: ${new Date().toISOString()}
310
+ For support, visit: https://github.com/praveenkay/AppClean
311
+ `;
312
+
313
+ return report;
314
+ }
315
+
316
+ /**
317
+ * Save report to file
318
+ */
319
+ static saveReport(record: RemovalRecord, format: 'html' | 'text' = 'html'): string {
320
+ const home = path.resolve(process.env.HOME || '/root');
321
+ const reportsDir = path.join(home, '.appclean-reports');
322
+
323
+ // Create reports directory
324
+ if (!fs.existsSync(reportsDir)) {
325
+ fs.mkdirSync(reportsDir, { recursive: true });
326
+ }
327
+
328
+ const timestamp = record.timestamp.getTime();
329
+ const filename = `${record.appName}-removal-${timestamp}.${format === 'html' ? 'html' : 'txt'}`;
330
+ const filepath = path.join(reportsDir, filename);
331
+
332
+ const content = format === 'html'
333
+ ? this.generateHTMLReport(record)
334
+ : this.generateTextReport(record);
335
+
336
+ fs.writeFileSync(filepath, content, 'utf-8');
337
+
338
+ return filepath;
339
+ }
340
+
341
+ /**
342
+ * Display report in console
343
+ */
344
+ static displayReport(record: RemovalRecord): void {
345
+ console.log(chalk.cyan(REMOVAL_DISCLAIMER));
346
+ console.log('\n' + chalk.bold('Removal Summary:'));
347
+ console.log(chalk.gray(` App: ${record.appName}`));
348
+ console.log(chalk.gray(` Method: ${record.installMethod}`));
349
+ console.log(chalk.gray(` Timestamp: ${formatDate(record.timestamp)}`));
350
+ console.log(chalk.gray(` Space Freed: ${formatBytes(record.totalSpaceFreed)}`));
351
+ console.log(chalk.gray(` Status: ${record.completionStatus.toUpperCase()}`));
352
+ console.log(chalk.gray(` Verification: ${record.verificationStatus.replace(/_/g, ' ').toUpperCase()}`));
353
+ }
354
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Scheduled Cleanup Automation
3
+ * Automates regular cleanup of applications and artifacts
4
+ * v1.6.0 Feature
5
+ */
6
+
7
+ import { getHomeDir } from '../utils/platform';
8
+ import path from 'path';
9
+ import fs from 'fs';
10
+ import { Logger } from '../utils/logger';
11
+
12
+ export type CleanupFrequency = 'daily' | 'weekly' | 'monthly' | 'custom';
13
+
14
+ export interface CleanupSchedule {
15
+ id: string;
16
+ appName: string;
17
+ frequency: CleanupFrequency;
18
+ enabled: boolean;
19
+ lastRun?: Date;
20
+ nextRun?: Date;
21
+ customCron?: string;
22
+ dryRun: boolean;
23
+ createBackup: boolean;
24
+ }
25
+
26
+ export interface CleanupResult {
27
+ scheduleId: string;
28
+ appName: string;
29
+ timestamp: Date;
30
+ success: boolean;
31
+ filesRemoved: number;
32
+ spaceFreed: number;
33
+ errors: string[];
34
+ }
35
+
36
+ export class ScheduledCleanup {
37
+ private schedules: Map<string, CleanupSchedule> = new Map();
38
+ private schedulesFile: string;
39
+
40
+ constructor() {
41
+ const home = getHomeDir();
42
+ this.schedulesFile = path.join(home, '.appclean-schedules.json');
43
+ this.loadSchedules();
44
+ }
45
+
46
+ /**
47
+ * Create a new cleanup schedule
48
+ */
49
+ createSchedule(
50
+ appName: string,
51
+ frequency: CleanupFrequency,
52
+ options?: Partial<CleanupSchedule>
53
+ ): CleanupSchedule {
54
+ const id = `schedule-${Date.now()}`;
55
+ const schedule: CleanupSchedule = {
56
+ id,
57
+ appName,
58
+ frequency,
59
+ enabled: true,
60
+ dryRun: options?.dryRun ?? false,
61
+ createBackup: options?.createBackup ?? true,
62
+ ...options,
63
+ };
64
+
65
+ this.schedules.set(id, schedule);
66
+ this.saveSchedules();
67
+
68
+ Logger.info(`Cleanup schedule created: ${id} for ${appName} (${frequency})`);
69
+ return schedule;
70
+ }
71
+
72
+ /**
73
+ * Delete a cleanup schedule
74
+ */
75
+ deleteSchedule(scheduleId: string): boolean {
76
+ const deleted = this.schedules.delete(scheduleId);
77
+ if (deleted) {
78
+ this.saveSchedules();
79
+ Logger.info(`Cleanup schedule deleted: ${scheduleId}`);
80
+ }
81
+ return deleted;
82
+ }
83
+
84
+ /**
85
+ * Get all cleanup schedules
86
+ */
87
+ getSchedules(): CleanupSchedule[] {
88
+ return Array.from(this.schedules.values());
89
+ }
90
+
91
+ /**
92
+ * Get schedule by ID
93
+ */
94
+ getSchedule(scheduleId: string): CleanupSchedule | null {
95
+ return this.schedules.get(scheduleId) || null;
96
+ }
97
+
98
+ /**
99
+ * Update cleanup schedule
100
+ */
101
+ updateSchedule(scheduleId: string, updates: Partial<CleanupSchedule>): boolean {
102
+ const schedule = this.schedules.get(scheduleId);
103
+ if (schedule) {
104
+ Object.assign(schedule, updates);
105
+ this.saveSchedules();
106
+ Logger.info(`Cleanup schedule updated: ${scheduleId}`);
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+
112
+ /**
113
+ * Enable/disable schedule
114
+ */
115
+ setScheduleEnabled(scheduleId: string, enabled: boolean): void {
116
+ const schedule = this.schedules.get(scheduleId);
117
+ if (schedule) {
118
+ schedule.enabled = enabled;
119
+ this.saveSchedules();
120
+ Logger.info(`Cleanup schedule ${scheduleId} ${enabled ? 'enabled' : 'disabled'}`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Calculate next run time
126
+ */
127
+ calculateNextRun(schedule: CleanupSchedule): Date {
128
+ const now = new Date();
129
+
130
+ switch (schedule.frequency) {
131
+ case 'daily':
132
+ return new Date(now.getTime() + 24 * 60 * 60 * 1000);
133
+ case 'weekly':
134
+ return new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
135
+ case 'monthly':
136
+ return new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
137
+ case 'custom':
138
+ // Would parse cron expression
139
+ return new Date(now.getTime() + 24 * 60 * 60 * 1000);
140
+ default:
141
+ return now;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get schedules due for cleanup
147
+ */
148
+ getSchedulesDue(): CleanupSchedule[] {
149
+ const now = new Date();
150
+ return this.getSchedules().filter((schedule) => {
151
+ if (!schedule.enabled) return false;
152
+ if (!schedule.nextRun) return true;
153
+ return schedule.nextRun <= now;
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Record cleanup result
159
+ */
160
+ recordResult(result: CleanupResult): void {
161
+ const home = getHomeDir();
162
+ const resultsDir = path.join(home, '.appclean-cleanup-results');
163
+
164
+ if (!fs.existsSync(resultsDir)) {
165
+ fs.mkdirSync(resultsDir, { recursive: true });
166
+ }
167
+
168
+ const filename = `${result.scheduleId}-${result.timestamp.getTime()}.json`;
169
+ const filepath = path.join(resultsDir, filename);
170
+
171
+ fs.writeFileSync(filepath, JSON.stringify(result, null, 2), 'utf-8');
172
+ Logger.debug(`Cleanup result recorded: ${filepath}`);
173
+ }
174
+
175
+ /**
176
+ * Load schedules from file
177
+ */
178
+ private loadSchedules(): void {
179
+ try {
180
+ if (fs.existsSync(this.schedulesFile)) {
181
+ const content = fs.readFileSync(this.schedulesFile, 'utf-8');
182
+ const data = JSON.parse(content);
183
+
184
+ for (const schedule of data) {
185
+ this.schedules.set(schedule.id, schedule);
186
+ }
187
+ }
188
+ } catch (error) {
189
+ Logger.debug(`Failed to load schedules: ${(error as Error).message}`);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Save schedules to file
195
+ */
196
+ private saveSchedules(): void {
197
+ try {
198
+ const data = Array.from(this.schedules.values());
199
+ fs.writeFileSync(this.schedulesFile, JSON.stringify(data, null, 2), 'utf-8');
200
+ } catch (error) {
201
+ Logger.error(`Failed to save schedules: ${(error as Error).message}`);
202
+ }
203
+ }
204
+ }