diff --git a/server.js b/server.js new file mode 100644 index 0000000..de24ee7 --- /dev/null +++ b/server.js @@ -0,0 +1,109 @@ +import express from 'express'; +import fs from 'node:fs'; +import path from 'node:path'; +import { execSync } from 'node:child_process'; +import yaml from 'js-yaml'; + +const app = express(); +const PORT = 3233; +const AGENTS_DIR = '/agents'; +const HERDCTL_CONTAINER = 'herdctl'; + +app.use(express.json({ limit: '1mb' })); +app.use(express.static('public')); + +// List all agents +app.get('/api/agents', (req, res) => { + try { + const files = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')); + const agents = files.map(file => { + const content = fs.readFileSync(path.join(AGENTS_DIR, file), 'utf8'); + let parsed = {}; + try { parsed = yaml.load(content); } catch(e) {} + return { + filename: file, + name: parsed.name || file.replace(/\.ya?ml$/, ''), + description: parsed.description || '', + schedules: parsed.schedules ? Object.keys(parsed.schedules) : [] + }; + }); + res.json(agents); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// Get agent YAML content +app.get('/api/agents/:filename', (req, res) => { + try { + const filename = path.basename(req.params.filename); + if (!filename.endsWith('.yaml') && !filename.endsWith('.yml')) + return res.status(400).json({ error: 'Invalid file' }); + const filepath = path.join(AGENTS_DIR, filename); + if (!fs.existsSync(filepath)) return res.status(404).json({ error: 'Not found' }); + res.json({ filename, content: fs.readFileSync(filepath, 'utf8') }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// Save agent YAML and restart herdctl +app.put('/api/agents/:filename', (req, res) => { + try { + const filename = path.basename(req.params.filename); + if (!filename.endsWith('.yaml') && !filename.endsWith('.yml')) + return res.status(400).json({ error: 'Invalid file' }); + const { content } = req.body; + if (!content) return res.status(400).json({ error: 'No content' }); + try { yaml.load(content); } catch(e) { + return res.status(400).json({ error: `Invalid YAML: ${e.message}` }); + } + const filepath = path.join(AGENTS_DIR, filename); + if (fs.existsSync(filepath)) fs.copyFileSync(filepath, filepath + '.bak'); + fs.writeFileSync(filepath, content, 'utf8'); + try { + execSync(`docker restart ${HERDCTL_CONTAINER}`, { timeout: 15000 }); + res.json({ success: true, message: 'Saved and herdctl restarted.' }); + } catch (e) { + res.json({ success: true, message: 'Saved. Restart failed: ' + e.message }); + } + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// Create new agent +app.post('/api/agents', (req, res) => { + try { + const { filename, content } = req.body; + if (!filename || !content) return res.status(400).json({ error: 'filename and content required' }); + const safe = path.basename(filename); + if (!safe.endsWith('.yaml') && !safe.endsWith('.yml')) + return res.status(400).json({ error: 'Filename must end in .yaml or .yml' }); + try { yaml.load(content); } catch(e) { + return res.status(400).json({ error: `Invalid YAML: ${e.message}` }); + } + const filepath = path.join(AGENTS_DIR, safe); + if (fs.existsSync(filepath)) return res.status(409).json({ error: 'File already exists' }); + fs.writeFileSync(filepath, content, 'utf8'); + res.json({ success: true, filename: safe }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// Trigger agent immediately via herdctl API +app.post('/api/agents/:filename/trigger', (req, res) => { + try { + const filename = path.basename(req.params.filename); + const content = fs.readFileSync(path.join(AGENTS_DIR, filename), 'utf8'); + const parsed = yaml.load(content); + const agentName = parsed.name || filename.replace(/\.ya?ml$/, ''); + execSync(`curl -s -X POST http://herdctl:3232/api/agents/${agentName}/trigger`, { timeout: 10000 }); + res.json({ success: true, message: `Triggered ${agentName}` }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// Proxy herdctl agent status +app.get('/api/status', (req, res) => { + try { + const result = execSync('curl -s http://herdctl:3232/api/agents', { timeout: 5000 }).toString(); + res.json(JSON.parse(result)); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log(`herdctl-editor running on port ${PORT}`); +});