Files
openchamber/packages/web/server/lib/opencode/config-entity-routes.js
T

442 lines
15 KiB
JavaScript

import { buildDeferredRestartResponse } from './config-mutation-response.js';
export const registerConfigEntityRoutes = (app, dependencies) => {
const {
resolveProjectDirectory,
resolveOptionalProjectDirectory,
getAgentSources,
getAgentConfig,
createAgent,
updateAgent,
deleteAgent,
getCommandSources,
createCommand,
updateCommand,
deleteCommand,
listMcpConfigs,
getMcpConfig,
createMcpConfig,
updateMcpConfig,
deleteMcpConfig,
listSnippets,
getSnippet,
createSnippet,
updateSnippet,
deleteSnippet,
expandSnippets,
} = dependencies;
// Persist to disk immediately; OpenCode restart is deferred to an explicit
// Apply & Restart so settings edits do not interrupt live sessions.
const completeMcpMutation = async (res, action, name, applyChange) => {
applyChange();
const past = action === 'delete' ? 'deleted' : `${action}d`;
return res.json(buildDeferredRestartResponse(
`MCP server "${name}" ${past}. Restart OpenCode to apply.`,
));
};
app.get('/api/config/agents/:name', async (req, res) => {
try {
const agentName = req.params.name;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
const sources = getAgentSources(agentName, directory);
const scope = sources.md.exists
? sources.md.scope
: (sources.json.exists ? sources.json.scope : null);
res.json({
name: agentName,
sources: sources,
scope,
isBuiltIn: !sources.md.exists && !sources.json.exists
});
} catch (error) {
console.error('Failed to get agent sources:', error);
res.status(500).json({ error: 'Failed to get agent configuration metadata' });
}
});
app.get('/api/config/agents/:name/config', async (req, res) => {
try {
const agentName = req.params.name;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
const configInfo = getAgentConfig(agentName, directory);
res.json(configInfo);
} catch (error) {
console.error('Failed to get agent config:', error);
res.status(500).json({ error: 'Failed to get agent configuration' });
}
});
app.post('/api/config/agents/:name', async (req, res) => {
try {
const agentName = req.params.name;
const { scope, ...config } = req.body;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
console.log('[Server] Creating agent:', agentName);
console.log('[Server] Config received:', JSON.stringify(config, null, 2));
console.log('[Server] Scope:', scope, 'Working directory:', directory);
createAgent(agentName, config, directory, scope);
res.json(buildDeferredRestartResponse(
`Agent ${agentName} created successfully. Restart OpenCode to apply.`,
));
} catch (error) {
console.error('Failed to create agent:', error);
res.status(500).json({ error: error.message || 'Failed to create agent' });
}
});
app.patch('/api/config/agents/:name', async (req, res) => {
try {
const agentName = req.params.name;
const updates = req.body;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
console.log(`[Server] Updating agent: ${agentName}`);
console.log('[Server] Updates:', JSON.stringify(updates, null, 2));
console.log('[Server] Working directory:', directory);
updateAgent(agentName, updates, directory);
console.log(`[Server] Agent ${agentName} updated successfully`);
res.json(buildDeferredRestartResponse(
`Agent ${agentName} updated successfully. Restart OpenCode to apply.`,
));
} catch (error) {
console.error('[Server] Failed to update agent:', error);
console.error('[Server] Error stack:', error.stack);
res.status(500).json({ error: error.message || 'Failed to update agent' });
}
});
app.delete('/api/config/agents/:name', async (req, res) => {
try {
const agentName = req.params.name;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
const scope = req.body?.scope;
deleteAgent(agentName, directory, scope);
res.json(buildDeferredRestartResponse(
`Agent ${agentName} deleted successfully. Restart OpenCode to apply.`,
));
} catch (error) {
console.error('Failed to delete agent:', error);
res.status(500).json({ error: error.message || 'Failed to delete agent' });
}
});
app.get('/api/config/mcp', async (req, res) => {
try {
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
const configs = listMcpConfigs(directory);
res.json(configs);
} catch (error) {
console.error('[API:GET /api/config/mcp] Failed:', error);
res.status(500).json({ error: error.message || 'Failed to list MCP configs' });
}
});
app.get('/api/config/mcp/:name', async (req, res) => {
try {
const name = req.params.name;
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
const config = getMcpConfig(name, directory);
if (!config) {
return res.status(404).json({ error: `MCP server "${name}" not found` });
}
res.json(config);
} catch (error) {
console.error('[API:GET /api/config/mcp/:name] Failed:', error);
res.status(500).json({ error: error.message || 'Failed to get MCP config' });
}
});
app.post('/api/config/mcp/:name', async (req, res) => {
try {
const name = req.params.name;
const { scope, ...config } = req.body || {};
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
console.log(`[API:POST /api/config/mcp] Creating MCP server: ${name}`);
await completeMcpMutation(res, 'create', name, () => {
createMcpConfig(name, config, directory, scope);
});
} catch (error) {
console.error('[API:POST /api/config/mcp/:name] Failed:', error);
res.status(500).json({ error: error.message || 'Failed to create MCP server' });
}
});
app.patch('/api/config/mcp/:name', async (req, res) => {
try {
const name = req.params.name;
const updates = req.body;
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
console.log(`[API:PATCH /api/config/mcp] Updating MCP server: ${name}`);
await completeMcpMutation(res, 'update', name, () => {
updateMcpConfig(name, updates, directory);
});
} catch (error) {
console.error('[API:PATCH /api/config/mcp/:name] Failed:', error);
if (error?.message === `MCP server "${req.params.name}" not found`) {
return res.status(404).json({ error: error.message });
}
res.status(500).json({ error: error.message || 'Failed to update MCP server' });
}
});
app.delete('/api/config/mcp/:name', async (req, res) => {
try {
const name = req.params.name;
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
console.log(`[API:DELETE /api/config/mcp] Deleting MCP server: ${name}`);
await completeMcpMutation(res, 'delete', name, () => {
deleteMcpConfig(name, directory);
});
} catch (error) {
console.error('[API:DELETE /api/config/mcp/:name] Failed:', error);
res.status(500).json({ error: error.message || 'Failed to delete MCP server' });
}
});
app.get('/api/config/commands/:name', async (req, res) => {
try {
const commandName = req.params.name;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
const sources = getCommandSources(commandName, directory);
const scope = sources.md.exists
? sources.md.scope
: (sources.json.exists ? sources.json.scope : null);
res.json({
name: commandName,
sources: sources,
scope,
isBuiltIn: !sources.md.exists && !sources.json.exists
});
} catch (error) {
console.error('Failed to get command sources:', error);
res.status(500).json({ error: 'Failed to get command configuration metadata' });
}
});
app.post('/api/config/commands/:name', async (req, res) => {
try {
const commandName = req.params.name;
const { scope, ...config } = req.body;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
console.log('[Server] Creating command:', commandName);
console.log('[Server] Config received:', JSON.stringify(config, null, 2));
console.log('[Server] Scope:', scope, 'Working directory:', directory);
createCommand(commandName, config, directory, scope);
res.json(buildDeferredRestartResponse(
`Command ${commandName} created successfully. Restart OpenCode to apply.`,
));
} catch (error) {
console.error('Failed to create command:', error);
res.status(500).json({ error: error.message || 'Failed to create command' });
}
});
app.patch('/api/config/commands/:name', async (req, res) => {
try {
const commandName = req.params.name;
const updates = req.body;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
console.log(`[Server] Updating command: ${commandName}`);
console.log('[Server] Updates:', JSON.stringify(updates, null, 2));
console.log('[Server] Working directory:', directory);
updateCommand(commandName, updates, directory);
console.log(`[Server] Command ${commandName} updated successfully`);
res.json(buildDeferredRestartResponse(
`Command ${commandName} updated successfully. Restart OpenCode to apply.`,
));
} catch (error) {
console.error('[Server] Failed to update command:', error);
console.error('[Server] Error stack:', error.stack);
res.status(500).json({ error: error.message || 'Failed to update command' });
}
});
app.delete('/api/config/commands/:name', async (req, res) => {
try {
const commandName = req.params.name;
const { directory, error } = await resolveProjectDirectory(req);
if (!directory) {
return res.status(400).json({ error });
}
deleteCommand(commandName, directory);
res.json(buildDeferredRestartResponse(
`Command ${commandName} deleted successfully. Restart OpenCode to apply.`,
));
} catch (error) {
console.error('Failed to delete command:', error);
res.status(500).json({ error: error.message || 'Failed to delete command' });
}
});
app.get('/api/config/snippets', async (req, res) => {
try {
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
res.json(listSnippets(directory));
} catch (error) {
console.error('[API:GET /api/config/snippets] Failed:', error);
res.status(500).json({ error: error.message || 'Failed to list snippets' });
}
});
app.post('/api/config/snippets/expand', async (req, res) => {
try {
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
res.json({ text: expandSnippets(req.body?.text ?? '', directory) });
} catch (error) {
console.error('[API:POST /api/config/snippets/expand] Failed:', error);
res.status(500).json({ error: error.message || 'Failed to expand snippets' });
}
});
app.get('/api/config/snippets/:name', async (req, res) => {
try {
const name = req.params.name;
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
const snippet = getSnippet(name, directory);
if (!snippet) {
return res.status(404).json({ error: `Snippet "${name}" not found` });
}
res.json(snippet);
} catch (error) {
console.error('[API:GET /api/config/snippets/:name] Failed:', error);
if (error.message?.includes('Snippet name')) {
return res.status(400).json({ error: error.message });
}
res.status(500).json({ error: error.message || 'Failed to get snippet' });
}
});
app.post('/api/config/snippets/:name', async (req, res) => {
try {
const name = req.params.name;
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
const snippet = createSnippet(name, req.body || {}, directory, req.body?.scope || 'global');
res.json({ success: true, snippet });
} catch (error) {
console.error('[API:POST /api/config/snippets/:name] Failed:', error);
if (error.message?.includes('already exists')) {
return res.status(409).json({ error: error.message });
}
if (error.message?.includes('Snippet name') || error.message?.includes('Project directory')) {
return res.status(400).json({ error: error.message });
}
res.status(500).json({ error: error.message || 'Failed to create snippet' });
}
});
app.patch('/api/config/snippets/:name', async (req, res) => {
try {
const name = req.params.name;
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
res.json({ success: true, snippet: updateSnippet(name, req.body || {}, directory) });
} catch (error) {
console.error('[API:PATCH /api/config/snippets/:name] Failed:', error);
if (error.message?.includes('not found')) {
return res.status(404).json({ error: error.message });
}
if (error.message?.includes('Snippet name')) {
return res.status(400).json({ error: error.message });
}
res.status(500).json({ error: error.message || 'Failed to update snippet' });
}
});
app.delete('/api/config/snippets/:name', async (req, res) => {
try {
const name = req.params.name;
const { directory, error } = await resolveOptionalProjectDirectory(req);
if (error) {
return res.status(400).json({ error });
}
deleteSnippet(name, directory);
res.json({ success: true });
} catch (error) {
console.error('[API:DELETE /api/config/snippets/:name] Failed:', error);
if (error.message?.includes('not found')) {
return res.status(404).json({ error: error.message });
}
if (error.message?.includes('Snippet name')) {
return res.status(400).json({ error: error.message });
}
res.status(500).json({ error: error.message || 'Failed to delete snippet' });
}
});
};