imcp 0.0.3 → 0.0.5

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 (93) hide show
  1. package/README.md +5 -6
  2. package/dist/cli/commands/install.js +2 -0
  3. package/dist/cli/commands/list.js +1 -0
  4. package/dist/cli/index.js +1 -2
  5. package/dist/core/ConfigurationLoader.d.ts +32 -0
  6. package/dist/core/ConfigurationLoader.js +213 -0
  7. package/dist/core/ConfigurationProvider.d.ts +2 -3
  8. package/dist/core/ConfigurationProvider.js +13 -182
  9. package/dist/core/InstallationService.d.ts +8 -0
  10. package/dist/core/InstallationService.js +124 -96
  11. package/dist/core/RequirementService.d.ts +1 -1
  12. package/dist/core/RequirementService.js +5 -9
  13. package/dist/core/constants.js +14 -1
  14. package/dist/core/installers/BaseInstaller.d.ts +5 -4
  15. package/dist/core/installers/BaseInstaller.js +17 -28
  16. package/dist/core/installers/ClientInstaller.js +159 -39
  17. package/dist/core/installers/CommandInstaller.d.ts +1 -0
  18. package/dist/core/installers/CommandInstaller.js +3 -0
  19. package/dist/core/installers/GeneralInstaller.d.ts +1 -0
  20. package/dist/core/installers/GeneralInstaller.js +3 -0
  21. package/dist/core/installers/InstallerFactory.d.ts +9 -7
  22. package/dist/core/installers/InstallerFactory.js +10 -8
  23. package/dist/core/installers/NpmInstaller.d.ts +1 -0
  24. package/dist/core/installers/NpmInstaller.js +3 -0
  25. package/dist/core/installers/PipInstaller.d.ts +6 -3
  26. package/dist/core/installers/PipInstaller.js +21 -8
  27. package/dist/core/installers/RequirementInstaller.d.ts +4 -3
  28. package/dist/core/installers/clients/ClientInstaller.d.ts +23 -0
  29. package/dist/core/installers/clients/ClientInstaller.js +573 -0
  30. package/dist/core/installers/clients/ExtensionInstaller.d.ts +26 -0
  31. package/dist/core/installers/clients/ExtensionInstaller.js +149 -0
  32. package/dist/core/installers/index.d.ts +8 -6
  33. package/dist/core/installers/index.js +8 -6
  34. package/dist/core/installers/requirements/BaseInstaller.d.ts +59 -0
  35. package/dist/core/installers/requirements/BaseInstaller.js +168 -0
  36. package/dist/core/installers/requirements/CommandInstaller.d.ts +37 -0
  37. package/dist/core/installers/requirements/CommandInstaller.js +173 -0
  38. package/dist/core/installers/requirements/GeneralInstaller.d.ts +33 -0
  39. package/dist/core/installers/requirements/GeneralInstaller.js +86 -0
  40. package/dist/core/installers/requirements/InstallerFactory.d.ts +54 -0
  41. package/dist/core/installers/requirements/InstallerFactory.js +97 -0
  42. package/dist/core/installers/requirements/NpmInstaller.d.ts +26 -0
  43. package/dist/core/installers/requirements/NpmInstaller.js +128 -0
  44. package/dist/core/installers/requirements/PipInstaller.d.ts +28 -0
  45. package/dist/core/installers/requirements/PipInstaller.js +128 -0
  46. package/{src/core/installers/RequirementInstaller.ts → dist/core/installers/requirements/RequirementInstaller.d.ts} +33 -38
  47. package/dist/core/installers/requirements/RequirementInstaller.js +3 -0
  48. package/dist/core/types.d.ts +4 -1
  49. package/dist/services/ServerService.js +1 -1
  50. package/dist/utils/clientUtils.d.ts +0 -6
  51. package/dist/utils/clientUtils.js +3 -2
  52. package/dist/utils/githubUtils.d.ts +11 -0
  53. package/dist/utils/githubUtils.js +88 -0
  54. package/dist/utils/osUtils.d.ts +17 -0
  55. package/dist/utils/osUtils.js +184 -0
  56. package/dist/web/public/css/modal.css +97 -3
  57. package/dist/web/public/index.html +21 -2
  58. package/dist/web/public/js/modal.js +177 -28
  59. package/dist/web/public/js/serverCategoryDetails.js +12 -10
  60. package/dist/web/public/js/serverCategoryList.js +20 -5
  61. package/dist/web/public/modal.html +27 -13
  62. package/dist/web/server.js +1 -1
  63. package/package.json +2 -1
  64. package/src/cli/commands/install.ts +4 -2
  65. package/src/cli/commands/list.ts +1 -0
  66. package/src/cli/index.ts +1 -1
  67. package/src/core/ConfigurationLoader.ts +251 -0
  68. package/src/core/ConfigurationProvider.ts +13 -195
  69. package/src/core/InstallationService.ts +140 -106
  70. package/src/core/RequirementService.ts +5 -10
  71. package/src/core/constants.ts +15 -1
  72. package/src/core/installers/{ClientInstaller.ts → clients/ClientInstaller.ts} +185 -46
  73. package/src/core/installers/clients/ExtensionInstaller.ts +162 -0
  74. package/src/core/installers/index.ts +9 -7
  75. package/src/core/installers/{BaseInstaller.ts → requirements/BaseInstaller.ts} +10 -118
  76. package/src/core/installers/{CommandInstaller.ts → requirements/CommandInstaller.ts} +7 -3
  77. package/src/core/installers/{GeneralInstaller.ts → requirements/GeneralInstaller.ts} +6 -2
  78. package/src/core/installers/{InstallerFactory.ts → requirements/InstallerFactory.ts} +11 -9
  79. package/src/core/installers/{NpmInstaller.ts → requirements/NpmInstaller.ts} +7 -4
  80. package/src/core/installers/{PipInstaller.ts → requirements/PipInstaller.ts} +26 -10
  81. package/src/core/installers/requirements/RequirementInstaller.ts +41 -0
  82. package/src/core/types.ts +4 -1
  83. package/src/services/ServerService.ts +1 -1
  84. package/src/utils/clientUtils.ts +4 -2
  85. package/src/utils/githubUtils.ts +103 -0
  86. package/src/utils/osUtils.ts +206 -15
  87. package/src/web/public/css/modal.css +97 -3
  88. package/src/web/public/index.html +21 -2
  89. package/src/web/public/js/modal.js +177 -28
  90. package/src/web/public/js/serverCategoryDetails.js +12 -10
  91. package/src/web/public/js/serverCategoryList.js +20 -5
  92. package/src/web/public/modal.html +27 -13
  93. package/src/web/server.ts +1 -1
@@ -1,3 +1,4 @@
1
+ import { ExtensionInstaller } from './ExtensionInstaller.js';
1
2
  import {
2
3
  ServerOperationResult,
3
4
  OperationStatus,
@@ -6,13 +7,18 @@ import {
6
7
  ServerInstallOptions,
7
8
  FeedConfiguration,
8
9
  McpConfig
9
- } from '../types.js';
10
- import { ConfigurationProvider } from '../ConfigurationProvider.js';
11
- import { SUPPORTED_CLIENTS } from '../constants.js';
12
- import { resolveNpmModulePath, readJsonFile, writeJsonFile } from '../../utils/clientUtils.js';
10
+ } 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';
13
15
  import { exec } from 'child_process';
14
16
  import { promisify } from 'util';
15
- import { Logger } from '../../utils/logger.js';
17
+ import * as path from 'path';
18
+ 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
16
22
 
17
23
  export class ClientInstaller {
18
24
  private configProvider: ConfigurationProvider;
@@ -32,7 +38,6 @@ export class ClientInstaller {
32
38
  }
33
39
 
34
40
  private async getNpmPath(): Promise<string> {
35
- const execAsync = promisify(exec);
36
41
  try {
37
42
  // Execute the get-command npm command to find the npm path
38
43
  const { stdout } = await execAsync('powershell -Command "get-command npm | Select-Object -ExpandProperty Source"');
@@ -47,28 +52,48 @@ export class ClientInstaller {
47
52
  }
48
53
  }
49
54
 
55
+
50
56
  /**
51
57
  * Check if a command is available on the system
52
58
  * @param command The command to check
53
59
  * @returns True if the command is available, false otherwise
54
60
  */
55
61
  private async isCommandAvailable(command: string): Promise<boolean> {
56
- const execAsync = promisify(exec);
57
62
  try {
58
63
  if (process.platform === 'win32') {
59
64
  // Windows-specific command check
60
65
  await execAsync(`where ${command}`);
66
+ } else if (process.platform === 'darwin' && (command === 'code' || command === 'code-insiders')) {
67
+ // macOS-specific VS Code check
68
+ const vscodePath = command === 'code' ?
69
+ '/Applications/Visual Studio Code.app' :
70
+ '/Applications/Visual Studio Code - Insiders.app';
71
+ await execAsync(`test -d "${vscodePath}"`);
61
72
  } else {
62
73
  // Unix-like systems
63
74
  await execAsync(`which ${command}`);
64
75
  }
65
76
  return true;
66
77
  } catch (error) {
78
+ if (process.platform === 'darwin' && (command === 'code' || command === 'code-insiders')) {
79
+ // Try checking in ~/Applications as well for macOS
80
+ try {
81
+ const homedir = process.env.HOME;
82
+ const vscodePath = command === 'code' ?
83
+ `${homedir}/Applications/Visual Studio Code.app` :
84
+ `${homedir}/Applications/Visual Studio Code - Insiders.app`;
85
+ await execAsync(`test -d "${vscodePath}"`);
86
+ return true;
87
+ } catch (error) {
88
+ return false;
89
+ }
90
+ }
67
91
  return false;
68
92
  }
69
93
  }
70
94
 
71
- private async installClient(clientName: string, env?: Record<string, string>): Promise<OperationStatus> {
95
+ // Modified to accept ServerInstallOptions
96
+ private async installClient(clientName: string, options: ServerInstallOptions): Promise<OperationStatus> {
72
97
  // Check if client is supported
73
98
  if (!SUPPORTED_CLIENTS[clientName]) {
74
99
  return {
@@ -99,14 +124,18 @@ export class ClientInstaller {
99
124
  );
100
125
 
101
126
  // Start the asynchronous installation process without awaiting it
102
- this.processInstallation(clientName, operationId, env);
127
+ // Pass options down
128
+ this.processInstallation(clientName, operationId, options);
103
129
 
104
130
  // Return the initial status immediately
105
131
  return initialStatus;
106
132
  }
107
133
 
108
- private async processInstallation(clientName: string, operationId: string, env?: Record<string, string>): Promise<void> {
134
+ // Modified to accept ServerInstallOptions
135
+ private async processInstallation(clientName: string, operationId: string, options: ServerInstallOptions): Promise<void> {
109
136
  try {
137
+ await ExtensionInstaller.installExtension(clientName);
138
+
110
139
  // Check requirements before installation
111
140
  let requirementsReady = await this.configProvider.isRequirementsReady(this.categoryName, this.serverName);
112
141
 
@@ -217,8 +246,16 @@ export class ClientInstaller {
217
246
  }
218
247
 
219
248
  try {
220
- // Install client-specific configuration
221
- const result = await this.installClientConfig(clientName, env || {}, serverConfig, feedConfiguration);
249
+ // Install extension if available
250
+ if (SUPPORTED_CLIENTS[clientName]?.extension) {
251
+ const extensionResult = await ExtensionInstaller.installExtension(clientName);
252
+ if (!extensionResult) {
253
+ Logger.debug(`Failed to install extension for client ${clientName}`);
254
+ }
255
+ }
256
+
257
+ // Install client-specific configuration, passing options down
258
+ const result = await this.installClientConfig(clientName, options, serverConfig, feedConfiguration);
222
259
 
223
260
  const finalStatus: OperationStatus = {
224
261
  status: result.success ? 'completed' : 'failed',
@@ -236,6 +273,10 @@ export class ClientInstaller {
236
273
  finalStatus
237
274
  );
238
275
 
276
+ if (result.success) {
277
+ await this.configProvider.reloadClientMCPSettings();
278
+ }
279
+
239
280
  } catch (error) {
240
281
  const errorStatus: OperationStatus = {
241
282
  status: 'failed',
@@ -272,9 +313,10 @@ export class ClientInstaller {
272
313
  }
273
314
  }
274
315
 
316
+ // Modified to accept ServerInstallOptions
275
317
  private async installClientConfig(
276
318
  clientName: string,
277
- env: Record<string, string>,
319
+ options: ServerInstallOptions, // Use options directly
278
320
  serverConfig: McpConfig,
279
321
  feedConfig: FeedConfiguration
280
322
  ): Promise<{ success: boolean; message: string }> {
@@ -299,7 +341,6 @@ export class ClientInstaller {
299
341
  const isVSCodeInsidersInstalled = await this.isCommandAvailable('code-insiders');
300
342
  Logger.debug(isVSCodeInstalled ? 'VS Code detected on system' : 'VS Code not detected on system');
301
343
  Logger.debug(isVSCodeInsidersInstalled ? 'VS Code Insiders detected on system' : 'VS Code Insiders not detected on system');
302
- Logger.debug(`VS Code Insiders installed: ${isVSCodeInsidersInstalled}`);
303
344
 
304
345
  // If neither is installed, we can't proceed
305
346
  if (!isVSCodeInstalled && !isVSCodeInsidersInstalled) {
@@ -310,29 +351,102 @@ export class ClientInstaller {
310
351
  };
311
352
  }
312
353
 
313
- // Clone the installation configuration to make modifications
354
+ // --- Start of new logic ---
355
+
356
+ // Clone the base installation configuration to avoid modifying the original serverConfig
314
357
  const installConfig = JSON.parse(JSON.stringify(serverConfig.installation));
358
+ const pythonEnv = options.settings?.pythonEnv;
359
+ let pythonDir: string | null = null;
360
+
361
+ // 1. Determine which args to use and resolve npm paths
362
+ let finalArgs: string[] = [];
363
+ if (options.args && options.args.length > 0) {
364
+ Logger.debug(`Using args from ServerInstallOptions: ${options.args.join(' ')}`);
365
+ finalArgs = options.args.map(arg => resolveNpmModulePath(arg));
366
+ } else if (installConfig.args && installConfig.args.length > 0) {
367
+ Logger.debug(`Using args from serverConfig.installation: ${installConfig.args.join(' ')}`);
368
+ finalArgs = installConfig.args.map((arg: string) => resolveNpmModulePath(arg));
369
+ } else {
370
+ Logger.debug('No args found in options or serverConfig.installation.');
371
+ finalArgs = [];
372
+ }
373
+
374
+ // 2. Handle pythonEnv settings
375
+ if (pythonEnv) {
376
+ // 2.1 If pythonEnv is set
377
+ Logger.debug(`Python environment specified: ${pythonEnv}`);
378
+ pythonDir = getPythonPackagePath(pythonEnv);
315
379
 
316
- // Replace template variables in args
317
- installConfig.args = installConfig.args.map((arg: string) =>
318
- resolveNpmModulePath(arg));
319
-
320
- // Add environment variables from options
321
- installConfig.env = {};
322
- if (serverConfig.installation.env) {
323
- // Add default env variables from config
324
- for (const [key, config] of Object.entries(serverConfig.installation.env)) {
325
- const envConfig = config as any; // Type assertion for dynamic access
326
- if (envConfig.Default) {
327
- installConfig.env[key] = envConfig.Default;
380
+ // 2.1.1 Replace ${PYTHON_PACKAGE} in args
381
+ if (pythonDir) {
382
+ finalArgs = finalArgs.map(arg => arg.includes('${PYTHON_PACKAGE}') ? arg.replace('${PYTHON_PACKAGE}', pythonDir!) : arg);
383
+ Logger.debug(`Args after PYTHON_PACKAGE replacement (using pythonEnv): ${finalArgs.join(' ')}`);
384
+ } else {
385
+ Logger.debug(`Could not determine directory for pythonEnv: ${pythonEnv}`);
386
+ }
387
+
388
+ // 2.1.2 Replace command if it's 'python'
389
+ if (installConfig.command === 'python') {
390
+ Logger.debug(`Replacing command 'python' with specified pythonEnv: ${pythonEnv}`);
391
+ installConfig.command = pythonEnv;
392
+ }
393
+ } else {
394
+ // 2.2 If pythonEnv is not set
395
+ Logger.debug('No Python environment specified in settings.');
396
+ // 2.2.1 Replace ${PYTHON_PACKAGE} with system python directory if needed
397
+ if (finalArgs.some(arg => arg.includes('${PYTHON_PACKAGE}'))) {
398
+ Logger.debug('Attempting to find system Python directory for ${PYTHON_PACKAGE} replacement.');
399
+ pythonDir = await getSystemPythonPackageDirectory();
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 system python): ${finalArgs.join(' ')}`);
403
+ } else {
404
+ Logger.debug('Could not find system Python directory. ${PYTHON_PACKAGE} replacement skipped.');
405
+ // Optionally, remove or handle the arg containing ${PYTHON_PACKAGE} if Python is required
406
+ // finalArgs = finalArgs.filter(arg => !arg.includes('${PYTHON_PACKAGE}'));
328
407
  }
329
408
  }
330
409
  }
331
410
 
332
- // Override with provided env variables
333
- if (env) {
334
- Object.assign(installConfig.env, env);
411
+ // Update installConfig with potentially modified args
412
+ installConfig.args = finalArgs;
413
+
414
+
415
+ // 3. Handle environment variables (merge default, serverConfig, and options.env)
416
+ const baseEnv = serverConfig.installation.env || {};
417
+ const defaultEnv: Record<string, string> = {};
418
+ for (const [key, config] of Object.entries(baseEnv)) {
419
+ const envConfig = config as any; // Type assertion
420
+ if (envConfig.Default) {
421
+ defaultEnv[key] = envConfig.Default;
422
+ }
335
423
  }
424
+ // Merge: options.env overrides defaultEnv
425
+ installConfig.env = { ...defaultEnv, ...(options.env || {}) };
426
+
427
+ // Replace ${BROWSER_PATH} with actual browser path
428
+ const replacements = Object.entries(installConfig.env)
429
+ .filter(([_, value]) => typeof value === 'string' && value.includes('${BROWSER_PATH}'))
430
+ .map(async ([key, value]) => {
431
+ try {
432
+ const browserPath = await GetBrowserPath();
433
+ return [key, (value as string).replace('${BROWSER_PATH}', browserPath)] as [string, string];
434
+ } catch (error) {
435
+ Logger.error(`Failed to get system browser path for env var ${key}:`, error);
436
+ return [key, value] as [string, string];
437
+ }
438
+ });
439
+
440
+ // Wait for all replacements to complete
441
+ const replacedValues = await Promise.all(replacements);
442
+ replacedValues.forEach(([key, value]) => {
443
+ installConfig.env[key] = value;
444
+ });
445
+
446
+ Logger.debug(`Final environment variables: ${JSON.stringify(installConfig.env)}`);
447
+
448
+ // --- End of new logic ---
449
+
336
450
 
337
451
  // Keep track of success for both installations
338
452
  let regularSuccess = false;
@@ -345,6 +459,7 @@ export class ClientInstaller {
345
459
  if (isVSCodeInstalled) {
346
460
  try {
347
461
  Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
462
+ // Pass the modified installConfig
348
463
  await this.updateClineOrMSRooSettings(regularSettingPath, this.serverName, installConfig, clientName);
349
464
  regularSuccess = true;
350
465
  Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
@@ -359,6 +474,7 @@ export class ClientInstaller {
359
474
  if (isVSCodeInsidersInstalled) {
360
475
  try {
361
476
  Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
477
+ // Pass the modified installConfig
362
478
  await this.updateClineOrMSRooSettings(insidersSettingPath, this.serverName, installConfig, clientName);
363
479
  insidersSuccess = true;
364
480
  Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
@@ -373,6 +489,7 @@ export class ClientInstaller {
373
489
  if (isVSCodeInstalled) {
374
490
  try {
375
491
  Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
492
+ // Pass the modified installConfig
376
493
  await this.updateGithubCopilotSettings(regularSettingPath, this.serverName, installConfig);
377
494
  regularSuccess = true;
378
495
  Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
@@ -387,6 +504,7 @@ export class ClientInstaller {
387
504
  if (isVSCodeInsidersInstalled) {
388
505
  try {
389
506
  Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
507
+ // Pass the modified installConfig
390
508
  await this.updateGithubCopilotSettings(insidersSettingPath, this.serverName, installConfig);
391
509
  insidersSuccess = true;
392
510
  Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
@@ -459,7 +577,7 @@ export class ClientInstaller {
459
577
  private async updateClineOrMSRooSettings(
460
578
  settingPath: string,
461
579
  serverName: string,
462
- installConfig: any,
580
+ installConfig: any, // Use the processed installConfig
463
581
  clientName: string
464
582
  ): Promise<void> {
465
583
  // Read the Cline/MSRoo settings file
@@ -471,35 +589,44 @@ export class ClientInstaller {
471
589
  }
472
590
 
473
591
  // Special handling for Windows when command is npx for Cline and MSROO clients
474
- const serverConfig = { ...installConfig };
592
+ // Use a copy to avoid modifying the passed installConfig directly if needed elsewhere
593
+ const serverConfigForClient = { ...installConfig };
475
594
  if (process.platform === 'win32' &&
476
- serverConfig.command === 'npx' &&
595
+ serverConfigForClient.command === 'npx' &&
477
596
  (clientName === 'Cline' || clientName === 'MSRooCode' || clientName === 'MSROO')) {
478
597
  // Update command to cmd
479
- serverConfig.command = 'cmd';
598
+ serverConfigForClient.command = 'cmd';
480
599
 
481
600
  // Add /c and npx at the beginning of args
482
- serverConfig.args = ['/c', 'npx', ...serverConfig.args];
601
+ serverConfigForClient.args = ['/c', 'npx', ...serverConfigForClient.args];
483
602
 
484
603
  // Add APPDATA environment variable pointing to npm directory
485
- if (!serverConfig.env) {
486
- serverConfig.env = {};
604
+ if (!serverConfigForClient.env) {
605
+ serverConfigForClient.env = {};
487
606
  }
488
607
 
489
608
  // Dynamically get npm path and set APPDATA to it
490
609
  const npmPath = await this.getNpmPath();
491
- serverConfig.env['APPDATA'] = npmPath;
610
+ serverConfigForClient.env['APPDATA'] = npmPath;
611
+ Logger.debug(`Windows npx fix: command=${serverConfigForClient.command}, args=${serverConfigForClient.args.join(' ')}, env=${JSON.stringify(serverConfigForClient.env)}`);
612
+ }
613
+ // Convert backslashes to forward slashes in args paths
614
+ if (serverConfigForClient.args) {
615
+ serverConfigForClient.args = serverConfigForClient.args.map((arg: string) =>
616
+ typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg
617
+ );
492
618
  }
493
619
 
494
620
  // Add or update the server configuration
495
621
  settings.mcpServers[serverName] = {
496
- command: serverConfig.command,
497
- args: serverConfig.args,
498
- env: serverConfig.env,
622
+ command: serverConfigForClient.command,
623
+ args: serverConfigForClient.args,
624
+ env: serverConfigForClient.env,
499
625
  autoApprove: [],
500
626
  disabled: false,
501
627
  alwaysAllow: []
502
628
  };
629
+ Logger.debug(`Updating ${settingPath} for ${serverName}: ${JSON.stringify(settings.mcpServers[serverName])}`);
503
630
 
504
631
  // Write the updated settings back to the file
505
632
  await writeJsonFile(settingPath, settings);
@@ -508,7 +635,7 @@ export class ClientInstaller {
508
635
  private async updateGithubCopilotSettings(
509
636
  settingPath: string,
510
637
  serverName: string,
511
- installConfig: any
638
+ installConfig: any // Use the processed installConfig
512
639
  ): Promise<void> {
513
640
  // Read the VS Code settings.json file
514
641
  const settings = await readJsonFile(settingPath, true);
@@ -525,12 +652,23 @@ export class ClientInstaller {
525
652
  settings.mcp.servers = {};
526
653
  }
527
654
 
655
+ // Use a copy to avoid modifying the passed installConfig directly
656
+ const serverConfigForClient = { ...installConfig };
657
+
658
+ // Convert backslashes to forward slashes in args paths
659
+ if (serverConfigForClient.args) {
660
+ serverConfigForClient.args = serverConfigForClient.args.map((arg: string) =>
661
+ typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg
662
+ );
663
+ }
664
+
528
665
  // Add or update the server configuration
529
666
  settings.mcp.servers[serverName] = {
530
- command: installConfig.command,
531
- args: installConfig.args,
532
- env: installConfig.env
667
+ command: serverConfigForClient.command,
668
+ args: serverConfigForClient.args,
669
+ env: serverConfigForClient.env
533
670
  };
671
+ Logger.debug(`Updating ${settingPath} for ${serverName}: ${JSON.stringify(settings.mcp.servers[serverName])}`);
534
672
 
535
673
  // Write the updated settings back to the file
536
674
  await writeJsonFile(settingPath, settings);
@@ -540,8 +678,9 @@ export class ClientInstaller {
540
678
  const initialStatuses: OperationStatus[] = [];
541
679
 
542
680
  // Start installation for each client asynchronously and collect initial statuses
681
+ // Pass options down to installClient
543
682
  const installPromises = this.clients.map(async (clientName) => {
544
- const initialStatus = await this.installClient(clientName, options.env || {});
683
+ const initialStatus = await this.installClient(clientName, options);
545
684
  initialStatuses.push(initialStatus);
546
685
  return initialStatus;
547
686
  });
@@ -0,0 +1,162 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { SUPPORTED_CLIENTS } from '../../constants.js';
4
+ import { Logger } from '../../../utils/logger.js';
5
+ import { handleGitHubRelease } from '../../../utils/githubUtils.js';
6
+ import { compareVersions } from '../../../utils/versionUtils.js';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ interface ExtensionInfo {
11
+ name: string;
12
+ publisher: string;
13
+ version: string;
14
+ }
15
+
16
+ export class ExtensionInstaller {
17
+ /**
18
+ * Get VSCode path based on the OS type
19
+ */
20
+ private static async getVSCodePath(isInsiders: boolean): Promise<string | null> {
21
+ const command = isInsiders ? 'code-insiders' : 'code';
22
+ try {
23
+ if (process.platform === 'win32') {
24
+ // Windows: Check command availability first
25
+ await execAsync(`where ${command}`);
26
+ return command;
27
+ } else if (process.platform === 'darwin') {
28
+ // macOS: Check in both system and user Applications
29
+ const appName = isInsiders ? 'Visual Studio Code - Insiders.app' : 'Visual Studio Code.app';
30
+ const systemPath = `/Applications/${appName}`;
31
+ const userPath = `${process.env.HOME}/Applications/${appName}`;
32
+
33
+ try {
34
+ await execAsync(`test -d "${systemPath}"`);
35
+ return systemPath;
36
+ } catch {
37
+ try {
38
+ await execAsync(`test -d "${userPath}"`);
39
+ return userPath;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ }
45
+ return command;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * List installed extensions for VSCode or VSCode Insiders
53
+ */
54
+ private static async listExtensions(isInsiders: boolean): Promise<ExtensionInfo[]> {
55
+ const command = isInsiders ? 'code-insiders' : 'code';
56
+ try {
57
+ const { stdout } = await execAsync(`${command} --list-extensions --show-versions`);
58
+ return stdout.split('\n')
59
+ .filter(line => line.trim())
60
+ .map(line => {
61
+ const [extension, version] = line.split('@');
62
+ const [publisher, name] = extension.split('.');
63
+ return { name, publisher, version: version || '' };
64
+ });
65
+ } catch (error) {
66
+ Logger.error(`Failed to list extensions for ${command}:`, error);
67
+ return [];
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if an extension is installed and get its version
73
+ */
74
+ private static async checkExtension(extensionId: string, isInsiders: boolean): Promise<string | null> {
75
+ const extensions = await this.listExtensions(isInsiders);
76
+ const [publisher, name] = extensionId.split('.');
77
+ const extension = extensions.find(ext =>
78
+ ext.publisher.toLowerCase() === publisher.toLowerCase() &&
79
+ ext.name.toLowerCase() === name.toLowerCase()
80
+ );
81
+ return extension ? extension.version : null;
82
+ }
83
+
84
+ /**
85
+ * Install extension from marketplace
86
+ */
87
+ private static async installPublicExtension(extensionId: string, isInsiders: boolean): Promise<boolean> {
88
+ const command = isInsiders ? 'code-insiders' : 'code';
89
+ try {
90
+ await execAsync(`${command} --install-extension ${extensionId}`);
91
+ return true;
92
+ } catch (error) {
93
+ Logger.error(`Failed to install extension ${extensionId}:`, error);
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Install extension from VSIX file
100
+ */
101
+ private static async installPrivateExtension(vsixPath: string, isInsiders: boolean): Promise<boolean> {
102
+ const command = isInsiders ? 'code-insiders' : 'code';
103
+ try {
104
+ await execAsync(`${command} --install-extension "${vsixPath}"`);
105
+ return true;
106
+ } catch (error) {
107
+ Logger.error(`Failed to install extension from VSIX ${vsixPath}:`, error);
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Install extension for a specific client
114
+ */
115
+ public static async installExtension(clientName: string): Promise<boolean> {
116
+ const client = SUPPORTED_CLIENTS[clientName];
117
+ if (!client?.extension?.extensionId) {
118
+ Logger.error(`No extension configuration found for client ${clientName}`);
119
+ return false;
120
+ }
121
+
122
+ const { extensionId, leastVersion, repository, assetName, private: isPrivate } = client.extension;
123
+ let success = false;
124
+
125
+ // Check both VSCode and VSCode Insiders
126
+ for (const isInsiders of [false, true]) {
127
+ const vscodePath = await this.getVSCodePath(isInsiders);
128
+ if (!vscodePath) {
129
+ Logger.debug(`${isInsiders ? 'VSCode Insiders' : 'VSCode'} not found, skipping...`);
130
+ continue;
131
+ }
132
+
133
+ const currentVersion = await this.checkExtension(extensionId, isInsiders);
134
+
135
+ if (!currentVersion || (isPrivate && leastVersion && compareVersions(currentVersion, leastVersion) < 0)) {
136
+ // Extension not installed or needs update (for private extensions)
137
+ if (!isPrivate) {
138
+ // Install public extension from marketplace
139
+ success = await this.installPublicExtension(extensionId, isInsiders);
140
+ } else {
141
+ // Install private extension from GitHub release using latest version
142
+ try {
143
+ const { resolvedPath } = await handleGitHubRelease(
144
+ { name: extensionId, version: 'latest', type: 'extension' },
145
+ { repository, assetName }
146
+ );
147
+ success = await this.installPrivateExtension(resolvedPath, isInsiders);
148
+ } catch (error) {
149
+ Logger.error(`Failed to install/update private extension ${extensionId}:`, error);
150
+ continue;
151
+ }
152
+ }
153
+ } else {
154
+ // Extension already installed and up to date
155
+ Logger.debug(`Extension ${extensionId} is already installed and up to date`);
156
+ success = true;
157
+ }
158
+ }
159
+
160
+ return success;
161
+ }
162
+ }
@@ -1,9 +1,11 @@
1
1
  // Export the interface
2
- export { RequirementInstaller } from './RequirementInstaller.js';
2
+ export { RequirementInstaller } from './requirements/RequirementInstaller.js';
3
+ export { ClientInstaller } from './clients/ClientInstaller.js';
3
4
 
4
- // Export all installer implementations
5
- export { BaseInstaller } from './BaseInstaller.js';
6
- export { NpmInstaller } from './NpmInstaller.js';
7
- export { PipInstaller } from './PipInstaller.js';
8
- export { GeneralInstaller } from './GeneralInstaller.js';
9
- export { InstallerFactory, createInstallerFactory } from './InstallerFactory.js';
5
+ // Export all requirement installer implementations
6
+ export { BaseInstaller } from './requirements/BaseInstaller.js';
7
+ export { NpmInstaller } from './requirements/NpmInstaller.js';
8
+ export { PipInstaller } from './requirements/PipInstaller.js';
9
+ export { GeneralInstaller } from './requirements/GeneralInstaller.js';
10
+ export { CommandInstaller } from './requirements/CommandInstaller.js';
11
+ export { InstallerFactory, createInstallerFactory } from './requirements/InstallerFactory.js';