Add Coolify REST API server with Scalar docs and UI integration
Express API server on :3100 exposing all Coolify operations: - CRUD for apps, env vars, servers - Full upsert pipeline (create/update + env + route + deploy) - Drift detection, Traefik route management via SSH - Scalar API docs at /reference, OpenAPI 3.1 spec UI: New Coolify page with app cards, deploy/delete actions, env var expansion. Sidebar nav + React Query hooks + fetch client. Both UI and LLM/CLI use the same HTTP endpoints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
24
api/lib/config.js
Normal file
24
api/lib/config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Shared config from gitea.repo.management
|
||||
const CONFIG_PATH = join(__dirname, '..', '..', '..', 'gitea.repo.management', 'config.json');
|
||||
|
||||
let _cached = null;
|
||||
|
||||
export function loadConfig() {
|
||||
if (_cached) return _cached;
|
||||
if (!existsSync(CONFIG_PATH)) {
|
||||
throw new Error(`Config not found at ${CONFIG_PATH}`);
|
||||
}
|
||||
_cached = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
||||
return _cached;
|
||||
}
|
||||
|
||||
export function reloadConfig() {
|
||||
_cached = null;
|
||||
return loadConfig();
|
||||
}
|
||||
28
api/lib/coolify-client.js
Normal file
28
api/lib/coolify-client.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Authenticated fetch wrapper for the Coolify API.
|
||||
* Ported from gitea.repo.management/electron/main.cjs:166-183
|
||||
*/
|
||||
export async function coolifyFetch(config, apiPath, options = {}) {
|
||||
const base = (config.coolify?.apiUrl || '').replace(/\/$/, '');
|
||||
const token = config.coolify?.apiToken || '';
|
||||
const url = `${base}/api/v1${apiPath}`;
|
||||
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {})
|
||||
};
|
||||
|
||||
const res = await fetch(url, { ...options, headers });
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err = new Error(`Coolify ${options.method || 'GET'} ${apiPath} failed: ${res.status} ${text}`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
if (ct.includes('application/json')) return res.json();
|
||||
return res.text();
|
||||
}
|
||||
45
api/lib/ssh.js
Normal file
45
api/lib/ssh.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Client } from 'ssh2';
|
||||
|
||||
/**
|
||||
* Execute a command on the remote server via SSH.
|
||||
* Uses the ssh2 library (already a project dependency) instead of
|
||||
* the plink/sshpass fallback chain from gitea.repo.management.
|
||||
*/
|
||||
export function sshExec(config, command) {
|
||||
const host = config.coolify?.sshHost;
|
||||
const user = config.coolify?.sshUser;
|
||||
const password = config.coolify?.sshPassword;
|
||||
|
||||
if (!host || !user) {
|
||||
return Promise.reject(new Error('SSH not configured — set coolify.sshHost and coolify.sshUser in config.json'));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = new Client();
|
||||
|
||||
conn.on('ready', () => {
|
||||
conn.exec(command, (err, stream) => {
|
||||
if (err) { conn.end(); return reject(err); }
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
stream.on('close', (code) => {
|
||||
conn.end();
|
||||
if (code !== 0 && stderr) {
|
||||
reject(new Error(stderr.trim()));
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('data', (data) => { stdout += data.toString(); });
|
||||
stream.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
});
|
||||
});
|
||||
|
||||
conn.on('error', (err) => reject(err));
|
||||
|
||||
conn.connect({ host, port: 22, username: user, password });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user