imcp 0.1.2 → 0.1.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.
- package/dist/core/installers/requirements/InstallerFactory.js +2 -0
- package/dist/core/installers/requirements/NugetInstaller.d.ts +37 -0
- package/dist/core/installers/requirements/NugetInstaller.js +189 -0
- package/dist/core/metadatas/recordingConstants.d.ts +2 -0
- package/dist/core/metadatas/recordingConstants.js +2 -0
- package/dist/core/metadatas/types.d.ts +1 -1
- package/dist/core/onboard/FeedOnboardService.js +1 -22
- package/dist/core/validators/StdioServerValidator.js +5 -5
- package/dist/utils/osUtils.d.ts +11 -0
- package/dist/utils/osUtils.js +100 -0
- package/dist/web/public/js/onboard/templates.js +1 -0
- package/dist/web/public/js/serverCategoryList.js +3 -3
- package/package.json +1 -1
- package/src/core/installers/requirements/InstallerFactory.ts +2 -0
- package/src/core/installers/requirements/NugetInstaller.ts +203 -0
- package/src/core/metadatas/recordingConstants.ts +3 -0
- package/src/core/metadatas/types.ts +1 -1
- package/src/core/onboard/FeedOnboardService.ts +1 -24
- package/src/core/validators/StdioServerValidator.ts +6 -5
- package/src/utils/osUtils.ts +104 -1
- package/src/web/public/js/onboard/templates.js +1 -0
- package/src/web/public/js/serverCategoryList.js +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NpmInstaller } from './NpmInstaller.js';
|
|
2
2
|
import { PipInstaller } from './PipInstaller.js';
|
|
3
|
+
import { NugetInstaller } from './NugetInstaller.js';
|
|
3
4
|
import { CommandInstaller } from './CommandInstaller.js';
|
|
4
5
|
import { GeneralInstaller } from './GeneralInstaller.js';
|
|
5
6
|
import { exec } from 'child_process';
|
|
@@ -25,6 +26,7 @@ export class InstallerFactory {
|
|
|
25
26
|
registerDefaultInstallers() {
|
|
26
27
|
this.registerInstaller(new NpmInstaller(this.execPromise));
|
|
27
28
|
this.registerInstaller(new PipInstaller(this.execPromise));
|
|
29
|
+
this.registerInstaller(new NugetInstaller(this.execPromise));
|
|
28
30
|
this.registerInstaller(new CommandInstaller(this.execPromise));
|
|
29
31
|
this.registerInstaller(new GeneralInstaller(this.execPromise));
|
|
30
32
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { RequirementConfig, RequirementStatus, ServerInstallOptions } from '../../metadatas/types.js';
|
|
2
|
+
import { BaseInstaller } from './BaseInstaller.js';
|
|
3
|
+
import { InstallOperationManager } from '../../loaders/InstallOperationManager.js';
|
|
4
|
+
/**
|
|
5
|
+
* Installer implementation for .NET packages using NuGet
|
|
6
|
+
*/
|
|
7
|
+
export declare class NugetInstaller extends BaseInstaller {
|
|
8
|
+
/**
|
|
9
|
+
* Check if this installer can handle the given requirement type
|
|
10
|
+
* @param requirement The requirement to check
|
|
11
|
+
* @returns True if this installer can handle the requirement
|
|
12
|
+
*/
|
|
13
|
+
canHandle(requirement: RequirementConfig): boolean;
|
|
14
|
+
supportCheckUpdates(): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Get the latest version available for the NuGet package.
|
|
17
|
+
* @param requirement The requirement to check.
|
|
18
|
+
* @param _options Optional server install options (not used for NuGet).
|
|
19
|
+
* @returns The latest version string, or undefined if not found or not applicable.
|
|
20
|
+
*/
|
|
21
|
+
getLatestVersion(requirement: RequirementConfig, _options?: ServerInstallOptions): Promise<string | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* Check if the .NET tool is already installed
|
|
24
|
+
* @param requirement The requirement to check
|
|
25
|
+
* @param _options Optional server install options (not used for NuGet)
|
|
26
|
+
* @returns The status of the requirement
|
|
27
|
+
*/
|
|
28
|
+
checkInstallation(requirement: RequirementConfig, _options?: ServerInstallOptions): Promise<RequirementStatus>;
|
|
29
|
+
/**
|
|
30
|
+
* Install the .NET tool
|
|
31
|
+
* @param requirement The requirement to install
|
|
32
|
+
* @param recorder Optional InstallOperationManager for recording steps
|
|
33
|
+
* @param _options Optional server install options (not used for NuGet)
|
|
34
|
+
* @returns The status of the installation
|
|
35
|
+
*/
|
|
36
|
+
install(requirement: RequirementConfig, recorder: InstallOperationManager, _options?: ServerInstallOptions): Promise<RequirementStatus>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { BaseInstaller } from './BaseInstaller.js';
|
|
2
|
+
import { handleGitHubRelease, getGitHubLatestVersion } from '../../../utils/githubUtils.js';
|
|
3
|
+
import { compareVersions } from '../../../utils/versionUtils.js';
|
|
4
|
+
import { Logger } from '../../../utils/logger.js';
|
|
5
|
+
import * as RecordingConstants from '../../metadatas/recordingConstants.js';
|
|
6
|
+
import { ensureDotnetToolsInPath } from '../../../utils/osUtils.js';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
/**
|
|
10
|
+
* Installer implementation for .NET packages using NuGet
|
|
11
|
+
*/
|
|
12
|
+
export class NugetInstaller extends BaseInstaller {
|
|
13
|
+
/**
|
|
14
|
+
* Check if this installer can handle the given requirement type
|
|
15
|
+
* @param requirement The requirement to check
|
|
16
|
+
* @returns True if this installer can handle the requirement
|
|
17
|
+
*/
|
|
18
|
+
canHandle(requirement) {
|
|
19
|
+
return requirement.type === 'nuget';
|
|
20
|
+
}
|
|
21
|
+
supportCheckUpdates() {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the latest version available for the NuGet package.
|
|
26
|
+
* @param requirement The requirement to check.
|
|
27
|
+
* @param _options Optional server install options (not used for NuGet).
|
|
28
|
+
* @returns The latest version string, or undefined if not found or not applicable.
|
|
29
|
+
*/
|
|
30
|
+
async getLatestVersion(requirement, _options) {
|
|
31
|
+
if (requirement.registry && requirement.registry.githubRelease) {
|
|
32
|
+
return getGitHubLatestVersion(this.execPromise, requirement.registry.githubRelease.repository);
|
|
33
|
+
}
|
|
34
|
+
// Artifacts registry is not supported for nuget tools
|
|
35
|
+
if (requirement.registry && requirement.registry.artifacts) {
|
|
36
|
+
Logger.warn(`Artifacts registry is not supported for NuGet tool '${requirement.name}'.`);
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
// Default behavior: Nuget tools are often specific versions from specific sources,
|
|
40
|
+
// or global tools might not have a central "latest version" query like pip/npm.
|
|
41
|
+
// Returning current version if specified, otherwise undefined.
|
|
42
|
+
Logger.warn(`Direct latest version check for NuGet tool '${requirement.name}' without a GitHub release registry is not supported. Please specify a version or use a GitHub release.`);
|
|
43
|
+
return requirement.version || undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if the .NET tool is already installed
|
|
47
|
+
* @param requirement The requirement to check
|
|
48
|
+
* @param _options Optional server install options (not used for NuGet)
|
|
49
|
+
* @returns The status of the requirement
|
|
50
|
+
*/
|
|
51
|
+
async checkInstallation(requirement, _options) {
|
|
52
|
+
try {
|
|
53
|
+
// Command: dotnet tool list -g
|
|
54
|
+
// Output:
|
|
55
|
+
// Package Id Version Commands
|
|
56
|
+
// -----------------------------------------
|
|
57
|
+
// jarvistools 1.0.0 jarvistools
|
|
58
|
+
const { stdout } = await this.execPromise(`dotnet tool list -g`);
|
|
59
|
+
const lines = stdout.split('\n');
|
|
60
|
+
let installedVersion;
|
|
61
|
+
let isInstalled = false;
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const parts = line.trim().split(/\s+/);
|
|
64
|
+
if (parts.length >= 2 && parts[0].toLowerCase() === requirement.name.toLowerCase()) {
|
|
65
|
+
installedVersion = parts[1];
|
|
66
|
+
isInstalled = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name: requirement.name,
|
|
72
|
+
type: 'nuget',
|
|
73
|
+
installed: isInstalled,
|
|
74
|
+
version: installedVersion,
|
|
75
|
+
inProgress: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// If 'dotnet tool list -g' fails, it might mean dotnet CLI is not properly installed or configured.
|
|
80
|
+
// Or it could mean no tools are installed, which in some dotnet versions might return non-zero exit code.
|
|
81
|
+
// We'll assume not installed in case of error, but log it.
|
|
82
|
+
Logger.debug(`Error checking NuGet tool installation for ${requirement.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
return {
|
|
84
|
+
name: requirement.name,
|
|
85
|
+
type: 'nuget',
|
|
86
|
+
installed: false,
|
|
87
|
+
error: `Failed to check installation: ${error instanceof Error ? error.message : String(error)}`,
|
|
88
|
+
inProgress: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Install the .NET tool
|
|
94
|
+
* @param requirement The requirement to install
|
|
95
|
+
* @param recorder Optional InstallOperationManager for recording steps
|
|
96
|
+
* @param _options Optional server install options (not used for NuGet)
|
|
97
|
+
* @returns The status of the installation
|
|
98
|
+
*/
|
|
99
|
+
async install(requirement, recorder, _options) {
|
|
100
|
+
return await recorder.recording(async () => {
|
|
101
|
+
const status = await this.checkInstallation(requirement, _options);
|
|
102
|
+
if (status.installed && status.version && requirement.version &&
|
|
103
|
+
compareVersions(status.version, requirement.version) === 0 &&
|
|
104
|
+
!requirement.version.toLowerCase().includes('latest')) {
|
|
105
|
+
Logger.log(`NuGet tool ${requirement.name}==${status.version} already installed.`);
|
|
106
|
+
return status;
|
|
107
|
+
}
|
|
108
|
+
let command;
|
|
109
|
+
if (requirement.registry && requirement.registry.githubRelease) {
|
|
110
|
+
const result = await handleGitHubRelease(requirement, requirement.registry.githubRelease);
|
|
111
|
+
// Nuget package name might be different from the requirement name if alias is used.
|
|
112
|
+
// However, dotnet tool install uses the package ID from the nupkg.
|
|
113
|
+
// We assume requirement.name is the package ID.
|
|
114
|
+
const packageId = requirement.name;
|
|
115
|
+
const resolvedDir = fs.existsSync(result.resolvedPath) && fs.lstatSync(result.resolvedPath).isDirectory() ? result.resolvedPath : path.dirname(result.resolvedPath);
|
|
116
|
+
if (requirement.version && !requirement.version.toLowerCase().includes('latest')) {
|
|
117
|
+
command = `dotnet tool install --global --add-source "${resolvedDir}" ${packageId} --version ${requirement.version}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Install latest from the source
|
|
121
|
+
command = `dotnet tool install --global --add-source "${resolvedDir}" ${packageId}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (requirement.registry && requirement.registry.artifacts) {
|
|
125
|
+
const errorMessage = `Artifacts registry is not supported for NuGet tool yet'${requirement.name}'. Only GitHubRelease is supported.`;
|
|
126
|
+
Logger.error(errorMessage);
|
|
127
|
+
await recorder.recordStep('NugetInstaller:RegistryConfig', 'failed', errorMessage);
|
|
128
|
+
throw new Error(errorMessage);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Default installation from nuget.org or configured feeds
|
|
132
|
+
if (requirement.version && !requirement.version.toLowerCase().includes('latest')) {
|
|
133
|
+
command = `dotnet tool install --global ${requirement.name} --version ${requirement.version}`;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
command = `dotnet tool install --global ${requirement.name}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return await recorder.recording(async () => {
|
|
140
|
+
const { stdout, stderr } = await this.execPromise(command);
|
|
141
|
+
if (stderr && !stdout.toLowerCase().includes('already installed')) { // Some warnings might go to stderr
|
|
142
|
+
Logger.debug(`NuGet tool installation stderr for ${requirement.name}: ${stderr}`);
|
|
143
|
+
// Check if it was actually an error or just a warning
|
|
144
|
+
const checkStatus = await this.checkInstallation(requirement, _options);
|
|
145
|
+
if (!checkStatus.installed) {
|
|
146
|
+
Logger.error(`NuGet tool ${requirement.name} not found after install command, stderr: ${stderr}`);
|
|
147
|
+
throw new Error(`NuGet tool installation failed with: ${stderr}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const finalStatus = await this.checkInstallation(requirement, _options);
|
|
151
|
+
if (!finalStatus.installed) {
|
|
152
|
+
throw new Error(`NuGet tool ${requirement.name} failed to install. Please check logs.`);
|
|
153
|
+
}
|
|
154
|
+
// After successful installation, ensure .NET tools path is in system PATH
|
|
155
|
+
await ensureDotnetToolsInPath();
|
|
156
|
+
return {
|
|
157
|
+
name: requirement.name,
|
|
158
|
+
type: 'nuget',
|
|
159
|
+
installed: true,
|
|
160
|
+
version: finalStatus.version || requirement.version, // Use checked version if available
|
|
161
|
+
inProgress: false,
|
|
162
|
+
};
|
|
163
|
+
}, {
|
|
164
|
+
stepName: `${RecordingConstants.STEP_INSTALL_COMMAND_PREFIX}: ${requirement.name} : ${requirement.version || 'latest'}`,
|
|
165
|
+
inProgressMessage: `Running: ${command}`,
|
|
166
|
+
endMessage: (result) => result.installed ? `Succeeded: ${command}` : `Failed: ${command}`,
|
|
167
|
+
});
|
|
168
|
+
}, {
|
|
169
|
+
stepName: RecordingConstants.STEP_NUGET_INSTALLER_INSTALL,
|
|
170
|
+
inProgressMessage: `Installing NuGet tool: ${requirement.name}`,
|
|
171
|
+
endMessage: (result) => result.installed
|
|
172
|
+
? `Install completed for ${requirement.name} with version ${result.version}`
|
|
173
|
+
: `Install failed for ${requirement.name}`,
|
|
174
|
+
onError: (error) => {
|
|
175
|
+
return {
|
|
176
|
+
result: {
|
|
177
|
+
name: requirement.name,
|
|
178
|
+
type: 'nuget',
|
|
179
|
+
installed: false,
|
|
180
|
+
error: error instanceof Error ? error.message : String(error),
|
|
181
|
+
inProgress: false,
|
|
182
|
+
},
|
|
183
|
+
message: error instanceof Error ? error.message : String(error),
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=NugetInstaller.js.map
|
|
@@ -12,6 +12,8 @@ export declare const STEP_PROCESS_REQUIREMENT_UPDATES = "Processing all requirem
|
|
|
12
12
|
export declare const STEP_CHECKING_REQUIREMENT_STATUS = "Checking the status of requirement";
|
|
13
13
|
/** Step for installing requirements in the background process. */
|
|
14
14
|
export declare const STEP_INSTALLING_REQUIREMENTS_IN_BACKGROUND = "Installing requirements in the background";
|
|
15
|
+
/** Step for running the install logic in the NugetInstaller. */
|
|
16
|
+
export declare const STEP_NUGET_INSTALLER_INSTALL = "Running install in NugetInstaller";
|
|
15
17
|
/** Step for checking and installing all requirements as needed. */
|
|
16
18
|
export declare const STEP_CHECK_AND_INSTALL_REQUIREMENTS = "Checking and installing all requirements";
|
|
17
19
|
/** Step for running the install logic in the CommandInstaller. */
|
|
@@ -12,6 +12,8 @@ export const STEP_PROCESS_REQUIREMENT_UPDATES = 'Processing all requirement upda
|
|
|
12
12
|
export const STEP_CHECKING_REQUIREMENT_STATUS = 'Checking the status of requirement';
|
|
13
13
|
/** Step for installing requirements in the background process. */
|
|
14
14
|
export const STEP_INSTALLING_REQUIREMENTS_IN_BACKGROUND = 'Installing requirements in the background';
|
|
15
|
+
/** Step for running the install logic in the NugetInstaller. */
|
|
16
|
+
export const STEP_NUGET_INSTALLER_INSTALL = 'Running install in NugetInstaller';
|
|
15
17
|
/** Step for checking and installing all requirements as needed. */
|
|
16
18
|
export const STEP_CHECK_AND_INSTALL_REQUIREMENTS = 'Checking and installing all requirements';
|
|
17
19
|
/** Step for running the install logic in the CommandInstaller. */
|
|
@@ -121,7 +121,7 @@ export interface RegistryConfig {
|
|
|
121
121
|
}
|
|
122
122
|
export interface RequirementConfig {
|
|
123
123
|
name: string;
|
|
124
|
-
type: 'npm' | 'pip' | 'command' | 'extension' | 'other';
|
|
124
|
+
type: 'npm' | 'pip' | 'command' | 'extension' | 'nuget' | 'other';
|
|
125
125
|
alias?: string;
|
|
126
126
|
version: string;
|
|
127
127
|
registry?: RegistryConfig;
|
|
@@ -91,28 +91,7 @@ export class FeedOnboardService {
|
|
|
91
91
|
* @returns A promise that resolves to the operation status.
|
|
92
92
|
*/
|
|
93
93
|
async _initiateOperation(config, operationType, serverList, forExistingCategory) {
|
|
94
|
-
//
|
|
95
|
-
let existingOperation = await onboardStatusManager._findExistingNonCompletedOperation(config.name, operationType);
|
|
96
|
-
if (existingOperation) {
|
|
97
|
-
const fiveMinutesInMs = 5 * 60 * 1000;
|
|
98
|
-
const lastUpdateTimestamp = existingOperation.lastUpdated ? new Date(existingOperation.lastUpdated).getTime() : 0;
|
|
99
|
-
const currentTime = new Date().getTime();
|
|
100
|
-
if (lastUpdateTimestamp > 0 && (currentTime - lastUpdateTimestamp) > fiveMinutesInMs) {
|
|
101
|
-
Logger.log(`WARNING: [${existingOperation.onboardingId}] Found stale ${operationType} operation for feed: ${config.name} (last updated at: ${existingOperation.lastUpdated}). Proceeding to create a new operation.`);
|
|
102
|
-
existingOperation = undefined; // Treat as no existing operation for starting a new one
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
Logger.log(`[${existingOperation.onboardingId}] Found existing non-completed ${operationType} operation for feed: ${config.name}. Returning its status.`);
|
|
106
|
-
const lastStep = existingOperation.steps && existingOperation.steps.length > 0 ? existingOperation.steps[existingOperation.steps.length - 1].stepName : 'N/A';
|
|
107
|
-
return {
|
|
108
|
-
onboardingId: existingOperation.onboardingId,
|
|
109
|
-
status: existingOperation.status,
|
|
110
|
-
message: `An ${operationType} process for this feed (${existingOperation.onboardingId}) is already in status: ${existingOperation.status}. Last step: ${lastStep}`,
|
|
111
|
-
lastQueried: new Date().toISOString(),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// Then, check for successful operations with matching configuration
|
|
94
|
+
// Check for successful operations with matching configuration
|
|
116
95
|
const succeededOperation = await onboardStatusManager.findSucceededOperation(config.name, operationType, config);
|
|
117
96
|
if (succeededOperation) {
|
|
118
97
|
Logger.log(`[${succeededOperation.onboardingId}] Found existing successful ${operationType} operation for feed: ${config.name} with matching configuration.`);
|
|
@@ -235,11 +235,6 @@ export class StdioServerValidator {
|
|
|
235
235
|
const fullCommand = server.installation.command;
|
|
236
236
|
const [baseCommand, ...defaultArgs] = fullCommand.split(' ');
|
|
237
237
|
const args = [...defaultArgs, ...(server.installation.args || [])];
|
|
238
|
-
// Validate command exists and is executable
|
|
239
|
-
const isExecutable = await this.isCommandExecutable(baseCommand);
|
|
240
|
-
if (!isExecutable) {
|
|
241
|
-
throw new Error(`Command not found or not executable: ${baseCommand}`);
|
|
242
|
-
}
|
|
243
238
|
// Validate required environment variables if specified
|
|
244
239
|
const envVars = server.installation.env;
|
|
245
240
|
if (envVars) {
|
|
@@ -264,6 +259,11 @@ export class StdioServerValidator {
|
|
|
264
259
|
}
|
|
265
260
|
}
|
|
266
261
|
}
|
|
262
|
+
// Validate command exists and is executable
|
|
263
|
+
const isExecutable = await this.isCommandExecutable(baseCommand);
|
|
264
|
+
if (!isExecutable) {
|
|
265
|
+
throw new Error(`Command not found or not executable: ${baseCommand}`);
|
|
266
|
+
}
|
|
267
267
|
// Test server startup
|
|
268
268
|
const serverStarted = await this.testServerStartup(baseCommand, args, config.requirements?.find(r => r.type === 'npm'));
|
|
269
269
|
if (!serverStarted) {
|
package/dist/utils/osUtils.d.ts
CHANGED
|
@@ -36,3 +36,14 @@ export declare function getGlobalNPMPackagePath(): string;
|
|
|
36
36
|
* Returns the directory containing the npm executable, or a platform-appropriate default if not found.
|
|
37
37
|
*/
|
|
38
38
|
export declare function getNpmExecutablePath(): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Ensures the .NET global tools path is added to the system PATH for the current user
|
|
41
|
+
* on macOS and Linux by modifying shell configuration files.
|
|
42
|
+
* This function is idempotent and will not add the path if it already exists.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ensureDotnetToolsInPath(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
* @returns The path to the .dotnet global tools directory.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getDotnetGlobalToolsPath(): string;
|
package/dist/utils/osUtils.js
CHANGED
|
@@ -562,4 +562,104 @@ export async function getNpmExecutablePath() {
|
|
|
562
562
|
}
|
|
563
563
|
}
|
|
564
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Ensures the .NET global tools path is added to the system PATH for the current user
|
|
567
|
+
* on macOS and Linux by modifying shell configuration files.
|
|
568
|
+
* This function is idempotent and will not add the path if it already exists.
|
|
569
|
+
*/
|
|
570
|
+
export async function ensureDotnetToolsInPath() {
|
|
571
|
+
const osType = getOSType();
|
|
572
|
+
Logger.debug({
|
|
573
|
+
action: 'ensure_dotnet_tools_in_path',
|
|
574
|
+
osType
|
|
575
|
+
});
|
|
576
|
+
if (osType === OSType.Windows) {
|
|
577
|
+
Logger.debug('.NET tools PATH is usually handled by the installer on Windows. Skipping explicit PATH modification.');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (osType === OSType.MacOS || osType === OSType.Linux) {
|
|
581
|
+
const dotnetToolsPath = getDotnetGlobalToolsPath(); // This is typically ~/.dotnet/tools
|
|
582
|
+
const exportLine = `export PATH="${dotnetToolsPath}:$PATH"`;
|
|
583
|
+
// Determine shell configuration files to check/update
|
|
584
|
+
const shellConfigFiles = [];
|
|
585
|
+
const shell = process.env.SHELL;
|
|
586
|
+
if (shell && shell.includes('zsh')) {
|
|
587
|
+
shellConfigFiles.push(path.join(os.homedir(), '.zshrc'));
|
|
588
|
+
}
|
|
589
|
+
else if (shell && shell.includes('bash')) {
|
|
590
|
+
shellConfigFiles.push(path.join(os.homedir(), '.bashrc'));
|
|
591
|
+
// .bash_profile is often sourced by .bashrc or used for login shells
|
|
592
|
+
shellConfigFiles.push(path.join(os.homedir(), '.bash_profile'));
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
// Fallback for other shells or if SHELL is not set
|
|
596
|
+
shellConfigFiles.push(path.join(os.homedir(), '.profile'));
|
|
597
|
+
shellConfigFiles.push(path.join(os.homedir(), '.bashrc'));
|
|
598
|
+
shellConfigFiles.push(path.join(os.homedir(), '.zshrc'));
|
|
599
|
+
}
|
|
600
|
+
let updatedAnyFile = false;
|
|
601
|
+
for (const configFile of shellConfigFiles) {
|
|
602
|
+
try {
|
|
603
|
+
if (fs.existsSync(configFile)) {
|
|
604
|
+
const content = await fs.promises.readFile(configFile, 'utf-8');
|
|
605
|
+
// Check if the exact line or a line setting dotnetToolsPath in PATH exists
|
|
606
|
+
// A more robust check might involve parsing, but this covers common cases.
|
|
607
|
+
// Regex to check if dotnetToolsPath is part of an PATH export, avoiding duplicates.
|
|
608
|
+
// It looks for `export PATH=...$dotnetToolsPath...` or `export PATH=...${HOME}/.dotnet/tools...`
|
|
609
|
+
const pathRegex = new RegExp(`export\\s+PATH=.*${dotnetToolsPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*`);
|
|
610
|
+
const homePathRegex = new RegExp(`export\\s+PATH=.*\\$HOME\\/\\.dotnet\\/tools.*`);
|
|
611
|
+
if (!pathRegex.test(content) && !homePathRegex.test(content)) {
|
|
612
|
+
Logger.debug(`Adding .NET tools path to ${configFile}`);
|
|
613
|
+
await fs.promises.appendFile(configFile, `\n# Add .NET Core SDK tools to PATH\n${exportLine}\n`);
|
|
614
|
+
Logger.log(`Appended '${exportLine}' to ${configFile}. You may need to source this file or restart your terminal.`);
|
|
615
|
+
updatedAnyFile = true;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
Logger.debug(`.NET tools path already configured in ${configFile}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
else if (shellConfigFiles.length === 1 || configFile === path.join(os.homedir(), '.profile')) {
|
|
622
|
+
// If it's the only config file determined or it's .profile, and it doesn't exist, create it.
|
|
623
|
+
Logger.debug(`${configFile} does not exist. Creating and adding .NET tools path.`);
|
|
624
|
+
await fs.promises.writeFile(configFile, `# Add .NET Core SDK tools to PATH\n${exportLine}\n`);
|
|
625
|
+
Logger.log(`Created ${configFile} and added '${exportLine}'. You may need to source this file or restart your terminal.`);
|
|
626
|
+
updatedAnyFile = true;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
Logger.error(`Failed to update ${configFile} for .NET tools PATH: ${error instanceof Error ? error.message : String(error)}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (updatedAnyFile) {
|
|
634
|
+
Logger.log(`Dotnet tools path has been added to shell configuration. Please source the relevant file (e.g., 'source ~/.zshrc') or restart your terminal session for changes to take effect.`);
|
|
635
|
+
}
|
|
636
|
+
// Also update the current process's PATH environment variable
|
|
637
|
+
if (process.env.PATH && !process.env.PATH.includes(dotnetToolsPath)) {
|
|
638
|
+
process.env.PATH = `${dotnetToolsPath}:${process.env.PATH}`;
|
|
639
|
+
Logger.debug(`Updated current process.env.PATH to include: ${dotnetToolsPath}`);
|
|
640
|
+
}
|
|
641
|
+
else if (!process.env.PATH) {
|
|
642
|
+
process.env.PATH = dotnetToolsPath;
|
|
643
|
+
Logger.debug(`Set current process.env.PATH to: ${dotnetToolsPath}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
*
|
|
649
|
+
* @returns The path to the .dotnet global tools directory.
|
|
650
|
+
*/
|
|
651
|
+
export function getDotnetGlobalToolsPath() {
|
|
652
|
+
const homeDir = os.homedir();
|
|
653
|
+
if (process.platform === 'win32') {
|
|
654
|
+
// Example: C:\Users\YourName\.dotnet\tools
|
|
655
|
+
return path.join(homeDir, '.dotnet', 'tools');
|
|
656
|
+
}
|
|
657
|
+
else if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
658
|
+
// macOS or Linux: ~/.dotnet/tools
|
|
659
|
+
return path.join(homeDir, '.dotnet', 'tools');
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
565
665
|
//# sourceMappingURL=osUtils.js.map
|
|
@@ -235,6 +235,7 @@ export const serverRequirementTemplate = (serverIndex, reqIndex, isReadOnly = fa
|
|
|
235
235
|
<option value="">Select Type</option>
|
|
236
236
|
<option value="npm">NPM Package</option>
|
|
237
237
|
<option value="pip">PIP Package</option>
|
|
238
|
+
<option value="nuget">Nuget Package</option>
|
|
238
239
|
<option value="command">Command</option>
|
|
239
240
|
</select>
|
|
240
241
|
</div>
|
|
@@ -158,16 +158,16 @@ function renderServerCategoryList(servers) {
|
|
|
158
158
|
|
|
159
159
|
return `
|
|
160
160
|
<div class="server-item border border-gray-200 p-3 rounded hover:bg-gray-50 cursor-pointer transition duration-150 ease-in-out ${pinnedClass}"
|
|
161
|
-
data-server-name="${server.name}">
|
|
161
|
+
data-server-name="${server.name}" onclick="navigateToCategory('${server.name}')">
|
|
162
162
|
<div class="flex justify-between items-center">
|
|
163
|
-
<h3 class="font-semibold text-gray-800"
|
|
163
|
+
<h3 class="font-semibold text-gray-800">${server.displayName || server.name}</h3>
|
|
164
164
|
<div class="flex items-center">
|
|
165
165
|
<div class="pin-button ${pinnedClass}" onclick="togglePinCategoryItem('${server.name}', event)" title="${isPinned ? 'Unpin' : 'Pin'} this category">
|
|
166
166
|
${pinIcon}
|
|
167
167
|
</div>
|
|
168
168
|
</div>
|
|
169
169
|
</div>
|
|
170
|
-
<div class="text-sm text-gray-500 flex items-center mt-1"
|
|
170
|
+
<div class="text-sm text-gray-500 flex items-center mt-1">
|
|
171
171
|
${statusHtml}
|
|
172
172
|
${systemTagsHtml}
|
|
173
173
|
</div>
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { RequirementConfig, RequirementStatus, ServerInstallOptions } from '../.
|
|
|
2
2
|
import { RequirementInstaller } from './RequirementInstaller.js';
|
|
3
3
|
import { NpmInstaller } from './NpmInstaller.js';
|
|
4
4
|
import { PipInstaller } from './PipInstaller.js';
|
|
5
|
+
import { NugetInstaller } from './NugetInstaller.js';
|
|
5
6
|
import { CommandInstaller } from './CommandInstaller.js';
|
|
6
7
|
import { GeneralInstaller } from './GeneralInstaller.js';
|
|
7
8
|
import { exec } from 'child_process';
|
|
@@ -30,6 +31,7 @@ export class InstallerFactory {
|
|
|
30
31
|
private registerDefaultInstallers(): void {
|
|
31
32
|
this.registerInstaller(new NpmInstaller(this.execPromise));
|
|
32
33
|
this.registerInstaller(new PipInstaller(this.execPromise));
|
|
34
|
+
this.registerInstaller(new NugetInstaller(this.execPromise));
|
|
33
35
|
this.registerInstaller(new CommandInstaller(this.execPromise));
|
|
34
36
|
this.registerInstaller(new GeneralInstaller(this.execPromise));
|
|
35
37
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { RequirementConfig, RequirementStatus, ServerInstallOptions } from '../../metadatas/types.js';
|
|
2
|
+
import { BaseInstaller } from './BaseInstaller.js';
|
|
3
|
+
import { handleGitHubRelease, getGitHubLatestVersion } from '../../../utils/githubUtils.js';
|
|
4
|
+
import { compareVersions } from '../../../utils/versionUtils.js';
|
|
5
|
+
import { Logger } from '../../../utils/logger.js';
|
|
6
|
+
import { InstallOperationManager } from '../../loaders/InstallOperationManager.js';
|
|
7
|
+
import * as RecordingConstants from '../../metadatas/recordingConstants.js';
|
|
8
|
+
import { ensureDotnetToolsInPath } from '../../../utils/osUtils.js';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Installer implementation for .NET packages using NuGet
|
|
14
|
+
*/
|
|
15
|
+
export class NugetInstaller extends BaseInstaller {
|
|
16
|
+
/**
|
|
17
|
+
* Check if this installer can handle the given requirement type
|
|
18
|
+
* @param requirement The requirement to check
|
|
19
|
+
* @returns True if this installer can handle the requirement
|
|
20
|
+
*/
|
|
21
|
+
canHandle(requirement: RequirementConfig): boolean {
|
|
22
|
+
return requirement.type === 'nuget';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
supportCheckUpdates(): boolean {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the latest version available for the NuGet package.
|
|
31
|
+
* @param requirement The requirement to check.
|
|
32
|
+
* @param _options Optional server install options (not used for NuGet).
|
|
33
|
+
* @returns The latest version string, or undefined if not found or not applicable.
|
|
34
|
+
*/
|
|
35
|
+
async getLatestVersion(requirement: RequirementConfig, _options?: ServerInstallOptions): Promise<string | undefined> {
|
|
36
|
+
if (requirement.registry && requirement.registry.githubRelease) {
|
|
37
|
+
return getGitHubLatestVersion(this.execPromise, requirement.registry.githubRelease.repository);
|
|
38
|
+
}
|
|
39
|
+
// Artifacts registry is not supported for nuget tools
|
|
40
|
+
if (requirement.registry && requirement.registry.artifacts) {
|
|
41
|
+
Logger.warn(`Artifacts registry is not supported for NuGet tool '${requirement.name}'.`);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
// Default behavior: Nuget tools are often specific versions from specific sources,
|
|
45
|
+
// or global tools might not have a central "latest version" query like pip/npm.
|
|
46
|
+
// Returning current version if specified, otherwise undefined.
|
|
47
|
+
Logger.warn(`Direct latest version check for NuGet tool '${requirement.name}' without a GitHub release registry is not supported. Please specify a version or use a GitHub release.`);
|
|
48
|
+
return requirement.version || undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if the .NET tool is already installed
|
|
53
|
+
* @param requirement The requirement to check
|
|
54
|
+
* @param _options Optional server install options (not used for NuGet)
|
|
55
|
+
* @returns The status of the requirement
|
|
56
|
+
*/
|
|
57
|
+
async checkInstallation(requirement: RequirementConfig, _options?: ServerInstallOptions): Promise<RequirementStatus> {
|
|
58
|
+
try {
|
|
59
|
+
// Command: dotnet tool list -g
|
|
60
|
+
// Output:
|
|
61
|
+
// Package Id Version Commands
|
|
62
|
+
// -----------------------------------------
|
|
63
|
+
// jarvistools 1.0.0 jarvistools
|
|
64
|
+
const { stdout } = await this.execPromise(`dotnet tool list -g`);
|
|
65
|
+
const lines = stdout.split('\n');
|
|
66
|
+
let installedVersion: string | undefined;
|
|
67
|
+
let isInstalled = false;
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const parts = line.trim().split(/\s+/);
|
|
71
|
+
if (parts.length >= 2 && parts[0].toLowerCase() === requirement.name.toLowerCase()) {
|
|
72
|
+
installedVersion = parts[1];
|
|
73
|
+
isInstalled = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
name: requirement.name,
|
|
80
|
+
type: 'nuget',
|
|
81
|
+
installed: isInstalled,
|
|
82
|
+
version: installedVersion,
|
|
83
|
+
inProgress: false,
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// If 'dotnet tool list -g' fails, it might mean dotnet CLI is not properly installed or configured.
|
|
87
|
+
// Or it could mean no tools are installed, which in some dotnet versions might return non-zero exit code.
|
|
88
|
+
// We'll assume not installed in case of error, but log it.
|
|
89
|
+
Logger.debug(`Error checking NuGet tool installation for ${requirement.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
return {
|
|
91
|
+
name: requirement.name,
|
|
92
|
+
type: 'nuget',
|
|
93
|
+
installed: false,
|
|
94
|
+
error: `Failed to check installation: ${error instanceof Error ? error.message : String(error)}`,
|
|
95
|
+
inProgress: false,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Install the .NET tool
|
|
102
|
+
* @param requirement The requirement to install
|
|
103
|
+
* @param recorder Optional InstallOperationManager for recording steps
|
|
104
|
+
* @param _options Optional server install options (not used for NuGet)
|
|
105
|
+
* @returns The status of the installation
|
|
106
|
+
*/
|
|
107
|
+
async install(requirement: RequirementConfig, recorder: InstallOperationManager, _options?: ServerInstallOptions): Promise<RequirementStatus> {
|
|
108
|
+
return await recorder.recording(
|
|
109
|
+
async (): Promise<RequirementStatus> => {
|
|
110
|
+
const status = await this.checkInstallation(requirement, _options);
|
|
111
|
+
if (status.installed && status.version && requirement.version &&
|
|
112
|
+
compareVersions(status.version, requirement.version) === 0 &&
|
|
113
|
+
!requirement.version.toLowerCase().includes('latest')) {
|
|
114
|
+
Logger.log(`NuGet tool ${requirement.name}==${status.version} already installed.`);
|
|
115
|
+
return status;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let command: string;
|
|
119
|
+
|
|
120
|
+
if (requirement.registry && requirement.registry.githubRelease) {
|
|
121
|
+
const result = await handleGitHubRelease(requirement, requirement.registry.githubRelease);
|
|
122
|
+
// Nuget package name might be different from the requirement name if alias is used.
|
|
123
|
+
// However, dotnet tool install uses the package ID from the nupkg.
|
|
124
|
+
// We assume requirement.name is the package ID.
|
|
125
|
+
const packageId = requirement.name;
|
|
126
|
+
const resolvedDir = fs.existsSync(result.resolvedPath) && fs.lstatSync(result.resolvedPath).isDirectory() ? result.resolvedPath : path.dirname(result.resolvedPath);
|
|
127
|
+
|
|
128
|
+
if (requirement.version && !requirement.version.toLowerCase().includes('latest')) {
|
|
129
|
+
command = `dotnet tool install --global --add-source "${resolvedDir}" ${packageId} --version ${requirement.version}`;
|
|
130
|
+
} else {
|
|
131
|
+
// Install latest from the source
|
|
132
|
+
command = `dotnet tool install --global --add-source "${resolvedDir}" ${packageId}`;
|
|
133
|
+
}
|
|
134
|
+
} else if (requirement.registry && requirement.registry.artifacts) {
|
|
135
|
+
const errorMessage = `Artifacts registry is not supported for NuGet tool yet'${requirement.name}'. Only GitHubRelease is supported.`;
|
|
136
|
+
Logger.error(errorMessage);
|
|
137
|
+
await recorder.recordStep('NugetInstaller:RegistryConfig', 'failed', errorMessage);
|
|
138
|
+
throw new Error(errorMessage);
|
|
139
|
+
} else {
|
|
140
|
+
// Default installation from nuget.org or configured feeds
|
|
141
|
+
if (requirement.version && !requirement.version.toLowerCase().includes('latest')) {
|
|
142
|
+
command = `dotnet tool install --global ${requirement.name} --version ${requirement.version}`;
|
|
143
|
+
} else {
|
|
144
|
+
command = `dotnet tool install --global ${requirement.name}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return await recorder.recording(
|
|
149
|
+
async () => {
|
|
150
|
+
const { stdout, stderr } = await this.execPromise(command);
|
|
151
|
+
if (stderr && !stdout.toLowerCase().includes('already installed')) { // Some warnings might go to stderr
|
|
152
|
+
Logger.debug(`NuGet tool installation stderr for ${requirement.name}: ${stderr}`);
|
|
153
|
+
// Check if it was actually an error or just a warning
|
|
154
|
+
const checkStatus = await this.checkInstallation(requirement, _options);
|
|
155
|
+
if (!checkStatus.installed) {
|
|
156
|
+
Logger.error(`NuGet tool ${requirement.name} not found after install command, stderr: ${stderr}`);
|
|
157
|
+
throw new Error(`NuGet tool installation failed with: ${stderr}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const finalStatus = await this.checkInstallation(requirement, _options);
|
|
162
|
+
if (!finalStatus.installed) {
|
|
163
|
+
throw new Error(`NuGet tool ${requirement.name} failed to install. Please check logs.`);
|
|
164
|
+
}
|
|
165
|
+
// After successful installation, ensure .NET tools path is in system PATH
|
|
166
|
+
await ensureDotnetToolsInPath();
|
|
167
|
+
return {
|
|
168
|
+
name: requirement.name,
|
|
169
|
+
type: 'nuget',
|
|
170
|
+
installed: true,
|
|
171
|
+
version: finalStatus.version || requirement.version, // Use checked version if available
|
|
172
|
+
inProgress: false,
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
stepName: `${RecordingConstants.STEP_INSTALL_COMMAND_PREFIX}: ${requirement.name} : ${requirement.version || 'latest'}`,
|
|
177
|
+
inProgressMessage: `Running: ${command}`,
|
|
178
|
+
endMessage: (result) => result.installed ? `Succeeded: ${command}` : `Failed: ${command}`,
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
stepName: RecordingConstants.STEP_NUGET_INSTALLER_INSTALL,
|
|
184
|
+
inProgressMessage: `Installing NuGet tool: ${requirement.name}`,
|
|
185
|
+
endMessage: (result) => result.installed
|
|
186
|
+
? `Install completed for ${requirement.name} with version ${result.version}`
|
|
187
|
+
: `Install failed for ${requirement.name}`,
|
|
188
|
+
onError: (error) => {
|
|
189
|
+
return {
|
|
190
|
+
result: {
|
|
191
|
+
name: requirement.name,
|
|
192
|
+
type: 'nuget',
|
|
193
|
+
installed: false,
|
|
194
|
+
error: error instanceof Error ? error.message : String(error),
|
|
195
|
+
inProgress: false,
|
|
196
|
+
},
|
|
197
|
+
message: error instanceof Error ? error.message : String(error),
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -16,6 +16,9 @@ export const STEP_CHECKING_REQUIREMENT_STATUS = 'Checking the status of requirem
|
|
|
16
16
|
/** Step for installing requirements in the background process. */
|
|
17
17
|
export const STEP_INSTALLING_REQUIREMENTS_IN_BACKGROUND = 'Installing requirements in the background';
|
|
18
18
|
|
|
19
|
+
/** Step for running the install logic in the NugetInstaller. */
|
|
20
|
+
export const STEP_NUGET_INSTALLER_INSTALL = 'Running install in NugetInstaller';
|
|
21
|
+
|
|
19
22
|
/** Step for checking and installing all requirements as needed. */
|
|
20
23
|
export const STEP_CHECK_AND_INSTALL_REQUIREMENTS = 'Checking and installing all requirements';
|
|
21
24
|
|
|
@@ -139,7 +139,7 @@ export interface RegistryConfig {
|
|
|
139
139
|
|
|
140
140
|
export interface RequirementConfig {
|
|
141
141
|
name: string;
|
|
142
|
-
type: 'npm' | 'pip' | 'command' | 'extension' | 'other'; // Add other requirement types if needed
|
|
142
|
+
type: 'npm' | 'pip' | 'command' | 'extension' | 'nuget' | 'other'; // Add other requirement types if needed
|
|
143
143
|
alias?: string; // Alias for the command type
|
|
144
144
|
version: string;
|
|
145
145
|
registry?: RegistryConfig;
|
|
@@ -106,30 +106,7 @@ export class FeedOnboardService {
|
|
|
106
106
|
* @returns A promise that resolves to the operation status.
|
|
107
107
|
*/
|
|
108
108
|
private async _initiateOperation(config: FeedConfiguration, operationType: OperationType, serverList: string[], forExistingCategory?: boolean): Promise<OperationStatus & { feedConfiguration?: FeedConfiguration }> {
|
|
109
|
-
//
|
|
110
|
-
let existingOperation = await onboardStatusManager._findExistingNonCompletedOperation(config.name, operationType);
|
|
111
|
-
|
|
112
|
-
if (existingOperation) {
|
|
113
|
-
const fiveMinutesInMs = 5 * 60 * 1000;
|
|
114
|
-
const lastUpdateTimestamp = existingOperation.lastUpdated ? new Date(existingOperation.lastUpdated).getTime() : 0;
|
|
115
|
-
const currentTime = new Date().getTime();
|
|
116
|
-
|
|
117
|
-
if (lastUpdateTimestamp > 0 && (currentTime - lastUpdateTimestamp) > fiveMinutesInMs) {
|
|
118
|
-
Logger.log(`WARNING: [${existingOperation.onboardingId}] Found stale ${operationType} operation for feed: ${config.name} (last updated at: ${existingOperation.lastUpdated}). Proceeding to create a new operation.`);
|
|
119
|
-
existingOperation = undefined; // Treat as no existing operation for starting a new one
|
|
120
|
-
} else {
|
|
121
|
-
Logger.log(`[${existingOperation.onboardingId}] Found existing non-completed ${operationType} operation for feed: ${config.name}. Returning its status.`);
|
|
122
|
-
const lastStep = existingOperation.steps && existingOperation.steps.length > 0 ? existingOperation.steps[existingOperation.steps.length - 1].stepName : 'N/A';
|
|
123
|
-
return {
|
|
124
|
-
onboardingId: existingOperation.onboardingId,
|
|
125
|
-
status: existingOperation.status,
|
|
126
|
-
message: `An ${operationType} process for this feed (${existingOperation.onboardingId}) is already in status: ${existingOperation.status}. Last step: ${lastStep}`,
|
|
127
|
-
lastQueried: new Date().toISOString(),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Then, check for successful operations with matching configuration
|
|
109
|
+
// Check for successful operations with matching configuration
|
|
133
110
|
const succeededOperation = await onboardStatusManager.findSucceededOperation(config.name, operationType, config);
|
|
134
111
|
if (succeededOperation) {
|
|
135
112
|
Logger.log(`[${succeededOperation.onboardingId}] Found existing successful ${operationType} operation for feed: ${config.name} with matching configuration.`);
|
|
@@ -262,11 +262,6 @@ export class StdioServerValidator implements IServerValidator {
|
|
|
262
262
|
const [baseCommand, ...defaultArgs] = fullCommand.split(' ');
|
|
263
263
|
const args = [...defaultArgs, ...(server.installation.args || [])];
|
|
264
264
|
|
|
265
|
-
// Validate command exists and is executable
|
|
266
|
-
const isExecutable = await this.isCommandExecutable(baseCommand);
|
|
267
|
-
if (!isExecutable) {
|
|
268
|
-
throw new Error(`Command not found or not executable: ${baseCommand}`);
|
|
269
|
-
}
|
|
270
265
|
|
|
271
266
|
// Validate required environment variables if specified
|
|
272
267
|
const envVars = server.installation.env;
|
|
@@ -295,6 +290,12 @@ export class StdioServerValidator implements IServerValidator {
|
|
|
295
290
|
}
|
|
296
291
|
}
|
|
297
292
|
}
|
|
293
|
+
// Validate command exists and is executable
|
|
294
|
+
const isExecutable = await this.isCommandExecutable(baseCommand);
|
|
295
|
+
if (!isExecutable) {
|
|
296
|
+
throw new Error(`Command not found or not executable: ${baseCommand}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
298
299
|
// Test server startup
|
|
299
300
|
const serverStarted = await this.testServerStartup(baseCommand, args, config.requirements?.find(r => r.type === 'npm'));
|
|
300
301
|
if (!serverStarted) {
|
package/src/utils/osUtils.ts
CHANGED
|
@@ -594,4 +594,107 @@ export async function getNpmExecutablePath(): Promise<string> {
|
|
|
594
594
|
return '/usr/local/bin';
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
|
-
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Ensures the .NET global tools path is added to the system PATH for the current user
|
|
601
|
+
* on macOS and Linux by modifying shell configuration files.
|
|
602
|
+
* This function is idempotent and will not add the path if it already exists.
|
|
603
|
+
*/
|
|
604
|
+
export async function ensureDotnetToolsInPath(): Promise<void> {
|
|
605
|
+
const osType = getOSType();
|
|
606
|
+
Logger.debug({
|
|
607
|
+
action: 'ensure_dotnet_tools_in_path',
|
|
608
|
+
osType
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
if (osType === OSType.Windows) {
|
|
612
|
+
Logger.debug('.NET tools PATH is usually handled by the installer on Windows. Skipping explicit PATH modification.');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (osType === OSType.MacOS || osType === OSType.Linux) {
|
|
617
|
+
const dotnetToolsPath = getDotnetGlobalToolsPath(); // This is typically ~/.dotnet/tools
|
|
618
|
+
const exportLine = `export PATH="${dotnetToolsPath}:$PATH"`;
|
|
619
|
+
|
|
620
|
+
// Determine shell configuration files to check/update
|
|
621
|
+
const shellConfigFiles: string[] = [];
|
|
622
|
+
const shell = process.env.SHELL;
|
|
623
|
+
|
|
624
|
+
if (shell && shell.includes('zsh')) {
|
|
625
|
+
shellConfigFiles.push(path.join(os.homedir(), '.zshrc'));
|
|
626
|
+
} else if (shell && shell.includes('bash')) {
|
|
627
|
+
shellConfigFiles.push(path.join(os.homedir(), '.bashrc'));
|
|
628
|
+
// .bash_profile is often sourced by .bashrc or used for login shells
|
|
629
|
+
shellConfigFiles.push(path.join(os.homedir(), '.bash_profile'));
|
|
630
|
+
} else {
|
|
631
|
+
// Fallback for other shells or if SHELL is not set
|
|
632
|
+
shellConfigFiles.push(path.join(os.homedir(), '.profile'));
|
|
633
|
+
shellConfigFiles.push(path.join(os.homedir(), '.bashrc'));
|
|
634
|
+
shellConfigFiles.push(path.join(os.homedir(), '.zshrc'));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let updatedAnyFile = false;
|
|
638
|
+
|
|
639
|
+
for (const configFile of shellConfigFiles) {
|
|
640
|
+
try {
|
|
641
|
+
if (fs.existsSync(configFile)) {
|
|
642
|
+
const content = await fs.promises.readFile(configFile, 'utf-8');
|
|
643
|
+
// Check if the exact line or a line setting dotnetToolsPath in PATH exists
|
|
644
|
+
// A more robust check might involve parsing, but this covers common cases.
|
|
645
|
+
// Regex to check if dotnetToolsPath is part of an PATH export, avoiding duplicates.
|
|
646
|
+
// It looks for `export PATH=...$dotnetToolsPath...` or `export PATH=...${HOME}/.dotnet/tools...`
|
|
647
|
+
const pathRegex = new RegExp(`export\\s+PATH=.*${dotnetToolsPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*`);
|
|
648
|
+
const homePathRegex = new RegExp(`export\\s+PATH=.*\\$HOME\\/\\.dotnet\\/tools.*`);
|
|
649
|
+
|
|
650
|
+
if (!pathRegex.test(content) && !homePathRegex.test(content)) {
|
|
651
|
+
Logger.debug(`Adding .NET tools path to ${configFile}`);
|
|
652
|
+
await fs.promises.appendFile(configFile, `\n# Add .NET Core SDK tools to PATH\n${exportLine}\n`);
|
|
653
|
+
Logger.log(`Appended '${exportLine}' to ${configFile}. You may need to source this file or restart your terminal.`);
|
|
654
|
+
updatedAnyFile = true;
|
|
655
|
+
} else {
|
|
656
|
+
Logger.debug(`.NET tools path already configured in ${configFile}`);
|
|
657
|
+
}
|
|
658
|
+
} else if (shellConfigFiles.length === 1 || configFile === path.join(os.homedir(), '.profile')) {
|
|
659
|
+
// If it's the only config file determined or it's .profile, and it doesn't exist, create it.
|
|
660
|
+
Logger.debug(`${configFile} does not exist. Creating and adding .NET tools path.`);
|
|
661
|
+
await fs.promises.writeFile(configFile, `# Add .NET Core SDK tools to PATH\n${exportLine}\n`);
|
|
662
|
+
Logger.log(`Created ${configFile} and added '${exportLine}'. You may need to source this file or restart your terminal.`);
|
|
663
|
+
updatedAnyFile = true;
|
|
664
|
+
}
|
|
665
|
+
} catch (error) {
|
|
666
|
+
Logger.error(`Failed to update ${configFile} for .NET tools PATH: ${error instanceof Error ? error.message : String(error)}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (updatedAnyFile) {
|
|
670
|
+
Logger.log(`Dotnet tools path has been added to shell configuration. Please source the relevant file (e.g., 'source ~/.zshrc') or restart your terminal session for changes to take effect.`);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Also update the current process's PATH environment variable
|
|
674
|
+
if (process.env.PATH && !process.env.PATH.includes(dotnetToolsPath)) {
|
|
675
|
+
process.env.PATH = `${dotnetToolsPath}:${process.env.PATH}`;
|
|
676
|
+
Logger.debug(`Updated current process.env.PATH to include: ${dotnetToolsPath}`);
|
|
677
|
+
} else if (!process.env.PATH) {
|
|
678
|
+
process.env.PATH = dotnetToolsPath;
|
|
679
|
+
Logger.debug(`Set current process.env.PATH to: ${dotnetToolsPath}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
*
|
|
685
|
+
* @returns The path to the .dotnet global tools directory.
|
|
686
|
+
*/
|
|
687
|
+
export function getDotnetGlobalToolsPath(): string {
|
|
688
|
+
const homeDir = os.homedir();
|
|
689
|
+
|
|
690
|
+
if (process.platform === 'win32') {
|
|
691
|
+
// Example: C:\Users\YourName\.dotnet\tools
|
|
692
|
+
return path.join(homeDir, '.dotnet', 'tools');
|
|
693
|
+
} else if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
694
|
+
// macOS or Linux: ~/.dotnet/tools
|
|
695
|
+
return path.join(homeDir, '.dotnet', 'tools');
|
|
696
|
+
} else {
|
|
697
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
@@ -235,6 +235,7 @@ export const serverRequirementTemplate = (serverIndex, reqIndex, isReadOnly = fa
|
|
|
235
235
|
<option value="">Select Type</option>
|
|
236
236
|
<option value="npm">NPM Package</option>
|
|
237
237
|
<option value="pip">PIP Package</option>
|
|
238
|
+
<option value="nuget">Nuget Package</option>
|
|
238
239
|
<option value="command">Command</option>
|
|
239
240
|
</select>
|
|
240
241
|
</div>
|
|
@@ -158,16 +158,16 @@ function renderServerCategoryList(servers) {
|
|
|
158
158
|
|
|
159
159
|
return `
|
|
160
160
|
<div class="server-item border border-gray-200 p-3 rounded hover:bg-gray-50 cursor-pointer transition duration-150 ease-in-out ${pinnedClass}"
|
|
161
|
-
data-server-name="${server.name}">
|
|
161
|
+
data-server-name="${server.name}" onclick="navigateToCategory('${server.name}')">
|
|
162
162
|
<div class="flex justify-between items-center">
|
|
163
|
-
<h3 class="font-semibold text-gray-800"
|
|
163
|
+
<h3 class="font-semibold text-gray-800">${server.displayName || server.name}</h3>
|
|
164
164
|
<div class="flex items-center">
|
|
165
165
|
<div class="pin-button ${pinnedClass}" onclick="togglePinCategoryItem('${server.name}', event)" title="${isPinned ? 'Unpin' : 'Pin'} this category">
|
|
166
166
|
${pinIcon}
|
|
167
167
|
</div>
|
|
168
168
|
</div>
|
|
169
169
|
</div>
|
|
170
|
-
<div class="text-sm text-gray-500 flex items-center mt-1"
|
|
170
|
+
<div class="text-sm text-gray-500 flex items-center mt-1">
|
|
171
171
|
${statusHtml}
|
|
172
172
|
${systemTagsHtml}
|
|
173
173
|
</div>
|