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.
- package/README.md +3 -4
- package/dist/cli/commands/install.js +2 -0
- package/dist/cli/commands/list.js +1 -0
- package/dist/cli/index.js +1 -2
- package/dist/core/ConfigurationLoader.d.ts +32 -0
- package/dist/core/ConfigurationLoader.js +213 -0
- package/dist/core/ConfigurationProvider.d.ts +2 -3
- package/dist/core/ConfigurationProvider.js +13 -182
- package/dist/core/InstallationService.d.ts +8 -0
- package/dist/core/InstallationService.js +124 -96
- package/dist/core/RequirementService.d.ts +1 -1
- package/dist/core/RequirementService.js +5 -9
- package/dist/core/constants.js +14 -1
- package/dist/core/installers/BaseInstaller.d.ts +5 -4
- package/dist/core/installers/BaseInstaller.js +17 -28
- package/dist/core/installers/ClientInstaller.js +134 -43
- package/dist/core/installers/CommandInstaller.d.ts +1 -0
- package/dist/core/installers/CommandInstaller.js +3 -0
- package/dist/core/installers/GeneralInstaller.d.ts +1 -0
- package/dist/core/installers/GeneralInstaller.js +3 -0
- package/dist/core/installers/InstallerFactory.d.ts +9 -7
- package/dist/core/installers/InstallerFactory.js +10 -8
- package/dist/core/installers/NpmInstaller.d.ts +1 -0
- package/dist/core/installers/NpmInstaller.js +3 -0
- package/dist/core/installers/PipInstaller.d.ts +6 -3
- package/dist/core/installers/PipInstaller.js +21 -8
- package/dist/core/installers/RequirementInstaller.d.ts +4 -3
- package/dist/core/installers/clients/ClientInstaller.d.ts +23 -0
- package/dist/core/installers/clients/ClientInstaller.js +573 -0
- package/dist/core/installers/clients/ExtensionInstaller.d.ts +26 -0
- package/dist/core/installers/clients/ExtensionInstaller.js +149 -0
- package/dist/core/installers/index.d.ts +8 -6
- package/dist/core/installers/index.js +8 -6
- package/dist/core/installers/requirements/BaseInstaller.d.ts +59 -0
- package/dist/core/installers/requirements/BaseInstaller.js +168 -0
- package/dist/core/installers/requirements/CommandInstaller.d.ts +37 -0
- package/dist/core/installers/requirements/CommandInstaller.js +173 -0
- package/dist/core/installers/requirements/GeneralInstaller.d.ts +33 -0
- package/dist/core/installers/requirements/GeneralInstaller.js +86 -0
- package/dist/core/installers/requirements/InstallerFactory.d.ts +54 -0
- package/dist/core/installers/requirements/InstallerFactory.js +97 -0
- package/dist/core/installers/requirements/NpmInstaller.d.ts +26 -0
- package/dist/core/installers/requirements/NpmInstaller.js +128 -0
- package/dist/core/installers/requirements/PipInstaller.d.ts +28 -0
- package/dist/core/installers/requirements/PipInstaller.js +128 -0
- package/{src/core/installers/RequirementInstaller.ts → dist/core/installers/requirements/RequirementInstaller.d.ts} +33 -38
- package/dist/core/installers/requirements/RequirementInstaller.js +3 -0
- package/dist/core/types.d.ts +4 -1
- package/dist/services/ServerService.js +1 -1
- package/dist/utils/githubUtils.d.ts +11 -0
- package/dist/utils/githubUtils.js +88 -0
- package/dist/utils/osUtils.d.ts +17 -0
- package/dist/utils/osUtils.js +184 -0
- package/dist/web/public/css/modal.css +97 -3
- package/dist/web/public/index.html +21 -2
- package/dist/web/public/js/modal.js +177 -28
- package/dist/web/public/js/serverCategoryDetails.js +12 -10
- package/dist/web/public/js/serverCategoryList.js +20 -5
- package/dist/web/public/modal.html +27 -13
- package/package.json +1 -1
- package/src/cli/commands/install.ts +4 -2
- package/src/cli/commands/list.ts +1 -0
- package/src/cli/index.ts +1 -1
- package/src/core/ConfigurationLoader.ts +251 -0
- package/src/core/ConfigurationProvider.ts +13 -195
- package/src/core/InstallationService.ts +140 -106
- package/src/core/RequirementService.ts +5 -10
- package/src/core/constants.ts +15 -1
- package/src/core/installers/{ClientInstaller.ts → clients/ClientInstaller.ts} +157 -51
- package/src/core/installers/clients/ExtensionInstaller.ts +162 -0
- package/src/core/installers/index.ts +9 -7
- package/src/core/installers/{BaseInstaller.ts → requirements/BaseInstaller.ts} +10 -118
- package/src/core/installers/{CommandInstaller.ts → requirements/CommandInstaller.ts} +7 -3
- package/src/core/installers/{GeneralInstaller.ts → requirements/GeneralInstaller.ts} +6 -2
- package/src/core/installers/{InstallerFactory.ts → requirements/InstallerFactory.ts} +11 -9
- package/src/core/installers/{NpmInstaller.ts → requirements/NpmInstaller.ts} +7 -4
- package/src/core/installers/{PipInstaller.ts → requirements/PipInstaller.ts} +26 -10
- package/src/core/installers/requirements/RequirementInstaller.ts +41 -0
- package/src/core/types.ts +4 -1
- package/src/services/ServerService.ts +1 -1
- package/src/utils/githubUtils.ts +103 -0
- package/src/utils/osUtils.ts +206 -15
- package/src/web/public/css/modal.css +97 -3
- package/src/web/public/index.html +21 -2
- package/src/web/public/js/modal.js +177 -28
- package/src/web/public/js/serverCategoryDetails.js +12 -10
- package/src/web/public/js/serverCategoryList.js +20 -5
- 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 '
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
595
|
+
serverConfigForClient.command === 'npx' &&
|
|
496
596
|
(clientName === 'Cline' || clientName === 'MSRooCode' || clientName === 'MSROO')) {
|
|
497
597
|
// Update command to cmd
|
|
498
|
-
|
|
598
|
+
serverConfigForClient.command = 'cmd';
|
|
499
599
|
|
|
500
600
|
// Add /c and npx at the beginning of args
|
|
501
|
-
|
|
601
|
+
serverConfigForClient.args = ['/c', 'npx', ...serverConfigForClient.args];
|
|
502
602
|
|
|
503
603
|
// Add APPDATA environment variable pointing to npm directory
|
|
504
|
-
if (!
|
|
505
|
-
|
|
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
|
-
|
|
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 (
|
|
514
|
-
|
|
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:
|
|
522
|
-
args:
|
|
523
|
-
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 (
|
|
555
|
-
|
|
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:
|
|
563
|
-
args:
|
|
564
|
-
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
|
|
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 {
|
|
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';
|