imcp 0.0.2 → 0.0.4

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.
@@ -12,6 +12,7 @@ import { SUPPORTED_CLIENTS } from '../constants.js';
12
12
  import { resolveNpmModulePath, readJsonFile, writeJsonFile } from '../../utils/clientUtils.js';
13
13
  import { exec } from 'child_process';
14
14
  import { promisify } from 'util';
15
+ import { Logger } from '../../utils/logger.js';
15
16
 
16
17
  export class ClientInstaller {
17
18
  private configProvider: ConfigurationProvider;
@@ -46,6 +47,46 @@ export class ClientInstaller {
46
47
  }
47
48
  }
48
49
 
50
+ /**
51
+ * Check if a command is available on the system
52
+ * @param command The command to check
53
+ * @returns True if the command is available, false otherwise
54
+ */
55
+ private async isCommandAvailable(command: string): Promise<boolean> {
56
+ const execAsync = promisify(exec);
57
+ try {
58
+ if (process.platform === 'win32') {
59
+ // Windows-specific command check
60
+ await execAsync(`where ${command}`);
61
+ } else if (process.platform === 'darwin' && (command === 'code' || command === 'code-insiders')) {
62
+ // macOS-specific VS Code check
63
+ const vscodePath = command === 'code' ?
64
+ '/Applications/Visual Studio Code.app' :
65
+ '/Applications/Visual Studio Code - Insiders.app';
66
+ await execAsync(`test -d "${vscodePath}"`);
67
+ } else {
68
+ // Unix-like systems
69
+ await execAsync(`which ${command}`);
70
+ }
71
+ return true;
72
+ } catch (error) {
73
+ if (process.platform === 'darwin' && (command === 'code' || command === 'code-insiders')) {
74
+ // Try checking in ~/Applications as well for macOS
75
+ try {
76
+ const homedir = process.env.HOME;
77
+ const vscodePath = command === 'code' ?
78
+ `${homedir}/Applications/Visual Studio Code.app` :
79
+ `${homedir}/Applications/Visual Studio Code - Insiders.app`;
80
+ await execAsync(`test -d "${vscodePath}"`);
81
+ return true;
82
+ } catch (error) {
83
+ return false;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+ }
89
+
49
90
  private async installClient(clientName: string, env?: Record<string, string>): Promise<OperationStatus> {
50
91
  // Check if client is supported
51
92
  if (!SUPPORTED_CLIENTS[clientName]) {
@@ -258,18 +299,34 @@ export class ClientInstaller {
258
299
  ): Promise<{ success: boolean; message: string }> {
259
300
  try {
260
301
  if (!SUPPORTED_CLIENTS[clientName]) {
302
+ Logger.debug(`Client ${clientName} is not supported.`);
261
303
  return { success: false, message: `Unsupported client: ${clientName}` };
262
304
  }
263
305
 
264
306
  const clientSettings = SUPPORTED_CLIENTS[clientName];
265
307
 
266
- // Determine which setting path to use based on VS Code type (regular or insiders)
267
- const settingPath = process.env.CODE_INSIDERS
268
- ? clientSettings.codeInsiderSettingPath
269
- : clientSettings.codeSettingPath;
308
+ // Get both setting paths for VS Code and VS Code Insiders
309
+ const regularSettingPath = clientSettings.codeSettingPath;
310
+ const insidersSettingPath = clientSettings.codeInsiderSettingPath;
311
+
312
+ Logger.debug(`Starting installation of ${this.serverName} for client ${clientName}`);
313
+ Logger.debug(`VS Code settings path configured as: ${regularSettingPath}`);
314
+ Logger.debug(`VS Code Insiders settings path configured as: ${insidersSettingPath}`);
270
315
 
271
- if (!settingPath) {
272
- return { success: false, message: `No settings path found for client: ${clientName}` };
316
+ // Check if VS Code and VS Code Insiders are installed
317
+ const isVSCodeInstalled = await this.isCommandAvailable('code');
318
+ const isVSCodeInsidersInstalled = await this.isCommandAvailable('code-insiders');
319
+ Logger.debug(isVSCodeInstalled ? 'VS Code detected on system' : 'VS Code not detected on system');
320
+ 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
+
323
+ // If neither is installed, we can't proceed
324
+ if (!isVSCodeInstalled && !isVSCodeInsidersInstalled) {
325
+ Logger.debug('No VS Code installations detected on system. Cannot update settings.');
326
+ return {
327
+ success: false,
328
+ 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.`
329
+ };
273
330
  }
274
331
 
275
332
  // Clone the installation configuration to make modifications
@@ -296,26 +353,124 @@ export class ClientInstaller {
296
353
  Object.assign(installConfig.env, env);
297
354
  }
298
355
 
299
- // Update client-specific settings
356
+ // Keep track of success for both installations
357
+ let regularSuccess = false;
358
+ let insidersSuccess = false;
359
+ let errorMessages: string[] = [];
360
+
361
+ // Update client-specific settings for both VS Code and VS Code Insiders
300
362
  if (clientName === 'MSRooCode' || clientName === 'Cline') {
301
- await this.updateClineOrMSRooSettings(settingPath, this.serverName, installConfig, clientName);
363
+ // Update VS Code settings if VS Code is installed
364
+ if (isVSCodeInstalled) {
365
+ try {
366
+ Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
367
+ await this.updateClineOrMSRooSettings(regularSettingPath, this.serverName, installConfig, clientName);
368
+ regularSuccess = true;
369
+ Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
370
+ } catch (error) {
371
+ const errorMsg = `Error updating VS Code settings: ${error instanceof Error ? error.message : String(error)}`;
372
+ errorMessages.push(errorMsg);
373
+ console.error(errorMsg);
374
+ }
375
+ }
376
+
377
+ // Update VS Code Insiders settings if VS Code Insiders is installed
378
+ if (isVSCodeInsidersInstalled) {
379
+ try {
380
+ Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
381
+ await this.updateClineOrMSRooSettings(insidersSettingPath, this.serverName, installConfig, clientName);
382
+ insidersSuccess = true;
383
+ Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
384
+ } catch (error) {
385
+ const errorMsg = `Error updating VS Code Insiders settings: ${error instanceof Error ? error.message : String(error)}`;
386
+ errorMessages.push(errorMsg);
387
+ console.error(errorMsg);
388
+ }
389
+ }
302
390
  } else if (clientName === 'GithubCopilot') {
303
- await this.updateGithubCopilotSettings(settingPath, this.serverName, installConfig);
391
+ // Update VS Code settings if VS Code is installed
392
+ if (isVSCodeInstalled) {
393
+ try {
394
+ Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
395
+ await this.updateGithubCopilotSettings(regularSettingPath, this.serverName, installConfig);
396
+ regularSuccess = true;
397
+ Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
398
+ } catch (error) {
399
+ const errorMsg = `Error updating VS Code settings: ${error instanceof Error ? error.message : String(error)}`;
400
+ errorMessages.push(errorMsg);
401
+ console.error(errorMsg);
402
+ }
403
+ }
404
+
405
+ // Update VS Code Insiders settings if VS Code Insiders is installed
406
+ if (isVSCodeInsidersInstalled) {
407
+ try {
408
+ Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
409
+ await this.updateGithubCopilotSettings(insidersSettingPath, this.serverName, installConfig);
410
+ insidersSuccess = true;
411
+ Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
412
+ } catch (error) {
413
+ const errorMsg = `Error updating VS Code Insiders settings: ${error instanceof Error ? error.message : String(error)}`;
414
+ errorMessages.push(errorMsg);
415
+ console.error(errorMsg);
416
+ }
417
+ }
304
418
  } else {
419
+ Logger.debug(`No implementation exists for updating settings for client: ${clientName}`);
305
420
  return {
306
421
  success: false,
307
422
  message: `Client ${clientName} is supported but no implementation exists for updating its settings`
308
423
  };
309
424
  }
310
425
 
426
+ // Determine overall success status and message
427
+ const overallSuccess = regularSuccess || insidersSuccess;
428
+ let message = '';
429
+
430
+ if (overallSuccess) {
431
+ const successDetails = [];
432
+ if (regularSuccess) successDetails.push('VS Code');
433
+ if (insidersSuccess) successDetails.push('VS Code Insiders');
434
+
435
+ // Create a more compact success message with specific paths
436
+ const pathDetails = [];
437
+ if (regularSuccess) {
438
+ pathDetails.push(`\n[VS Code]: ${regularSettingPath}`);
439
+ }
440
+ if (insidersSuccess) {
441
+ pathDetails.push(`\n[VS Code Insiders]: ${insidersSettingPath}`);
442
+ }
443
+ message = `Successfully installed ${this.serverName} for client: ${clientName}. ${pathDetails.join(' ')}`;
444
+
445
+ // Add partial failure information if applicable
446
+ if (errorMessages.length > 0) {
447
+ message += `\nHowever, some errors occurred:\n${errorMessages.join('\n- ')}`;
448
+ }
449
+ Logger.debug(`Installation complete: ${message}`);
450
+ } else {
451
+ // Create a more detailed failure message that includes the detection results
452
+ const detectionInfo = [];
453
+ if (!isVSCodeInstalled) detectionInfo.push('VS Code not detected');
454
+ if (!isVSCodeInsidersInstalled) detectionInfo.push('VS Code Insiders not detected');
455
+
456
+ if (errorMessages.length > 0) {
457
+ message = `Failed to install ${this.serverName} for client: ${clientName}.\nDetection status: [${detectionInfo.join(', ')}].\nErrors:\n- ${errorMessages.join('\n- ')}`;
458
+ } else {
459
+ message = `Failed to install ${this.serverName} for client: ${clientName}.\nDetection status: [${detectionInfo.join(', ')}]`;
460
+ }
461
+ console.error(`Installation failed: ${message}`);
462
+ }
463
+
311
464
  return {
312
- success: true,
313
- message: `Successfully installed ${this.serverName} for client: ${clientName}`
465
+ success: overallSuccess,
466
+ message: message
314
467
  };
315
468
  } catch (error) {
469
+ const errorMsg = `Error installing client ${clientName}: ${error instanceof Error ? error.message : String(error)}`;
470
+ console.error(errorMsg);
316
471
  return {
317
472
  success: false,
318
- message: `Error installing client ${clientName}: ${error instanceof Error ? error.message : String(error)}`
473
+ message: errorMsg
319
474
  };
320
475
  }
321
476
  }
@@ -354,6 +509,12 @@ export class ClientInstaller {
354
509
  const npmPath = await this.getNpmPath();
355
510
  serverConfig.env['APPDATA'] = npmPath;
356
511
  }
512
+ // Convert backslashes to forward slashes in args paths
513
+ if (serverConfig.args) {
514
+ serverConfig.args = serverConfig.args.map((arg: string) =>
515
+ typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg
516
+ );
517
+ }
357
518
 
358
519
  // Add or update the server configuration
359
520
  settings.mcpServers[serverName] = {
@@ -389,6 +550,13 @@ export class ClientInstaller {
389
550
  settings.mcp.servers = {};
390
551
  }
391
552
 
553
+ // Convert backslashes to forward slashes in args paths
554
+ if (installConfig.args) {
555
+ installConfig.args = installConfig.args.map((arg: string) =>
556
+ typeof arg === 'string' ? arg.replace(/\\/g, '/') : arg
557
+ );
558
+ }
559
+
392
560
  // Add or update the server configuration
393
561
  settings.mcp.servers[serverName] = {
394
562
  command: installConfig.command,
@@ -396,6 +564,7 @@ export class ClientInstaller {
396
564
  env: installConfig.env
397
565
  };
398
566
 
567
+
399
568
  // Write the updated settings back to the file
400
569
  await writeJsonFile(settingPath, settings);
401
570
  }
@@ -84,6 +84,8 @@ export function resolveNpmModulePath(pathString: string): string {
84
84
  * @param createIfNotExist Whether to create the file if it doesn't exist
85
85
  * @returns The parsed JSON content
86
86
  */
87
+ import stripJsonComments from 'strip-json-comments';
88
+
87
89
  export async function readJsonFile(filePath: string, createIfNotExist = false): Promise<any> {
88
90
  try {
89
91
  // Ensure directory exists
@@ -92,7 +94,9 @@ export async function readJsonFile(filePath: string, createIfNotExist = false):
92
94
  }
93
95
 
94
96
  const content = await fs.readFile(filePath, 'utf8');
95
- return JSON.parse(content);
97
+ // Remove comments and trailing commas from JSON content
98
+ const sanitizedContent = stripJsonComments(content).replace(/,(\s*[}\]])/g, '$1');
99
+ return JSON.parse(sanitizedContent);
96
100
  } catch (error) {
97
101
  if ((error as NodeJS.ErrnoException).code === 'ENOENT' && createIfNotExist) {
98
102
  return {};
@@ -34,6 +34,12 @@ body {
34
34
  min-height: 120px;
35
35
  opacity: 1 !important;
36
36
  box-shadow: 0 0 16px #3498db;
37
+ position: relative;
38
+ padding-top: 24px;
39
+ }
40
+
41
+ #installLoadingModal .modal-close-button {
42
+ z-index: 3200 !important;
37
43
  }
38
44
  /* Loading modal always on top */
39
45
  #installLoadingModal {
@@ -61,8 +67,37 @@ body {
61
67
  transition: all 0.3s ease-out;
62
68
  animation: slideIn 0.3s ease-out;
63
69
  }
64
-
65
70
  /* Close button */
71
+ .modal-close-button {
72
+ position: absolute;
73
+ right: 12px;
74
+ top: 12px;
75
+ width: 32px;
76
+ height: 32px;
77
+ border-radius: 50%;
78
+ background: #ffffff;
79
+ border: 2px solid #3498db;
80
+ color: #3498db;
81
+ font-size: 22px;
82
+ cursor: pointer;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ transition: all 0.2s ease;
87
+ z-index: 10;
88
+ padding: 0;
89
+ line-height: 1;
90
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
91
+ }
92
+
93
+ .modal-close-button:hover {
94
+ background-color: #3498db;
95
+ color: #ffffff;
96
+ border-color: #3498db;
97
+ transform: scale(1.05);
98
+ box-shadow: 0 0 8px rgba(52, 152, 219, 0.4);
99
+ }
100
+
66
101
  .close {
67
102
  position: absolute;
68
103
  right: 1.5rem;
@@ -71,20 +106,14 @@ body {
71
106
  font-weight: 600;
72
107
  color: #6b7280;
73
108
  cursor: pointer;
74
- width: 32px;
75
- height: 32px;
76
- display: flex;
77
- align-items: center;
78
- justify-content: center;
79
- border-radius: 50%;
80
109
  transition: color 0.2s ease;
81
110
  }
82
111
 
83
112
  .close:hover {
84
- background-color: #f3f4f6;
85
113
  color: #111827;
86
114
  }
87
115
 
116
+
88
117
  /* Sections layout */
89
118
  .modal-sections {
90
119
  margin-top: 1.5rem;
@@ -240,11 +269,62 @@ body {
240
269
  }
241
270
  }
242
271
 
243
- /* Center loading icon in loading modal */
272
+ /* Center loading icon in loading modal */
244
273
  #installLoadingModal .loading-icon {
245
274
  display: flex;
246
275
  justify-content: center;
247
276
  align-items: center;
248
277
  width: 100%;
249
278
  margin-bottom: 8px;
279
+ }
280
+
281
+ /* Loading message styles */
282
+ #installLoadingMessage {
283
+ font-size: 0.85rem !important;
284
+ line-height: 1.6;
285
+ word-break: break-word;
286
+ max-height: 200px;
287
+ overflow-y: auto;
288
+ scrollbar-width: thin;
289
+ scrollbar-color: #3498db #f0f0f0;
290
+ }
291
+
292
+ #installLoadingMessage::-webkit-scrollbar {
293
+ width: 6px;
294
+ }
295
+
296
+ #installLoadingMessage::-webkit-scrollbar-track {
297
+ background: #f0f0f0;
298
+ border-radius: 3px;
299
+ }
300
+
301
+ #installLoadingMessage::-webkit-scrollbar-thumb {
302
+ background-color: #3498db;
303
+ border-radius: 3px;
304
+ }
305
+
306
+ #installLoadingMessage div {
307
+ margin-bottom: 8px;
308
+ padding: 4px 0;
309
+ }
310
+
311
+ /* Error message styling */
312
+ #installLoadingMessage span[style*="color:red"] {
313
+ color: #f59e0b !important;
314
+ font-weight: 500;
315
+ display: block;
316
+ padding: 4px 8px;
317
+ background: rgba(245, 158, 11, 0.1);
318
+ border-radius: 4px;
319
+ margin: 4px 0;
320
+ }
321
+
322
+ /* File path styling */
323
+ #installLoadingMessage .file-path {
324
+ font-family: 'Consolas', monospace;
325
+ background: #f8fafc;
326
+ padding: 2px 4px;
327
+ border-radius: 3px;
328
+ border: 1px solid #e2e8f0;
329
+ color: #2563eb;
250
330
  }
@@ -112,18 +112,24 @@
112
112
  </div>
113
113
  <!-- Loading Modal -->
114
114
  <div id="installLoadingModal" class="modal" style="display:none; z-index:2000;">
115
- <div class="modal-content" style="text-align:center; pointer-events:auto;">
115
+ <div class="modal-content" style="text-align:center; pointer-events:auto; position:relative;">
116
+ <button class="modal-close-button" onclick="hideInstallLoadingModal()" aria-label="Close">&times;</button>
116
117
  <div style="margin-top:40px;">
117
118
  <div class="loading-icon" style="margin-bottom:16px;">
118
119
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
119
- <circle cx="24" cy="24" r="20" stroke="#888" stroke-width="4" opacity="0.2"/>
120
- <circle cx="24" cy="24" r="20" stroke="#3498db" stroke-width="4" stroke-linecap="round" stroke-dasharray="100" stroke-dashoffset="60">
121
- <animateTransform attributeName="transform" type="rotate" from="0 24 24" to="360 24 24" dur="1s" repeatCount="indefinite"/>
120
+ <circle cx="24" cy="24" r="20" stroke="#888" stroke-width="4" opacity="0.2" />
121
+ <circle cx="24" cy="24" r="20" stroke="#3498db" stroke-width="4" stroke-linecap="round"
122
+ stroke-dasharray="100" stroke-dashoffset="60">
123
+ <animateTransform attributeName="transform" type="rotate" from="0 24 24" to="360 24 24"
124
+ dur="1s" repeatCount="indefinite" />
122
125
  </circle>
123
126
  </svg>
124
127
  </div>
125
- <div class="loading-title" style="font-size:1.5rem; font-weight:bold; margin-bottom:8px;">Installing...</div>
126
- <div id="installLoadingMessage" style="min-height:48px; max-height:160px; overflow:auto; background:#f8f8f8; border-radius:6px; padding:12px; font-size:1rem; color:#444; text-align:left;"></div>
128
+ <div class="loading-title" style="font-size:1.5rem; font-weight:bold; margin-bottom:8px;">Installing...
129
+ </div>
130
+ <div id="installLoadingMessage"
131
+ style="min-height:48px; max-height:160px; overflow:auto; background:#f8f8f8; border-radius:6px; padding:12px; font-size:1rem; color:#444; text-align:left;">
132
+ </div>
127
133
  </div>
128
134
  </div>
129
135
  </div>