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>; type PlatformCommandMap = Readonly>; // 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 = ( operation: (...args: T) => Promise ) => async (...args: T): Promise => { 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 => { 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 => { 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 => { 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 => { await executeVSCodeCommand('code', ['--install-extension', config.extensionId]); }; // Curried function to open VSCode workspace export const openWorkspace = (config: VSCodeConfig) => async (): Promise => { await executeVSCodeCommand('code', [config.workspaceRoot]); }; // Pure function to check if VSCode is available export const isVSCodeAvailable = async (): Promise => { try { await executeVSCodeCommand('code', ['--version']); return true; } catch (error) { return false; } }; // Pure function to get VSCode version export const getVSCodeVersion = async (): Promise => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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> => ({ '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>): Promise => { 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 = (...fns: Array<(arg: T) => T>) => (value: T): T => fns.reduce((acc, fn) => fn(acc), value); export const compose = (...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 = ( operation: (...args: T) => Promise, maxRetries: number = 3, delay: number = 1000 ) => async (...args: T): Promise => { 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 = ( operation: (...args: T) => Promise, timeoutMs: number = 30000 ) => async (...args: T): Promise => { return Promise.race([ operation(...args), new Promise((_, 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; };