Add server, Dockerfile, package.json: server.js
This commit is contained in:
parent
1cf66699cf
commit
d706959f65
1 changed files with 109 additions and 0 deletions
109
server.js
Normal file
109
server.js
Normal file
|
|
@ -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}`);
|
||||
});
|
||||
Loading…
Reference in a new issue