imcp 0.0.4 → 0.0.5

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 (88) hide show
  1. package/README.md +3 -4
  2. package/dist/cli/commands/install.js +2 -0
  3. package/dist/cli/commands/list.js +1 -0
  4. package/dist/cli/index.js +1 -2
  5. package/dist/core/ConfigurationLoader.d.ts +32 -0
  6. package/dist/core/ConfigurationLoader.js +213 -0
  7. package/dist/core/ConfigurationProvider.d.ts +2 -3
  8. package/dist/core/ConfigurationProvider.js +13 -182
  9. package/dist/core/InstallationService.d.ts +8 -0
  10. package/dist/core/InstallationService.js +124 -96
  11. package/dist/core/RequirementService.d.ts +1 -1
  12. package/dist/core/RequirementService.js +5 -9
  13. package/dist/core/constants.js +14 -1
  14. package/dist/core/installers/BaseInstaller.d.ts +5 -4
  15. package/dist/core/installers/BaseInstaller.js +17 -28
  16. package/dist/core/installers/ClientInstaller.js +134 -43
  17. package/dist/core/installers/CommandInstaller.d.ts +1 -0
  18. package/dist/core/installers/CommandInstaller.js +3 -0
  19. package/dist/core/installers/GeneralInstaller.d.ts +1 -0
  20. package/dist/core/installers/GeneralInstaller.js +3 -0
  21. package/dist/core/installers/InstallerFactory.d.ts +9 -7
  22. package/dist/core/installers/InstallerFactory.js +10 -8
  23. package/dist/core/installers/NpmInstaller.d.ts +1 -0
  24. package/dist/core/installers/NpmInstaller.js +3 -0
  25. package/dist/core/installers/PipInstaller.d.ts +6 -3
  26. package/dist/core/installers/PipInstaller.js +21 -8
  27. package/dist/core/installers/RequirementInstaller.d.ts +4 -3
  28. package/dist/core/installers/clients/ClientInstaller.d.ts +23 -0
  29. package/dist/core/installers/clients/ClientInstaller.js +573 -0
  30. package/dist/core/installers/clients/ExtensionInstaller.d.ts +26 -0
  31. package/dist/core/installers/clients/ExtensionInstaller.js +149 -0
  32. package/dist/core/installers/index.d.ts +8 -6
  33. package/dist/core/installers/index.js +8 -6
  34. package/dist/core/installers/requirements/BaseInstaller.d.ts +59 -0
  35. package/dist/core/installers/requirements/BaseInstaller.js +168 -0
  36. package/dist/core/installers/requirements/CommandInstaller.d.ts +37 -0
  37. package/dist/core/installers/requirements/CommandInstaller.js +173 -0
  38. package/dist/core/installers/requirements/GeneralInstaller.d.ts +33 -0
  39. package/dist/core/installers/requirements/GeneralInstaller.js +86 -0
  40. package/dist/core/installers/requirements/InstallerFactory.d.ts +54 -0
  41. package/dist/core/installers/requirements/InstallerFactory.js +97 -0
  42. package/dist/core/installers/requirements/NpmInstaller.d.ts +26 -0
  43. package/dist/core/installers/requirements/NpmInstaller.js +128 -0
  44. package/dist/core/installers/requirements/PipInstaller.d.ts +28 -0
  45. package/dist/core/installers/requirements/PipInstaller.js +128 -0
  46. package/{src/core/installers/RequirementInstaller.ts → dist/core/installers/requirements/RequirementInstaller.d.ts} +33 -38
  47. package/dist/core/installers/requirements/RequirementInstaller.js +3 -0
  48. package/dist/core/types.d.ts +4 -1
  49. package/dist/services/ServerService.js +1 -1
  50. package/dist/utils/githubUtils.d.ts +11 -0
  51. package/dist/utils/githubUtils.js +88 -0
  52. package/dist/utils/osUtils.d.ts +17 -0
  53. package/dist/utils/osUtils.js +184 -0
  54. package/dist/web/public/css/modal.css +97 -3
  55. package/dist/web/public/index.html +21 -2
  56. package/dist/web/public/js/modal.js +177 -28
  57. package/dist/web/public/js/serverCategoryDetails.js +12 -10
  58. package/dist/web/public/js/serverCategoryList.js +20 -5
  59. package/dist/web/public/modal.html +27 -13
  60. package/package.json +1 -1
  61. package/src/cli/commands/install.ts +4 -2
  62. package/src/cli/commands/list.ts +1 -0
  63. package/src/cli/index.ts +1 -1
  64. package/src/core/ConfigurationLoader.ts +251 -0
  65. package/src/core/ConfigurationProvider.ts +13 -195
  66. package/src/core/InstallationService.ts +140 -106
  67. package/src/core/RequirementService.ts +5 -10
  68. package/src/core/constants.ts +15 -1
  69. package/src/core/installers/{ClientInstaller.ts → clients/ClientInstaller.ts} +157 -51
  70. package/src/core/installers/clients/ExtensionInstaller.ts +162 -0
  71. package/src/core/installers/index.ts +9 -7
  72. package/src/core/installers/{BaseInstaller.ts → requirements/BaseInstaller.ts} +10 -118
  73. package/src/core/installers/{CommandInstaller.ts → requirements/CommandInstaller.ts} +7 -3
  74. package/src/core/installers/{GeneralInstaller.ts → requirements/GeneralInstaller.ts} +6 -2
  75. package/src/core/installers/{InstallerFactory.ts → requirements/InstallerFactory.ts} +11 -9
  76. package/src/core/installers/{NpmInstaller.ts → requirements/NpmInstaller.ts} +7 -4
  77. package/src/core/installers/{PipInstaller.ts → requirements/PipInstaller.ts} +26 -10
  78. package/src/core/installers/requirements/RequirementInstaller.ts +41 -0
  79. package/src/core/types.ts +4 -1
  80. package/src/services/ServerService.ts +1 -1
  81. package/src/utils/githubUtils.ts +103 -0
  82. package/src/utils/osUtils.ts +206 -15
  83. package/src/web/public/css/modal.css +97 -3
  84. package/src/web/public/index.html +21 -2
  85. package/src/web/public/js/modal.js +177 -28
  86. package/src/web/public/js/serverCategoryDetails.js +12 -10
  87. package/src/web/public/js/serverCategoryList.js +20 -5
  88. package/src/web/public/modal.html +27 -13
@@ -0,0 +1,149 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { SUPPORTED_CLIENTS } from '../../constants.js';
4
+ import { Logger } from '../../../utils/logger.js';
5
+ import { handleGitHubRelease } from '../../../utils/githubUtils.js';
6
+ import { compareVersions } from '../../../utils/versionUtils.js';
7
+ const execAsync = promisify(exec);
8
+ export class ExtensionInstaller {
9
+ /**
10
+ * Get VSCode path based on the OS type
11
+ */
12
+ static async getVSCodePath(isInsiders) {
13
+ const command = isInsiders ? 'code-insiders' : 'code';
14
+ try {
15
+ if (process.platform === 'win32') {
16
+ // Windows: Check command availability first
17
+ await execAsync(`where ${command}`);
18
+ return command;
19
+ }
20
+ else if (process.platform === 'darwin') {
21
+ // macOS: Check in both system and user Applications
22
+ const appName = isInsiders ? 'Visual Studio Code - Insiders.app' : 'Visual Studio Code.app';
23
+ const systemPath = `/Applications/${appName}`;
24
+ const userPath = `${process.env.HOME}/Applications/${appName}`;
25
+ try {
26
+ await execAsync(`test -d "${systemPath}"`);
27
+ return systemPath;
28
+ }
29
+ catch {
30
+ try {
31
+ await execAsync(`test -d "${userPath}"`);
32
+ return userPath;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ }
39
+ return command;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * List installed extensions for VSCode or VSCode Insiders
47
+ */
48
+ static async listExtensions(isInsiders) {
49
+ const command = isInsiders ? 'code-insiders' : 'code';
50
+ try {
51
+ const { stdout } = await execAsync(`${command} --list-extensions --show-versions`);
52
+ return stdout.split('\n')
53
+ .filter(line => line.trim())
54
+ .map(line => {
55
+ const [extension, version] = line.split('@');
56
+ const [publisher, name] = extension.split('.');
57
+ return { name, publisher, version: version || '' };
58
+ });
59
+ }
60
+ catch (error) {
61
+ Logger.error(`Failed to list extensions for ${command}:`, error);
62
+ return [];
63
+ }
64
+ }
65
+ /**
66
+ * Check if an extension is installed and get its version
67
+ */
68
+ static async checkExtension(extensionId, isInsiders) {
69
+ const extensions = await this.listExtensions(isInsiders);
70
+ const [publisher, name] = extensionId.split('.');
71
+ const extension = extensions.find(ext => ext.publisher.toLowerCase() === publisher.toLowerCase() &&
72
+ ext.name.toLowerCase() === name.toLowerCase());
73
+ return extension ? extension.version : null;
74
+ }
75
+ /**
76
+ * Install extension from marketplace
77
+ */
78
+ static async installPublicExtension(extensionId, isInsiders) {
79
+ const command = isInsiders ? 'code-insiders' : 'code';
80
+ try {
81
+ await execAsync(`${command} --install-extension ${extensionId}`);
82
+ return true;
83
+ }
84
+ catch (error) {
85
+ Logger.error(`Failed to install extension ${extensionId}:`, error);
86
+ return false;
87
+ }
88
+ }
89
+ /**
90
+ * Install extension from VSIX file
91
+ */
92
+ static async installPrivateExtension(vsixPath, isInsiders) {
93
+ const command = isInsiders ? 'code-insiders' : 'code';
94
+ try {
95
+ await execAsync(`${command} --install-extension "${vsixPath}"`);
96
+ return true;
97
+ }
98
+ catch (error) {
99
+ Logger.error(`Failed to install extension from VSIX ${vsixPath}:`, error);
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * Install extension for a specific client
105
+ */
106
+ static async installExtension(clientName) {
107
+ const client = SUPPORTED_CLIENTS[clientName];
108
+ if (!client?.extension?.extensionId) {
109
+ Logger.error(`No extension configuration found for client ${clientName}`);
110
+ return false;
111
+ }
112
+ const { extensionId, leastVersion, repository, assetName, private: isPrivate } = client.extension;
113
+ let success = false;
114
+ // Check both VSCode and VSCode Insiders
115
+ for (const isInsiders of [false, true]) {
116
+ const vscodePath = await this.getVSCodePath(isInsiders);
117
+ if (!vscodePath) {
118
+ Logger.debug(`${isInsiders ? 'VSCode Insiders' : 'VSCode'} not found, skipping...`);
119
+ continue;
120
+ }
121
+ const currentVersion = await this.checkExtension(extensionId, isInsiders);
122
+ if (!currentVersion || (isPrivate && leastVersion && compareVersions(currentVersion, leastVersion) < 0)) {
123
+ // Extension not installed or needs update (for private extensions)
124
+ if (!isPrivate) {
125
+ // Install public extension from marketplace
126
+ success = await this.installPublicExtension(extensionId, isInsiders);
127
+ }
128
+ else {
129
+ // Install private extension from GitHub release using latest version
130
+ try {
131
+ const { resolvedPath } = await handleGitHubRelease({ name: extensionId, version: 'latest', type: 'extension' }, { repository, assetName });
132
+ success = await this.installPrivateExtension(resolvedPath, isInsiders);
133
+ }
134
+ catch (error) {
135
+ Logger.error(`Failed to install/update private extension ${extensionId}:`, error);
136
+ continue;
137
+ }
138
+ }
139
+ }
140
+ else {
141
+ // Extension already installed and up to date
142
+ Logger.debug(`Extension ${extensionId} is already installed and up to date`);
143
+ success = true;
144
+ }
145
+ }
146
+ return success;
147
+ }
148
+ }
149
+ //# sourceMappingURL=ExtensionInstaller.js.map
@@ -1,6 +1,8 @@
1
- export { RequirementInstaller } from './RequirementInstaller.js';
2
- export { BaseInstaller } from './BaseInstaller.js';
3
- export { NpmInstaller } from './NpmInstaller.js';
4
- export { PipInstaller } from './PipInstaller.js';
5
- export { GeneralInstaller } from './GeneralInstaller.js';
6
- export { InstallerFactory, createInstallerFactory } from './InstallerFactory.js';
1
+ export { RequirementInstaller } from './requirements/RequirementInstaller.js';
2
+ export { ClientInstaller } from './clients/ClientInstaller.js';
3
+ export { BaseInstaller } from './requirements/BaseInstaller.js';
4
+ export { NpmInstaller } from './requirements/NpmInstaller.js';
5
+ export { PipInstaller } from './requirements/PipInstaller.js';
6
+ export { GeneralInstaller } from './requirements/GeneralInstaller.js';
7
+ export { CommandInstaller } from './requirements/CommandInstaller.js';
8
+ export { InstallerFactory, createInstallerFactory } from './requirements/InstallerFactory.js';
@@ -1,7 +1,9 @@
1
- // Export all installer implementations
2
- export { BaseInstaller } from './BaseInstaller.js';
3
- export { NpmInstaller } from './NpmInstaller.js';
4
- export { PipInstaller } from './PipInstaller.js';
5
- export { GeneralInstaller } from './GeneralInstaller.js';
6
- export { InstallerFactory, createInstallerFactory } from './InstallerFactory.js';
1
+ export { ClientInstaller } from './clients/ClientInstaller.js';
2
+ // Export all requirement installer implementations
3
+ export { BaseInstaller } from './requirements/BaseInstaller.js';
4
+ export { NpmInstaller } from './requirements/NpmInstaller.js';
5
+ export { PipInstaller } from './requirements/PipInstaller.js';
6
+ export { GeneralInstaller } from './requirements/GeneralInstaller.js';
7
+ export { CommandInstaller } from './requirements/CommandInstaller.js';
8
+ export { InstallerFactory, createInstallerFactory } from './requirements/InstallerFactory.js';
7
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,59 @@
1
+ import { RequirementConfig, RequirementStatus, RegistryConfig, ServerInstallOptions } from '../../types.js';
2
+ import { RequirementInstaller } from './RequirementInstaller.js';
3
+ /**
4
+ * Abstract base class with common functionality for all requirement installers
5
+ */
6
+ export declare abstract class BaseInstaller implements RequirementInstaller {
7
+ protected execPromise: (command: string) => Promise<{
8
+ stdout: string;
9
+ stderr: string;
10
+ }>;
11
+ protected downloadsDir: string;
12
+ constructor(execPromise: (command: string) => Promise<{
13
+ stdout: string;
14
+ stderr: string;
15
+ }>);
16
+ abstract canHandle(requirement: RequirementConfig): boolean;
17
+ abstract supportCheckUpdates(): boolean;
18
+ abstract install(requirement: RequirementConfig, options?: ServerInstallOptions): Promise<RequirementStatus>;
19
+ abstract checkInstallation(requirement: RequirementConfig, options?: ServerInstallOptions): Promise<RequirementStatus>;
20
+ /**
21
+ * Check if updates are available for the requirement
22
+ * @param requirement The requirement to check
23
+ * @param currentStatus The current status of the requirement
24
+ * @returns The status of the requirement with update information
25
+ */
26
+ checkForUpdates(requirement: RequirementConfig, currentStatus: RequirementStatus): Promise<RequirementStatus>;
27
+ /**
28
+ * Helper to handle artifact registry downloads
29
+ * @param requirement The requirement configuration
30
+ * @param registry The artifacts registry configuration
31
+ * @returns The registry URL
32
+ */
33
+ protected handleArtifactsRegistry(requirement: RequirementConfig, registry: RegistryConfig['artifacts']): Promise<string>;
34
+ /**
35
+ * Helper to handle local path registry
36
+ * @param requirement The requirement configuration
37
+ * @param registry The local registry configuration
38
+ * @returns The local path or extracted asset path
39
+ */
40
+ protected handleLocalRegistry(requirement: RequirementConfig, registry: RegistryConfig['local']): Promise<string>;
41
+ /**
42
+ * Get the latest version available for a GitHub repository
43
+ * @param repository The GitHub repository in format 'owner/repo'
44
+ * @returns The latest version or tag
45
+ */
46
+ protected getGitHubLatestVersion(repository: string): Promise<string>;
47
+ /**
48
+ * Get the latest version available for an NPM package
49
+ * @param packageName The name of the NPM package
50
+ * @returns The latest version
51
+ */
52
+ protected getNpmLatestVersion(packageName: string): Promise<string>;
53
+ /**
54
+ * Get the latest version available for a pip package
55
+ * @param packageName The name of the pip package
56
+ * @returns The latest version
57
+ */
58
+ protected getPipLatestVersion(packageName: string, options?: ServerInstallOptions): Promise<string>;
59
+ }
@@ -0,0 +1,168 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import { SETTINGS_DIR } from '../../constants.js';
4
+ import { extractZipFile } from '../../../utils/clientUtils.js';
5
+ /**
6
+ * Abstract base class with common functionality for all requirement installers
7
+ */
8
+ export class BaseInstaller {
9
+ execPromise;
10
+ downloadsDir;
11
+ constructor(execPromise) {
12
+ this.execPromise = execPromise;
13
+ this.downloadsDir = path.join(SETTINGS_DIR, 'downloads');
14
+ }
15
+ /**
16
+ * Check if updates are available for the requirement
17
+ * @param requirement The requirement to check
18
+ * @param currentStatus The current status of the requirement
19
+ * @returns The status of the requirement with update information
20
+ */
21
+ async checkForUpdates(requirement, currentStatus) {
22
+ try {
23
+ // If requirement is not installed, no need to check for updates
24
+ if (!currentStatus.installed) {
25
+ return currentStatus;
26
+ }
27
+ // If the version doesn't contain "latest", no update check needed
28
+ if (!requirement.version.includes('latest')) {
29
+ return currentStatus;
30
+ }
31
+ let latestVersion;
32
+ // Check based on registry type
33
+ if (requirement.registry?.githubRelease) {
34
+ latestVersion = await this.getGitHubLatestVersion(requirement.registry.githubRelease.repository);
35
+ }
36
+ else {
37
+ // Use common practice based on requirement type
38
+ switch (requirement.type) {
39
+ case 'npm':
40
+ latestVersion = await this.getNpmLatestVersion(requirement.name);
41
+ break;
42
+ case 'pip':
43
+ latestVersion = await this.getPipLatestVersion(requirement.name);
44
+ break;
45
+ // Add other types as needed
46
+ }
47
+ }
48
+ // If we found a latest version and it's different from current
49
+ if (latestVersion && latestVersion !== currentStatus.version) {
50
+ return {
51
+ ...currentStatus,
52
+ availableUpdate: {
53
+ version: latestVersion,
54
+ message: `Update available: ${currentStatus.version} → ${latestVersion}`
55
+ },
56
+ lastCheckTime: new Date().toISOString()
57
+ };
58
+ }
59
+ else {
60
+ return {
61
+ ...currentStatus,
62
+ lastCheckTime: new Date().toISOString()
63
+ };
64
+ }
65
+ }
66
+ catch (error) {
67
+ // Don't update status on error, just log it
68
+ console.warn(`Error checking for updates for ${requirement.name}: ${error instanceof Error ? error.message : String(error)}`);
69
+ return currentStatus;
70
+ }
71
+ }
72
+ /**
73
+ * Helper to handle artifact registry downloads
74
+ * @param requirement The requirement configuration
75
+ * @param registry The artifacts registry configuration
76
+ * @returns The registry URL
77
+ */
78
+ async handleArtifactsRegistry(requirement, registry) {
79
+ if (!registry) {
80
+ throw new Error('Artifacts registry configuration is required');
81
+ }
82
+ const { registryUrl, assetName } = registry;
83
+ if (!registryUrl) {
84
+ throw new Error('Registry URL is required for artifacts downloads');
85
+ }
86
+ return registryUrl;
87
+ }
88
+ /**
89
+ * Helper to handle local path registry
90
+ * @param requirement The requirement configuration
91
+ * @param registry The local registry configuration
92
+ * @returns The local path or extracted asset path
93
+ */
94
+ async handleLocalRegistry(requirement, registry) {
95
+ if (!registry) {
96
+ throw new Error('Local registry configuration is required');
97
+ }
98
+ const { localPath, assetName } = registry;
99
+ if (!localPath) {
100
+ throw new Error('Local path is required for local registry');
101
+ }
102
+ // Verify the local path exists
103
+ try {
104
+ await fs.access(localPath);
105
+ }
106
+ catch (error) {
107
+ throw new Error(`Local path ${localPath} does not exist or is not accessible`);
108
+ }
109
+ // If the path is a zip file and assetName is specified, extract it
110
+ if (localPath.endsWith('.zip') && assetName) {
111
+ const extractDir = path.join(this.downloadsDir, path.basename(localPath, '.zip'));
112
+ await fs.mkdir(extractDir, { recursive: true });
113
+ // Extract the zip file
114
+ await extractZipFile(localPath, { dir: extractDir });
115
+ // Find the asset in the extracted directory
116
+ const assetPath = path.join(extractDir, assetName);
117
+ try {
118
+ await fs.access(assetPath);
119
+ return assetPath;
120
+ }
121
+ catch (error) {
122
+ throw new Error(`Asset ${assetName} not found in extracted directory ${extractDir}`);
123
+ }
124
+ }
125
+ return localPath;
126
+ }
127
+ /**
128
+ * Get the latest version available for a GitHub repository
129
+ * @param repository The GitHub repository in format 'owner/repo'
130
+ * @returns The latest version or tag
131
+ */
132
+ async getGitHubLatestVersion(repository) {
133
+ try {
134
+ // Use GitHub CLI to get the latest release
135
+ const { stdout } = await this.execPromise(`gh release view --repo ${repository} --json tagName --jq .tagName`);
136
+ const latestTag = stdout.trim();
137
+ // Remove 'v' prefix if present
138
+ return latestTag.startsWith('v') ? latestTag.substring(1) : latestTag;
139
+ }
140
+ catch (error) {
141
+ // If gh command fails, try to get the latest tag
142
+ 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}'`);
143
+ let latestTag = stdout.trim();
144
+ // Remove 'v' prefix if present
145
+ return latestTag.startsWith('v') ? latestTag.substring(1) : latestTag;
146
+ }
147
+ }
148
+ /**
149
+ * Get the latest version available for an NPM package
150
+ * @param packageName The name of the NPM package
151
+ * @returns The latest version
152
+ */
153
+ async getNpmLatestVersion(packageName) {
154
+ const { stdout } = await this.execPromise(`npm view ${packageName} version`);
155
+ return stdout.trim();
156
+ }
157
+ /**
158
+ * Get the latest version available for a pip package
159
+ * @param packageName The name of the pip package
160
+ * @returns The latest version
161
+ */
162
+ async getPipLatestVersion(packageName, options) {
163
+ const pythonCmd = options?.settings?.pythonEnv || 'python';
164
+ const { stdout } = await this.execPromise(`${pythonCmd} -m pip index versions ${packageName} --pre=0 | grep -oP "(?<=Latest:\\s)[^\\s]+" | head -1`);
165
+ return stdout.trim();
166
+ }
167
+ }
168
+ //# sourceMappingURL=BaseInstaller.js.map
@@ -0,0 +1,37 @@
1
+ import { RequirementConfig, RequirementStatus } from '../../types.js';
2
+ import { BaseInstaller } from './BaseInstaller.js';
3
+ /**
4
+ * Installer implementation for command-line tools
5
+ */
6
+ export declare class CommandInstaller extends BaseInstaller {
7
+ /**
8
+ * Mapping of command names to their package IDs
9
+ * This handles special cases where the command name differs from the package ID
10
+ */
11
+ private commandMappings;
12
+ /**
13
+ * Check if this installer can handle the given requirement type
14
+ * @param requirement The requirement to check
15
+ * @returns True if this installer can handle the requirement
16
+ */
17
+ canHandle(requirement: RequirementConfig): boolean;
18
+ supportCheckUpdates(): boolean;
19
+ /**
20
+ * Get the mapped package ID for a command
21
+ * @param commandName The command name to map
22
+ * @returns The mapped package ID
23
+ */
24
+ private getMappedPackageId;
25
+ /**
26
+ * Check if the command is already installed
27
+ * @param requirement The requirement to check
28
+ * @returns The status of the requirement
29
+ */
30
+ checkInstallation(requirement: RequirementConfig): Promise<RequirementStatus>;
31
+ /**
32
+ * Install the command
33
+ * @param requirement The requirement to install
34
+ * @returns The status of the installation
35
+ */
36
+ install(requirement: RequirementConfig): Promise<RequirementStatus>;
37
+ }
@@ -0,0 +1,173 @@
1
+ import { OSType } from '../../types.js';
2
+ import { BaseInstaller } from './BaseInstaller.js';
3
+ import { getOSType, refreshPathEnv } from '../../../utils/osUtils.js';
4
+ import { Logger } from '../../../utils/logger.js';
5
+ /**
6
+ * Installer implementation for command-line tools
7
+ */
8
+ export class CommandInstaller extends BaseInstaller {
9
+ /**
10
+ * Mapping of command names to their package IDs
11
+ * This handles special cases where the command name differs from the package ID
12
+ */
13
+ commandMappings = {
14
+ 'uv': { windows: 'astral-sh.uv', macos: 'uv' }
15
+ // Add more mappings as needed
16
+ };
17
+ /**
18
+ * Check if this installer can handle the given requirement type
19
+ * @param requirement The requirement to check
20
+ * @returns True if this installer can handle the requirement
21
+ */
22
+ canHandle(requirement) {
23
+ return requirement.type === 'command';
24
+ }
25
+ supportCheckUpdates() {
26
+ return false;
27
+ }
28
+ /**
29
+ * Get the mapped package ID for a command
30
+ * @param commandName The command name to map
31
+ * @returns The mapped package ID
32
+ */
33
+ getMappedPackageId(commandName) {
34
+ const osType = getOSType();
35
+ const mapping = this.commandMappings[commandName];
36
+ if (mapping) {
37
+ return osType === OSType.Windows ? mapping.windows : mapping.macos;
38
+ }
39
+ // If no mapping exists, use the command name itself
40
+ return commandName;
41
+ }
42
+ /**
43
+ * Check if the command is already installed
44
+ * @param requirement The requirement to check
45
+ * @returns The status of the requirement
46
+ */
47
+ async checkInstallation(requirement) {
48
+ try {
49
+ await refreshPathEnv();
50
+ const commandName = requirement.alias || requirement.name;
51
+ const osType = getOSType();
52
+ let commandResult;
53
+ if (osType === OSType.Windows) {
54
+ // Check if command exists on Windows
55
+ try {
56
+ commandResult = await this.execPromise(`where ${commandName} 2>nul`);
57
+ }
58
+ catch (error) {
59
+ Logger.debug(`Error checking command existence: ${error}`);
60
+ // On Windows, 'where' command returns non-zero exit code if the command is not found
61
+ // We'll handle this as "command not found" rather than an error
62
+ commandResult = { stdout: '', stderr: '' };
63
+ }
64
+ }
65
+ else {
66
+ // Check if command exists on macOS/Linux
67
+ try {
68
+ commandResult = await this.execPromise(`which ${commandName} 2>/dev/null`);
69
+ }
70
+ catch (error) {
71
+ Logger.debug(`Error checking command existence: ${error}`);
72
+ // Similarly handle command not found on Unix systems
73
+ commandResult = { stdout: '', stderr: '' };
74
+ }
75
+ }
76
+ // If the command exists, it will return a path or multiple paths
77
+ const installed = commandResult.stdout.trim().length > 0;
78
+ // Try to get version information if available
79
+ let version;
80
+ if (installed) {
81
+ try {
82
+ const versionResult = await this.execPromise(`${commandName} --version`);
83
+ if (versionResult.stdout) {
84
+ // Extract version information - this is a simple approach that might need refinement
85
+ const versionMatch = versionResult.stdout.match(/\d+\.\d+(\.\d+)?/);
86
+ version = versionMatch ? versionMatch[0] : undefined;
87
+ }
88
+ }
89
+ catch (error) {
90
+ Logger.debug(`Error checking command version: ${error}`);
91
+ // Ignore errors from version check, consider it installed anyway
92
+ }
93
+ }
94
+ return {
95
+ name: requirement.name,
96
+ type: 'command',
97
+ installed,
98
+ version,
99
+ inProgress: false
100
+ };
101
+ }
102
+ catch (error) {
103
+ Logger.error(`Error checking installation: ${error}`);
104
+ return {
105
+ name: requirement.name,
106
+ type: 'command',
107
+ installed: false,
108
+ error: error instanceof Error ? error.message : String(error),
109
+ inProgress: false
110
+ };
111
+ }
112
+ }
113
+ /**
114
+ * Install the command
115
+ * @param requirement The requirement to install
116
+ * @returns The status of the installation
117
+ */
118
+ async install(requirement) {
119
+ try {
120
+ const status = await this.checkInstallation(requirement);
121
+ if (status.installed) {
122
+ return status;
123
+ }
124
+ const packageId = this.getMappedPackageId(requirement.name);
125
+ const osType = getOSType();
126
+ let installCommand;
127
+ if (osType === OSType.Windows) {
128
+ // Windows installation using winget
129
+ installCommand = `winget install --id ${packageId}`;
130
+ if (requirement.version && requirement.version !== 'latest') {
131
+ installCommand += ` --version ${requirement.version}`;
132
+ }
133
+ }
134
+ else if (osType === OSType.MacOS) {
135
+ // macOS installation using Homebrew
136
+ installCommand = `brew install ${packageId}`;
137
+ if (requirement.version && requirement.version !== 'latest') {
138
+ installCommand += `@${requirement.version}`;
139
+ }
140
+ }
141
+ else {
142
+ throw new Error(`Unsupported operating system for installing ${requirement.name}`);
143
+ }
144
+ // Execute the installation command
145
+ const { stderr } = await this.execPromise(installCommand);
146
+ if (stderr && stderr.toLowerCase().includes('error')) {
147
+ throw new Error(stderr);
148
+ }
149
+ // Check if installation was successful
150
+ const updatedStatus = await this.checkInstallation(requirement);
151
+ if (!updatedStatus.installed) {
152
+ throw new Error(`Failed to install ${requirement.name}`);
153
+ }
154
+ return {
155
+ name: requirement.name,
156
+ type: 'command',
157
+ installed: true,
158
+ version: updatedStatus.version || requirement.version,
159
+ inProgress: false
160
+ };
161
+ }
162
+ catch (error) {
163
+ return {
164
+ name: requirement.name,
165
+ type: 'command',
166
+ installed: false,
167
+ error: error instanceof Error ? error.message : String(error),
168
+ inProgress: false
169
+ };
170
+ }
171
+ }
172
+ }
173
+ //# sourceMappingURL=CommandInstaller.js.map
@@ -0,0 +1,33 @@
1
+ import { RequirementConfig, RequirementStatus } from '../../types.js';
2
+ import { BaseInstaller } from './BaseInstaller.js';
3
+ /**
4
+ * Installer implementation for general requirements (type 'other')
5
+ * This installer handles requirements that don't fit into specific package manager categories
6
+ */
7
+ export declare class GeneralInstaller extends BaseInstaller {
8
+ /**
9
+ * Check if this installer can handle the given requirement type
10
+ * @param requirement The requirement to check
11
+ * @returns True if this installer can handle the requirement
12
+ */
13
+ canHandle(requirement: RequirementConfig): boolean;
14
+ supportCheckUpdates(): boolean;
15
+ /**
16
+ * Check if the requirement is already installed
17
+ * For general installers, we can't easily check if something is installed
18
+ * without specific knowledge of the requirement, so we always return false
19
+ *
20
+ * @param requirement The requirement to check
21
+ * @returns The status of the requirement
22
+ */
23
+ checkInstallation(requirement: RequirementConfig): Promise<RequirementStatus>;
24
+ /**
25
+ * Install the general requirement
26
+ * For type 'other', this doesn't actually install anything, but downloads
27
+ * or locates the asset and returns the path for the caller to use
28
+ *
29
+ * @param requirement The requirement to install
30
+ * @returns The status of the installation, including the install path in updateInfo
31
+ */
32
+ install(requirement: RequirementConfig): Promise<RequirementStatus>;
33
+ }