feat: Add multi-host config and dynamic project scanning
Backend: - Load hosts from config/hosts.json - New /api/hosts endpoint listing available hosts - Dynamic project scanning with configurable depth - Support for local and SSH hosts (SSH execution coming next) Frontend (by Web-UI Claude): - Slash commands: /clear, /help, /export, /scroll, /new, /info - Chat export as Markdown Config: - hosts.json defines hosts with connection info and base paths - hosts.example.json as template (real config is gitignored) - Each host has name, description, color, icon, basePaths Next steps: - SSH command execution for remote hosts - Frontend host selector UI - Multi-agent collaboration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { spawn } from 'child_process';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import cors from 'cors';
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { join, basename } from 'path';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
@@ -14,27 +14,114 @@ app.use(express.json());
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
// Load hosts configuration
|
||||
const CONFIG_PATH = process.env.CONFIG_PATH || '/app/config/hosts.json';
|
||||
let hostsConfig = { hosts: {}, defaults: { scanSubdirs: true, maxDepth: 1 } };
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
if (existsSync(CONFIG_PATH)) {
|
||||
hostsConfig = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
||||
console.log('Loaded hosts config:', Object.keys(hostsConfig.hosts));
|
||||
} else {
|
||||
console.log('No hosts.json found, using defaults');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading config:', err);
|
||||
}
|
||||
}
|
||||
loadConfig();
|
||||
|
||||
// Store active Claude sessions
|
||||
const sessions = new Map();
|
||||
|
||||
const server = createServer(app);
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
// REST endpoint to list available projects
|
||||
// Scan directory for projects
|
||||
function scanProjects(basePath, depth = 0, maxDepth = 1) {
|
||||
const projects = [];
|
||||
|
||||
if (!existsSync(basePath)) return projects;
|
||||
|
||||
try {
|
||||
const entries = readdirSync(basePath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
||||
const fullPath = join(basePath, entry.name);
|
||||
projects.push({
|
||||
path: fullPath,
|
||||
name: entry.name,
|
||||
type: 'directory'
|
||||
});
|
||||
|
||||
// Recurse if not at max depth
|
||||
if (depth < maxDepth - 1) {
|
||||
projects.push(...scanProjects(fullPath, depth + 1, maxDepth));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error scanning ${basePath}:`, err.message);
|
||||
}
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
// REST endpoint to list hosts
|
||||
app.get('/api/hosts', (req, res) => {
|
||||
const hosts = Object.entries(hostsConfig.hosts).map(([id, host]) => ({
|
||||
id,
|
||||
name: host.name,
|
||||
description: host.description,
|
||||
color: host.color,
|
||||
icon: host.icon,
|
||||
connectionType: host.connection.type,
|
||||
isLocal: host.connection.type === 'local'
|
||||
}));
|
||||
res.json({ hosts, defaultHost: hostsConfig.defaults?.host || 'neko' });
|
||||
});
|
||||
|
||||
// REST endpoint to list projects for a host
|
||||
app.get('/api/projects', (req, res) => {
|
||||
const baseDirs = [
|
||||
{ path: '/projects', name: 'projects', description: 'Development projects' },
|
||||
{ path: '/docker', name: 'docker', description: 'Docker configurations' },
|
||||
{ path: '/stacks', name: 'stacks', description: 'Production stacks' }
|
||||
];
|
||||
const hostId = req.query.host || hostsConfig.defaults?.host || 'neko';
|
||||
const host = hostsConfig.hosts[hostId];
|
||||
|
||||
if (!host) {
|
||||
return res.status(404).json({ error: `Host '${hostId}' not found` });
|
||||
}
|
||||
|
||||
// Only local hosts for now
|
||||
if (host.connection.type !== 'local') {
|
||||
return res.json({
|
||||
projects: [],
|
||||
host: hostId,
|
||||
message: 'SSH hosts not yet supported for project listing'
|
||||
});
|
||||
}
|
||||
|
||||
const projects = [];
|
||||
for (const dir of baseDirs) {
|
||||
if (existsSync(dir.path)) {
|
||||
projects.push({ ...dir, type: 'directory' });
|
||||
const scanSubdirs = hostsConfig.defaults?.scanSubdirs ?? true;
|
||||
const maxDepth = hostsConfig.defaults?.maxDepth ?? 1;
|
||||
|
||||
for (const basePath of host.basePaths) {
|
||||
// Add base path itself
|
||||
if (existsSync(basePath)) {
|
||||
projects.push({
|
||||
path: basePath,
|
||||
name: basename(basePath),
|
||||
type: 'base',
|
||||
isBase: true
|
||||
});
|
||||
|
||||
// Scan subdirectories if enabled
|
||||
if (scanSubdirs) {
|
||||
projects.push(...scanProjects(basePath, 0, maxDepth));
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json(projects);
|
||||
|
||||
res.json({ projects, host: hostId, hostInfo: { name: host.name, color: host.color } });
|
||||
});
|
||||
|
||||
// Health check
|
||||
|
||||
Reference in New Issue
Block a user