imcp 0.0.14 → 0.0.16

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 (136) hide show
  1. package/dist/core/ConfigurationProvider.d.ts +1 -0
  2. package/dist/core/ConfigurationProvider.js +15 -0
  3. package/dist/core/InstallationService.js +2 -7
  4. package/dist/core/MCPManager.d.ts +11 -2
  5. package/dist/core/MCPManager.js +24 -1
  6. package/dist/core/RequirementService.js +2 -8
  7. package/dist/core/installers/clients/BaseClientInstaller.d.ts +51 -0
  8. package/dist/core/installers/clients/BaseClientInstaller.js +160 -0
  9. package/dist/core/installers/clients/ClientInstaller.d.ts +16 -9
  10. package/dist/core/installers/clients/ClientInstaller.js +80 -527
  11. package/dist/core/installers/clients/ClientInstallerFactory.d.ts +20 -0
  12. package/dist/core/installers/clients/ClientInstallerFactory.js +37 -0
  13. package/dist/core/installers/clients/ClineInstaller.d.ts +18 -0
  14. package/dist/core/installers/clients/ClineInstaller.js +124 -0
  15. package/dist/core/installers/clients/GithubCopilotInstaller.d.ts +34 -0
  16. package/dist/core/installers/clients/GithubCopilotInstaller.js +162 -0
  17. package/dist/core/installers/clients/MSRooCodeInstaller.d.ts +15 -0
  18. package/dist/core/installers/clients/MSRooCodeInstaller.js +122 -0
  19. package/dist/core/installers/requirements/BaseInstaller.d.ts +11 -34
  20. package/dist/core/installers/requirements/BaseInstaller.js +5 -116
  21. package/dist/core/installers/requirements/CommandInstaller.d.ts +6 -1
  22. package/dist/core/installers/requirements/CommandInstaller.js +7 -0
  23. package/dist/core/installers/requirements/GeneralInstaller.d.ts +6 -1
  24. package/dist/core/installers/requirements/GeneralInstaller.js +9 -4
  25. package/dist/core/installers/requirements/NpmInstaller.d.ts +46 -7
  26. package/dist/core/installers/requirements/NpmInstaller.js +150 -58
  27. package/dist/core/installers/requirements/PipInstaller.d.ts +9 -0
  28. package/dist/core/installers/requirements/PipInstaller.js +66 -28
  29. package/dist/core/onboard/FeedOnboardService.d.ts +50 -13
  30. package/dist/core/onboard/FeedOnboardService.js +263 -88
  31. package/dist/core/onboard/OnboardProcessor.d.ts +79 -0
  32. package/dist/core/onboard/OnboardProcessor.js +290 -0
  33. package/dist/core/onboard/OnboardStatus.d.ts +49 -0
  34. package/dist/core/onboard/OnboardStatus.js +10 -0
  35. package/dist/core/onboard/OnboardStatusManager.d.ts +57 -0
  36. package/dist/core/onboard/OnboardStatusManager.js +176 -0
  37. package/dist/core/types.d.ts +4 -5
  38. package/dist/core/validators/FeedValidator.d.ts +8 -1
  39. package/dist/core/validators/FeedValidator.js +60 -7
  40. package/dist/core/validators/IServerValidator.d.ts +19 -0
  41. package/dist/core/validators/IServerValidator.js +2 -0
  42. package/dist/core/validators/SSEServerValidator.d.ts +15 -0
  43. package/dist/core/validators/SSEServerValidator.js +39 -0
  44. package/dist/core/validators/ServerValidatorFactory.d.ts +24 -0
  45. package/dist/core/validators/ServerValidatorFactory.js +45 -0
  46. package/dist/core/validators/StdioServerValidator.d.ts +46 -0
  47. package/dist/core/validators/StdioServerValidator.js +229 -0
  48. package/dist/services/InstallRequestValidator.d.ts +1 -1
  49. package/dist/services/ServerService.d.ts +9 -6
  50. package/dist/services/ServerService.js +18 -7
  51. package/dist/utils/adoUtils.d.ts +29 -0
  52. package/dist/utils/adoUtils.js +252 -0
  53. package/dist/utils/clientUtils.d.ts +0 -7
  54. package/dist/utils/clientUtils.js +0 -42
  55. package/dist/utils/githubUtils.d.ts +10 -0
  56. package/dist/utils/githubUtils.js +22 -0
  57. package/dist/utils/macroExpressionUtils.d.ts +38 -0
  58. package/dist/utils/macroExpressionUtils.js +116 -0
  59. package/dist/utils/osUtils.d.ts +4 -20
  60. package/dist/utils/osUtils.js +78 -23
  61. package/dist/web/contract/serverContract.d.ts +3 -1
  62. package/dist/web/public/css/notifications.css +48 -17
  63. package/dist/web/public/css/onboard.css +66 -3
  64. package/dist/web/public/index.html +84 -16
  65. package/dist/web/public/js/api.js +3 -6
  66. package/dist/web/public/js/flights/flights.js +127 -0
  67. package/dist/web/public/js/modal/installation.js +5 -5
  68. package/dist/web/public/js/modal/modalSetup.js +3 -2
  69. package/dist/web/public/js/notifications.js +66 -27
  70. package/dist/web/public/js/onboard/ONBOARDING_PAGE_DESIGN.md +338 -0
  71. package/dist/web/public/js/onboard/formProcessor.js +810 -255
  72. package/dist/web/public/js/onboard/index.js +328 -85
  73. package/dist/web/public/js/onboard/publishHandler.js +132 -0
  74. package/dist/web/public/js/onboard/state.js +61 -17
  75. package/dist/web/public/js/onboard/templates.js +217 -249
  76. package/dist/web/public/js/onboard/uiHandlers.js +679 -117
  77. package/dist/web/public/js/onboard/validationHandlers.js +378 -0
  78. package/dist/web/public/js/serverCategoryList.js +15 -2
  79. package/dist/web/public/onboard.html +191 -45
  80. package/dist/web/public/styles.css +91 -1
  81. package/dist/web/server.d.ts +0 -10
  82. package/dist/web/server.js +131 -22
  83. package/package.json +2 -2
  84. package/src/core/ConfigurationProvider.ts +15 -0
  85. package/src/core/InstallationService.ts +2 -7
  86. package/src/core/MCPManager.ts +26 -1
  87. package/src/core/RequirementService.ts +2 -9
  88. package/src/core/installers/clients/BaseClientInstaller.ts +196 -0
  89. package/src/core/installers/clients/ClientInstaller.ts +97 -608
  90. package/src/core/installers/clients/ClientInstallerFactory.ts +43 -0
  91. package/src/core/installers/clients/ClineInstaller.ts +135 -0
  92. package/src/core/installers/clients/GithubCopilotInstaller.ts +179 -0
  93. package/src/core/installers/clients/MSRooCodeInstaller.ts +133 -0
  94. package/src/core/installers/requirements/BaseInstaller.ts +13 -136
  95. package/src/core/installers/requirements/CommandInstaller.ts +9 -1
  96. package/src/core/installers/requirements/GeneralInstaller.ts +11 -4
  97. package/src/core/installers/requirements/NpmInstaller.ts +178 -61
  98. package/src/core/installers/requirements/PipInstaller.ts +68 -29
  99. package/src/core/onboard/FeedOnboardService.ts +346 -0
  100. package/src/core/onboard/OnboardProcessor.ts +305 -0
  101. package/src/core/onboard/OnboardStatus.ts +55 -0
  102. package/src/core/onboard/OnboardStatusManager.ts +188 -0
  103. package/src/core/types.ts +4 -5
  104. package/src/core/validators/FeedValidator.ts +79 -0
  105. package/src/core/validators/IServerValidator.ts +21 -0
  106. package/src/core/validators/SSEServerValidator.ts +43 -0
  107. package/src/core/validators/ServerValidatorFactory.ts +51 -0
  108. package/src/core/validators/StdioServerValidator.ts +259 -0
  109. package/src/services/InstallRequestValidator.ts +1 -1
  110. package/src/services/ServerService.ts +22 -7
  111. package/src/utils/adoUtils.ts +291 -0
  112. package/src/utils/clientUtils.ts +0 -44
  113. package/src/utils/githubUtils.ts +24 -0
  114. package/src/utils/macroExpressionUtils.ts +121 -0
  115. package/src/utils/osUtils.ts +89 -24
  116. package/src/web/contract/serverContract.ts +74 -0
  117. package/src/web/public/css/notifications.css +48 -17
  118. package/src/web/public/css/onboard.css +107 -0
  119. package/src/web/public/index.html +84 -16
  120. package/src/web/public/js/api.js +3 -6
  121. package/src/web/public/js/flights/flights.js +127 -0
  122. package/src/web/public/js/modal/installation.js +5 -5
  123. package/src/web/public/js/modal/modalSetup.js +3 -2
  124. package/src/web/public/js/notifications.js +66 -27
  125. package/src/web/public/js/onboard/ONBOARDING_PAGE_DESIGN.md +338 -0
  126. package/src/web/public/js/onboard/formProcessor.js +864 -0
  127. package/src/web/public/js/onboard/index.js +374 -0
  128. package/src/web/public/js/onboard/publishHandler.js +132 -0
  129. package/src/web/public/js/onboard/state.js +76 -0
  130. package/src/web/public/js/onboard/templates.js +343 -0
  131. package/src/web/public/js/onboard/uiHandlers.js +758 -0
  132. package/src/web/public/js/onboard/validationHandlers.js +378 -0
  133. package/src/web/public/js/serverCategoryList.js +15 -2
  134. package/src/web/public/onboard.html +296 -0
  135. package/src/web/public/styles.css +91 -1
  136. package/src/web/server.ts +167 -58
@@ -1,28 +1,21 @@
1
- import { ExtensionInstaller } from './ExtensionInstaller.js';
1
+ import { ConfigurationProvider } from '../../ConfigurationProvider.js';
2
+ import { SUPPORTED_CLIENTS } from '../../constants.js';
2
3
  import {
3
4
  ServerOperationResult,
4
5
  OperationStatus,
5
- MCPServerStatus,
6
- RequirementStatus,
7
6
  ServerInstallOptions,
8
- FeedConfiguration,
9
- McpConfig
7
+ McpConfig,
8
+ RequirementStatus
10
9
  } from '../../types.js';
11
- import { GetBrowserPath } from '../../../utils/osUtils.js';
12
- import { ConfigurationProvider } from '../../ConfigurationProvider.js';
13
- import { SUPPORTED_CLIENTS } from '../../constants.js';
14
- import { resolveNpmModulePath, readJsonFile, writeJsonFile } from '../../../utils/clientUtils.js';
15
- import { exec } from 'child_process';
16
- import { promisify } from 'util';
17
- import * as path from 'path';
18
10
  import { Logger } from '../../../utils/logger.js';
19
- import { getPythonPackagePath, getSystemPythonPackageDirectory } from '../../../utils/osUtils.js';
20
-
21
- const execAsync = promisify(exec); // Moved promisify here for reuse
11
+ import { ClientInstallerFactory } from './ClientInstallerFactory.js';
22
12
 
13
+ /**
14
+ * Main client installer class that orchestrates client installation process
15
+ * Handles requirements checking and delegates to specific client installers
16
+ */
23
17
  export class ClientInstaller {
24
18
  private configProvider: ConfigurationProvider;
25
- private operationStatuses: Map<string, OperationStatus>;
26
19
 
27
20
  constructor(
28
21
  private categoryName: string,
@@ -30,109 +23,89 @@ export class ClientInstaller {
30
23
  private clients: string[]
31
24
  ) {
32
25
  this.configProvider = ConfigurationProvider.getInstance();
33
- this.operationStatuses = new Map();
34
26
  }
35
27
 
28
+ /**
29
+ * Generate a unique operation ID for tracking installations
30
+ */
36
31
  private generateOperationId(): string {
37
32
  return `install-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
38
33
  }
39
34
 
40
- private async getNpmPath(): Promise<string> {
41
- try {
42
- // Execute the get-command npm command to find the npm path
43
- const { stdout } = await execAsync('powershell -Command "get-command npm | Select-Object -ExpandProperty Source"');
44
-
45
- // Extract the directory from the full path (removing npm.cmd)
46
- const npmPath = stdout.trim().replace(/\\npm\.cmd$/, '');
47
- return npmPath;
48
- } catch (error) {
49
- console.error('Error getting npm path:', error);
50
- // Return a default path if the command fails
51
- return 'C:\\Program Files\\nodejs';
52
- }
53
- }
54
-
55
-
56
35
  /**
57
- * Check if a command is available on the system
58
- * @param command The command to check
59
- * @returns True if the command is available, false otherwise
36
+ * Check if server requirements are ready
37
+ * Waits for requirements to be ready with timeout
60
38
  */
61
- private async isCommandAvailable(command: string): Promise<boolean> {
62
- try {
63
- // For VS Code on macOS, check both command-line tool and app bundle
64
- if (process.platform === 'darwin' && (command === 'code' || command === 'code-insiders')) {
65
- try {
66
- // Try which command first
67
- await execAsync(`which ${command}`);
68
- return true;
69
- } catch (error) {
70
- // If which fails, check application bundles
71
- const systemVSCodePath = command === 'code' ?
72
- '/Applications/Visual Studio Code.app' :
73
- '/Applications/Visual Studio Code - Insiders.app';
39
+ private async checkRequirements(operationId: string, clientName: string, options: ServerInstallOptions): Promise<boolean> {
40
+ let requirementsReady = await this.configProvider.isRequirementsReady(this.categoryName, this.serverName);
74
41
 
75
- try {
76
- // Check system Applications first
77
- await execAsync(`test -d "${systemVSCodePath}"`);
78
- return true;
79
- } catch (error) {
80
- // If system Applications check fails, try user Applications
81
- const homedir = process.env.HOME;
82
- if (homedir) {
83
- const userVSCodePath = command === 'code' ?
84
- `${homedir}/Applications/Visual Studio Code.app` :
85
- `${homedir}/Applications/Visual Studio Code - Insiders.app`;
42
+ if (!requirementsReady) {
43
+ const pendingStatus: OperationStatus = {
44
+ status: 'pending',
45
+ type: 'install',
46
+ target: 'server',
47
+ message: `Waiting for requirements to be ready for client: ${clientName}`,
48
+ operationId
49
+ };
86
50
 
87
- try {
88
- await execAsync(`test -d "${userVSCodePath}"`);
89
- return true;
90
- } catch (error) {
91
- return false;
92
- }
93
- }
94
- return false;
95
- }
96
- }
97
- }
51
+ await this.configProvider.updateServerOperationStatus(
52
+ this.categoryName,
53
+ this.serverName,
54
+ clientName,
55
+ pendingStatus
56
+ );
57
+
58
+ // Set up periodic checking with timeout
59
+ const startTime = Date.now();
60
+ const timeoutMs = 5 * 60 * 1000; // 5 minutes
61
+ const intervalMs = 5 * 1000; // 5 seconds
98
62
 
99
- // For Windows, use where command
100
- if (process.platform === 'win32') {
101
- await execAsync(`where ${command}`);
102
- return true;
63
+ while (!requirementsReady && (Date.now() - startTime) < timeoutMs) {
64
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
65
+ requirementsReady = await this.configProvider.isRequirementsReady(this.categoryName, this.serverName);
103
66
  }
67
+ }
104
68
 
105
- // For all other cases (Unix-like systems), use which command
106
- await execAsync(`which ${command}`);
107
- return true;
108
- } catch (error) {
109
- return false;
69
+ var requirementsStatus: RequirementStatus[] = await this.configProvider.GetServerRequirementStatus(this.categoryName, this.serverName);
70
+
71
+ // Find first non-empty npmPath from requirements status
72
+ const npmPathRequirement = requirementsStatus.find(status => status.npmPath && status.npmPath.length > 0);
73
+ if (npmPathRequirement && npmPathRequirement.npmPath) {
74
+ options.settings = options.settings || {};
75
+ options.settings.npmPath = npmPathRequirement.npmPath;
110
76
  }
77
+
78
+ return requirementsReady;
111
79
  }
112
80
 
113
- // Modified to accept ServerInstallOptions
81
+ /**
82
+ * Install client with requirements checking
83
+ */
114
84
  private async installClient(clientName: string, options: ServerInstallOptions): Promise<OperationStatus> {
85
+ const operationId = this.generateOperationId();
86
+
115
87
  // Check if client is supported
116
- if (!SUPPORTED_CLIENTS[clientName]) {
88
+ if (!ClientInstallerFactory.isClientSupported(clientName)) {
117
89
  return {
118
90
  status: 'failed',
119
91
  type: 'install',
120
92
  target: 'server',
121
93
  message: `Unsupported client: ${clientName}`,
122
- operationId: this.generateOperationId()
94
+ operationId
123
95
  };
124
96
  }
125
97
 
126
98
  // Create initial operation status
127
- const operationId = this.generateOperationId();
128
99
  const initialStatus: OperationStatus = {
129
100
  status: 'pending',
130
101
  type: 'install',
131
102
  target: 'server',
132
103
  message: `Initializing installation for client: ${clientName}`,
133
- operationId: operationId
104
+ operationId
134
105
  };
135
106
 
107
+ this.processInstallation(clientName, operationId, options);
108
+
136
109
  // Update server status with initial client installation status
137
110
  await this.configProvider.updateServerOperationStatus(
138
111
  this.categoryName,
@@ -140,73 +113,41 @@ export class ClientInstaller {
140
113
  clientName,
141
114
  initialStatus
142
115
  );
143
-
144
- // Start the asynchronous installation process without awaiting it
145
- // Pass options down
146
- this.processInstallation(clientName, operationId, options);
147
-
148
- // Return the initial status immediately
149
116
  return initialStatus;
150
117
  }
151
118
 
152
- // Modified to accept ServerInstallOptions
153
119
  private async processInstallation(clientName: string, operationId: string, options: ServerInstallOptions): Promise<void> {
154
120
  try {
155
- await ExtensionInstaller.installExtension(clientName);
156
-
157
- // Check requirements before installation
158
- let requirementsReady = await this.configProvider.isRequirementsReady(this.categoryName, this.serverName);
159
-
160
- // If requirements are not ready, periodically check with timeout
121
+ // Check requirements
122
+ const requirementsReady = await this.checkRequirements(operationId, clientName, options);
161
123
  if (!requirementsReady) {
162
- const pendingStatus: OperationStatus = {
163
- status: 'pending',
124
+ const failedStatus: OperationStatus = {
125
+ status: 'failed',
164
126
  type: 'install',
165
127
  target: 'server',
166
- message: `Waiting for requirements to be ready for client: ${clientName}`,
167
- operationId: operationId
128
+ message: `Requirements not ready for client: ${clientName} after timeout`,
129
+ operationId
168
130
  };
169
131
 
170
- // Update status to pending with reference to configProvider
171
132
  await this.configProvider.updateServerOperationStatus(
172
133
  this.categoryName,
173
134
  this.serverName,
174
135
  clientName,
175
- pendingStatus
136
+ failedStatus
176
137
  );
138
+ return;
139
+ }
177
140
 
178
- // Set up periodic checking with timeout
179
- const startTime = Date.now();
180
- const timeoutMs = 5 * 60 * 1000; // 5 minutes in milliseconds
181
- const intervalMs = 5 * 1000; // 5 seconds in milliseconds
182
-
183
- while (!requirementsReady && (Date.now() - startTime) < timeoutMs) {
184
- // Wait for the interval
185
- await new Promise(resolve => setTimeout(resolve, intervalMs));
186
-
187
- // Check again
188
- requirementsReady = await this.configProvider.isRequirementsReady(this.categoryName, this.serverName);
189
- }
190
-
191
- // If still not ready after timeout, update status as failed and exit
192
- if (!requirementsReady) {
193
- const failedStatus: OperationStatus = {
194
- status: 'failed',
195
- type: 'install',
196
- target: 'server',
197
- message: `Timed out waiting for requirements to be ready for client: ${clientName} after 5 minutes`,
198
- operationId: operationId
199
- };
141
+ // Create client-specific installer
142
+ const installer = ClientInstallerFactory.getInstaller(clientName);
200
143
 
201
- await this.configProvider.updateServerOperationStatus(
202
- this.categoryName,
203
- this.serverName,
204
- clientName,
205
- failedStatus
206
- );
144
+ if (!installer) {
145
+ throw new Error(`Failed to create installer for client: ${clientName}`);
146
+ }
207
147
 
208
- return; // Exit the installation process
209
- }
148
+ const serverConfig = await this.configProvider.getServerMcpConfig(this.categoryName, this.serverName);
149
+ if (!serverConfig) {
150
+ throw new Error(`Server configuration not found for category: ${this.categoryName}, server: ${this.serverName}`);
210
151
  }
211
152
 
212
153
  // If we've reached here, requirements are ready - update status to in-progress
@@ -224,101 +165,25 @@ export class ClientInstaller {
224
165
  clientName,
225
166
  inProgressStatus
226
167
  );
168
+ // Install client
169
+ const status = await installer.install(serverConfig, options);
170
+ await this.configProvider.updateServerOperationStatus(
171
+ this.categoryName,
172
+ this.serverName,
173
+ clientName,
174
+ status
175
+ );
227
176
 
228
- // Get feed configuration for the server
229
- const feedConfiguration = await this.configProvider.getFeedConfiguration(this.categoryName);
230
- if (!feedConfiguration) {
231
- const errorStatus: OperationStatus = {
232
- status: 'failed',
233
- type: 'install',
234
- target: 'server',
235
- message: `Failed to get feed configuration for category: ${this.categoryName}`,
236
- operationId: operationId
237
- };
238
- await this.configProvider.updateServerOperationStatus(
239
- this.categoryName,
240
- this.serverName,
241
- clientName,
242
- errorStatus
243
- );
244
- return;
245
- }
246
-
247
- // Find the server config in the feed configuration
248
- const serverConfig = feedConfiguration.mcpServers.find(s => s.name === this.serverName);
249
- if (!serverConfig) {
250
- const errorStatus: OperationStatus = {
251
- status: 'failed',
252
- type: 'install',
253
- target: 'server',
254
- message: `Server ${this.serverName} not found in feed configuration`,
255
- operationId: operationId
256
- };
257
- await this.configProvider.updateServerOperationStatus(
258
- this.categoryName,
259
- this.serverName,
260
- clientName,
261
- errorStatus
262
- );
263
- return;
264
- }
265
-
266
- try {
267
- // Install extension if available
268
- if (SUPPORTED_CLIENTS[clientName]?.extension) {
269
- const extensionResult = await ExtensionInstaller.installExtension(clientName);
270
- if (!extensionResult) {
271
- Logger.debug(`Failed to install extension for client ${clientName}`);
272
- }
273
- }
274
-
275
- // Install client-specific configuration, passing options down
276
- const result = await this.installClientConfig(clientName, options, serverConfig, feedConfiguration);
277
-
278
- const finalStatus: OperationStatus = {
279
- status: result.success ? 'completed' : 'failed',
280
- type: 'install',
281
- target: 'server',
282
- message: result.message || `Installation for client ${clientName}: ${result.success ? 'successful' : 'failed'}`,
283
- operationId: operationId,
284
- error: result.success ? undefined : result.message
285
- };
286
-
287
- await this.configProvider.updateServerOperationStatus(
288
- this.categoryName,
289
- this.serverName,
290
- clientName,
291
- finalStatus
292
- );
293
-
294
- if (result.success) {
295
- await this.configProvider.reloadClientMCPSettings();
296
- }
297
-
298
- } catch (error) {
299
- const errorStatus: OperationStatus = {
300
- status: 'failed',
301
- type: 'install',
302
- target: 'server',
303
- message: `Failed to install client: ${clientName}. Error: ${error instanceof Error ? error.message : String(error)}`,
304
- operationId: operationId,
305
- error: error instanceof Error ? error.message : String(error)
306
- };
307
-
308
- await this.configProvider.updateServerOperationStatus(
309
- this.categoryName,
310
- this.serverName,
311
- clientName,
312
- errorStatus
313
- );
177
+ if (status.status === 'completed') {
178
+ await this.configProvider.reloadClientMCPSettings();
314
179
  }
315
180
  } catch (error) {
316
181
  const errorStatus: OperationStatus = {
317
182
  status: 'failed',
318
183
  type: 'install',
319
184
  target: 'server',
320
- message: `Unexpected error in installation process for client: ${clientName}. Error: ${error instanceof Error ? error.message : String(error)}`,
321
- operationId: operationId,
185
+ message: `Error installing client ${clientName}: ${error instanceof Error ? error.message : String(error)}`,
186
+ operationId,
322
187
  error: error instanceof Error ? error.message : String(error)
323
188
  };
324
189
 
@@ -331,402 +196,26 @@ export class ClientInstaller {
331
196
  }
332
197
  }
333
198
 
334
- // Modified to accept ServerInstallOptions
335
- private async installClientConfig(
336
- clientName: string,
337
- options: ServerInstallOptions, // Use options directly
338
- serverConfig: McpConfig,
339
- feedConfig: FeedConfiguration
340
- ): Promise<{ success: boolean; message: string }> {
341
- try {
342
- if (!SUPPORTED_CLIENTS[clientName]) {
343
- Logger.debug(`Client ${clientName} is not supported.`);
344
- return { success: false, message: `Unsupported client: ${clientName}` };
345
- }
346
-
347
- const clientSettings = SUPPORTED_CLIENTS[clientName];
348
-
349
- // Get both setting paths for VS Code and VS Code Insiders
350
- const regularSettingPath = clientSettings.codeSettingPath;
351
- const insidersSettingPath = clientSettings.codeInsiderSettingPath;
352
-
353
- Logger.debug(`Starting installation of ${this.serverName} for client ${clientName}`);
354
- Logger.debug(`VS Code settings path configured as: ${regularSettingPath}`);
355
- Logger.debug(`VS Code Insiders settings path configured as: ${insidersSettingPath}`);
356
-
357
- // Check if VS Code and VS Code Insiders are installed
358
- const isVSCodeInstalled = await this.isCommandAvailable('code');
359
- const isVSCodeInsidersInstalled = await this.isCommandAvailable('code-insiders');
360
- Logger.debug(isVSCodeInstalled ? 'VS Code detected on system' : 'VS Code not detected on system');
361
- Logger.debug(isVSCodeInsidersInstalled ? 'VS Code Insiders detected on system' : 'VS Code Insiders not detected on system');
362
-
363
- // If neither is installed, we can't proceed
364
- if (!isVSCodeInstalled && !isVSCodeInsidersInstalled) {
365
- Logger.debug('No VS Code installations detected on system. Cannot update settings.');
366
- return {
367
- success: false,
368
- message: `Neither VS Code nor VS Code Insiders are installed on this system. Cannot update settings for client: ${clientName}. Please install VS Code or VS Code Insiders and try again.`
369
- };
370
- }
371
-
372
- // --- Start of new logic ---
373
-
374
- // Clone the base installation configuration to avoid modifying the original serverConfig
375
- const installConfig = JSON.parse(JSON.stringify(serverConfig.installation));
376
- installConfig.mode = serverConfig.mode
377
- const pythonEnv = options.settings?.pythonEnv;
378
- let pythonDir: string | null = null;
379
-
380
- // 1. Determine which args to use and resolve npm paths
381
- let finalArgs: string[] = [];
382
- if (options.args && options.args.length > 0) {
383
- Logger.debug(`Using args from ServerInstallOptions: ${options.args.join(' ')}`);
384
- finalArgs = options.args.map(arg => resolveNpmModulePath(arg));
385
- } else if (installConfig.args && installConfig.args.length > 0) {
386
- Logger.debug(`Using args from serverConfig.installation: ${installConfig.args.join(' ')}`);
387
- finalArgs = installConfig.args.map((arg: string) => resolveNpmModulePath(arg));
388
- } else {
389
- Logger.debug('No args found in options or serverConfig.installation.');
390
- finalArgs = [];
391
- }
392
-
393
- // 2. Handle pythonEnv settings
394
- if (pythonEnv) {
395
- // 2.1 If pythonEnv is set
396
- Logger.debug(`Python environment specified: ${pythonEnv}`);
397
- pythonDir = getPythonPackagePath(pythonEnv);
398
-
399
- // 2.1.1 Replace ${PYTHON_PACKAGE} in args
400
- if (pythonDir) {
401
- finalArgs = finalArgs.map(arg => arg.includes('${PYTHON_PACKAGE}') ? arg.replace('${PYTHON_PACKAGE}', pythonDir!) : arg);
402
- Logger.debug(`Args after PYTHON_PACKAGE replacement (using pythonEnv): ${finalArgs.join(' ')}`);
403
- } else {
404
- Logger.debug(`Could not determine directory for pythonEnv: ${pythonEnv}`);
405
- }
406
-
407
- // 2.1.2 Replace command if it's 'python'
408
- if (installConfig.command === 'python') {
409
- Logger.debug(`Replacing command 'python' with specified pythonEnv: ${pythonEnv}`);
410
- installConfig.command = pythonEnv;
411
- }
412
- } else {
413
- // 2.2 If pythonEnv is not set
414
- Logger.debug('No Python environment specified in settings.');
415
- // 2.2.1 Replace ${PYTHON_PACKAGE} with system python directory if needed
416
- if (finalArgs.some(arg => arg.includes('${PYTHON_PACKAGE}'))) {
417
- Logger.debug('Attempting to find system Python directory for ${PYTHON_PACKAGE} replacement.');
418
- pythonDir = await getSystemPythonPackageDirectory();
419
- if (pythonDir) {
420
- finalArgs = finalArgs.map(arg => arg.includes('${PYTHON_PACKAGE}') ? arg.replace('${PYTHON_PACKAGE}', pythonDir!) : arg);
421
- Logger.debug(`Args after PYTHON_PACKAGE replacement (using system python): ${finalArgs.join(' ')}`);
422
- } else {
423
- Logger.debug('Could not find system Python directory. ${PYTHON_PACKAGE} replacement skipped.');
424
- // Optionally, remove or handle the arg containing ${PYTHON_PACKAGE} if Python is required
425
- // finalArgs = finalArgs.filter(arg => !arg.includes('${PYTHON_PACKAGE}'));
426
- }
427
- }
428
- }
429
-
430
- // Update installConfig with potentially modified args
431
- installConfig.args = finalArgs;
432
-
433
-
434
- // 3. Handle environment variables (merge default, serverConfig, and options.env)
435
- const baseEnv = serverConfig.installation.env || {};
436
- const defaultEnv: Record<string, string> = {};
437
- for (const [key, config] of Object.entries(baseEnv)) {
438
- const envConfig = config as any; // Type assertion
439
- if (envConfig.Default) {
440
- defaultEnv[key] = envConfig.Default;
441
- }
442
- }
443
- // Merge: options.env overrides defaultEnv
444
- installConfig.env = { ...defaultEnv, ...(options.env || {}) };
445
-
446
- // Replace ${BROWSER_PATH} with actual browser path
447
- const replacements = Object.entries(installConfig.env)
448
- .filter(([_, value]) => typeof value === 'string' && value.includes('${BROWSER_PATH}'))
449
- .map(async ([key, value]) => {
450
- try {
451
- const browserPath = await GetBrowserPath();
452
- return [key, (value as string).replace('${BROWSER_PATH}', browserPath)] as [string, string];
453
- } catch (error) {
454
- Logger.error(`Failed to get system browser path for env var ${key}:`, error);
455
- return [key, value] as [string, string];
456
- }
457
- });
458
-
459
- // Wait for all replacements to complete
460
- const replacedValues = await Promise.all(replacements);
461
- replacedValues.forEach(([key, value]) => {
462
- installConfig.env[key] = value;
463
- });
464
-
465
- Logger.debug(`Final environment variables: ${JSON.stringify(installConfig.env)}`);
466
-
467
- // --- End of new logic ---
468
-
469
-
470
- // Keep track of success for both installations
471
- let regularSuccess = false;
472
- let insidersSuccess = false;
473
- let errorMessages: string[] = [];
474
-
475
- // Update client-specific settings for both VS Code and VS Code Insiders
476
- if (clientName === 'MSRooCode' || clientName === 'Cline') {
477
- // Update VS Code settings if VS Code is installed
478
- if (isVSCodeInstalled) {
479
- try {
480
- Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
481
- // Pass the modified installConfig
482
- await this.updateClineOrMSRooSettings(regularSettingPath, this.serverName, installConfig, clientName);
483
- regularSuccess = true;
484
- Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
485
- } catch (error) {
486
- const errorMsg = `Error updating VS Code settings: ${error instanceof Error ? error.message : String(error)}`;
487
- errorMessages.push(errorMsg);
488
- console.error(errorMsg);
489
- }
490
- }
491
-
492
- // Update VS Code Insiders settings if VS Code Insiders is installed
493
- if (isVSCodeInsidersInstalled) {
494
- try {
495
- Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
496
- // Pass the modified installConfig
497
- await this.updateClineOrMSRooSettings(insidersSettingPath, this.serverName, installConfig, clientName);
498
- insidersSuccess = true;
499
- Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
500
- } catch (error) {
501
- const errorMsg = `Error updating VS Code Insiders settings: ${error instanceof Error ? error.message : String(error)}`;
502
- errorMessages.push(errorMsg);
503
- console.error(errorMsg);
504
- }
505
- }
506
- } else if (clientName === 'GithubCopilot') {
507
- // Update VS Code settings if VS Code is installed
508
- if (isVSCodeInstalled) {
509
- try {
510
- Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
511
- // Pass the modified installConfig
512
- await this.updateGithubCopilotSettings(regularSettingPath, this.serverName, installConfig);
513
- regularSuccess = true;
514
- Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
515
- } catch (error) {
516
- const errorMsg = `Error updating VS Code settings: ${error instanceof Error ? error.message : String(error)}`;
517
- errorMessages.push(errorMsg);
518
- console.error(errorMsg);
519
- }
520
- }
521
-
522
- // Update VS Code Insiders settings if VS Code Insiders is installed
523
- if (isVSCodeInsidersInstalled) {
524
- try {
525
- Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
526
- // Pass the modified installConfig
527
- await this.updateGithubCopilotSettings(insidersSettingPath, this.serverName, installConfig);
528
- insidersSuccess = true;
529
- Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
530
- } catch (error) {
531
- const errorMsg = `Error updating VS Code Insiders settings: ${error instanceof Error ? error.message : String(error)}`;
532
- errorMessages.push(errorMsg);
533
- console.error(errorMsg);
534
- }
535
- }
536
- } else {
537
- Logger.debug(`No implementation exists for updating settings for client: ${clientName}`);
538
- return {
539
- success: false,
540
- message: `Client ${clientName} is supported but no implementation exists for updating its settings`
541
- };
542
- }
543
-
544
- // Determine overall success status and message
545
- const overallSuccess = regularSuccess || insidersSuccess;
546
- let message = '';
547
-
548
- if (overallSuccess) {
549
- const successDetails = [];
550
- if (regularSuccess) successDetails.push('VS Code');
551
- if (insidersSuccess) successDetails.push('VS Code Insiders');
552
-
553
- // Create a more compact success message with specific paths
554
- const pathDetails = [];
555
- if (regularSuccess) {
556
- pathDetails.push(`\n[VS Code]: ${regularSettingPath}`);
557
- }
558
- if (insidersSuccess) {
559
- pathDetails.push(`\n[VS Code Insiders]: ${insidersSettingPath}`);
560
- }
561
- message = `Successfully installed ${this.serverName} for client: ${clientName}. ${pathDetails.join(' ')}`;
562
-
563
- // Add partial failure information if applicable
564
- if (errorMessages.length > 0) {
565
- message += `\nHowever, some errors occurred:\n${errorMessages.join('\n- ')}`;
566
- }
567
- Logger.debug(`Installation complete: ${message}`);
568
- } else {
569
- // Create a more detailed failure message that includes the detection results
570
- const detectionInfo = [];
571
- if (!isVSCodeInstalled) detectionInfo.push('VS Code not detected');
572
- if (!isVSCodeInsidersInstalled) detectionInfo.push('VS Code Insiders not detected');
573
-
574
- if (errorMessages.length > 0) {
575
- message = `Failed to install ${this.serverName} for client: ${clientName}.\nDetection status: [${detectionInfo.join(', ')}].\nErrors:\n- ${errorMessages.join('\n- ')}`;
576
- } else {
577
- message = `Failed to install ${this.serverName} for client: ${clientName}.\nDetection status: [${detectionInfo.join(', ')}]`;
578
- }
579
- console.error(`Installation failed: ${message}`);
580
- }
581
-
582
- return {
583
- success: overallSuccess,
584
- message: message
585
- };
586
- } catch (error) {
587
- const errorMsg = `Error installing client ${clientName}: ${error instanceof Error ? error.message : String(error)}`;
588
- console.error(errorMsg);
589
- return {
590
- success: false,
591
- message: errorMsg
592
- };
593
- }
594
- }
595
-
596
- private async updateClineOrMSRooSettings(
597
- settingPath: string,
598
- serverName: string,
599
- installConfig: any, // Use the processed installConfig
600
- clientName: string
601
- ): Promise<void> {
602
- // Read the Cline/MSRoo settings file
603
- const settings = await readJsonFile(settingPath, true);
604
-
605
- // Initialize mcpServers section if it doesn't exist
606
- if (!settings.mcpServers) {
607
- settings.mcpServers = {};
608
- }
609
-
610
- if (installConfig.mode === 'stdio') {
611
- // Special handling for Windows when command is npx for Cline and MSROO clients
612
- // Use a copy to avoid modifying the passed installConfig directly if needed elsewhere
613
- const serverConfigForClient = { ...installConfig };
614
- if (process.platform === 'win32' &&
615
- serverConfigForClient.command === 'npx' &&
616
- (clientName === 'Cline' || clientName === 'MSRooCode' || clientName === 'MSROO')) {
617
- // Update command to cmd
618
- serverConfigForClient.command = 'cmd';
619
-
620
- // Add /c and npx at the beginning of args
621
- serverConfigForClient.args = ['/c', 'npx', ...serverConfigForClient.args];
622
-
623
- // Add APPDATA environment variable pointing to npm directory
624
- if (!serverConfigForClient.env) {
625
- serverConfigForClient.env = {};
626
- }
627
-
628
- // Dynamically get npm path and set APPDATA to it
629
- const npmPath = await this.getNpmPath();
630
- serverConfigForClient.env['APPDATA'] = npmPath;
631
- Logger.debug(`Windows npx fix: command=${serverConfigForClient.command}, args=${serverConfigForClient.args.join(' ')}, env=${JSON.stringify(serverConfigForClient.env)}`);
632
- }
633
- // Convert backslashes to forward slashes in args paths
634
- if (serverConfigForClient.args) {
635
- serverConfigForClient.args = serverConfigForClient.args.map((arg: string) =>
636
- typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg
637
- );
638
- }
639
-
640
- // Add or update the server configuration
641
- settings.mcpServers[serverName] = {
642
- command: serverConfigForClient.command,
643
- args: serverConfigForClient.args,
644
- env: serverConfigForClient.env,
645
- autoApprove: [],
646
- disabled: false,
647
- alwaysAllow: []
648
- };
649
- Logger.debug(`Updating ${settingPath} for ${serverName}: ${JSON.stringify(settings.mcpServers[serverName])}`);
650
- } else if (installConfig.mode === 'sse') {
651
- // Handle SSE mode for Cline and MSRoo clients
652
- settings.mcpServers[serverName] = {
653
- type: 'sse',
654
- url: installConfig.url
655
- };
656
- }
657
-
658
- // Write the updated settings back to the file
659
- await writeJsonFile(settingPath, settings);
660
- }
661
-
662
- private async updateGithubCopilotSettings(
663
- settingPath: string,
664
- serverName: string,
665
- installConfig: any // Use the processed installConfig
666
- ): Promise<void> {
667
- // Read the VS Code settings.json file
668
- const settings = await readJsonFile(settingPath, true);
669
-
670
- // Initialize the mcp section if it doesn't exist
671
- if (!settings.mcp) {
672
- settings.mcp = {
673
- servers: {},
674
- inputs: []
675
- };
676
- }
677
-
678
- if (!settings.mcp.servers) {
679
- settings.mcp.servers = {};
680
- }
681
-
682
- // Use a copy to avoid modifying the passed installConfig directly
683
- const serverConfigForClient = { ...installConfig };
684
-
685
- if (installConfig.mode === 'stdio') {
686
- if (serverConfigForClient.args) {
687
- serverConfigForClient.args = serverConfigForClient.args.map((arg: string) =>
688
- typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg
689
- );
690
- }
691
-
692
- // Add or update the server configuration
693
- settings.mcp.servers[serverName] = {
694
- command: serverConfigForClient.command,
695
- args: serverConfigForClient.args,
696
- env: serverConfigForClient.env
697
- };
698
- Logger.debug(`Updating ${settingPath} for ${serverName}: ${JSON.stringify(settings.mcp.servers[serverName])}`);
699
- }
700
- else if (installConfig.mode === 'sse') {
701
- // Handle SSE mode for Github Copilot
702
- settings.mcp.servers[serverName] = {
703
- type: 'sse',
704
- url: installConfig.url
705
- };
706
- }
707
-
708
- // Write the updated settings back to the file
709
- await writeJsonFile(settingPath, settings);
710
- }
711
-
199
+ /**
200
+ * Install all specified clients
201
+ */
712
202
  async install(options: ServerInstallOptions): Promise<ServerOperationResult> {
713
203
  const initialStatuses: OperationStatus[] = [];
714
204
 
715
- // Start installation for each client asynchronously and collect initial statuses
716
- // Pass options down to installClient
205
+ // Start installation for each client asynchronously
717
206
  const installPromises = this.clients.map(async (clientName) => {
718
- const initialStatus = await this.installClient(clientName, options);
719
- initialStatuses.push(initialStatus);
720
- return initialStatus;
207
+ const status = await this.installClient(clientName, options);
208
+ initialStatuses.push(status);
209
+ return status;
721
210
  });
722
211
 
723
- // Wait for all initial statuses (but actual installation continues asynchronously)
212
+ // Wait for all installations to complete
724
213
  await Promise.all(installPromises);
725
214
 
726
- // Return initial result showing installations have been initiated
215
+ // Return result
727
216
  return {
728
217
  success: true,
729
- message: 'Client installations initiated successfully',
218
+ message: 'Client installations completed',
730
219
  status: initialStatuses
731
220
  };
732
221
  }