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
@@ -0,0 +1,140 @@
1
+ /* Design System - CSS Variables */
2
+
3
+ :root {
4
+ /* Colors - Primary */
5
+ --color-primary: #3b82f6;
6
+ --color-primary-hover: #2563eb;
7
+ --color-primary-light: #dbeafe;
8
+ --color-primary-dark: #1e40af;
9
+
10
+ /* Colors - Semantic */
11
+ --color-success: #10b981;
12
+ --color-success-light: #d1fae5;
13
+ --color-warning: #f59e0b;
14
+ --color-warning-light: #fef3c7;
15
+ --color-danger: #ef4444;
16
+ --color-danger-light: #fee2e2;
17
+ --color-info: #0ea5e9;
18
+ --color-info-light: #cffafe;
19
+
20
+ /* Colors - Neutral */
21
+ --color-neutral-0: #ffffff;
22
+ --color-neutral-50: #f9fafb;
23
+ --color-neutral-100: #f3f4f6;
24
+ --color-neutral-200: #e5e7eb;
25
+ --color-neutral-300: #d1d5db;
26
+ --color-neutral-400: #9ca3af;
27
+ --color-neutral-500: #6b7280;
28
+ --color-neutral-600: #4b5563;
29
+ --color-neutral-700: #374151;
30
+ --color-neutral-800: #1f2937;
31
+ --color-neutral-900: #111827;
32
+
33
+ /* Background & Surface */
34
+ --bg-primary: var(--color-neutral-0);
35
+ --bg-secondary: var(--color-neutral-50);
36
+ --bg-hover: var(--color-neutral-100);
37
+ --text-primary: var(--color-neutral-900);
38
+ --text-secondary: var(--color-neutral-600);
39
+ --text-muted: var(--color-neutral-500);
40
+ --border-color: var(--color-neutral-200);
41
+ --border-color-hover: var(--color-neutral-300);
42
+
43
+ /* Dark Mode */
44
+ --dark-bg-primary: var(--color-neutral-900);
45
+ --dark-bg-secondary: var(--color-neutral-800);
46
+ --dark-bg-hover: var(--color-neutral-700);
47
+ --dark-text-primary: var(--color-neutral-0);
48
+ --dark-text-secondary: var(--color-neutral-300);
49
+ --dark-text-muted: var(--color-neutral-400);
50
+ --dark-border-color: var(--color-neutral-700);
51
+ --dark-border-color-hover: var(--color-neutral-600);
52
+
53
+ /* Typography */
54
+ --font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
55
+ --font-family-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
56
+
57
+ /* Font Sizes */
58
+ --font-xs: 0.75rem; /* 12px */
59
+ --font-sm: 0.875rem; /* 14px */
60
+ --font-base: 1rem; /* 16px */
61
+ --font-lg: 1.125rem; /* 18px */
62
+ --font-xl: 1.25rem; /* 20px */
63
+ --font-2xl: 1.5rem; /* 24px */
64
+ --font-3xl: 1.875rem; /* 30px */
65
+
66
+ /* Font Weights */
67
+ --fw-regular: 400;
68
+ --fw-medium: 500;
69
+ --fw-semibold: 600;
70
+ --fw-bold: 700;
71
+
72
+ /* Line Heights */
73
+ --lh-tight: 1.25;
74
+ --lh-normal: 1.5;
75
+ --lh-relaxed: 1.625;
76
+ --lh-loose: 2;
77
+
78
+ /* Spacing Scale (4px base unit) */
79
+ --space-0: 0;
80
+ --space-1: 0.25rem; /* 4px */
81
+ --space-2: 0.5rem; /* 8px */
82
+ --space-3: 0.75rem; /* 12px */
83
+ --space-4: 1rem; /* 16px */
84
+ --space-6: 1.5rem; /* 24px */
85
+ --space-8: 2rem; /* 32px */
86
+ --space-12: 3rem; /* 48px */
87
+ --space-16: 4rem; /* 64px */
88
+
89
+ /* Border Radius */
90
+ --radius-none: 0;
91
+ --radius-sm: 0.375rem; /* 6px */
92
+ --radius-md: 0.5rem; /* 8px */
93
+ --radius-lg: 0.75rem; /* 12px */
94
+ --radius-xl: 1rem; /* 16px */
95
+ --radius-2xl: 1.5rem; /* 24px */
96
+ --radius-full: 9999px; /* Pill-shaped */
97
+
98
+ /* Shadows */
99
+ --shadow-none: 0 0 0 0 rgba(0, 0, 0, 0);
100
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
101
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
102
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
103
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
104
+
105
+ /* Transitions */
106
+ --transition-fast: 150ms ease-out;
107
+ --transition-normal: 250ms ease-out;
108
+ --transition-slow: 350ms ease-out;
109
+
110
+ /* Layout */
111
+ --max-width-sm: 640px;
112
+ --max-width-md: 768px;
113
+ --max-width-lg: 1024px;
114
+ --max-width-xl: 1280px;
115
+ --max-width-2xl: 1536px;
116
+
117
+ /* Z-index */
118
+ --z-hide: -1;
119
+ --z-auto: auto;
120
+ --z-base: 0;
121
+ --z-dropdown: 1000;
122
+ --z-sticky: 1020;
123
+ --z-fixed: 1030;
124
+ --z-modal-backdrop: 1040;
125
+ --z-modal: 1050;
126
+ --z-popover: 1060;
127
+ --z-tooltip: 1070;
128
+ }
129
+
130
+ /* Dark mode */
131
+ body.dark-mode {
132
+ --bg-primary: var(--dark-bg-primary);
133
+ --bg-secondary: var(--dark-bg-secondary);
134
+ --bg-hover: var(--dark-bg-hover);
135
+ --text-primary: var(--dark-text-primary);
136
+ --text-secondary: var(--dark-text-secondary);
137
+ --text-muted: var(--dark-text-muted);
138
+ --border-color: var(--dark-border-color);
139
+ --border-color-hover: var(--dark-border-color-hover);
140
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Simple Event Emitter for Reactive State Management
3
+ * Provides publish/subscribe pattern for state changes
4
+ */
5
+
6
+ export type EventListener<T = any> = (data: T) => void;
7
+
8
+ export class EventEmitter<T = any> {
9
+ private listeners: EventListener<T>[] = [];
10
+
11
+ on(listener: EventListener<T>): () => void {
12
+ this.listeners.push(listener);
13
+
14
+ // Return unsubscribe function
15
+ return () => {
16
+ this.listeners = this.listeners.filter((l) => l !== listener);
17
+ };
18
+ }
19
+
20
+ once(listener: EventListener<T>): void {
21
+ const unsubscribe = this.on((data: T) => {
22
+ listener(data);
23
+ unsubscribe();
24
+ });
25
+ }
26
+
27
+ emit(data: T): void {
28
+ this.listeners.forEach((listener) => {
29
+ try {
30
+ listener(data);
31
+ } catch (error) {
32
+ console.error('Event listener error:', error);
33
+ }
34
+ });
35
+ }
36
+
37
+ clear(): void {
38
+ this.listeners = [];
39
+ }
40
+
41
+ getListenerCount(): number {
42
+ return this.listeners.length;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Base Store class with reactive updates
48
+ */
49
+ export abstract class Store<T> {
50
+ protected state: T;
51
+ protected stateChanged = new EventEmitter<T>();
52
+
53
+ constructor(initialState: T) {
54
+ this.state = initialState;
55
+ }
56
+
57
+ getState(): T {
58
+ return this.state;
59
+ }
60
+
61
+ setState(newState: Partial<T>): void {
62
+ this.state = { ...this.state, ...newState };
63
+ this.stateChanged.emit(this.state);
64
+ }
65
+
66
+ subscribe(listener: EventListener<T>): () => void {
67
+ return this.stateChanged.on(listener);
68
+ }
69
+
70
+ protected updateState(updater: (state: T) => T): void {
71
+ this.state = updater(this.state);
72
+ this.stateChanged.emit(this.state);
73
+ }
74
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Formatting utilities for displaying data
3
+ */
4
+
5
+ /**
6
+ * Format bytes to human-readable format
7
+ */
8
+ export function formatBytes(bytes: number, decimals = 2): string {
9
+ if (bytes === 0) return '0 Bytes';
10
+
11
+ const k = 1024;
12
+ const dm = decimals < 0 ? 0 : decimals;
13
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
14
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
15
+
16
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
17
+ }
18
+
19
+ /**
20
+ * Format date to readable string
21
+ */
22
+ export function formatDate(timestamp: number | string | Date): string {
23
+ const date = new Date(timestamp);
24
+ const today = new Date();
25
+ const yesterday = new Date(today);
26
+ yesterday.setDate(yesterday.getDate() - 1);
27
+
28
+ const isToday = date.toDateString() === today.toDateString();
29
+ const isYesterday = date.toDateString() === yesterday.toDateString();
30
+
31
+ if (isToday) {
32
+ return 'Today at ' + date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
33
+ }
34
+
35
+ if (isYesterday) {
36
+ return 'Yesterday at ' + date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
37
+ }
38
+
39
+ return date.toLocaleDateString('en-US', {
40
+ month: 'short',
41
+ day: 'numeric',
42
+ year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined,
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Format relative time (e.g., "2 hours ago")
48
+ */
49
+ export function formatRelativeTime(timestamp: number | string | Date): string {
50
+ const date = new Date(timestamp);
51
+ const now = new Date();
52
+ const diffMs = now.getTime() - date.getTime();
53
+ const diffSecs = Math.floor(diffMs / 1000);
54
+ const diffMins = Math.floor(diffSecs / 60);
55
+ const diffHours = Math.floor(diffMins / 60);
56
+ const diffDays = Math.floor(diffHours / 24);
57
+
58
+ if (diffSecs < 60) return 'just now';
59
+ if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
60
+ if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
61
+ if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
62
+
63
+ return formatDate(timestamp);
64
+ }
65
+
66
+ /**
67
+ * Truncate string to max length with ellipsis
68
+ */
69
+ export function truncate(str: string, maxLength: number): string {
70
+ if (str.length <= maxLength) return str;
71
+ return str.slice(0, maxLength - 3) + '...';
72
+ }
73
+
74
+ /**
75
+ * Capitalize first letter
76
+ */
77
+ export function capitalize(str: string): string {
78
+ return str.charAt(0).toUpperCase() + str.slice(1);
79
+ }
80
+
81
+ /**
82
+ * Format install method name for display
83
+ */
84
+ export function formatInstallMethod(method: string): string {
85
+ const methodMap: Record<string, string> = {
86
+ npm: 'npm',
87
+ yarn: 'Yarn',
88
+ pnpm: 'pnpm',
89
+ brew: 'Homebrew',
90
+ apt: 'apt',
91
+ yum: 'yum',
92
+ dnf: 'dnf',
93
+ custom: 'Custom',
94
+ };
95
+
96
+ return methodMap[method] || capitalize(method);
97
+ }
98
+
99
+ /**
100
+ * Get color for install method badge
101
+ */
102
+ export function getMethodBadgeColor(method: string): string {
103
+ const colorMap: Record<string, string> = {
104
+ npm: 'badge-primary',
105
+ yarn: 'badge-primary',
106
+ pnpm: 'badge-primary',
107
+ brew: 'badge-success',
108
+ apt: 'badge-warning',
109
+ yum: 'badge-warning',
110
+ dnf: 'badge-warning',
111
+ custom: 'badge-neutral',
112
+ };
113
+
114
+ return colorMap[method] || 'badge-neutral';
115
+ }
116
+
117
+ /**
118
+ * Format percentage for display
119
+ */
120
+ export function formatPercent(value: number, decimals = 1): string {
121
+ return parseFloat(value.toFixed(decimals)) + '%';
122
+ }
123
+
124
+ /**
125
+ * Debounce function for search/filter
126
+ */
127
+ export function debounce<T extends (...args: any[]) => any>(
128
+ func: T,
129
+ wait: number
130
+ ): (...args: Parameters<T>) => void {
131
+ let timeout: NodeJS.Timeout | null = null;
132
+
133
+ return function (...args: Parameters<T>) {
134
+ if (timeout) clearTimeout(timeout);
135
+ timeout = setTimeout(() => {
136
+ func(...args);
137
+ }, wait);
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Throttle function
143
+ */
144
+ export function throttle<T extends (...args: any[]) => any>(
145
+ func: T,
146
+ limit: number
147
+ ): (...args: Parameters<T>) => void {
148
+ let lastRun = 0;
149
+
150
+ return function (...args: Parameters<T>) {
151
+ const now = Date.now();
152
+ if (now - lastRun >= limit) {
153
+ lastRun = now;
154
+ func(...args);
155
+ }
156
+ };
157
+ }
@@ -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();