Files
Claude-Code-Workflow/ccw/src/core/routes/cli-settings-routes.ts
catlog22 d9f1d14d5e feat: add CCW Loop System for automated iterative workflow execution
Implements a complete loop execution system with multi-loop parallel support,
dashboard monitoring, and comprehensive security validation.

Core features:
- Loop orchestration engine (loop-manager, loop-state-manager)
- Multi-loop parallel execution with independent state management
- REST API endpoints for loop control (pause, resume, stop, retry)
- WebSocket real-time status updates
- Dashboard Loop Monitor view with live updates
- Security: path traversal protection and sandboxed JavaScript evaluation

Test coverage:
- 42 comprehensive tests covering multi-loop, API, WebSocket, security
- Security validation for success_condition injection attacks
- Edge case handling and end-to-end workflow tests
2026-01-21 22:55:24 +08:00

280 lines
8.8 KiB
TypeScript

/**
* CLI Settings Routes Module
* Handles Claude CLI settings file management API endpoints
*/
import type { RouteContext } from './types.js';
import {
saveEndpointSettings,
loadEndpointSettings,
deleteEndpointSettings,
listAllSettings,
toggleEndpointEnabled,
getSettingsFilePath,
ensureSettingsDir,
sanitizeEndpointId
} from '../../config/cli-settings-manager.js';
import type { SaveEndpointRequest } from '../../types/cli-settings.js';
import { validateSettings } from '../../types/cli-settings.js';
import { syncBuiltinToolsAvailability, getBuiltinToolsSyncReport } from '../../tools/claude-cli-tools.js';
/**
* Handle CLI Settings routes
* @returns true if route was handled, false otherwise
*/
export async function handleCliSettingsRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, req, res, handlePostRequest, broadcastToClients } = ctx;
// Ensure settings directory exists
ensureSettingsDir();
// ========== LIST ALL SETTINGS ==========
// GET /api/cli/settings
if (pathname === '/api/cli/settings' && req.method === 'GET') {
try {
const result = listAllSettings();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// ========== CREATE/UPDATE SETTINGS ==========
// POST /api/cli/settings
if (pathname === '/api/cli/settings' && req.method === 'POST') {
handlePostRequest(req, res, async (body: unknown) => {
try {
const request = body as SaveEndpointRequest;
// Validate required fields
if (!request.name) {
return { error: 'name is required', status: 400 };
}
if (!request.settings || !request.settings.env) {
return { error: 'settings.env is required', status: 400 };
}
// Deep validation of settings object
if (!validateSettings(request.settings)) {
return { error: 'Invalid settings object format', status: 400 };
}
const result = saveEndpointSettings(request);
if (result.success) {
// Broadcast settings created/updated event
broadcastToClients({
type: 'CLI_SETTINGS_UPDATED',
payload: {
endpoint: result.endpoint,
filePath: result.filePath,
timestamp: new Date().toISOString()
}
});
return result;
} else {
return { error: result.message, status: 500 };
}
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
// ========== GET SINGLE SETTINGS ==========
// GET /api/cli/settings/:id
const getMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
if (getMatch && req.method === 'GET') {
const endpointId = sanitizeEndpointId(getMatch[1]);
try {
const endpoint = loadEndpointSettings(endpointId);
if (!endpoint) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Endpoint not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
endpoint,
filePath: getSettingsFilePath(endpointId)
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// ========== UPDATE SETTINGS ==========
// PUT /api/cli/settings/:id
const putMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
if (putMatch && req.method === 'PUT') {
const endpointId = sanitizeEndpointId(putMatch[1]);
handlePostRequest(req, res, async (body: unknown) => {
try {
const request = body as Partial<SaveEndpointRequest>;
// Check if just toggling enabled status
if (Object.keys(request).length === 1 && 'enabled' in request) {
const result = toggleEndpointEnabled(endpointId, request.enabled as boolean);
if (result.success) {
broadcastToClients({
type: 'CLI_SETTINGS_TOGGLED',
payload: {
endpointId,
enabled: request.enabled,
timestamp: new Date().toISOString()
}
});
}
return result;
}
// Full update
const existing = loadEndpointSettings(endpointId);
if (!existing) {
return { error: 'Endpoint not found', status: 404 };
}
const updateRequest: SaveEndpointRequest = {
id: endpointId,
name: request.name || existing.name,
description: request.description ?? existing.description,
settings: request.settings || existing.settings,
enabled: request.enabled ?? existing.enabled
};
const result = saveEndpointSettings(updateRequest);
if (result.success) {
broadcastToClients({
type: 'CLI_SETTINGS_UPDATED',
payload: {
endpoint: result.endpoint,
filePath: result.filePath,
timestamp: new Date().toISOString()
}
});
}
return result;
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
});
return true;
}
// ========== DELETE SETTINGS ==========
// DELETE /api/cli/settings/:id
const deleteMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
if (deleteMatch && req.method === 'DELETE') {
const endpointId = sanitizeEndpointId(deleteMatch[1]);
try {
const result = deleteEndpointSettings(endpointId);
if (result.success) {
broadcastToClients({
type: 'CLI_SETTINGS_DELETED',
payload: {
endpointId,
timestamp: new Date().toISOString()
}
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
}
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// ========== GET SETTINGS FILE PATH ==========
// GET /api/cli/settings/:id/path
const pathMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)\/path$/);
if (pathMatch && req.method === 'GET') {
const endpointId = sanitizeEndpointId(pathMatch[1]);
try {
const endpoint = loadEndpointSettings(endpointId);
if (!endpoint) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Endpoint not found' }));
return true;
}
const filePath = getSettingsFilePath(endpointId);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
endpointId,
filePath,
enabled: endpoint.enabled
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// ========== SYNC BUILTIN TOOLS AVAILABILITY ==========
// POST /api/cli/settings/sync-tools
if (pathname === '/api/cli/settings/sync-tools' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { initialPath } = ctx;
try {
const result = await syncBuiltinToolsAvailability(initialPath);
// Broadcast update event
broadcastToClients({
type: 'CLI_TOOLS_CONFIG_UPDATED',
payload: {
tools: result.config,
timestamp: new Date().toISOString()
}
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
changes: result.changes,
config: result.config
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
});
return true;
}
// GET /api/cli/settings/sync-report
if (pathname === '/api/cli/settings/sync-report' && req.method === 'GET') {
try {
const { initialPath } = ctx;
const report = await getBuiltinToolsSyncReport(initialPath);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(report));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
return false;
}