appclean 1.9.0 → 2.0.2
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/npm-publish.yml +61 -0
- package/DEVELOPMENT.md +84 -0
- package/GUI_IMPLEMENTATION_STATUS.md +143 -0
- package/MD_Files/INDEX.md +51 -0
- package/PHASE2_COMPLETION.md +281 -0
- package/PHASE3_COMPLETION.md +364 -0
- package/README.md +411 -91
- package/RELEASE_GUIDE.md +236 -0
- 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 +118 -156
- package/dist/index.js.map +1 -1
- package/dist/managers/brewManager.js +30 -37
- package/dist/managers/brewManager.js.map +1 -1
- package/dist/managers/customManager.js +23 -30
- 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 +100 -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 +75 -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 +221 -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 +175 -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 +130 -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 +76 -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 +349 -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 +11 -5
- package/dist/ui/guiServer.d.ts.map +1 -1
- package/dist/ui/guiServer.js +180 -501
- 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 +2 -1
- package/dist/utils/upgrade.d.ts.map +1 -1
- package/dist/utils/upgrade.js +24 -15
- package/dist/utils/upgrade.js.map +1 -1
- package/package.json +4 -2
- package/scripts/publish-npm.sh +64 -0
- 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 +8 -8
- package/src/managers/brewManager.ts +3 -3
- package/src/managers/customManager.ts +2 -2
- package/src/managers/linuxManager.ts +3 -3
- package/src/managers/npmManager.ts +3 -3
- package/src/ui/client/api/client.ts +168 -0
- package/src/ui/client/app.ts +125 -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 +283 -0
- package/src/ui/client/pages/dashboard.ts +211 -0
- package/src/ui/client/pages/settings.ts +342 -0
- package/src/ui/client/state/appStore.ts +181 -0
- package/src/ui/client/state/dashboardStore.ts +123 -0
- package/src/ui/client/state/uiStore.ts +166 -0
- package/src/ui/client/styles/animations.css +349 -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 +245 -498
- 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 +19 -2
- 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/{PUBLICATION_SUCCESS_REPORT.md → MD_Files/PUBLICATION_SUCCESS_REPORT.md} +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
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
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Routes - Handle app listing, searching, analysis, and removal
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
6
|
+
import { appService } from '../services/appService.js';
|
|
7
|
+
import { removalService } from '../services/removalService.js';
|
|
8
|
+
import {
|
|
9
|
+
sendJson,
|
|
10
|
+
sendError,
|
|
11
|
+
parseQueryParams,
|
|
12
|
+
extractPathParams,
|
|
13
|
+
parseBody,
|
|
14
|
+
asyncHandler,
|
|
15
|
+
matchPattern,
|
|
16
|
+
} from '../middleware/errorHandler.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* GET /api/apps/list - Get paginated list of all apps
|
|
20
|
+
*/
|
|
21
|
+
export const listApps = asyncHandler(async (req, res) => {
|
|
22
|
+
const params = parseQueryParams(req.url || '');
|
|
23
|
+
const limit = Math.min(parseInt(params.limit || '50'), 100); // Max 100 per page
|
|
24
|
+
const offset = parseInt(params.offset || '0');
|
|
25
|
+
|
|
26
|
+
const result = await appService.listApps(limit, offset);
|
|
27
|
+
sendJson(res, result);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* GET /api/apps/search - Search, filter, and sort apps
|
|
32
|
+
*/
|
|
33
|
+
export const searchApps = asyncHandler(async (req, res) => {
|
|
34
|
+
const params = parseQueryParams(req.url || '');
|
|
35
|
+
|
|
36
|
+
const result = await appService.searchApps({
|
|
37
|
+
q: params.q,
|
|
38
|
+
method: params.method,
|
|
39
|
+
sort: (params.sort as any) || 'name',
|
|
40
|
+
limit: Math.min(parseInt(params.limit || '50'), 100),
|
|
41
|
+
offset: parseInt(params.offset || '0'),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
sendJson(res, result);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* GET /api/apps/:appName/analysis - Analyze app and get artifacts
|
|
49
|
+
*/
|
|
50
|
+
export const analyzeApp = asyncHandler(async (req, res, params) => {
|
|
51
|
+
if (!params?.appName) {
|
|
52
|
+
return sendError(res, 'App name is required', 400);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = await appService.analyzeApp(params.appName);
|
|
56
|
+
sendJson(res, result);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* GET /api/apps/:appName/preview - Get dry-run preview
|
|
61
|
+
*/
|
|
62
|
+
export const previewRemoval = asyncHandler(async (req, res, params) => {
|
|
63
|
+
if (!params?.appName) {
|
|
64
|
+
return sendError(res, 'App name is required', 400);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await appService.previewRemoval(params.appName);
|
|
68
|
+
sendJson(res, result);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* POST /api/apps/:appName/remove - Execute app removal
|
|
73
|
+
*/
|
|
74
|
+
export const removeApp = asyncHandler(async (req, res, params) => {
|
|
75
|
+
if (!params?.appName) {
|
|
76
|
+
return sendError(res, 'App name is required', 400);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const body = await parseBody(req);
|
|
80
|
+
const options = {
|
|
81
|
+
dryRun: body.dryRun || false,
|
|
82
|
+
createBackup: body.createBackup || false,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = await removalService.removeApp(params.appName, options);
|
|
86
|
+
sendJson(res, result);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Route matcher and handler
|
|
91
|
+
*/
|
|
92
|
+
export function handleAppRoutes(
|
|
93
|
+
method: string,
|
|
94
|
+
pathname: string,
|
|
95
|
+
req: IncomingMessage,
|
|
96
|
+
res: ServerResponse
|
|
97
|
+
): boolean {
|
|
98
|
+
// GET /api/apps/list
|
|
99
|
+
if (method === 'GET' && pathname === '/api/apps/list') {
|
|
100
|
+
listApps(req, res);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// GET /api/apps/search
|
|
105
|
+
if (method === 'GET' && pathname.startsWith('/api/apps/search')) {
|
|
106
|
+
searchApps(req, res);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// GET /api/apps/:appName/analysis
|
|
111
|
+
if (method === 'GET' && matchPattern('/api/apps/:appName/analysis', pathname)) {
|
|
112
|
+
const params = extractPathParams('/api/apps/:appName/analysis', pathname);
|
|
113
|
+
analyzeApp(req, res, params);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// GET /api/apps/:appName/preview
|
|
118
|
+
if (method === 'GET' && matchPattern('/api/apps/:appName/preview', pathname)) {
|
|
119
|
+
const params = extractPathParams('/api/apps/:appName/preview', pathname);
|
|
120
|
+
previewRemoval(req, res, params);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// POST /api/apps/:appName/remove
|
|
125
|
+
if (method === 'POST' && matchPattern('/api/apps/:appName/remove', pathname)) {
|
|
126
|
+
const params = extractPathParams('/api/apps/:appName/remove', pathname);
|
|
127
|
+
removeApp(req, res, params);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Routes - Handle statistics and metrics
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
6
|
+
import { Detector } from '../../../core/detector.js';
|
|
7
|
+
import { removalService } from '../services/removalService.js';
|
|
8
|
+
import { sendJson, asyncHandler } from '../middleware/errorHandler.js';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* GET /api/dashboard/stats - Get dashboard statistics
|
|
13
|
+
*/
|
|
14
|
+
export const getDashboardStats = asyncHandler(async (req, res) => {
|
|
15
|
+
const detector = new Detector();
|
|
16
|
+
|
|
17
|
+
// Get all apps
|
|
18
|
+
const allApps = await detector.searchApps({});
|
|
19
|
+
const totalApps = allApps.length;
|
|
20
|
+
|
|
21
|
+
// Calculate total space used
|
|
22
|
+
let totalSpaceUsed = 0;
|
|
23
|
+
for (const app of allApps) {
|
|
24
|
+
try {
|
|
25
|
+
const artifacts = await detector.findArtifacts(app.name, app.installMethod);
|
|
26
|
+
totalSpaceUsed += artifacts.reduce((sum, a) => sum + (a.size || 0), 0);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// Skip errors for individual apps
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Get disk usage
|
|
33
|
+
const diskUsagePercent = getDiskUsagePercent();
|
|
34
|
+
|
|
35
|
+
// Get session stats from removal service
|
|
36
|
+
const sessionRemovals = removalService.getSessionRemovals();
|
|
37
|
+
const sessionAppsRemoved = sessionRemovals.length;
|
|
38
|
+
const sessionSpaceFreed = sessionRemovals.reduce((sum, r) => sum + r.freedSpace, 0);
|
|
39
|
+
|
|
40
|
+
// Format recently removed for display
|
|
41
|
+
const recentlyRemoved = sessionRemovals.map((removal) => ({
|
|
42
|
+
appName: removal.appName,
|
|
43
|
+
timestamp: Date.now(), // Should track actual time, but using now for simplicity
|
|
44
|
+
freedSpace: removal.freedSpace,
|
|
45
|
+
filesRemoved: removal.removedFiles,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const stats = {
|
|
49
|
+
totalApps,
|
|
50
|
+
totalSpaceUsed,
|
|
51
|
+
sessionAppsRemoved,
|
|
52
|
+
sessionSpaceFreed,
|
|
53
|
+
diskUsagePercent,
|
|
54
|
+
recentlyRemoved,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
sendJson(res, stats);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get disk usage percentage
|
|
62
|
+
*/
|
|
63
|
+
function getDiskUsagePercent(): number {
|
|
64
|
+
try {
|
|
65
|
+
// Try using 'df' command on Unix systems
|
|
66
|
+
const result = execSync('df -h / | tail -1', { encoding: 'utf-8' });
|
|
67
|
+
const parts = result.trim().split(/\s+/);
|
|
68
|
+
const percentStr = parts[4]?.replace('%', '');
|
|
69
|
+
const percent = parseInt(percentStr || '0', 10);
|
|
70
|
+
return isNaN(percent) ? 0 : Math.min(percent, 100);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Fallback: return 0 if we can't determine
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Route matcher and handler
|
|
79
|
+
*/
|
|
80
|
+
export function handleDashboardRoutes(
|
|
81
|
+
method: string,
|
|
82
|
+
pathname: string,
|
|
83
|
+
req: IncomingMessage,
|
|
84
|
+
res: ServerResponse
|
|
85
|
+
): boolean {
|
|
86
|
+
// GET /api/dashboard/stats
|
|
87
|
+
if (method === 'GET' && pathname === '/api/dashboard/stats') {
|
|
88
|
+
getDashboardStats(req, res);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Routes - Handle version, upgrade, and uninstall
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
6
|
+
import { UpgradeManager } from '../../../utils/upgrade.js';
|
|
7
|
+
import { sendJson, sendError, asyncHandler } from '../middleware/errorHandler.js';
|
|
8
|
+
|
|
9
|
+
const upgradeManager = new UpgradeManager();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* GET /api/version - Check version and get update info
|
|
13
|
+
*/
|
|
14
|
+
export const checkVersion = asyncHandler(async (req, res) => {
|
|
15
|
+
const versionInfo = await upgradeManager.checkForUpdates();
|
|
16
|
+
sendJson(res, versionInfo);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* POST /api/upgrade - Upgrade to latest version
|
|
21
|
+
*/
|
|
22
|
+
export const upgrade = asyncHandler(async (req, res) => {
|
|
23
|
+
const result = await upgradeManager.upgrade();
|
|
24
|
+
sendJson(res, result);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* POST /api/uninstall - Uninstall AppClean
|
|
29
|
+
*/
|
|
30
|
+
export const uninstall = asyncHandler(async (req, res) => {
|
|
31
|
+
const result = await upgradeManager.uninstall();
|
|
32
|
+
sendJson(res, result);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Route matcher and handler
|
|
37
|
+
*/
|
|
38
|
+
export function handleSettingsRoutes(
|
|
39
|
+
method: string,
|
|
40
|
+
pathname: string,
|
|
41
|
+
req: IncomingMessage,
|
|
42
|
+
res: ServerResponse
|
|
43
|
+
): boolean {
|
|
44
|
+
// GET /api/version
|
|
45
|
+
if (method === 'GET' && pathname === '/api/version') {
|
|
46
|
+
checkVersion(req, res);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// POST /api/upgrade
|
|
51
|
+
if (method === 'POST' && pathname === '/api/upgrade') {
|
|
52
|
+
upgrade(req, res);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// POST /api/uninstall
|
|
57
|
+
if (method === 'POST' && pathname === '/api/uninstall') {
|
|
58
|
+
uninstall(req, res);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppService - Wraps Detector to provide app listing, searching, and analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Detector } from '../../../core/detector.js';
|
|
6
|
+
import { InstalledApp } from '../../../types/index.js';
|
|
7
|
+
|
|
8
|
+
export interface SearchQuery {
|
|
9
|
+
q?: string;
|
|
10
|
+
method?: string;
|
|
11
|
+
sort?: 'name' | 'size' | 'date';
|
|
12
|
+
limit?: number;
|
|
13
|
+
offset?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AppAnalysis {
|
|
17
|
+
app: InstalledApp;
|
|
18
|
+
artifacts: any[];
|
|
19
|
+
totalSize: number;
|
|
20
|
+
breakdown: {
|
|
21
|
+
binaries: number;
|
|
22
|
+
configs: number;
|
|
23
|
+
caches: number;
|
|
24
|
+
data: number;
|
|
25
|
+
logs: number;
|
|
26
|
+
other: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class AppService {
|
|
31
|
+
private detector: Detector;
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
this.detector = new Detector();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get paginated list of all apps
|
|
39
|
+
*/
|
|
40
|
+
async listApps(limit = 50, offset = 0): Promise<{
|
|
41
|
+
apps: InstalledApp[];
|
|
42
|
+
total: number;
|
|
43
|
+
page: number;
|
|
44
|
+
pageSize: number;
|
|
45
|
+
}> {
|
|
46
|
+
try {
|
|
47
|
+
const allApps = await this.detector.searchApps({ sortBy: 'name' });
|
|
48
|
+
const page = Math.floor(offset / limit) + 1;
|
|
49
|
+
const paginatedApps = allApps.slice(offset, offset + limit);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
apps: paginatedApps,
|
|
53
|
+
total: allApps.length,
|
|
54
|
+
page,
|
|
55
|
+
pageSize: limit,
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`Failed to list apps: ${(error as Error).message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Search, filter, and sort apps
|
|
64
|
+
*/
|
|
65
|
+
async searchApps(query: SearchQuery): Promise<{
|
|
66
|
+
apps: InstalledApp[];
|
|
67
|
+
count: number;
|
|
68
|
+
}> {
|
|
69
|
+
try {
|
|
70
|
+
const searchOptions: any = {
|
|
71
|
+
query: query.q || '',
|
|
72
|
+
sortBy: query.sort || 'name',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (query.method) {
|
|
76
|
+
searchOptions.installMethod = query.method;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const apps = await this.detector.searchApps(searchOptions);
|
|
80
|
+
const limit = query.limit || 50;
|
|
81
|
+
const offset = query.offset || 0;
|
|
82
|
+
const paginatedApps = apps.slice(offset, offset + limit);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
apps: paginatedApps,
|
|
86
|
+
count: apps.length,
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(`Failed to search apps: ${(error as Error).message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Analyze an app and get all artifacts
|
|
95
|
+
*/
|
|
96
|
+
async analyzeApp(appName: string): Promise<AppAnalysis> {
|
|
97
|
+
try {
|
|
98
|
+
// Find the app first
|
|
99
|
+
const allApps = await this.detector.searchApps({ query: appName });
|
|
100
|
+
const app = allApps.find((a) => a.name.toLowerCase() === appName.toLowerCase());
|
|
101
|
+
|
|
102
|
+
if (!app) {
|
|
103
|
+
throw new Error(`App "${appName}" not found`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Find all artifacts
|
|
107
|
+
const artifacts = await this.detector.findArtifacts(app.name, app.installMethod);
|
|
108
|
+
|
|
109
|
+
// Calculate breakdown
|
|
110
|
+
const breakdown = this.calculateBreakdown(artifacts);
|
|
111
|
+
const totalSize = artifacts.reduce((sum, a) => sum + (a.size || 0), 0);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
app,
|
|
115
|
+
artifacts,
|
|
116
|
+
totalSize,
|
|
117
|
+
breakdown,
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(`Failed to analyze app: ${(error as Error).message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get preview of what will be removed (dry-run)
|
|
126
|
+
*/
|
|
127
|
+
async previewRemoval(appName: string): Promise<{
|
|
128
|
+
appName: string;
|
|
129
|
+
totalSize: number;
|
|
130
|
+
filesCount: number;
|
|
131
|
+
artifacts: any[];
|
|
132
|
+
}> {
|
|
133
|
+
try {
|
|
134
|
+
const analysis = await this.analyzeApp(appName);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
appName: analysis.app.name,
|
|
138
|
+
totalSize: analysis.totalSize,
|
|
139
|
+
filesCount: analysis.artifacts.length,
|
|
140
|
+
artifacts: analysis.artifacts,
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new Error(`Failed to preview removal: ${(error as Error).message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Calculate artifact breakdown by category
|
|
149
|
+
*/
|
|
150
|
+
private calculateBreakdown(artifacts: any[]): AppAnalysis['breakdown'] {
|
|
151
|
+
const breakdown: AppAnalysis['breakdown'] = {
|
|
152
|
+
binaries: 0,
|
|
153
|
+
configs: 0,
|
|
154
|
+
caches: 0,
|
|
155
|
+
data: 0,
|
|
156
|
+
logs: 0,
|
|
157
|
+
other: 0,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
artifacts.forEach((artifact) => {
|
|
161
|
+
const path = artifact.path.toLowerCase();
|
|
162
|
+
const size = artifact.size || 0;
|
|
163
|
+
|
|
164
|
+
if (path.includes('bin') || path.includes('/usr/')) {
|
|
165
|
+
breakdown.binaries += size;
|
|
166
|
+
} else if (path.includes('config') || path.includes('.config')) {
|
|
167
|
+
breakdown.configs += size;
|
|
168
|
+
} else if (path.includes('cache') || path.includes('.cache')) {
|
|
169
|
+
breakdown.caches += size;
|
|
170
|
+
} else if (path.includes('log')) {
|
|
171
|
+
breakdown.logs += size;
|
|
172
|
+
} else if (path.includes('data') || path.includes('share')) {
|
|
173
|
+
breakdown.data += size;
|
|
174
|
+
} else {
|
|
175
|
+
breakdown.other += size;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return breakdown;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Export singleton
|
|
184
|
+
export const appService = new AppService();
|