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.
- package/README.md +5 -6
- 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 +159 -39
- 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/clientUtils.d.ts +0 -6
- package/dist/utils/clientUtils.js +3 -2
- 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/dist/web/server.js +1 -1
- package/package.json +2 -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} +185 -46
- 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/clientUtils.ts +4 -2
- 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
- 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 '
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
595
|
+
serverConfigForClient.command === 'npx' &&
|
|
477
596
|
(clientName === 'Cline' || clientName === 'MSRooCode' || clientName === 'MSROO')) {
|
|
478
597
|
// Update command to cmd
|
|
479
|
-
|
|
598
|
+
serverConfigForClient.command = 'cmd';
|
|
480
599
|
|
|
481
600
|
// Add /c and npx at the beginning of args
|
|
482
|
-
|
|
601
|
+
serverConfigForClient.args = ['/c', 'npx', ...serverConfigForClient.args];
|
|
483
602
|
|
|
484
603
|
// Add APPDATA environment variable pointing to npm directory
|
|
485
|
-
if (!
|
|
486
|
-
|
|
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
|
-
|
|
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:
|
|
497
|
-
args:
|
|
498
|
-
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:
|
|
531
|
-
args:
|
|
532
|
-
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
|
|
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 {
|
|
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';
|