Files
idea.llm.gitea.repo.docker.…/api/routes/servers.js
Clint Masden feec35ffce Add full REST API for all deployment operations (projects, servers, docker)
Port all IPC handlers to HTTP endpoints so the UI and LLM use the same
API. Adds routes/projects.js (scan, compare, init), routes/servers.js
(CRUD, containers, logs), routes/docker.js (build, deploy, pull, vscode-diff).
Enhanced ssh.js with full SSHService class (SFTP upload/download).
Updated renderer api.js to use fetch instead of window.api IPC.
Added concurrently for npm run dev (API + Vite + Electron).
OpenAPI spec now covers all 24 endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 11:17:40 -06:00

154 lines
4.9 KiB
JavaScript

import { Router } from 'express';
import { loadDeployConfig, saveDeployConfig, getServerSshConfig } from '../lib/config.js';
import { SSHService } from '../lib/ssh.js';
const router = Router();
function wrap(fn) {
return async (req, res) => {
try {
const result = await fn(req, res);
if (!res.headersSent) res.json(result);
} catch (err) {
res.status(err.status || 500).json({ error: err.message });
}
};
}
// GET /api/servers — list all configured deployment servers
router.get('/', wrap(async () => {
const config = loadDeployConfig();
return config.servers || [];
}));
// POST /api/servers — add or update a server
router.post('/', wrap(async (req) => {
const server = req.body;
const config = loadDeployConfig();
if (!config.servers) config.servers = [];
const index = config.servers.findIndex(s => s.id === server.id);
if (index >= 0) {
config.servers[index] = server;
} else {
server.id = Date.now().toString();
config.servers.push(server);
}
saveDeployConfig(config);
return { success: true, server };
}));
// DELETE /api/servers/:id — delete a server
router.delete('/:id', wrap(async (req) => {
const config = loadDeployConfig();
config.servers = (config.servers || []).filter(s => s.id !== req.params.id);
saveDeployConfig(config);
return { success: true };
}));
// GET /api/servers/:id/scan — scan remote server for deployed containers
router.get('/:id/scan', wrap(async (req) => {
const config = loadDeployConfig();
const server = config.servers.find(s => s.id === req.params.id);
if (!server) throw Object.assign(new Error('Server not found'), { status: 404 });
const sshConfig = getServerSshConfig(server);
const ssh = new SSHService(sshConfig);
try {
await ssh.connect();
const result = await ssh.exec('ls -1 ~/containers 2>/dev/null || echo ""');
const projectDirs = result.split('\n').filter(Boolean);
const deployed = [];
for (const dir of projectDirs) {
const remotePath = `~/containers/${dir}`;
try {
const filesResult = await ssh.exec(`ls -la ${remotePath} 2>/dev/null || echo ""`);
const hasDockerCompose = filesResult.includes('docker-compose.yml');
const hasEnv = filesResult.includes('.env');
const hasData = filesResult.includes('data');
const tarMatch = filesResult.match(/(\S+\.tar)/);
let dockerComposeContent = null;
if (hasDockerCompose) {
try { dockerComposeContent = await ssh.exec(`cat ${remotePath}/docker-compose.yml 2>/dev/null`); } catch { /* ignore */ }
}
deployed.push({
name: dir,
remotePath,
hasDockerCompose,
hasEnv,
hasData,
tarFile: tarMatch ? tarMatch[1] : null,
dockerComposeContent,
});
} catch { /* skip */ }
}
ssh.disconnect();
return { success: true, deployed };
} catch (err) {
throw new Error(`Scan failed: ${err.message}`);
}
}));
// GET /api/servers/:id/containers — list running docker containers
router.get('/:id/containers', wrap(async (req) => {
const config = loadDeployConfig();
const server = config.servers.find(s => s.id === req.params.id);
if (!server) throw Object.assign(new Error('Server not found'), { status: 404 });
const sshConfig = getServerSshConfig(server);
const ssh = new SSHService(sshConfig);
try {
await ssh.connect();
const result = await ssh.exec('docker ps --format "{{.Names}}|{{.Status}}|{{.Ports}}"');
ssh.disconnect();
const containers = result.split('\n').filter(Boolean).map(line => {
const [name, status, ports] = line.split('|');
return { name, status, ports };
});
return { success: true, containers };
} catch (err) {
throw new Error(`Failed to get containers: ${err.message}`);
}
}));
// GET /api/servers/:id/logs — get container logs
router.get('/:id/logs', wrap(async (req) => {
const config = loadDeployConfig();
const server = config.servers.find(s => s.id === req.params.id);
if (!server) throw Object.assign(new Error('Server not found'), { status: 404 });
const { containerName, remotePath, lines = 100 } = req.query;
const sshConfig = getServerSshConfig(server);
const ssh = new SSHService(sshConfig);
const password = sshConfig.password;
const sudoPrefix = server.useSudo ? `echo '${password}' | sudo -S ` : '';
try {
await ssh.connect();
let logs;
if (remotePath) {
logs = await ssh.exec(`cd ${remotePath} && ${sudoPrefix}docker compose logs --tail ${lines} 2>&1`);
} else if (containerName) {
logs = await ssh.exec(`${sudoPrefix}docker logs ${containerName} --tail ${lines} 2>&1`);
} else {
throw Object.assign(new Error('containerName or remotePath required'), { status: 400 });
}
ssh.disconnect();
return { success: true, logs };
} catch (err) {
throw new Error(`Failed to get logs: ${err.message}`);
}
}));
export default router;