imcp 0.0.1

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 (124) hide show
  1. package/.github/ISSUE_TEMPLATE/JitAccess.yml +28 -0
  2. package/.github/acl/access.yml +20 -0
  3. package/.github/compliance/inventory.yml +5 -0
  4. package/.github/policies/jit.yml +19 -0
  5. package/README.md +137 -0
  6. package/dist/cli/commands/install.d.ts +2 -0
  7. package/dist/cli/commands/install.js +105 -0
  8. package/dist/cli/commands/list.d.ts +2 -0
  9. package/dist/cli/commands/list.js +90 -0
  10. package/dist/cli/commands/pull.d.ts +2 -0
  11. package/dist/cli/commands/pull.js +17 -0
  12. package/dist/cli/commands/serve.d.ts +2 -0
  13. package/dist/cli/commands/serve.js +32 -0
  14. package/dist/cli/commands/start.d.ts +2 -0
  15. package/dist/cli/commands/start.js +32 -0
  16. package/dist/cli/commands/sync.d.ts +2 -0
  17. package/dist/cli/commands/sync.js +17 -0
  18. package/dist/cli/commands/uninstall.d.ts +2 -0
  19. package/dist/cli/commands/uninstall.js +39 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.js +114 -0
  22. package/dist/core/ConfigurationProvider.d.ts +31 -0
  23. package/dist/core/ConfigurationProvider.js +416 -0
  24. package/dist/core/InstallationService.d.ts +17 -0
  25. package/dist/core/InstallationService.js +144 -0
  26. package/dist/core/MCPManager.d.ts +17 -0
  27. package/dist/core/MCPManager.js +98 -0
  28. package/dist/core/RequirementService.d.ts +45 -0
  29. package/dist/core/RequirementService.js +123 -0
  30. package/dist/core/constants.d.ts +29 -0
  31. package/dist/core/constants.js +55 -0
  32. package/dist/core/installers/BaseInstaller.d.ts +73 -0
  33. package/dist/core/installers/BaseInstaller.js +247 -0
  34. package/dist/core/installers/ClientInstaller.d.ts +17 -0
  35. package/dist/core/installers/ClientInstaller.js +307 -0
  36. package/dist/core/installers/CommandInstaller.d.ts +36 -0
  37. package/dist/core/installers/CommandInstaller.js +170 -0
  38. package/dist/core/installers/GeneralInstaller.d.ts +32 -0
  39. package/dist/core/installers/GeneralInstaller.js +87 -0
  40. package/dist/core/installers/InstallerFactory.d.ts +52 -0
  41. package/dist/core/installers/InstallerFactory.js +95 -0
  42. package/dist/core/installers/NpmInstaller.d.ts +25 -0
  43. package/dist/core/installers/NpmInstaller.js +123 -0
  44. package/dist/core/installers/PipInstaller.d.ts +25 -0
  45. package/dist/core/installers/PipInstaller.js +114 -0
  46. package/dist/core/installers/RequirementInstaller.d.ts +32 -0
  47. package/dist/core/installers/RequirementInstaller.js +3 -0
  48. package/dist/core/installers/index.d.ts +6 -0
  49. package/dist/core/installers/index.js +7 -0
  50. package/dist/core/types.d.ts +152 -0
  51. package/dist/core/types.js +16 -0
  52. package/dist/index.d.ts +11 -0
  53. package/dist/index.js +19 -0
  54. package/dist/services/InstallRequestValidator.d.ts +21 -0
  55. package/dist/services/InstallRequestValidator.js +99 -0
  56. package/dist/services/ServerService.d.ts +47 -0
  57. package/dist/services/ServerService.js +145 -0
  58. package/dist/utils/UpdateCheckTracker.d.ts +39 -0
  59. package/dist/utils/UpdateCheckTracker.js +80 -0
  60. package/dist/utils/clientUtils.d.ts +29 -0
  61. package/dist/utils/clientUtils.js +105 -0
  62. package/dist/utils/feedUtils.d.ts +5 -0
  63. package/dist/utils/feedUtils.js +29 -0
  64. package/dist/utils/githubAuth.d.ts +1 -0
  65. package/dist/utils/githubAuth.js +123 -0
  66. package/dist/utils/logger.d.ts +14 -0
  67. package/dist/utils/logger.js +90 -0
  68. package/dist/utils/osUtils.d.ts +16 -0
  69. package/dist/utils/osUtils.js +235 -0
  70. package/dist/web/public/css/modal.css +250 -0
  71. package/dist/web/public/css/notifications.css +70 -0
  72. package/dist/web/public/index.html +157 -0
  73. package/dist/web/public/js/api.js +213 -0
  74. package/dist/web/public/js/modal.js +572 -0
  75. package/dist/web/public/js/notifications.js +99 -0
  76. package/dist/web/public/js/serverCategoryDetails.js +210 -0
  77. package/dist/web/public/js/serverCategoryList.js +82 -0
  78. package/dist/web/public/modal.html +61 -0
  79. package/dist/web/public/styles.css +155 -0
  80. package/dist/web/server.d.ts +5 -0
  81. package/dist/web/server.js +150 -0
  82. package/package.json +53 -0
  83. package/src/cli/commands/install.ts +140 -0
  84. package/src/cli/commands/list.ts +112 -0
  85. package/src/cli/commands/pull.ts +16 -0
  86. package/src/cli/commands/serve.ts +37 -0
  87. package/src/cli/commands/uninstall.ts +54 -0
  88. package/src/cli/index.ts +127 -0
  89. package/src/core/ConfigurationProvider.ts +489 -0
  90. package/src/core/InstallationService.ts +173 -0
  91. package/src/core/MCPManager.ts +134 -0
  92. package/src/core/RequirementService.ts +147 -0
  93. package/src/core/constants.ts +61 -0
  94. package/src/core/installers/BaseInstaller.ts +292 -0
  95. package/src/core/installers/ClientInstaller.ts +423 -0
  96. package/src/core/installers/CommandInstaller.ts +185 -0
  97. package/src/core/installers/GeneralInstaller.ts +89 -0
  98. package/src/core/installers/InstallerFactory.ts +109 -0
  99. package/src/core/installers/NpmInstaller.ts +128 -0
  100. package/src/core/installers/PipInstaller.ts +121 -0
  101. package/src/core/installers/RequirementInstaller.ts +38 -0
  102. package/src/core/installers/index.ts +9 -0
  103. package/src/core/types.ts +163 -0
  104. package/src/index.ts +44 -0
  105. package/src/services/InstallRequestValidator.ts +112 -0
  106. package/src/services/ServerService.ts +181 -0
  107. package/src/utils/UpdateCheckTracker.ts +86 -0
  108. package/src/utils/clientUtils.ts +112 -0
  109. package/src/utils/feedUtils.ts +31 -0
  110. package/src/utils/githubAuth.ts +142 -0
  111. package/src/utils/logger.ts +101 -0
  112. package/src/utils/osUtils.ts +250 -0
  113. package/src/web/public/css/modal.css +250 -0
  114. package/src/web/public/css/notifications.css +70 -0
  115. package/src/web/public/index.html +157 -0
  116. package/src/web/public/js/api.js +213 -0
  117. package/src/web/public/js/modal.js +572 -0
  118. package/src/web/public/js/notifications.js +99 -0
  119. package/src/web/public/js/serverCategoryDetails.js +210 -0
  120. package/src/web/public/js/serverCategoryList.js +82 -0
  121. package/src/web/public/modal.html +61 -0
  122. package/src/web/public/styles.css +155 -0
  123. package/src/web/server.ts +195 -0
  124. package/tsconfig.json +18 -0
@@ -0,0 +1,572 @@
1
+ import { showToast, showConfirm } from './notifications.js';
2
+
3
+ // Wait for DOM to be loaded before initializing modal functionality
4
+ document.addEventListener('DOMContentLoaded', () => {
5
+ setupModalOutsideClick();
6
+ });
7
+ /** Delayed message queue for loading modal */
8
+ let messageQueue = [];
9
+ let isAppending = false;
10
+ function processMessageQueue() {
11
+ if (isAppending || messageQueue.length === 0) return;
12
+ isAppending = true;
13
+ const { msg } = messageQueue.shift();
14
+ _appendInstallLoadingMessage(msg);
15
+ setTimeout(() => {
16
+ isAppending = false;
17
+ processMessageQueue();
18
+ }, 1000);
19
+ }
20
+ // Always use this to append messages with delay
21
+ function delayedAppendInstallLoadingMessage(msg) {
22
+ messageQueue.push({ msg });
23
+ processMessageQueue();
24
+ }
25
+ // Internal: the original append function
26
+ function _appendInstallLoadingMessage(message) {
27
+ const loadingMsg = document.getElementById('installLoadingMessage');
28
+ if (loadingMsg) {
29
+ loadingMsg.innerHTML += `<div>${message}</div>`;
30
+ loadingMsg.scrollTop = loadingMsg.scrollHeight;
31
+ } else {
32
+ console.error('[LoadingModal] loading message DOM not found');
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Show the install loading modal and optionally append a message.
38
+ * @param {string} message
39
+ */
40
+
41
+ /**
42
+ * Show the install loading modal and optionally append a message.
43
+ * @param {string} message
44
+ */
45
+ function showInstallLoadingModal() {
46
+ const loadingModal = document.getElementById('installLoadingModal');
47
+ const loadingMsg = document.getElementById('installLoadingMessage');
48
+ if (loadingModal && loadingMsg) {
49
+ loadingModal.style.display = 'block';
50
+ loadingMsg.innerHTML = '';
51
+ } else {
52
+ console.error('[LoadingModal] loading modal DOM not found');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Append a message to the loading modal.
58
+ * @param {string} message
59
+ */
60
+ function appendInstallLoadingMessage(message) {
61
+ console.log('[LoadingModal] appendInstallLoadingMessage:', message);
62
+ const loadingMsg = document.getElementById('installLoadingMessage');
63
+ if (loadingMsg) {
64
+ loadingMsg.innerHTML += `<div>${message}</div>`;
65
+ loadingMsg.scrollTop = loadingMsg.scrollHeight;
66
+ } else {
67
+ console.error('[LoadingModal] loading message DOM not found');
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Hide the install loading modal.
73
+ */
74
+ function hideInstallLoadingModal() {
75
+ console.log('[LoadingModal] hideInstallLoadingModal called');
76
+ const loadingModal = document.getElementById('installLoadingModal');
77
+ if (loadingModal) {
78
+ loadingModal.style.display = 'none';
79
+ } else {
80
+ console.error('[LoadingModal] loading modal DOM not found');
81
+ }
82
+ }
83
+
84
+ // Show install modal for MCP tools
85
+ async function showInstallModal(categoryName, serverName, callback) {
86
+ console.log("Showing install modal for:", serverName);
87
+
88
+ // Wait for a short delay to ensure modal is loaded
89
+ await new Promise(resolve => setTimeout(resolve, 100));
90
+
91
+ const modal = document.getElementById('installModal');
92
+ if (!modal) {
93
+ console.error('Modal container not found');
94
+ return;
95
+ }
96
+
97
+ const title = modal.querySelector('#modalTitle');
98
+ const envInputsDiv = modal.querySelector('#modalEnvInputs');
99
+ const targetDiv = modal.querySelector('#modalTargets');
100
+ const modalRequirements = modal.querySelector('#modalRequirements');
101
+
102
+ // Global array to track selected clients
103
+ window.selectedClients = [];
104
+
105
+ // Verify all required modal elements exist
106
+ if (!title || !envInputsDiv || !targetDiv || !modalRequirements) {
107
+ console.error('Required modal elements not found');
108
+ return;
109
+ }
110
+
111
+ title.textContent = `Install ${serverName}`;
112
+ envInputsDiv.innerHTML = ''; // Clear previous inputs
113
+ targetDiv.innerHTML = ''; // Clear previous targets
114
+ modalRequirements.innerHTML = ''; // Clear previous requirements
115
+
116
+ try {
117
+ // Fetch both targets and server data simultaneously
118
+ const [targetResponse, serverResponse] = await Promise.all([
119
+ fetch('/api/targets'),
120
+ fetch(`/api/categories/${categoryName}`)
121
+ ]);
122
+
123
+ if (!targetResponse.ok || !serverResponse.ok) {
124
+ throw new Error('Failed to fetch required data');
125
+ }
126
+
127
+ const [targetData, serverData] = await Promise.all([
128
+ targetResponse.json(),
129
+ serverResponse.json()
130
+ ]);
131
+
132
+ if (!targetData.success || !serverData.success) {
133
+ throw new Error('Invalid data received');
134
+ }
135
+
136
+ const mcpServer = serverData.data.feedConfiguration.mcpServers.find(server => server.name === serverName);
137
+ if (!mcpServer) {
138
+ throw new Error('Server configuration not found');
139
+ }
140
+
141
+ const installationStatus = serverData.data.installationStatus || {};
142
+ const serverStatuses = installationStatus.serversStatus || {};
143
+ const serverStatus = serverStatuses[serverName] || { installedStatus: {} };
144
+
145
+ // Create client items with switch toggles
146
+ targetData.data.forEach(target => {
147
+ const operationStatus = serverStatus.installedStatus[target] || { status: 'not-installed', type: 'check', target: 'server' };
148
+
149
+ // Determine client status
150
+ let statusText = 'Not Installed';
151
+ let statusClass = 'not-installed';
152
+
153
+ if (operationStatus.status === 'completed' && operationStatus.type === 'install') {
154
+ statusText = 'Installed';
155
+ statusClass = 'installed';
156
+ } else if (operationStatus.status === 'pending') {
157
+ statusText = 'Pending Requirements';
158
+ statusClass = 'pending';
159
+ } else if (operationStatus.status === 'in-progress') {
160
+ statusText = 'In Progress';
161
+ statusClass = 'pending';
162
+ } else if (operationStatus.status === 'failed') {
163
+ statusText = 'Failed';
164
+ statusClass = 'not-installed';
165
+ }
166
+
167
+ const isConfigured = operationStatus.status === 'completed' && operationStatus.type === 'install';
168
+
169
+ // Create client item
170
+ const clientItem = document.createElement('div');
171
+ clientItem.className = 'client-item';
172
+ clientItem.dataset.target = target;
173
+ clientItem.dataset.selected = 'false';
174
+
175
+ // Determine if item should be selectable
176
+ const isInProgress = operationStatus.status === 'in-progress';
177
+ const isSelectable = !isConfigured && !isInProgress;
178
+
179
+ // Add appropriate non-selectable classes
180
+ if (!isSelectable) {
181
+ clientItem.classList.add('non-selectable');
182
+
183
+ if (isConfigured) {
184
+ clientItem.classList.add('installed-item');
185
+ clientItem.title = 'Already installed';
186
+ } else if (isInProgress) {
187
+ clientItem.classList.add('in-progress-item');
188
+ clientItem.title = 'Installation in progress';
189
+ }
190
+ }
191
+
192
+ // Make selectable clients clickable
193
+ if (isSelectable) {
194
+ clientItem.addEventListener('click', (e) => {
195
+ const isSelected = clientItem.dataset.selected === 'true';
196
+ clientItem.dataset.selected = isSelected ? 'false' : 'true';
197
+
198
+ // Update class
199
+ if (isSelected) {
200
+ clientItem.classList.remove('selected');
201
+ window.selectedClients = window.selectedClients.filter(c => c !== target);
202
+ } else {
203
+ clientItem.classList.add('selected');
204
+ if (!window.selectedClients.includes(target)) {
205
+ window.selectedClients.push(target);
206
+ }
207
+ }
208
+
209
+ console.log('Selected clients:', window.selectedClients);
210
+ });
211
+ }
212
+
213
+ // Create client info (name)
214
+ const clientInfo = document.createElement('div');
215
+ clientInfo.className = 'client-info';
216
+
217
+ // Client name label
218
+ const clientName = document.createElement('span');
219
+ clientName.className = 'text-sm font-medium text-gray-900';
220
+ clientName.textContent = target;
221
+
222
+ // Add elements to client info
223
+ clientInfo.appendChild(clientName);
224
+
225
+ // Status badge
226
+ const statusBadge = document.createElement('span');
227
+ statusBadge.className = `status-badge ${statusClass}`;
228
+ statusBadge.textContent = statusText;
229
+
230
+ // Add components to client item
231
+ clientItem.appendChild(clientInfo);
232
+ clientItem.appendChild(statusBadge);
233
+
234
+ // Add client item to target div
235
+ targetDiv.appendChild(clientItem);
236
+ });
237
+
238
+ // Add section title at the top of the modal target div
239
+ if (!targetDiv.querySelector('.section-title')) {
240
+ const titleElement = document.createElement('h3');
241
+ titleElement.className = 'section-title text-lg font-semibold text-gray-700 mb-3';
242
+ titleElement.textContent = 'Client Status';
243
+ targetDiv.insertBefore(titleElement, targetDiv.firstChild);
244
+ }
245
+
246
+ // Handle environment variables section
247
+ const envRequirements = mcpServer.installation?.['env'] || mcpServer.installation?.env || {};
248
+
249
+ // Make sure environment variables section has a title
250
+ if (!envInputsDiv.querySelector('.section-title')) {
251
+ const envTitle = document.createElement('h3');
252
+ envTitle.className = 'section-title text-lg font-semibold text-gray-700 mb-3';
253
+ envTitle.textContent = 'Environment Variables';
254
+ envInputsDiv.insertBefore(envTitle, envInputsDiv.firstChild);
255
+ }
256
+
257
+ if (Object.keys(envRequirements).length === 0) {
258
+ const noEnvMessage = document.createElement('p');
259
+ noEnvMessage.className = 'text-gray-600';
260
+ noEnvMessage.textContent = 'No environment variables required for this MCP server.';
261
+ envInputsDiv.appendChild(noEnvMessage);
262
+ } else {
263
+ // Create inputs for each environment variable
264
+ Object.keys(envRequirements).forEach(key => {
265
+ const req = envRequirements[key];
266
+ const inputId = `env_${key}`;
267
+ const inputWrapper = document.createElement('div');
268
+ inputWrapper.className = 'mb-3';
269
+
270
+ const label = document.createElement('label');
271
+ label.htmlFor = inputId;
272
+ label.className = 'block text-sm font-medium text-gray-700 mb-1';
273
+ label.innerHTML = `${key} ${req.Required ? '<span class="text-red-500">*</span>' : ''}`;
274
+
275
+ const input = document.createElement('input');
276
+ input.type = 'text';
277
+ input.id = inputId;
278
+ input.name = key;
279
+ input.placeholder = req.Description || key;
280
+ input.value = req.Default || '';
281
+ input.required = req.Required;
282
+ input.className = 'input-field';
283
+
284
+ inputWrapper.appendChild(label);
285
+ inputWrapper.appendChild(input);
286
+
287
+ if (req.Description) {
288
+ const description = document.createElement('p');
289
+ description.className = 'text-xs text-gray-500 mt-1';
290
+ description.textContent = req.Description;
291
+ inputWrapper.appendChild(description);
292
+ }
293
+
294
+ envInputsDiv.appendChild(inputWrapper);
295
+ });
296
+ }
297
+
298
+ // Handle server requirements section last
299
+ const serverRequirements = mcpServer.dependencies?.requirements || [];
300
+ const requirements = serverData.data.installationStatus?.requirementsStatus || {};
301
+
302
+ if (serverRequirements.length > 0) {
303
+ const reqHtml = serverRequirements.map(req => {
304
+ const status = requirements[req.name] || {};
305
+ const statusClass = status.installed
306
+ ? 'text-green-600 bg-green-50'
307
+ : 'text-yellow-600 bg-yellow-50';
308
+ const statusText = status.installed ? 'Installed' : 'Required';
309
+
310
+ return `
311
+ <div class="border border-gray-200 p-3 rounded-lg mb-2 hover:bg-gray-50">
312
+ <div class="flex justify-between items-center">
313
+ <div>
314
+ <div class="font-semibold text-gray-800">${req.name}</div>
315
+ <div class="text-sm text-gray-600 shadow-sm p-1 rounded bg-gray-50">
316
+ <span class="font-medium">${status.type || 'package'}</span>${status.version ? ` • <span class="font-medium">${status.version}</span>` : ''}
317
+ </div>
318
+ </div>
319
+ <span class="${statusClass} inline-flex items-center px-3 py-1 rounded-full text-sm">
320
+ ${statusText}
321
+ </span>
322
+ </div>
323
+ </div>
324
+ `;
325
+ }).join('');
326
+
327
+ modalRequirements.innerHTML = `
328
+ <h3 class="text-lg font-semibold text-gray-700 mb-3">Dependencies</h3>
329
+ <p class="text-sm text-gray-600 mb-4">These dependencies will be automatically installed when installing the server</p>
330
+ ${reqHtml}
331
+ `;
332
+ } else {
333
+ modalRequirements.innerHTML = '<p class="text-gray-600">No additional dependencies required.</p>';
334
+ }
335
+
336
+ // Set up the install form submit handler
337
+ const installForm = document.getElementById('installForm');
338
+ installForm.onsubmit = (e) => {
339
+ e.preventDefault();
340
+
341
+ // Get all environment variables
342
+ const envVars = {};
343
+ const inputs = envInputsDiv.querySelectorAll('input');
344
+ inputs.forEach(input => {
345
+ if (input.name && input.value) {
346
+ envVars[input.name] = input.value;
347
+ }
348
+ });
349
+
350
+ // Get selected clients
351
+ const selectedTargets = window.selectedClients.length > 0 ?
352
+ window.selectedClients :
353
+ Array.from(document.querySelectorAll('.client-item.selected'))
354
+ .map(item => item.dataset.target);
355
+
356
+ if (selectedTargets.length === 0) {
357
+ showToast('Please select at least one client to configure.', 'error');
358
+ return;
359
+ }
360
+
361
+ // Call install function with selected targets
362
+ // Find installing message for the first selected target
363
+ let installingMessage = "Starting installation...";
364
+ const serverStatus = serverStatuses[serverName] || { installedStatus: {} };
365
+ if (selectedTargets.length > 0) {
366
+ const target = selectedTargets[0];
367
+ const msg = serverStatus.installedStatus?.[target]?.message;
368
+ if (msg) installingMessage = msg;
369
+ }
370
+ handleBulkClientInstall(categoryName, serverName, selectedTargets, envVars, installingMessage, serverData);
371
+ };
372
+
373
+ } catch (error) {
374
+ console.error("Error loading data:", error);
375
+ targetDiv.innerHTML = `<p class="text-red-500">Error: ${error.message}</p>`;
376
+ }
377
+
378
+ modal.style.display = "block";
379
+ }
380
+
381
+ // Function to handle bulk client installations
382
+ async function handleBulkClientInstall(categoryName, serverName, targets, envVars = {}, installingMessage = "Starting installation...", serverData = null) {
383
+ console.log('[LoadingModal] handleBulkClientInstall called', { categoryName, serverName, targets, envVars });
384
+ // Hide install modal, show loading modal
385
+ const installModal = document.getElementById('installModal');
386
+ console.log('[LoadingModal] installModal:', installModal);
387
+ if (installModal) installModal.style.display = "none";
388
+
389
+ // If serverData is provided, extract the installing message from it (latest status)
390
+ if (serverData && serverData.data && serverData.data.installationStatus && serverData.data.installationStatus.serversStatus) {
391
+ const serverStatuses = serverData.data.installationStatus.serversStatus;
392
+ const serverStatus = serverStatuses[serverName] || { installedStatus: {} };
393
+ if (targets && targets.length > 0) {
394
+ const target = targets[0];
395
+ const msg = serverStatus.installedStatus?.[target]?.message;
396
+ if (msg) installingMessage = msg;
397
+ }
398
+ // Append the installing message for user visibility
399
+ if (installingMessage && installingMessage !== "Starting installation...") {
400
+ delayedAppendInstallLoadingMessage(installingMessage);
401
+ }
402
+ }
403
+
404
+ showInstallLoadingModal(installingMessage);
405
+
406
+ delayedAppendInstallLoadingMessage(installingMessage);
407
+
408
+ try {
409
+ delayedAppendInstallLoadingMessage("Installing, please wait...");
410
+ const response = await fetch(`/api/categories/${categoryName}/install`, {
411
+ method: 'POST',
412
+ headers: {
413
+ 'Content-Type': 'application/json',
414
+ 'Accept': 'application/json'
415
+ },
416
+ body: JSON.stringify({
417
+ serverList: {
418
+ [serverName]: {
419
+ targetClients: targets,
420
+ env: envVars
421
+ }
422
+ }
423
+ })
424
+ });
425
+
426
+ console.log('[LoadingModal] fetch install response:', response);
427
+
428
+ if (!response.ok) {
429
+ const errorData = await response.text();
430
+ delayedAppendInstallLoadingMessage(`<span style="color:red;">Installation failed: ${errorData || response.statusText}</span>`);
431
+ console.error('[LoadingModal] Installation failed:', errorData || response.statusText);
432
+ throw new Error(`Installation failed: ${errorData || response.statusText}`);
433
+ }
434
+
435
+ const result = await response.json();
436
+ console.log('[LoadingModal] install result:', result);
437
+ if (!result.success) {
438
+ delayedAppendInstallLoadingMessage(`<span style="color:red;">${result.error || 'Installation failed'}</span>`);
439
+ console.error('[LoadingModal] install result error:', result.error || 'Installation failed');
440
+ throw new Error(result.error || 'Installation failed');
441
+ }
442
+
443
+ // Optionally, you can refresh modal data here or trigger a callback
444
+ // Start polling for install status
445
+ pollInstallStatus(categoryName, serverName, targets);
446
+ } catch (error) {
447
+ console.error('[LoadingModal] Error applying configuration:', error);
448
+ delayedAppendInstallLoadingMessage(`<span style="color:red;">Error: ${error.message}</span>`);
449
+ setTimeout(() => {
450
+ hideInstallLoadingModal();
451
+ setTimeout(() => {
452
+ location.reload();
453
+ }, 200);
454
+ }, 3200);
455
+ }
456
+ }
457
+
458
+ // Poll install status for the given server/targets
459
+ async function pollInstallStatus(categoryName, serverName, targets, interval = 2000, maxTries = 60) {
460
+ let tries = 0;
461
+ let lastMessages = {};
462
+ // Use global delayedAppendInstallLoadingMessage and queue logic
463
+ while (tries < maxTries) {
464
+ try {
465
+ const resp = await fetch(`/api/categories/${categoryName}`);
466
+ if (resp.ok) {
467
+ const data = await resp.json();
468
+ const serverStatuses = data?.data?.installationStatus?.serversStatus || {};
469
+ const serverStatus = serverStatuses[serverName] || { installedStatus: {} };
470
+ let allCompleted = true;
471
+ for (const target of targets) {
472
+ const status = serverStatus.installedStatus?.[target]?.status;
473
+ const msg = serverStatus.installedStatus?.[target]?.message;
474
+ // Only append new messages
475
+ if (msg && lastMessages[target] !== msg) {
476
+ delayedAppendInstallLoadingMessage(`[${target}] ${msg}`);
477
+ lastMessages[target] = msg;
478
+ }
479
+ if (status !== "completed") {
480
+ allCompleted = false;
481
+ }
482
+ }
483
+ if (allCompleted) {
484
+ delayedAppendInstallLoadingMessage(`<span style="color:green;">Configuration applied successfully for ${targets.length} client(s).</span>`);
485
+ setTimeout(() => {
486
+ hideInstallLoadingModal();
487
+ setTimeout(() => {
488
+ location.reload();
489
+ }, 200);
490
+ }, 5000);
491
+ return;
492
+ }
493
+ }
494
+ } catch (e) {
495
+ // Ignore errors and continue polling
496
+ }
497
+ await new Promise(resolve => setTimeout(resolve, interval));
498
+ tries++;
499
+ }
500
+ // Timeout: close modal and reload anyway
501
+ hideInstallLoadingModal();
502
+ setTimeout(() => {
503
+ location.reload();
504
+ }, 200);
505
+ }
506
+
507
+ // Function to handle client uninstallation for multiple targets
508
+ async function uninstallTools(categoryName, serverList, targets) {
509
+ if (!Array.isArray(targets)) {
510
+ targets = [targets]; // Convert single target to array for backward compatibility
511
+ }
512
+
513
+ const confirmed = await showConfirm(`Are you sure you want to uninstall this server for ${targets.length} client(s)?`);
514
+ if (!confirmed) {
515
+ return;
516
+ }
517
+
518
+ try {
519
+ const response = await fetch(`/api/categories/${categoryName}/uninstall`, {
520
+ method: 'POST',
521
+ headers: { 'Content-Type': 'application/json' },
522
+ body: JSON.stringify({
523
+ toolList: serverList,
524
+ options: {
525
+ targets: targets
526
+ }
527
+ })
528
+ });
529
+
530
+ if (!response.ok) {
531
+ const errorData = await response.text();
532
+ throw new Error(`Uninstallation failed: ${errorData || response.statusText}`);
533
+ }
534
+
535
+ const result = await response.json();
536
+ if (!result.success) {
537
+ throw new Error(result.error || 'Uninstallation failed');
538
+ }
539
+
540
+ showToast(`Successfully uninstalled for ${targets.length} client(s).`, 'success');
541
+ location.reload(); // Refresh the page to update the UI
542
+ } catch (error) {
543
+ console.error('Error uninstalling tools:', error);
544
+ showToast(`Error uninstalling tools: ${error.message}`, 'error');
545
+ }
546
+ }
547
+
548
+ // Close modal
549
+ function closeModal() {
550
+ document.getElementById('installModal').style.display = "none";
551
+ }
552
+
553
+ // Close modal if clicked outside content
554
+ function setupModalOutsideClick() {
555
+ window.onclick = function (event) {
556
+ const modal = document.getElementById('installModal');
557
+ if (event.target == modal) {
558
+ closeModal();
559
+ }
560
+ };
561
+ }
562
+
563
+ // Compatibility function for individual client installation
564
+ async function handleClientInstall(categoryName, serverName, target, envVars = {}) {
565
+ return handleBulkClientInstall(categoryName, serverName, [target], envVars);
566
+ }
567
+ window.showInstallLoadingModal = showInstallLoadingModal;
568
+ window.appendInstallLoadingMessage = appendInstallLoadingMessage;
569
+ window.hideInstallLoadingModal = hideInstallLoadingModal;
570
+
571
+
572
+ export { showInstallModal, closeModal, setupModalOutsideClick, uninstallTools };
@@ -0,0 +1,99 @@
1
+ // Function to show a Bootstrap alert notification
2
+ function showToast(message, type = 'success') {
3
+ const alertContainer = document.querySelector('.alert-container');
4
+ const alertId = `alert-${Date.now()}`;
5
+
6
+ const alertHtml = `
7
+ <div id="${alertId}" class="alert alert-${type} alert-dismissible fade show" role="alert">
8
+ ${type === 'success' ?
9
+ '<i class="bi bi-check-circle-fill me-2"></i>' :
10
+ '<i class="bi bi-exclamation-triangle-fill me-2"></i>'
11
+ }
12
+ ${message}
13
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
14
+ </div>
15
+ `;
16
+
17
+ alertContainer.insertAdjacentHTML('beforeend', alertHtml);
18
+
19
+ const alertElement = document.getElementById(alertId);
20
+
21
+ // Remove the alert after 5 seconds
22
+ setTimeout(() => {
23
+ const bsAlert = new bootstrap.Alert(alertElement);
24
+ bsAlert.close();
25
+ }, 3000); // Reduced to 3 seconds
26
+
27
+ // Remove the element after animation
28
+ alertElement.addEventListener('closed.bs.alert', () => {
29
+ alertElement.remove();
30
+ });
31
+ }
32
+
33
+ // Function to show a Bootstrap confirmation dialog
34
+ function showConfirm(message) {
35
+ return new Promise((resolve) => {
36
+ const modalId = `confirm-modal-${Date.now()}`;
37
+ const modalHtml = `
38
+ <div class="modal fade" id="${modalId}" tabindex="-1" aria-labelledby="${modalId}-label" aria-hidden="true">
39
+ <div class="modal-dialog">
40
+ <div class="modal-content">
41
+ <div class="modal-header">
42
+ <h5 class="modal-title" id="${modalId}-label">Confirm Action</h5>
43
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
44
+ </div>
45
+ <div class="modal-body">
46
+ ${message}
47
+ </div>
48
+ <div class="modal-footer">
49
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
50
+ <button type="button" class="btn btn-primary confirm-action">
51
+ <span class="spinner-border spinner-border-sm d-none me-2" role="status" aria-hidden="true"></span>
52
+ Confirm
53
+ </button>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ `;
59
+
60
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
61
+
62
+ const modalElement = document.getElementById(modalId);
63
+ const modal = new bootstrap.Modal(modalElement);
64
+
65
+ // Handle confirm button click
66
+ modalElement.querySelector('.confirm-action').addEventListener('click', (event) => {
67
+ const button = event.currentTarget;
68
+ const spinner = button.querySelector('.spinner-border');
69
+ button.disabled = true;
70
+ spinner.classList.remove('d-none');
71
+
72
+ // Add small delay to show the loading state
73
+ setTimeout(() => {
74
+ modal.hide();
75
+ resolve(true);
76
+ }, 300);
77
+ });
78
+
79
+ // Handle modal hidden event
80
+ modalElement.addEventListener('hidden.bs.modal', () => {
81
+ modalElement.remove();
82
+ resolve(false);
83
+ });
84
+
85
+ modal.show();
86
+ });
87
+ }
88
+
89
+ export { showToast, showConfirm };
90
+
91
+ // Display install notification after page reload
92
+ document.addEventListener('DOMContentLoaded', () => {
93
+ const data = sessionStorage.getItem('installNotification');
94
+ if (data) {
95
+ const { message, type } = JSON.parse(data);
96
+ showToast(message, type);
97
+ sessionStorage.removeItem('installNotification');
98
+ }
99
+ });