171 lines
6.6 KiB
Python
171 lines
6.6 KiB
Python
Node.js Runtime Adapter for NebulaShell
|
|
=====================================
|
|
This plugin acts as a pure service provider (Adapter). It does NOT contain its own business logic or pkg.
|
|
Instead, it exposes standard interfaces for OTHER plugins to execute Node.js/npm commands
|
|
within THEIR own contexts (specifically their ./pkg directories).
|
|
|
|
Usage by other plugins:
|
|
1. Get this adapter from the shared service registry.
|
|
2. Call adapter.execute_in_context(plugin_root="./path/to/other-plugin", command="npm start")
|
|
3. The adapter will automatically switch CWD to "./path/to/other-plugin/pkg" and run the command.
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import subprocess
|
|
import shutil
|
|
from typing import Dict, Any, List, Optional
|
|
|
|
class NodeJSAdapter:
|
|
Pure Node.js Runtime Adapter.
|
|
Provides execution context switching for other plugins.
|
|
|
|
def __init__(self):
|
|
self.name = "nodejs-adapter"
|
|
self.version = "1.0.0"
|
|
self.description = "Stateless Node.js runtime adapter for cross-plugin execution"
|
|
self.node_path = None
|
|
self.npm_path = None
|
|
self._detect_runtime()
|
|
|
|
def _detect_runtime(self):
|
|
versions = self.check_versions()
|
|
return {
|
|
'available': bool(self.node_path),
|
|
'npm_available': bool(self.npm_path),
|
|
'versions': versions,
|
|
'features': ['run_script', 'install_deps', 'exec_command', 'context_switching']
|
|
}
|
|
|
|
def check_versions(self) -> Dict[str, str]:
|
|
CORE METHOD: Execute a command within the context of another plugin.
|
|
|
|
Args:
|
|
plugin_root: The root directory of the CALLING plugin (e.g., /workspace/oss/plugins/my-web-app)
|
|
command_args: The command arguments (e.g., ['start'] or ['install', 'express'])
|
|
is_npm: If True, uses 'npm'. If False, uses 'node'.
|
|
|
|
Behavior:
|
|
1. Targets the './pkg' subdirectory inside plugin_root.
|
|
2. Sets cwd to that pkg directory.
|
|
3. Executes the command.
|
|
4. Ensures dependencies install into that specific pkg folder.
|
|
if not self.node_path:
|
|
return {'success': False, 'error': 'Node.js runtime not found'}
|
|
if is_npm and not self.npm_path:
|
|
return {'success': False, 'error': 'npm not found'}
|
|
|
|
work_dir = os.path.join(plugin_root, 'pkg')
|
|
|
|
if not os.path.exists(work_dir):
|
|
return {'success': False, 'error': f'Target pkg directory not found: {work_dir}'}
|
|
|
|
try:
|
|
executable = self.npm_path if is_npm else self.node_path
|
|
cmd = [executable] + command_args
|
|
|
|
env = os.environ.copy()
|
|
env['npm_config_prefix'] = work_dir
|
|
env['NODE_PATH'] = os.path.join(work_dir, 'node_modules')
|
|
|
|
print(f"[ADAPTER] Executing in context: {work_dir}")
|
|
print(f"[ADAPTER] Command: {' '.join(cmd)}")
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
cwd=work_dir,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300 )
|
|
|
|
return {
|
|
'success': result.returncode == 0,
|
|
'stdout': result.stdout,
|
|
'stderr': result.stderr,
|
|
'returncode': result.returncode,
|
|
'cwd': work_dir
|
|
}
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return {'success': False, 'error': 'Command execution timeout'}
|
|
except Exception as e:
|
|
return {'success': False, 'error': f'{type(e).__name__} - {e}'}
|
|
|
|
def install_dependencies(self, plugin_root: str, packages: List[str] = None) -> Dict[str, Any]:
|
|
Helper: Install dependencies for a specific plugin.
|
|
If packages is None, runs 'npm install' (installs from package.json).
|
|
If packages is provided, runs 'npm install <pkg1> <pkg2>...'.
|
|
args = ['install']
|
|
if packages:
|
|
args.extend(packages)
|
|
return self.execute_in_context(plugin_root, args, is_npm=True)
|
|
|
|
def run_script(self, plugin_root: str, script_name: str, extra_args: List[str] = None) -> Dict[str, Any]:
|
|
Helper: Run an npm script (e.g., 'start', 'build') for a specific plugin.
|
|
args = ['run', script_name]
|
|
if extra_args:
|
|
args.append('--')
|
|
args.extend(extra_args)
|
|
return self.execute_in_context(plugin_root, args, is_npm=True)
|
|
|
|
def run_file(self, plugin_root: str, file_path: str, args: List[str] = None) -> Dict[str, Any]:
|
|
Helper: Run a specific JS file within a plugin's pkg directory.
|
|
file_path should be relative to the pkg dir (e.g., 'index.js').
|
|
cmd_args = [file_path]
|
|
if args:
|
|
cmd_args.extend(args)
|
|
return self.execute_in_context(plugin_root, cmd_args, is_npm=False)
|
|
|
|
def init_project(self, plugin_root: str, name: str = "plugin-project") -> Dict[str, Any]:
|
|
Helper: Initialize a package.json in the plugin's pkg directory.
|
|
res = self.execute_in_context(plugin_root, ['init', '-y'], is_npm=True)
|
|
if not res['success']:
|
|
return res
|
|
|
|
pkg_json_path = os.path.join(plugin_root, 'pkg', 'package.json')
|
|
if os.path.exists(pkg_json_path):
|
|
try:
|
|
with open(pkg_json_path, 'r+') as f:
|
|
data = json.load(f)
|
|
data['name'] = name
|
|
data['private'] = True
|
|
f.seek(0)
|
|
json.dump(data, f, indent=2)
|
|
f.truncate()
|
|
return {'success': True, 'message': f'Initialized project {name}'}
|
|
except Exception as e:
|
|
return {'success': False, 'error': f'Failed to update package.json: {e}'}
|
|
return res
|
|
|
|
|
|
|
|
def init(context):
|
|
Initialize the adapter and register it as a shared service.
|
|
This plugin does NOT start any server or run any code itself.
|
|
It just registers the tool for others to use.
|
|
adapter = NodeJSAdapter()
|
|
versions = adapter.check_versions()
|
|
|
|
print(f"[INFO] Node.js Adapter Service Registered")
|
|
if versions.get('node'):
|
|
print(f"[INFO] Runtime: Node {versions['node']}")
|
|
if versions.get('npm'):
|
|
print(f"[INFO] Package Manager: npm {versions['npm']}")
|
|
|
|
if 'services' not in context:
|
|
context['services'] = {}
|
|
context['services']['nodejs-adapter'] = adapter
|
|
|
|
return {
|
|
'status': 'ready',
|
|
'service_name': 'nodejs-adapter',
|
|
'runtime_available': bool(versions.get('node')),
|
|
'versions': versions
|
|
}
|
|
|
|
def start(context):
|
|
return {'status': 'inactive'}
|
|
|
|
def get_info(context):
|