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,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GUI Server for AppClean
|
|
3
|
+
* Provides web-based GUI interface for macOS, Linux, and Windows
|
|
4
|
+
* v1.2.0 Feature
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createServer } from 'http';
|
|
8
|
+
import { Logger } from '../utils/logger';
|
|
9
|
+
|
|
10
|
+
export class GUIServer {
|
|
11
|
+
private port: number = 3000;
|
|
12
|
+
private server: any;
|
|
13
|
+
|
|
14
|
+
constructor(port: number = 3000) {
|
|
15
|
+
this.port = port;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Start GUI server
|
|
20
|
+
*/
|
|
21
|
+
async start(): Promise<void> {
|
|
22
|
+
Logger.info(`Starting AppClean GUI server on port ${this.port}...`);
|
|
23
|
+
|
|
24
|
+
this.server = createServer((req, res) => {
|
|
25
|
+
if (req.url === '/') {
|
|
26
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
27
|
+
res.end(this.getIndexHTML());
|
|
28
|
+
} else if (req.url?.startsWith('/api/')) {
|
|
29
|
+
this.handleAPIRequest(req, res);
|
|
30
|
+
} else {
|
|
31
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
32
|
+
res.end('Not Found');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.server.listen(this.port, () => {
|
|
37
|
+
Logger.success(`GUI server running at http://localhost:${this.port}`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stop GUI server
|
|
43
|
+
*/
|
|
44
|
+
async stop(): Promise<void> {
|
|
45
|
+
if (this.server) {
|
|
46
|
+
this.server.close();
|
|
47
|
+
Logger.info('GUI server stopped');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Handle API requests
|
|
53
|
+
*/
|
|
54
|
+
private handleAPIRequest(req: any, res: any): void {
|
|
55
|
+
// API endpoints for GUI
|
|
56
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end(JSON.stringify({ message: 'API endpoint' }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get index HTML for GUI
|
|
62
|
+
*/
|
|
63
|
+
private getIndexHTML(): string {
|
|
64
|
+
return `
|
|
65
|
+
<!DOCTYPE html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="UTF-8">
|
|
69
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
70
|
+
<title>AppClean GUI - v1.2.0</title>
|
|
71
|
+
<style>
|
|
72
|
+
* {
|
|
73
|
+
margin: 0;
|
|
74
|
+
padding: 0;
|
|
75
|
+
box-sizing: border-box;
|
|
76
|
+
}
|
|
77
|
+
body {
|
|
78
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
79
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
80
|
+
min-height: 100vh;
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
padding: 20px;
|
|
85
|
+
}
|
|
86
|
+
.container {
|
|
87
|
+
background: white;
|
|
88
|
+
border-radius: 12px;
|
|
89
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
90
|
+
max-width: 800px;
|
|
91
|
+
width: 100%;
|
|
92
|
+
padding: 40px;
|
|
93
|
+
}
|
|
94
|
+
h1 {
|
|
95
|
+
color: #333;
|
|
96
|
+
margin-bottom: 10px;
|
|
97
|
+
}
|
|
98
|
+
.version {
|
|
99
|
+
color: #666;
|
|
100
|
+
font-size: 14px;
|
|
101
|
+
margin-bottom: 30px;
|
|
102
|
+
}
|
|
103
|
+
.feature-badge {
|
|
104
|
+
display: inline-block;
|
|
105
|
+
background: #667eea;
|
|
106
|
+
color: white;
|
|
107
|
+
padding: 8px 16px;
|
|
108
|
+
border-radius: 20px;
|
|
109
|
+
font-size: 12px;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
margin-bottom: 20px;
|
|
112
|
+
}
|
|
113
|
+
p {
|
|
114
|
+
color: #666;
|
|
115
|
+
line-height: 1.6;
|
|
116
|
+
margin-bottom: 20px;
|
|
117
|
+
}
|
|
118
|
+
.coming-soon {
|
|
119
|
+
background: #f0f4ff;
|
|
120
|
+
border-left: 4px solid #667eea;
|
|
121
|
+
padding: 20px;
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
margin-top: 30px;
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
126
|
+
</head>
|
|
127
|
+
<body>
|
|
128
|
+
<div class="container">
|
|
129
|
+
<h1>🎨 AppClean GUI</h1>
|
|
130
|
+
<p class="version">v1.2.0 - GUI Application Feature</p>
|
|
131
|
+
<span class="feature-badge">Coming Soon</span>
|
|
132
|
+
|
|
133
|
+
<p>
|
|
134
|
+
A beautiful, intuitive graphical user interface for AppClean, bringing the power of
|
|
135
|
+
intelligent app removal to users who prefer a visual interface.
|
|
136
|
+
</p>
|
|
137
|
+
|
|
138
|
+
<div class="coming-soon">
|
|
139
|
+
<h3>🚀 Features Coming in v1.2.0</h3>
|
|
140
|
+
<ul style="margin-left: 20px; margin-top: 10px;">
|
|
141
|
+
<li>✨ Modern, responsive GUI design</li>
|
|
142
|
+
<li>🖥️ Cross-platform support (macOS, Linux, Windows)</li>
|
|
143
|
+
<li>🔍 Visual app search and discovery</li>
|
|
144
|
+
<li>📊 Beautiful artifact visualization</li>
|
|
145
|
+
<li>🗑️ Drag-and-drop app removal</li>
|
|
146
|
+
<li>📈 Real-time removal progress</li>
|
|
147
|
+
<li>📋 Interactive report viewer</li>
|
|
148
|
+
</ul>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</body>
|
|
152
|
+
</html>
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/ui/menu.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export type MainMenuAction =
|
|
5
|
+
| 'search'
|
|
6
|
+
| 'browse'
|
|
7
|
+
| 'list-all'
|
|
8
|
+
| 'help'
|
|
9
|
+
| 'exit';
|
|
10
|
+
|
|
11
|
+
export async function showMainMenu(): Promise<MainMenuAction> {
|
|
12
|
+
const { action } = await inquirer.prompt([
|
|
13
|
+
{
|
|
14
|
+
type: 'list',
|
|
15
|
+
name: 'action',
|
|
16
|
+
message: 'What would you like to do?',
|
|
17
|
+
choices: [
|
|
18
|
+
{ name: '🔍 Search for an app', value: 'search' },
|
|
19
|
+
{ name: '📋 List all installed apps', value: 'list-all' },
|
|
20
|
+
{ name: '❓ Help', value: 'help' },
|
|
21
|
+
{ name: '❌ Exit', value: 'exit' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
return action;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function showAppMenu(): Promise<'remove' | 'details' | 'back'> {
|
|
30
|
+
const { action } = await inquirer.prompt([
|
|
31
|
+
{
|
|
32
|
+
type: 'list',
|
|
33
|
+
name: 'action',
|
|
34
|
+
message: 'What would you like to do?',
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: '📊 View details and artifacts', value: 'details' },
|
|
37
|
+
{ name: '🗑️ Remove this app', value: 'remove' },
|
|
38
|
+
{ name: '⬅️ Back to search', value: 'back' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
return action;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function showHeader(): void {
|
|
47
|
+
console.clear();
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.cyan(`
|
|
50
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
51
|
+
║ ║
|
|
52
|
+
║ ${chalk.bold('AppClean - App Uninstaller')} ║
|
|
53
|
+
║ Remove apps and all their artifacts ║
|
|
54
|
+
║ ║
|
|
55
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
56
|
+
`)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function showHelp(): void {
|
|
61
|
+
console.log(
|
|
62
|
+
chalk.cyan(`
|
|
63
|
+
${chalk.bold('AppClean - Help')}
|
|
64
|
+
|
|
65
|
+
${chalk.bold('Features:')}
|
|
66
|
+
• Search and find all installed applications
|
|
67
|
+
• Identify installation method (npm, brew, apt, custom, etc)
|
|
68
|
+
• Locate all related files, configs, and caches
|
|
69
|
+
• Safely remove apps with dry-run preview
|
|
70
|
+
• Create backups before deletion
|
|
71
|
+
• Cross-platform support (macOS, Linux, Windows)
|
|
72
|
+
|
|
73
|
+
${chalk.bold('Commands:')}
|
|
74
|
+
appclean Start interactive mode
|
|
75
|
+
appclean search <name> Search for an app
|
|
76
|
+
appclean remove <name> Remove an app
|
|
77
|
+
appclean list List all apps
|
|
78
|
+
appclean analyze <name> Show app artifacts
|
|
79
|
+
|
|
80
|
+
${chalk.bold('Options:')}
|
|
81
|
+
--dry-run Preview without deleting
|
|
82
|
+
--backup Create backup before removal
|
|
83
|
+
--help Show this help message
|
|
84
|
+
--version Show version
|
|
85
|
+
|
|
86
|
+
${chalk.bold('Examples:')}
|
|
87
|
+
appclean search nodejs Search for nodejs
|
|
88
|
+
appclean remove myapp --dry-run Preview removal
|
|
89
|
+
appclean list | grep npm Find npm-installed apps
|
|
90
|
+
|
|
91
|
+
${chalk.bold('Safety:')}
|
|
92
|
+
• Always previews what will be deleted
|
|
93
|
+
• Use --dry-run first to see what will happen
|
|
94
|
+
• Use --backup to create backups before deletion
|
|
95
|
+
• Double confirmation before actual removal
|
|
96
|
+
|
|
97
|
+
${chalk.bold('Website:')} https://github.com/YOUR_USERNAME/appclean
|
|
98
|
+
`)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { InstalledApp, ArtifactPath } from '../types';
|
|
3
|
+
import { Logger, formatBytes } from '../utils/logger';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export async function promptSearchQuery(): Promise<string> {
|
|
7
|
+
const { query } = await inquirer.prompt([
|
|
8
|
+
{
|
|
9
|
+
type: 'input',
|
|
10
|
+
name: 'query',
|
|
11
|
+
message: 'Search for an app (partial name):',
|
|
12
|
+
default: '',
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
return query;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function promptSelectApp(apps: InstalledApp[]): Promise<InstalledApp | null> {
|
|
19
|
+
if (apps.length === 0) {
|
|
20
|
+
Logger.warn('No apps found');
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const choices = apps.map((app) => ({
|
|
25
|
+
name: `${app.name} (${app.installMethod}) - v${app.version}`,
|
|
26
|
+
value: app,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const { app } = await inquirer.prompt([
|
|
30
|
+
{
|
|
31
|
+
type: 'list',
|
|
32
|
+
name: 'app',
|
|
33
|
+
message: 'Select an app to remove:',
|
|
34
|
+
choices: [
|
|
35
|
+
...choices,
|
|
36
|
+
new inquirer.Separator(),
|
|
37
|
+
{ name: 'Cancel', value: null },
|
|
38
|
+
],
|
|
39
|
+
pageSize: 10,
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
return app;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function promptConfirmRemoval(appName: string): Promise<boolean> {
|
|
47
|
+
const { confirmed } = await inquirer.prompt([
|
|
48
|
+
{
|
|
49
|
+
type: 'confirm',
|
|
50
|
+
name: 'confirmed',
|
|
51
|
+
message: chalk.yellow(`Are you sure you want to remove ${appName}?`),
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
return confirmed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function promptRemovalOptions(): Promise<{
|
|
59
|
+
dryRun: boolean;
|
|
60
|
+
createBackup: boolean;
|
|
61
|
+
}> {
|
|
62
|
+
const { options } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: 'checkbox',
|
|
65
|
+
name: 'options',
|
|
66
|
+
message: 'Removal options:',
|
|
67
|
+
choices: [
|
|
68
|
+
{
|
|
69
|
+
name: 'Dry run (preview without removing)',
|
|
70
|
+
value: 'dryRun',
|
|
71
|
+
checked: true,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Create backup before removal',
|
|
75
|
+
value: 'backup',
|
|
76
|
+
checked: false,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
dryRun: options.includes('dryRun'),
|
|
84
|
+
createBackup: options.includes('backup'),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function promptInstallMethodFilter(): Promise<string | null> {
|
|
89
|
+
const { method } = await inquirer.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: 'list',
|
|
92
|
+
name: 'method',
|
|
93
|
+
message: 'Filter by installation method:',
|
|
94
|
+
choices: [
|
|
95
|
+
{ name: 'All', value: null },
|
|
96
|
+
{ name: 'npm', value: 'npm' },
|
|
97
|
+
{ name: 'yarn', value: 'yarn' },
|
|
98
|
+
{ name: 'pnpm', value: 'pnpm' },
|
|
99
|
+
{ name: 'Homebrew', value: 'brew' },
|
|
100
|
+
{ name: 'apt', value: 'apt' },
|
|
101
|
+
{ name: 'yum', value: 'yum' },
|
|
102
|
+
{ name: 'dnf', value: 'dnf' },
|
|
103
|
+
{ name: 'Custom', value: 'custom' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
return method;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function promptSortBy(): Promise<'name' | 'date' | 'size'> {
|
|
112
|
+
const { sortBy } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: 'list',
|
|
115
|
+
name: 'sortBy',
|
|
116
|
+
message: 'Sort results by:',
|
|
117
|
+
choices: [
|
|
118
|
+
{ name: 'Name', value: 'name' },
|
|
119
|
+
{ name: 'Installation Date', value: 'date' },
|
|
120
|
+
{ name: 'Size', value: 'size' },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
return sortBy;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function displayAppDetails(app: InstalledApp, artifacts: ArtifactPath[]): void {
|
|
129
|
+
Logger.space();
|
|
130
|
+
Logger.info(`App: ${chalk.cyan(app.name)}`);
|
|
131
|
+
Logger.info(`Method: ${chalk.yellow(app.installMethod)}`);
|
|
132
|
+
Logger.info(`Version: ${app.version}`);
|
|
133
|
+
Logger.info(`Location: ${app.mainPath}`);
|
|
134
|
+
Logger.space();
|
|
135
|
+
|
|
136
|
+
Logger.info('Associated files and directories:');
|
|
137
|
+
Logger.space();
|
|
138
|
+
|
|
139
|
+
let totalSize = 0;
|
|
140
|
+
|
|
141
|
+
for (const artifact of artifacts) {
|
|
142
|
+
const size = artifact.size || 0;
|
|
143
|
+
totalSize += size;
|
|
144
|
+
|
|
145
|
+
const icon =
|
|
146
|
+
artifact.type === 'binary'
|
|
147
|
+
? '⚙️'
|
|
148
|
+
: artifact.type === 'config'
|
|
149
|
+
? '⚙️'
|
|
150
|
+
: artifact.type === 'cache'
|
|
151
|
+
? '📦'
|
|
152
|
+
: '📁';
|
|
153
|
+
|
|
154
|
+
console.log(
|
|
155
|
+
` ${icon} [${artifact.type.padEnd(7)}] ${formatBytes(size).padEnd(10)} ${artifact.path}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
Logger.space();
|
|
160
|
+
Logger.info(`Total space to be freed: ${chalk.green(formatBytes(totalSize))}`);
|
|
161
|
+
Logger.space();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function promptFinalConfirmation(appName: string): Promise<boolean> {
|
|
165
|
+
const { confirmed } = await inquirer.prompt([
|
|
166
|
+
{
|
|
167
|
+
type: 'confirm',
|
|
168
|
+
name: 'confirmed',
|
|
169
|
+
message: chalk.red(
|
|
170
|
+
`This action cannot be undone. Remove ${appName} and all its files?`
|
|
171
|
+
),
|
|
172
|
+
default: false,
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
return confirmed;
|
|
177
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function getFileSize(filePath: string): number {
|
|
5
|
+
try {
|
|
6
|
+
const stats = fs.statSync(filePath);
|
|
7
|
+
if (stats.isDirectory()) {
|
|
8
|
+
return getDirectorySize(filePath);
|
|
9
|
+
}
|
|
10
|
+
return stats.size;
|
|
11
|
+
} catch {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getDirectorySize(dirPath: string): number {
|
|
17
|
+
try {
|
|
18
|
+
let size = 0;
|
|
19
|
+
const files = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
20
|
+
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const fullPath = path.join(dirPath, file.name);
|
|
23
|
+
if (file.isDirectory()) {
|
|
24
|
+
size += getDirectorySize(fullPath);
|
|
25
|
+
} else {
|
|
26
|
+
const stats = fs.statSync(fullPath);
|
|
27
|
+
size += stats.size;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return size;
|
|
31
|
+
} catch {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function pathExists(filePath: string): boolean {
|
|
37
|
+
return fs.existsSync(filePath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isDirectory(filePath: string): boolean {
|
|
41
|
+
try {
|
|
42
|
+
return fs.statSync(filePath).isDirectory();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isFile(filePath: string): boolean {
|
|
49
|
+
try {
|
|
50
|
+
return fs.statSync(filePath).isFile();
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function listDirectory(dirPath: string): string[] {
|
|
57
|
+
try {
|
|
58
|
+
return fs.readdirSync(dirPath);
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function listDirectoryDeep(
|
|
65
|
+
dirPath: string,
|
|
66
|
+
maxDepth: number = 5,
|
|
67
|
+
currentDepth: number = 0
|
|
68
|
+
): string[] {
|
|
69
|
+
const files: string[] = [];
|
|
70
|
+
|
|
71
|
+
if (currentDepth >= maxDepth) return files;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Skip symlinks and permission issues
|
|
81
|
+
if (entry.isSymbolicLink()) continue;
|
|
82
|
+
|
|
83
|
+
files.push(fullPath);
|
|
84
|
+
|
|
85
|
+
if (entry.isDirectory() && currentDepth < maxDepth - 1) {
|
|
86
|
+
files.push(...listDirectoryDeep(fullPath, maxDepth, currentDepth + 1));
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Skip entries that can't be accessed
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function deleteFile(filePath: string): boolean {
|
|
101
|
+
try {
|
|
102
|
+
fs.unlinkSync(filePath);
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function deleteDirectory(dirPath: string): boolean {
|
|
110
|
+
try {
|
|
111
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function readFile(filePath: string): string | null {
|
|
119
|
+
try {
|
|
120
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function writeFile(filePath: string, content: string): boolean {
|
|
127
|
+
try {
|
|
128
|
+
const dir = path.dirname(filePath);
|
|
129
|
+
if (!fs.existsSync(dir)) {
|
|
130
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getModificationTime(filePath: string): Date | null {
|
|
140
|
+
try {
|
|
141
|
+
return fs.statSync(filePath).mtime;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export class Logger {
|
|
4
|
+
static info(message: string): void {
|
|
5
|
+
console.log(chalk.blue('ℹ'), message);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static success(message: string): void {
|
|
9
|
+
console.log(chalk.green('✓'), message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static warn(message: string): void {
|
|
13
|
+
console.log(chalk.yellow('⚠'), message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static error(message: string): void {
|
|
17
|
+
console.log(chalk.red('✗'), message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static debug(message: string): void {
|
|
21
|
+
if (process.env.DEBUG) {
|
|
22
|
+
console.log(chalk.gray('🐛'), message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static table(data: any[]): void {
|
|
27
|
+
console.table(data);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static space(): void {
|
|
31
|
+
console.log('');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatBytes(bytes: number): string {
|
|
36
|
+
if (bytes === 0) return '0 B';
|
|
37
|
+
|
|
38
|
+
const k = 1024;
|
|
39
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
40
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
41
|
+
|
|
42
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatDate(date: Date | undefined): string {
|
|
46
|
+
if (!date) return 'Unknown';
|
|
47
|
+
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
48
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { Platform } from '../types';
|
|
3
|
+
|
|
4
|
+
export function getPlatform(): Platform {
|
|
5
|
+
return (os.platform() as Platform);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isMacOS(): boolean {
|
|
9
|
+
return getPlatform() === 'darwin';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isLinux(): boolean {
|
|
13
|
+
return getPlatform() === 'linux';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isWindows(): boolean {
|
|
17
|
+
return getPlatform() === 'win32';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getHomeDir(): string {
|
|
21
|
+
return os.homedir();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getConfigDir(): string {
|
|
25
|
+
const platform = getPlatform();
|
|
26
|
+
const home = getHomeDir();
|
|
27
|
+
|
|
28
|
+
switch (platform) {
|
|
29
|
+
case 'darwin':
|
|
30
|
+
return home;
|
|
31
|
+
case 'linux':
|
|
32
|
+
return home;
|
|
33
|
+
case 'win32':
|
|
34
|
+
return process.env.APPDATA || home;
|
|
35
|
+
default:
|
|
36
|
+
return home;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getCommonPaths(subpath: string): string[] {
|
|
41
|
+
const platform = getPlatform();
|
|
42
|
+
const home = getHomeDir();
|
|
43
|
+
const paths: string[] = [];
|
|
44
|
+
|
|
45
|
+
switch (platform) {
|
|
46
|
+
case 'darwin':
|
|
47
|
+
paths.push(`${home}/.config/${subpath}`);
|
|
48
|
+
paths.push(`${home}/Library/Application Support/${subpath}`);
|
|
49
|
+
paths.push(`${home}/Library/Preferences/${subpath}`);
|
|
50
|
+
paths.push(`${home}/Library/Caches/${subpath}`);
|
|
51
|
+
paths.push(`${home}/Library/Logs/${subpath}`);
|
|
52
|
+
paths.push(`${home}/.${subpath}`);
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case 'linux':
|
|
56
|
+
paths.push(`${home}/.config/${subpath}`);
|
|
57
|
+
paths.push(`${home}/.local/share/${subpath}`);
|
|
58
|
+
paths.push(`${home}/.cache/${subpath}`);
|
|
59
|
+
paths.push(`${home}/.${subpath}`);
|
|
60
|
+
paths.push(`/var/log/${subpath}`);
|
|
61
|
+
paths.push(`/usr/local/etc/${subpath}`);
|
|
62
|
+
paths.push(`/etc/${subpath}`);
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'win32':
|
|
66
|
+
const appData = process.env.APPDATA || `${home}\\AppData\\Roaming`;
|
|
67
|
+
const localAppData = process.env.LOCALAPPDATA || `${home}\\AppData\\Local`;
|
|
68
|
+
paths.push(`${appData}\\${subpath}`);
|
|
69
|
+
paths.push(`${localAppData}\\${subpath}`);
|
|
70
|
+
paths.push(`${home}\\.${subpath}`);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return paths;
|
|
75
|
+
}
|