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.
Files changed (124) hide show
  1. package/.github/ISSUE_TEMPLATE/JitAccess.yml +28 -0
  2. package/.github/acl/access.yml +20 -0
  3. package/.github/compliance/inventory.yml +5 -0
  4. package/.github/policies/jit.yml +19 -0
  5. package/README.md +137 -0
  6. package/dist/cli/commands/install.d.ts +2 -0
  7. package/dist/cli/commands/install.js +105 -0
  8. package/dist/cli/commands/list.d.ts +2 -0
  9. package/dist/cli/commands/list.js +90 -0
  10. package/dist/cli/commands/pull.d.ts +2 -0
  11. package/dist/cli/commands/pull.js +17 -0
  12. package/dist/cli/commands/serve.d.ts +2 -0
  13. package/dist/cli/commands/serve.js +32 -0
  14. package/dist/cli/commands/start.d.ts +2 -0
  15. package/dist/cli/commands/start.js +32 -0
  16. package/dist/cli/commands/sync.d.ts +2 -0
  17. package/dist/cli/commands/sync.js +17 -0
  18. package/dist/cli/commands/uninstall.d.ts +2 -0
  19. package/dist/cli/commands/uninstall.js +39 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.js +114 -0
  22. package/dist/core/ConfigurationProvider.d.ts +31 -0
  23. package/dist/core/ConfigurationProvider.js +416 -0
  24. package/dist/core/InstallationService.d.ts +17 -0
  25. package/dist/core/InstallationService.js +144 -0
  26. package/dist/core/MCPManager.d.ts +17 -0
  27. package/dist/core/MCPManager.js +98 -0
  28. package/dist/core/RequirementService.d.ts +45 -0
  29. package/dist/core/RequirementService.js +123 -0
  30. package/dist/core/constants.d.ts +29 -0
  31. package/dist/core/constants.js +55 -0
  32. package/dist/core/installers/BaseInstaller.d.ts +73 -0
  33. package/dist/core/installers/BaseInstaller.js +247 -0
  34. package/dist/core/installers/ClientInstaller.d.ts +17 -0
  35. package/dist/core/installers/ClientInstaller.js +307 -0
  36. package/dist/core/installers/CommandInstaller.d.ts +36 -0
  37. package/dist/core/installers/CommandInstaller.js +170 -0
  38. package/dist/core/installers/GeneralInstaller.d.ts +32 -0
  39. package/dist/core/installers/GeneralInstaller.js +87 -0
  40. package/dist/core/installers/InstallerFactory.d.ts +52 -0
  41. package/dist/core/installers/InstallerFactory.js +95 -0
  42. package/dist/core/installers/NpmInstaller.d.ts +25 -0
  43. package/dist/core/installers/NpmInstaller.js +123 -0
  44. package/dist/core/installers/PipInstaller.d.ts +25 -0
  45. package/dist/core/installers/PipInstaller.js +114 -0
  46. package/dist/core/installers/RequirementInstaller.d.ts +32 -0
  47. package/dist/core/installers/RequirementInstaller.js +3 -0
  48. package/dist/core/installers/index.d.ts +6 -0
  49. package/dist/core/installers/index.js +7 -0
  50. package/dist/core/types.d.ts +152 -0
  51. package/dist/core/types.js +16 -0
  52. package/dist/index.d.ts +11 -0
  53. package/dist/index.js +19 -0
  54. package/dist/services/InstallRequestValidator.d.ts +21 -0
  55. package/dist/services/InstallRequestValidator.js +99 -0
  56. package/dist/services/ServerService.d.ts +47 -0
  57. package/dist/services/ServerService.js +145 -0
  58. package/dist/utils/UpdateCheckTracker.d.ts +39 -0
  59. package/dist/utils/UpdateCheckTracker.js +80 -0
  60. package/dist/utils/clientUtils.d.ts +29 -0
  61. package/dist/utils/clientUtils.js +105 -0
  62. package/dist/utils/feedUtils.d.ts +5 -0
  63. package/dist/utils/feedUtils.js +29 -0
  64. package/dist/utils/githubAuth.d.ts +1 -0
  65. package/dist/utils/githubAuth.js +123 -0
  66. package/dist/utils/logger.d.ts +14 -0
  67. package/dist/utils/logger.js +90 -0
  68. package/dist/utils/osUtils.d.ts +16 -0
  69. package/dist/utils/osUtils.js +235 -0
  70. package/dist/web/public/css/modal.css +250 -0
  71. package/dist/web/public/css/notifications.css +70 -0
  72. package/dist/web/public/index.html +157 -0
  73. package/dist/web/public/js/api.js +213 -0
  74. package/dist/web/public/js/modal.js +572 -0
  75. package/dist/web/public/js/notifications.js +99 -0
  76. package/dist/web/public/js/serverCategoryDetails.js +210 -0
  77. package/dist/web/public/js/serverCategoryList.js +82 -0
  78. package/dist/web/public/modal.html +61 -0
  79. package/dist/web/public/styles.css +155 -0
  80. package/dist/web/server.d.ts +5 -0
  81. package/dist/web/server.js +150 -0
  82. package/package.json +53 -0
  83. package/src/cli/commands/install.ts +140 -0
  84. package/src/cli/commands/list.ts +112 -0
  85. package/src/cli/commands/pull.ts +16 -0
  86. package/src/cli/commands/serve.ts +37 -0
  87. package/src/cli/commands/uninstall.ts +54 -0
  88. package/src/cli/index.ts +127 -0
  89. package/src/core/ConfigurationProvider.ts +489 -0
  90. package/src/core/InstallationService.ts +173 -0
  91. package/src/core/MCPManager.ts +134 -0
  92. package/src/core/RequirementService.ts +147 -0
  93. package/src/core/constants.ts +61 -0
  94. package/src/core/installers/BaseInstaller.ts +292 -0
  95. package/src/core/installers/ClientInstaller.ts +423 -0
  96. package/src/core/installers/CommandInstaller.ts +185 -0
  97. package/src/core/installers/GeneralInstaller.ts +89 -0
  98. package/src/core/installers/InstallerFactory.ts +109 -0
  99. package/src/core/installers/NpmInstaller.ts +128 -0
  100. package/src/core/installers/PipInstaller.ts +121 -0
  101. package/src/core/installers/RequirementInstaller.ts +38 -0
  102. package/src/core/installers/index.ts +9 -0
  103. package/src/core/types.ts +163 -0
  104. package/src/index.ts +44 -0
  105. package/src/services/InstallRequestValidator.ts +112 -0
  106. package/src/services/ServerService.ts +181 -0
  107. package/src/utils/UpdateCheckTracker.ts +86 -0
  108. package/src/utils/clientUtils.ts +112 -0
  109. package/src/utils/feedUtils.ts +31 -0
  110. package/src/utils/githubAuth.ts +142 -0
  111. package/src/utils/logger.ts +101 -0
  112. package/src/utils/osUtils.ts +250 -0
  113. package/src/web/public/css/modal.css +250 -0
  114. package/src/web/public/css/notifications.css +70 -0
  115. package/src/web/public/index.html +157 -0
  116. package/src/web/public/js/api.js +213 -0
  117. package/src/web/public/js/modal.js +572 -0
  118. package/src/web/public/js/notifications.js +99 -0
  119. package/src/web/public/js/serverCategoryDetails.js +210 -0
  120. package/src/web/public/js/serverCategoryList.js +82 -0
  121. package/src/web/public/modal.html +61 -0
  122. package/src/web/public/styles.css +155 -0
  123. package/src/web/server.ts +195 -0
  124. 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
+ }