imcp 0.0.19 → 0.1.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 (183) hide show
  1. package/.roo/rules-code/rules.md +88 -0
  2. package/dist/cli/index.js +1 -45
  3. package/dist/core/installers/clients/BaseClientInstaller.d.ts +1 -5
  4. package/dist/core/installers/clients/BaseClientInstaller.js +40 -38
  5. package/dist/core/installers/clients/ClientInstaller.d.ts +9 -9
  6. package/dist/core/installers/clients/ClientInstaller.js +105 -99
  7. package/dist/core/installers/requirements/BaseInstaller.d.ts +9 -1
  8. package/dist/core/installers/requirements/CommandInstaller.d.ts +9 -1
  9. package/dist/core/installers/requirements/CommandInstaller.js +46 -12
  10. package/dist/core/installers/requirements/GeneralInstaller.d.ts +11 -1
  11. package/dist/core/installers/requirements/GeneralInstaller.js +46 -10
  12. package/dist/core/installers/requirements/InstallerFactory.d.ts +3 -1
  13. package/dist/core/installers/requirements/InstallerFactory.js +3 -2
  14. package/dist/core/installers/requirements/NpmInstaller.d.ts +4 -2
  15. package/dist/core/installers/requirements/NpmInstaller.js +38 -22
  16. package/dist/core/installers/requirements/PipInstaller.d.ts +3 -1
  17. package/dist/core/installers/requirements/PipInstaller.js +58 -36
  18. package/dist/core/installers/requirements/RequirementInstaller.d.ts +4 -1
  19. package/dist/core/loaders/InstallOperationManager.d.ts +115 -0
  20. package/dist/core/loaders/InstallOperationManager.js +311 -0
  21. package/dist/core/loaders/SystemSettingsManager.d.ts +54 -0
  22. package/dist/core/loaders/SystemSettingsManager.js +257 -0
  23. package/dist/core/metadatas/constants.d.ts +7 -0
  24. package/dist/core/metadatas/constants.js +7 -0
  25. package/dist/core/metadatas/recordingConstants.d.ts +44 -0
  26. package/dist/core/metadatas/recordingConstants.js +45 -0
  27. package/dist/core/metadatas/types.d.ts +21 -0
  28. package/dist/core/onboard/FeedOnboardService.d.ts +7 -3
  29. package/dist/core/onboard/FeedOnboardService.js +52 -5
  30. package/dist/core/onboard/InstallOperationManager.d.ts +23 -0
  31. package/dist/core/onboard/InstallOperationManager.js +144 -0
  32. package/dist/core/onboard/OnboardStatusManager.js +2 -1
  33. package/dist/core/validators/StdioServerValidator.js +4 -3
  34. package/dist/services/InstallationService.d.ts +2 -37
  35. package/dist/services/InstallationService.js +45 -313
  36. package/dist/services/MCPManager.d.ts +1 -1
  37. package/dist/services/MCPManager.js +53 -47
  38. package/dist/services/RequirementService.d.ts +85 -12
  39. package/dist/services/RequirementService.js +488 -49
  40. package/dist/services/ServerService.d.ts +0 -6
  41. package/dist/services/ServerService.js +0 -74
  42. package/dist/services/TelemetryService.d.ts +15 -0
  43. package/dist/services/TelemetryService.js +54 -0
  44. package/dist/utils/adoUtils.js +6 -3
  45. package/dist/utils/githubAuth.js +65 -0
  46. package/dist/utils/logger.d.ts +16 -0
  47. package/dist/utils/logger.js +78 -1
  48. package/dist/utils/macroExpressionUtils.js +3 -25
  49. package/dist/utils/osUtils.d.ts +22 -1
  50. package/dist/utils/osUtils.js +92 -1
  51. package/dist/utils/versionUtils.d.ts +20 -0
  52. package/dist/utils/versionUtils.js +76 -0
  53. package/dist/web/public/css/modal.css +292 -1
  54. package/dist/web/public/css/serverCategoryList.css +120 -0
  55. package/dist/web/public/css/serverDetails.css +14 -1
  56. package/dist/web/public/index.html +126 -21
  57. package/dist/web/public/js/flights/flights.js +1 -1
  58. package/dist/web/public/js/modal/index.js +8 -14
  59. package/dist/web/public/js/modal/installModal.js +3 -4
  60. package/dist/web/public/js/modal/installation.js +122 -137
  61. package/dist/web/public/js/modal/loadingModal.js +155 -25
  62. package/dist/web/public/js/modal/messageQueue.js +45 -101
  63. package/dist/web/public/js/modal/modalSetup.js +125 -43
  64. package/dist/web/public/js/modal/modalUtils.js +0 -12
  65. package/dist/web/public/js/modal.js +23 -10
  66. package/dist/web/public/js/onboard/formProcessor.js +18 -11
  67. package/dist/web/public/js/onboard/publishHandler.js +35 -3
  68. package/dist/web/public/js/onboard/templates.js +5 -1
  69. package/dist/web/public/js/onboard/uiHandlers.js +266 -39
  70. package/dist/web/public/js/onboard/validationHandlers.js +71 -39
  71. package/dist/web/public/js/serverCategoryDetails.js +60 -11
  72. package/dist/web/public/js/serverCategoryList.js +93 -9
  73. package/dist/web/public/js/settings.js +314 -0
  74. package/dist/web/public/onboard.html +2 -2
  75. package/dist/web/public/settings.html +135 -0
  76. package/dist/web/public/styles.css +32 -0
  77. package/dist/web/server.js +93 -1
  78. package/{src/web/public/js/onboard → docs}/ONBOARDING_PAGE_DESIGN.md +15 -125
  79. package/docs/Telemetry.md +136 -0
  80. package/memory-bank/activeContext.md +26 -0
  81. package/memory-bank/decisionLog.md +91 -0
  82. package/memory-bank/productContext.md +41 -0
  83. package/memory-bank/progress.md +35 -0
  84. package/memory-bank/systemPatterns.md +10 -0
  85. package/package.json +2 -1
  86. package/src/cli/index.ts +1 -48
  87. package/src/core/installers/clients/BaseClientInstaller.ts +64 -50
  88. package/src/core/installers/clients/ClientInstaller.ts +130 -130
  89. package/src/core/installers/requirements/BaseInstaller.ts +9 -1
  90. package/src/core/installers/requirements/CommandInstaller.ts +47 -13
  91. package/src/core/installers/requirements/GeneralInstaller.ts +48 -10
  92. package/src/core/installers/requirements/InstallerFactory.ts +4 -3
  93. package/src/core/installers/requirements/NpmInstaller.ts +90 -68
  94. package/src/core/installers/requirements/PipInstaller.ts +81 -55
  95. package/src/core/installers/requirements/RequirementInstaller.ts +4 -3
  96. package/src/core/loaders/InstallOperationManager.ts +367 -0
  97. package/src/core/loaders/SystemSettingsManager.ts +278 -0
  98. package/src/core/metadatas/constants.ts +9 -0
  99. package/src/core/metadatas/recordingConstants.ts +62 -0
  100. package/src/core/metadatas/types.ts +23 -0
  101. package/src/core/onboard/FeedOnboardService.ts +59 -5
  102. package/src/core/onboard/OnboardStatusManager.ts +2 -1
  103. package/src/core/validators/StdioServerValidator.ts +4 -3
  104. package/src/services/InstallationService.ts +54 -399
  105. package/src/services/MCPManager.ts +61 -64
  106. package/src/services/RequirementService.ts +564 -67
  107. package/src/services/ServerService.ts +0 -90
  108. package/src/services/TelemetryService.ts +59 -0
  109. package/src/utils/adoUtils.ts +6 -4
  110. package/src/utils/githubAuth.ts +84 -1
  111. package/src/utils/logger.ts +83 -1
  112. package/src/utils/macroExpressionUtils.ts +4 -21
  113. package/src/utils/osUtils.ts +92 -1
  114. package/src/utils/versionUtils.ts +98 -13
  115. package/src/web/public/css/modal.css +292 -1
  116. package/src/web/public/css/serverCategoryList.css +120 -0
  117. package/src/web/public/css/serverDetails.css +14 -1
  118. package/src/web/public/index.html +126 -21
  119. package/src/web/public/js/flights/flights.js +1 -1
  120. package/src/web/public/js/modal/index.js +8 -14
  121. package/src/web/public/js/modal/installModal.js +3 -4
  122. package/src/web/public/js/modal/installation.js +122 -137
  123. package/src/web/public/js/modal/loadingModal.js +155 -25
  124. package/src/web/public/js/modal/modalSetup.js +125 -43
  125. package/src/web/public/js/modal/modalUtils.js +0 -12
  126. package/src/web/public/js/modal.js +23 -10
  127. package/src/web/public/js/onboard/formProcessor.js +18 -11
  128. package/src/web/public/js/onboard/publishHandler.js +35 -3
  129. package/src/web/public/js/onboard/templates.js +5 -1
  130. package/src/web/public/js/onboard/uiHandlers.js +266 -39
  131. package/src/web/public/js/onboard/validationHandlers.js +71 -39
  132. package/src/web/public/js/serverCategoryDetails.js +60 -11
  133. package/src/web/public/js/serverCategoryList.js +93 -9
  134. package/src/web/public/js/settings.js +314 -0
  135. package/src/web/public/onboard.html +2 -2
  136. package/src/web/public/settings.html +135 -0
  137. package/src/web/public/styles.css +32 -0
  138. package/src/web/server.ts +96 -1
  139. package/dist/cli/commands/start.d.ts +0 -2
  140. package/dist/cli/commands/start.js +0 -32
  141. package/dist/cli/commands/sync.d.ts +0 -2
  142. package/dist/cli/commands/sync.js +0 -17
  143. package/dist/core/ConfigurationLoader.d.ts +0 -32
  144. package/dist/core/ConfigurationLoader.js +0 -236
  145. package/dist/core/ConfigurationProvider.d.ts +0 -35
  146. package/dist/core/ConfigurationProvider.js +0 -375
  147. package/dist/core/InstallationService.d.ts +0 -50
  148. package/dist/core/InstallationService.js +0 -350
  149. package/dist/core/MCPManager.d.ts +0 -28
  150. package/dist/core/MCPManager.js +0 -188
  151. package/dist/core/RequirementService.d.ts +0 -40
  152. package/dist/core/RequirementService.js +0 -110
  153. package/dist/core/ServerSchemaLoader.d.ts +0 -11
  154. package/dist/core/ServerSchemaLoader.js +0 -43
  155. package/dist/core/ServerSchemaProvider.d.ts +0 -17
  156. package/dist/core/ServerSchemaProvider.js +0 -120
  157. package/dist/core/constants.d.ts +0 -47
  158. package/dist/core/constants.js +0 -94
  159. package/dist/core/installers/BaseInstaller.d.ts +0 -74
  160. package/dist/core/installers/BaseInstaller.js +0 -253
  161. package/dist/core/installers/ClientInstaller.d.ts +0 -23
  162. package/dist/core/installers/ClientInstaller.js +0 -564
  163. package/dist/core/installers/CommandInstaller.d.ts +0 -37
  164. package/dist/core/installers/CommandInstaller.js +0 -173
  165. package/dist/core/installers/GeneralInstaller.d.ts +0 -33
  166. package/dist/core/installers/GeneralInstaller.js +0 -85
  167. package/dist/core/installers/InstallerFactory.d.ts +0 -54
  168. package/dist/core/installers/InstallerFactory.js +0 -97
  169. package/dist/core/installers/NpmInstaller.d.ts +0 -26
  170. package/dist/core/installers/NpmInstaller.js +0 -127
  171. package/dist/core/installers/PipInstaller.d.ts +0 -28
  172. package/dist/core/installers/PipInstaller.js +0 -127
  173. package/dist/core/installers/RequirementInstaller.d.ts +0 -33
  174. package/dist/core/installers/RequirementInstaller.js +0 -3
  175. package/dist/core/types.d.ts +0 -166
  176. package/dist/core/types.js +0 -16
  177. package/dist/services/InstallRequestValidator.d.ts +0 -21
  178. package/dist/services/InstallRequestValidator.js +0 -99
  179. package/dist/web/public/js/modal/installHandler.js +0 -227
  180. package/dist/web/public/js/modal/loadingUI.js +0 -74
  181. package/dist/web/public/js/modal/modalUI.js +0 -214
  182. package/dist/web/public/js/modal/version.js +0 -20
  183. package/src/web/public/js/modal/messageQueue.js +0 -112
@@ -1,16 +1,33 @@
1
- import { RequirementConfig, RequirementStatus, ServerInstallOptions } from '../core/metadatas/types.js';
2
- import { createInstallerFactory } from '../core/installers/index.js';
1
+ import {
2
+ RequirementConfig,
3
+ RequirementStatus,
4
+ ServerInstallOptions,
5
+ OperationStatus,
6
+ FeedConfiguration,
7
+ McpConfig,
8
+ ServerOperationResult,
9
+ MCPServerCategory
10
+ } from '../core/metadatas/types.js';
11
+ import { createInstallerFactory, InstallerFactory } from '../core/installers/index.js';
3
12
  import { exec } from 'child_process';
4
13
  import util from 'util';
5
-
14
+ import { configProvider, ConfigurationProvider } from '../core/loaders/ConfigurationProvider.js';
15
+ import { Logger } from '../utils/logger.js';
16
+ import { updateCheckTracker } from '../utils/UpdateCheckTracker.js';
17
+ import { InstallOperationManager } from '../core/loaders/InstallOperationManager.js';
18
+ import { UPDATE_CHECK_INTERVAL_MS } from '../core/metadatas/constants.js';
19
+ import * as RecordingConstants from '../core/metadatas/recordingConstants.js';
20
+ import { systemSettingsManager } from '../core/loaders/SystemSettingsManager.js';
6
21
  /**
7
22
  * Service responsible for managing requirements installation and status
8
23
  */
9
24
  export class RequirementService {
10
25
  private static instance: RequirementService;
11
- private installerFactory = createInstallerFactory(util.promisify(exec));
26
+ private installerFactory: InstallerFactory;
12
27
 
13
- private constructor() { }
28
+ private constructor() {
29
+ this.installerFactory = createInstallerFactory(util.promisify(exec));
30
+ }
14
31
 
15
32
  /**
16
33
  * Get the singleton instance of RequirementService
@@ -23,107 +40,587 @@ export class RequirementService {
23
40
  return RequirementService.instance;
24
41
  }
25
42
 
43
+ private generateOperationId(): string {
44
+ return `install-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
45
+ }
46
+
47
+ /**
48
+ * Checks and installs requirements for a server if needed.
49
+ * @param categoryName The category name.
50
+ * @param serverName The server name.
51
+ * @param options The installation options.
52
+ * @returns A failure result if requirements check fails, null if requirements are satisfied or installation started in background.
53
+ */
54
+ public async checkAndInstallRequirements(categoryName: string, serverName: string, options: ServerInstallOptions): Promise<ServerOperationResult | null> {
55
+ const recoder = InstallOperationManager.getInstance(categoryName, serverName);
56
+
57
+ return recoder.recording(async () => {
58
+ const configProvider = ConfigurationProvider.getInstance();
59
+ const feedConfig = await configProvider.getFeedConfiguration(categoryName);
60
+
61
+ if (!feedConfig) {
62
+ const errorMsg = 'Feed configuration not found';
63
+ // No need to record step here, recoder.recording will handle failure
64
+ return {
65
+ success: false, message: errorMsg,
66
+ status: [{ status: 'failed', type: 'install', target: 'server', message: errorMsg }]
67
+ };
68
+ }
69
+
70
+
71
+ const serverConfig = feedConfig.mcpServers.find((s: McpConfig) => s.name === serverName);
72
+ if (!serverConfig?.dependencies?.requirements || serverConfig.dependencies.requirements.length === 0) {
73
+ const msg = `No requirements for ${serverName} in category ${categoryName}`;
74
+ Logger.debug(msg);
75
+ await recoder.recordStep(RecordingConstants.STEP_CHECKING_REQUIREMENT_STATUS, 'completed', msg);
76
+ return null;
77
+ }
78
+
79
+ const ready = await recoder.recording(
80
+ async () => {
81
+ await recoder.recordStep(RecordingConstants.STEP_CHECKING_REQUIREMENT_STATUS, 'in-progress', 'Checking status of all requirements.');
82
+ const requirementStatuses = await Promise.all(
83
+ serverConfig.dependencies!.requirements!.map(async (req) => {
84
+ const reqConfig = feedConfig.requirements?.find((r: RequirementConfig) => r.name === req.name) || {
85
+ name: req.name, version: req.version, type: 'npm' // Default to npm if not fully defined
86
+ };
87
+ return await this.installerFactory.checkInstallation(reqConfig, options);
88
+ })
89
+ );
90
+
91
+ const requirementsReady = await configProvider.isRequirementsReady(categoryName, serverName);
92
+ for (const status of requirementStatuses) {
93
+ if (status.installed && !requirementsReady) {
94
+ await configProvider.updateRequirementStatus(categoryName, status.name, { ...status, installed: true });
95
+ } else if (!status.installed && requirementsReady) {
96
+ await configProvider.updateRequirementStatus(categoryName, status.name, { ...status, installed: false });
97
+ }
98
+ }
99
+
100
+ return requirementStatuses.every(status => status.installed);
101
+ },
102
+ {
103
+ stepName: RecordingConstants.STEP_CHECKING_REQUIREMENT_STATUS,
104
+ inProgressMessage: `Checking status of all requirements for ${serverName}`,
105
+ endMessage: (ready) => ready ? `Requirements for ${serverName} in ${categoryName} are already installed.` : 'Requirements check completed. Some requirements need installation.',
106
+ }
107
+ )
108
+
109
+ // If requirements are already installed, return null
110
+ if (ready) return null;
111
+
112
+ const sortedRequirements = [...serverConfig.dependencies.requirements].sort((a, b) => {
113
+ const orderA = a.order ?? Infinity;
114
+ const orderB = b.order ?? Infinity;
115
+ return orderA - orderB;
116
+ });
117
+
118
+ // Use recordingAsync for the background task
119
+ recoder.recordingAsync(
120
+ () => this.installRequirementsInBackground(categoryName, serverName, feedConfig, sortedRequirements, options, recoder),
121
+ {
122
+ stepName: RecordingConstants.STEP_INSTALLING_REQUIREMENTS_IN_BACKGROUND,
123
+ inProgressMessage: 'Starting background installation of requirements.',
124
+ endMessage: () => 'Background installation of requirements completed.',
125
+ onError: (err) => {
126
+ const errorMsg = `Error in background requirement installations for ${serverName} in ${categoryName}: ${err instanceof Error ? err.message : String(err)}`;
127
+ Logger.error(errorMsg);
128
+ return errorMsg; // This message will be recorded for the 'failed' step
129
+ }
130
+ }
131
+ );
132
+ // The main operation returns null as installation is happening in background
133
+ return null;
134
+ }, {
135
+ stepName: RecordingConstants.STEP_CHECK_AND_INSTALL_REQUIREMENTS,
136
+ inProgressMessage: `Checking and installing requirements for ${serverName}`,
137
+ endMessage: (result) => result?.success === false ? `Requirement check/install failed: ${result.message}` : `Requirement check and install process for ${serverName} completed.`,
138
+ onResult: (result) => result === null || result.success !== false // Success if null (bg install) or result.success is not false
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Installs requirements in background without blocking the main thread.
144
+ * Requirements with the same order are installed in parallel.
145
+ * @param categoryName The category name.
146
+ * @param feedConfig The feed configuration.
147
+ * @param sortedRequirements Array of requirements sorted by order.
148
+ * @param options The installation options.
149
+ */
150
+ private async installRequirementsInBackground(
151
+ categoryName: string,
152
+ serverName: string,
153
+ feedConfig: FeedConfiguration,
154
+ sortedRequirements: Array<{ name: string; version: string; order?: number }>,
155
+ options: ServerInstallOptions,
156
+ recoder: InstallOperationManager // Added recoder parameter
157
+ ): Promise<void> {
158
+ const configProvider = ConfigurationProvider.getInstance();
159
+ type RequirementType = { name: string; version: string; order?: number };
160
+ const requirementGroups = sortedRequirements.reduce<Record<number, RequirementType[]>>((groups, req) => {
161
+ const order = req.order ?? Infinity;
162
+ if (!groups[order]) {
163
+ groups[order] = [];
164
+ }
165
+ groups[order].push(req);
166
+ return groups;
167
+ }, {});
168
+
169
+ const orderKeys = Object.keys(requirementGroups).map(Number).sort((a, b) => a - b);
170
+ for (const order of orderKeys) {
171
+ const group = requirementGroups[order];
172
+ await Promise.all(group.map(async requirement => {
173
+ const stepName = `InstallRequirement_${requirement.name}`;
174
+ return recoder.recording(async () => {
175
+ let requirementConfig = feedConfig.requirements?.find((r: RequirementConfig) => r.name === requirement.name);
176
+ if (!requirementConfig) {
177
+ requirementConfig = { name: requirement.name, version: requirement.version, type: 'npm' }; // Default
178
+ const warnMsg = `Requirement ${requirement.name} not found in feed's requirements definitions for category ${categoryName}. Using default config.`;
179
+ Logger.warn(warnMsg);
180
+ // This case is a "completion" of this specific step with a warning, not a failure.
181
+ // The recording will mark it completed, and the message will be this warning.
182
+ return { installed: true, message: warnMsg, type: requirementConfig.type, name: requirement.name, version: requirement.version };
183
+ }
184
+
185
+ let currentOptions = { ...options };
186
+ const currentStatus = await configProvider.getRequirementStatus(categoryName, requirement.name);
187
+ if (requirementConfig.type === 'pip' && currentStatus?.pythonEnv && !currentOptions?.settings?.pythonEnv) {
188
+ currentOptions = {
189
+ ...currentOptions,
190
+ settings: { ...currentOptions?.settings, pythonEnv: currentStatus.pythonEnv }
191
+ };
192
+ }
193
+
194
+ const installer = this.installerFactory.getInstaller(requirementConfig);
195
+ if (!installer) {
196
+ const errorMsg = `No installer found for requirement type: ${requirementConfig.type}`;
197
+ await this.updateRequirementFailureStatus(categoryName, requirement.name, requirementConfig.type, errorMsg);
198
+ throw new Error(errorMsg); // Caught by recoder.recording
199
+ }
200
+
201
+ const operationId = this.generateOperationId();
202
+ await this.updateRequirementProgressStatus(categoryName, requirement.name, requirementConfig.type, operationId);
203
+
204
+ const installStatus = await installer.install(requirementConfig, recoder, currentOptions);
205
+ const success = installStatus.installed;
206
+ const message = success ? `Requirement ${requirement.name} installed successfully` : `Failed to install ${requirement.name}: ${installStatus.error || 'Unknown error'}`;
207
+
208
+ await this.updateRequirementCompletionStatus(categoryName, requirement.name, installStatus, operationId);
209
+ if (!success) {
210
+ throw new Error(message); // Caught by recoder.recording
211
+ }
212
+ return { ...installStatus, message };
213
+ }, {
214
+ stepName: stepName,
215
+ inProgressMessage: `Starting installation of ${requirement.name}`,
216
+ onResult: (status) => status.installed,
217
+ endMessage: (status) => status.message // Use the message from the installStatus
218
+ });
219
+ }));
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Helper to update requirement status for failure case.
225
+ * @param categoryName The category name.
226
+ * @param requirementName The name of the requirement.
227
+ * @param requirementType The type of the requirement.
228
+ * @param errorMessage The error message.
229
+ */
230
+ private async updateRequirementFailureStatus(
231
+ categoryName: string, requirementName: string, requirementType: string, errorMessage: string
232
+ ): Promise<void> {
233
+ const configProvider = ConfigurationProvider.getInstance();
234
+ await configProvider.updateRequirementStatus(categoryName, requirementName, {
235
+ name: requirementName, type: requirementType, installed: false, error: errorMessage,
236
+ operationStatus: {
237
+ status: 'failed', type: 'install', target: 'requirement',
238
+ message: `Error installing requirement: ${errorMessage}`,
239
+ operationId: this.generateOperationId()
240
+ }
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Helper to update requirement status for in-progress case.
246
+ * @param categoryName The category name.
247
+ * @param requirementName The name of the requirement.
248
+ * @param requirementType The type of the requirement.
249
+ * @param operationId The operation ID.
250
+ */
251
+ private async updateRequirementProgressStatus(
252
+ categoryName: string, requirementName: string, requirementType: string, operationId: string
253
+ ): Promise<void> {
254
+ const configProvider = ConfigurationProvider.getInstance();
255
+ await configProvider.updateRequirementStatus(categoryName, requirementName, {
256
+ name: requirementName, type: requirementType, installed: false, inProgress: true,
257
+ operationStatus: {
258
+ status: 'in-progress', type: 'install', target: 'requirement',
259
+ message: `Installing requirement: ${requirementName}`, operationId
260
+ }
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Helper to update requirement status for completion case.
266
+ * @param categoryName The category name.
267
+ * @param requirementName The name of the requirement.
268
+ * @param installStatus The installation status.
269
+ * @param operationId The operation ID.
270
+ */
271
+ private async updateRequirementCompletionStatus(
272
+ categoryName: string, requirementName: string, installStatus: RequirementStatus, operationId: string
273
+ ): Promise<void> {
274
+ const configProvider = ConfigurationProvider.getInstance();
275
+ await configProvider.updateRequirementStatus(categoryName, requirementName, {
276
+ ...installStatus,
277
+ operationStatus: {
278
+ status: installStatus.installed ? 'completed' : 'failed',
279
+ type: 'install', target: 'requirement',
280
+ message: installStatus.installed
281
+ ? `Requirement ${requirementName} installed successfully`
282
+ : `Failed to install ${requirementName}`,
283
+ operationId
284
+ }
285
+ });
286
+ }
287
+
288
+ /**
289
+ * Validate a requirement configuration.
290
+ * @param requirement The requirement to validate.
291
+ * @throws Error if the requirement is invalid.
292
+ */
293
+ private validateRequirement(requirement: RequirementConfig): void {
294
+ if (!requirement.name) { throw new Error('Requirement name is required'); }
295
+ if (!requirement.type) { throw new Error('Requirement type is required'); }
296
+ if (requirement.type === 'other' && !requirement.registry) {
297
+ throw new Error('Registry must be specified for requirement type "other"');
298
+ }
299
+ if (requirement.registry) {
300
+ const { githubRelease, artifacts } = requirement.registry;
301
+ if (githubRelease) {
302
+ if (!githubRelease.repository) { throw new Error('Repository is required for GitHub release registry'); }
303
+ if (!githubRelease.assetName) { throw new Error('Asset name is required for GitHub release registry'); }
304
+ }
305
+ if (artifacts) {
306
+ if (!artifacts.registryUrl) { throw new Error('Registry URL is required for artifacts registry'); }
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Check for updates to requirements for all server categories
313
+ * This method is called periodically to check for updates
314
+ */
315
+ public async checkRequirementForUpdateAsync(): Promise<void> {
316
+ const allCategories = await configProvider.getServerCategories();
317
+ allCategories.forEach(
318
+ async (serverCategory) => {
319
+ // Start async check for requirement updates if one isn't already in progress
320
+ if (serverCategory && serverCategory.feedConfiguration && serverCategory.name) {
321
+ // Check if update is already in progress using the tracker
322
+ const shouldCheckForUpdates = await updateCheckTracker.startOperation(serverCategory.name);
323
+
324
+ if (shouldCheckForUpdates) {
325
+ Logger.info(`Checking the status of requirements in ${serverCategory.name}`);
326
+ this.checkRequirementsForUpdateInternal(serverCategory).catch(error => {
327
+ // Ensure we mark the operation as complete on error
328
+ if (serverCategory.name) {
329
+ updateCheckTracker.endOperation(serverCategory.name)
330
+ .catch(lockError => console.error(`Failed to mark update check as complete: ${lockError.message}`));
331
+ }
332
+ Logger.error(`Error checking requirements for updates: ${error.message}`);
333
+ });
334
+ } else {
335
+ Logger.debug(`Update check already in progress for ${serverCategory.name}, skipping`);
336
+ }
337
+ }
338
+ }
339
+ )
340
+ }
341
+
342
+ /**
343
+ * Check for updates to requirements for a specific server category
344
+ * @param categoryName The name of the server category
345
+ * @param serverName The name of the server (optional)
346
+ */
347
+ public async checkServerRequirementForUpdateAsync(categoryName: string, serverName: string): Promise<void> {
348
+ const serverCategory = await configProvider.getServerCategory(categoryName);
349
+ if (!serverCategory) {
350
+ Logger.error(`Server category ${categoryName} not found`);
351
+ return;
352
+ }
353
+ const shouldCheckForUpdates = await updateCheckTracker.startOperation(`${serverCategory.name}:${serverName}`);
354
+ if (shouldCheckForUpdates) {
355
+ this.checkRequirementsForUpdateInternal(serverCategory, serverName)
356
+ .then(() => {
357
+ // Mark the operation as complete
358
+ updateCheckTracker.endOperation(`${serverCategory.name}:${serverName}`)
359
+ .catch(lockError => console.error(`Failed to mark update check as complete: ${lockError.message}`));
360
+
361
+ })
362
+ .catch(error => {
363
+ // Ensure we mark the operation as complete on error
364
+ updateCheckTracker.endOperation(`${serverCategory.name}:${serverName}`)
365
+ .catch(lockError => console.error(`Failed to mark update check as complete: ${lockError.message}`));
366
+
367
+ Logger.error(`Error checking requirements for updates: ${error.message}`);
368
+ });
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Check for updates to requirements for a server category
374
+ * @param serverCategory The server category to check
375
+ * @param serverName The name of the server (optional). When serverName is provided, check the requirement always
376
+ * @private
377
+ */
378
+ private async checkRequirementsForUpdateInternal(serverCategory: MCPServerCategory, serverName?: string): Promise<void> {
379
+ if (!serverCategory.name || !serverCategory.feedConfiguration?.requirements?.length) {
380
+ return;
381
+ }
382
+ try {
383
+
384
+ for (const requirement of serverCategory.feedConfiguration.requirements) {
385
+ if (requirement.version.includes('latest')) {
386
+ // Get current status if available
387
+ const currentStatus = serverCategory.installationStatus?.requirementsStatus[requirement.name];
388
+ if (!currentStatus) continue;
389
+
390
+ let providedServerName: string | undefined = serverName;
391
+ if (providedServerName) {
392
+ // If serverName is provided, check if the requirement is associated with that server
393
+ Logger.debug(`Checking whether requirement ${requirement.name} has update for server ${serverName}`);
394
+ const serverConfig = serverCategory.feedConfiguration.mcpServers.find((s: McpConfig) => s.name === serverName);
395
+ if (!serverConfig?.dependencies?.requirements?.some((r) => r.name == requirement.name)) {
396
+ Logger.debug(`Requirement ${requirement.name} is not associated with server ${serverName}. Skipping update check.`);
397
+ continue;
398
+ }
399
+ } else {
400
+ providedServerName = serverCategory.feedConfiguration.mcpServers.find((s: McpConfig) => s.dependencies?.requirements?.some((r) => r.name == requirement.name))?.name;
401
+ }
402
+
403
+ if (!providedServerName) {
404
+ // We need to find the server name for the requirement, if not able to find, skip as the requirement is not associated with any server
405
+ Logger.debug(`No server using requirement ${requirement.name}. Skipping update check.`);
406
+ continue;
407
+ }
408
+ // Check for updates
409
+ const updatedStatus = await this.checkRequirementForUpdates(requirement, currentStatus, serverCategory.name, providedServerName);
410
+
411
+ // If update information is found, update the configuration
412
+ if (updatedStatus.availableUpdate && serverCategory.name) {
413
+ await configProvider.updateRequirementStatus(
414
+ serverCategory.name,
415
+ requirement.name,
416
+ updatedStatus
417
+ );
418
+
419
+ // Also update the in-memory status for immediate use
420
+ if (serverCategory.installationStatus?.requirementsStatus) {
421
+ serverCategory.installationStatus.requirementsStatus[requirement.name] = updatedStatus;
422
+ }
423
+ }
424
+ currentStatus.lastCheckTime = new Date().toISOString();
425
+ await configProvider.updateRequirementStatus(
426
+ serverCategory.name,
427
+ requirement.name,
428
+ currentStatus
429
+ );
430
+ if (serverCategory.installationStatus?.requirementsStatus) {
431
+ serverCategory.installationStatus.requirementsStatus[requirement.name] = updatedStatus;
432
+ }
433
+ }
434
+ }
435
+ } catch (error) {
436
+ Logger.error(`Error checking requirements for updates: ${error instanceof Error ? error.message : String(error)}`);
437
+ throw error; // Re-throw the error to be handled by the caller
438
+ }
439
+ }
26
440
  /**
27
441
  * Check the installation status of a requirement
28
442
  * @param requirement The requirement to check
443
+ * @param options Optional installation options
29
444
  * @returns The installation status
30
445
  */
31
446
  public async checkRequirementStatus(requirement: RequirementConfig, options?: ServerInstallOptions): Promise<RequirementStatus> {
32
- // Validate requirement
33
447
  this.validateRequirement(requirement);
34
-
35
- // Check the installation status
36
448
  return await this.installerFactory.checkInstallation(requirement, options);
37
449
  }
38
450
 
39
451
  /**
40
452
  * Check if updates are available for a requirement
41
453
  * @param requirement The requirement to check for updates
454
+ * @param currentStatus The current status of the requirement
455
+ * @param categoryName The category name
456
+ * @param serverName The server name (optional)
42
457
  * @returns Updated status with available updates information
43
458
  */
44
- public async checkRequirementForUpdates(requirement: RequirementConfig, currentStatus: RequirementStatus): Promise<RequirementStatus> {
45
- // Validate requirement
459
+ public async checkRequirementForUpdates(requirement: RequirementConfig, currentStatus: RequirementStatus, categoryName: string, serverName?: string): Promise<RequirementStatus> {
46
460
  this.validateRequirement(requirement);
47
-
48
- // Get current status
49
-
50
- // Check for updates using the appropriate installer
51
461
  const installer = this.installerFactory.getInstaller(requirement);
52
462
  if (!installer || !installer.supportCheckUpdates()) {
53
463
  return currentStatus;
54
464
  }
55
-
56
- // Pass pythonEnv from currentStatus if it exists for pip packages
57
- const options = requirement.type === 'pip' && currentStatus.pythonEnv
58
- ? { settings: { pythonEnv: currentStatus.pythonEnv } }
465
+ var systemSettings = await systemSettingsManager.getSystemSettings();
466
+ const options = requirement.type === 'pip'
467
+ ? { settings: { pythonEnv: systemSettings.pythonEnvs?.[`${categoryName}:${serverName}`] || systemSettings.pythonEnvs?.system || currentStatus.pythonEnv } }
59
468
  : undefined;
60
469
  const status = await this.checkRequirementStatus(requirement, options);
61
470
  return await (installer as any).checkForUpdates(requirement, status);
62
471
  }
63
472
 
64
473
  /**
65
- * Update a requirement to a new version
66
- * @param requirement The requirement configuration
67
- * @param updateVersion The version to update to
68
- * @returns The updated requirement status
474
+ * Process requirement updates specified in serverInstallOptions.
475
+ * All updates are processed in parallel for maximum efficiency.
476
+ * @param categoryName The category name.
477
+ * @param serverName The server name.
478
+ * @param options The installation options.
69
479
  */
70
- public async updateRequirement(requirement: RequirementConfig, updateVersion: string, options?: ServerInstallOptions): Promise<RequirementStatus> {
71
- // Validate requirement
72
- this.validateRequirement(requirement);
480
+ public async processRequirementUpdates(categoryName: string, serverName: string, options: ServerInstallOptions): Promise<void> {
481
+ const operationKey = `requirement-updates-${categoryName}-${serverName}`;
482
+ const canProceed = await updateCheckTracker.startOperation(operationKey);
483
+ if (!canProceed) {
484
+ Logger.info(`Requirement updates for ${categoryName}/${serverName} already in progress, skipping`);
485
+ return;
486
+ }
73
487
 
74
- // Create an updated requirement with the new version
75
- const updatedRequirement: RequirementConfig = {
76
- ...requirement,
77
- version: requirement.version.includes('latest') ? requirement.version : updateVersion,
78
- };
488
+ const recoder = InstallOperationManager.getInstance(categoryName, serverName);
79
489
 
80
- // Install the updated version
81
- return await this.installerFactory.install(updatedRequirement, options);
82
- }
490
+ try {
491
+ await recoder.recording(async () => {
492
+ const configProvider = ConfigurationProvider.getInstance();
493
+ const feedConfig = await configProvider.getFeedConfiguration(categoryName);
83
494
 
84
- /**
85
- * Validate a requirement configuration
86
- * @param requirement The requirement to validate
87
- * @throws Error if the requirement is invalid
88
- */
89
- private validateRequirement(requirement: RequirementConfig): void {
90
- // Ensure requirement has required fields
91
- if (!requirement.name) {
92
- throw new Error('Requirement name is required');
93
- }
495
+ if (!feedConfig) {
496
+ const errorMsg = `Feed configuration not found for category: ${categoryName}`;
497
+ Logger.error(errorMsg);
498
+ throw new Error(errorMsg); // This will be caught by recoder.recording
499
+ }
94
500
 
95
- if (!requirement.type) {
96
- throw new Error('Requirement type is required');
97
- }
501
+ const updatePromises = options.requirements?.map(async (reqToUpdate) => {
502
+ const stepName = `${RecordingConstants.STEP_INSTALL_REQUIREMENT_PREFIX}: ${reqToUpdate.name}`;
503
+ return recoder.recording(
504
+ async () => {
505
+ let reqConfig: RequirementConfig | undefined;
506
+ let currentStatus: RequirementStatus | undefined | null;
507
+ try {
508
+ reqConfig = feedConfig.requirements?.find((r: RequirementConfig) => r.name === reqToUpdate.name);
509
+ if (!reqConfig) {
510
+ const errorMsg = `Requirement configuration not found for: ${reqToUpdate.name}`;
511
+ Logger.error(errorMsg);
512
+ await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
513
+ name: reqToUpdate.name, type: 'unknown', installed: false, inProgress: false,
514
+ error: errorMsg,
515
+ operationStatus: { status: 'failed', type: 'update', target: 'requirement', message: errorMsg }
516
+ });
517
+ throw new Error(errorMsg);
518
+ }
98
519
 
99
- // For type 'other', registry must be specified
100
- if (requirement.type === 'other' && !requirement.registry) {
101
- throw new Error('Registry must be specified for requirement type "other"');
102
- }
520
+ currentStatus = await configProvider.getRequirementStatus(categoryName, reqToUpdate.name);
521
+ if (!currentStatus) {
522
+ const errorMsg = `No current status found for requirement: ${reqToUpdate.name}`;
523
+ Logger.error(errorMsg);
524
+ await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
525
+ name: reqToUpdate.name, type: reqConfig.type, installed: false, inProgress: false,
526
+ error: errorMsg,
527
+ operationStatus: { status: 'failed', type: 'update', target: 'requirement', message: errorMsg }
528
+ });
529
+ throw new Error(errorMsg);
530
+ }
103
531
 
104
- // Validate registry configuration if provided
105
- if (requirement.registry) {
106
- const { githubRelease, artifacts } = requirement.registry;
532
+ Logger.info(`Updating ${reqToUpdate.name} from ${currentStatus.version || 'unknown'} to ${reqToUpdate.version}`);
533
+ await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
534
+ ...currentStatus, name: reqToUpdate.name,
535
+ type: currentStatus.type || reqConfig.type || 'unknown',
536
+ installed: currentStatus.installed || false, inProgress: true,
537
+ operationStatus: {
538
+ status: 'in-progress', type: 'update', target: 'requirement',
539
+ message: `Updating ${reqToUpdate.name} from ${currentStatus.version || 'unknown'} to ${reqToUpdate.version}`
540
+ }
541
+ });
107
542
 
108
- // Validate GitHub release configuration
109
- if (githubRelease) {
110
- if (!githubRelease.repository) {
111
- throw new Error('Repository is required for GitHub release registry');
112
- }
543
+ let currentOptions = { ...options };
544
+ if (reqConfig.type === 'pip' && currentStatus.pythonEnv && !currentOptions?.settings?.pythonEnv) {
545
+ currentOptions = {
546
+ ...currentOptions,
547
+ settings: { ...currentOptions?.settings, pythonEnv: currentStatus.pythonEnv }
548
+ };
549
+ }
113
550
 
114
- if (!githubRelease.assetName) {
115
- throw new Error('Asset name is required for GitHub release registry');
116
- }
117
- }
551
+ const updatedStatus = await this.updateRequirement(reqConfig, reqToUpdate.version, recoder, currentOptions);
118
552
 
119
- // Validate artifacts registry configuration
120
- if (artifacts) {
121
- if (!artifacts.registryUrl) {
122
- throw new Error('Registry URL is required for artifacts registry');
553
+ const successMessage = `Successfully updated ${reqToUpdate.name} to version ${reqToUpdate.version}`;
554
+ const failureMessage = `Failed to update ${reqToUpdate.name} to version ${reqToUpdate.version}. Error: ${updatedStatus.error || 'Unknown'}`;
555
+
556
+ await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
557
+ ...updatedStatus, name: reqToUpdate.name,
558
+ type: updatedStatus.type || currentStatus.type || reqConfig.type || 'unknown',
559
+ installed: updatedStatus.installed, inProgress: false,
560
+ operationStatus: {
561
+ status: updatedStatus.installed ? 'completed' : 'failed',
562
+ type: 'update', target: 'requirement',
563
+ message: updatedStatus.installed ? successMessage : failureMessage
564
+ },
565
+ availableUpdate: updatedStatus.installed ? undefined : currentStatus.availableUpdate
566
+ });
567
+
568
+ if (!updatedStatus.installed) {
569
+ Logger.error(failureMessage);
570
+ throw new Error(failureMessage);
571
+ }
572
+ Logger.info(successMessage);
573
+ return updatedStatus;
574
+ } catch (error) {
575
+ const errorMsg = `Error updating requirement ${reqToUpdate.name}: ${error instanceof Error ? error.message : String(error)}`;
576
+ Logger.error(errorMsg);
577
+ await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
578
+ name: reqToUpdate.name,
579
+ type: reqConfig?.type || currentStatus?.type || 'unknown',
580
+ installed: false, inProgress: false,
581
+ error: error instanceof Error ? error.message : String(error),
582
+ operationStatus: { status: 'failed', type: 'update', target: 'requirement', message: errorMsg }
583
+ });
584
+ throw error; // Re-throw to be handled by recoder.recording
585
+ }
586
+ },
587
+ {
588
+ stepName: stepName,
589
+ inProgressMessage: `Processing update for requirement ${reqToUpdate.name}`,
590
+ onResult: (status) => status.installed, // status is the returned updatedStatus
591
+ }
592
+ );
593
+ });
594
+
595
+ if (updatePromises) {
596
+ await Promise.all(updatePromises);
123
597
  }
124
- }
598
+ }, {
599
+ stepName: RecordingConstants.STEP_PROCESS_REQUIREMENT_UPDATES,
600
+ inProgressMessage: `Starting to process requirement updates for ${serverName}`,
601
+ // If fn() succeeds, data is void. If fn() throws, recording() uses the error message.
602
+ endMessage: () => `Finished processing requirement updates for ${serverName}`
603
+ });
604
+ } finally {
605
+ await updateCheckTracker.endOperation(operationKey);
125
606
  }
126
607
  }
608
+
609
+ /**
610
+ * Update a requirement to a new version
611
+ * @param requirement The requirement configuration
612
+ * @param updateVersion The version to update to
613
+ * @param options Optional installation options
614
+ * @returns The updated requirement status
615
+ */
616
+ private async updateRequirement(requirement: RequirementConfig, updateVersion: string, recorder: InstallOperationManager, options?: ServerInstallOptions): Promise<RequirementStatus> {
617
+ this.validateRequirement(requirement);
618
+ const updatedRequirement: RequirementConfig = {
619
+ ...requirement,
620
+ version: requirement.version.includes('latest') ? requirement.version : updateVersion,
621
+ };
622
+ return await this.installerFactory.install(updatedRequirement, recorder, options);
623
+ }
127
624
  }
128
625
 
129
626
  // Export a singleton instance