""" Node.js Adapter Plugin for FutureOSS This plugin provides Node.js and npm capabilities to other plugins. Other plugins can specify this adapter in their manifest to run Node.js projects located in their /pkg directory with isolated dependencies. Features: - Install npm packages to plugin-specific directories - Execute Node.js scripts and npm commands - Check Node.js and npm versions - List installed packages - Dependency isolation per plugin """ import subprocess import json import os import shutil from pathlib import Path from typing import Dict, List, Optional, Any class NodeJSAdapter: """Node.js runtime adapter for managing Node.js projects and dependencies.""" def __init__(self, config: Optional[Dict[str, Any]] = None): """Initialize the Node.js adapter with configuration.""" self.config = config or {} self.node_path = self.config.get('node_path', '/usr/bin/node') self.npm_path = self.config.get('npm_path', '/usr/bin/npm') self.default_registry = self.config.get('default_registry', 'https://registry.npmjs.org') self.cache_dir = Path(self.config.get('cache_dir', '~/.futureoss/nodejs-cache')).expanduser() # Ensure cache directory exists self.cache_dir.mkdir(parents=True, exist_ok=True) self._validate_runtime() def _validate_runtime(self) -> bool: """Validate that Node.js and npm are available.""" try: node_result = subprocess.run( [self.node_path, '--version'], capture_output=True, text=True, timeout=10 ) if node_result.returncode != 0: raise RuntimeError(f"Node.js not found: {node_result.stderr}") npm_result = subprocess.run( [self.npm_path, '--version'], capture_output=True, text=True, timeout=10 ) if npm_result.returncode != 0: raise RuntimeError(f"npm not found: {npm_result.stderr}") return True except FileNotFoundError as e: raise RuntimeError(f"Node.js or npm not found in system: {str(e)}") except subprocess.TimeoutExpired as e: raise RuntimeError(f"Timeout while checking Node.js/npm versions: {str(e)}") def check_versions(self) -> Dict[str, str]: """Check Node.js and npm versions.""" try: node_result = subprocess.run( [self.node_path, '--version'], capture_output=True, text=True, timeout=10 ) npm_result = subprocess.run( [self.npm_path, '--version'], capture_output=True, text=True, timeout=10 ) return { 'node': node_result.stdout.strip(), 'npm': npm_result.stdout.strip(), 'status': 'ok' } except subprocess.TimeoutExpired as e: return { 'node': 'unknown', 'npm': 'unknown', 'status': 'error', 'error': f'Timeout: {str(e)}' } except Exception as e: return { 'node': 'unknown', 'npm': 'unknown', 'status': 'error', 'error': str(e) } def install(self, plugin_id: str, packages: List[str], pkg_dir: Optional[Path] = None, is_dev: bool = False) -> Dict[str, Any]: """ Install npm packages to a plugin-specific directory. Args: plugin_id: Unique identifier for the plugin packages: List of npm packages to install (e.g., ['express', 'lodash@4.17.21']) pkg_dir: Optional custom package directory (defaults to plugin storage dir) is_dev: Whether to install as dev dependencies Returns: Dict with installation result """ try: # Determine target directory if pkg_dir is None: # Default to plugin storage directory target_dir = self.cache_dir / plugin_id else: target_dir = Path(pkg_dir) target_dir.mkdir(parents=True, exist_ok=True) # Build npm install command cmd = [self.npm_path, 'install'] if is_dev: cmd.append('--save-dev') else: cmd.append('--save') # Set registry if specified if self.default_registry: cmd.extend(['--registry', self.default_registry]) # Add packages cmd.extend(packages) # Execute installation result = subprocess.run( cmd, cwd=str(target_dir), capture_output=True, text=True, timeout=300 # 5 minutes timeout for installation ) if result.returncode == 0: return { 'status': 'success', 'plugin_id': plugin_id, 'packages': packages, 'target_dir': str(target_dir), 'output': result.stdout, 'is_dev': is_dev } else: return { 'status': 'error', 'plugin_id': plugin_id, 'packages': packages, 'target_dir': str(target_dir), 'error': result.stderr, 'output': result.stdout } except subprocess.TimeoutExpired as e: return { 'status': 'error', 'plugin_id': plugin_id, 'packages': packages, 'error': f'Installation timeout: {str(e)}' } except Exception as e: return { 'status': 'error', 'plugin_id': plugin_id, 'packages': packages, 'error': str(e) } def run(self, plugin_id: str, script: str, pkg_dir: Optional[Path] = None, args: Optional[List[str]] = None, env: Optional[Dict[str, str]] = None) -> Dict[str, Any]: """ Execute a Node.js script or npm command. Args: plugin_id: Unique identifier for the plugin script: Script to run (e.g., 'start', 'build', or path to .js file) pkg_dir: Optional custom package directory args: Additional arguments to pass env: Custom environment variables Returns: Dict with execution result """ try: # Determine working directory if pkg_dir is None: work_dir = self.cache_dir / plugin_id else: work_dir = Path(pkg_dir) if not work_dir.exists(): return { 'status': 'error', 'error': f'Plugin directory not found: {work_dir}' } # Determine if it's an npm script or direct node execution if script.endswith('.js') or script.endswith('.ts'): # Direct Node.js execution cmd = [self.node_path, script] if args: cmd.extend(args) else: # NPM script execution cmd = [self.npm_path, 'run', script] if args: cmd.append('--') cmd.extend(args) # Prepare environment run_env = os.environ.copy() if env: run_env.update(env) # Execute result = subprocess.run( cmd, cwd=str(work_dir), capture_output=True, text=True, timeout=300, env=run_env ) return { 'status': 'success' if result.returncode == 0 else 'error', 'plugin_id': plugin_id, 'script': script, 'exit_code': result.returncode, 'stdout': result.stdout, 'stderr': result.stderr, 'work_dir': str(work_dir) } except subprocess.TimeoutExpired as e: return { 'status': 'error', 'plugin_id': plugin_id, 'script': script, 'error': f'Execution timeout: {str(e)}' } except Exception as e: return { 'status': 'error', 'plugin_id': plugin_id, 'script': script, 'error': str(e) } def list_packages(self, plugin_id: str, pkg_dir: Optional[Path] = None) -> Dict[str, Any]: """ List installed packages in a plugin directory. Args: plugin_id: Unique identifier for the plugin pkg_dir: Optional custom package directory Returns: Dict with list of installed packages """ try: # Determine working directory if pkg_dir is None: work_dir = self.cache_dir / plugin_id else: work_dir = Path(pkg_dir) if not work_dir.exists(): return { 'status': 'error', 'error': f'Plugin directory not found: {work_dir}' } # Run npm list result = subprocess.run( [self.npm_path, 'list', '--json', '--depth=0'], cwd=str(work_dir), capture_output=True, text=True, timeout=60 ) if result.returncode == 0: try: packages = json.loads(result.stdout) return { 'status': 'success', 'plugin_id': plugin_id, 'packages': packages.get('dependencies', {}), 'work_dir': str(work_dir) } except json.JSONDecodeError as e: return { 'status': 'error', 'plugin_id': plugin_id, 'error': f'Failed to parse npm list output: {str(e)}', 'raw_output': result.stdout } else: return { 'status': 'error', 'plugin_id': plugin_id, 'error': result.stderr, 'work_dir': str(work_dir) } except subprocess.TimeoutExpired as e: return { 'status': 'error', 'plugin_id': plugin_id, 'error': f'Timeout listing packages: {str(e)}' } except Exception as e: return { 'status': 'error', 'plugin_id': plugin_id, 'error': str(e) } def init_project(self, plugin_id: str, pkg_dir: Optional[Path] = None, package_name: Optional[str] = None, version: str = "1.0.0") -> Dict[str, Any]: """ Initialize a new Node.js project in a plugin directory. Args: plugin_id: Unique identifier for the plugin pkg_dir: Optional custom package directory package_name: Optional package name (defaults to plugin_id) version: Package version Returns: Dict with initialization result """ try: # Determine working directory if pkg_dir is None: work_dir = self.cache_dir / plugin_id else: work_dir = Path(pkg_dir) work_dir.mkdir(parents=True, exist_ok=True) # Create package.json package_json = { 'name': package_name or plugin_id.replace('/', '-'), 'version': version, 'description': f'Node.js project for plugin {plugin_id}', 'main': 'index.js', 'scripts': { 'start': 'node index.js', 'test': 'echo "Error: no test specified" && exit 1' }, 'keywords': [], 'author': '', 'license': 'ISC' } package_json_path = work_dir / 'package.json' with open(package_json_path, 'w', encoding='utf-8') as f: json.dump(package_json, f, indent=2) # Create basic index.js index_js_path = work_dir / 'index.js' with open(index_js_path, 'w', encoding='utf-8') as f: f.write('// Node.js project for FutureOSS plugin\n') f.write(f'// Plugin ID: {plugin_id}\n') f.write('console.log("Hello from FutureOSS Node.js plugin!");\n') return { 'status': 'success', 'plugin_id': plugin_id, 'work_dir': str(work_dir), 'package_json': str(package_json_path), 'index_js': str(index_js_path) } except Exception as e: return { 'status': 'error', 'plugin_id': plugin_id, 'error': str(e) } # Plugin lifecycle hooks def init(config: Dict[str, Any]) -> NodeJSAdapter: """Initialize the Node.js adapter plugin.""" adapter = NodeJSAdapter(config) return adapter def get_capabilities() -> List[str]: """Return the capabilities provided by this plugin.""" return [ 'nodejs_runtime', 'npm_package_manager', 'dependency_isolation', 'script_execution', 'project_initialization' ] def execute_command(adapter: NodeJSAdapter, command: str, **kwargs) -> Dict[str, Any]: """ Execute a command through the adapter. Available commands: - check_versions: Check Node.js and npm versions - install: Install npm packages - run: Execute Node.js scripts or npm commands - list_packages: List installed packages - init_project: Initialize a new Node.js project """ if command == 'check_versions': return adapter.check_versions() elif command == 'install': return adapter.install(**kwargs) elif command == 'run': return adapter.run(**kwargs) elif command == 'list_packages': return adapter.list_packages(**kwargs) elif command == 'init_project': return adapter.init_project(**kwargs) else: return { 'status': 'error', 'error': f'Unknown command: {command}' } if __name__ == '__main__': # Test the adapter print("Node.js Adapter Plugin for FutureOSS") print("=" * 50) adapter = init({}) # Check versions versions = adapter.check_versions() print(f"\nNode.js Version: {versions.get('node', 'N/A')}") print(f"npm Version: {versions.get('npm', 'N/A')}") # Get capabilities caps = get_capabilities() print(f"\nCapabilities: {', '.join(caps)}") print("\n✓ Node.js Adapter initialized successfully!")