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
package/src/index.ts ADDED
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { Detector } from './core/detector';
7
+ import { Remover } from './core/remover';
8
+ import {
9
+ showMainMenu,
10
+ showAppMenu,
11
+ showHeader,
12
+ showHelp,
13
+ } from './ui/menu';
14
+ import {
15
+ promptSearchQuery,
16
+ promptSelectApp,
17
+ promptConfirmRemoval,
18
+ promptRemovalOptions,
19
+ promptInstallMethodFilter,
20
+ promptSortBy,
21
+ displayAppDetails,
22
+ promptFinalConfirmation,
23
+ } from './ui/prompts';
24
+ import { Logger, formatBytes } from './utils/logger';
25
+ import { InstalledApp } from './types';
26
+
27
+ const VERSION = '1.8.0';
28
+
29
+ async function interactiveMode(): Promise<void> {
30
+ showHeader();
31
+
32
+ const detector = new Detector();
33
+
34
+ let running = true;
35
+
36
+ while (running) {
37
+ const action = await showMainMenu();
38
+
39
+ switch (action) {
40
+ case 'search': {
41
+ await handleSearch(detector);
42
+ break;
43
+ }
44
+
45
+ case 'list-all': {
46
+ await handleListAll(detector);
47
+ break;
48
+ }
49
+
50
+ case 'help': {
51
+ showHelp();
52
+ await new Promise((resolve) =>
53
+ setTimeout(resolve, 3000)
54
+ );
55
+ break;
56
+ }
57
+
58
+ case 'exit': {
59
+ running = false;
60
+ Logger.space();
61
+ Logger.info('Goodbye!');
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ async function handleSearch(detector: Detector): Promise<void> {
69
+ Logger.space();
70
+ const query = await promptSearchQuery();
71
+
72
+ if (!query.trim()) {
73
+ Logger.warn('Please enter a search query');
74
+ return;
75
+ }
76
+
77
+ const spinner = ora('Searching for apps...').start();
78
+
79
+ try {
80
+ const results = await detector.searchApps({ query });
81
+
82
+ spinner.succeed(`Found ${results.length} app(s)`);
83
+ Logger.space();
84
+
85
+ if (results.length === 0) {
86
+ Logger.warn('No apps found matching your query');
87
+ return;
88
+ }
89
+
90
+ const selectedApp = await promptSelectApp(results);
91
+
92
+ if (!selectedApp) {
93
+ return;
94
+ }
95
+
96
+ await handleAppSelected(selectedApp, detector);
97
+ } catch (error) {
98
+ spinner.fail((error as Error).message);
99
+ }
100
+ }
101
+
102
+ async function handleListAll(detector: Detector): Promise<void> {
103
+ Logger.space();
104
+
105
+ const filterMethod = await promptInstallMethodFilter();
106
+ const sortBy = await promptSortBy();
107
+
108
+ const spinner = ora('Scanning installed apps...').start();
109
+
110
+ try {
111
+ const apps = await detector.searchApps({
112
+ installMethod: (filterMethod as any) || undefined,
113
+ sortBy,
114
+ });
115
+
116
+ spinner.succeed(`Found ${apps.length} app(s)`);
117
+ Logger.space();
118
+
119
+ if (apps.length === 0) {
120
+ Logger.warn('No apps found');
121
+ return;
122
+ }
123
+
124
+ // Display apps in a table format
125
+ const table = apps.map((app) => ({
126
+ Name: app.name,
127
+ Version: app.version,
128
+ Method: app.installMethod,
129
+ Size: app.size ? formatBytes(app.size) : 'N/A',
130
+ }));
131
+
132
+ Logger.table(table);
133
+ Logger.space();
134
+
135
+ const selectedApp = await promptSelectApp(apps);
136
+
137
+ if (!selectedApp) {
138
+ return;
139
+ }
140
+
141
+ await handleAppSelected(selectedApp, detector);
142
+ } catch (error) {
143
+ spinner.fail((error as Error).message);
144
+ }
145
+ }
146
+
147
+ async function handleAppSelected(
148
+ app: InstalledApp,
149
+ detector: Detector
150
+ ): Promise<void> {
151
+ Logger.space();
152
+
153
+ const action = await showAppMenu();
154
+
155
+ switch (action) {
156
+ case 'details': {
157
+ const spinner = ora('Analyzing artifacts...').start();
158
+
159
+ try {
160
+ const artifacts = await detector.findArtifacts(
161
+ app.name,
162
+ app.installMethod
163
+ );
164
+
165
+ spinner.succeed('Analysis complete');
166
+
167
+ displayAppDetails(app, artifacts);
168
+
169
+ Logger.space();
170
+ } catch (error) {
171
+ spinner.fail((error as Error).message);
172
+ }
173
+
174
+ break;
175
+ }
176
+
177
+ case 'remove': {
178
+ await handleRemoveApp(app, detector);
179
+ break;
180
+ }
181
+
182
+ case 'back':
183
+ default:
184
+ break;
185
+ }
186
+ }
187
+
188
+ async function handleRemoveApp(
189
+ app: InstalledApp,
190
+ detector: Detector
191
+ ): Promise<void> {
192
+ const confirmed = await promptConfirmRemoval(app.name);
193
+
194
+ if (!confirmed) {
195
+ Logger.info('Removal cancelled');
196
+ return;
197
+ }
198
+
199
+ const options = await promptRemovalOptions();
200
+
201
+ Logger.space();
202
+ const spinner = ora('Analyzing artifacts...').start();
203
+
204
+ try {
205
+ const artifacts = await detector.findArtifacts(
206
+ app.name,
207
+ app.installMethod
208
+ );
209
+
210
+ spinner.succeed('Analysis complete');
211
+
212
+ const remover = new Remover();
213
+
214
+ if (options.dryRun) {
215
+ Logger.info(`${chalk.bold('DRY RUN:')} Preview of files to be removed:`);
216
+ Logger.space();
217
+ await remover.previewRemoval(artifacts);
218
+ Logger.space();
219
+ Logger.warn('This is a preview only. No files were removed.');
220
+ } else {
221
+ const finalConfirm = await promptFinalConfirmation(app.name);
222
+
223
+ if (!finalConfirm) {
224
+ Logger.info('Removal cancelled');
225
+ return;
226
+ }
227
+
228
+ const removeSpinner = ora('Removing app and artifacts...').start();
229
+
230
+ try {
231
+ const result = await remover.removeApp(app.name, app.installMethod, artifacts, {
232
+ createBackup: options.createBackup,
233
+ });
234
+
235
+ if (result.success) {
236
+ removeSpinner.succeed(
237
+ `Successfully removed ${app.name} (freed ${formatBytes(result.freedSpace)})`
238
+ );
239
+
240
+ if (result.backupPath) {
241
+ Logger.info(`Backup saved at: ${result.backupPath}`);
242
+ }
243
+ } else {
244
+ removeSpinner.warn(`Removal completed with issues`);
245
+
246
+ if (result.errors && result.errors.length > 0) {
247
+ Logger.warn('Errors:');
248
+ result.errors.forEach((error) => console.log(` • ${error}`));
249
+ }
250
+ }
251
+ } catch (error) {
252
+ removeSpinner.fail((error as Error).message);
253
+ }
254
+ }
255
+ } catch (error) {
256
+ spinner.fail((error as Error).message);
257
+ }
258
+
259
+ Logger.space();
260
+ }
261
+
262
+ async function main(): Promise<void> {
263
+ const program = new Command();
264
+
265
+ program
266
+ .name('appclean')
267
+ .description(
268
+ 'Intelligently find and remove applications with all their artifacts'
269
+ )
270
+ .version(VERSION);
271
+
272
+ program
273
+ .command('search [query]')
274
+ .description('Search for installed applications')
275
+ .action(async (query) => {
276
+ const detector = new Detector();
277
+
278
+ const searchQuery = query || (await promptSearchQuery());
279
+
280
+ const spinner = ora('Searching for apps...').start();
281
+
282
+ try {
283
+ const results = await detector.searchApps({ query: searchQuery });
284
+
285
+ spinner.succeed(`Found ${results.length} app(s)`);
286
+
287
+ if (results.length === 0) {
288
+ Logger.warn('No apps found');
289
+ return;
290
+ }
291
+
292
+ Logger.space();
293
+ const selectedApp = await promptSelectApp(results);
294
+
295
+ if (selectedApp) {
296
+ await handleAppSelected(selectedApp, detector);
297
+ }
298
+ } catch (error) {
299
+ spinner.fail((error as Error).message);
300
+ }
301
+ });
302
+
303
+ program
304
+ .command('list')
305
+ .description('List all installed applications')
306
+ .action(async () => {
307
+ const detector = new Detector();
308
+ const spinner = ora('Scanning installed apps...').start();
309
+
310
+ try {
311
+ const apps = await detector.searchApps({ sortBy: 'name' });
312
+
313
+ spinner.succeed(`Found ${apps.length} app(s)`);
314
+
315
+ if (apps.length === 0) {
316
+ Logger.warn('No apps found');
317
+ return;
318
+ }
319
+
320
+ Logger.space();
321
+
322
+ const table = apps.map((app) => ({
323
+ Name: app.name,
324
+ Version: app.version,
325
+ Method: app.installMethod,
326
+ }));
327
+
328
+ Logger.table(table);
329
+ } catch (error) {
330
+ spinner.fail((error as Error).message);
331
+ }
332
+ });
333
+
334
+ program
335
+ .command('analyze <appName>')
336
+ .description('Analyze an application and show its artifacts')
337
+ .action(async (appName) => {
338
+ const detector = new Detector();
339
+ const spinner = ora('Analyzing app...').start();
340
+
341
+ try {
342
+ const apps = await detector.searchApps({ query: appName });
343
+
344
+ if (apps.length === 0) {
345
+ spinner.fail('App not found');
346
+ return;
347
+ }
348
+
349
+ const app = apps[0];
350
+ const artifacts = await detector.findArtifacts(app.name, app.installMethod);
351
+
352
+ spinner.succeed('Analysis complete');
353
+
354
+ displayAppDetails(app, artifacts);
355
+ } catch (error) {
356
+ spinner.fail((error as Error).message);
357
+ }
358
+ });
359
+
360
+ program
361
+ .command('remove <appName>')
362
+ .description('Remove an application')
363
+ .option('--dry-run', 'Preview without removing')
364
+ .option('--backup', 'Create backup before removal')
365
+ .option('--force', 'Skip confirmation prompts')
366
+ .action(async (appName, options) => {
367
+ const detector = new Detector();
368
+ const spinner = ora('Searching for app...').start();
369
+
370
+ try {
371
+ const apps = await detector.searchApps({ query: appName });
372
+
373
+ if (apps.length === 0) {
374
+ spinner.fail('App not found');
375
+ return;
376
+ }
377
+
378
+ const app = apps[0];
379
+
380
+ spinner.text = 'Analyzing artifacts...';
381
+ const artifacts = await detector.findArtifacts(app.name, app.installMethod);
382
+
383
+ spinner.succeed('Analysis complete');
384
+
385
+ const remover = new Remover();
386
+
387
+ Logger.space();
388
+ Logger.info(`App: ${chalk.cyan(app.name)}`);
389
+ Logger.info(`Method: ${chalk.yellow(app.installMethod)}`);
390
+ Logger.space();
391
+
392
+ if (options.dryRun) {
393
+ Logger.info('DRY RUN - Preview of files to be removed:');
394
+ Logger.space();
395
+ await remover.previewRemoval(artifacts);
396
+ } else {
397
+ if (!options.force) {
398
+ const confirmed = await promptFinalConfirmation(app.name);
399
+ if (!confirmed) {
400
+ Logger.info('Removal cancelled');
401
+ return;
402
+ }
403
+ }
404
+
405
+ const removeSpinner = ora('Removing app...').start();
406
+
407
+ const result = await remover.removeApp(
408
+ app.name,
409
+ app.installMethod,
410
+ artifacts,
411
+ { createBackup: options.backup }
412
+ );
413
+
414
+ if (result.success) {
415
+ removeSpinner.succeed(
416
+ `Removed ${app.name} (freed ${formatBytes(result.freedSpace)})`
417
+ );
418
+ } else {
419
+ removeSpinner.warn('Removal completed with errors');
420
+ }
421
+ }
422
+ } catch (error) {
423
+ spinner.fail((error as Error).message);
424
+ }
425
+ });
426
+
427
+ program.on('command:*', () => {
428
+ if (process.argv.length < 3) {
429
+ interactiveMode().catch((error) => {
430
+ Logger.error((error as Error).message);
431
+ process.exit(1);
432
+ });
433
+ }
434
+ });
435
+
436
+ program.parse(process.argv);
437
+
438
+ if (process.argv.length < 3) {
439
+ interactiveMode().catch((error) => {
440
+ Logger.error((error as Error).message);
441
+ process.exit(1);
442
+ });
443
+ }
444
+ }
445
+
446
+ main().catch((error) => {
447
+ Logger.error((error as Error).message);
448
+ process.exit(1);
449
+ });
@@ -0,0 +1,149 @@
1
+ import { execSync } from 'child_process';
2
+ import path from 'path';
3
+ import { getHomeDir } from '../utils/platform';
4
+ import { pathExists, listDirectory } from '../utils/filesystem';
5
+ import { InstalledApp, ArtifactPath } from '../types';
6
+ import { Logger } from '../utils/logger';
7
+
8
+ export class BrewManager {
9
+ private brewPrefix: string;
10
+
11
+ constructor() {
12
+ this.brewPrefix = this.getBrewPrefix();
13
+ }
14
+
15
+ private getBrewPrefix(): string {
16
+ try {
17
+ return execSync('brew --prefix').toString().trim();
18
+ } catch {
19
+ // Default based on architecture
20
+ if (process.arch === 'arm64') {
21
+ return '/opt/homebrew';
22
+ }
23
+ return '/usr/local';
24
+ }
25
+ }
26
+
27
+ async getInstalledPackages(): Promise<InstalledApp[]> {
28
+ const packages: InstalledApp[] = [];
29
+
30
+ try {
31
+ const output = execSync('brew list --json').toString();
32
+ const data = JSON.parse(output) as any[];
33
+
34
+ for (const pkg of data) {
35
+ const cellPath = path.join(this.brewPrefix, 'Cellar', pkg);
36
+ packages.push({
37
+ name: pkg,
38
+ version: 'unknown',
39
+ installMethod: 'brew',
40
+ mainPath: cellPath,
41
+ installedDate: undefined,
42
+ });
43
+ }
44
+ } catch (error) {
45
+ Logger.debug('Failed to get brew packages: ' + (error as Error).message);
46
+ }
47
+
48
+ return packages;
49
+ }
50
+
51
+ async findArtifacts(appName: string): Promise<ArtifactPath[]> {
52
+ const artifacts: ArtifactPath[] = [];
53
+ const home = getHomeDir();
54
+
55
+ // Main Cellar directory
56
+ const cellarPath = path.join(this.brewPrefix, 'Cellar', appName);
57
+ if (pathExists(cellarPath)) {
58
+ artifacts.push({
59
+ path: cellarPath,
60
+ type: 'other',
61
+ size: 0,
62
+ description: 'Cellar directory',
63
+ });
64
+ }
65
+
66
+ // Binary links in bin
67
+ const binPath = path.join(this.brewPrefix, 'bin', appName);
68
+ if (pathExists(binPath)) {
69
+ artifacts.push({
70
+ path: binPath,
71
+ type: 'binary',
72
+ size: 0,
73
+ description: 'Binary symlink',
74
+ });
75
+ }
76
+
77
+ // Man pages
78
+ const manPath = path.join(this.brewPrefix, 'share', 'man', 'man1', `${appName}.1`);
79
+ if (pathExists(manPath)) {
80
+ artifacts.push({
81
+ path: manPath,
82
+ type: 'data',
83
+ size: 0,
84
+ description: 'Man page',
85
+ });
86
+ }
87
+
88
+ // Preferences (macOS)
89
+ const prefPath = path.join(home, 'Library', 'Preferences', `com.${appName}*`);
90
+ const prefDir = path.join(home, 'Library', 'Preferences');
91
+ if (pathExists(prefDir)) {
92
+ const files = listDirectory(prefDir);
93
+ for (const file of files) {
94
+ if (file.includes(appName)) {
95
+ artifacts.push({
96
+ path: path.join(prefDir, file),
97
+ type: 'config',
98
+ size: 0,
99
+ description: 'Preference file',
100
+ });
101
+ }
102
+ }
103
+ }
104
+
105
+ // Application Support (macOS)
106
+ const appSupportPath = path.join(
107
+ home,
108
+ 'Library',
109
+ 'Application Support',
110
+ appName
111
+ );
112
+ if (pathExists(appSupportPath)) {
113
+ artifacts.push({
114
+ path: appSupportPath,
115
+ type: 'config',
116
+ size: 0,
117
+ description: 'Application support directory',
118
+ });
119
+ }
120
+
121
+ // Launch agents/daemons (macOS)
122
+ const launchAgentsPath = path.join(home, 'Library', 'LaunchAgents');
123
+ if (pathExists(launchAgentsPath)) {
124
+ const files = listDirectory(launchAgentsPath);
125
+ for (const file of files) {
126
+ if (file.includes(appName)) {
127
+ artifacts.push({
128
+ path: path.join(launchAgentsPath, file),
129
+ type: 'service',
130
+ size: 0,
131
+ description: 'Launch agent',
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ return artifacts;
138
+ }
139
+
140
+ async removePackage(appName: string): Promise<boolean> {
141
+ try {
142
+ execSync(`brew uninstall ${appName}`);
143
+ return true;
144
+ } catch (error) {
145
+ Logger.debug('Failed to uninstall brew package: ' + (error as Error).message);
146
+ return false;
147
+ }
148
+ }
149
+ }