imcp 0.0.1 → 0.0.3

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 (50) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/commands/install.js +8 -8
  3. package/dist/cli/index.js +3 -2
  4. package/dist/core/ConfigurationProvider.d.ts +2 -0
  5. package/dist/core/ConfigurationProvider.js +49 -3
  6. package/dist/core/InstallationService.d.ts +8 -0
  7. package/dist/core/InstallationService.js +117 -0
  8. package/dist/core/MCPManager.d.ts +1 -0
  9. package/dist/core/MCPManager.js +42 -0
  10. package/dist/core/RequirementService.d.ts +7 -0
  11. package/dist/core/RequirementService.js +17 -0
  12. package/dist/core/constants.d.ts +5 -0
  13. package/dist/core/constants.js +29 -6
  14. package/dist/core/installers/BaseInstaller.js +26 -9
  15. package/dist/core/installers/ClientInstaller.d.ts +6 -0
  16. package/dist/core/installers/ClientInstaller.js +149 -12
  17. package/dist/core/installers/GeneralInstaller.js +0 -5
  18. package/dist/core/installers/NpmInstaller.js +2 -1
  19. package/dist/core/types.d.ts +7 -6
  20. package/dist/services/ServerService.js +16 -0
  21. package/dist/utils/clientUtils.js +3 -1
  22. package/dist/utils/versionUtils.d.ts +12 -0
  23. package/dist/utils/versionUtils.js +26 -0
  24. package/dist/web/public/css/modal.css +89 -9
  25. package/dist/web/public/index.html +12 -6
  26. package/dist/web/public/js/modal.js +357 -97
  27. package/dist/web/public/modal.html +20 -11
  28. package/dist/web/server.d.ts +6 -0
  29. package/dist/web/server.js +6 -1
  30. package/package.json +1 -1
  31. package/src/cli/commands/install.ts +11 -14
  32. package/src/cli/index.ts +4 -2
  33. package/src/core/ConfigurationProvider.ts +51 -3
  34. package/src/core/InstallationService.ts +131 -0
  35. package/src/core/MCPManager.ts +60 -1
  36. package/src/core/RequirementService.ts +21 -1
  37. package/src/core/constants.ts +32 -7
  38. package/src/core/installers/BaseInstaller.ts +33 -17
  39. package/src/core/installers/ClientInstaller.ts +148 -12
  40. package/src/core/installers/GeneralInstaller.ts +0 -5
  41. package/src/core/installers/NpmInstaller.ts +2 -1
  42. package/src/core/types.ts +8 -6
  43. package/src/services/ServerService.ts +22 -0
  44. package/src/utils/clientUtils.ts +3 -1
  45. package/src/utils/versionUtils.ts +29 -0
  46. package/src/web/public/css/modal.css +89 -9
  47. package/src/web/public/index.html +12 -6
  48. package/src/web/public/js/modal.js +357 -97
  49. package/src/web/public/modal.html +20 -11
  50. package/src/web/server.ts +16 -2
@@ -4,6 +4,7 @@ import fs from 'fs/promises';
4
4
  import { SETTINGS_DIR } from '../constants.js';
5
5
  import { extractZipFile } from '../../utils/clientUtils.js';
6
6
  import { RequirementInstaller } from './RequirementInstaller.js';
7
+ import { Logger } from '../../utils/logger.js';
7
8
 
8
9
  /**
9
10
  * Abstract base class with common functionality for all requirement installers
@@ -20,7 +21,7 @@ export abstract class BaseInstaller implements RequirementInstaller {
20
21
  abstract canHandle(requirement: RequirementConfig): boolean;
21
22
  abstract install(requirement: RequirementConfig): Promise<RequirementStatus>;
22
23
  abstract checkInstallation(requirement: RequirementConfig): Promise<RequirementStatus>;
23
-
24
+
24
25
  /**
25
26
  * Check if updates are available for the requirement
26
27
  * @param requirement The requirement to check
@@ -33,14 +34,14 @@ export abstract class BaseInstaller implements RequirementInstaller {
33
34
  if (!currentStatus.installed) {
34
35
  return currentStatus;
35
36
  }
36
-
37
+
37
38
  // If the version doesn't contain "latest", no update check needed
38
39
  if (!requirement.version.includes('latest')) {
39
40
  return currentStatus;
40
41
  }
41
-
42
+
42
43
  let latestVersion: string | undefined;
43
-
44
+
44
45
  // Check based on registry type
45
46
  if (requirement.registry?.githubRelease) {
46
47
  latestVersion = await this.getGitHubLatestVersion(requirement.registry.githubRelease.repository);
@@ -56,20 +57,24 @@ export abstract class BaseInstaller implements RequirementInstaller {
56
57
  // Add other types as needed
57
58
  }
58
59
  }
59
-
60
+
60
61
  // If we found a latest version and it's different from current
61
62
  if (latestVersion && latestVersion !== currentStatus.version) {
62
63
  return {
63
64
  ...currentStatus,
64
65
  availableUpdate: {
65
66
  version: latestVersion,
66
- message: `Update available: ${currentStatus.version} → ${latestVersion}`,
67
- checkTime: new Date().toISOString()
68
- }
67
+ message: `Update available: ${currentStatus.version} → ${latestVersion}`
68
+ },
69
+ lastCheckTime: new Date().toISOString()
70
+ };
71
+ } else {
72
+ return {
73
+ ...currentStatus,
74
+ lastCheckTime: new Date().toISOString()
69
75
  };
70
76
  }
71
-
72
- return currentStatus;
77
+
73
78
  } catch (error) {
74
79
  // Don't update status on error, just log it
75
80
  console.warn(`Error checking for updates for ${requirement.name}: ${error instanceof Error ? error.message : String(error)}`);
@@ -109,24 +114,35 @@ export abstract class BaseInstaller implements RequirementInstaller {
109
114
  let resolvedAssetName = assetName || '';
110
115
  let resolvedAssetsName = assetsName || '';
111
116
 
117
+ const { stdout } = await this.execPromise(`gh release view --repo ${repository} --json tagName --jq .tagName`);
118
+ const latestTag = stdout.trim();
112
119
  // Handle latest version detection
113
120
  if (version.includes('${latest}') || version === 'latest') {
114
- const { stdout } = await this.execPromise(`gh release view --repo ${repository} --json tagName --jq .tagName`);
115
- const latestTag = stdout.trim();
121
+
116
122
  let latestVersion = latestTag
117
123
  if (latestVersion.startsWith('v') && version.startsWith('v')) {
118
124
  latestVersion = latestVersion.substring(1); // Remove 'v' prefix if present
119
125
  // Replace ${latest} in version and asset names
120
126
  version = version.replace('${latest}', latestVersion);
121
127
  if (assetsName) {
122
- resolvedAssetsName = assetsName.replace('${latest}', latestVersion);
128
+ resolvedAssetsName = assetsName.replace('${latest}', latestVersion).replace('${version}', version);
123
129
  }
124
130
  if (assetName) {
125
- resolvedAssetName = assetName.replace('${latest}', latestVersion);
131
+ resolvedAssetName = assetName.replace('${latest}', latestVersion).replace('${version}', version);
126
132
  }
127
133
  }
134
+ } else {
135
+ if (assetsName) {
136
+ resolvedAssetsName = assetsName.replace('${latest}', version).replace('${version}', version);
137
+ }
138
+ if (assetName) {
139
+ resolvedAssetName = assetName.replace('${latest}', version).replace('${version}', version);
140
+ }
141
+ Logger.debug(`Downloading ${requirement.name} from GitHub release ${repository} version ${version}`);
142
+ Logger.debug(`ResolvedAssetsName} ${resolvedAssetName}; ResolvedAsetName} ${resolvedAssetName}`);
128
143
  }
129
144
  const pattern = resolvedAssetsName ? resolvedAssetsName : resolvedAssetName;
145
+ Logger.debug(`Resolved pattern: ${pattern}`);
130
146
 
131
147
  if (!pattern) {
132
148
  throw new Error('Either assetsName or assetName must be specified for GitHub release downloads');
@@ -135,7 +151,7 @@ export abstract class BaseInstaller implements RequirementInstaller {
135
151
  // Download the release asset
136
152
  const downloadPath = path.join(this.downloadsDir, path.basename(pattern));
137
153
  if (!await this.fileExists(downloadPath)) {
138
- await this.execPromise(`gh release download ${version} --repo ${repository} --pattern "${pattern}" -O "${downloadPath}"`);
154
+ await this.execPromise(`gh release download ${version.startsWith('v') ? version : `v${version}`} --repo ${repository} --pattern "${pattern}" -O "${downloadPath}"`);
139
155
  }
140
156
 
141
157
  // Handle zip file extraction if the downloaded file is a zip
@@ -257,14 +273,14 @@ export abstract class BaseInstaller implements RequirementInstaller {
257
273
  // Use GitHub CLI to get the latest release
258
274
  const { stdout } = await this.execPromise(`gh release view --repo ${repository} --json tagName --jq .tagName`);
259
275
  const latestTag = stdout.trim();
260
-
276
+
261
277
  // Remove 'v' prefix if present
262
278
  return latestTag.startsWith('v') ? latestTag.substring(1) : latestTag;
263
279
  } catch (error) {
264
280
  // If gh command fails, try to get the latest tag
265
281
  const { stdout } = await this.execPromise(`git ls-remote --tags --refs https://github.com/${repository}.git | sort -t '/' -k 3 -V | tail -n 1 | awk -F/ '{print $3}'`);
266
282
  let latestTag = stdout.trim();
267
-
283
+
268
284
  // Remove 'v' prefix if present
269
285
  return latestTag.startsWith('v') ? latestTag.substring(1) : latestTag;
270
286
  }
@@ -12,6 +12,7 @@ import { SUPPORTED_CLIENTS } from '../constants.js';
12
12
  import { resolveNpmModulePath, readJsonFile, writeJsonFile } from '../../utils/clientUtils.js';
13
13
  import { exec } from 'child_process';
14
14
  import { promisify } from 'util';
15
+ import { Logger } from '../../utils/logger.js';
15
16
 
16
17
  export class ClientInstaller {
17
18
  private configProvider: ConfigurationProvider;
@@ -46,6 +47,27 @@ export class ClientInstaller {
46
47
  }
47
48
  }
48
49
 
50
+ /**
51
+ * Check if a command is available on the system
52
+ * @param command The command to check
53
+ * @returns True if the command is available, false otherwise
54
+ */
55
+ private async isCommandAvailable(command: string): Promise<boolean> {
56
+ const execAsync = promisify(exec);
57
+ try {
58
+ if (process.platform === 'win32') {
59
+ // Windows-specific command check
60
+ await execAsync(`where ${command}`);
61
+ } else {
62
+ // Unix-like systems
63
+ await execAsync(`which ${command}`);
64
+ }
65
+ return true;
66
+ } catch (error) {
67
+ return false;
68
+ }
69
+ }
70
+
49
71
  private async installClient(clientName: string, env?: Record<string, string>): Promise<OperationStatus> {
50
72
  // Check if client is supported
51
73
  if (!SUPPORTED_CLIENTS[clientName]) {
@@ -258,18 +280,34 @@ export class ClientInstaller {
258
280
  ): Promise<{ success: boolean; message: string }> {
259
281
  try {
260
282
  if (!SUPPORTED_CLIENTS[clientName]) {
283
+ Logger.debug(`Client ${clientName} is not supported.`);
261
284
  return { success: false, message: `Unsupported client: ${clientName}` };
262
285
  }
263
286
 
264
287
  const clientSettings = SUPPORTED_CLIENTS[clientName];
265
288
 
266
- // Determine which setting path to use based on VS Code type (regular or insiders)
267
- const settingPath = process.env.CODE_INSIDERS
268
- ? clientSettings.codeInsiderSettingPath
269
- : clientSettings.codeSettingPath;
289
+ // Get both setting paths for VS Code and VS Code Insiders
290
+ const regularSettingPath = clientSettings.codeSettingPath;
291
+ const insidersSettingPath = clientSettings.codeInsiderSettingPath;
292
+
293
+ Logger.debug(`Starting installation of ${this.serverName} for client ${clientName}`);
294
+ Logger.debug(`VS Code settings path configured as: ${regularSettingPath}`);
295
+ Logger.debug(`VS Code Insiders settings path configured as: ${insidersSettingPath}`);
270
296
 
271
- if (!settingPath) {
272
- return { success: false, message: `No settings path found for client: ${clientName}` };
297
+ // Check if VS Code and VS Code Insiders are installed
298
+ const isVSCodeInstalled = await this.isCommandAvailable('code');
299
+ const isVSCodeInsidersInstalled = await this.isCommandAvailable('code-insiders');
300
+ Logger.debug(isVSCodeInstalled ? 'VS Code detected on system' : 'VS Code not detected on system');
301
+ Logger.debug(isVSCodeInsidersInstalled ? 'VS Code Insiders detected on system' : 'VS Code Insiders not detected on system');
302
+ Logger.debug(`VS Code Insiders installed: ${isVSCodeInsidersInstalled}`);
303
+
304
+ // If neither is installed, we can't proceed
305
+ if (!isVSCodeInstalled && !isVSCodeInsidersInstalled) {
306
+ Logger.debug('No VS Code installations detected on system. Cannot update settings.');
307
+ return {
308
+ success: false,
309
+ message: `Neither VS Code nor VS Code Insiders are installed on this system. Cannot update settings for client: ${clientName}. Please install VS Code or VS Code Insiders and try again.`
310
+ };
273
311
  }
274
312
 
275
313
  // Clone the installation configuration to make modifications
@@ -296,26 +334,124 @@ export class ClientInstaller {
296
334
  Object.assign(installConfig.env, env);
297
335
  }
298
336
 
299
- // Update client-specific settings
337
+ // Keep track of success for both installations
338
+ let regularSuccess = false;
339
+ let insidersSuccess = false;
340
+ let errorMessages: string[] = [];
341
+
342
+ // Update client-specific settings for both VS Code and VS Code Insiders
300
343
  if (clientName === 'MSRooCode' || clientName === 'Cline') {
301
- await this.updateClineOrMSRooSettings(settingPath, this.serverName, installConfig, clientName);
344
+ // Update VS Code settings if VS Code is installed
345
+ if (isVSCodeInstalled) {
346
+ try {
347
+ Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
348
+ await this.updateClineOrMSRooSettings(regularSettingPath, this.serverName, installConfig, clientName);
349
+ regularSuccess = true;
350
+ Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
351
+ } catch (error) {
352
+ const errorMsg = `Error updating VS Code settings: ${error instanceof Error ? error.message : String(error)}`;
353
+ errorMessages.push(errorMsg);
354
+ console.error(errorMsg);
355
+ }
356
+ }
357
+
358
+ // Update VS Code Insiders settings if VS Code Insiders is installed
359
+ if (isVSCodeInsidersInstalled) {
360
+ try {
361
+ Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
362
+ await this.updateClineOrMSRooSettings(insidersSettingPath, this.serverName, installConfig, clientName);
363
+ insidersSuccess = true;
364
+ Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
365
+ } catch (error) {
366
+ const errorMsg = `Error updating VS Code Insiders settings: ${error instanceof Error ? error.message : String(error)}`;
367
+ errorMessages.push(errorMsg);
368
+ console.error(errorMsg);
369
+ }
370
+ }
302
371
  } else if (clientName === 'GithubCopilot') {
303
- await this.updateGithubCopilotSettings(settingPath, this.serverName, installConfig);
372
+ // Update VS Code settings if VS Code is installed
373
+ if (isVSCodeInstalled) {
374
+ try {
375
+ Logger.debug(`Updating VS Code settings for ${clientName} at path: ${regularSettingPath}`);
376
+ await this.updateGithubCopilotSettings(regularSettingPath, this.serverName, installConfig);
377
+ regularSuccess = true;
378
+ Logger.debug(`Settings successfully updated to ${regularSettingPath} for ${clientName} (VS Code)`);
379
+ } catch (error) {
380
+ const errorMsg = `Error updating VS Code settings: ${error instanceof Error ? error.message : String(error)}`;
381
+ errorMessages.push(errorMsg);
382
+ console.error(errorMsg);
383
+ }
384
+ }
385
+
386
+ // Update VS Code Insiders settings if VS Code Insiders is installed
387
+ if (isVSCodeInsidersInstalled) {
388
+ try {
389
+ Logger.debug(`Updating VS Code Insiders settings for ${clientName} at path: ${insidersSettingPath}`);
390
+ await this.updateGithubCopilotSettings(insidersSettingPath, this.serverName, installConfig);
391
+ insidersSuccess = true;
392
+ Logger.debug(`Settings successfully updated to ${insidersSettingPath} for ${clientName} (VS Code Insiders)`);
393
+ } catch (error) {
394
+ const errorMsg = `Error updating VS Code Insiders settings: ${error instanceof Error ? error.message : String(error)}`;
395
+ errorMessages.push(errorMsg);
396
+ console.error(errorMsg);
397
+ }
398
+ }
304
399
  } else {
400
+ Logger.debug(`No implementation exists for updating settings for client: ${clientName}`);
305
401
  return {
306
402
  success: false,
307
403
  message: `Client ${clientName} is supported but no implementation exists for updating its settings`
308
404
  };
309
405
  }
310
406
 
407
+ // Determine overall success status and message
408
+ const overallSuccess = regularSuccess || insidersSuccess;
409
+ let message = '';
410
+
411
+ if (overallSuccess) {
412
+ const successDetails = [];
413
+ if (regularSuccess) successDetails.push('VS Code');
414
+ if (insidersSuccess) successDetails.push('VS Code Insiders');
415
+
416
+ // Create a more compact success message with specific paths
417
+ const pathDetails = [];
418
+ if (regularSuccess) {
419
+ pathDetails.push(`\n[VS Code]: ${regularSettingPath}`);
420
+ }
421
+ if (insidersSuccess) {
422
+ pathDetails.push(`\n[VS Code Insiders]: ${insidersSettingPath}`);
423
+ }
424
+ message = `Successfully installed ${this.serverName} for client: ${clientName}. ${pathDetails.join(' ')}`;
425
+
426
+ // Add partial failure information if applicable
427
+ if (errorMessages.length > 0) {
428
+ message += `\nHowever, some errors occurred:\n${errorMessages.join('\n- ')}`;
429
+ }
430
+ Logger.debug(`Installation complete: ${message}`);
431
+ } else {
432
+ // Create a more detailed failure message that includes the detection results
433
+ const detectionInfo = [];
434
+ if (!isVSCodeInstalled) detectionInfo.push('VS Code not detected');
435
+ if (!isVSCodeInsidersInstalled) detectionInfo.push('VS Code Insiders not detected');
436
+
437
+ if (errorMessages.length > 0) {
438
+ message = `Failed to install ${this.serverName} for client: ${clientName}.\nDetection status: [${detectionInfo.join(', ')}].\nErrors:\n- ${errorMessages.join('\n- ')}`;
439
+ } else {
440
+ message = `Failed to install ${this.serverName} for client: ${clientName}.\nDetection status: [${detectionInfo.join(', ')}]`;
441
+ }
442
+ console.error(`Installation failed: ${message}`);
443
+ }
444
+
311
445
  return {
312
- success: true,
313
- message: `Successfully installed ${this.serverName} for client: ${clientName}`
446
+ success: overallSuccess,
447
+ message: message
314
448
  };
315
449
  } catch (error) {
450
+ const errorMsg = `Error installing client ${clientName}: ${error instanceof Error ? error.message : String(error)}`;
451
+ console.error(errorMsg);
316
452
  return {
317
453
  success: false,
318
- message: `Error installing client ${clientName}: ${error instanceof Error ? error.message : String(error)}`
454
+ message: errorMsg
319
455
  };
320
456
  }
321
457
  }
@@ -70,11 +70,6 @@ export class GeneralInstaller extends BaseInstaller {
70
70
  installed: true,
71
71
  version: requirement.version,
72
72
  inProgress: false,
73
- // Store installation path in a way that it can be retrieved later if needed
74
- updateInfo: {
75
- available: false,
76
- installPath
77
- }
78
73
  };
79
74
  } catch (error) {
80
75
  return {
@@ -1,6 +1,7 @@
1
1
  import { stat } from 'fs';
2
2
  import { RequirementConfig, RequirementStatus } from '../types.js';
3
3
  import { BaseInstaller } from './BaseInstaller.js';
4
+ import { compareVersions } from '../../utils/versionUtils.js';
4
5
 
5
6
  /**
6
7
  * Installer implementation for NPM packages
@@ -52,7 +53,7 @@ export class NpmInstaller extends BaseInstaller {
52
53
  async install(requirement: RequirementConfig): Promise<RequirementStatus> {
53
54
  try {
54
55
  const status = await this.checkInstallation(requirement);
55
- if (status.installed) {
56
+ if (status.installed && status.version && compareVersions(status.version, requirement.version) === 0) {
56
57
  return status;
57
58
  }
58
59
 
package/src/core/types.ts CHANGED
@@ -14,14 +14,9 @@ export interface RequirementStatus {
14
14
  availableUpdate?: {
15
15
  version: string;
16
16
  message: string;
17
- checkTime: string;
18
17
  };
18
+ lastCheckTime?: string;
19
19
  operationStatus?: OperationStatus;
20
- updateInfo?: {
21
- available: boolean;
22
- latestVersion?: string;
23
- [key: string]: any;
24
- } | null;
25
20
  }
26
21
 
27
22
  export interface MCPServerStatus {
@@ -70,12 +65,19 @@ export interface ServerOperationResult {
70
65
  export interface MCPConfiguration {
71
66
  localServerCategories: MCPServerCategory[];
72
67
  feeds: Record<string, FeedConfiguration>;
68
+ clientMCPSettings?: Record<string, Record<string, any>>;
73
69
  }
74
70
 
75
71
  export interface ServerInstallOptions {
76
72
  force?: boolean;
77
73
  env?: Record<string, string>; // Environment variables for installation
78
74
  targetClients?: string[]; // Target clients for configuration
75
+ requirements?: RequirementConfig[];
76
+ }
77
+
78
+ export interface UpdateRequirementOptions {
79
+ requirementName: string;
80
+ updateVersion: string;
79
81
  }
80
82
 
81
83
  export interface ServerUninstallOptions {
@@ -9,6 +9,7 @@ import {
9
9
  ServerUninstallOptions
10
10
  } from '../core/types.js';
11
11
  import { mcpManager } from '../core/MCPManager.js';
12
+ import { UPDATE_CHECK_INTERVAL_MS } from '../core/constants.js';
12
13
  import { updateCheckTracker } from '../utils/UpdateCheckTracker.js';
13
14
 
14
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -74,6 +75,18 @@ export class ServerService {
74
75
  const currentStatus = serverCategory.installationStatus?.requirementsStatus[requirement.name];
75
76
  if (!currentStatus) continue;
76
77
 
78
+ // Skip update check if last check was less than UPDATE_CHECK_INTERVAL_MS ago
79
+ if (currentStatus.lastCheckTime) {
80
+ const lastCheckTime = new Date(currentStatus.lastCheckTime);
81
+ const currentTime = new Date();
82
+ const timeSinceLastCheck = currentTime.getTime() - lastCheckTime.getTime();
83
+
84
+ if (timeSinceLastCheck < UPDATE_CHECK_INTERVAL_MS) {
85
+ Logger.debug(`Skipping update check for ${requirement.name}, last check was ${Math.round(timeSinceLastCheck / 1000)} seconds ago`);
86
+ continue;
87
+ }
88
+ }
89
+
77
90
  // Check for updates
78
91
  const updatedStatus = await requirementService.checkRequirementForUpdates(requirement);
79
92
 
@@ -90,6 +103,15 @@ export class ServerService {
90
103
  serverCategory.installationStatus.requirementsStatus[requirement.name] = updatedStatus;
91
104
  }
92
105
  }
106
+ currentStatus.lastCheckTime = new Date().toISOString();
107
+ await configProvider.updateRequirementStatus(
108
+ serverCategory.name,
109
+ requirement.name,
110
+ currentStatus
111
+ );
112
+ if (serverCategory.installationStatus?.requirementsStatus) {
113
+ serverCategory.installationStatus.requirementsStatus[requirement.name] = updatedStatus;
114
+ }
93
115
  }
94
116
  }
95
117
  } finally {
@@ -92,7 +92,9 @@ export async function readJsonFile(filePath: string, createIfNotExist = false):
92
92
  }
93
93
 
94
94
  const content = await fs.readFile(filePath, 'utf8');
95
- return JSON.parse(content);
95
+ // Remove trailing commas from JSON content
96
+ const sanitizedContent = content.replace(/,(\s*[}\]])/g, '$1');
97
+ return JSON.parse(sanitizedContent);
96
98
  } catch (error) {
97
99
  if ((error as NodeJS.ErrnoException).code === 'ENOENT' && createIfNotExist) {
98
100
  return {};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Utility functions for version comparison and management
3
+ */
4
+
5
+ /**
6
+ * Compare two semantic version strings
7
+ * @param v1 First version
8
+ * @param v2 Second version
9
+ * @returns -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
10
+ * (or more specifically, a negative number if v1 < v2,
11
+ * a positive number if v1 > v2, 0 if equal)
12
+ */
13
+ export function compareVersions(v1: string, v2: string): number {
14
+ const v1Parts = v1.split('.').map(Number);
15
+ const v2Parts = v2.split('.').map(Number);
16
+
17
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
18
+ const v1Part = i < v1Parts.length ? v1Parts[i] : 0;
19
+ const v2Part = i < v2Parts.length ? v2Parts[i] : 0;
20
+
21
+ if (v1Part !== v2Part) {
22
+ // This returns the actual difference, which is:
23
+ // negative if v1Part < v2Part, positive if v1Part > v2Part
24
+ return v1Part - v2Part;
25
+ }
26
+ }
27
+
28
+ return 0;
29
+ }
@@ -34,6 +34,12 @@ body {
34
34
  min-height: 120px;
35
35
  opacity: 1 !important;
36
36
  box-shadow: 0 0 16px #3498db;
37
+ position: relative;
38
+ padding-top: 24px;
39
+ }
40
+
41
+ #installLoadingModal .modal-close-button {
42
+ z-index: 3200 !important;
37
43
  }
38
44
  /* Loading modal always on top */
39
45
  #installLoadingModal {
@@ -61,8 +67,37 @@ body {
61
67
  transition: all 0.3s ease-out;
62
68
  animation: slideIn 0.3s ease-out;
63
69
  }
64
-
65
70
  /* Close button */
71
+ .modal-close-button {
72
+ position: absolute;
73
+ right: 12px;
74
+ top: 12px;
75
+ width: 32px;
76
+ height: 32px;
77
+ border-radius: 50%;
78
+ background: #ffffff;
79
+ border: 2px solid #3498db;
80
+ color: #3498db;
81
+ font-size: 22px;
82
+ cursor: pointer;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ transition: all 0.2s ease;
87
+ z-index: 10;
88
+ padding: 0;
89
+ line-height: 1;
90
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
91
+ }
92
+
93
+ .modal-close-button:hover {
94
+ background-color: #3498db;
95
+ color: #ffffff;
96
+ border-color: #3498db;
97
+ transform: scale(1.05);
98
+ box-shadow: 0 0 8px rgba(52, 152, 219, 0.4);
99
+ }
100
+
66
101
  .close {
67
102
  position: absolute;
68
103
  right: 1.5rem;
@@ -71,20 +106,14 @@ body {
71
106
  font-weight: 600;
72
107
  color: #6b7280;
73
108
  cursor: pointer;
74
- width: 32px;
75
- height: 32px;
76
- display: flex;
77
- align-items: center;
78
- justify-content: center;
79
- border-radius: 50%;
80
109
  transition: color 0.2s ease;
81
110
  }
82
111
 
83
112
  .close:hover {
84
- background-color: #f3f4f6;
85
113
  color: #111827;
86
114
  }
87
115
 
116
+
88
117
  /* Sections layout */
89
118
  .modal-sections {
90
119
  margin-top: 1.5rem;
@@ -240,11 +269,62 @@ body {
240
269
  }
241
270
  }
242
271
 
243
- /* Center loading icon in loading modal */
272
+ /* Center loading icon in loading modal */
244
273
  #installLoadingModal .loading-icon {
245
274
  display: flex;
246
275
  justify-content: center;
247
276
  align-items: center;
248
277
  width: 100%;
249
278
  margin-bottom: 8px;
279
+ }
280
+
281
+ /* Loading message styles */
282
+ #installLoadingMessage {
283
+ font-size: 0.85rem !important;
284
+ line-height: 1.6;
285
+ word-break: break-word;
286
+ max-height: 200px;
287
+ overflow-y: auto;
288
+ scrollbar-width: thin;
289
+ scrollbar-color: #3498db #f0f0f0;
290
+ }
291
+
292
+ #installLoadingMessage::-webkit-scrollbar {
293
+ width: 6px;
294
+ }
295
+
296
+ #installLoadingMessage::-webkit-scrollbar-track {
297
+ background: #f0f0f0;
298
+ border-radius: 3px;
299
+ }
300
+
301
+ #installLoadingMessage::-webkit-scrollbar-thumb {
302
+ background-color: #3498db;
303
+ border-radius: 3px;
304
+ }
305
+
306
+ #installLoadingMessage div {
307
+ margin-bottom: 8px;
308
+ padding: 4px 0;
309
+ }
310
+
311
+ /* Error message styling */
312
+ #installLoadingMessage span[style*="color:red"] {
313
+ color: #f59e0b !important;
314
+ font-weight: 500;
315
+ display: block;
316
+ padding: 4px 8px;
317
+ background: rgba(245, 158, 11, 0.1);
318
+ border-radius: 4px;
319
+ margin: 4px 0;
320
+ }
321
+
322
+ /* File path styling */
323
+ #installLoadingMessage .file-path {
324
+ font-family: 'Consolas', monospace;
325
+ background: #f8fafc;
326
+ padding: 2px 4px;
327
+ border-radius: 3px;
328
+ border: 1px solid #e2e8f0;
329
+ color: #2563eb;
250
330
  }
@@ -112,18 +112,24 @@
112
112
  </div>
113
113
  <!-- Loading Modal -->
114
114
  <div id="installLoadingModal" class="modal" style="display:none; z-index:2000;">
115
- <div class="modal-content" style="text-align:center; pointer-events:auto;">
115
+ <div class="modal-content" style="text-align:center; pointer-events:auto; position:relative;">
116
+ <button class="modal-close-button" onclick="hideInstallLoadingModal()" aria-label="Close">&times;</button>
116
117
  <div style="margin-top:40px;">
117
118
  <div class="loading-icon" style="margin-bottom:16px;">
118
119
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
119
- <circle cx="24" cy="24" r="20" stroke="#888" stroke-width="4" opacity="0.2"/>
120
- <circle cx="24" cy="24" r="20" stroke="#3498db" stroke-width="4" stroke-linecap="round" stroke-dasharray="100" stroke-dashoffset="60">
121
- <animateTransform attributeName="transform" type="rotate" from="0 24 24" to="360 24 24" dur="1s" repeatCount="indefinite"/>
120
+ <circle cx="24" cy="24" r="20" stroke="#888" stroke-width="4" opacity="0.2" />
121
+ <circle cx="24" cy="24" r="20" stroke="#3498db" stroke-width="4" stroke-linecap="round"
122
+ stroke-dasharray="100" stroke-dashoffset="60">
123
+ <animateTransform attributeName="transform" type="rotate" from="0 24 24" to="360 24 24"
124
+ dur="1s" repeatCount="indefinite" />
122
125
  </circle>
123
126
  </svg>
124
127
  </div>
125
- <div class="loading-title" style="font-size:1.5rem; font-weight:bold; margin-bottom:8px;">Installing...</div>
126
- <div id="installLoadingMessage" style="min-height:48px; max-height:160px; overflow:auto; background:#f8f8f8; border-radius:6px; padding:12px; font-size:1rem; color:#444; text-align:left;"></div>
128
+ <div class="loading-title" style="font-size:1.5rem; font-weight:bold; margin-bottom:8px;">Installing...
129
+ </div>
130
+ <div id="installLoadingMessage"
131
+ style="min-height:48px; max-height:160px; overflow:auto; background:#f8f8f8; border-radius:6px; padding:12px; font-size:1rem; color:#444; text-align:left;">
132
+ </div>
127
133
  </div>
128
134
  </div>
129
135
  </div>