imcp 0.0.1
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/.github/ISSUE_TEMPLATE/JitAccess.yml +28 -0
- package/.github/acl/access.yml +20 -0
- package/.github/compliance/inventory.yml +5 -0
- package/.github/policies/jit.yml +19 -0
- package/README.md +137 -0
- package/dist/cli/commands/install.d.ts +2 -0
- package/dist/cli/commands/install.js +105 -0
- package/dist/cli/commands/list.d.ts +2 -0
- package/dist/cli/commands/list.js +90 -0
- package/dist/cli/commands/pull.d.ts +2 -0
- package/dist/cli/commands/pull.js +17 -0
- package/dist/cli/commands/serve.d.ts +2 -0
- package/dist/cli/commands/serve.js +32 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.js +32 -0
- package/dist/cli/commands/sync.d.ts +2 -0
- package/dist/cli/commands/sync.js +17 -0
- package/dist/cli/commands/uninstall.d.ts +2 -0
- package/dist/cli/commands/uninstall.js +39 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +114 -0
- package/dist/core/ConfigurationProvider.d.ts +31 -0
- package/dist/core/ConfigurationProvider.js +416 -0
- package/dist/core/InstallationService.d.ts +17 -0
- package/dist/core/InstallationService.js +144 -0
- package/dist/core/MCPManager.d.ts +17 -0
- package/dist/core/MCPManager.js +98 -0
- package/dist/core/RequirementService.d.ts +45 -0
- package/dist/core/RequirementService.js +123 -0
- package/dist/core/constants.d.ts +29 -0
- package/dist/core/constants.js +55 -0
- package/dist/core/installers/BaseInstaller.d.ts +73 -0
- package/dist/core/installers/BaseInstaller.js +247 -0
- package/dist/core/installers/ClientInstaller.d.ts +17 -0
- package/dist/core/installers/ClientInstaller.js +307 -0
- package/dist/core/installers/CommandInstaller.d.ts +36 -0
- package/dist/core/installers/CommandInstaller.js +170 -0
- package/dist/core/installers/GeneralInstaller.d.ts +32 -0
- package/dist/core/installers/GeneralInstaller.js +87 -0
- package/dist/core/installers/InstallerFactory.d.ts +52 -0
- package/dist/core/installers/InstallerFactory.js +95 -0
- package/dist/core/installers/NpmInstaller.d.ts +25 -0
- package/dist/core/installers/NpmInstaller.js +123 -0
- package/dist/core/installers/PipInstaller.d.ts +25 -0
- package/dist/core/installers/PipInstaller.js +114 -0
- package/dist/core/installers/RequirementInstaller.d.ts +32 -0
- package/dist/core/installers/RequirementInstaller.js +3 -0
- package/dist/core/installers/index.d.ts +6 -0
- package/dist/core/installers/index.js +7 -0
- package/dist/core/types.d.ts +152 -0
- package/dist/core/types.js +16 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +19 -0
- package/dist/services/InstallRequestValidator.d.ts +21 -0
- package/dist/services/InstallRequestValidator.js +99 -0
- package/dist/services/ServerService.d.ts +47 -0
- package/dist/services/ServerService.js +145 -0
- package/dist/utils/UpdateCheckTracker.d.ts +39 -0
- package/dist/utils/UpdateCheckTracker.js +80 -0
- package/dist/utils/clientUtils.d.ts +29 -0
- package/dist/utils/clientUtils.js +105 -0
- package/dist/utils/feedUtils.d.ts +5 -0
- package/dist/utils/feedUtils.js +29 -0
- package/dist/utils/githubAuth.d.ts +1 -0
- package/dist/utils/githubAuth.js +123 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.js +90 -0
- package/dist/utils/osUtils.d.ts +16 -0
- package/dist/utils/osUtils.js +235 -0
- package/dist/web/public/css/modal.css +250 -0
- package/dist/web/public/css/notifications.css +70 -0
- package/dist/web/public/index.html +157 -0
- package/dist/web/public/js/api.js +213 -0
- package/dist/web/public/js/modal.js +572 -0
- package/dist/web/public/js/notifications.js +99 -0
- package/dist/web/public/js/serverCategoryDetails.js +210 -0
- package/dist/web/public/js/serverCategoryList.js +82 -0
- package/dist/web/public/modal.html +61 -0
- package/dist/web/public/styles.css +155 -0
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.js +150 -0
- package/package.json +53 -0
- package/src/cli/commands/install.ts +140 -0
- package/src/cli/commands/list.ts +112 -0
- package/src/cli/commands/pull.ts +16 -0
- package/src/cli/commands/serve.ts +37 -0
- package/src/cli/commands/uninstall.ts +54 -0
- package/src/cli/index.ts +127 -0
- package/src/core/ConfigurationProvider.ts +489 -0
- package/src/core/InstallationService.ts +173 -0
- package/src/core/MCPManager.ts +134 -0
- package/src/core/RequirementService.ts +147 -0
- package/src/core/constants.ts +61 -0
- package/src/core/installers/BaseInstaller.ts +292 -0
- package/src/core/installers/ClientInstaller.ts +423 -0
- package/src/core/installers/CommandInstaller.ts +185 -0
- package/src/core/installers/GeneralInstaller.ts +89 -0
- package/src/core/installers/InstallerFactory.ts +109 -0
- package/src/core/installers/NpmInstaller.ts +128 -0
- package/src/core/installers/PipInstaller.ts +121 -0
- package/src/core/installers/RequirementInstaller.ts +38 -0
- package/src/core/installers/index.ts +9 -0
- package/src/core/types.ts +163 -0
- package/src/index.ts +44 -0
- package/src/services/InstallRequestValidator.ts +112 -0
- package/src/services/ServerService.ts +181 -0
- package/src/utils/UpdateCheckTracker.ts +86 -0
- package/src/utils/clientUtils.ts +112 -0
- package/src/utils/feedUtils.ts +31 -0
- package/src/utils/githubAuth.ts +142 -0
- package/src/utils/logger.ts +101 -0
- package/src/utils/osUtils.ts +250 -0
- package/src/web/public/css/modal.css +250 -0
- package/src/web/public/css/notifications.css +70 -0
- package/src/web/public/index.html +157 -0
- package/src/web/public/js/api.js +213 -0
- package/src/web/public/js/modal.js +572 -0
- package/src/web/public/js/notifications.js +99 -0
- package/src/web/public/js/serverCategoryDetails.js +210 -0
- package/src/web/public/js/serverCategoryList.js +82 -0
- package/src/web/public/modal.html +61 -0
- package/src/web/public/styles.css +155 -0
- package/src/web/server.ts +195 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { FeedConfiguration, EnvVariableConfig, ServerInstallOptions } from '../core/types.js';
|
|
4
|
+
import { InstallServersRequestBody } from '../web/server.js'; // Assuming InstallRequestBody is defined here
|
|
5
|
+
import { SUPPORTED_CLIENT_NAMES } from '../core/constants.js';
|
|
6
|
+
|
|
7
|
+
export class InstallRequestValidator {
|
|
8
|
+
private serverFeedConfigs: Map<string, FeedConfiguration> = new Map();
|
|
9
|
+
private feedDirectory: string;
|
|
10
|
+
|
|
11
|
+
constructor(feedDirectory: string = path.join(__dirname, '../feeds')) {
|
|
12
|
+
this.feedDirectory = feedDirectory;
|
|
13
|
+
// Consider loading configs lazily or providing an explicit load method
|
|
14
|
+
// For now, let's assume pre-loading or loading on demand in validate
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private async loadServerFeedConfiguration(serverName: string): Promise<FeedConfiguration | undefined> {
|
|
18
|
+
if (this.serverFeedConfigs.has(serverName)) {
|
|
19
|
+
return this.serverFeedConfigs.get(serverName);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const filePath = path.join(this.feedDirectory, `${serverName}.json`);
|
|
23
|
+
try {
|
|
24
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
25
|
+
const config: FeedConfiguration = JSON.parse(fileContent);
|
|
26
|
+
// Basic validation of the loaded config structure could be added here
|
|
27
|
+
if (config && config.name === serverName) {
|
|
28
|
+
this.serverFeedConfigs.set(serverName, config);
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
console.error(`Configuration name mismatch in ${filePath}`);
|
|
32
|
+
return undefined;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
35
|
+
console.warn(`Server feed configuration not found for: ${serverName} at ${filePath}`);
|
|
36
|
+
} else {
|
|
37
|
+
console.error(`Error loading server feed configuration for ${serverName}:`, error);
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validates the install request body for a specific server.
|
|
45
|
+
* @param serverName The name of the server to validate against.
|
|
46
|
+
* @param requestBody The installation request body.
|
|
47
|
+
* @returns An array of error messages, or an empty array if validation passes.
|
|
48
|
+
*/
|
|
49
|
+
async validate(serverName: string, requestBody: ServerInstallOptions): Promise<string[]> {
|
|
50
|
+
const errors: string[] = [];
|
|
51
|
+
const config = await this.loadServerFeedConfiguration(serverName);
|
|
52
|
+
|
|
53
|
+
if (!config) {
|
|
54
|
+
errors.push(`Server configuration feed not found for '${serverName}'.`);
|
|
55
|
+
// Cannot perform further validation without the config
|
|
56
|
+
return errors;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 1.2 Validate required environment variables
|
|
60
|
+
config.mcpServers.forEach(mcp => {
|
|
61
|
+
if (mcp.installation?.env) {
|
|
62
|
+
Object.entries(mcp.installation.env).forEach(([envVar, envConfig]) => {
|
|
63
|
+
if (envConfig.Required) {
|
|
64
|
+
if (!requestBody.env || !(envVar in requestBody.env) || !requestBody.env[envVar]) {
|
|
65
|
+
errors.push(`Missing required environment variable '${envVar}' for server '${serverName}' (category: ${mcp.name}).`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 1.3 Validate target clients
|
|
73
|
+
if (!requestBody.targetClients || requestBody.targetClients.length === 0) {
|
|
74
|
+
errors.push(`Request body must include a non-empty 'targetClients' array for server '${serverName}'.`);
|
|
75
|
+
} else {
|
|
76
|
+
requestBody.targetClients.forEach((client: string) => {
|
|
77
|
+
if (!SUPPORTED_CLIENT_NAMES.includes(client)) {
|
|
78
|
+
errors.push(`Unsupported target client '${client}' specified for server '${serverName}'. Supported clients are: ${SUPPORTED_CLIENT_NAMES.join(', ')}.`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add other validation rules as needed
|
|
84
|
+
|
|
85
|
+
return errors;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validates the install request body for multiple servers.
|
|
90
|
+
* @param requestBody The installation request body containing multiple server names.
|
|
91
|
+
* @returns An object mapping server names to their validation errors. Empty arrays indicate success.
|
|
92
|
+
*/
|
|
93
|
+
async validateMultiple(requestBody: InstallServersRequestBody): Promise<Record<string, string[]>> {
|
|
94
|
+
const results: Record<string, string[]> = {};
|
|
95
|
+
if (!requestBody.serverList || Object.keys(requestBody.serverList).length === 0) {
|
|
96
|
+
return { '_global': ['Request body must include a non-empty \'serverCategoryList\' record.'] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const serverName of Object.keys(requestBody.serverList)) {
|
|
100
|
+
results[serverName] = await this.validate(serverName, requestBody.serverList[serverName]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Helper __dirname for ES modules
|
|
108
|
+
import { fileURLToPath } from 'url';
|
|
109
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
110
|
+
|
|
111
|
+
// Export a singleton instance (optional, depends on usage pattern)
|
|
112
|
+
// export const installRequestValidator = new InstallRequestValidator();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import {
|
|
5
|
+
MCPServerCategory,
|
|
6
|
+
ServerInstallOptions,
|
|
7
|
+
ServerCategoryListOptions,
|
|
8
|
+
ServerOperationResult,
|
|
9
|
+
ServerUninstallOptions
|
|
10
|
+
} from '../core/types.js';
|
|
11
|
+
import { mcpManager } from '../core/MCPManager.js';
|
|
12
|
+
import { updateCheckTracker } from '../utils/UpdateCheckTracker.js';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ServerService provides a unified interface for server management operations.
|
|
18
|
+
* This layer handles business logic that's shared between the CLI and web interface.
|
|
19
|
+
*/
|
|
20
|
+
export class ServerService {
|
|
21
|
+
/**
|
|
22
|
+
* Lists available MCP servers based on the provided options
|
|
23
|
+
*/
|
|
24
|
+
async listServerCategories(options: ServerCategoryListOptions = {}): Promise<MCPServerCategory[]> {
|
|
25
|
+
return mcpManager.listServerCategories(options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gets a server by name
|
|
30
|
+
*/
|
|
31
|
+
async getServerCategory(categoryName: string): Promise<MCPServerCategory | undefined> {
|
|
32
|
+
const serverCategories = await this.listServerCategories();
|
|
33
|
+
const serverCategory = serverCategories.find(s => s.name === categoryName);
|
|
34
|
+
|
|
35
|
+
// Start async check for requirement updates if one isn't already in progress
|
|
36
|
+
if (serverCategory && serverCategory.feedConfiguration && serverCategory.name) {
|
|
37
|
+
// Check if update is already in progress using the tracker
|
|
38
|
+
const shouldCheckForUpdates = await updateCheckTracker.startOperation(serverCategory.name);
|
|
39
|
+
|
|
40
|
+
if (shouldCheckForUpdates) {
|
|
41
|
+
this.checkRequirementsForUpdate(serverCategory).catch(error => {
|
|
42
|
+
// Ensure we mark the operation as complete on error
|
|
43
|
+
if (serverCategory.name) {
|
|
44
|
+
updateCheckTracker.endOperation(serverCategory.name)
|
|
45
|
+
.catch(lockError => console.error(`Failed to mark update check as complete: ${lockError.message}`));
|
|
46
|
+
}
|
|
47
|
+
Logger.error(`Error checking requirements for updates: ${error.message}`);
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
Logger.debug(`Update check already in progress for ${serverCategory.name}, skipping`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return serverCategory;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check for updates to requirements for a server category
|
|
59
|
+
* @param serverCategory The server category to check
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
private async checkRequirementsForUpdate(serverCategory: MCPServerCategory): Promise<void> {
|
|
63
|
+
if (!serverCategory.name || !serverCategory.feedConfiguration?.requirements?.length) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const { requirementService } = await import('../core/RequirementService.js');
|
|
69
|
+
const { configProvider } = await import('../core/ConfigurationProvider.js');
|
|
70
|
+
|
|
71
|
+
for (const requirement of serverCategory.feedConfiguration.requirements) {
|
|
72
|
+
if (requirement.version.includes('latest')) {
|
|
73
|
+
// Get current status if available
|
|
74
|
+
const currentStatus = serverCategory.installationStatus?.requirementsStatus[requirement.name];
|
|
75
|
+
if (!currentStatus) continue;
|
|
76
|
+
|
|
77
|
+
// Check for updates
|
|
78
|
+
const updatedStatus = await requirementService.checkRequirementForUpdates(requirement);
|
|
79
|
+
|
|
80
|
+
// If update information is found, update the configuration
|
|
81
|
+
if (updatedStatus.availableUpdate && serverCategory.name) {
|
|
82
|
+
await configProvider.updateRequirementStatus(
|
|
83
|
+
serverCategory.name,
|
|
84
|
+
requirement.name,
|
|
85
|
+
updatedStatus
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Also update the in-memory status for immediate use
|
|
89
|
+
if (serverCategory.installationStatus?.requirementsStatus) {
|
|
90
|
+
serverCategory.installationStatus.requirementsStatus[requirement.name] = updatedStatus;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
// Always mark the operation as complete when done, even if there was an error
|
|
97
|
+
if (serverCategory.name) {
|
|
98
|
+
await updateCheckTracker.endOperation(serverCategory.name)
|
|
99
|
+
.catch(error => console.error(`Failed to mark update check as complete: ${error.message}`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Installs a specific mcp tool for a server.
|
|
106
|
+
* TODO: This might require enhancing MCPManager to handle category-specific installs.
|
|
107
|
+
*/
|
|
108
|
+
async installMcpServer(
|
|
109
|
+
category: string,
|
|
110
|
+
serverName: string,
|
|
111
|
+
options: ServerInstallOptions = {} // Reuse ServerInstallOptions for env etc.
|
|
112
|
+
): Promise<ServerOperationResult> {
|
|
113
|
+
Logger.debug(`Installing MCP server: ${JSON.stringify({ category, serverName, options })}`);
|
|
114
|
+
try {
|
|
115
|
+
const result = await mcpManager.installServer(category, serverName, options);
|
|
116
|
+
Logger.debug(`Installation result: ${JSON.stringify(result)}`);
|
|
117
|
+
return result;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
Logger.error(`Failed to install MCP server: ${serverName}`, error);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Installs a specific mcp tool for a server.
|
|
126
|
+
* TODO: This might require enhancing MCPManager to handle category-specific installs.
|
|
127
|
+
*/
|
|
128
|
+
async uninstallMcpServer(
|
|
129
|
+
category: string,
|
|
130
|
+
serverName: string,
|
|
131
|
+
options: ServerUninstallOptions = {} // Reuse ServerInstallOptions for env etc.
|
|
132
|
+
): Promise<ServerOperationResult> {
|
|
133
|
+
return mcpManager.uninstallServer(category, serverName, options);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validates server names
|
|
138
|
+
*/
|
|
139
|
+
async validateServerName(category: string, name: string): Promise<boolean> {
|
|
140
|
+
Logger.debug(`Validating server name: ${JSON.stringify({ category, name })}`);
|
|
141
|
+
|
|
142
|
+
// Check if category exists in feeds
|
|
143
|
+
const feedConfig = await mcpManager.getFeedConfiguration(category);
|
|
144
|
+
if (!feedConfig) {
|
|
145
|
+
Logger.debug(`Validation failed: Category "${category}" not found in feeds`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if server exists in the category's mcpServers
|
|
150
|
+
const serverExists = feedConfig.mcpServers.some(server => server.name === name);
|
|
151
|
+
if (!serverExists) {
|
|
152
|
+
Logger.debug(`Validation failed: Server "${name}" not found in category "${category}"`);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Formats success/error messages for operations
|
|
161
|
+
*/
|
|
162
|
+
formatOperationResults(results: ServerOperationResult[]): {
|
|
163
|
+
success: boolean;
|
|
164
|
+
messages: string[];
|
|
165
|
+
} {
|
|
166
|
+
return {
|
|
167
|
+
success: results.every(r => r.success),
|
|
168
|
+
messages: results.map(r => r.message).filter((m): m is string => m !== undefined)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Syncs MCP server configurations from remote feed source
|
|
174
|
+
*/
|
|
175
|
+
async syncFeeds(): Promise<void> {
|
|
176
|
+
return mcpManager.syncFeeds();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Export a singleton instance
|
|
181
|
+
export const serverService = new ServerService();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility class to track update check operations in progress
|
|
3
|
+
* Provides thread-safe access to check status with proper locking
|
|
4
|
+
*/
|
|
5
|
+
export class UpdateCheckTracker {
|
|
6
|
+
private static instance: UpdateCheckTracker;
|
|
7
|
+
|
|
8
|
+
// Map to track operations in progress (key -> inProgress)
|
|
9
|
+
private operationsInProgress: Map<string, boolean> = new Map();
|
|
10
|
+
|
|
11
|
+
// Lock for thread safety when checking/updating the in-progress map
|
|
12
|
+
private operationsLock: Promise<void> = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
private constructor() { }
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the singleton instance of the tracker
|
|
18
|
+
*/
|
|
19
|
+
public static getInstance(): UpdateCheckTracker {
|
|
20
|
+
if (!UpdateCheckTracker.instance) {
|
|
21
|
+
UpdateCheckTracker.instance = new UpdateCheckTracker();
|
|
22
|
+
}
|
|
23
|
+
return UpdateCheckTracker.instance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Execute an operation with a lock to ensure thread safety
|
|
28
|
+
* @param operation The operation to execute
|
|
29
|
+
* @returns The result of the operation
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
32
|
+
private async withLock<T>(operation: () => Promise<T>): Promise<T> {
|
|
33
|
+
const current = this.operationsLock;
|
|
34
|
+
let resolve: () => void;
|
|
35
|
+
this.operationsLock = new Promise<void>(r => resolve = r);
|
|
36
|
+
try {
|
|
37
|
+
await current;
|
|
38
|
+
return await operation();
|
|
39
|
+
} finally {
|
|
40
|
+
resolve!();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if an operation can be started and mark it as in progress if so
|
|
46
|
+
* @param key The key to identify the operation
|
|
47
|
+
* @returns True if the operation can be started, false if already in progress
|
|
48
|
+
*/
|
|
49
|
+
async startOperation(key: string): Promise<boolean> {
|
|
50
|
+
return await this.withLock(async () => {
|
|
51
|
+
if (!this.operationsInProgress.get(key)) {
|
|
52
|
+
this.operationsInProgress.set(key, true);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mark an operation as complete
|
|
61
|
+
* @param key The key to identify the operation
|
|
62
|
+
*/
|
|
63
|
+
async endOperation(key: string): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
await this.withLock(async () => {
|
|
66
|
+
this.operationsInProgress.set(key, false);
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`Failed to end operation for key: ${key}`, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if an operation is in progress
|
|
75
|
+
* @param key The key to identify the operation
|
|
76
|
+
* @returns True if the operation is in progress
|
|
77
|
+
*/
|
|
78
|
+
async isOperationInProgress(key: string): Promise<boolean> {
|
|
79
|
+
return await this.withLock(async () => {
|
|
80
|
+
return !!this.operationsInProgress.get(key);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Export a singleton instance
|
|
86
|
+
export const updateCheckTracker = UpdateCheckTracker.getInstance();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import * as fsSync from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import extractZipModule from 'extract-zip';
|
|
6
|
+
import { Logger } from './logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract a zip file to a directory
|
|
10
|
+
* @param zipPath The path to the zip file
|
|
11
|
+
* @param options Extraction options
|
|
12
|
+
* @returns A promise that resolves when extraction is complete
|
|
13
|
+
*/
|
|
14
|
+
export async function extractZipFile(zipPath: string, options: { dir: string }): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
// Try to use extract-zip if available
|
|
17
|
+
if (typeof extractZipModule === 'function') {
|
|
18
|
+
return await extractZipModule(zipPath, options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback to Node.js built-in unzipper
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const unzipper = require('unzipper');
|
|
24
|
+
|
|
25
|
+
await fs.promises.mkdir(options.dir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
return new Promise<void>((resolve, reject) => {
|
|
28
|
+
fs.createReadStream(zipPath)
|
|
29
|
+
.pipe(unzipper.Extract({ path: options.dir }))
|
|
30
|
+
.on('close', resolve)
|
|
31
|
+
.on('error', reject);
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`Error extracting zip: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
throw new Error(`Failed to extract zip file ${zipPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the path to an NPM module, replacing template variables
|
|
42
|
+
* First checks if the module path exists under NVM-controlled Node, then falls back to global npm
|
|
43
|
+
* @param pathString The path string potentially containing template variables
|
|
44
|
+
* @returns The resolved path
|
|
45
|
+
*/
|
|
46
|
+
export function resolveNpmModulePath(pathString: string): string {
|
|
47
|
+
// If the path doesn't contain the ${NPMPATH} template, return it as is
|
|
48
|
+
if (!pathString.includes('${NPMPATH}')) {
|
|
49
|
+
return pathString;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// First try to get NVM-controlled npm path
|
|
53
|
+
const nvmHome = process.env.NVM_HOME;
|
|
54
|
+
if (nvmHome) {
|
|
55
|
+
try {
|
|
56
|
+
// Get current node version
|
|
57
|
+
const nodeVersion = execSync('node -v').toString().trim();
|
|
58
|
+
|
|
59
|
+
// Construct the path to npm in the NVM directory
|
|
60
|
+
const nvmNodePath = path.join(nvmHome, nodeVersion);
|
|
61
|
+
const resolvedPath = pathString.replace('${NPMPATH}', nvmNodePath);
|
|
62
|
+
// Check if this path exists
|
|
63
|
+
try {
|
|
64
|
+
fsSync.accessSync(resolvedPath);
|
|
65
|
+
return resolvedPath;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
Logger.debug(`NVM controlled path doesn't exist: ${resolvedPath}, will try global npm`);
|
|
68
|
+
// Path doesn't exist, will fall back to global npm
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
Logger.debug(`Error determining Node version: ${error}, will use global npm`);
|
|
72
|
+
// Error getting node version, will fall back to global npm
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fall back to global npm path
|
|
77
|
+
const globalNpmPath = execSync('npm root -g').toString().trim();
|
|
78
|
+
return pathString.replace('${NPMPATH}', globalNpmPath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reads a JSON file and parses its content
|
|
83
|
+
* @param filePath Path to the JSON file
|
|
84
|
+
* @param createIfNotExist Whether to create the file if it doesn't exist
|
|
85
|
+
* @returns The parsed JSON content
|
|
86
|
+
*/
|
|
87
|
+
export async function readJsonFile(filePath: string, createIfNotExist = false): Promise<any> {
|
|
88
|
+
try {
|
|
89
|
+
// Ensure directory exists
|
|
90
|
+
if (createIfNotExist) {
|
|
91
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
95
|
+
return JSON.parse(content);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT' && createIfNotExist) {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Writes content to a JSON file
|
|
106
|
+
* @param filePath Path to the JSON file
|
|
107
|
+
* @param content Content to write to the file
|
|
108
|
+
*/
|
|
109
|
+
export async function writeJsonFile(filePath: string, content: any): Promise<void> {
|
|
110
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
111
|
+
await fs.writeFile(filePath, JSON.stringify(content, null, 2), 'utf8');
|
|
112
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { Logger } from './logger.js';
|
|
3
|
+
import { LOCAL_FEEDS_DIR } from '../core/constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if local feeds exist in the LOCAL_FEEDS_DIR
|
|
7
|
+
* Returns true if the directory exists and contains at least one .json file
|
|
8
|
+
*/
|
|
9
|
+
export async function hasLocalFeeds(): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
Logger.debug('Checking for local feeds existence');
|
|
12
|
+
const feedsExist = await fs.access(LOCAL_FEEDS_DIR)
|
|
13
|
+
.then(() => true)
|
|
14
|
+
.catch(() => false);
|
|
15
|
+
|
|
16
|
+
if (!feedsExist) {
|
|
17
|
+
Logger.debug('Local feeds directory does not exist');
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if directory contains any json files
|
|
22
|
+
const files = await fs.readdir(LOCAL_FEEDS_DIR);
|
|
23
|
+
const hasJsonFiles = files.some(file => file.endsWith('.json'));
|
|
24
|
+
|
|
25
|
+
Logger.debug(`Local feeds directory ${hasJsonFiles ? 'contains' : 'does not contain'} JSON files`);
|
|
26
|
+
return hasJsonFiles;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
Logger.error('Error checking local feeds:', error);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { isToolInstalled, installCLI } from './osUtils.js';
|
|
2
|
+
import { exec, spawn } from 'child_process';
|
|
3
|
+
import util from 'util';
|
|
4
|
+
import { Logger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
const execAsync = util.promisify(exec);
|
|
7
|
+
|
|
8
|
+
// Create a promisified version of spawn that returns a Promise
|
|
9
|
+
const spawnAsync = (command: string, args: string[], options: any = {}) => {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const childProcess = spawn(command, args, options);
|
|
12
|
+
|
|
13
|
+
childProcess.on('close', (code) => {
|
|
14
|
+
if (code === 0) {
|
|
15
|
+
resolve(code);
|
|
16
|
+
} else {
|
|
17
|
+
reject(new Error(`Process exited with code ${code}`));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
childProcess.on('error', (err) => {
|
|
22
|
+
reject(err);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
class GithubAuthError extends Error {
|
|
28
|
+
constructor(message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'GithubAuthError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function checkGithubAuth(): Promise<void> {
|
|
35
|
+
Logger.debug('Starting GitHub authentication check');
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Check if git is installed
|
|
39
|
+
if (!await isToolInstalled('git')) {
|
|
40
|
+
Logger.log('Installing required Git...');
|
|
41
|
+
await installCLI('git');
|
|
42
|
+
|
|
43
|
+
// Verify git was installed correctly, with retry mechanism
|
|
44
|
+
if (!await isToolInstalled('git')) {
|
|
45
|
+
throw new Error('Failed to install Git. Please install it manually and try again.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Logger.debug('Git installed successfully and verified');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if gh CLI is installed
|
|
52
|
+
if (!await isToolInstalled('gh')) {
|
|
53
|
+
Logger.log('Installing required GitHub CLI...');
|
|
54
|
+
await installCLI('gh');
|
|
55
|
+
|
|
56
|
+
// Verify gh CLI was installed correctly, with retry mechanism
|
|
57
|
+
if (!await isToolInstalled('gh')) {
|
|
58
|
+
throw new Error('Failed to install GitHub CLI. Please install it manually and try again.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Logger.debug('GitHub CLI installed successfully and verified');
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
Logger.error('Error during tool installation:', error);
|
|
65
|
+
throw new Error(`Tool installation failed: ${(error as Error).message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
Logger.debug('Checking GitHub authentication status');
|
|
70
|
+
// Check if user is authenticated
|
|
71
|
+
const { stdout: viewerData } = await execAsync('gh api user');
|
|
72
|
+
const viewer = JSON.parse(viewerData);
|
|
73
|
+
|
|
74
|
+
Logger.debug({
|
|
75
|
+
action: 'github_auth_check',
|
|
76
|
+
username: viewer.login
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Check if user is using company account (ends with _microsoft)
|
|
80
|
+
if (!viewer.login.toLowerCase().endsWith('_microsoft')) {
|
|
81
|
+
const error = 'Error: You must be logged in with a Microsoft account (username should end with _microsoft). ' +
|
|
82
|
+
'Please run "gh auth logout" and then "gh auth login" with your Microsoft account. Current username: ' +
|
|
83
|
+
viewer.login;
|
|
84
|
+
Logger.error(error, {
|
|
85
|
+
username: viewer.login
|
|
86
|
+
});
|
|
87
|
+
throw new GithubAuthError(error);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Logger.debug('GitHub authentication verified successfully with Microsoft account');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof GithubAuthError) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If the error is due to not being authenticated
|
|
97
|
+
const errorMessage = (error as any)?.stderr || (error as Error).message;
|
|
98
|
+
if (errorMessage.includes('please run: gh auth login') || errorMessage.includes('GH_TOKEN')) {
|
|
99
|
+
Logger.log('GitHub authentication required at the first run. Please login account end with _microsoft.');
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Use spawnAsync for interactive authentication
|
|
103
|
+
await spawnAsync('gh', ['auth', 'login', '--web', '--hostname', 'github.com', '--git-protocol', 'https'], {
|
|
104
|
+
stdio: 'inherit' // User sees & interacts directly with the process
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
Logger.debug('GitHub authentication process completed');
|
|
108
|
+
|
|
109
|
+
// Verify the authentication was successful
|
|
110
|
+
const { stdout: viewerData } = await execAsync('gh api user');
|
|
111
|
+
const viewer = JSON.parse(viewerData);
|
|
112
|
+
|
|
113
|
+
// Check if user is using company account (ends with _microsoft)
|
|
114
|
+
if (!viewer.login.toLowerCase().endsWith('_microsoft')) {
|
|
115
|
+
throw new GithubAuthError('You must be logged in with a Microsoft account (username should end with _microsoft).');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Logger.debug(`Successfully authenticated as ${viewer.login}`);
|
|
119
|
+
return; // Auth successful, continue execution
|
|
120
|
+
} catch (loginError) {
|
|
121
|
+
Logger.error('Error during GitHub authentication process', loginError);
|
|
122
|
+
|
|
123
|
+
// If the interactive login failed, provide manual instructions
|
|
124
|
+
const authInstructions =
|
|
125
|
+
'\nError: GitHub authentication required. Please follow these steps:\n\n' +
|
|
126
|
+
'1. Run this command:\n' +
|
|
127
|
+
' gh auth login --web --hostname github.com --git-protocol https\n' +
|
|
128
|
+
'2. Choose Y when prompted authenticating Git with your GitHub credentials.\n' +
|
|
129
|
+
'3. Follow the prompts to login with your Microsoft account (username must end with _microsoft)\n' +
|
|
130
|
+
'4. Authorize ai-microsoft organization.\n' +
|
|
131
|
+
'5. After successful login, run imcp command again.\n\n';
|
|
132
|
+
|
|
133
|
+
Logger.log(authInstructions);
|
|
134
|
+
throw new GithubAuthError('GitHub authentication required. Please login first and try again.');
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
const errorMessage = `Failed to verify GitHub authentication: ${(error as Error).message}`;
|
|
138
|
+
Logger.error(errorMessage, error);
|
|
139
|
+
throw new GithubAuthError(errorMessage);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|