import express from 'express'; import path from 'path'; import mime from 'mime-types'; import fs from 'fs'; import { scanPlugins, getPluginsConfig, savePluginsConfig, resolvePluginAssetPath, installPluginFromGit, updatePluginFromGit, uninstallPlugin, } from '../utils/plugin-loader.js'; const router = express.Router(); // GET / — List all installed plugins router.get('/', (req, res) => { try { const plugins = scanPlugins(); res.json({ plugins }); } catch (err) { res.status(500).json({ error: 'Failed to scan plugins', details: err.message }); } }); // GET /:name/manifest — Get single plugin manifest router.get('/:name/manifest', (req, res) => { try { const plugins = scanPlugins(); const plugin = plugins.find(p => p.name === req.params.name); if (!plugin) { return res.status(404).json({ error: 'Plugin not found' }); } res.json(plugin); } catch (err) { res.status(500).json({ error: 'Failed to read plugin manifest', details: err.message }); } }); // GET /:name/assets/* — Serve plugin static files router.get('/:name/assets/*', (req, res) => { const pluginName = req.params.name; const assetPath = req.params[0]; if (!assetPath) { return res.status(400).json({ error: 'No asset path specified' }); } const resolvedPath = resolvePluginAssetPath(pluginName, assetPath); if (!resolvedPath) { return res.status(404).json({ error: 'Asset not found' }); } const contentType = mime.lookup(resolvedPath) || 'application/octet-stream'; res.setHeader('Content-Type', contentType); fs.createReadStream(resolvedPath).pipe(res); }); // PUT /:name/enable — Toggle plugin enabled/disabled router.put('/:name/enable', (req, res) => { try { const { enabled } = req.body; if (typeof enabled !== 'boolean') { return res.status(400).json({ error: '"enabled" must be a boolean' }); } const plugins = scanPlugins(); const plugin = plugins.find(p => p.name === req.params.name); if (!plugin) { return res.status(404).json({ error: 'Plugin not found' }); } const config = getPluginsConfig(); config[req.params.name] = { ...config[req.params.name], enabled }; savePluginsConfig(config); res.json({ success: true, name: req.params.name, enabled }); } catch (err) { res.status(500).json({ error: 'Failed to update plugin', details: err.message }); } }); // POST /install — Install plugin from git URL router.post('/install', async (req, res) => { try { const { url } = req.body; if (!url || typeof url !== 'string') { return res.status(400).json({ error: '"url" is required and must be a string' }); } // Basic URL validation if (!url.startsWith('https://') && !url.startsWith('git@')) { return res.status(400).json({ error: 'URL must start with https:// or git@' }); } const manifest = await installPluginFromGit(url); res.json({ success: true, plugin: manifest }); } catch (err) { res.status(400).json({ error: 'Failed to install plugin', details: err.message }); } }); // POST /:name/update — Pull latest from git router.post('/:name/update', async (req, res) => { try { const pluginName = req.params.name; if (!/^[a-zA-Z0-9_-]+$/.test(pluginName)) { return res.status(400).json({ error: 'Invalid plugin name' }); } const manifest = await updatePluginFromGit(pluginName); res.json({ success: true, plugin: manifest }); } catch (err) { res.status(400).json({ error: 'Failed to update plugin', details: err.message }); } }); // DELETE /:name — Uninstall plugin router.delete('/:name', (req, res) => { try { const pluginName = req.params.name; // Validate name format to prevent path traversal if (!/^[a-zA-Z0-9_-]+$/.test(pluginName)) { return res.status(400).json({ error: 'Invalid plugin name' }); } uninstallPlugin(pluginName); res.json({ success: true, name: pluginName }); } catch (err) { res.status(400).json({ error: 'Failed to uninstall plugin', details: err.message }); } }); export default router;