110 lines
4.3 KiB
JavaScript
110 lines
4.3 KiB
JavaScript
|
|
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}`);
|
||
|
|
});
|