herdctl-editor/server.js

113 lines
4.4 KiB
JavaScript
Raw Permalink Normal View History

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';
const HERDCTL_API = 'http://herdctl:3232';
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', async (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$/, '');
const r = await fetch(`${HERDCTL_API}/api/agents/${agentName}/trigger`, { method: 'POST' });
const body = await r.text();
res.json({ success: true, message: `Triggered ${agentName}`, response: body });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// Proxy herdctl agent status
app.get('/api/status', async (req, res) => {
try {
const r = await fetch(`${HERDCTL_API}/api/agents`);
const data = await r.json();
res.json(data);
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`herdctl-editor running on port ${PORT}`);
});