imcp 0.0.18 → 0.1.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 (96) hide show
  1. package/.roo/rules-code/rules.md +88 -0
  2. package/dist/cli/index.js +0 -0
  3. package/dist/core/metadatas/constants.d.ts +7 -0
  4. package/dist/core/metadatas/constants.js +7 -0
  5. package/dist/core/onboard/FeedOnboardService.d.ts +7 -3
  6. package/dist/core/onboard/FeedOnboardService.js +52 -5
  7. package/dist/core/onboard/OnboardProcessor.js +22 -22
  8. package/dist/services/MCPManager.js +66 -6
  9. package/dist/services/TelemetryService.d.ts +15 -0
  10. package/dist/services/TelemetryService.js +54 -0
  11. package/dist/utils/githubAuth.js +65 -0
  12. package/dist/utils/logger.d.ts +16 -0
  13. package/dist/utils/logger.js +78 -1
  14. package/dist/utils/versionUtils.d.ts +1 -0
  15. package/dist/utils/versionUtils.js +29 -0
  16. package/dist/web/public/css/serverCategoryList.css +120 -0
  17. package/dist/web/public/index.html +6 -3
  18. package/dist/web/public/js/flights/flights.js +0 -1
  19. package/dist/web/public/js/onboard/formProcessor.js +18 -11
  20. package/dist/web/public/js/onboard/publishHandler.js +30 -0
  21. package/dist/web/public/js/onboard/templates.js +5 -1
  22. package/dist/web/public/js/onboard/uiHandlers.js +266 -39
  23. package/dist/web/public/js/onboard/validationHandlers.js +71 -39
  24. package/dist/web/public/js/serverCategoryList.js +91 -7
  25. package/dist/web/public/onboard.html +2 -2
  26. package/dist/web/server.js +11 -1
  27. package/{src/web/public/js/onboard → docs}/ONBOARDING_PAGE_DESIGN.md +15 -125
  28. package/docs/Telemetry.md +136 -0
  29. package/memory-bank/activeContext.md +14 -0
  30. package/memory-bank/decisionLog.md +28 -0
  31. package/memory-bank/productContext.md +41 -0
  32. package/memory-bank/progress.md +5 -0
  33. package/memory-bank/systemPatterns.md +3 -0
  34. package/package.json +2 -1
  35. package/src/core/metadatas/constants.ts +9 -0
  36. package/src/core/onboard/FeedOnboardService.ts +59 -5
  37. package/src/core/onboard/OnboardProcessor.ts +25 -23
  38. package/src/services/MCPManager.ts +78 -8
  39. package/src/services/TelemetryService.ts +59 -0
  40. package/src/utils/githubAuth.ts +84 -1
  41. package/src/utils/logger.ts +83 -1
  42. package/src/utils/versionUtils.ts +33 -0
  43. package/src/web/public/css/serverCategoryList.css +120 -0
  44. package/src/web/public/index.html +6 -3
  45. package/src/web/public/js/onboard/formProcessor.js +18 -11
  46. package/src/web/public/js/onboard/publishHandler.js +30 -0
  47. package/src/web/public/js/onboard/templates.js +5 -1
  48. package/src/web/public/js/onboard/uiHandlers.js +266 -39
  49. package/src/web/public/js/onboard/validationHandlers.js +71 -39
  50. package/src/web/public/js/serverCategoryList.js +91 -7
  51. package/src/web/public/onboard.html +2 -2
  52. package/src/web/server.ts +11 -1
  53. package/dist/cli/commands/start.d.ts +0 -2
  54. package/dist/cli/commands/start.js +0 -32
  55. package/dist/cli/commands/sync.d.ts +0 -2
  56. package/dist/cli/commands/sync.js +0 -17
  57. package/dist/core/ConfigurationLoader.d.ts +0 -32
  58. package/dist/core/ConfigurationLoader.js +0 -236
  59. package/dist/core/ConfigurationProvider.d.ts +0 -35
  60. package/dist/core/ConfigurationProvider.js +0 -375
  61. package/dist/core/InstallationService.d.ts +0 -50
  62. package/dist/core/InstallationService.js +0 -350
  63. package/dist/core/MCPManager.d.ts +0 -28
  64. package/dist/core/MCPManager.js +0 -188
  65. package/dist/core/RequirementService.d.ts +0 -40
  66. package/dist/core/RequirementService.js +0 -110
  67. package/dist/core/ServerSchemaLoader.d.ts +0 -11
  68. package/dist/core/ServerSchemaLoader.js +0 -43
  69. package/dist/core/ServerSchemaProvider.d.ts +0 -17
  70. package/dist/core/ServerSchemaProvider.js +0 -120
  71. package/dist/core/constants.d.ts +0 -47
  72. package/dist/core/constants.js +0 -94
  73. package/dist/core/installers/BaseInstaller.d.ts +0 -74
  74. package/dist/core/installers/BaseInstaller.js +0 -253
  75. package/dist/core/installers/ClientInstaller.d.ts +0 -23
  76. package/dist/core/installers/ClientInstaller.js +0 -564
  77. package/dist/core/installers/CommandInstaller.d.ts +0 -37
  78. package/dist/core/installers/CommandInstaller.js +0 -173
  79. package/dist/core/installers/GeneralInstaller.d.ts +0 -33
  80. package/dist/core/installers/GeneralInstaller.js +0 -85
  81. package/dist/core/installers/InstallerFactory.d.ts +0 -54
  82. package/dist/core/installers/InstallerFactory.js +0 -97
  83. package/dist/core/installers/NpmInstaller.d.ts +0 -26
  84. package/dist/core/installers/NpmInstaller.js +0 -127
  85. package/dist/core/installers/PipInstaller.d.ts +0 -28
  86. package/dist/core/installers/PipInstaller.js +0 -127
  87. package/dist/core/installers/RequirementInstaller.d.ts +0 -33
  88. package/dist/core/installers/RequirementInstaller.js +0 -3
  89. package/dist/core/types.d.ts +0 -166
  90. package/dist/core/types.js +0 -16
  91. package/dist/services/InstallRequestValidator.d.ts +0 -21
  92. package/dist/services/InstallRequestValidator.js +0 -99
  93. package/dist/web/public/js/modal/installHandler.js +0 -227
  94. package/dist/web/public/js/modal/loadingUI.js +0 -74
  95. package/dist/web/public/js/modal/modalUI.js +0 -214
  96. package/dist/web/public/js/modal/version.js +0 -20
@@ -70,29 +70,29 @@ function reindexServers(serversListId = 'serversList') {
70
70
  // Ensure the server header toggle onclick is correctly re-indexed for its ID parameters
71
71
  const headerToggle = serverItem.querySelector(`#${serversListId}-server-header-${oldServerIndex}`);
72
72
  if (headerToggle) {
73
- headerToggle.id = `${serversListId}-server-header-${newServerIndex}`;
74
- const oldContentId = `${serversListId}-server-content-${oldServerIndex}`;
75
- const newContentId = `${serversListId}-server-content-${newServerIndex}`;
76
- let onclickAttr = headerToggle.getAttribute('onclick');
77
- if (onclickAttr) {
78
- onclickAttr = onclickAttr.replace(new RegExp(oldContentId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newContentId);
79
- headerToggle.setAttribute('onclick', onclickAttr);
80
- }
81
- let onkeydownAttr = headerToggle.getAttribute('onkeydown');
82
- if (onkeydownAttr) {
83
- onkeydownAttr = onkeydownAttr.replace(new RegExp(oldContentId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newContentId);
84
- headerToggle.setAttribute('onkeydown', onkeydownAttr);
85
- }
86
- headerToggle.setAttribute('aria-controls', newContentId);
73
+ headerToggle.id = `${serversListId}-server-header-${newServerIndex}`;
74
+ const oldContentId = `${serversListId}-server-content-${oldServerIndex}`;
75
+ const newContentId = `${serversListId}-server-content-${newServerIndex}`;
76
+ let onclickAttr = headerToggle.getAttribute('onclick');
77
+ if (onclickAttr) {
78
+ onclickAttr = onclickAttr.replace(new RegExp(oldContentId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newContentId);
79
+ headerToggle.setAttribute('onclick', onclickAttr);
80
+ }
81
+ let onkeydownAttr = headerToggle.getAttribute('onkeydown');
82
+ if (onkeydownAttr) {
83
+ onkeydownAttr = onkeydownAttr.replace(new RegExp(oldContentId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newContentId);
84
+ headerToggle.setAttribute('onkeydown', onkeydownAttr);
85
+ }
86
+ headerToggle.setAttribute('aria-controls', newContentId);
87
87
  }
88
88
  // Update title ID for aria-labelledby
89
89
  const titleForAria = serverItem.querySelector(`#${serversListId}-server-title-${oldServerIndex}`);
90
90
  if (titleForAria) {
91
- titleForAria.id = `${serversListId}-server-title-${newServerIndex}`;
91
+ titleForAria.id = `${serversListId}-server-title-${newServerIndex}`;
92
92
  }
93
93
  const contentRegion = serverItem.querySelector(`#${serversListId}-server-content-${oldServerIndex}`);
94
94
  if (contentRegion) {
95
- contentRegion.setAttribute('aria-labelledby', `${serversListId}-server-title-${newServerIndex}`);
95
+ contentRegion.setAttribute('aria-labelledby', `${serversListId}-server-title-${newServerIndex}`);
96
96
  }
97
97
 
98
98
 
@@ -225,17 +225,24 @@ export function addServer(serversListId = 'serversList', isContextGenerallyReadO
225
225
 
226
226
  const serverItem = container.querySelector(`.server-item[data-index="${newServerIndex}"]`);
227
227
  if (serverItem) {
228
- if (serverData && serverData.systemTags) { // Case 1: Populating from existing serverData that has systemTags
228
+ // Handle systemTags dataset
229
+ if (serverData && serverData.systemTags) {
229
230
  serverItem.dataset.systemTags = JSON.stringify(serverData.systemTags);
230
- } else if (serverData && !serverData.systemTags) { // Case 3: Populating from serverData that *lacks* systemTags
231
- // Ensure no stale adhoc tag if the DOM element was somehow reused and had it previously.
231
+ } else if (serverData && !serverData.systemTags) {
232
232
  delete serverItem.dataset.systemTags;
233
- } else if (!serverData) { // Case 2: Adding a brand new server via "Add Server" button click (serverData is null)
234
- // A brand new server is NOT adhoc by default. It becomes adhoc if modified (e.g. via JSON).
235
- // This fulfills part of Req 3: newly added one in create-server tab should not have systemTags.
236
- // And implicitly part of Req 2: new server in create-category tab also won't have systemTags.
233
+ } else if (!serverData) {
237
234
  delete serverItem.dataset.systemTags;
238
235
  }
236
+
237
+ // Handle originalName dataset
238
+ if (serverData && serverData.name) {
239
+ serverItem.dataset.originalName = serverData.name;
240
+ } else {
241
+ // If it's a new server (serverData is null) or serverData has no name,
242
+ // ensure no stale originalName if the DOM element was somehow reused.
243
+ delete serverItem.dataset.originalName;
244
+ }
245
+
239
246
  setupServerMode(serverItem, newServerIndex, serversListId, actualReadOnlyForThisServer, serverData);
240
247
  // Pass serverData to setupReadOnlyState so it can accurately determine adhoc status for title.
241
248
  setupReadOnlyState(serverItem, actualReadOnlyForThisServer, serverData, serversListId, newServerIndex);
@@ -334,11 +341,11 @@ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPop
334
341
  // If it's somehow missing and it's adhoc, ensure it's present.
335
342
  // This situation should be rare if template logic is correct.
336
343
  if (!titleElement.querySelector('span.text-blue-600')) {
337
- titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
344
+ titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
338
345
  } else {
339
- // Ensure base title is correct if adhoc span is already there
340
- const adhocSpanHTML = titleElement.querySelector('span.text-blue-600').outerHTML;
341
- titleElement.innerHTML = `${baseTitle} ${adhocSpanHTML}`;
346
+ // Ensure base title is correct if adhoc span is already there
347
+ const adhocSpanHTML = titleElement.querySelector('span.text-blue-600').outerHTML;
348
+ titleElement.innerHTML = `${baseTitle} ${adhocSpanHTML}`;
342
349
  }
343
350
  }
344
351
  }
@@ -353,10 +360,20 @@ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPop
353
360
  }
354
361
  });
355
362
 
356
- // Hide all action buttons (Add Dependency, Add Env Var, Remove Server) for this server if it's read-only.
363
+ // Hide action buttons (Add Dependency, Add Env Var, Remove Server) for this server if it's read-only,
364
+ // but ensure the "Duplicate Server" button remains visible.
357
365
  serverItem.querySelectorAll('.action-button-in-server').forEach(btn => {
366
+ if (!btn.classList.contains('duplicate-mcp-server-button')) {
367
+ // This is NOT the duplicate button, so hide it if the server is read-only.
358
368
  btn.style.display = 'none';
359
369
  btn.classList.add('hidden');
370
+ } else {
371
+ // This IS the duplicate button. Ensure it's visible.
372
+ // (It should be visible by default as it doesn't have conditional hide classes from the template)
373
+ btn.style.display = 'flex'; // Or its appropriate display style
374
+ btn.classList.remove('hidden');
375
+ btn.disabled = false; // Ensure it's enabled
376
+ }
360
377
  });
361
378
 
362
379
  // Expand server content and all key sections
@@ -385,11 +402,11 @@ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPop
385
402
  // Check if the adhoc span is already there from the template.
386
403
  // If not (e.g. server became adhoc after initial render and this is a re-evaluation), add it.
387
404
  if (!titleElement.querySelector('span.text-blue-600')) {
388
- titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
405
+ titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
389
406
  } else {
390
- // Ensure base title is correct if adhoc span is already there
391
- const adhocSpanHTML = titleElement.querySelector('span.text-blue-600').outerHTML;
392
- titleElement.innerHTML = `${baseTitle} ${adhocSpanHTML}`;
407
+ // Ensure base title is correct if adhoc span is already there
408
+ const adhocSpanHTML = titleElement.querySelector('span.text-blue-600').outerHTML;
409
+ titleElement.innerHTML = `${baseTitle} ${adhocSpanHTML}`;
393
410
  }
394
411
  } else {
395
412
  // Editable and not adhoc, just the base title.
@@ -399,19 +416,19 @@ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPop
399
416
  setTimeout(() => {
400
417
  // Enable main server fields
401
418
  serverItem.querySelectorAll('input, select, textarea').forEach(el => {
402
- if (!el.closest('.env-var-item') && !el.closest('.server-requirement-item')) {
403
- el.disabled = false;
404
- el.removeAttribute('readonly'); // Ensure readonly is also removed
405
- el.classList.remove('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
419
+ if (!el.closest('.env-var-item') && !el.closest('.server-requirement-item')) {
420
+ el.disabled = false;
421
+ el.removeAttribute('readonly'); // Ensure readonly is also removed
422
+ el.classList.remove('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
406
423
  }
407
424
  });
408
425
 
409
426
  // Show ALL action buttons for this server (Add Dependency, Add Env Var, Remove Server)
410
427
  // as the server is determined to be effectively editable.
411
428
  serverItem.querySelectorAll('.action-button-in-server').forEach(btn => {
412
- btn.style.display = 'flex'; // Or 'inline-flex' or whatever its default visible display is
413
- btn.disabled = false;
414
- btn.classList.remove('hidden', 'opacity-50', 'cursor-not-allowed');
429
+ btn.style.display = 'flex'; // Or 'inline-flex' or whatever its default visible display is
430
+ btn.disabled = false;
431
+ btn.classList.remove('hidden', 'opacity-50', 'cursor-not-allowed');
415
432
  });
416
433
  }, 0);
417
434
  }
@@ -832,6 +849,216 @@ export async function copyJsonToClipboard() {
832
849
  }
833
850
  }
834
851
 
852
+ /**
853
+ * Duplicates an MCP server item in the form.
854
+ * @param {number} serverIndexToDuplicate - The index of the server to duplicate.
855
+ * @param {string} serversListId - The ID of the servers list container (e.g., 'serversList', 'existingCategoryServersList').
856
+ */
857
+ export function duplicateServer(serverIndexToDuplicate, serversListId) {
858
+ const formId = serversListId === 'serversList' ? 'onboardForm' : 'onboardServerForm';
859
+ const formElement = document.getElementById(formId);
860
+ if (!formElement) {
861
+ showToast(`Form with ID ${formId} not found. Cannot duplicate server.`, 'error');
862
+ return;
863
+ }
864
+
865
+ // Determine if we are in the "Create Server in Existing Category" tab context
866
+ const isExistingCategoryContext = serversListId === 'existingCategoryServersList';
867
+ let baseCategoryDataForFormExtraction = null;
868
+ if (isExistingCategoryContext) {
869
+ // If in existing category context, getFormData might need the base category data
870
+ // This depends on how getFormData is structured to handle this tab.
871
+ // Assuming window.currentSelectedCategoryData holds the loaded category.
872
+ baseCategoryDataForFormExtraction = window.currentSelectedCategoryData || null;
873
+ }
874
+
875
+ const serverItemToDuplicate = formElement.querySelector(`.server-item[data-index="${serverIndexToDuplicate}"]`);
876
+ if (!serverItemToDuplicate) {
877
+ showToast('Could not find the server item to duplicate in the DOM.', 'error');
878
+ return;
879
+ }
880
+
881
+ // Track and temporarily enable ALL disabled fields in the form to ensure we get complete data
882
+ const disabledFields = [];
883
+ formElement.querySelectorAll('input, select, textarea').forEach(el => {
884
+ if (el.disabled) {
885
+ disabledFields.push({element: el, wasDisabled: true});
886
+ el.disabled = false;
887
+ }
888
+ });
889
+
890
+ // Get all current server data from the form
891
+ const currentFullFeedConfig = getFormData(formElement, isExistingCategoryContext, baseCategoryDataForFormExtraction);
892
+
893
+ // Restore original disabled state for all fields
894
+ disabledFields.forEach(({element, wasDisabled}) => {
895
+ element.disabled = wasDisabled;
896
+ });
897
+
898
+ if (!currentFullFeedConfig || !currentFullFeedConfig.mcpServers ||
899
+ currentFullFeedConfig.mcpServers.length <= serverIndexToDuplicate) {
900
+ showToast('Could not retrieve data for the server to duplicate after attempting to enable fields.', 'error');
901
+ return;
902
+ }
903
+
904
+ // 2. Get the specific server data to duplicate (deep copy)
905
+ let serverToDuplicateData = JSON.parse(JSON.stringify(currentFullFeedConfig.mcpServers[serverIndexToDuplicate]));
906
+
907
+ // 3. IMPORTANT: Remove systemTags from the duplicated data.
908
+ // The duplicated server should be treated as a new, fully editable server.
909
+ if (serverToDuplicateData.systemTags) {
910
+ delete serverToDuplicateData.systemTags;
911
+ }
912
+ // Also, ensure its name is distinct if needed, or clear it to force user input.
913
+ // For now, we'll copy the name but it will be editable.
914
+ // Consider adding a suffix like "-copy" to the name if automatic distinct names are desired.
915
+ // serverToDuplicateData.name = serverToDuplicateData.name ? `${serverToDuplicateData.name}-copy` : 'duplicated-server';
916
+
917
+
918
+ // 4. Determine if the general context for adding a new server is read-only
919
+ // This is primarily for the 'Edit Existing Category' tab.
920
+ // 'Create New Category' tab is never read-only in this sense.
921
+ let isContextGenerallyReadOnlyForAddServer = false;
922
+ if (isExistingCategoryContext) {
923
+ isContextGenerallyReadOnlyForAddServer = window.isExistingCategoryReadOnly || false;
924
+ }
925
+
926
+ // 5. Add a new server item. Pass `null` for serverData to ensure it's treated as a new item.
927
+ // The `isContextGenerallyReadOnlyForAddServer` helps `addServer` decide initial template state.
928
+ const newServerIndex = addServer(serversListId, isContextGenerallyReadOnlyForAddServer, null);
929
+
930
+ if (newServerIndex === -1) {
931
+ showToast('Failed to add a new server item for duplication.', 'error');
932
+ return;
933
+ }
934
+
935
+ // 6. Populate the new server item with the copied data.
936
+ // The new server should be fully editable.
937
+ // We need to call populateForm or a similar function for just this new server.
938
+ // populateForm expects a full FeedConfiguration. We'll construct a minimal one.
939
+ const tempFeedConfigForPopulation = {
940
+ mcpServers: [serverToDuplicateData] // Contains only the server data to populate
941
+ };
942
+
943
+ // Call populateForm, but ensure it targets the *new* server index and makes it editable.
944
+ // The populateForm function needs to be aware it's populating a *specific, new* server.
945
+ // The last `true` tells populateForm to make this specific server editable, overriding general read-only context.
946
+ // This might require adjustments in populateForm or a dedicated populateSingleServerForm function.
947
+ // For now, assuming populateForm can handle this if we pass a single server and target the new index.
948
+ // We pass `false` for `renderServersAsReadOnly` to ensure the duplicated server is editable.
949
+ // And we pass `true` for a hypothetical `forceEditableForSingleServer` if populateForm supported it.
950
+ // Let's simplify: populateForm will populate based on the data. addServer already set it up as editable.
951
+ // We need to ensure that `populateForm` correctly populates the server at `newServerIndex`.
952
+ // The `populateForm` function in formProcessor.js needs to be able to populate a *specific* server
953
+ // if we pass only one server in `mcpServers`.
954
+
955
+ // Get the newly added server item
956
+ const serversList = document.getElementById(serversListId);
957
+ const newServerItem = serversList.querySelector(`.server-item[data-index="${newServerIndex}"]`);
958
+
959
+ if (newServerItem) {
960
+ // Directly populate fields for the new server.
961
+ // This is a simplified version of what populateForm does for a single server.
962
+ // This avoids needing to modify populateForm extensively for this specific use case.
963
+ populateServerManually(newServerItem, newServerIndex, serverToDuplicateData, serversListId);
964
+ showToast(`Server #${serverIndexToDuplicate + 1} duplicated to Server #${newServerIndex + 1}.`, 'success');
965
+ } else {
966
+ showToast('Failed to find the new server item after duplication.', 'error');
967
+ }
968
+ }
969
+ window.duplicateServer = duplicateServer;
970
+
971
+ /**
972
+ * Manually populates a single server item's form fields with provided data.
973
+ * This is a helper for the duplicateServer functionality.
974
+ * @param {HTMLElement} serverItemElement - The HTML element of the server item.
975
+ * @param {number} serverIndex - The index of the server item.
976
+ * @param {object} serverData - The data object for the server.
977
+ * @param {string} serversListId - The ID of the servers list.
978
+ */
979
+ function populateServerManually(serverItemElement, serverIndex, serverData, serversListId) {
980
+ // Populate basic fields
981
+ serverItemElement.querySelector(`input[name="servers[${serverIndex}].name"]`).value = serverData.name || '';
982
+ serverItemElement.querySelector(`textarea[name="servers[${serverIndex}].description"]`).value = serverData.description || '';
983
+ serverItemElement.querySelector(`select[name="servers[${serverIndex}].mode"]`).value = serverData.mode || 'stdio';
984
+ serverItemElement.querySelector(`input[name="servers[${serverIndex}].schemas"]`).value = serverData.schemas || '';
985
+ serverItemElement.querySelector(`input[name="servers[${serverIndex}].repository"]`).value = serverData.repository || '';
986
+
987
+ // Trigger change on mode to render correct installation config
988
+ const modeSelect = serverItemElement.querySelector(`select[name="servers[${serverIndex}].mode"]`);
989
+ if (modeSelect) {
990
+ modeSelect.dispatchEvent(new Event('change')); // This will call renderInstallationConfig
991
+ }
992
+
993
+ // Populate installation config (command/args for stdio, url for sse)
994
+ // renderInstallationConfig should have been called by the modeSelect change event.
995
+ // Now, set the values based on serverData.installation
996
+ if (serverData.mode === 'stdio' && serverData.installation) {
997
+ const commandInput = serverItemElement.querySelector(`input[name="servers[${serverIndex}].installation.command"]`);
998
+ if (commandInput) commandInput.value = serverData.installation.command || '';
999
+ const argsInput = serverItemElement.querySelector(`input[name="servers[${serverIndex}].installation.args"]`);
1000
+ if (argsInput) argsInput.value = serverData.installation.args ? serverData.installation.args.join(', ') : '';
1001
+ } else if (serverData.mode === 'sse' && serverData.installation) {
1002
+ const urlInput = serverItemElement.querySelector(`input[name="servers[${serverIndex}].installation.url"]`);
1003
+ if (urlInput) urlInput.value = serverData.installation.url || '';
1004
+ }
1005
+
1006
+ // Populate environment variables
1007
+ const envVarsContainer = serverItemElement.querySelector(`#envVarsContainer_${serverIndex}`);
1008
+ if (envVarsContainer && serverData.installation && serverData.installation.env) {
1009
+ Object.entries(serverData.installation.env).forEach(([name, envConfig]) => {
1010
+ const envVarIndex = addEnvVariable(serverIndex, serversListId, false); // Add as editable
1011
+ const envVarItem = envVarsContainer.querySelector(`.env-var-item[data-env-index="${envVarIndex}"]`);
1012
+ if (envVarItem) {
1013
+ envVarItem.querySelector(`input[name="servers[${serverIndex}].installation.env[${envVarIndex}].name"]`).value = name;
1014
+ envVarItem.querySelector(`input[name="servers[${serverIndex}].installation.env[${envVarIndex}].default"]`).value = envConfig.Default || '';
1015
+ envVarItem.querySelector(`input[name="servers[${serverIndex}].installation.env[${envVarIndex}].required"]`).checked = envConfig.Required || false;
1016
+ envVarItem.querySelector(`textarea[name="servers[${serverIndex}].installation.env[${envVarIndex}].description"]`).value = envConfig.Description || '';
1017
+ }
1018
+ });
1019
+ }
1020
+
1021
+ // Populate server requirements (dependencies)
1022
+ const serverReqsContainer = serverItemElement.querySelector(`#server-requirements-list-${serverIndex}`);
1023
+ if (serverReqsContainer && serverData.dependencies && serverData.dependencies.requirements) {
1024
+ serverData.dependencies.requirements.forEach(req => {
1025
+ const reqIndex = addServerRequirement(serverIndex, serversListId, false); // Add as editable
1026
+ const reqItem = serverReqsContainer.querySelector(`.server-requirement-item[data-req-index="${reqIndex}"]`);
1027
+ if (reqItem) {
1028
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].name"]`).value = req.name || '';
1029
+ reqItem.querySelector(`select[name="servers[${serverIndex}].requirements[${reqIndex}].type"]`).value = req.type || '';
1030
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].version"]`).value = req.version || '';
1031
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].order"]`).value = req.order || '';
1032
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].alias"]`).value = req.alias || '';
1033
+
1034
+ const typeSelect = reqItem.querySelector(`select[name="servers[${serverIndex}].requirements[${reqIndex}].type"]`);
1035
+ if (typeSelect) typeSelect.dispatchEvent(new Event('change')); // To show/hide alias
1036
+
1037
+ const registryTypeSelect = reqItem.querySelector(`select[name="servers[${serverIndex}].requirements[${reqIndex}].registryType"]`);
1038
+ if (registryTypeSelect) {
1039
+ registryTypeSelect.value = req.registryType || 'public';
1040
+ registryTypeSelect.dispatchEvent(new Event('change')); // To show/hide specific registry configs
1041
+
1042
+ if (req.registry) {
1043
+ if (req.registryType === 'github' && req.registry.githubRelease) {
1044
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.githubRelease.repository"]`).value = req.registry.githubRelease.repository || '';
1045
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.githubRelease.assetsName"]`).value = req.registry.githubRelease.assetsName || '';
1046
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.githubRelease.assetName"]`).value = req.registry.githubRelease.assetName || '';
1047
+ } else if (req.registryType === 'artifacts' && req.registry.artifacts) {
1048
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.artifacts.registryName"]`).value = req.registry.artifacts.registryName || '';
1049
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.artifacts.registryUrl"]`).value = req.registry.artifacts.registryUrl || '';
1050
+ } else if (req.registryType === 'local' && req.registry.local) {
1051
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.local.localPath"]`).value = req.registry.local.localPath || '';
1052
+ reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.local.assetName"]`).value = req.registry.local.assetName || '';
1053
+ }
1054
+ }
1055
+ }
1056
+ }
1057
+ });
1058
+ }
1059
+ // Ensure the duplicated server is fully editable by calling setupReadOnlyState with false
1060
+ setupReadOnlyState(serverItemElement, false, null, serversListId, serverIndex);
1061
+ }
835
1062
  Object.entries({
836
1063
  addServer,
837
1064
  removeServer,
@@ -9,7 +9,7 @@ import { toggleSectionContent } from './uiHandlers.js'; // Assuming this is corr
9
9
  * @param {string} message - The validation message to display.
10
10
  * @param {boolean} isError - Whether this is an error message (true) or success message (false).
11
11
  */
12
- function showValidationMessage(element, message, isError = true) {
12
+ export function showValidationMessage(element, message, isError = true) {
13
13
  // Remove any existing validation message
14
14
  const existingMessage = element.nextElementSibling;
15
15
  if (existingMessage && existingMessage.classList.contains('validation-message')) {
@@ -34,6 +34,72 @@ function showValidationMessage(element, message, isError = true) {
34
34
  }
35
35
  }
36
36
 
37
+ /**
38
+ * Common form field validation used by both validate and publish handlers
39
+ * @param {HTMLFormElement} form - The form to validate
40
+ * @param {string} activeTab - The active tab ('create-category' or 'create-server')
41
+ * @returns {{isValid: boolean, errors: string[]}} Validation result with error messages
42
+ */
43
+ export function validateFormFields(form, activeTab) {
44
+ // Clear any existing validation messages
45
+ form.querySelectorAll('.validation-message').forEach(el => el.remove());
46
+
47
+ let hasErrors = false;
48
+ const errors = [];
49
+
50
+ // Check for empty required fields and show validation messages
51
+ const emptyRequiredFields = Array.from(form.querySelectorAll('[required]'));
52
+ emptyRequiredFields.forEach(field => {
53
+ if (!field.value.trim()) {
54
+ showValidationMessage(field, 'This field is required', true);
55
+ hasErrors = true;
56
+ errors.push(`${field.name || 'Field'} is required`);
57
+ }
58
+ });
59
+
60
+ // Check category name format if this is the create category tab
61
+ if (activeTab === 'create-category') {
62
+ const nameInput = form.querySelector('input[name="name"]');
63
+ if (nameInput && nameInput.value.trim()) {
64
+ if (!/^[a-zA-Z0-9-_]+$/.test(nameInput.value.trim())) {
65
+ showValidationMessage(nameInput, 'Only alphanumeric characters, hyphens, and underscores allowed', true);
66
+ hasErrors = true;
67
+ errors.push('Category name must only contain alphanumeric characters, hyphens, and underscores');
68
+ }
69
+ }
70
+ }
71
+
72
+ // Check for other validation errors
73
+ const envNameFields = form.querySelectorAll('input[name$=".env\\[\\].name"]');
74
+ envNameFields.forEach(field => {
75
+ const value = field.value.trim();
76
+ if (value && !/^[A-Z_][A-Z0-9_]*$/.test(value)) {
77
+ showValidationMessage(field, 'Must be uppercase with only letters, numbers, and underscores', true);
78
+ hasErrors = true;
79
+ errors.push('Environment variable names must be uppercase with only letters, numbers, and underscores');
80
+ }
81
+ });
82
+
83
+ const urlFields = form.querySelectorAll('input[name$=".url"]');
84
+ urlFields.forEach(field => {
85
+ const value = field.value.trim();
86
+ if (value) {
87
+ try {
88
+ new URL(value);
89
+ } catch (e) {
90
+ showValidationMessage(field, 'Invalid URL format', true);
91
+ hasErrors = true;
92
+ errors.push('Invalid URL format');
93
+ }
94
+ }
95
+ });
96
+
97
+ return {
98
+ isValid: !hasErrors,
99
+ errors
100
+ };
101
+ }
102
+
37
103
  const POLLING_INTERVAL = 3000; // 3 seconds
38
104
  let pollingIntervalId = null;
39
105
 
@@ -278,45 +344,11 @@ export async function handleValidation(event, activeTab, currentSelectedCategory
278
344
  return;
279
345
  }
280
346
 
281
- // First clear any existing validation messages
282
- onboardForm.querySelectorAll('.validation-message').forEach(el => el.remove());
283
-
284
- // Check for empty required fields and show validation messages
285
- const emptyRequiredFields = Array.from(onboardForm.querySelectorAll('[required]'));
286
- let hasErrors = false;
287
-
288
- emptyRequiredFields.forEach(field => {
289
- if (!field.value.trim()) {
290
- showValidationMessage(field, 'This field is required', true);
291
- hasErrors = true;
292
- }
293
- });
294
-
295
- // Check for other validation errors
296
- const envNameFields = onboardForm.querySelectorAll('input[name$=".env\\[\\].name"]');
297
- envNameFields.forEach(field => {
298
- const value = field.value.trim();
299
- if (value && !/^[A-Z_][A-Z0-9_]*$/.test(value)) {
300
- showValidationMessage(field, 'Must be uppercase with only letters, numbers, and underscores', true);
301
- hasErrors = true;
302
- }
303
- });
304
-
305
- const urlFields = onboardForm.querySelectorAll('input[name$=".url"]');
306
- urlFields.forEach(field => {
307
- const value = field.value.trim();
308
- if (value) {
309
- try {
310
- new URL(value);
311
- } catch (e) {
312
- showValidationMessage(field, 'Invalid URL format', true);
313
- hasErrors = true;
314
- }
315
- }
316
- });
317
-
318
- if (hasErrors) {
347
+ const validationResult = validateFormFields(onboardForm, activeTab);
348
+ if (!validationResult.isValid) {
319
349
  showToast('Please fix all validation errors before proceeding.', 'error');
350
+ validateButton.disabled = false;
351
+ validateButton.innerHTML = "<i class='bx bx-check-shield mr-2'></i>Validate";
320
352
  return;
321
353
  }
322
354
 
@@ -3,6 +3,9 @@ import { showServerDetails } from './serverCategoryDetails.js'; // Still needed
3
3
  import { showToast } from './notifications.js';
4
4
  import { buildUrlWithFlights } from './flights/flights.js';
5
5
 
6
+ // Store state for categories
7
+ let pinnedCategories = {};
8
+
6
9
  // Wait for data to be loaded
7
10
  async function waitForData() {
8
11
  if (allServerCategoriesData && allServerCategoriesData.length > 0) {
@@ -12,6 +15,54 @@ async function waitForData() {
12
15
  return allServerCategoriesData && allServerCategoriesData.length > 0;
13
16
  }
14
17
 
18
+ // Load pinned state from localStorage
19
+ function loadPinnedState() {
20
+ const saved = localStorage.getItem('pinnedCategories');
21
+ if (saved) {
22
+ try {
23
+ pinnedCategories = JSON.parse(saved);
24
+ } catch (e) {
25
+ console.error('Error parsing pinned categories from localStorage:', e);
26
+ pinnedCategories = {};
27
+ }
28
+ }
29
+ }
30
+
31
+ // Save pinned state to localStorage
32
+ function savePinnedState() {
33
+ localStorage.setItem('pinnedCategories', JSON.stringify(pinnedCategories));
34
+ }
35
+
36
+ // Toggle pin status for a category
37
+ function togglePinCategory(categoryName, event) {
38
+ // Stop propagation to prevent navigation
39
+ event.stopPropagation();
40
+
41
+ // Toggle pin status
42
+ if (pinnedCategories[categoryName]) {
43
+ delete pinnedCategories[categoryName];
44
+ } else {
45
+ pinnedCategories[categoryName] = true;
46
+ }
47
+
48
+ // Save to localStorage
49
+ savePinnedState();
50
+
51
+ // Find the server item element and add a temporary animation class
52
+ const serverItem = event.target.closest('.server-item');
53
+ if (serverItem) {
54
+ serverItem.classList.add('pin-animation');
55
+ setTimeout(() => {
56
+ serverItem.classList.remove('pin-animation');
57
+ }, 300); // Animation duration
58
+ }
59
+
60
+ // Re-render list with updated pin status
61
+ if (allServerCategoriesData && allServerCategoriesData.length > 0) {
62
+ renderServerCategoryList(allServerCategoriesData);
63
+ }
64
+ }
65
+
15
66
  // Function to show the last selected category on page load
16
67
  async function loadLastSelectedCategory() {
17
68
  const lastSelected = localStorage.getItem('lastSelectedCategory');
@@ -33,8 +84,24 @@ function renderServerCategoryList(servers) {
33
84
  document.getElementById('serverCategoryDetails').innerHTML = '<p>Select a server from the list to see details.</p>'; // Clear details
34
85
  return;
35
86
  }
87
+
88
+ // Load pinned state
89
+ loadPinnedState();
90
+
91
+ // Create a copy of the servers array to avoid modifying the original
92
+ const sortedServers = [...servers];
93
+
94
+ // Sort servers with pinned ones at the top
95
+ sortedServers.sort((a, b) => {
96
+ const isPinnedA = pinnedCategories[a.name] === true;
97
+ const isPinnedB = pinnedCategories[b.name] === true;
98
+
99
+ if (isPinnedA && !isPinnedB) return -1;
100
+ if (!isPinnedA && isPinnedB) return 1;
101
+ return 0; // Keep original order for items with same pin status
102
+ });
36
103
 
37
- serverCategoryList.innerHTML = servers.map(server => {
104
+ serverCategoryList.innerHTML = sortedServers.map(server => {
38
105
  let statusHtml = '';
39
106
 
40
107
  // Add tool status summary if available
@@ -80,13 +147,27 @@ function renderServerCategoryList(servers) {
80
147
  }
81
148
  systemTagsHtml += '</div>';
82
149
  }
150
+ // Check if this server is pinned
151
+ const isPinned = pinnedCategories[server.name] === true;
152
+ const pinnedClass = isPinned ? 'pinned' : '';
153
+
154
+ // Pin/unpin button with appropriate icon
155
+ const pinIcon = isPinned
156
+ ? '<i class="bx bxs-pin"></i>' // Solid pin icon for pinned items
157
+ : '<i class="bx bx-pin"></i>'; // Outline pin icon for unpinned items
83
158
 
84
159
  return `
85
- <div class="server-item border border-gray-200 p-3 rounded hover:bg-gray-50 cursor-pointer transition duration-150 ease-in-out"
86
- onclick="navigateToCategory('${server.name}')"
160
+ <div class="server-item border border-gray-200 p-3 rounded hover:bg-gray-50 cursor-pointer transition duration-150 ease-in-out ${pinnedClass}"
87
161
  data-server-name="${server.name}">
88
- <h3 class="font-semibold text-gray-800">${server.displayName || server.name}</h3>
89
- <div class="text-sm text-gray-500 flex items-center mt-1">
162
+ <div class="flex justify-between items-center">
163
+ <h3 class="font-semibold text-gray-800" onclick="navigateToCategory('${server.name}')">${server.displayName || server.name}</h3>
164
+ <div class="flex items-center">
165
+ <div class="pin-button ${pinnedClass}" onclick="togglePinCategoryItem('${server.name}', event)" title="${isPinned ? 'Unpin' : 'Pin'} this category">
166
+ ${pinIcon}
167
+ </div>
168
+ </div>
169
+ </div>
170
+ <div class="text-sm text-gray-500 flex items-center mt-1" onclick="navigateToCategory('${server.name}')">
90
171
  ${statusHtml}
91
172
  ${systemTagsHtml}
92
173
  </div>
@@ -101,6 +182,9 @@ function renderServerCategoryList(servers) {
101
182
  // Setup search functionality
102
183
  function setupSearch() {
103
184
  const searchBox = document.getElementById('searchBox');
185
+
186
+ // Load pinned state on page load
187
+ loadPinnedState();
104
188
 
105
189
  searchBox.addEventListener('input', async function () {
106
190
  const searchTerm = this.value.toLowerCase();
@@ -151,7 +235,7 @@ function navigateToCategory(categoryName) {
151
235
  // Alternatively, attach event listeners dynamically after rendering the list.
152
236
  // For simplicity with current structure, we'll make it global.
153
237
  window.navigateToCategory = navigateToCategory;
154
-
238
+ window.togglePinCategoryItem = togglePinCategory;
155
239
 
156
240
  // Export functions
157
- export { renderServerCategoryList, setupSearch, loadLastSelectedCategory };
241
+ export { renderServerCategoryList, setupSearch, loadLastSelectedCategory, togglePinCategory };
@@ -106,8 +106,8 @@
106
106
  placeholder="e.g., Coder Tools">
107
107
  </div>
108
108
  <div class="md:col-span-2">
109
- <label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
110
- <textarea name="description" rows="3"
109
+ <label class="block text-sm font-medium text-gray-700 mb-1">Description*</label>
110
+ <textarea name="description" rows="3" required
111
111
  class="w-full px-3 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
112
112
  placeholder="Describe the purpose and capabilities of this server category"></textarea>
113
113
  </div>