imcp 0.0.4 → 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 (88) hide show
  1. package/README.md +3 -4
  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 +134 -43
  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/githubUtils.d.ts +11 -0
  51. package/dist/utils/githubUtils.js +88 -0
  52. package/dist/utils/osUtils.d.ts +17 -0
  53. package/dist/utils/osUtils.js +184 -0
  54. package/dist/web/public/css/modal.css +97 -3
  55. package/dist/web/public/index.html +21 -2
  56. package/dist/web/public/js/modal.js +177 -28
  57. package/dist/web/public/js/serverCategoryDetails.js +12 -10
  58. package/dist/web/public/js/serverCategoryList.js +20 -5
  59. package/dist/web/public/modal.html +27 -13
  60. package/package.json +1 -1
  61. package/src/cli/commands/install.ts +4 -2
  62. package/src/cli/commands/list.ts +1 -0
  63. package/src/cli/index.ts +1 -1
  64. package/src/core/ConfigurationLoader.ts +251 -0
  65. package/src/core/ConfigurationProvider.ts +13 -195
  66. package/src/core/InstallationService.ts +140 -106
  67. package/src/core/RequirementService.ts +5 -10
  68. package/src/core/constants.ts +15 -1
  69. package/src/core/installers/{ClientInstaller.ts → clients/ClientInstaller.ts} +157 -51
  70. package/src/core/installers/clients/ExtensionInstaller.ts +162 -0
  71. package/src/core/installers/index.ts +9 -7
  72. package/src/core/installers/{BaseInstaller.ts → requirements/BaseInstaller.ts} +10 -118
  73. package/src/core/installers/{CommandInstaller.ts → requirements/CommandInstaller.ts} +7 -3
  74. package/src/core/installers/{GeneralInstaller.ts → requirements/GeneralInstaller.ts} +6 -2
  75. package/src/core/installers/{InstallerFactory.ts → requirements/InstallerFactory.ts} +11 -9
  76. package/src/core/installers/{NpmInstaller.ts → requirements/NpmInstaller.ts} +7 -4
  77. package/src/core/installers/{PipInstaller.ts → requirements/PipInstaller.ts} +26 -10
  78. package/src/core/installers/requirements/RequirementInstaller.ts +41 -0
  79. package/src/core/types.ts +4 -1
  80. package/src/services/ServerService.ts +1 -1
  81. package/src/utils/githubUtils.ts +103 -0
  82. package/src/utils/osUtils.ts +206 -15
  83. package/src/web/public/css/modal.css +97 -3
  84. package/src/web/public/index.html +21 -2
  85. package/src/web/public/js/modal.js +177 -28
  86. package/src/web/public/js/serverCategoryDetails.js +12 -10
  87. package/src/web/public/js/serverCategoryList.js +20 -5
  88. package/src/web/public/modal.html +27 -13
@@ -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,13 +52,13 @@ 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
@@ -87,7 +92,8 @@ export class ClientInstaller {
87
92
  }
88
93
  }
89
94
 
90
- 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> {
91
97
  // Check if client is supported
92
98
  if (!SUPPORTED_CLIENTS[clientName]) {
93
99
  return {
@@ -118,14 +124,18 @@ export class ClientInstaller {
118
124
  );
119
125
 
120
126
  // Start the asynchronous installation process without awaiting it
121
- this.processInstallation(clientName, operationId, env);
127
+ // Pass options down
128
+ this.processInstallation(clientName, operationId, options);
122
129
 
123
130
  // Return the initial status immediately
124
131
  return initialStatus;
125
132
  }
126
133
 
127
- 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> {
128
136
  try {
137
+ await ExtensionInstaller.installExtension(clientName);
138
+
129
139
  // Check requirements before installation
130
140
  let requirementsReady = await this.configProvider.isRequirementsReady(this.categoryName, this.serverName);
131
141
 
@@ -236,8 +246,16 @@ export class ClientInstaller {
236
246
  }
237
247
 
238
248
  try {
239
- // Install client-specific configuration
240
- 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);
241
259
 
242
260
  const finalStatus: OperationStatus = {
243
261
  status: result.success ? 'completed' : 'failed',
@@ -255,6 +273,10 @@ export class ClientInstaller {
255
273
  finalStatus
256
274
  );
257
275
 
276
+ if (result.success) {
277
+ await this.configProvider.reloadClientMCPSettings();
278
+ }
279
+
258
280
  } catch (error) {
259
281
  const errorStatus: OperationStatus = {
260
282
  status: 'failed',
@@ -291,9 +313,10 @@ export class ClientInstaller {
291
313
  }
292
314
  }
293
315
 
316
+ // Modified to accept ServerInstallOptions
294
317
  private async installClientConfig(
295
318
  clientName: string,
296
- env: Record<string, string>,
319
+ options: ServerInstallOptions, // Use options directly
297
320
  serverConfig: McpConfig,
298
321
  feedConfig: FeedConfiguration
299
322
  ): Promise<{ success: boolean; message: string }> {
@@ -318,7 +341,6 @@ export class ClientInstaller {
318
341
  const isVSCodeInsidersInstalled = await this.isCommandAvailable('code-insiders');
319
342
  Logger.debug(isVSCodeInstalled ? 'VS Code detected on system' : 'VS Code not detected on system');
320
343
  Logger.debug(isVSCodeInsidersInstalled ? 'VS Code Insiders detected on system' : 'VS Code Insiders not detected on system');
321
- Logger.debug(`VS Code Insiders installed: ${isVSCodeInsidersInstalled}`);
322
344
 
323
345
  // If neither is installed, we can't proceed
324
346
  if (!isVSCodeInstalled && !isVSCodeInsidersInstalled) {
@@ -329,29 +351,102 @@ export class ClientInstaller {
329
351
  };
330
352
  }
331
353
 
332
- // 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
333
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);
379
+
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
+ }
334
387
 
335
- // Replace template variables in args
336
- installConfig.args = installConfig.args.map((arg: string) =>
337
- resolveNpmModulePath(arg));
338
-
339
- // Add environment variables from options
340
- installConfig.env = {};
341
- if (serverConfig.installation.env) {
342
- // Add default env variables from config
343
- for (const [key, config] of Object.entries(serverConfig.installation.env)) {
344
- const envConfig = config as any; // Type assertion for dynamic access
345
- if (envConfig.Default) {
346
- installConfig.env[key] = envConfig.Default;
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}'));
347
407
  }
348
408
  }
349
409
  }
350
410
 
351
- // Override with provided env variables
352
- if (env) {
353
- 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
+ }
354
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
+
355
450
 
356
451
  // Keep track of success for both installations
357
452
  let regularSuccess = false;
@@ -364,6 +459,7 @@ export class ClientInstaller {
364
459
  if (isVSCodeInstalled) {
365
460
  try {
366
461
  Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
462
+ // Pass the modified installConfig
367
463
  await this.updateClineOrMSRooSettings(regularSettingPath, this.serverName, installConfig, clientName);
368
464
  regularSuccess = true;
369
465
  Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
@@ -378,6 +474,7 @@ export class ClientInstaller {
378
474
  if (isVSCodeInsidersInstalled) {
379
475
  try {
380
476
  Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
477
+ // Pass the modified installConfig
381
478
  await this.updateClineOrMSRooSettings(insidersSettingPath, this.serverName, installConfig, clientName);
382
479
  insidersSuccess = true;
383
480
  Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
@@ -392,6 +489,7 @@ export class ClientInstaller {
392
489
  if (isVSCodeInstalled) {
393
490
  try {
394
491
  Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
492
+ // Pass the modified installConfig
395
493
  await this.updateGithubCopilotSettings(regularSettingPath, this.serverName, installConfig);
396
494
  regularSuccess = true;
397
495
  Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
@@ -406,6 +504,7 @@ export class ClientInstaller {
406
504
  if (isVSCodeInsidersInstalled) {
407
505
  try {
408
506
  Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
507
+ // Pass the modified installConfig
409
508
  await this.updateGithubCopilotSettings(insidersSettingPath, this.serverName, installConfig);
410
509
  insidersSuccess = true;
411
510
  Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
@@ -478,7 +577,7 @@ export class ClientInstaller {
478
577
  private async updateClineOrMSRooSettings(
479
578
  settingPath: string,
480
579
  serverName: string,
481
- installConfig: any,
580
+ installConfig: any, // Use the processed installConfig
482
581
  clientName: string
483
582
  ): Promise<void> {
484
583
  // Read the Cline/MSRoo settings file
@@ -490,41 +589,44 @@ export class ClientInstaller {
490
589
  }
491
590
 
492
591
  // Special handling for Windows when command is npx for Cline and MSROO clients
493
- const serverConfig = { ...installConfig };
592
+ // Use a copy to avoid modifying the passed installConfig directly if needed elsewhere
593
+ const serverConfigForClient = { ...installConfig };
494
594
  if (process.platform === 'win32' &&
495
- serverConfig.command === 'npx' &&
595
+ serverConfigForClient.command === 'npx' &&
496
596
  (clientName === 'Cline' || clientName === 'MSRooCode' || clientName === 'MSROO')) {
497
597
  // Update command to cmd
498
- serverConfig.command = 'cmd';
598
+ serverConfigForClient.command = 'cmd';
499
599
 
500
600
  // Add /c and npx at the beginning of args
501
- serverConfig.args = ['/c', 'npx', ...serverConfig.args];
601
+ serverConfigForClient.args = ['/c', 'npx', ...serverConfigForClient.args];
502
602
 
503
603
  // Add APPDATA environment variable pointing to npm directory
504
- if (!serverConfig.env) {
505
- serverConfig.env = {};
604
+ if (!serverConfigForClient.env) {
605
+ serverConfigForClient.env = {};
506
606
  }
507
607
 
508
608
  // Dynamically get npm path and set APPDATA to it
509
609
  const npmPath = await this.getNpmPath();
510
- 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)}`);
511
612
  }
512
613
  // Convert backslashes to forward slashes in args paths
513
- if (serverConfig.args) {
514
- serverConfig.args = serverConfig.args.map((arg: string) =>
614
+ if (serverConfigForClient.args) {
615
+ serverConfigForClient.args = serverConfigForClient.args.map((arg: string) =>
515
616
  typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg
516
617
  );
517
618
  }
518
619
 
519
620
  // Add or update the server configuration
520
621
  settings.mcpServers[serverName] = {
521
- command: serverConfig.command,
522
- args: serverConfig.args,
523
- env: serverConfig.env,
622
+ command: serverConfigForClient.command,
623
+ args: serverConfigForClient.args,
624
+ env: serverConfigForClient.env,
524
625
  autoApprove: [],
525
626
  disabled: false,
526
627
  alwaysAllow: []
527
628
  };
629
+ Logger.debug(`Updating ${settingPath} for ${serverName}: ${JSON.stringify(settings.mcpServers[serverName])}`);
528
630
 
529
631
  // Write the updated settings back to the file
530
632
  await writeJsonFile(settingPath, settings);
@@ -533,7 +635,7 @@ export class ClientInstaller {
533
635
  private async updateGithubCopilotSettings(
534
636
  settingPath: string,
535
637
  serverName: string,
536
- installConfig: any
638
+ installConfig: any // Use the processed installConfig
537
639
  ): Promise<void> {
538
640
  // Read the VS Code settings.json file
539
641
  const settings = await readJsonFile(settingPath, true);
@@ -550,20 +652,23 @@ export class ClientInstaller {
550
652
  settings.mcp.servers = {};
551
653
  }
552
654
 
655
+ // Use a copy to avoid modifying the passed installConfig directly
656
+ const serverConfigForClient = { ...installConfig };
657
+
553
658
  // Convert backslashes to forward slashes in args paths
554
- if (installConfig.args) {
555
- installConfig.args = installConfig.args.map((arg: string) =>
659
+ if (serverConfigForClient.args) {
660
+ serverConfigForClient.args = serverConfigForClient.args.map((arg: string) =>
556
661
  typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg
557
662
  );
558
663
  }
559
664
 
560
665
  // Add or update the server configuration
561
666
  settings.mcp.servers[serverName] = {
562
- command: installConfig.command,
563
- args: installConfig.args,
564
- env: installConfig.env
667
+ command: serverConfigForClient.command,
668
+ args: serverConfigForClient.args,
669
+ env: serverConfigForClient.env
565
670
  };
566
-
671
+ Logger.debug(`Updating ${settingPath} for ${serverName}: ${JSON.stringify(settings.mcp.servers[serverName])}`);
567
672
 
568
673
  // Write the updated settings back to the file
569
674
  await writeJsonFile(settingPath, settings);
@@ -573,8 +678,9 @@ export class ClientInstaller {
573
678
  const initialStatuses: OperationStatus[] = [];
574
679
 
575
680
  // Start installation for each client asynchronously and collect initial statuses
681
+ // Pass options down to installClient
576
682
  const installPromises = this.clients.map(async (clientName) => {
577
- const initialStatus = await this.installClient(clientName, options.env || {});
683
+ const initialStatus = await this.installClient(clientName, options);
578
684
  initialStatuses.push(initialStatus);
579
685
  return initialStatus;
580
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';