package-manager.js•10.5 kB
import { exec as execCallback } from 'child_process';
import * as path from 'path';
import { promisify } from 'util';
import { REGISTRY_FILE } from '../config.js';
import { ensureDir, fileExists, readJsonFileOrDefault, writeJsonFile } from '../utils/fs-utils.js';
// Promisified exec
const exec = promisify(execCallback);
/**
* Service for managing MCP package installations
*/
export class PackageManager {
configService;
registryPath;
packagesDir;
registry;
preferredPackageManager;
/**
* Creates a new PackageManager instance
* @param configService Configuration service
*/
constructor(configService) {
this.configService = configService;
const config = configService.getInstallationConfig();
this.packagesDir = config.packageManager.installationDir;
this.registryPath = REGISTRY_FILE;
this.preferredPackageManager = config.packageManager.preferredPackageManager;
this.registry = { packages: {} };
}
/**
* Loads the package registry from disk
*/
async loadRegistry() {
this.registry = await readJsonFileOrDefault(this.registryPath, { packages: {} });
// Ensure packages directory exists
await ensureDir(this.packagesDir);
}
/**
* Saves the package registry to disk
*/
async saveRegistry() {
await writeJsonFile(this.registryPath, this.registry);
}
/**
* Gets all installed packages
*/
getInstalledPackages() {
return Object.values(this.registry.packages);
}
/**
* Gets an installed package by name
*/
getInstalledPackage(packageName) {
return this.registry.packages[packageName];
}
/**
* Installs an MCP package
* @param packageName Package name to install
* @param version Optional specific version to install
*/
async installPackage(packageName, version) {
try {
// Create package-specific directory
const packageDir = path.join(this.packagesDir, packageName.replace('/', '-'));
await ensureDir(packageDir);
// Create package.json if it doesn't exist
const packageJsonPath = path.join(packageDir, 'package.json');
if (!(await fileExists(packageJsonPath))) {
await writeJsonFile(packageJsonPath, {
name: 'mcp-package-wrapper',
version: '1.0.0',
private: true,
type: 'module'
});
}
// Install the package
const installCmd = this.getInstallCommand(packageName, version);
await exec(installCmd, { cwd: packageDir });
// Get installed version
const nodeModulesDir = path.join(packageDir, 'node_modules');
const installedVersion = await this.getInstalledVersion(nodeModulesDir, packageName);
// Find bin path if it exists
const binPath = await this.findPackageBin(nodeModulesDir, packageName);
// Add to registry
const now = new Date().toISOString();
const localPath = path.join(nodeModulesDir, packageName);
const installedPackage = {
name: packageName,
version: installedVersion,
localPath,
binPath,
installedAt: now,
updatedAt: now,
dependencies: [], // TODO: Extract dependencies
usedByConfigs: []
};
this.registry.packages[packageName] = installedPackage;
await this.saveRegistry();
return {
success: true,
packageName,
version: installedVersion,
localPath,
binPath
};
}
catch (error) {
return {
success: false,
packageName,
version: version || 'latest',
localPath: '',
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Updates an installed package
* @param packageName Package name to update
* @param version Optional target version
*/
async updatePackage(packageName, version) {
const existing = this.getInstalledPackage(packageName);
if (!existing) {
return this.installPackage(packageName, version);
}
try {
const packageDir = path.dirname(path.dirname(existing.localPath));
// Update the package
const updateCmd = this.getInstallCommand(packageName, version);
await exec(updateCmd, { cwd: packageDir });
// Get installed version
const nodeModulesDir = path.join(packageDir, 'node_modules');
const installedVersion = await this.getInstalledVersion(nodeModulesDir, packageName);
// Find bin path if it exists
const binPath = await this.findPackageBin(nodeModulesDir, packageName);
// Update registry
const now = new Date().toISOString();
const localPath = path.join(nodeModulesDir, packageName);
this.registry.packages[packageName] = {
...existing,
version: installedVersion,
localPath,
binPath,
updatedAt: now
};
await this.saveRegistry();
return {
success: true,
packageName,
version: installedVersion,
localPath,
binPath
};
}
catch (error) {
return {
success: false,
packageName,
version: version || 'latest',
localPath: existing.localPath,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Uninstalls a package
* @param packageName Package name to uninstall
*/
async uninstallPackage(packageName) {
const existing = this.getInstalledPackage(packageName);
if (!existing) {
return false;
}
try {
const packageDir = path.dirname(path.dirname(existing.localPath));
// Uninstall the package
const uninstallCmd = `${this.preferredPackageManager} remove ${packageName}`;
await exec(uninstallCmd, { cwd: packageDir });
// Remove from registry
delete this.registry.packages[packageName];
await this.saveRegistry();
return true;
}
catch (error) {
console.error(`Failed to uninstall package ${packageName}:`, error);
return false;
}
}
/**
* Adds a configuration reference to a package
* @param packageName Package name
* @param configRef Configuration reference
*/
async addConfigReference(packageName, configRef) {
const existing = this.getInstalledPackage(packageName);
if (!existing) {
throw new Error(`Package not installed: ${packageName}`);
}
// Check if reference already exists
const existingRef = existing.usedByConfigs.find(ref => ref.path === configRef.path && ref.serverName === configRef.serverName);
if (!existingRef) {
existing.usedByConfigs.push(configRef);
await this.saveRegistry();
}
}
/**
* Removes a configuration reference from a package
* @param packageName Package name
* @param configPath Configuration file path
* @param serverName Server name
*/
async removeConfigReference(packageName, configPath, serverName) {
const existing = this.getInstalledPackage(packageName);
if (!existing) {
return;
}
existing.usedByConfigs = existing.usedByConfigs.filter(ref => ref.path !== configPath || ref.serverName !== serverName);
await this.saveRegistry();
}
/**
* Gets the install command for a package
* @param packageName Package name
* @param version Optional specific version
*/
getInstallCommand(packageName, version) {
const versionSuffix = version ? `@${version}` : '';
return `${this.preferredPackageManager} install ${packageName}${versionSuffix}`;
}
/**
* Gets the installed version of a package
* @param nodeModulesDir Node modules directory
* @param packageName Package name
*/
async getInstalledVersion(nodeModulesDir, packageName) {
try {
// Find the package.json in node_modules
const packageJsonPath = path.join(nodeModulesDir, packageName, 'package.json');
const packageJson = await readJsonFileOrDefault(packageJsonPath, { version: 'unknown' });
return packageJson.version;
}
catch (error) {
return 'unknown';
}
}
/**
* Finds the binary path for a package
* @param nodeModulesDir Node modules directory
* @param packageName Package name
*/
async findPackageBin(nodeModulesDir, packageName) {
try {
// Check package.json for bin field
const packageJsonPath = path.join(nodeModulesDir, packageName, 'package.json');
const packageJson = await readJsonFileOrDefault(packageJsonPath, { bin: undefined });
if (!packageJson.bin) {
return undefined;
}
// If bin is a string, use that
if (typeof packageJson.bin === 'string') {
return path.join(nodeModulesDir, packageName, packageJson.bin);
}
// If bin is an object, use the first entry
const binEntries = Object.entries(packageJson.bin);
if (binEntries.length > 0) {
const [, binPath] = binEntries[0];
return path.join(nodeModulesDir, packageName, binPath);
}
return undefined;
}
catch (error) {
return undefined;
}
}
}
/**
* Initializes the package manager
* @param configService Configuration service
*/
export async function initializePackageManager(configService) {
const packageManager = new PackageManager(configService);
await packageManager.loadRegistry();
return packageManager;
}
//# sourceMappingURL=package-manager.js.map