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.
Files changed (212) hide show
  1. package/.github/workflows/npm-publish.yml +61 -0
  2. package/DEVELOPMENT.md +84 -0
  3. package/GUI_IMPLEMENTATION_STATUS.md +143 -0
  4. package/MD_Files/INDEX.md +51 -0
  5. package/PHASE2_COMPLETION.md +281 -0
  6. package/PHASE3_COMPLETION.md +364 -0
  7. package/README.md +411 -91
  8. package/RELEASE_GUIDE.md +236 -0
  9. package/assets/logo.svg +34 -0
  10. package/dist/core/appUpdateChecker.js +12 -16
  11. package/dist/core/appUpdateChecker.js.map +1 -1
  12. package/dist/core/detector.js +14 -18
  13. package/dist/core/detector.js.map +1 -1
  14. package/dist/core/duplicateFileFinder.js +12 -19
  15. package/dist/core/duplicateFileFinder.js.map +1 -1
  16. package/dist/core/orphanedDependencyDetector.js +19 -26
  17. package/dist/core/orphanedDependencyDetector.js.map +1 -1
  18. package/dist/core/performanceOptimizer.js +6 -10
  19. package/dist/core/performanceOptimizer.js.map +1 -1
  20. package/dist/core/permissionHandler.js +21 -25
  21. package/dist/core/permissionHandler.js.map +1 -1
  22. package/dist/core/pluginSystem.js +9 -13
  23. package/dist/core/pluginSystem.js.map +1 -1
  24. package/dist/core/removalRecorder.js +12 -19
  25. package/dist/core/removalRecorder.js.map +1 -1
  26. package/dist/core/remover.js +59 -66
  27. package/dist/core/remover.js.map +1 -1
  28. package/dist/core/reportGenerator.d.ts +1 -1
  29. package/dist/core/reportGenerator.d.ts.map +1 -1
  30. package/dist/core/reportGenerator.js +27 -34
  31. package/dist/core/reportGenerator.js.map +1 -1
  32. package/dist/core/scheduledCleanup.js +23 -30
  33. package/dist/core/scheduledCleanup.js.map +1 -1
  34. package/dist/core/serviceFileDetector.js +24 -31
  35. package/dist/core/serviceFileDetector.js.map +1 -1
  36. package/dist/core/verificationModule.js +10 -14
  37. package/dist/core/verificationModule.js.map +1 -1
  38. package/dist/index.js +118 -156
  39. package/dist/index.js.map +1 -1
  40. package/dist/managers/brewManager.js +30 -37
  41. package/dist/managers/brewManager.js.map +1 -1
  42. package/dist/managers/customManager.js +23 -30
  43. package/dist/managers/customManager.js.map +1 -1
  44. package/dist/managers/linuxManager.js +29 -36
  45. package/dist/managers/linuxManager.js.map +1 -1
  46. package/dist/managers/npmManager.js +27 -34
  47. package/dist/managers/npmManager.js.map +1 -1
  48. package/dist/types/index.js +1 -2
  49. package/dist/ui/client/api/client.d.ts +24 -0
  50. package/dist/ui/client/api/client.d.ts.map +1 -0
  51. package/dist/ui/client/api/client.js +100 -0
  52. package/dist/ui/client/api/client.js.map +1 -0
  53. package/dist/ui/client/app.d.ts +7 -0
  54. package/dist/ui/client/app.d.ts.map +1 -0
  55. package/dist/ui/client/app.js +75 -0
  56. package/dist/ui/client/app.js.map +1 -0
  57. package/dist/ui/client/index.html +107 -0
  58. package/dist/ui/client/pages/appDetails.d.ts +8 -0
  59. package/dist/ui/client/pages/appDetails.d.ts.map +1 -0
  60. package/dist/ui/client/pages/appDetails.js +287 -0
  61. package/dist/ui/client/pages/appDetails.js.map +1 -0
  62. package/dist/ui/client/pages/appSearch.d.ts +2 -0
  63. package/dist/ui/client/pages/appSearch.d.ts.map +1 -0
  64. package/dist/ui/client/pages/appSearch.js +221 -0
  65. package/dist/ui/client/pages/appSearch.js.map +1 -0
  66. package/dist/ui/client/pages/dashboard.d.ts +2 -0
  67. package/dist/ui/client/pages/dashboard.d.ts.map +1 -0
  68. package/dist/ui/client/pages/dashboard.js +175 -0
  69. package/dist/ui/client/pages/dashboard.js.map +1 -0
  70. package/dist/ui/client/pages/settings.d.ts +7 -0
  71. package/dist/ui/client/pages/settings.d.ts.map +1 -0
  72. package/dist/ui/client/pages/settings.js +279 -0
  73. package/dist/ui/client/pages/settings.js.map +1 -0
  74. package/dist/ui/client/state/appStore.d.ts +38 -0
  75. package/dist/ui/client/state/appStore.d.ts.map +1 -0
  76. package/dist/ui/client/state/appStore.js +130 -0
  77. package/dist/ui/client/state/appStore.js.map +1 -0
  78. package/dist/ui/client/state/dashboardStore.d.ts +31 -0
  79. package/dist/ui/client/state/dashboardStore.d.ts.map +1 -0
  80. package/dist/ui/client/state/dashboardStore.js +76 -0
  81. package/dist/ui/client/state/dashboardStore.js.map +1 -0
  82. package/dist/ui/client/state/uiStore.d.ts +43 -0
  83. package/dist/ui/client/state/uiStore.d.ts.map +1 -0
  84. package/dist/ui/client/state/uiStore.js +109 -0
  85. package/dist/ui/client/state/uiStore.js.map +1 -0
  86. package/dist/ui/client/styles/animations.css +349 -0
  87. package/dist/ui/client/styles/base.css +214 -0
  88. package/dist/ui/client/styles/components.css +400 -0
  89. package/dist/ui/client/styles/layout.css +224 -0
  90. package/dist/ui/client/styles/variables.css +140 -0
  91. package/dist/ui/client/utils/events.d.ts +19 -0
  92. package/dist/ui/client/utils/events.d.ts.map +1 -0
  93. package/dist/ui/client/utils/events.js +54 -0
  94. package/dist/ui/client/utils/events.js.map +1 -0
  95. package/dist/ui/client/utils/formatting.d.ts +11 -0
  96. package/dist/ui/client/utils/formatting.d.ts.map +1 -0
  97. package/dist/ui/client/utils/formatting.js +104 -0
  98. package/dist/ui/client/utils/formatting.js.map +1 -0
  99. package/dist/ui/client/utils/router.d.ts +25 -0
  100. package/dist/ui/client/utils/router.d.ts.map +1 -0
  101. package/dist/ui/client/utils/router.js +90 -0
  102. package/dist/ui/client/utils/router.js.map +1 -0
  103. package/dist/ui/guiServer.d.ts +11 -5
  104. package/dist/ui/guiServer.d.ts.map +1 -1
  105. package/dist/ui/guiServer.js +180 -501
  106. package/dist/ui/guiServer.js.map +1 -1
  107. package/dist/ui/menu.js +18 -27
  108. package/dist/ui/menu.js.map +1 -1
  109. package/dist/ui/prompts.js +34 -47
  110. package/dist/ui/prompts.js.map +1 -1
  111. package/dist/ui/server/middleware/errorHandler.d.ts +19 -0
  112. package/dist/ui/server/middleware/errorHandler.d.ts.map +1 -0
  113. package/dist/ui/server/middleware/errorHandler.js +100 -0
  114. package/dist/ui/server/middleware/errorHandler.js.map +1 -0
  115. package/dist/ui/server/routes/apps.d.ts +8 -0
  116. package/dist/ui/server/routes/apps.d.ts.map +1 -0
  117. package/dist/ui/server/routes/apps.js +74 -0
  118. package/dist/ui/server/routes/apps.js.map +1 -0
  119. package/dist/ui/server/routes/dashboard.d.ts +4 -0
  120. package/dist/ui/server/routes/dashboard.d.ts.map +1 -0
  121. package/dist/ui/server/routes/dashboard.js +57 -0
  122. package/dist/ui/server/routes/dashboard.js.map +1 -0
  123. package/dist/ui/server/routes/settings.d.ts +6 -0
  124. package/dist/ui/server/routes/settings.d.ts.map +1 -0
  125. package/dist/ui/server/routes/settings.js +31 -0
  126. package/dist/ui/server/routes/settings.js.map +1 -0
  127. package/dist/ui/server/services/appService.d.ts +45 -0
  128. package/dist/ui/server/services/appService.d.ts.map +1 -0
  129. package/dist/ui/server/services/appService.js +114 -0
  130. package/dist/ui/server/services/appService.js.map +1 -0
  131. package/dist/ui/server/services/removalService.d.ts +24 -0
  132. package/dist/ui/server/services/removalService.d.ts.map +1 -0
  133. package/dist/ui/server/services/removalService.js +83 -0
  134. package/dist/ui/server/services/removalService.js.map +1 -0
  135. package/dist/utils/filesystem.js +32 -49
  136. package/dist/utils/filesystem.js.map +1 -1
  137. package/dist/utils/logger.js +9 -18
  138. package/dist/utils/logger.js.map +1 -1
  139. package/dist/utils/platform.js +10 -22
  140. package/dist/utils/platform.js.map +1 -1
  141. package/dist/utils/upgrade.d.ts +2 -1
  142. package/dist/utils/upgrade.d.ts.map +1 -1
  143. package/dist/utils/upgrade.js +24 -15
  144. package/dist/utils/upgrade.js.map +1 -1
  145. package/package.json +4 -2
  146. package/scripts/publish-npm.sh +64 -0
  147. package/src/core/appUpdateChecker.ts +1 -1
  148. package/src/core/detector.ts +6 -6
  149. package/src/core/duplicateFileFinder.ts +1 -1
  150. package/src/core/orphanedDependencyDetector.ts +2 -2
  151. package/src/core/performanceOptimizer.ts +1 -1
  152. package/src/core/permissionHandler.ts +2 -2
  153. package/src/core/pluginSystem.ts +1 -1
  154. package/src/core/removalRecorder.ts +2 -2
  155. package/src/core/remover.ts +11 -11
  156. package/src/core/reportGenerator.ts +2 -2
  157. package/src/core/scheduledCleanup.ts +2 -2
  158. package/src/core/serviceFileDetector.ts +2 -2
  159. package/src/core/verificationModule.ts +2 -2
  160. package/src/index.ts +8 -8
  161. package/src/managers/brewManager.ts +3 -3
  162. package/src/managers/customManager.ts +2 -2
  163. package/src/managers/linuxManager.ts +3 -3
  164. package/src/managers/npmManager.ts +3 -3
  165. package/src/ui/client/api/client.ts +168 -0
  166. package/src/ui/client/app.ts +125 -0
  167. package/src/ui/client/index.html +107 -0
  168. package/src/ui/client/pages/appDetails.ts +356 -0
  169. package/src/ui/client/pages/appSearch.ts +283 -0
  170. package/src/ui/client/pages/dashboard.ts +211 -0
  171. package/src/ui/client/pages/settings.ts +342 -0
  172. package/src/ui/client/state/appStore.ts +181 -0
  173. package/src/ui/client/state/dashboardStore.ts +123 -0
  174. package/src/ui/client/state/uiStore.ts +166 -0
  175. package/src/ui/client/styles/animations.css +349 -0
  176. package/src/ui/client/styles/base.css +214 -0
  177. package/src/ui/client/styles/components.css +400 -0
  178. package/src/ui/client/styles/layout.css +224 -0
  179. package/src/ui/client/styles/variables.css +140 -0
  180. package/src/ui/client/utils/events.ts +74 -0
  181. package/src/ui/client/utils/formatting.ts +157 -0
  182. package/src/ui/client/utils/router.ts +161 -0
  183. package/src/ui/guiServer.ts +245 -498
  184. package/src/ui/prompts.ts +1 -1
  185. package/src/ui/server/middleware/errorHandler.ts +174 -0
  186. package/src/ui/server/routes/apps.ts +132 -0
  187. package/src/ui/server/routes/dashboard.ts +93 -0
  188. package/src/ui/server/routes/settings.ts +63 -0
  189. package/src/ui/server/services/appService.ts +184 -0
  190. package/src/ui/server/services/removalService.ts +138 -0
  191. package/src/utils/upgrade.ts +19 -2
  192. package/tsconfig.json +3 -2
  193. package/INDEX.md +0 -165
  194. /package/{ACTION_CHECKLIST.md → MD_Files/ACTION_CHECKLIST.md} +0 -0
  195. /package/{APPCLEAN_SUMMARY.md → MD_Files/APPCLEAN_SUMMARY.md} +0 -0
  196. /package/{CHANGELOG.md → MD_Files/CHANGELOG.md} +0 -0
  197. /package/{CODE_OF_CONDUCT.md → MD_Files/CODE_OF_CONDUCT.md} +0 -0
  198. /package/{CODE_REVIEW_REPORT.md → MD_Files/CODE_REVIEW_REPORT.md} +0 -0
  199. /package/{COMMUNITY_POSTS.md → MD_Files/COMMUNITY_POSTS.md} +0 -0
  200. /package/{DEPLOYMENT_GUIDE.md → MD_Files/DEPLOYMENT_GUIDE.md} +0 -0
  201. /package/{DEPLOYMENT_STATUS.md → MD_Files/DEPLOYMENT_STATUS.md} +0 -0
  202. /package/{EXECUTIVE_REPORT.md → MD_Files/EXECUTIVE_REPORT.md} +0 -0
  203. /package/{GITHUB_OPTIMIZATION.md → MD_Files/GITHUB_OPTIMIZATION.md} +0 -0
  204. /package/{MARKETING_SUMMARY.md → MD_Files/MARKETING_SUMMARY.md} +0 -0
  205. /package/{NPM_PACKAGE_OPTIMIZATION.md → MD_Files/NPM_PACKAGE_OPTIMIZATION.md} +0 -0
  206. /package/{NPM_PUBLISH.md → MD_Files/NPM_PUBLISH.md} +0 -0
  207. /package/{PROJECT_SUMMARY.txt → MD_Files/PROJECT_SUMMARY.txt} +0 -0
  208. /package/{PUBLICATION_SUCCESS_REPORT.md → MD_Files/PUBLICATION_SUCCESS_REPORT.md} +0 -0
  209. /package/{QUICKSTART.md → MD_Files/QUICKSTART.md} +0 -0
  210. /package/{SETUP_GITHUB.md → MD_Files/SETUP_GITHUB.md} +0 -0
  211. /package/{TESTING_SUMMARY.md → MD_Files/TESTING_SUMMARY.md} +0 -0
  212. /package/{setup-github.sh → MD_Files/setup-github.sh} +0 -0
@@ -1,21 +1,32 @@
1
1
  /**
2
- * GUI Server for AppClean
3
- * Provides web-based GUI interface for macOS, Linux, and Windows
4
- * v1.2.0 Feature
2
+ * GUI Server for AppClean v2.0.0
3
+ * Modern SPA with API endpoints
5
4
  */
6
5
 
7
6
  import { createServer, IncomingMessage, ServerResponse } from 'http';
8
- import { Logger } from '../utils/logger';
9
- import { UpgradeManager } from '../utils/upgrade';
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname } from 'path';
11
+ import { execFile, spawn, type ChildProcess } from 'child_process';
12
+ import { Logger } from '../utils/logger.js';
13
+ import { sendJson, sendError, parseQueryParams } from './server/middleware/errorHandler.js';
14
+ import { handleAppRoutes } from './server/routes/apps.js';
15
+ import { handleDashboardRoutes } from './server/routes/dashboard.js';
16
+ import { handleSettingsRoutes } from './server/routes/settings.js';
17
+
18
+ // ES module compatibility: Define __dirname and __filename
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
10
21
 
11
22
  export class GUIServer {
12
23
  private port: number = 3000;
13
24
  private server: any;
14
- private upgradeManager: UpgradeManager;
25
+ private spaHtml: string | null = null;
26
+ private browserProcess: ChildProcess | null = null;
15
27
 
16
28
  constructor(port: number = 3000) {
17
29
  this.port = port;
18
- this.upgradeManager = new UpgradeManager();
19
30
  }
20
31
 
21
32
  /**
@@ -24,20 +35,23 @@ export class GUIServer {
24
35
  async start(): Promise<void> {
25
36
  Logger.info(`Starting AppClean GUI server on port ${this.port}...`);
26
37
 
38
+ // Try to load SPA HTML (from compiled dist)
39
+ this.loadSPAHtml();
40
+
27
41
  this.server = createServer((req, res) => {
28
- if (req.url === '/') {
29
- res.writeHead(200, { 'Content-Type': 'text/html' });
30
- res.end(this.getIndexHTML());
31
- } else if (req.url?.startsWith('/api/')) {
32
- this.handleAPIRequest(req, res);
33
- } else {
34
- res.writeHead(404, { 'Content-Type': 'text/plain' });
35
- res.end('Not Found');
36
- }
42
+ this.handleRequest(req, res);
37
43
  });
38
44
 
39
- this.server.listen(this.port, () => {
40
- Logger.success(`GUI server running at http://localhost:${this.port}`);
45
+ return new Promise((resolve) => {
46
+ this.server.listen(this.port, async () => {
47
+ Logger.success(`✨ AppClean GUI running at http://localhost:${this.port}`);
48
+ Logger.info('Press Ctrl+C to stop the server');
49
+
50
+ // Open the default browser
51
+ await this.openBrowser();
52
+
53
+ resolve();
54
+ });
41
55
  });
42
56
  }
43
57
 
@@ -45,6 +59,9 @@ export class GUIServer {
45
59
  * Stop GUI server
46
60
  */
47
61
  async stop(): Promise<void> {
62
+ // Close the browser
63
+ await this.closeBrowser();
64
+
48
65
  if (this.server) {
49
66
  this.server.close();
50
67
  Logger.info('GUI server stopped');
@@ -52,524 +69,254 @@ export class GUIServer {
52
69
  }
53
70
 
54
71
  /**
55
- * Handle API requests
72
+ * Open the default browser
56
73
  */
57
- private async handleAPIRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
58
- const url = req.url || '';
74
+ private async openBrowser(): Promise<void> {
75
+ const url = `http://localhost:${this.port}`;
59
76
 
60
77
  try {
61
- if (url === '/api/version') {
62
- await this.handleVersionCheck(res);
63
- } else if (url === '/api/upgrade') {
64
- await this.handleUpgrade(res);
65
- } else if (url === '/api/uninstall') {
66
- await this.handleUninstall(res);
78
+ const platform = process.platform;
79
+
80
+ if (platform === 'darwin') {
81
+ // macOS
82
+ this.browserProcess = execFile('open', [url]);
83
+ } else if (platform === 'win32') {
84
+ // Windows
85
+ this.browserProcess = execFile('cmd', ['/c', 'start', url]);
67
86
  } else {
68
- res.writeHead(404, { 'Content-Type': 'application/json' });
69
- res.end(JSON.stringify({ error: 'API endpoint not found' }));
87
+ // Linux and other Unix-like systems
88
+ this.browserProcess = execFile('xdg-open', [url]);
70
89
  }
90
+
91
+ this.browserProcess.on('error', (error) => {
92
+ Logger.warn(`Could not open browser: ${error.message}`);
93
+ });
71
94
  } catch (error) {
72
- res.writeHead(500, { 'Content-Type': 'application/json' });
73
- res.end(
74
- JSON.stringify({ error: (error as Error).message })
75
- );
95
+ Logger.warn(`Failed to open browser: ${(error as Error).message}`);
76
96
  }
77
97
  }
78
98
 
79
99
  /**
80
- * Handle version check request
100
+ * Close the browser
81
101
  */
82
- private async handleVersionCheck(res: ServerResponse): Promise<void> {
83
- const versionInfo = await this.upgradeManager.checkForUpdates();
102
+ private async closeBrowser(): Promise<void> {
103
+ if (!this.browserProcess) {
104
+ return;
105
+ }
106
+
107
+ try {
108
+ const platform = process.platform;
109
+
110
+ if (platform === 'win32') {
111
+ // Windows: kill the process
112
+ if (this.browserProcess.pid) {
113
+ process.kill(this.browserProcess.pid, 'SIGTERM');
114
+ }
115
+ } else {
116
+ // macOS and Linux: kill the process
117
+ if (this.browserProcess.pid) {
118
+ process.kill(this.browserProcess.pid);
119
+ }
120
+ }
84
121
 
85
- res.writeHead(200, { 'Content-Type': 'application/json' });
86
- res.end(JSON.stringify(versionInfo));
122
+ this.browserProcess = null;
123
+ } catch (error) {
124
+ Logger.debug(`Note: Could not close browser process: ${(error as Error).message}`);
125
+ }
87
126
  }
88
127
 
89
128
  /**
90
- * Handle upgrade request
129
+ * Main request handler
91
130
  */
92
- private async handleUpgrade(res: ServerResponse): Promise<void> {
93
- const result = await this.upgradeManager.upgrade();
131
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
132
+ const url = req.url || '/';
133
+ const method = req.method || 'GET';
134
+ const pathname = url.split('?')[0];
94
135
 
95
- res.writeHead(result.success ? 200 : 500, {
96
- 'Content-Type': 'application/json',
97
- });
98
- res.end(JSON.stringify(result));
136
+ // Set CORS headers
137
+ this.setCORSHeaders(res);
138
+
139
+ // Handle preflight requests
140
+ if (method === 'OPTIONS') {
141
+ res.writeHead(200);
142
+ res.end();
143
+ return;
144
+ }
145
+
146
+ try {
147
+ // Route API requests
148
+ if (pathname.startsWith('/api/')) {
149
+ return this.handleAPIRequest(method, pathname, req, res);
150
+ }
151
+
152
+ // Serve static assets
153
+ if (pathname.startsWith('/static/')) {
154
+ return this.serveStaticAsset(pathname, res);
155
+ }
156
+
157
+ // Serve SPA for all other routes
158
+ this.serveSPA(res);
159
+ } catch (error) {
160
+ Logger.error(`Request error: ${(error as Error).message}`);
161
+ sendError(res, 'Internal server error', 500);
162
+ }
99
163
  }
100
164
 
101
165
  /**
102
- * Handle uninstall request
166
+ * Handle API requests
103
167
  */
104
- private async handleUninstall(res: ServerResponse): Promise<void> {
105
- const result = await this.upgradeManager.uninstall();
168
+ private handleAPIRequest(
169
+ method: string,
170
+ pathname: string,
171
+ req: IncomingMessage,
172
+ res: ServerResponse
173
+ ): void {
174
+ // Try app routes
175
+ if (handleAppRoutes(method, pathname, req, res)) {
176
+ return;
177
+ }
106
178
 
107
- res.writeHead(result.success ? 200 : 500, {
108
- 'Content-Type': 'application/json',
109
- });
110
- res.end(JSON.stringify(result));
179
+ // Try dashboard routes
180
+ if (handleDashboardRoutes(method, pathname, req, res)) {
181
+ return;
182
+ }
183
+
184
+ // Try settings routes
185
+ if (handleSettingsRoutes(method, pathname, req, res)) {
186
+ return;
187
+ }
188
+
189
+ // Unknown endpoint
190
+ sendError(res, 'API endpoint not found', 404);
191
+ }
192
+
193
+ /**
194
+ * Serve static assets (CSS, JS)
195
+ */
196
+ private serveStaticAsset(pathname: string, res: ServerResponse): void {
197
+ // Remove /static/ prefix
198
+ const relativePath = pathname.slice(8);
199
+
200
+ // Security: prevent directory traversal
201
+ if (relativePath.includes('..')) {
202
+ sendError(res, 'Access denied', 403);
203
+ return;
204
+ }
205
+
206
+ // Construct file path
207
+ const filePath = join(__dirname, 'client', relativePath);
208
+
209
+ // Check if file exists
210
+ if (!existsSync(filePath)) {
211
+ sendError(res, 'Asset not found', 404);
212
+ return;
213
+ }
214
+
215
+ try {
216
+ const content = readFileSync(filePath);
217
+ const contentType = this.getContentType(filePath);
218
+
219
+ res.writeHead(200, {
220
+ 'Content-Type': contentType,
221
+ 'Cache-Control': 'public, max-age=3600', // 1 hour
222
+ });
223
+ res.end(content);
224
+ } catch (error) {
225
+ Logger.warn(`Failed to serve asset ${pathname}: ${(error as Error).message}`);
226
+ sendError(res, 'Failed to load asset', 500);
227
+ }
111
228
  }
112
229
 
113
230
  /**
114
- * Get index HTML for GUI
231
+ * Serve SPA HTML
115
232
  */
116
- private getIndexHTML(): string {
117
- return `
233
+ private serveSPA(res: ServerResponse): void {
234
+ if (!this.spaHtml) {
235
+ // Fallback: serve minimal HTML with error message
236
+ const fallbackHtml = `
118
237
  <!DOCTYPE html>
119
- <html lang="en">
238
+ <html>
120
239
  <head>
121
240
  <meta charset="UTF-8">
122
241
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
123
- <title>AppClean GUI - v1.2.0</title>
124
- <style>
125
- * {
126
- margin: 0;
127
- padding: 0;
128
- box-sizing: border-box;
129
- }
130
- body {
131
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
132
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
133
- min-height: 100vh;
134
- display: flex;
135
- align-items: center;
136
- justify-content: center;
137
- padding: 20px;
138
- }
139
- .container {
140
- background: white;
141
- border-radius: 12px;
142
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
143
- max-width: 800px;
144
- width: 100%;
145
- padding: 40px;
146
- }
147
- h1 {
148
- color: #333;
149
- margin-bottom: 10px;
150
- }
151
- h3 {
152
- color: #333;
153
- margin-top: 20px;
154
- margin-bottom: 15px;
155
- }
156
- .version {
157
- color: #666;
158
- font-size: 14px;
159
- margin-bottom: 30px;
160
- }
161
- .feature-badge {
162
- display: inline-block;
163
- background: #667eea;
164
- color: white;
165
- padding: 8px 16px;
166
- border-radius: 20px;
167
- font-size: 12px;
168
- font-weight: 600;
169
- margin-bottom: 20px;
170
- }
171
- p {
172
- color: #666;
173
- line-height: 1.6;
174
- margin-bottom: 20px;
175
- }
176
- .section {
177
- background: #f0f4ff;
178
- border-left: 4px solid #667eea;
179
- padding: 20px;
180
- border-radius: 4px;
181
- margin-top: 30px;
182
- }
183
- .version-info {
184
- background: #f9f9f9;
185
- padding: 15px;
186
- border-radius: 6px;
187
- margin: 15px 0;
188
- font-family: monospace;
189
- font-size: 14px;
190
- }
191
- .version-row {
192
- display: flex;
193
- justify-content: space-between;
194
- margin: 8px 0;
195
- }
196
- .label {
197
- color: #666;
198
- font-weight: 500;
199
- }
200
- .value {
201
- color: #333;
202
- font-weight: 600;
203
- }
204
- .update-available {
205
- color: #f59e0b;
206
- font-weight: 600;
207
- }
208
- .up-to-date {
209
- color: #10b981;
210
- font-weight: 600;
211
- }
212
- .button-group {
213
- display: flex;
214
- gap: 10px;
215
- margin-top: 20px;
216
- }
217
- button {
218
- flex: 1;
219
- padding: 12px 20px;
220
- border: none;
221
- border-radius: 6px;
222
- font-size: 14px;
223
- font-weight: 600;
224
- cursor: pointer;
225
- transition: all 0.3s ease;
226
- }
227
- .btn-primary {
228
- background: #667eea;
229
- color: white;
230
- }
231
- .btn-primary:hover {
232
- background: #5568d3;
233
- transform: translateY(-2px);
234
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
235
- }
236
- .btn-primary:disabled {
237
- background: #ccc;
238
- cursor: not-allowed;
239
- transform: none;
240
- }
241
- .btn-secondary {
242
- background: #e5e7eb;
243
- color: #333;
244
- }
245
- .btn-secondary:hover {
246
- background: #d1d5db;
247
- }
248
- .status-message {
249
- margin-top: 15px;
250
- padding: 12px;
251
- border-radius: 6px;
252
- display: none;
253
- font-weight: 500;
254
- }
255
- .status-success {
256
- background: #d1fae5;
257
- color: #065f46;
258
- display: block;
259
- }
260
- .status-error {
261
- background: #fee2e2;
262
- color: #991b1b;
263
- display: block;
264
- }
265
- .status-loading {
266
- background: #dbeafe;
267
- color: #1e40af;
268
- display: block;
269
- }
270
- .spinner {
271
- display: inline-block;
272
- width: 16px;
273
- height: 16px;
274
- border: 2px solid rgba(30, 64, 175, 0.3);
275
- border-top-color: #1e40af;
276
- border-radius: 50%;
277
- animation: spin 0.8s linear infinite;
278
- margin-right: 8px;
279
- vertical-align: middle;
280
- }
281
- @keyframes spin {
282
- to { transform: rotate(360deg); }
283
- }
284
- ul {
285
- margin-left: 20px;
286
- margin-top: 10px;
287
- }
288
- .danger-zone {
289
- background: #fee2e2;
290
- border-left: 4px solid #dc2626;
291
- padding: 20px;
292
- border-radius: 4px;
293
- margin-top: 30px;
294
- }
295
- .danger-zone h3 {
296
- color: #991b1b;
297
- margin-top: 0;
298
- }
299
- .danger-zone p {
300
- color: #7c2d12;
301
- margin-bottom: 15px;
302
- }
303
- .btn-danger {
304
- background: #dc2626;
305
- color: white;
306
- }
307
- .btn-danger:hover {
308
- background: #b91c1c;
309
- transform: translateY(-2px);
310
- box-shadow: 0 5px 15px rgba(220, 38, 38, 0.4);
311
- }
312
- .btn-danger:disabled {
313
- background: #ccc;
314
- cursor: not-allowed;
315
- transform: none;
316
- }
317
- /* Modal Styles */
318
- .modal {
319
- display: none;
320
- position: fixed;
321
- z-index: 1000;
322
- left: 0;
323
- top: 0;
324
- width: 100%;
325
- height: 100%;
326
- background-color: rgba(0,0,0,0.5);
327
- }
328
- .modal-content {
329
- background-color: white;
330
- margin: auto;
331
- padding: 30px;
332
- border-radius: 12px;
333
- width: 90%;
334
- max-width: 500px;
335
- position: absolute;
336
- top: 50%;
337
- left: 50%;
338
- transform: translate(-50%, -50%);
339
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
340
- }
341
- .modal-content h2 {
342
- color: #dc2626;
343
- margin-bottom: 15px;
344
- }
345
- .modal-content p {
346
- color: #666;
347
- margin-bottom: 20px;
348
- }
349
- .modal-buttons {
350
- display: flex;
351
- gap: 10px;
352
- justify-content: flex-end;
353
- }
354
- .modal-buttons button {
355
- padding: 10px 20px;
356
- border: none;
357
- border-radius: 6px;
358
- font-weight: 600;
359
- cursor: pointer;
360
- transition: all 0.3s ease;
361
- }
362
- .modal-buttons .btn-cancel {
363
- background: #e5e7eb;
364
- color: #333;
365
- }
366
- .modal-buttons .btn-cancel:hover {
367
- background: #d1d5db;
368
- }
369
- .modal-buttons .btn-confirm {
370
- background: #dc2626;
371
- color: white;
372
- }
373
- .modal-buttons .btn-confirm:hover {
374
- background: #b91c1c;
375
- }
376
- </style>
242
+ <title>AppClean</title>
377
243
  </head>
378
- <body>
379
- <div class="container">
380
- <h1>🎨 AppClean GUI</h1>
381
- <p class="version">v1.2.0 - GUI Application Feature</p>
382
- <span class="feature-badge">Coming Soon</span>
383
-
384
- <p>
385
- A beautiful, intuitive graphical user interface for AppClean, bringing the power of
386
- intelligent app removal to users who prefer a visual interface.
387
- </p>
388
-
389
- <div class="section">
390
- <h3>📦 Version & Updates</h3>
391
- <div class="version-info">
392
- <div class="version-row">
393
- <span class="label">Current Version:</span>
394
- <span class="value" id="currentVersion">Loading...</span>
395
- </div>
396
- <div class="version-row">
397
- <span class="label">Latest Version:</span>
398
- <span class="value" id="latestVersion">Loading...</span>
399
- </div>
400
- <div class="version-row">
401
- <span class="label">Status:</span>
402
- <span id="updateStatus">Checking...</span>
403
- </div>
404
- </div>
405
- <div class="button-group">
406
- <button class="btn-secondary" onclick="checkVersion()">🔄 Check for Updates</button>
407
- <button class="btn-primary" id="upgradeBtn" onclick="upgradeAppClean()" disabled>⬆️ Upgrade</button>
408
- </div>
409
- <div id="statusMessage" class="status-message"></div>
410
- </div>
411
-
412
- <div class="section">
413
- <h3>🚀 Features Coming in v1.2.0</h3>
414
- <ul>
415
- <li>✨ Modern, responsive GUI design</li>
416
- <li>🖥️ Cross-platform support (macOS, Linux, Windows)</li>
417
- <li>🔍 Visual app search and discovery</li>
418
- <li>📊 Beautiful artifact visualization</li>
419
- <li>🗑️ Drag-and-drop app removal</li>
420
- <li>📈 Real-time removal progress</li>
421
- <li>📋 Interactive report viewer</li>
422
- </ul>
423
- </div>
424
-
425
- <div class="danger-zone">
426
- <h3>⚠️ Danger Zone</h3>
427
- <p>
428
- Uninstall AppClean from your system. This action will remove the application and
429
- all its global files. This cannot be undone easily.
430
- </p>
431
- <div class="button-group">
432
- <button class="btn-danger" onclick="showUninstallConfirm()">🗑️ Uninstall AppClean</button>
433
- </div>
434
- </div>
435
- </div>
436
-
437
- <!-- Uninstall Confirmation Modal -->
438
- <div id="uninstallModal" class="modal">
439
- <div class="modal-content">
440
- <h2>⚠️ Confirm Uninstall</h2>
441
- <p>
442
- Are you sure you want to uninstall AppClean? This action will remove the application
443
- from your system and cannot be easily undone.
444
- </p>
445
- <p style="color: #dc2626; font-weight: 600;">
446
- This action cannot be undone!
447
- </p>
448
- <div class="modal-buttons">
449
- <button class="btn-cancel" onclick="closeUninstallConfirm()">Cancel</button>
450
- <button class="btn-confirm" onclick="confirmUninstall()">Uninstall</button>
451
- </div>
452
- </div>
453
- </div>
454
-
455
- <script>
456
- // Check version on page load
457
- window.addEventListener('load', checkVersion);
458
-
459
- async function checkVersion() {
460
- const statusEl = document.getElementById('statusMessage');
461
- statusEl.textContent = '🔄 Checking for updates...';
462
- statusEl.className = 'status-message status-loading';
463
- statusEl.innerHTML = '<span class="spinner"></span>Checking for updates...';
464
-
465
- try {
466
- const response = await fetch('/api/version');
467
- const data = await response.json();
468
-
469
- document.getElementById('currentVersion').textContent = 'v' + data.current;
470
- document.getElementById('latestVersion').textContent = 'v' + data.latest;
471
-
472
- const upgradeBtn = document.getElementById('upgradeBtn');
473
- const updateStatus = document.getElementById('updateStatus');
474
-
475
- if (data.isUpdateAvailable) {
476
- updateStatus.innerHTML = '<span class="update-available">⚠️ Update available!</span>';
477
- upgradeBtn.disabled = false;
478
- statusEl.textContent = '✓ Update available! Click the Upgrade button to install.';
479
- statusEl.className = 'status-message status-success';
480
- } else {
481
- updateStatus.innerHTML = '<span class="up-to-date">✓ Up to date</span>';
482
- upgradeBtn.disabled = true;
483
- statusEl.textContent = '✓ AppClean is already up to date!';
484
- statusEl.className = 'status-message status-success';
485
- }
486
- } catch (error) {
487
- statusEl.textContent = '✗ Failed to check for updates: ' + error.message;
488
- statusEl.className = 'status-message status-error';
489
- }
490
- }
244
+ <body style="font-family: system-ui; padding: 20px; text-align: center;">
245
+ <h1>⚠️ GUI Not Ready</h1>
246
+ <p>The SPA assets haven't been compiled yet.</p>
247
+ <p>Run <code>npm run build</code> to compile the TypeScript/CSS files.</p>
248
+ <p>For now, use the CLI: <code>appclean --help</code></p>
249
+ </body>
250
+ </html>`;
491
251
 
492
- async function upgradeAppClean() {
493
- const statusEl = document.getElementById('statusMessage');
494
- const upgradeBtn = document.getElementById('upgradeBtn');
495
-
496
- upgradeBtn.disabled = true;
497
- statusEl.innerHTML = '<span class="spinner"></span>Upgrading AppClean...';
498
- statusEl.className = 'status-message status-loading';
499
-
500
- try {
501
- const response = await fetch('/api/upgrade');
502
- const data = await response.json();
503
-
504
- if (data.success) {
505
- statusEl.textContent = '✓ ' + data.message;
506
- statusEl.className = 'status-message status-success';
507
- setTimeout(() => {
508
- statusEl.textContent = 'Please refresh the page or restart the GUI server.';
509
- checkVersion();
510
- }, 2000);
511
- } else {
512
- statusEl.textContent = '✗ ' + data.message;
513
- statusEl.className = 'status-message status-error';
514
- upgradeBtn.disabled = false;
515
- }
516
- } catch (error) {
517
- statusEl.textContent = '✗ Upgrade failed: ' + error.message;
518
- statusEl.className = 'status-message status-error';
519
- upgradeBtn.disabled = false;
520
- }
252
+ res.writeHead(200, { 'Content-Type': 'text/html' });
253
+ res.end(fallbackHtml);
254
+ return;
521
255
  }
522
256
 
523
- // Uninstall Functions
524
- function showUninstallConfirm() {
525
- document.getElementById('uninstallModal').style.display = 'block';
526
- }
257
+ res.writeHead(200, {
258
+ 'Content-Type': 'text/html',
259
+ 'Cache-Control': 'no-cache',
260
+ });
261
+ res.end(this.spaHtml);
262
+ }
527
263
 
528
- function closeUninstallConfirm() {
529
- document.getElementById('uninstallModal').style.display = 'none';
530
- }
264
+ /**
265
+ * Load SPA HTML from compiled dist
266
+ */
267
+ private loadSPAHtml(): void {
268
+ const htmlPath = join(__dirname, 'client', 'index.html');
531
269
 
532
- async function confirmUninstall() {
533
- const modal = document.getElementById('uninstallModal');
534
- const btn = modal.querySelector('.btn-confirm');
535
-
536
- btn.disabled = true;
537
- btn.textContent = '🗑️ Uninstalling...';
538
-
539
- try {
540
- const response = await fetch('/api/uninstall');
541
- const data = await response.json();
542
-
543
- if (data.success) {
544
- modal.querySelector('p').textContent = '✓ ' + data.message;
545
- modal.querySelector('h2').textContent = '✓ Uninstall Complete';
546
- modal.querySelector('.modal-buttons').innerHTML =
547
- '<button class="btn-cancel" onclick="window.close()">Close</button>';
548
- setTimeout(() => {
549
- alert('AppClean has been uninstalled. You can close this window.');
550
- }, 500);
551
- } else {
552
- alert('❌ Uninstall failed: ' + data.message);
553
- btn.disabled = false;
554
- btn.textContent = 'Uninstall';
555
- }
556
- } catch (error) {
557
- alert('❌ Error: ' + error.message);
558
- btn.disabled = false;
559
- btn.textContent = 'Uninstall';
270
+ try {
271
+ if (existsSync(htmlPath)) {
272
+ this.spaHtml = readFileSync(htmlPath, 'utf-8');
273
+ Logger.debug('✓ Loaded SPA HTML');
274
+ } else {
275
+ Logger.warn(`⚠️ SPA HTML not found at ${htmlPath}`);
276
+ Logger.info('Make sure to run: npm run build');
560
277
  }
278
+ } catch (error) {
279
+ Logger.warn(`Failed to load SPA HTML: ${(error as Error).message}`);
561
280
  }
281
+ }
562
282
 
563
- // Close modal when clicking outside
564
- window.onclick = function(event) {
565
- const modal = document.getElementById('uninstallModal');
566
- if (event.target === modal) {
567
- modal.style.display = 'none';
568
- }
569
- }
570
- </script>
571
- </body>
572
- </html>
573
- `;
283
+ /**
284
+ * Determine content type from file extension
285
+ */
286
+ private getContentType(filePath: string): string {
287
+ const ext = filePath.toLowerCase().split('.').pop();
288
+
289
+ const typeMap: Record<string, string> = {
290
+ 'js': 'application/javascript; charset=utf-8',
291
+ 'css': 'text/css; charset=utf-8',
292
+ 'html': 'text/html; charset=utf-8',
293
+ 'json': 'application/json; charset=utf-8',
294
+ 'svg': 'image/svg+xml',
295
+ 'png': 'image/png',
296
+ 'jpg': 'image/jpeg',
297
+ 'jpeg': 'image/jpeg',
298
+ 'gif': 'image/gif',
299
+ 'ico': 'image/x-icon',
300
+ 'webp': 'image/webp',
301
+ 'woff': 'font/woff',
302
+ 'woff2': 'font/woff2',
303
+ 'ttf': 'font/ttf',
304
+ 'eot': 'application/vnd.ms-fontobject',
305
+ };
306
+
307
+ return typeMap[ext || ''] || 'application/octet-stream';
308
+ }
309
+
310
+ /**
311
+ * Set CORS headers
312
+ */
313
+ private setCORSHeaders(res: ServerResponse): void {
314
+ res.setHeader('Access-Control-Allow-Origin', '*');
315
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
316
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
317
+ res.setHeader('Access-Control-Max-Age', '3600');
574
318
  }
575
319
  }
320
+
321
+ // Export for use in CLI
322
+ export default GUIServer;