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>
154 lines
4.9 KiB
JavaScript
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;
|