Files
drawio-mcp-server/src/utils/vscode-integration.ts
Alejandro Lembke Barrientos bf088da9d5
Some checks failed
CI Pipeline / Test and Build (20.x) (push) Failing after 3m22s
CI Pipeline / Code Quality Check (push) Failing after 11m52s
CI Pipeline / Security Audit (push) Failing after 11m53s
CI Pipeline / Test and Build (18.x) (push) Failing after 12m31s
CI Pipeline / Build Release Artifacts (push) Has been cancelled
CI Pipeline / Notification (push) Has been cancelled
Creating first version of MCP server.
2025-07-22 07:11:59 +00:00

367 lines
12 KiB
TypeScript

import { exec } from 'child_process';
import { promisify } from 'util';
import * as path from 'path';
import * as fs from 'fs-extra';
import { DiagramFormat } from '../types/diagram-types.js';
const execAsync = promisify(exec);
// Functional types
type VSCodeConfig = Readonly<{
workspaceRoot: string;
extensionId: string;
}>;
type ExtensionStatus = Readonly<{
isInstalled: boolean;
isVSCodeAvailable: boolean;
version?: string;
}>;
type SetupResult = Readonly<{
success: boolean;
message: string;
}>;
type FileExtensionMap = Readonly<Record<DiagramFormat, string>>;
type PlatformCommandMap = Readonly<Record<string, string>>;
// Pure function to create VSCode configuration
export const createVSCodeConfig = (workspaceRoot?: string): VSCodeConfig => ({
workspaceRoot: workspaceRoot || process.cwd(),
extensionId: 'hediet.vscode-drawio'
});
// Pure function to get file extension mapping
const getFileExtensionMap = (): FileExtensionMap => ({
[DiagramFormat.DRAWIO]: 'drawio',
[DiagramFormat.DRAWIO_SVG]: 'drawio.svg',
[DiagramFormat.DRAWIO_PNG]: 'drawio.png',
[DiagramFormat.DIO]: 'dio',
[DiagramFormat.XML]: 'xml'
});
// Pure function to get file extension for diagram format
export const getFileExtension = (format: DiagramFormat): string => {
const extensionMap = getFileExtensionMap();
return extensionMap[format] || 'drawio';
};
// Pure function to get platform-specific file explorer commands
const getPlatformCommandMap = (targetPath: string): PlatformCommandMap => ({
'win32': `explorer "${targetPath}"`,
'darwin': `open "${targetPath}"`,
'linux': `xdg-open "${targetPath}"`
});
// Pure function to get platform-specific file explorer command
const getFileExplorerCommand = (targetPath: string): string => {
const platform = process.platform;
const commandMap = getPlatformCommandMap(targetPath);
const command = commandMap[platform];
if (!command) {
throw new Error(`Unsupported platform: ${platform}`);
}
return command;
};
// Higher-order function for command execution with error handling
const withCommandErrorHandling = <T extends any[], R>(
operation: (...args: T) => Promise<R>
) => async (...args: T): Promise<R> => {
try {
return await operation(...args);
} catch (error) {
throw new Error(`Command execution failed: ${error}`);
}
};
// Pure function to execute VSCode command
export const executeVSCodeCommand = withCommandErrorHandling(async (command: string, args?: readonly string[]): Promise<string> => {
const fullCommand = args ? `${command} ${args.join(' ')}` : command;
const { stdout } = await execAsync(fullCommand);
return stdout;
});
// Curried function to open diagram file in VSCode
export const openDiagramInVSCode = (config: VSCodeConfig) => async (filePath: string): Promise<void> => {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.resolve(config.workspaceRoot, filePath);
await executeVSCodeCommand('code', [absolutePath]);
};
// Curried function to check if draw.io extension is installed
export const isDrawioExtensionInstalled = (config: VSCodeConfig) => async (): Promise<boolean> => {
try {
const stdout = await executeVSCodeCommand('code', ['--list-extensions']);
return stdout.includes(config.extensionId);
} catch (error) {
console.warn('Could not check VSCode extensions:', error);
return false;
}
};
// Curried function to install draw.io extension
export const installDrawioExtension = (config: VSCodeConfig) => async (): Promise<void> => {
await executeVSCodeCommand('code', ['--install-extension', config.extensionId]);
};
// Curried function to open VSCode workspace
export const openWorkspace = (config: VSCodeConfig) => async (): Promise<void> => {
await executeVSCodeCommand('code', [config.workspaceRoot]);
};
// Pure function to check if VSCode is available
export const isVSCodeAvailable = async (): Promise<boolean> => {
try {
await executeVSCodeCommand('code', ['--version']);
return true;
} catch (error) {
return false;
}
};
// Pure function to get VSCode version
export const getVSCodeVersion = async (): Promise<string> => {
const stdout = await executeVSCodeCommand('code', ['--version']);
return stdout.split('\n')[0];
};
// Curried function to create and open new diagram in VSCode
export const createAndOpenDiagram = (config: VSCodeConfig) =>
(fileName: string) =>
(content: string) =>
(format: DiagramFormat) =>
async (outputDir?: string): Promise<string> => {
const dir = outputDir || config.workspaceRoot;
const extension = getFileExtension(format);
const fullFileName = fileName.endsWith(extension) ? fileName : `${fileName}.${extension}`;
const filePath = path.join(dir, fullFileName);
// Write file
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content, 'utf8');
// Open in VSCode
await openDiagramInVSCode(config)(filePath);
return filePath;
};
// Curried function to get workspace folders
export const getWorkspaceFolders = (config: VSCodeConfig) => async (): Promise<readonly string[]> => {
try {
// This would require VSCode API integration
// For now, return current workspace
return [config.workspaceRoot];
} catch (error) {
console.warn('Could not get workspace folders:', error);
return [config.workspaceRoot];
}
};
// Pure function to show notification (placeholder for VSCode API)
export const showNotification = async (
message: string,
type: 'info' | 'warning' | 'error' = 'info'
): Promise<void> => {
try {
// This would require VSCode extension API
// For now, just log to console
console.log(`[${type.toUpperCase()}] ${message}`);
} catch (error) {
console.warn('Could not show notification:', error);
}
};
// Curried function to open file explorer at specific path
export const openFileExplorer = (config: VSCodeConfig) => async (dirPath?: string): Promise<void> => {
const targetPath = dirPath || config.workspaceRoot;
const command = getFileExplorerCommand(targetPath);
await executeVSCodeCommand(command);
};
// Pure function to refresh workspace (placeholder for VSCode API)
export const refreshWorkspace = async (): Promise<void> => {
try {
// This would require VSCode API integration
// For now, just log
console.log('Workspace refresh requested');
} catch (error) {
console.warn('Could not refresh workspace:', error);
}
};
// Curried function to check VSCode extension status
export const checkExtensionStatus = (config: VSCodeConfig) => async (): Promise<ExtensionStatus> => {
const isVSCodeAvail = await isVSCodeAvailable();
if (!isVSCodeAvail) {
return {
isInstalled: false,
isVSCodeAvailable: false
};
}
const isInstalled = await isDrawioExtensionInstalled(config)();
const version = isVSCodeAvail ? await getVSCodeVersion() : undefined;
return {
isInstalled,
isVSCodeAvailable: isVSCodeAvail,
version
};
};
// Curried function to setup VSCode environment for draw.io
export const setupVSCodeEnvironment = (config: VSCodeConfig) => async (): Promise<SetupResult> => {
try {
const status = await checkExtensionStatus(config)();
if (!status.isVSCodeAvailable) {
return {
success: false,
message: 'VSCode is not available. Please install VSCode first.'
};
}
if (!status.isInstalled) {
await installDrawioExtension(config)();
return {
success: true,
message: 'Draw.io extension installed successfully.'
};
}
return {
success: true,
message: 'VSCode environment is ready for draw.io.'
};
} catch (error) {
return {
success: false,
message: `Failed to setup VSCode environment: ${error}`
};
}
};
// Curried function to open multiple diagrams in VSCode
export const openMultipleDiagrams = (config: VSCodeConfig) => async (filePaths: readonly string[]): Promise<readonly string[]> => {
const results: string[] = [];
const openDiagram = openDiagramInVSCode(config);
for (const filePath of filePaths) {
try {
await openDiagram(filePath);
results.push(`Successfully opened: ${filePath}`);
} catch (error) {
results.push(`Failed to open ${filePath}: ${error}`);
}
}
return results;
};
// Pure function to create VSCode workspace configuration for draw.io
export const createWorkspaceConfig = (config: VSCodeConfig): Readonly<Record<string, any>> => ({
'hediet.vscode-drawio': {
'local-storage': path.join(config.workspaceRoot, '.vscode', 'drawio-storage'),
'theme': 'automatic',
'online-url': 'https://embed.diagrams.net/',
'offline': false
}
});
// Curried function to save VSCode workspace settings
export const saveWorkspaceSettings = (config: VSCodeConfig) => async (settings: Readonly<Record<string, any>>): Promise<void> => {
const settingsPath = path.join(config.workspaceRoot, '.vscode', 'settings.json');
try {
await fs.ensureDir(path.dirname(settingsPath));
let existingSettings = {};
if (await fs.pathExists(settingsPath)) {
const content = await fs.readFile(settingsPath, 'utf8');
existingSettings = JSON.parse(content);
}
const mergedSettings = { ...existingSettings, ...settings };
await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf8');
} catch (error) {
throw new Error(`Failed to save workspace settings: ${error}`);
}
};
// Utility functions for functional composition
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
fns.reduceRight((acc, fn) => fn(acc), value);
// Higher-order function for operations with retry logic
export const withRetry = <T extends any[], R>(
operation: (...args: T) => Promise<R>,
maxRetries: number = 3,
delay: number = 1000
) => async (...args: T): Promise<R> => {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation(...args);
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) break;
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
throw lastError!;
};
// Higher-order function for operations with timeout
export const withTimeout = <T extends any[], R>(
operation: (...args: T) => Promise<R>,
timeoutMs: number = 30000
) => async (...args: T): Promise<R> => {
return Promise.race([
operation(...args),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
)
]);
};
// Functional VSCode operations with retry and timeout
export const openDiagramInVSCodeWithRetry = (config: VSCodeConfig) =>
withRetry(openDiagramInVSCode(config));
export const setupVSCodeEnvironmentWithTimeout = (config: VSCodeConfig) =>
withTimeout(setupVSCodeEnvironment(config));
// Pure function to validate VSCode configuration
export const validateVSCodeConfig = (config: VSCodeConfig): boolean => {
return typeof config.workspaceRoot === 'string' &&
config.workspaceRoot.length > 0 &&
typeof config.extensionId === 'string' &&
config.extensionId.length > 0;
};
// Pure function to create validated VSCode configuration
export const createValidatedVSCodeConfig = (workspaceRoot?: string): VSCodeConfig => {
const config = createVSCodeConfig(workspaceRoot);
if (!validateVSCodeConfig(config)) {
throw new Error('Invalid VSCode configuration');
}
return config;
};