Skip to content

Commit f112d4b

Browse files
catlog22claude
andcommitted
refactor: redesign cli settings export/import API with endpoint-based schema
Replace nested settings structure with flat endpoints array for export/import. Add conflict strategy options (skip/overwrite/merge) and skipInvalid/disableImported flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 45756aa commit f112d4b

3 files changed

Lines changed: 154 additions & 161 deletions

File tree

ccw/frontend/src/lib/api.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6159,28 +6159,17 @@ export async function upgradeCcwInstallation(
61596159
*/
61606160
export interface ExportedSettings {
61616161
version: string;
6162-
exportedAt: string;
6163-
settings: {
6164-
cliTools?: Record<string, unknown>;
6165-
chineseResponse?: {
6166-
claudeEnabled: boolean;
6167-
codexEnabled: boolean;
6168-
};
6169-
windowsPlatform?: {
6170-
enabled: boolean;
6171-
};
6172-
codexCliEnhancement?: {
6173-
enabled: boolean;
6174-
};
6175-
};
6162+
timestamp: string;
6163+
endpoints: Array<Record<string, unknown>>;
61766164
}
61776165

61786166
/**
61796167
* Import options for settings import
61806168
*/
61816169
export interface ImportOptions {
6182-
overwrite?: boolean;
6183-
dryRun?: boolean;
6170+
conflictStrategy?: 'skip' | 'overwrite' | 'merge';
6171+
skipInvalid?: boolean;
6172+
disableImported?: boolean;
61846173
}
61856174

61866175
/**

ccw/frontend/src/pages/SettingsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ function ResponseLanguageSection() {
619619
const data = JSON.parse(text) as ExportedSettings;
620620

621621
// Validate basic structure
622-
if (!data.version || !data.settings) {
622+
if (!data.version || !data.endpoints) {
623623
toast.error(formatMessage({ id: 'settings.responseLanguage.importInvalidStructure' }));
624624
return;
625625
}

ccw/src/core/routes/cli-settings-routes.ts

Lines changed: 148 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -213,150 +213,7 @@ export async function handleCliSettingsRoutes(ctx: RouteContext): Promise<boolea
213213
return true;
214214
}
215215

216-
// ========== GET SINGLE SETTINGS ==========
217-
// GET /api/cli/settings/:id
218-
const getMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
219-
if (getMatch && req.method === 'GET') {
220-
const endpointId = sanitizeEndpointId(getMatch[1]);
221-
try {
222-
const endpoint = loadEndpointSettings(endpointId);
223-
224-
if (!endpoint) {
225-
res.writeHead(404, { 'Content-Type': 'application/json' });
226-
res.end(JSON.stringify({ error: 'Endpoint not found' }));
227-
return true;
228-
}
229-
230-
res.writeHead(200, { 'Content-Type': 'application/json' });
231-
res.end(JSON.stringify({
232-
endpoint,
233-
filePath: getSettingsFilePath(endpointId)
234-
}));
235-
} catch (err) {
236-
res.writeHead(500, { 'Content-Type': 'application/json' });
237-
res.end(JSON.stringify({ error: (err as Error).message }));
238-
}
239-
return true;
240-
}
241-
242-
// ========== UPDATE SETTINGS ==========
243-
// PUT /api/cli/settings/:id
244-
const putMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
245-
if (putMatch && req.method === 'PUT') {
246-
const endpointId = sanitizeEndpointId(putMatch[1]);
247-
handlePostRequest(req, res, async (body: unknown) => {
248-
try {
249-
const request = body as Partial<SaveEndpointRequest>;
250-
251-
// Check if just toggling enabled status
252-
if (Object.keys(request).length === 1 && 'enabled' in request) {
253-
const result = toggleEndpointEnabled(endpointId, request.enabled as boolean);
254-
255-
if (result.success) {
256-
broadcastToClients({
257-
type: 'CLI_SETTINGS_TOGGLED',
258-
payload: {
259-
endpointId,
260-
enabled: request.enabled,
261-
timestamp: new Date().toISOString()
262-
}
263-
});
264-
}
265-
return result;
266-
}
267-
268-
// Full update
269-
const existing = loadEndpointSettings(endpointId);
270-
if (!existing) {
271-
return { error: 'Endpoint not found', status: 404 };
272-
}
273-
274-
const updateRequest: SaveEndpointRequest = {
275-
id: endpointId,
276-
name: request.name || existing.name,
277-
description: request.description ?? existing.description,
278-
settings: request.settings || existing.settings,
279-
enabled: request.enabled ?? existing.enabled
280-
};
281-
282-
const result = saveEndpointSettings(updateRequest);
283-
284-
if (result.success) {
285-
broadcastToClients({
286-
type: 'CLI_SETTINGS_UPDATED',
287-
payload: {
288-
endpoint: result.endpoint,
289-
filePath: result.filePath,
290-
timestamp: new Date().toISOString()
291-
}
292-
});
293-
}
294-
295-
return result;
296-
} catch (err) {
297-
return { error: (err as Error).message, status: 500 };
298-
}
299-
});
300-
return true;
301-
}
302-
303-
// ========== DELETE SETTINGS ==========
304-
// DELETE /api/cli/settings/:id
305-
const deleteMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
306-
if (deleteMatch && req.method === 'DELETE') {
307-
const endpointId = sanitizeEndpointId(deleteMatch[1]);
308-
try {
309-
const result = deleteEndpointSettings(endpointId);
310-
311-
if (result.success) {
312-
broadcastToClients({
313-
type: 'CLI_SETTINGS_DELETED',
314-
payload: {
315-
endpointId,
316-
timestamp: new Date().toISOString()
317-
}
318-
});
319-
320-
res.writeHead(200, { 'Content-Type': 'application/json' });
321-
res.end(JSON.stringify(result));
322-
} else {
323-
res.writeHead(404, { 'Content-Type': 'application/json' });
324-
res.end(JSON.stringify(result));
325-
}
326-
} catch (err) {
327-
res.writeHead(500, { 'Content-Type': 'application/json' });
328-
res.end(JSON.stringify({ error: (err as Error).message }));
329-
}
330-
return true;
331-
}
332-
333-
// ========== GET SETTINGS FILE PATH ==========
334-
// GET /api/cli/settings/:id/path
335-
const pathMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)\/path$/);
336-
if (pathMatch && req.method === 'GET') {
337-
const endpointId = sanitizeEndpointId(pathMatch[1]);
338-
try {
339-
const endpoint = loadEndpointSettings(endpointId);
340-
341-
if (!endpoint) {
342-
res.writeHead(404, { 'Content-Type': 'application/json' });
343-
res.end(JSON.stringify({ error: 'Endpoint not found' }));
344-
return true;
345-
}
346-
347-
const filePath = getSettingsFilePath(endpointId);
348-
res.writeHead(200, { 'Content-Type': 'application/json' });
349-
res.end(JSON.stringify({
350-
endpointId,
351-
filePath,
352-
enabled: endpoint.enabled
353-
}));
354-
} catch (err) {
355-
res.writeHead(500, { 'Content-Type': 'application/json' });
356-
res.end(JSON.stringify({ error: (err as Error).message }));
357-
}
358-
return true;
359-
}
216+
// ========== NAMED SUB-ROUTES (must come before :id routes) ==========
360217

361218
// ========== SYNC BUILTIN TOOLS AVAILABILITY ==========
362219
// POST /api/cli/settings/sync-tools
@@ -505,5 +362,152 @@ export async function handleCliSettingsRoutes(ctx: RouteContext): Promise<boolea
505362
return true;
506363
}
507364

365+
// ========== PARAMETERIZED :id ROUTES ==========
366+
367+
// ========== GET SINGLE SETTINGS ==========
368+
// GET /api/cli/settings/:id
369+
const getMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
370+
if (getMatch && req.method === 'GET') {
371+
const endpointId = sanitizeEndpointId(getMatch[1]);
372+
try {
373+
const endpoint = loadEndpointSettings(endpointId);
374+
375+
if (!endpoint) {
376+
res.writeHead(404, { 'Content-Type': 'application/json' });
377+
res.end(JSON.stringify({ error: 'Endpoint not found' }));
378+
return true;
379+
}
380+
381+
res.writeHead(200, { 'Content-Type': 'application/json' });
382+
res.end(JSON.stringify({
383+
endpoint,
384+
filePath: getSettingsFilePath(endpointId)
385+
}));
386+
} catch (err) {
387+
res.writeHead(500, { 'Content-Type': 'application/json' });
388+
res.end(JSON.stringify({ error: (err as Error).message }));
389+
}
390+
return true;
391+
}
392+
393+
// ========== UPDATE SETTINGS ==========
394+
// PUT /api/cli/settings/:id
395+
const putMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
396+
if (putMatch && req.method === 'PUT') {
397+
const endpointId = sanitizeEndpointId(putMatch[1]);
398+
handlePostRequest(req, res, async (body: unknown) => {
399+
try {
400+
const request = body as Partial<SaveEndpointRequest>;
401+
402+
// Check if just toggling enabled status
403+
if (Object.keys(request).length === 1 && 'enabled' in request) {
404+
const result = toggleEndpointEnabled(endpointId, request.enabled as boolean);
405+
406+
if (result.success) {
407+
broadcastToClients({
408+
type: 'CLI_SETTINGS_TOGGLED',
409+
payload: {
410+
endpointId,
411+
enabled: request.enabled,
412+
timestamp: new Date().toISOString()
413+
}
414+
});
415+
}
416+
return result;
417+
}
418+
419+
// Full update
420+
const existing = loadEndpointSettings(endpointId);
421+
if (!existing) {
422+
return { error: 'Endpoint not found', status: 404 };
423+
}
424+
425+
const updateRequest: SaveEndpointRequest = {
426+
id: endpointId,
427+
name: request.name || existing.name,
428+
description: request.description ?? existing.description,
429+
settings: request.settings || existing.settings,
430+
enabled: request.enabled ?? existing.enabled
431+
};
432+
433+
const result = saveEndpointSettings(updateRequest);
434+
435+
if (result.success) {
436+
broadcastToClients({
437+
type: 'CLI_SETTINGS_UPDATED',
438+
payload: {
439+
endpoint: result.endpoint,
440+
filePath: result.filePath,
441+
timestamp: new Date().toISOString()
442+
}
443+
});
444+
}
445+
446+
return result;
447+
} catch (err) {
448+
return { error: (err as Error).message, status: 500 };
449+
}
450+
});
451+
return true;
452+
}
453+
454+
// ========== DELETE SETTINGS ==========
455+
// DELETE /api/cli/settings/:id
456+
const deleteMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)$/);
457+
if (deleteMatch && req.method === 'DELETE') {
458+
const endpointId = sanitizeEndpointId(deleteMatch[1]);
459+
try {
460+
const result = deleteEndpointSettings(endpointId);
461+
462+
if (result.success) {
463+
broadcastToClients({
464+
type: 'CLI_SETTINGS_DELETED',
465+
payload: {
466+
endpointId,
467+
timestamp: new Date().toISOString()
468+
}
469+
});
470+
471+
res.writeHead(200, { 'Content-Type': 'application/json' });
472+
res.end(JSON.stringify(result));
473+
} else {
474+
res.writeHead(404, { 'Content-Type': 'application/json' });
475+
res.end(JSON.stringify(result));
476+
}
477+
} catch (err) {
478+
res.writeHead(500, { 'Content-Type': 'application/json' });
479+
res.end(JSON.stringify({ error: (err as Error).message }));
480+
}
481+
return true;
482+
}
483+
484+
// ========== GET SETTINGS FILE PATH ==========
485+
// GET /api/cli/settings/:id/path
486+
const pathMatch = pathname.match(/^\/api\/cli\/settings\/([^/]+)\/path$/);
487+
if (pathMatch && req.method === 'GET') {
488+
const endpointId = sanitizeEndpointId(pathMatch[1]);
489+
try {
490+
const endpoint = loadEndpointSettings(endpointId);
491+
492+
if (!endpoint) {
493+
res.writeHead(404, { 'Content-Type': 'application/json' });
494+
res.end(JSON.stringify({ error: 'Endpoint not found' }));
495+
return true;
496+
}
497+
498+
const filePath = getSettingsFilePath(endpointId);
499+
res.writeHead(200, { 'Content-Type': 'application/json' });
500+
res.end(JSON.stringify({
501+
endpointId,
502+
filePath,
503+
enabled: endpoint.enabled
504+
}));
505+
} catch (err) {
506+
res.writeHead(500, { 'Content-Type': 'application/json' });
507+
res.end(JSON.stringify({ error: (err as Error).message }));
508+
}
509+
return true;
510+
}
511+
508512
return false;
509513
}

0 commit comments

Comments
 (0)