Files
NebulaShell/store/@{FutureOSS}/nodejs-adapter/main.py
qwen.ai[bot] 64c8713945 update branch
2026-04-25 21:12:20 +08:00

464 lines
16 KiB
Python

"""
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!")