imcp 0.0.12 → 0.0.14

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 (81) hide show
  1. package/dist/core/ConfigurationProvider.d.ts +2 -1
  2. package/dist/core/ConfigurationProvider.js +20 -24
  3. package/dist/core/InstallationService.d.ts +17 -0
  4. package/dist/core/InstallationService.js +127 -61
  5. package/dist/core/MCPManager.d.ts +1 -0
  6. package/dist/core/MCPManager.js +3 -0
  7. package/dist/core/RequirementService.d.ts +4 -4
  8. package/dist/core/RequirementService.js +11 -7
  9. package/dist/core/ServerSchemaProvider.d.ts +1 -1
  10. package/dist/core/ServerSchemaProvider.js +15 -10
  11. package/dist/core/constants.d.ts +3 -0
  12. package/dist/core/constants.js +4 -1
  13. package/dist/core/installers/clients/ClientInstaller.js +58 -40
  14. package/dist/core/installers/requirements/PipInstaller.js +10 -5
  15. package/dist/core/onboard/FeedOnboardService.d.ts +35 -0
  16. package/dist/core/onboard/FeedOnboardService.js +137 -0
  17. package/dist/core/types.d.ts +6 -1
  18. package/dist/core/validators/FeedValidator.d.ts +13 -0
  19. package/dist/core/validators/FeedValidator.js +27 -0
  20. package/dist/services/ServerService.d.ts +5 -0
  21. package/dist/services/ServerService.js +15 -0
  22. package/dist/utils/githubAuth.js +0 -10
  23. package/dist/utils/githubUtils.d.ts +16 -0
  24. package/dist/utils/githubUtils.js +55 -39
  25. package/dist/web/contract/serverContract.d.ts +64 -0
  26. package/dist/web/contract/serverContract.js +2 -0
  27. package/dist/web/public/css/detailsWidget.css +157 -32
  28. package/dist/web/public/css/onboard.css +44 -0
  29. package/dist/web/public/css/serverDetails.css +35 -19
  30. package/dist/web/public/index.html +16 -10
  31. package/dist/web/public/js/detailsWidget.js +43 -40
  32. package/dist/web/public/js/modal/index.js +58 -0
  33. package/dist/web/public/js/modal/installHandler.js +227 -0
  34. package/dist/web/public/js/modal/installModal.js +163 -0
  35. package/dist/web/public/js/modal/installation.js +281 -0
  36. package/dist/web/public/js/modal/loadingModal.js +52 -0
  37. package/dist/web/public/js/modal/loadingUI.js +74 -0
  38. package/dist/web/public/js/modal/messageQueue.js +112 -0
  39. package/dist/web/public/js/modal/modalSetup.js +512 -0
  40. package/dist/web/public/js/modal/modalUI.js +214 -0
  41. package/dist/web/public/js/modal/modalUtils.js +49 -0
  42. package/dist/web/public/js/modal/version.js +20 -0
  43. package/dist/web/public/js/modal/versionUtils.js +20 -0
  44. package/dist/web/public/js/modal.js +25 -1041
  45. package/dist/web/public/js/onboard/formProcessor.js +309 -0
  46. package/dist/web/public/js/onboard/index.js +131 -0
  47. package/dist/web/public/js/onboard/state.js +32 -0
  48. package/dist/web/public/js/onboard/templates.js +375 -0
  49. package/dist/web/public/js/onboard/uiHandlers.js +196 -0
  50. package/dist/web/public/js/serverCategoryDetails.js +211 -123
  51. package/dist/web/public/onboard.html +150 -0
  52. package/dist/web/server.js +25 -0
  53. package/package.json +3 -4
  54. package/src/core/ConfigurationProvider.ts +37 -29
  55. package/src/core/InstallationService.ts +176 -62
  56. package/src/core/MCPManager.ts +4 -0
  57. package/src/core/RequirementService.ts +12 -8
  58. package/src/core/ServerSchemaLoader.ts +48 -0
  59. package/src/core/ServerSchemaProvider.ts +137 -0
  60. package/src/core/constants.ts +4 -1
  61. package/src/core/installers/clients/ClientInstaller.ts +66 -49
  62. package/src/core/installers/requirements/PipInstaller.ts +10 -5
  63. package/src/core/types.ts +6 -1
  64. package/src/services/ServerService.ts +15 -0
  65. package/src/utils/githubAuth.ts +14 -27
  66. package/src/utils/githubUtils.ts +84 -47
  67. package/src/web/public/css/detailsWidget.css +235 -0
  68. package/src/web/public/css/serverDetails.css +126 -0
  69. package/src/web/public/index.html +16 -10
  70. package/src/web/public/js/detailsWidget.js +264 -0
  71. package/src/web/public/js/modal/index.js +58 -0
  72. package/src/web/public/js/modal/installModal.js +163 -0
  73. package/src/web/public/js/modal/installation.js +281 -0
  74. package/src/web/public/js/modal/loadingModal.js +52 -0
  75. package/src/web/public/js/modal/messageQueue.js +112 -0
  76. package/src/web/public/js/modal/modalSetup.js +512 -0
  77. package/src/web/public/js/modal/modalUtils.js +49 -0
  78. package/src/web/public/js/modal/versionUtils.js +20 -0
  79. package/src/web/public/js/modal.js +25 -1041
  80. package/src/web/public/js/serverCategoryDetails.js +211 -123
  81. package/src/web/server.ts +31 -0
@@ -14,7 +14,8 @@ import {
14
14
  RequirementStatus,
15
15
  MCPServerStatus,
16
16
  OperationStatus,
17
- ClientSettings
17
+ ClientSettings,
18
+ McpConfig
18
19
  } from './types.js';
19
20
  import { ConfigurationLoader } from './ConfigurationLoader.js';
20
21
 
@@ -115,6 +116,12 @@ export class ConfigurationProvider {
115
116
  });
116
117
  }
117
118
 
119
+ async getServerMcpConfig(categoryName: string, serverName: string): Promise<McpConfig | undefined> {
120
+ return await this.withLock(async () => {
121
+ return this.configuration.feeds[categoryName]?.mcpServers.find(s => s.name === serverName);
122
+ });
123
+ }
124
+
118
125
  async getInstallationStatus(categoryName: string): Promise<InstallationStatus | undefined> {
119
126
  return await this.withLock(async () => {
120
127
  // Inline getServerCategory logic
@@ -300,35 +307,36 @@ export class ConfigurationProvider {
300
307
  await fs.mkdir(LOCAL_FEEDS_DIR, { recursive: true });
301
308
  await fs.mkdir(this.tempDir, { recursive: true });
302
309
 
303
- try {
304
- await fs.access(path.join(this.tempDir, '.git'));
305
- Logger.debug('Found existing repository, updating...');
306
- const { stdout, stderr } = await execAsync('git pull', { cwd: this.tempDir });
307
- Logger.debug({
308
- action: 'git_pull',
309
- stderr,
310
- stdout
311
- });
312
- } catch (err) {
313
- Logger.debug('No existing repository found, cloning...');
314
- await fs.rm(this.tempDir, { recursive: true, force: true });
315
- const { stdout, stderr } = await execAsync(`git clone ${GITHUB_REPO.url} ${this.tempDir}`);
316
- Logger.debug({
317
- action: 'git_clone',
318
- stderr,
319
- stdout,
320
- url: GITHUB_REPO.url
321
- });
322
- }
310
+ // Clean up temp directory
311
+ await fs.rm(this.tempDir, { recursive: true, force: true });
312
+
313
+ // Download latest release
314
+ Logger.debug('Downloading latest release...');
315
+ const { downloadGithubRelease } = await import('../utils/githubUtils.js');
316
+ const { version, downloadPath } = await downloadGithubRelease(
317
+ GITHUB_REPO.repoName,
318
+ 'latest',
319
+ GITHUB_REPO.feedAssetsName,
320
+ undefined,
321
+ true,
322
+ this.tempDir
323
+ );
324
+
325
+ Logger.debug({
326
+ action: 'download_release',
327
+ downloadPath,
328
+ version,
329
+ repoName: GITHUB_REPO.repoName,
330
+ });
323
331
 
324
332
  Logger.debug('Updating local feeds...');
325
333
  await fs.rm(LOCAL_FEEDS_DIR, { recursive: true, force: true });
326
- const sourceFeedsDir = path.join(this.tempDir, GITHUB_REPO.feedsPath);
334
+ const sourceFeedsDir = downloadPath;
327
335
 
328
336
  try {
329
- await fs.access(sourceFeedsDir);
337
+ await fs.access(downloadPath);
330
338
  } catch (err) {
331
- throw new Error(`Could not find feeds directory in cloned repository: ${sourceFeedsDir}`);
339
+ throw new Error(`Could not find feeds directory in downloaded path: ${sourceFeedsDir}`);
332
340
  }
333
341
 
334
342
  await fs.cp(sourceFeedsDir, LOCAL_FEEDS_DIR, { recursive: true, force: true });
@@ -362,7 +370,7 @@ export class ConfigurationProvider {
362
370
  return await this.withLock(async () => {
363
371
  // Load utils in async context to avoid circular dependencies
364
372
  const { readJsonFile, writeJsonFile } = await import('../utils/clientUtils.js');
365
-
373
+
366
374
  // Filter clients if target is specified
367
375
  const clientEntries = Object.entries(SUPPORTED_CLIENTS as Record<string, ClientSettings>);
368
376
  const targetClients = target
@@ -373,11 +381,11 @@ export class ConfigurationProvider {
373
381
  const settingPath = process.env.CODE_INSIDERS
374
382
  ? clientSettings.codeInsiderSettingPath
375
383
  : clientSettings.codeSettingPath;
376
-
384
+
377
385
  try {
378
386
  const content = await readJsonFile(settingPath, true);
379
387
  let modified = false;
380
-
388
+
381
389
  // Handle GitHub Copilot's different structure
382
390
  if (clientName === 'GithubCopilot' && content.mcp?.servers?.[serverName]) {
383
391
  delete content.mcp.servers[serverName];
@@ -386,7 +394,7 @@ export class ConfigurationProvider {
386
394
  delete content.mcpServers[serverName];
387
395
  modified = true;
388
396
  }
389
-
397
+
390
398
  // Only write if we actually modified the content
391
399
  if (modified) {
392
400
  await writeJsonFile(settingPath, content);
@@ -396,7 +404,7 @@ export class ConfigurationProvider {
396
404
  Logger.error(`Failed to remove server ${serverName} from client ${clientName} settings:`, error);
397
405
  }
398
406
  }
399
-
407
+
400
408
  // Also update our in-memory configuration
401
409
  if (this.configuration.clientMCPSettings) {
402
410
  if (target) {
@@ -50,7 +50,7 @@ export class InstallationService {
50
50
  // Fire off requirement updates in the background without awaiting completion
51
51
  if (options.requirements && options.requirements.length > 0) {
52
52
  // Start the process but don't await it - it will run in the background
53
- this.processRequirementUpdates(categoryName, serverName, options.requirements)
53
+ this.processRequirementUpdates(categoryName, serverName, options)
54
54
  .catch(error => {
55
55
  console.error(`Error in background requirement updates: ${error instanceof Error ? error.message : String(error)}`);
56
56
  });
@@ -91,7 +91,7 @@ export class InstallationService {
91
91
  * @param serverName The server name
92
92
  * @param requirements The requirements to update
93
93
  */
94
- private async processRequirementUpdates(categoryName: string, serverName: string, requirements: { name: string, version: string }[]): Promise<void> {
94
+ private async processRequirementUpdates(categoryName: string, serverName: string, options: ServerInstallOptions): Promise<void> {
95
95
  // Use UpdateCheckTracker to prevent concurrent updates
96
96
  const updateCheckTracker = await import('../utils/UpdateCheckTracker.js').then(m => m.updateCheckTracker);
97
97
  const operationKey = `requirement-updates-${categoryName}-${serverName}`;
@@ -116,7 +116,7 @@ export class InstallationService {
116
116
  const { requirementService } = await import('./RequirementService.js');
117
117
 
118
118
  // Create an array of promises to update all requirements in parallel
119
- const updatePromises = requirements.map(async (reqToUpdate) => {
119
+ const updatePromises = options.requirements?.map(async (reqToUpdate) => {
120
120
  try {
121
121
  // Find the full requirement config
122
122
  const reqConfig = feedConfig.requirements?.find((r: RequirementConfig) => r.name === reqToUpdate.name);
@@ -155,8 +155,16 @@ export class InstallationService {
155
155
  version: reqToUpdate.version
156
156
  };
157
157
 
158
- // Update the requirement
159
- const updatedStatus = await requirementService.updateRequirement(updatedReqConfig, reqToUpdate.version);
158
+ // For pip requirements, check if we have a stored pythonEnv
159
+ if (updatedReqConfig.type === 'pip' && currentStatus.pythonEnv && !options?.settings?.pythonEnv) {
160
+ options = {
161
+ ...options,
162
+ settings: { ...options?.settings, pythonEnv: currentStatus.pythonEnv }
163
+ };
164
+ }
165
+
166
+ // Update the requirement with options for pip environment
167
+ const updatedStatus = await requirementService.updateRequirement(updatedReqConfig, reqToUpdate.version, options);
160
168
 
161
169
  // Update requirement status
162
170
  await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
@@ -197,8 +205,10 @@ export class InstallationService {
197
205
  }
198
206
  });
199
207
 
200
- // Wait for all updates to complete in parallel
201
- await Promise.all(updatePromises);
208
+ // Wait for all updates to complete in parallel if there are any
209
+ if (updatePromises) {
210
+ await Promise.all(updatePromises);
211
+ }
202
212
  } finally {
203
213
  // Always release the lock when done, even if there was an error
204
214
  await updateCheckTracker.endOperation(operationKey);
@@ -270,67 +280,171 @@ export class InstallationService {
270
280
  return orderA - orderB;
271
281
  });
272
282
 
273
- // Chain installations in sequence while keeping them non-blocking
274
- await sortedRequirements.reduce((chain, requirement) => {
275
- return chain.then(async () => {
276
- const feeds = await configProvider.getFeedConfiguration(categoryName);
277
- const requirementConfig = feeds?.requirements?.find((r: RequirementConfig) => r.name === requirement.name) || {
278
- name: requirement.name,
279
- version: requirement.version,
280
- type: 'npm'
281
- };
283
+ // Start requirements installation in background
284
+ this.installRequirementsInBackground(categoryName, sortedRequirements, options)
285
+ .catch(error => {
286
+ Logger.error(`Error in background requirement installations: ${error instanceof Error ? error.message : String(error)}`);
287
+ });
282
288
 
283
- const installer = this.installerFactory.getInstaller(requirementConfig);
284
- if (!installer) {
285
- await configProvider.updateRequirementStatus(categoryName, requirement.name, {
286
- name: requirement.name,
287
- type: requirementConfig.type,
288
- installed: false,
289
- error: `No installer found for requirement type: ${requirementConfig.type}`,
290
- operationStatus: {
291
- status: 'failed',
292
- type: 'install',
293
- target: 'requirement',
294
- message: `No installer found for requirement type: ${requirementConfig.type}`,
295
- operationId: this.generateOperationId()
296
- }
297
- });
298
- return;
299
- }
289
+ // Return immediately while installation continues in background
290
+ return null;
291
+ }
300
292
 
301
- const operationStatus: OperationStatus = {
302
- status: 'pending',
303
- type: 'install',
304
- target: 'requirement',
305
- message: `Installing requirement: ${requirement.name}`,
306
- operationId: this.generateOperationId()
307
- };
293
+ /**
294
+ * Installs requirements in background without blocking the main thread
295
+ * Requirements with the same order are installed in parallel
296
+ */
297
+ private async installRequirementsInBackground(
298
+ categoryName: string,
299
+ sortedRequirements: Array<{ name: string; version: string; order?: number }>,
300
+ options: ServerInstallOptions
301
+ ): Promise<void> {
302
+ const configProvider = ConfigurationProvider.getInstance();
308
303
 
309
- await configProvider.updateRequirementStatus(categoryName, requirement.name, {
310
- name: requirement.name,
311
- type: requirementConfig.type,
312
- installed: false,
313
- inProgress: true,
314
- operationStatus
315
- });
304
+ // Group requirements by order
305
+ type RequirementType = { name: string; version: string; order?: number };
306
+ const requirementGroups = sortedRequirements.reduce<Record<number, RequirementType[]>>((groups, req) => {
307
+ const order = req.order ?? Infinity;
308
+ if (!groups[order]) {
309
+ groups[order] = [];
310
+ }
311
+ groups[order].push(req);
312
+ return groups;
313
+ }, {});
316
314
 
317
- return installer.install(requirementConfig, options).then(async (installStatus) => {
318
- const status: RequirementStatus = {
319
- ...installStatus,
320
- operationStatus: {
321
- status: installStatus.installed ? 'completed' : 'failed',
322
- type: 'install',
323
- target: 'requirement',
324
- message: installStatus.installed ? `Requirement ${requirement.name} installed successfully` : `Failed to install ${requirement.name}`,
325
- operationId: operationStatus.operationId
326
- }
315
+ // Process each group in sequence, but requirements within group in parallel
316
+ const orderKeys = Object.keys(requirementGroups).map(Number).sort((a, b) => a - b);
317
+ for (const order of orderKeys) {
318
+ const group = requirementGroups[order];
319
+
320
+ await Promise.all(group.map(async requirement => {
321
+ try {
322
+ const feeds = await configProvider.getFeedConfiguration(categoryName);
323
+ const requirementConfig = feeds?.requirements?.find((r: RequirementConfig) => r.name === requirement.name) || {
324
+ name: requirement.name,
325
+ version: requirement.version,
326
+ type: 'npm'
327
327
  };
328
- await configProvider.updateRequirementStatus(categoryName, requirement.name, status);
329
- });
330
- });
331
- }, Promise.resolve());
332
328
 
333
- return null;
329
+ // For pip requirements, check if we need to use stored pythonEnv
330
+ const currentStatus = await configProvider.getRequirementStatus(categoryName, requirement.name);
331
+ if (requirementConfig.type === 'pip' && currentStatus?.pythonEnv && !options?.settings?.pythonEnv) {
332
+ options = {
333
+ ...options,
334
+ settings: { ...options?.settings, pythonEnv: currentStatus.pythonEnv }
335
+ };
336
+ }
337
+
338
+ const installer = this.installerFactory.getInstaller(requirementConfig);
339
+ if (!installer) {
340
+ await this.updateRequirementFailureStatus(
341
+ categoryName,
342
+ requirement.name,
343
+ requirementConfig.type,
344
+ `No installer found for requirement type: ${requirementConfig.type}`
345
+ );
346
+ return;
347
+ }
348
+
349
+ const operationId = this.generateOperationId();
350
+ await this.updateRequirementProgressStatus(
351
+ categoryName,
352
+ requirement.name,
353
+ requirementConfig.type,
354
+ operationId
355
+ );
356
+
357
+ const installStatus = await installer.install(requirementConfig, options);
358
+ await this.updateRequirementCompletionStatus(
359
+ categoryName,
360
+ requirement.name,
361
+ installStatus,
362
+ operationId
363
+ );
364
+ } catch (error) {
365
+ await this.updateRequirementFailureStatus(
366
+ categoryName,
367
+ requirement.name,
368
+ 'unknown',
369
+ error instanceof Error ? error.message : String(error)
370
+ );
371
+ }
372
+ }));
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Helper to update requirement status for failure case
378
+ */
379
+ private async updateRequirementFailureStatus(
380
+ categoryName: string,
381
+ requirementName: string,
382
+ requirementType: string,
383
+ errorMessage: string
384
+ ): Promise<void> {
385
+ const configProvider = ConfigurationProvider.getInstance();
386
+ await configProvider.updateRequirementStatus(categoryName, requirementName, {
387
+ name: requirementName,
388
+ type: requirementType,
389
+ installed: false,
390
+ error: errorMessage,
391
+ operationStatus: {
392
+ status: 'failed',
393
+ type: 'install',
394
+ target: 'requirement',
395
+ message: `Error installing requirement: ${errorMessage}`,
396
+ operationId: this.generateOperationId()
397
+ }
398
+ });
399
+ }
400
+
401
+ /**
402
+ * Helper to update requirement status for in-progress case
403
+ */
404
+ private async updateRequirementProgressStatus(
405
+ categoryName: string,
406
+ requirementName: string,
407
+ requirementType: string,
408
+ operationId: string
409
+ ): Promise<void> {
410
+ const configProvider = ConfigurationProvider.getInstance();
411
+ await configProvider.updateRequirementStatus(categoryName, requirementName, {
412
+ name: requirementName,
413
+ type: requirementType,
414
+ installed: false,
415
+ inProgress: true,
416
+ operationStatus: {
417
+ status: 'in-progress',
418
+ type: 'install',
419
+ target: 'requirement',
420
+ message: `Installing requirement: ${requirementName}`,
421
+ operationId
422
+ }
423
+ });
424
+ }
425
+
426
+ /**
427
+ * Helper to update requirement status for completion case
428
+ */
429
+ private async updateRequirementCompletionStatus(
430
+ categoryName: string,
431
+ requirementName: string,
432
+ installStatus: RequirementStatus,
433
+ operationId: string
434
+ ): Promise<void> {
435
+ const configProvider = ConfigurationProvider.getInstance();
436
+ await configProvider.updateRequirementStatus(categoryName, requirementName, {
437
+ ...installStatus,
438
+ operationStatus: {
439
+ status: installStatus.installed ? 'completed' : 'failed',
440
+ type: 'install',
441
+ target: 'requirement',
442
+ message: installStatus.installed
443
+ ? `Requirement ${requirementName} installed successfully`
444
+ : `Failed to install ${requirementName}`,
445
+ operationId
446
+ }
447
+ });
334
448
  }
335
449
  }
336
450
 
@@ -48,6 +48,10 @@ export class MCPManager extends EventEmitter {
48
48
  return this.configProvider.getFeedConfiguration(categoryName);
49
49
  }
50
50
 
51
+ async getServerMcpConfig(categoryName: string, serverName: string) {
52
+ return this.configProvider.getServerMcpConfig(categoryName, serverName);
53
+ }
54
+
51
55
  async installServer(
52
56
  categoryName: string,
53
57
  serverName: string,
@@ -1,4 +1,4 @@
1
- import { RequirementConfig, RequirementStatus } from './types.js';
1
+ import { RequirementConfig, RequirementStatus, ServerInstallOptions } from './types.js';
2
2
  import { createInstallerFactory } from './installers/index.js';
3
3
  import { exec } from 'child_process';
4
4
  import util from 'util';
@@ -28,12 +28,12 @@ export class RequirementService {
28
28
  * @param requirement The requirement to install
29
29
  * @returns The installation status
30
30
  */
31
- public async installRequirement(requirement: RequirementConfig): Promise<RequirementStatus> {
31
+ public async installRequirement(requirement: RequirementConfig, options?: ServerInstallOptions): Promise<RequirementStatus> {
32
32
  // Validate requirement
33
33
  this.validateRequirement(requirement);
34
34
 
35
35
  // Install the requirement
36
- return await this.installerFactory.install(requirement);
36
+ return await this.installerFactory.install(requirement, options);
37
37
  }
38
38
 
39
39
  /**
@@ -41,12 +41,12 @@ export class RequirementService {
41
41
  * @param requirement The requirement to check
42
42
  * @returns The installation status
43
43
  */
44
- public async checkRequirementStatus(requirement: RequirementConfig): Promise<RequirementStatus> {
44
+ public async checkRequirementStatus(requirement: RequirementConfig, options?: ServerInstallOptions): Promise<RequirementStatus> {
45
45
  // Validate requirement
46
46
  this.validateRequirement(requirement);
47
47
 
48
48
  // Check the installation status
49
- return await this.installerFactory.checkInstallation(requirement);
49
+ return await this.installerFactory.checkInstallation(requirement, options);
50
50
  }
51
51
 
52
52
  /**
@@ -66,7 +66,11 @@ export class RequirementService {
66
66
  return currentStatus;
67
67
  }
68
68
 
69
- const status = await this.checkRequirementStatus(requirement);
69
+ // Pass pythonEnv from currentStatus if it exists for pip packages
70
+ const options = requirement.type === 'pip' && currentStatus.pythonEnv
71
+ ? { settings: { pythonEnv: currentStatus.pythonEnv } }
72
+ : undefined;
73
+ const status = await this.checkRequirementStatus(requirement, options);
70
74
  return await (installer as any).checkForUpdates(requirement, status);
71
75
  }
72
76
 
@@ -76,7 +80,7 @@ export class RequirementService {
76
80
  * @param updateVersion The version to update to
77
81
  * @returns The updated requirement status
78
82
  */
79
- public async updateRequirement(requirement: RequirementConfig, updateVersion: string): Promise<RequirementStatus> {
83
+ public async updateRequirement(requirement: RequirementConfig, updateVersion: string, options?: ServerInstallOptions): Promise<RequirementStatus> {
80
84
  // Validate requirement
81
85
  this.validateRequirement(requirement);
82
86
 
@@ -87,7 +91,7 @@ export class RequirementService {
87
91
  };
88
92
 
89
93
  // Install the updated version
90
- return await this.installRequirement(updatedRequirement);
94
+ return await this.installerFactory.install(updatedRequirement, options);
91
95
  }
92
96
 
93
97
  /**
@@ -0,0 +1,48 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { LOCAL_FEEDS_SCHEMA_DIR } from './constants.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ import { ServerSchema } from './ServerSchemaProvider.js';
6
+
7
+ export class ServerSchemaLoader {
8
+ /**
9
+ * Load schema for a specific server in a category
10
+ */
11
+ static async loadSchema(categoryName: string, serverName: string): Promise<ServerSchema | undefined> {
12
+ try {
13
+ const schemaPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryName, `${serverName}.json`);
14
+ const content = await fs.readFile(schemaPath, 'utf8');
15
+ const schema = JSON.parse(content);
16
+
17
+ // Validate schema structure
18
+ if (!schema.version || !schema.schema) {
19
+ Logger.debug(`Invalid schema format for server ${serverName} in category ${categoryName}`);
20
+ return undefined;
21
+ }
22
+
23
+ return {
24
+ schema: schema
25
+ };
26
+ } catch (error) {
27
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
28
+ Logger.debug(`No schema file found for server ${serverName} in category ${categoryName}`);
29
+ return undefined;
30
+ }
31
+ Logger.error(`Error loading schema for server ${serverName} in category ${categoryName}:`, error);
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Validate schema content against expected format
38
+ */
39
+ static validateSchema(schema: any): boolean {
40
+ return (
41
+ typeof schema === 'object' &&
42
+ schema !== null &&
43
+ typeof schema.version === 'string' &&
44
+ typeof schema.schema === 'object' &&
45
+ schema.schema !== null
46
+ );
47
+ }
48
+ }
@@ -0,0 +1,137 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { LOCAL_FEEDS_SCHEMA_DIR } from './constants.js';
4
+ import { Logger } from '../utils/logger.js';
5
+
6
+ export interface ServerSchema {
7
+ schema: Record<string, any>;
8
+ }
9
+
10
+ export class ServerSchemaProvider {
11
+ private static instance: ServerSchemaProvider;
12
+ private schemaMap: Map<string, Map<string, ServerSchema>>;
13
+ private schemaLock: Promise<void> = Promise.resolve();
14
+
15
+ private constructor() {
16
+ this.schemaMap = new Map();
17
+ }
18
+
19
+ public static async getInstance(): Promise<ServerSchemaProvider> {
20
+ if (!ServerSchemaProvider.instance) {
21
+ ServerSchemaProvider.instance = new ServerSchemaProvider();
22
+ await ServerSchemaProvider.instance.initialize();
23
+ }
24
+ return ServerSchemaProvider.instance;
25
+ }
26
+
27
+ private async withLock<T>(operation: () => Promise<T>): Promise<T> {
28
+ const current = this.schemaLock;
29
+ let resolve: () => void;
30
+ this.schemaLock = new Promise<void>(r => resolve = r);
31
+ try {
32
+ await current;
33
+ return await operation();
34
+ } finally {
35
+ resolve!();
36
+ }
37
+ }
38
+
39
+ async initialize(): Promise<void> {
40
+ await this.withLock(async () => {
41
+ try {
42
+ // Create feeds directory if it doesn't exist
43
+ await fs.mkdir(LOCAL_FEEDS_SCHEMA_DIR, { recursive: true });
44
+
45
+ // Load all schemas from the feeds directory
46
+ await this.loadAllSchemas();
47
+ } catch (error) {
48
+ Logger.error('Error during schema initialization:', error);
49
+ throw error;
50
+ }
51
+ });
52
+ }
53
+
54
+ private async loadSchema(categoryName: string, schemaFileName: string): Promise<ServerSchema | undefined> {
55
+ try {
56
+ const schemaPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryName, schemaFileName);
57
+ const content = await fs.readFile(schemaPath, 'utf8');
58
+ const schema = JSON.parse(content);
59
+
60
+ return {
61
+ schema: schema
62
+ };
63
+ } catch (error) {
64
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
65
+ Logger.debug(`No schema file found for ${schemaFileName} in category ${categoryName}`);
66
+ return undefined;
67
+ }
68
+ Logger.error(`Error loading schema ${schemaFileName} in category ${categoryName}:`, error);
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ private async loadAllSchemas(): Promise<void> {
74
+ this.schemaMap.clear();
75
+
76
+ // Read server category directories
77
+ const categoryDirs = await fs.readdir(LOCAL_FEEDS_SCHEMA_DIR, { withFileTypes: true });
78
+
79
+ for (const categoryDir of categoryDirs) {
80
+ if (categoryDir.isDirectory()) {
81
+ const categoryPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryDir.name);
82
+ const serverFiles = await fs.readdir(categoryPath);
83
+
84
+ const serverSchemas = new Map<string, ServerSchema>();
85
+
86
+ for (const file of serverFiles) {
87
+ if (file.endsWith('.json')) {
88
+ try {
89
+ const schema = await this.loadSchema(categoryDir.name, file);
90
+ if (schema) {
91
+ // Store with the complete file name for direct lookup
92
+ serverSchemas.set(file, schema);
93
+ }
94
+ } catch (error) {
95
+ Logger.error(`Error loading schema for file ${file} in category ${categoryDir.name}:`, error);
96
+ }
97
+ }
98
+ }
99
+
100
+ if (serverSchemas.size > 0) {
101
+ this.schemaMap.set(categoryDir.name, serverSchemas);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ async getSchema(categoryName: string, schemaFileName: string): Promise<ServerSchema | undefined> {
108
+ return await this.withLock(async () => {
109
+ const categorySchemas = this.schemaMap.get(categoryName);
110
+ if (!categorySchemas) {
111
+ Logger.debug(`No schemas found for category ${categoryName}`);
112
+ return undefined;
113
+ }
114
+ const schema = categorySchemas.get(schemaFileName);
115
+ if (!schema) {
116
+ Logger.debug(`Schema ${schemaFileName} not found in category ${categoryName}`);
117
+ }
118
+ return schema;
119
+ });
120
+ }
121
+
122
+ async reloadSchemas(): Promise<void> {
123
+ return await this.withLock(async () => {
124
+ await this.loadAllSchemas();
125
+ });
126
+ }
127
+ }
128
+
129
+ // Export a lazy initialized singleton instance getter
130
+ let initPromise: Promise<ServerSchemaProvider> | null = null;
131
+
132
+ export function getServerSchemaProvider(): Promise<ServerSchemaProvider> {
133
+ if (!initPromise) {
134
+ initPromise = ServerSchemaProvider.getInstance();
135
+ }
136
+ return initPromise;
137
+ }