appclean 1.8.0 → 2.0.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/GUI_IMPLEMENTATION_STATUS.md +143 -0
- package/MD_Files/INDEX.md +51 -0
- package/MD_Files/PUBLICATION_SUCCESS_REPORT.md +227 -0
- package/PHASE2_COMPLETION.md +281 -0
- package/PHASE3_COMPLETION.md +364 -0
- package/README.md +446 -376
- package/assets/logo.svg +34 -0
- package/dist/core/appUpdateChecker.js +12 -16
- package/dist/core/appUpdateChecker.js.map +1 -1
- package/dist/core/detector.js +14 -18
- package/dist/core/detector.js.map +1 -1
- package/dist/core/duplicateFileFinder.js +12 -19
- package/dist/core/duplicateFileFinder.js.map +1 -1
- package/dist/core/orphanedDependencyDetector.js +19 -26
- package/dist/core/orphanedDependencyDetector.js.map +1 -1
- package/dist/core/performanceOptimizer.js +6 -10
- package/dist/core/performanceOptimizer.js.map +1 -1
- package/dist/core/permissionHandler.js +21 -25
- package/dist/core/permissionHandler.js.map +1 -1
- package/dist/core/pluginSystem.js +9 -13
- package/dist/core/pluginSystem.js.map +1 -1
- package/dist/core/removalRecorder.js +12 -19
- package/dist/core/removalRecorder.js.map +1 -1
- package/dist/core/remover.js +59 -66
- package/dist/core/remover.js.map +1 -1
- package/dist/core/reportGenerator.d.ts +1 -1
- package/dist/core/reportGenerator.d.ts.map +1 -1
- package/dist/core/reportGenerator.js +27 -34
- package/dist/core/reportGenerator.js.map +1 -1
- package/dist/core/scheduledCleanup.js +23 -30
- package/dist/core/scheduledCleanup.js.map +1 -1
- package/dist/core/serviceFileDetector.js +24 -31
- package/dist/core/serviceFileDetector.js.map +1 -1
- package/dist/core/verificationModule.js +10 -14
- package/dist/core/verificationModule.js.map +1 -1
- package/dist/index.js +190 -90
- package/dist/index.js.map +1 -1
- package/dist/managers/brewManager.d.ts.map +1 -1
- package/dist/managers/brewManager.js +35 -41
- package/dist/managers/brewManager.js.map +1 -1
- package/dist/managers/customManager.d.ts +2 -1
- package/dist/managers/customManager.d.ts.map +1 -1
- package/dist/managers/customManager.js +79 -53
- package/dist/managers/customManager.js.map +1 -1
- package/dist/managers/linuxManager.js +29 -36
- package/dist/managers/linuxManager.js.map +1 -1
- package/dist/managers/npmManager.js +27 -34
- package/dist/managers/npmManager.js.map +1 -1
- package/dist/types/index.js +1 -2
- package/dist/ui/client/api/client.d.ts +24 -0
- package/dist/ui/client/api/client.d.ts.map +1 -0
- package/dist/ui/client/api/client.js +96 -0
- package/dist/ui/client/api/client.js.map +1 -0
- package/dist/ui/client/app.d.ts +7 -0
- package/dist/ui/client/app.d.ts.map +1 -0
- package/dist/ui/client/app.js +71 -0
- package/dist/ui/client/app.js.map +1 -0
- package/dist/ui/client/index.html +107 -0
- package/dist/ui/client/pages/appDetails.d.ts +8 -0
- package/dist/ui/client/pages/appDetails.d.ts.map +1 -0
- package/dist/ui/client/pages/appDetails.js +287 -0
- package/dist/ui/client/pages/appDetails.js.map +1 -0
- package/dist/ui/client/pages/appSearch.d.ts +2 -0
- package/dist/ui/client/pages/appSearch.d.ts.map +1 -0
- package/dist/ui/client/pages/appSearch.js +210 -0
- package/dist/ui/client/pages/appSearch.js.map +1 -0
- package/dist/ui/client/pages/dashboard.d.ts +2 -0
- package/dist/ui/client/pages/dashboard.d.ts.map +1 -0
- package/dist/ui/client/pages/dashboard.js +154 -0
- package/dist/ui/client/pages/dashboard.js.map +1 -0
- package/dist/ui/client/pages/settings.d.ts +7 -0
- package/dist/ui/client/pages/settings.d.ts.map +1 -0
- package/dist/ui/client/pages/settings.js +279 -0
- package/dist/ui/client/pages/settings.js.map +1 -0
- package/dist/ui/client/state/appStore.d.ts +38 -0
- package/dist/ui/client/state/appStore.d.ts.map +1 -0
- package/dist/ui/client/state/appStore.js +121 -0
- package/dist/ui/client/state/appStore.js.map +1 -0
- package/dist/ui/client/state/dashboardStore.d.ts +31 -0
- package/dist/ui/client/state/dashboardStore.d.ts.map +1 -0
- package/dist/ui/client/state/dashboardStore.js +70 -0
- package/dist/ui/client/state/dashboardStore.js.map +1 -0
- package/dist/ui/client/state/uiStore.d.ts +43 -0
- package/dist/ui/client/state/uiStore.d.ts.map +1 -0
- package/dist/ui/client/state/uiStore.js +109 -0
- package/dist/ui/client/state/uiStore.js.map +1 -0
- package/dist/ui/client/styles/animations.css +327 -0
- package/dist/ui/client/styles/base.css +214 -0
- package/dist/ui/client/styles/components.css +400 -0
- package/dist/ui/client/styles/layout.css +224 -0
- package/dist/ui/client/styles/variables.css +140 -0
- package/dist/ui/client/utils/events.d.ts +19 -0
- package/dist/ui/client/utils/events.d.ts.map +1 -0
- package/dist/ui/client/utils/events.js +54 -0
- package/dist/ui/client/utils/events.js.map +1 -0
- package/dist/ui/client/utils/formatting.d.ts +11 -0
- package/dist/ui/client/utils/formatting.d.ts.map +1 -0
- package/dist/ui/client/utils/formatting.js +104 -0
- package/dist/ui/client/utils/formatting.js.map +1 -0
- package/dist/ui/client/utils/router.d.ts +25 -0
- package/dist/ui/client/utils/router.d.ts.map +1 -0
- package/dist/ui/client/utils/router.js +90 -0
- package/dist/ui/client/utils/router.js.map +1 -0
- package/dist/ui/guiServer.d.ts +8 -1
- package/dist/ui/guiServer.d.ts.map +1 -1
- package/dist/ui/guiServer.js +148 -110
- package/dist/ui/guiServer.js.map +1 -1
- package/dist/ui/menu.js +18 -27
- package/dist/ui/menu.js.map +1 -1
- package/dist/ui/prompts.js +34 -47
- package/dist/ui/prompts.js.map +1 -1
- package/dist/ui/server/middleware/errorHandler.d.ts +19 -0
- package/dist/ui/server/middleware/errorHandler.d.ts.map +1 -0
- package/dist/ui/server/middleware/errorHandler.js +100 -0
- package/dist/ui/server/middleware/errorHandler.js.map +1 -0
- package/dist/ui/server/routes/apps.d.ts +8 -0
- package/dist/ui/server/routes/apps.d.ts.map +1 -0
- package/dist/ui/server/routes/apps.js +74 -0
- package/dist/ui/server/routes/apps.js.map +1 -0
- package/dist/ui/server/routes/dashboard.d.ts +4 -0
- package/dist/ui/server/routes/dashboard.d.ts.map +1 -0
- package/dist/ui/server/routes/dashboard.js +57 -0
- package/dist/ui/server/routes/dashboard.js.map +1 -0
- package/dist/ui/server/routes/settings.d.ts +6 -0
- package/dist/ui/server/routes/settings.d.ts.map +1 -0
- package/dist/ui/server/routes/settings.js +31 -0
- package/dist/ui/server/routes/settings.js.map +1 -0
- package/dist/ui/server/services/appService.d.ts +45 -0
- package/dist/ui/server/services/appService.d.ts.map +1 -0
- package/dist/ui/server/services/appService.js +114 -0
- package/dist/ui/server/services/appService.js.map +1 -0
- package/dist/ui/server/services/removalService.d.ts +24 -0
- package/dist/ui/server/services/removalService.d.ts.map +1 -0
- package/dist/ui/server/services/removalService.js +83 -0
- package/dist/ui/server/services/removalService.js.map +1 -0
- package/dist/utils/filesystem.js +32 -49
- package/dist/utils/filesystem.js.map +1 -1
- package/dist/utils/logger.js +9 -18
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/platform.js +10 -22
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/upgrade.d.ts +22 -0
- package/dist/utils/upgrade.d.ts.map +1 -0
- package/dist/utils/upgrade.js +94 -0
- package/dist/utils/upgrade.js.map +1 -0
- package/package.json +4 -2
- package/src/core/appUpdateChecker.ts +1 -1
- package/src/core/detector.ts +6 -6
- package/src/core/duplicateFileFinder.ts +1 -1
- package/src/core/orphanedDependencyDetector.ts +2 -2
- package/src/core/performanceOptimizer.ts +1 -1
- package/src/core/permissionHandler.ts +2 -2
- package/src/core/pluginSystem.ts +1 -1
- package/src/core/removalRecorder.ts +2 -2
- package/src/core/remover.ts +11 -11
- package/src/core/reportGenerator.ts +2 -2
- package/src/core/scheduledCleanup.ts +2 -2
- package/src/core/serviceFileDetector.ts +2 -2
- package/src/core/verificationModule.ts +2 -2
- package/src/index.ts +133 -6
- package/src/managers/brewManager.ts +11 -9
- package/src/managers/customManager.ts +71 -30
- package/src/managers/linuxManager.ts +3 -3
- package/src/managers/npmManager.ts +3 -3
- package/src/ui/client/api/client.ts +163 -0
- package/src/ui/client/app.ts +121 -0
- package/src/ui/client/index.html +107 -0
- package/src/ui/client/pages/appDetails.ts +356 -0
- package/src/ui/client/pages/appSearch.ts +270 -0
- package/src/ui/client/pages/dashboard.ts +189 -0
- package/src/ui/client/pages/settings.ts +342 -0
- package/src/ui/client/state/appStore.ts +169 -0
- package/src/ui/client/state/dashboardStore.ts +113 -0
- package/src/ui/client/state/uiStore.ts +166 -0
- package/src/ui/client/styles/animations.css +327 -0
- package/src/ui/client/styles/base.css +214 -0
- package/src/ui/client/styles/components.css +400 -0
- package/src/ui/client/styles/layout.css +224 -0
- package/src/ui/client/styles/variables.css +140 -0
- package/src/ui/client/utils/events.ts +74 -0
- package/src/ui/client/utils/formatting.ts +157 -0
- package/src/ui/client/utils/router.ts +161 -0
- package/src/ui/guiServer.ts +206 -105
- package/src/ui/prompts.ts +1 -1
- package/src/ui/server/middleware/errorHandler.ts +174 -0
- package/src/ui/server/routes/apps.ts +132 -0
- package/src/ui/server/routes/dashboard.ts +93 -0
- package/src/ui/server/routes/settings.ts +63 -0
- package/src/ui/server/services/appService.ts +184 -0
- package/src/ui/server/services/removalService.ts +138 -0
- package/src/utils/upgrade.ts +143 -0
- package/tsconfig.json +3 -2
- package/INDEX.md +0 -165
- /package/{ACTION_CHECKLIST.md → MD_Files/ACTION_CHECKLIST.md} +0 -0
- /package/{APPCLEAN_SUMMARY.md → MD_Files/APPCLEAN_SUMMARY.md} +0 -0
- /package/{CHANGELOG.md → MD_Files/CHANGELOG.md} +0 -0
- /package/{CODE_OF_CONDUCT.md → MD_Files/CODE_OF_CONDUCT.md} +0 -0
- /package/{CODE_REVIEW_REPORT.md → MD_Files/CODE_REVIEW_REPORT.md} +0 -0
- /package/{COMMUNITY_POSTS.md → MD_Files/COMMUNITY_POSTS.md} +0 -0
- /package/{DEPLOYMENT_GUIDE.md → MD_Files/DEPLOYMENT_GUIDE.md} +0 -0
- /package/{DEPLOYMENT_STATUS.md → MD_Files/DEPLOYMENT_STATUS.md} +0 -0
- /package/{EXECUTIVE_REPORT.md → MD_Files/EXECUTIVE_REPORT.md} +0 -0
- /package/{GITHUB_OPTIMIZATION.md → MD_Files/GITHUB_OPTIMIZATION.md} +0 -0
- /package/{MARKETING_SUMMARY.md → MD_Files/MARKETING_SUMMARY.md} +0 -0
- /package/{NPM_PACKAGE_OPTIMIZATION.md → MD_Files/NPM_PACKAGE_OPTIMIZATION.md} +0 -0
- /package/{NPM_PUBLISH.md → MD_Files/NPM_PUBLISH.md} +0 -0
- /package/{PROJECT_SUMMARY.txt → MD_Files/PROJECT_SUMMARY.txt} +0 -0
- /package/{QUICKSTART.md → MD_Files/QUICKSTART.md} +0 -0
- /package/{SETUP_GITHUB.md → MD_Files/SETUP_GITHUB.md} +0 -0
- /package/{TESTING_SUMMARY.md → MD_Files/TESTING_SUMMARY.md} +0 -0
- /package/{setup-github.sh → MD_Files/setup-github.sh} +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Hash-Based Router for SPA
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type RouteHandler = (params: Record<string, string>) => void;
|
|
6
|
+
|
|
7
|
+
export interface Route {
|
|
8
|
+
path: string;
|
|
9
|
+
handler: RouteHandler;
|
|
10
|
+
title?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Router {
|
|
14
|
+
private routes: Route[] = [];
|
|
15
|
+
private currentRoute: Route | null = null;
|
|
16
|
+
private onRouteChange: ((route: Route) => void) | null = null;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.listenToHashChanges();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a route
|
|
24
|
+
*/
|
|
25
|
+
register(path: string, handler: RouteHandler, title?: string): void {
|
|
26
|
+
this.routes.push({ path, handler, title });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Navigate to a route
|
|
31
|
+
*/
|
|
32
|
+
navigate(path: string): void {
|
|
33
|
+
window.location.hash = `#/${path}`.replace('##', '#');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get current route path
|
|
38
|
+
*/
|
|
39
|
+
getCurrentPath(): string {
|
|
40
|
+
return window.location.hash.slice(2) || '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Listen to hash changes and trigger handlers
|
|
45
|
+
*/
|
|
46
|
+
private listenToHashChanges(): void {
|
|
47
|
+
window.addEventListener('hashchange', () => {
|
|
48
|
+
this.route();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Initial route on load
|
|
52
|
+
this.route();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Route the current hash to a handler
|
|
57
|
+
*/
|
|
58
|
+
private route(): void {
|
|
59
|
+
const path = this.getCurrentPath();
|
|
60
|
+
const route = this.matchRoute(path);
|
|
61
|
+
|
|
62
|
+
if (route) {
|
|
63
|
+
const params = this.extractParams(route.path, path);
|
|
64
|
+
this.currentRoute = route;
|
|
65
|
+
|
|
66
|
+
if (route.title) {
|
|
67
|
+
document.title = route.title;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
route.handler(params);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Route handler error:', error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.onRouteChange) {
|
|
77
|
+
this.onRouteChange(route);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
console.warn(`No route found for: ${path}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Match a path to a registered route
|
|
86
|
+
*/
|
|
87
|
+
private matchRoute(path: string): Route | null {
|
|
88
|
+
// Exact match first
|
|
89
|
+
const exactMatch = this.routes.find((r) => r.path === path || r.path === `/${path}`);
|
|
90
|
+
if (exactMatch) return exactMatch;
|
|
91
|
+
|
|
92
|
+
// Dynamic route match
|
|
93
|
+
for (const route of this.routes) {
|
|
94
|
+
if (this.pathMatches(route.path, path)) {
|
|
95
|
+
return route;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if a route path matches a given path
|
|
104
|
+
* Supports dynamic segments like /apps/:appName
|
|
105
|
+
*/
|
|
106
|
+
private pathMatches(routePath: string, actualPath: string): boolean {
|
|
107
|
+
const routeParts = routePath.split('/').filter(Boolean);
|
|
108
|
+
const actualParts = actualPath.split('/').filter(Boolean);
|
|
109
|
+
|
|
110
|
+
if (routeParts.length !== actualParts.length) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return routeParts.every((part, i) => {
|
|
115
|
+
return part.startsWith(':') || part === actualParts[i];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract params from a path
|
|
121
|
+
* Example: /apps/:appName with path /apps/myapp returns { appName: 'myapp' }
|
|
122
|
+
*/
|
|
123
|
+
private extractParams(routePath: string, actualPath: string): Record<string, string> {
|
|
124
|
+
const params: Record<string, string> = {};
|
|
125
|
+
const routeParts = routePath.split('/').filter(Boolean);
|
|
126
|
+
const actualParts = actualPath.split('/').filter(Boolean);
|
|
127
|
+
|
|
128
|
+
routeParts.forEach((part, i) => {
|
|
129
|
+
if (part.startsWith(':')) {
|
|
130
|
+
const paramName = part.slice(1);
|
|
131
|
+
params[paramName] = decodeURIComponent(actualParts[i]);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return params;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Register a callback for route changes
|
|
140
|
+
*/
|
|
141
|
+
onchange(callback: (route: Route) => void): void {
|
|
142
|
+
this.onRouteChange = callback;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get all registered routes
|
|
147
|
+
*/
|
|
148
|
+
getRoutes(): Route[] {
|
|
149
|
+
return this.routes;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get current route
|
|
154
|
+
*/
|
|
155
|
+
getCurrentRoute(): Route | null {
|
|
156
|
+
return this.currentRoute;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Export singleton instance
|
|
161
|
+
export const router = new Router();
|
package/src/ui/guiServer.ts
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GUI Server for AppClean
|
|
3
|
-
*
|
|
4
|
-
* v1.2.0 Feature
|
|
2
|
+
* GUI Server for AppClean v2.0.0
|
|
3
|
+
* Modern SPA with API endpoints
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
import { createServer } from 'http';
|
|
8
|
-
import {
|
|
6
|
+
import { createServer, IncomingMessage, ServerResponse } from 'http';
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
import { Logger } from '../utils/logger.js';
|
|
12
|
+
import { sendJson, sendError, parseQueryParams } from './server/middleware/errorHandler.js';
|
|
13
|
+
import { handleAppRoutes } from './server/routes/apps.js';
|
|
14
|
+
import { handleDashboardRoutes } from './server/routes/dashboard.js';
|
|
15
|
+
import { handleSettingsRoutes } from './server/routes/settings.js';
|
|
16
|
+
|
|
17
|
+
// ES module compatibility: Define __dirname and __filename
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
9
20
|
|
|
10
21
|
export class GUIServer {
|
|
11
22
|
private port: number = 3000;
|
|
12
23
|
private server: any;
|
|
24
|
+
private spaHtml: string | null = null;
|
|
13
25
|
|
|
14
26
|
constructor(port: number = 3000) {
|
|
15
27
|
this.port = port;
|
|
@@ -21,20 +33,19 @@ export class GUIServer {
|
|
|
21
33
|
async start(): Promise<void> {
|
|
22
34
|
Logger.info(`Starting AppClean GUI server on port ${this.port}...`);
|
|
23
35
|
|
|
36
|
+
// Try to load SPA HTML (from compiled dist)
|
|
37
|
+
this.loadSPAHtml();
|
|
38
|
+
|
|
24
39
|
this.server = createServer((req, res) => {
|
|
25
|
-
|
|
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
|
-
}
|
|
40
|
+
this.handleRequest(req, res);
|
|
34
41
|
});
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
this.server.listen(this.port, () => {
|
|
45
|
+
Logger.success(`✨ AppClean GUI running at http://localhost:${this.port}`);
|
|
46
|
+
Logger.info('Press Ctrl+C to stop the server');
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
38
49
|
});
|
|
39
50
|
}
|
|
40
51
|
|
|
@@ -49,107 +60,197 @@ export class GUIServer {
|
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
/**
|
|
52
|
-
*
|
|
63
|
+
* Main request handler
|
|
53
64
|
*/
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
66
|
+
const url = req.url || '/';
|
|
67
|
+
const method = req.method || 'GET';
|
|
68
|
+
const pathname = url.split('?')[0];
|
|
69
|
+
|
|
70
|
+
// Set CORS headers
|
|
71
|
+
this.setCORSHeaders(res);
|
|
72
|
+
|
|
73
|
+
// Handle preflight requests
|
|
74
|
+
if (method === 'OPTIONS') {
|
|
75
|
+
res.writeHead(200);
|
|
76
|
+
res.end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Route API requests
|
|
82
|
+
if (pathname.startsWith('/api/')) {
|
|
83
|
+
return this.handleAPIRequest(method, pathname, req, res);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Serve static assets
|
|
87
|
+
if (pathname.startsWith('/static/')) {
|
|
88
|
+
return this.serveStaticAsset(pathname, res);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Serve SPA for all other routes
|
|
92
|
+
this.serveSPA(res);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
Logger.error(`Request error: ${(error as Error).message}`);
|
|
95
|
+
sendError(res, 'Internal server error', 500);
|
|
96
|
+
}
|
|
58
97
|
}
|
|
59
98
|
|
|
60
99
|
/**
|
|
61
|
-
*
|
|
100
|
+
* Handle API requests
|
|
62
101
|
*/
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
|
102
|
+
private handleAPIRequest(
|
|
103
|
+
method: string,
|
|
104
|
+
pathname: string,
|
|
105
|
+
req: IncomingMessage,
|
|
106
|
+
res: ServerResponse
|
|
107
|
+
): void {
|
|
108
|
+
// Try app routes
|
|
109
|
+
if (handleAppRoutes(method, pathname, req, res)) {
|
|
110
|
+
return;
|
|
85
111
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
max-width: 800px;
|
|
91
|
-
width: 100%;
|
|
92
|
-
padding: 40px;
|
|
93
|
-
}
|
|
94
|
-
h1 {
|
|
95
|
-
color: #333;
|
|
96
|
-
margin-bottom: 10px;
|
|
112
|
+
|
|
113
|
+
// Try dashboard routes
|
|
114
|
+
if (handleDashboardRoutes(method, pathname, req, res)) {
|
|
115
|
+
return;
|
|
97
116
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
|
|
118
|
+
// Try settings routes
|
|
119
|
+
if (handleSettingsRoutes(method, pathname, req, res)) {
|
|
120
|
+
return;
|
|
102
121
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
|
|
123
|
+
// Unknown endpoint
|
|
124
|
+
sendError(res, 'API endpoint not found', 404);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Serve static assets (CSS, JS)
|
|
129
|
+
*/
|
|
130
|
+
private serveStaticAsset(pathname: string, res: ServerResponse): void {
|
|
131
|
+
// Remove /static/ prefix
|
|
132
|
+
const relativePath = pathname.slice(8);
|
|
133
|
+
|
|
134
|
+
// Security: prevent directory traversal
|
|
135
|
+
if (relativePath.includes('..')) {
|
|
136
|
+
sendError(res, 'Access denied', 403);
|
|
137
|
+
return;
|
|
112
138
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
|
|
140
|
+
// Construct file path
|
|
141
|
+
const filePath = join(__dirname, 'client', relativePath);
|
|
142
|
+
|
|
143
|
+
// Check if file exists
|
|
144
|
+
if (!existsSync(filePath)) {
|
|
145
|
+
sendError(res, 'Asset not found', 404);
|
|
146
|
+
return;
|
|
117
147
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const content = readFileSync(filePath);
|
|
151
|
+
const contentType = this.getContentType(filePath);
|
|
152
|
+
|
|
153
|
+
res.writeHead(200, {
|
|
154
|
+
'Content-Type': contentType,
|
|
155
|
+
'Cache-Control': 'public, max-age=3600', // 1 hour
|
|
156
|
+
});
|
|
157
|
+
res.end(content);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
Logger.warn(`Failed to serve asset ${pathname}: ${(error as Error).message}`);
|
|
160
|
+
sendError(res, 'Failed to load asset', 500);
|
|
124
161
|
}
|
|
125
|
-
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Serve SPA HTML
|
|
166
|
+
*/
|
|
167
|
+
private serveSPA(res: ServerResponse): void {
|
|
168
|
+
if (!this.spaHtml) {
|
|
169
|
+
// Fallback: serve minimal HTML with error message
|
|
170
|
+
const fallbackHtml = `
|
|
171
|
+
<!DOCTYPE html>
|
|
172
|
+
<html>
|
|
173
|
+
<head>
|
|
174
|
+
<meta charset="UTF-8">
|
|
175
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
176
|
+
<title>AppClean</title>
|
|
126
177
|
</head>
|
|
127
|
-
<body>
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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>
|
|
178
|
+
<body style="font-family: system-ui; padding: 20px; text-align: center;">
|
|
179
|
+
<h1>⚠️ GUI Not Ready</h1>
|
|
180
|
+
<p>The SPA assets haven't been compiled yet.</p>
|
|
181
|
+
<p>Run <code>npm run build</code> to compile the TypeScript/CSS files.</p>
|
|
182
|
+
<p>For now, use the CLI: <code>appclean --help</code></p>
|
|
151
183
|
</body>
|
|
152
|
-
</html
|
|
153
|
-
|
|
184
|
+
</html>`;
|
|
185
|
+
|
|
186
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
187
|
+
res.end(fallbackHtml);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
res.writeHead(200, {
|
|
192
|
+
'Content-Type': 'text/html',
|
|
193
|
+
'Cache-Control': 'no-cache',
|
|
194
|
+
});
|
|
195
|
+
res.end(this.spaHtml);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Load SPA HTML from compiled dist
|
|
200
|
+
*/
|
|
201
|
+
private loadSPAHtml(): void {
|
|
202
|
+
const htmlPath = join(__dirname, 'client', 'index.html');
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
if (existsSync(htmlPath)) {
|
|
206
|
+
this.spaHtml = readFileSync(htmlPath, 'utf-8');
|
|
207
|
+
Logger.debug('✓ Loaded SPA HTML');
|
|
208
|
+
} else {
|
|
209
|
+
Logger.warn(`⚠️ SPA HTML not found at ${htmlPath}`);
|
|
210
|
+
Logger.info('Make sure to run: npm run build');
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
Logger.warn(`Failed to load SPA HTML: ${(error as Error).message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Determine content type from file extension
|
|
219
|
+
*/
|
|
220
|
+
private getContentType(filePath: string): string {
|
|
221
|
+
const ext = filePath.toLowerCase().split('.').pop();
|
|
222
|
+
|
|
223
|
+
const typeMap: Record<string, string> = {
|
|
224
|
+
'js': 'application/javascript; charset=utf-8',
|
|
225
|
+
'css': 'text/css; charset=utf-8',
|
|
226
|
+
'html': 'text/html; charset=utf-8',
|
|
227
|
+
'json': 'application/json; charset=utf-8',
|
|
228
|
+
'svg': 'image/svg+xml',
|
|
229
|
+
'png': 'image/png',
|
|
230
|
+
'jpg': 'image/jpeg',
|
|
231
|
+
'jpeg': 'image/jpeg',
|
|
232
|
+
'gif': 'image/gif',
|
|
233
|
+
'ico': 'image/x-icon',
|
|
234
|
+
'webp': 'image/webp',
|
|
235
|
+
'woff': 'font/woff',
|
|
236
|
+
'woff2': 'font/woff2',
|
|
237
|
+
'ttf': 'font/ttf',
|
|
238
|
+
'eot': 'application/vnd.ms-fontobject',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return typeMap[ext || ''] || 'application/octet-stream';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Set CORS headers
|
|
246
|
+
*/
|
|
247
|
+
private setCORSHeaders(res: ServerResponse): void {
|
|
248
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
249
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
250
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
251
|
+
res.setHeader('Access-Control-Max-Age', '3600');
|
|
154
252
|
}
|
|
155
253
|
}
|
|
254
|
+
|
|
255
|
+
// Export for use in CLI
|
|
256
|
+
export default GUIServer;
|
package/src/ui/prompts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { InstalledApp, ArtifactPath } from '../types';
|
|
3
|
-
import { Logger, formatBytes } from '../utils/logger';
|
|
3
|
+
import { Logger, formatBytes } from '../utils/logger.js';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
|
|
6
6
|
export async function promptSearchQuery(): Promise<string> {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Middleware
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
6
|
+
|
|
7
|
+
export interface ApiError {
|
|
8
|
+
success: false;
|
|
9
|
+
error: string;
|
|
10
|
+
statusCode?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ApiSuccess<T> {
|
|
14
|
+
success: true;
|
|
15
|
+
data: T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ApiResponse<T> = ApiSuccess<T> | ApiError;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Send JSON response
|
|
22
|
+
*/
|
|
23
|
+
export function sendJson<T>(
|
|
24
|
+
res: ServerResponse,
|
|
25
|
+
data: T,
|
|
26
|
+
statusCode = 200
|
|
27
|
+
): void {
|
|
28
|
+
res.statusCode = statusCode;
|
|
29
|
+
res.setHeader('Content-Type', 'application/json');
|
|
30
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
31
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
32
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
33
|
+
|
|
34
|
+
const response: ApiSuccess<T> = {
|
|
35
|
+
success: true,
|
|
36
|
+
data,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
res.end(JSON.stringify(response));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Send error response
|
|
44
|
+
*/
|
|
45
|
+
export function sendError(
|
|
46
|
+
res: ServerResponse,
|
|
47
|
+
message: string,
|
|
48
|
+
statusCode = 400
|
|
49
|
+
): void {
|
|
50
|
+
res.statusCode = statusCode;
|
|
51
|
+
res.setHeader('Content-Type', 'application/json');
|
|
52
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
53
|
+
|
|
54
|
+
const response: ApiError = {
|
|
55
|
+
success: false,
|
|
56
|
+
error: message,
|
|
57
|
+
statusCode,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
res.end(JSON.stringify(response));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse URL query parameters
|
|
65
|
+
*/
|
|
66
|
+
export function parseQueryParams(url: string): Record<string, string> {
|
|
67
|
+
const params: Record<string, string> = {};
|
|
68
|
+
const queryStart = url.indexOf('?');
|
|
69
|
+
|
|
70
|
+
if (queryStart === -1) return params;
|
|
71
|
+
|
|
72
|
+
const queryString = url.substring(queryStart + 1);
|
|
73
|
+
const pairs = queryString.split('&');
|
|
74
|
+
|
|
75
|
+
pairs.forEach((pair) => {
|
|
76
|
+
const [key, value] = pair.split('=');
|
|
77
|
+
if (key) {
|
|
78
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return params;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse URL path and extract parameters
|
|
87
|
+
* Example: /api/apps/:appName with path /api/apps/myapp returns { appName: 'myapp' }
|
|
88
|
+
*/
|
|
89
|
+
export function extractPathParams(
|
|
90
|
+
pattern: string,
|
|
91
|
+
path: string
|
|
92
|
+
): Record<string, string> | null {
|
|
93
|
+
const patternParts = pattern.split('/').filter(Boolean);
|
|
94
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
95
|
+
|
|
96
|
+
if (patternParts.length !== pathParts.length) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const params: Record<string, string> = {};
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
103
|
+
const patternPart = patternParts[i];
|
|
104
|
+
|
|
105
|
+
if (patternPart.startsWith(':')) {
|
|
106
|
+
const paramName = patternPart.slice(1);
|
|
107
|
+
params[paramName] = decodeURIComponent(pathParts[i]);
|
|
108
|
+
} else if (patternPart !== pathParts[i]) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return params;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse request body as JSON
|
|
118
|
+
*/
|
|
119
|
+
export function parseBody(req: IncomingMessage): Promise<any> {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
let data = '';
|
|
122
|
+
|
|
123
|
+
req.on('data', (chunk) => {
|
|
124
|
+
data += chunk;
|
|
125
|
+
// Prevent abuse: reject if body > 1MB
|
|
126
|
+
if (data.length > 1024 * 1024) {
|
|
127
|
+
reject(new Error('Request body too large'));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
req.on('end', () => {
|
|
132
|
+
try {
|
|
133
|
+
resolve(data ? JSON.parse(data) : {});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
reject(new Error('Invalid JSON'));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
req.on('error', reject);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Match URL pattern
|
|
145
|
+
*/
|
|
146
|
+
export function matchPattern(pattern: string, url: string): boolean {
|
|
147
|
+
const patternParts = pattern.split('/').filter(Boolean);
|
|
148
|
+
const urlParts = url.split('/').filter(Boolean);
|
|
149
|
+
|
|
150
|
+
if (patternParts.length !== urlParts.length) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return patternParts.every((part, i) => {
|
|
155
|
+
return part.startsWith(':') || part === urlParts[i];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Async route handler wrapper
|
|
161
|
+
*/
|
|
162
|
+
export function asyncHandler(
|
|
163
|
+
handler: (req: IncomingMessage, res: ServerResponse, params?: any) => Promise<void>
|
|
164
|
+
) {
|
|
165
|
+
return async (req: IncomingMessage, res: ServerResponse, params?: any) => {
|
|
166
|
+
try {
|
|
167
|
+
await handler(req, res, params);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('Route handler error:', error);
|
|
170
|
+
const message = (error as Error).message || 'Internal server error';
|
|
171
|
+
sendError(res, message, 500);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|