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>
This commit is contained in:
153
api/routes/servers.js
Normal file
153
api/routes/servers.js
Normal file
@@ -0,0 +1,153 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user