@toolprint/hypertool-mcp
Version:
HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers
888 lines • 36.6 kB
JavaScript
/**
* Unified Configuration Manager for HyperTool MCP
*
* This module provides centralized configuration management for integrating
* HyperTool with multiple MCP client applications.
*
* TODO: Full Profile Management System
* - Implement profile creation, switching, and deletion
* - Add profile-specific configuration storage
* - Support workspace/project-specific profiles
* - Add profile inheritance and merging
* - Implement profile templates and presets
*/
import { promises as fs } from "fs";
import { vol } from "memfs";
import { isTestMode } from "../config/environment.js";
import { join } from "path";
import { AppRegistry } from "./apps/registry.js";
import { BackupManager } from "./backup/manager.js";
import { TransformerRegistry } from "./transformers/base.js";
import { EnvironmentManager, } from "../config/environment.js";
import { ConfigMigration } from "./migration.js";
import { createChildLogger } from "../utils/logging.js";
const logger = createChildLogger({ module: "ConfigurationManager" });
export class ConfigurationManager {
basePath;
registry;
backupManager;
fs;
/**
* Get current working directory (for testing support)
*/
getCurrentWorkingDirectory() {
// In test mode, use test base path as working directory to avoid contamination
if (isTestMode()) {
return this.basePath;
}
return process.cwd();
}
constructor(basePath) {
// If no basePath provided, check environment manager
if (!basePath) {
const envConfig = EnvironmentManager.getInstance().getConfig();
basePath = envConfig.configRoot;
}
this.basePath = basePath;
this.registry = new AppRegistry(basePath);
this.backupManager = new BackupManager(basePath);
// Use memfs in test mode, real fs in production
this.fs = isTestMode() ? vol.promises : fs;
}
/**
* Create a ConfigurationManager from environment config
*/
static fromEnvironment(envConfig) {
const config = envConfig || EnvironmentManager.getInstance().getConfig();
return new ConfigurationManager(config.configRoot);
}
/**
* Initialize the configuration manager and directory structure
*/
async initialize() {
// Create directory structure
const dirs = [
this.basePath,
join(this.basePath, "apps"),
join(this.basePath, "apps/transformers"),
join(this.basePath, "backups"),
join(this.basePath, "cache"),
];
for (const dir of dirs) {
await this.fs.mkdir(dir, { recursive: true });
}
// Initialize registry (will create default if not exists)
await this.registry.load();
}
/**
* Discover and import configurations from all enabled applications
*/
async discoverAndImport() {
// Check if migration is needed first
const migration = new ConfigMigration(this.basePath);
if (await migration.needsMigration()) {
logger.info("Migrating from global mcp.json to per-app configurations...");
const migrationResult = await migration.migrate();
if (!migrationResult.success) {
logger.warn("Migration completed with errors", {
errors: migrationResult.errors,
});
}
else {
logger.info("Migration completed successfully");
}
}
// Create backup first
const backupResult = await this.backupManager.createBackup();
const backupPath = backupResult.success
? backupResult.backupPath || ""
: "";
const imported = [];
const importedDetails = [];
const failed = [];
const mergedServers = { mcpServers: {} };
// Get all enabled applications
const apps = await this.registry.getEnabledApplications();
// Import from each application and save to per-app configs
const appConfigPaths = {};
for (const [appId, app] of Object.entries(apps)) {
try {
const result = await this.importFromApplicationWithPath(appId, app);
if (result && result.config) {
// Merge servers with metadata
for (const [serverName, serverConfig] of Object.entries(result.config.mcpServers)) {
mergedServers.mcpServers[serverName] = serverConfig;
// Add metadata
if (!mergedServers._metadata) {
mergedServers._metadata = { sources: {} };
}
if (!mergedServers._metadata.sources) {
mergedServers._metadata.sources = {};
}
mergedServers._metadata.sources[serverName] = {
app: appId,
importedAt: new Date().toISOString(),
};
}
imported.push(appId);
importedDetails.push({ appId, configPath: result.configPath });
// Save app-specific config and track path
const appConfigPath = await this.saveAppConfig(appId, result.config);
appConfigPaths[appId] = appConfigPath;
}
}
catch (error) {
logger.warn(`Failed to import from ${appId}`, { appId, error });
failed.push(appId);
}
}
// Save merged configuration for backwards compatibility
await this.saveMergedConfig(mergedServers);
// Update main configuration with per-app config paths
await this.updateMainConfig(imported, appConfigPaths);
// Generate default toolsets
await this.generateDefaultToolsets(mergedServers);
return {
imported,
importedDetails,
failed,
backup: backupPath,
};
}
/**
* Import configuration from a specific application
*/
async importFromApplication(appId, app) {
// Check if application is installed
if (!(await this.registry.isApplicationInstalled(app))) {
return null;
}
// Get platform-specific config
const platformConfig = this.registry.getPlatformConfig(app);
if (!platformConfig) {
return null;
}
// Resolve configuration path
let configPath = this.registry.resolvePath(platformConfig.configPath);
// Handle project-local configurations
if (app.detection.type === "project-local") {
configPath = join(this.getCurrentWorkingDirectory(), platformConfig.configPath.replace("./", ""));
}
// Check if config exists
try {
await this.fs.access(configPath);
}
catch {
return null;
}
// Read configuration
const content = await this.fs.readFile(configPath, "utf-8");
const appConfig = JSON.parse(content);
// Transform to standard format
const transformerName = platformConfig.format === "custom" && platformConfig.transformer
? platformConfig.transformer
: platformConfig.format;
const transformer = TransformerRegistry.getTransformer(transformerName);
const standardConfig = transformer.toStandard(appConfig);
// Validate configuration
const validation = transformer.validate(appConfig);
if (!validation.valid) {
logger.warn(`Invalid configuration for ${appId}`, {
appId,
errors: validation.errors,
});
return null;
}
return standardConfig;
}
/**
* Import configuration from a specific application with path information
*/
async importFromApplicationWithPath(appId, app) {
// Check if application is installed
if (!(await this.registry.isApplicationInstalled(app))) {
return null;
}
// Get platform-specific config
const platformConfig = this.registry.getPlatformConfig(app);
if (!platformConfig) {
return null;
}
// Resolve configuration path
let configPath = this.registry.resolvePath(platformConfig.configPath);
// Handle project-local configurations
if (app.detection.type === "project-local") {
configPath = join(this.getCurrentWorkingDirectory(), platformConfig.configPath.replace("./", ""));
}
// Check if config exists
try {
await this.fs.access(configPath);
}
catch {
return null;
}
// Read configuration
const content = await this.fs.readFile(configPath, "utf-8");
const appConfig = JSON.parse(content);
// Transform to standard format
const transformerName = platformConfig.format === "custom" && platformConfig.transformer
? platformConfig.transformer
: platformConfig.format;
const transformer = TransformerRegistry.getTransformer(transformerName);
const standardConfig = transformer.toStandard(appConfig);
// Validate configuration
const validation = transformer.validate(appConfig);
if (!validation.valid) {
logger.warn(`Invalid configuration for ${appId}`, {
appId,
errors: validation.errors,
});
return null;
}
return { config: standardConfig, configPath };
}
/**
* Save the merged MCP configuration to database or file based on feature flag
*/
async saveMergedConfig(config) {
{
// File-based approach
const configPath = join(this.basePath, "mcp.json");
// Ensure the directory exists
await this.fs.mkdir(this.basePath, { recursive: true });
// Write the configuration file
await this.fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
logger.info(`Saved merged configuration to file: ${configPath}`);
return;
}
// Database approach
const { getCompositeDatabaseService } = await import("../db/compositeDatabaseService.js");
const dbService = getCompositeDatabaseService();
await dbService.init();
// Create or update global config source
let globalSource = await dbService.configSources.findByPath("global");
if (!globalSource) {
globalSource = await dbService.configSources.add({
type: "global",
path: "global",
priority: 100,
lastSynced: Date.now(),
});
}
if (!globalSource) {
throw new Error("Failed to create or find global config source");
}
// Save all servers and extensions to database with source reference
for (const [entryName, entryConfig] of Object.entries(config.mcpServers)) {
// Handle extensions separately from servers
if (entryConfig.type === "dxt-extension") {
// Extensions are handled by the extension manager, not stored as servers
// The extension manager will discover and load tools from DXT packages
logger.info(`DXT extension "${entryName}" found, will be handled by extension manager`);
continue;
}
// Handle server configs
const serverConfig = entryConfig;
// Skip websocket servers as they're not supported yet
if ("type" in serverConfig &&
serverConfig.type === "websocket") {
logger.warn(`Skipping websocket server "${entryName}" - not supported`);
continue;
}
const existingServer = await dbService.servers.findByName(entryName);
if (!existingServer) {
await dbService.servers.add({
name: entryName,
type: serverConfig.type,
config: serverConfig,
lastModified: Date.now(),
checksum: this.calculateChecksum(serverConfig),
sourceId: globalSource.id,
});
}
else if (existingServer?.id && existingServer?.name) {
await dbService.servers.update({
id: existingServer.id,
name: existingServer.name,
config: serverConfig,
type: serverConfig.type,
lastModified: Date.now(),
checksum: this.calculateChecksum(serverConfig),
sourceId: globalSource.id,
});
}
}
}
/**
* Calculate checksum for a server configuration
*/
calculateChecksum(config) {
const crypto = require("crypto");
const configString = JSON.stringify(config, Object.keys(config).sort());
return crypto.createHash("sha256").update(configString).digest("hex");
}
/**
* Save app-specific MCP configuration to database or file based on feature flag
*/
async saveAppConfig(appId, config) {
{
// File-based approach
const mcpDir = join(this.basePath, "mcp");
const configPath = join(mcpDir, `${appId}.json`);
// Ensure the mcp directory exists
await this.fs.mkdir(mcpDir, { recursive: true });
// Add metadata
const appConfig = {
...config,
_metadata: {
app: appId,
importedAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
...(config._metadata || {}),
},
};
// Write the configuration file
await this.fs.writeFile(configPath, JSON.stringify(appConfig, null, 2), "utf-8");
logger.info(`Saved app configuration to file: ${configPath}`);
return configPath;
}
// Database approach
const { getCompositeDatabaseService } = await import("../db/compositeDatabaseService.js");
const dbService = getCompositeDatabaseService();
await dbService.init();
// Create or update app config source
const configPath = `app/${appId}`;
let appSource = await dbService.configSources.findByPath(configPath);
if (!appSource) {
appSource = await dbService.configSources.add({
type: "app",
appId: appId,
path: configPath,
priority: 50,
lastSynced: Date.now(),
});
}
else {
await dbService.configSources.update({
...appSource,
lastSynced: Date.now(),
});
}
if (!appSource) {
throw new Error("Failed to create or find app config source");
}
// Save all servers and extensions to database with source reference
for (const [entryName, entryConfig] of Object.entries(config.mcpServers)) {
// Handle extensions separately from servers
if (entryConfig.type === "dxt-extension") {
// Extensions are handled by the extension manager, not stored as servers
logger.info(`DXT extension "${entryName}" found in app ${appId}, will be handled by extension manager`);
continue;
}
// Handle server configs
const serverConfig = entryConfig;
// Skip websocket servers as they're not supported yet
if ("type" in serverConfig &&
serverConfig.type === "websocket") {
logger.warn(`Skipping websocket server "${entryName}" - not supported`);
continue;
}
const existingServer = await dbService.servers.findByName(entryName);
if (!existingServer) {
await dbService.servers.add({
name: entryName,
type: serverConfig.type,
config: serverConfig,
lastModified: Date.now(),
checksum: this.calculateChecksum(serverConfig),
sourceId: appSource.id,
});
}
else if (existingServer?.sourceId && existingServer?.id) {
// Update only if this source has higher or equal priority
const existingSource = await dbService.configSources.findById(existingServer.sourceId);
if (!existingSource ||
(appSource.priority ?? 0) >= (existingSource?.priority ?? 0)) {
await dbService.servers.update({
id: existingServer.id,
name: existingServer.name,
config: serverConfig,
type: serverConfig.type,
lastModified: Date.now(),
checksum: this.calculateChecksum(serverConfig),
sourceId: appSource.id,
});
}
}
}
return configPath;
}
/**
* Update the main configuration in database
*/
async updateMainConfig(importedApps, appConfigPaths) {
// This method is no longer needed as all config is stored in database
// Keep it empty for backward compatibility
}
/**
* Generate default toolsets for each application
*/
async generateDefaultToolsets(mergedConfig) {
const prefsPath = join(this.basePath, "config.json");
let prefs;
try {
const content = await this.fs.readFile(prefsPath, "utf-8");
prefs = JSON.parse(content);
// Ensure toolsets property exists (for consolidated config structure)
if (!prefs.toolsets) {
prefs.toolsets = {};
}
// Ensure appDefaults property exists
if (!prefs.appDefaults) {
prefs.appDefaults = {};
}
}
catch {
prefs = {
toolsets: {},
appDefaults: {},
};
}
// Group servers by source application
const serversByApp = {};
if (mergedConfig._metadata?.sources) {
for (const [serverName, source] of Object.entries(mergedConfig._metadata.sources)) {
if (!serversByApp[source.app]) {
serversByApp[source.app] = [];
}
serversByApp[source.app].push(serverName);
}
}
// Create toolset for each app
for (const [appId] of Object.entries(serversByApp)) {
const toolsetId = `${appId}-default`;
// Skip if toolset already exists and is not auto-generated
if (prefs.toolsets[toolsetId] &&
!prefs.toolsets[toolsetId].metadata?.autoGenerated) {
continue;
}
const app = await this.registry.getApplication(appId);
if (!app)
continue;
// Create toolset with all tools from app's servers
const toolset = {
name: `${app.name} Default`,
description: `Auto-imported from ${app.name}`,
tools: [], // This would be populated by tool discovery
metadata: {
autoGenerated: true,
sourceApp: appId,
createdAt: new Date().toISOString(),
},
};
prefs.toolsets[toolsetId] = toolset;
// Set as default for the app
if (!prefs.appDefaults) {
prefs.appDefaults = {};
}
prefs.appDefaults[appId] = toolsetId;
}
// Save preferences
await this.fs.writeFile(prefsPath, JSON.stringify(prefs, null, 2), "utf-8");
}
/**
* Link HyperTool configuration to specified applications
*/
async linkApplications(appConfigs) {
const linked = [];
const failed = [];
// Get all enabled applications
const apps = await this.registry.getEnabledApplications();
// Handle legacy string array format for backward compatibility
let configs;
if (Array.isArray(appConfigs) && typeof appConfigs[0] === "string") {
// Legacy format - convert to new format
configs = appConfigs.map((appId) => ({
appId,
configType: "global",
}));
}
else {
configs = appConfigs;
}
for (const config of configs) {
const { appId, configType, perAppInit } = config;
if (!apps[appId]) {
logger.warn(`Application ${appId} not found`, { appId });
failed.push(appId);
continue;
}
try {
// Determine the config path based on user choice
let configPath;
if (configType === "global") {
configPath = join(this.basePath, "mcp.json");
}
else {
// Per-app config
const appConfigPath = join(this.basePath, "mcp", `${appId}.json`);
// Check if per-app config needs to be created
let configExists = false;
try {
await fs.access(appConfigPath);
configExists = true;
}
catch {
configExists = false;
}
if (!configExists && perAppInit) {
await this.initializePerAppConfig(appId, perAppInit);
}
configPath = appConfigPath;
}
await this.linkApplication(appId, apps[appId], configPath);
linked.push(appId);
}
catch (error) {
logger.warn(`Failed to link ${appId}`, { appId, error });
failed.push(appId);
}
}
return { linked, failed };
}
/**
* Initialize per-app configuration
*/
async initializePerAppConfig(appId, initMethod) {
const appConfigPath = join(this.basePath, "mcp", `${appId}.json`);
const mcpDir = join(this.basePath, "mcp");
// Ensure mcp directory exists
await fs.mkdir(mcpDir, { recursive: true });
let config = {
mcpServers: {},
_metadata: {
app: appId,
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
},
};
switch (initMethod) {
case "empty":
// Already initialized with empty mcpServers
break;
case "copy":
// Copy from global config
try {
const globalConfigPath = join(this.basePath, "mcp.json");
const globalContent = await fs.readFile(globalConfigPath, "utf-8");
const globalConfig = JSON.parse(globalContent);
config.mcpServers = globalConfig.mcpServers || {};
if (globalConfig._metadata) {
config._metadata = {
...config._metadata,
copiedFrom: "global",
sources: globalConfig._metadata.sources,
};
}
}
catch (error) {
logger.warn("Failed to copy global config, using empty config instead");
}
break;
case "import":
// Import from application's existing config
const apps = await this.registry.getEnabledApplications();
const app = apps[appId];
if (app) {
const appDef = app;
const platformConfig = this.registry.getPlatformConfig(appDef);
if (platformConfig) {
const configPath = this.registry.resolvePath(platformConfig.configPath);
try {
await fs.access(configPath);
const content = await fs.readFile(configPath, "utf-8");
const appConfig = JSON.parse(content);
// Import all servers except hypertool itself
if (appConfig.mcpServers) {
for (const [name, server] of Object.entries(appConfig.mcpServers)) {
if (name !== "hypertool" && name !== "toolprint-hypertool") {
config.mcpServers[name] = server;
}
}
}
config._metadata.importedFrom = configPath;
}
catch (error) {
logger.warn(`Failed to import from ${appId} config, using empty config instead`, { appId });
}
}
}
break;
}
// Write the per-app config
await fs.writeFile(appConfigPath, JSON.stringify(config, null, 2), "utf-8");
}
/**
* Link HyperTool configuration to a specific application
*/
async linkApplication(appId, app, hyperToolConfigPath) {
// Get platform-specific config
const platformConfig = this.registry.getPlatformConfig(app);
if (!platformConfig) {
throw new Error(`No platform configuration for ${appId}`);
}
// Check if we're in development mode
const isDevelopmentMode = await this.isInDevelopmentMode();
// Create HyperTool proxy configuration
let hyperToolProxy;
if (isDevelopmentMode) {
// Use local development build
const localBinPath = join(process.cwd(), "dist", "bin.js");
hyperToolProxy = {
hypertool: {
type: "stdio",
command: "node",
args: [
localBinPath,
"mcp",
"run",
"--mcp-config",
hyperToolConfigPath,
"--debug",
],
},
};
}
else {
// Use published NPM package
hyperToolProxy = {
hypertool: {
type: "stdio",
command: "npx",
args: [
"-y",
"@toolprint/hypertool-mcp@latest",
"--mcp-config",
hyperToolConfigPath,
],
},
};
}
// Resolve configuration path
let configPath = this.registry.resolvePath(platformConfig.configPath);
// Handle project-local configurations
if (app.detection.type === "project-local") {
configPath = join(this.getCurrentWorkingDirectory(), platformConfig.configPath.replace("./", ""));
}
// Read existing configuration if it exists
let existingConfig = null;
try {
const existingContent = await this.fs.readFile(configPath, "utf-8");
existingConfig = JSON.parse(existingContent);
}
catch (error) {
// File doesn't exist or is invalid, that's OK
}
// Transform to app-specific format
const transformerName = platformConfig.format === "custom" && platformConfig.transformer
? platformConfig.transformer
: platformConfig.format;
const transformer = TransformerRegistry.getTransformer(transformerName);
// For Claude Code, we need to merge servers to preserve existing ones
let configToTransform = { mcpServers: hyperToolProxy };
if (transformerName === "claude-code" && existingConfig) {
const standardExisting = transformer.toStandard(existingConfig);
configToTransform = {
mcpServers: {
...standardExisting.mcpServers,
...hyperToolProxy,
},
};
}
const appSpecificConfig = transformer.fromStandard(configToTransform, existingConfig);
// Ensure directory exists
const dir = join(configPath, "..");
await this.fs.mkdir(dir, { recursive: true });
// Write configuration
await this.fs.writeFile(configPath, JSON.stringify(appSpecificConfig, null, 2), "utf-8");
}
/**
* Create a backup of all current configurations
*/
async createBackup() {
return this.backupManager.createBackup();
}
/**
* List available backups
*/
async listBackups() {
return this.backupManager.listBackups();
}
/**
* Get a specific backup by ID
*/
async getBackup(backupId) {
return this.backupManager.getBackup(backupId);
}
/**
* Restore from a specific backup
*/
async restoreBackup(backupId, options) {
return this.backupManager.restoreBackup(backupId, options);
}
/**
* Delete a backup by ID
*/
async deleteBackup(backupId) {
return this.backupManager.deleteBackup(backupId);
}
/**
* Unlink HyperTool from specified applications
*/
async unlinkApplications(appIds, options) {
const unlinked = [];
const failed = [];
const restoredWithHypertool = [];
if (options?.restore && options?.backupId) {
// First restore from backup
await this.restoreBackup(options.backupId, { applications: appIds });
// Check each restored app for hypertool entries
for (const appId of appIds) {
try {
const hasHypertool = await this.checkAndRemoveHypertoolFromApp(appId);
if (hasHypertool) {
restoredWithHypertool.push(appId);
}
unlinked.push(appId);
}
catch (error) {
logger.warn(`Failed to process ${appId}`, { appId, error });
failed.push(appId);
}
}
}
else {
// Just remove hypertool without restoration
for (const appId of appIds) {
try {
await this.removeHypertoolFromApp(appId);
unlinked.push(appId);
}
catch (error) {
logger.warn(`Failed to unlink ${appId}`, { appId, error });
failed.push(appId);
}
}
}
return { unlinked, failed, restoredWithHypertool };
}
/**
* Check if app config has hypertool and remove it while preserving other servers
*/
async checkAndRemoveHypertoolFromApp(appId) {
const app = await this.registry.getApplication(appId);
if (!app) {
throw new Error(`Application ${appId} not found`);
}
const platformConfig = this.registry.getPlatformConfig(app);
if (!platformConfig) {
return false;
}
const configPath = this.registry.resolvePath(platformConfig.configPath);
try {
const content = await this.fs.readFile(configPath, "utf-8");
const config = JSON.parse(content);
// Check if config has hypertool
const transformerName = platformConfig.format === "custom" && platformConfig.transformer
? platformConfig.transformer
: platformConfig.format;
const transformer = TransformerRegistry.getTransformer(transformerName);
const standardConfig = transformer.toStandard(config);
if (standardConfig.mcpServers && standardConfig.mcpServers["hypertool"]) {
// Remove hypertool entry
delete standardConfig.mcpServers["hypertool"];
// Transform back to app format
const updatedConfig = transformer.fromStandard(standardConfig, config);
await this.fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2), "utf-8");
return true;
}
}
catch (error) {
// Config might not exist or be invalid
logger.warn(`Could not check config for ${appId}`, { appId, error });
}
return false;
}
/**
* Remove HyperTool from a specific application without restoration
*/
async removeHypertoolFromApp(appId) {
const app = await this.registry.getApplication(appId);
if (!app) {
throw new Error(`Application ${appId} not found`);
}
const platformConfig = this.registry.getPlatformConfig(app);
if (!platformConfig) {
return;
}
const configPath = this.registry.resolvePath(platformConfig.configPath);
try {
const content = await this.fs.readFile(configPath, "utf-8");
const config = JSON.parse(content);
// Get transformer with correct name resolution
const transformerName = platformConfig.format === "custom" && platformConfig.transformer
? platformConfig.transformer
: platformConfig.format;
const transformer = TransformerRegistry.getTransformer(transformerName);
const standardConfig = transformer.toStandard(config);
// Remove hypertool entry
delete standardConfig.mcpServers["hypertool"];
// Transform back to app format
const updatedConfig = transformer.fromStandard(standardConfig, config);
await this.fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2), "utf-8");
}
catch (error) {
// If config doesn't exist or is invalid, create empty one
const transformerName = platformConfig.format === "custom" && platformConfig.transformer
? platformConfig.transformer
: platformConfig.format;
const transformer = TransformerRegistry.getTransformer(transformerName);
const emptyConfig = transformer.fromStandard({ mcpServers: {} });
await this.fs.writeFile(configPath, JSON.stringify(emptyConfig, null, 2), "utf-8");
}
}
/**
* Check if we're running in development mode
*/
async isInDevelopmentMode() {
try {
// Check if we're in the hypertool-mcp project directory
const packageJsonPath = join(process.cwd(), "package.json");
const content = await this.fs.readFile(packageJsonPath, "utf-8");
const packageJson = JSON.parse(content);
// Check if this is the hypertool-mcp package
if (packageJson.name === "@toolprint/hypertool-mcp") {
// Also check if dist/bin.js exists (built)
const binPath = join(process.cwd(), "dist", "bin.js");
try {
await this.fs.access(binPath);
return true;
}
catch {
// Not built yet
return false;
}
}
}
catch {
// Not in a package directory or can't read package.json
}
return false;
}
}
// Export types and classes
export * from "./types/index.js";
export { AppRegistry } from "./apps/registry.js";
export { BackupManager } from "./backup/manager.js";
export { TransformerRegistry, StandardTransformer, } from "./transformers/base.js";
//# sourceMappingURL=index.js.map