From 15d5890861cda3dd6c3bbee34da139896adff6ee Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 21 Dec 2025 12:47:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20stopCommand=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E4=BB=A5=E7=A1=AE=E4=BF=9D=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E6=AD=A3=E5=B8=B8=E9=80=80=E5=87=BA=EF=BC=9B=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20MCP=20=E8=B7=AF=E7=94=B1=E5=BA=8F=E5=88=97=E5=8C=96=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B5=8C=E5=A5=97=E5=AF=B9=E8=B1=A1=EF=BC=9B?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20CSS=20=E7=B1=BB=E4=BB=A5=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E8=A1=8C=E6=95=B0=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ccw/src/commands/stop.ts | 8 ++- ccw/src/core/routes/mcp-routes.ts | 67 +++++++++++++++---- .../templates/dashboard-css/07-managers.css | 7 ++ .../dashboard-js/views/hook-manager.js | 31 +++++++-- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/ccw/src/commands/stop.ts b/ccw/src/commands/stop.ts index d868ac9e..6d406b97 100644 --- a/ccw/src/commands/stop.ts +++ b/ccw/src/commands/stop.ts @@ -72,7 +72,7 @@ export async function stopCommand(options: StopOptions): Promise { await new Promise(resolve => setTimeout(resolve, 500)); console.log(chalk.green.bold('\n Server stopped successfully!\n')); - return; + process.exit(0); } // No CCW server responding, check if port is in use @@ -80,7 +80,7 @@ export async function stopCommand(options: StopOptions): Promise { if (!pid) { console.log(chalk.yellow(` No server running on port ${port}\n`)); - return; + process.exit(0); } // Port is in use by another process @@ -92,16 +92,20 @@ export async function stopCommand(options: StopOptions): Promise { if (killed) { console.log(chalk.green.bold('\n Process killed successfully!\n')); + process.exit(0); } else { console.log(chalk.red('\n Failed to kill process. Try running as administrator.\n')); + process.exit(1); } } else { console.log(chalk.gray(`\n This is not a CCW server. Use --force to kill it:`)); console.log(chalk.white(` ccw stop --force\n`)); + process.exit(0); } } catch (err) { const error = err as Error; console.error(chalk.red(`\n Error: ${error.message}\n`)); + process.exit(1); } } diff --git a/ccw/src/core/routes/mcp-routes.ts b/ccw/src/core/routes/mcp-routes.ts index 303b9149..343cfa0e 100644 --- a/ccw/src/core/routes/mcp-routes.ts +++ b/ccw/src/core/routes/mcp-routes.ts @@ -153,10 +153,18 @@ function parseTomlValue(value: string): any { /** * Serialize object to TOML format for Codex config + * + * Handles mixed objects containing both simple values and sub-objects. + * For example: { command: "cmd", args: [...], env: { KEY: "value" } } + * becomes: + * [section] + * command = "cmd" + * args = [...] + * [section.env] + * KEY = "value" */ function serializeToml(obj: Record, prefix: string = ''): string { let result = ''; - const sections: string[] = []; for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined) continue; @@ -164,23 +172,58 @@ function serializeToml(obj: Record, prefix: string = ''): string { if (typeof value === 'object' && !Array.isArray(value)) { // Handle nested sections (like mcp_servers.server_name) const sectionKey = prefix ? `${prefix}.${key}` : key; - sections.push(sectionKey); - // Check if this is a section with sub-sections or direct values - const hasSubSections = Object.values(value).some(v => typeof v === 'object' && !Array.isArray(v)); + // Separate simple values from sub-objects + const simpleEntries: [string, any][] = []; + const objectEntries: [string, any][] = []; - if (hasSubSections) { - // This section has sub-sections, recurse without header - result += serializeToml(value, sectionKey); - } else { - // This section has direct values, add header and values + for (const [subKey, subValue] of Object.entries(value)) { + if (subValue === null || subValue === undefined) continue; + if (typeof subValue === 'object' && !Array.isArray(subValue)) { + objectEntries.push([subKey, subValue]); + } else { + simpleEntries.push([subKey, subValue]); + } + } + + // Write section header if there are simple values + if (simpleEntries.length > 0) { result += `\n[${sectionKey}]\n`; - for (const [subKey, subValue] of Object.entries(value)) { - if (subValue !== null && subValue !== undefined) { - result += `${subKey} = ${serializeTomlValue(subValue)}\n`; + for (const [subKey, subValue] of simpleEntries) { + result += `${subKey} = ${serializeTomlValue(subValue)}\n`; + } + } + + // Recursively handle sub-objects + if (objectEntries.length > 0) { + for (const [subKey, subValue] of objectEntries) { + const subSectionKey = `${sectionKey}.${subKey}`; + + // Check if sub-object has nested objects + const hasNestedObjects = Object.values(subValue).some( + v => typeof v === 'object' && v !== null && !Array.isArray(v) + ); + + if (hasNestedObjects) { + // Recursively process nested objects + result += serializeToml({ [subKey]: subValue }, sectionKey); + } else { + // Write sub-section with simple values + result += `\n[${subSectionKey}]\n`; + for (const [nestedKey, nestedValue] of Object.entries(subValue)) { + if (nestedValue !== null && nestedValue !== undefined) { + result += `${nestedKey} = ${serializeTomlValue(nestedValue)}\n`; + } + } } } } + + // If no simple values but has object entries, still need to process + if (simpleEntries.length === 0 && objectEntries.length === 0) { + // Empty section - write header only + result += `\n[${sectionKey}]\n`; + } } else if (!prefix) { // Top-level simple values result += `${key} = ${serializeTomlValue(value)}\n`; diff --git a/ccw/src/templates/dashboard-css/07-managers.css b/ccw/src/templates/dashboard-css/07-managers.css index bdcc1194..d06d31ac 100644 --- a/ccw/src/templates/dashboard-css/07-managers.css +++ b/ccw/src/templates/dashboard-css/07-managers.css @@ -514,6 +514,13 @@ overflow: hidden; } +.line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + /* Highlight pulse effect */ .highlight-pulse { animation: highlightPulse 0.5s ease-out 2; diff --git a/ccw/src/templates/dashboard-js/views/hook-manager.js b/ccw/src/templates/dashboard-js/views/hook-manager.js index c30b801d..35125376 100644 --- a/ccw/src/templates/dashboard-js/views/hook-manager.js +++ b/ccw/src/templates/dashboard-js/views/hook-manager.js @@ -371,9 +371,9 @@ function renderHooksByEvent(hooks, scope) { matcher ${escapeHtml(matcher)} -
+
command - ${escapeHtml(command)} + ${escapeHtml(command)}
${args.length > 0 ? `
@@ -570,13 +570,36 @@ function attachHookEventListeners() { const hook = hookList[index]; if (hook) { + // Support both Claude Code format (hooks[0].command) and legacy format (command + args) + let command = ''; + let args = []; + + if (hook.hooks && hook.hooks[0]) { + // Claude Code format: { hooks: [{ type: "command", command: "bash -c '...'" }] } + const fullCommand = hook.hooks[0].command || ''; + // Try to split command and args for bash -c commands + const bashMatch = fullCommand.match(/^(bash|sh|cmd)\s+(-c)\s+(.+)$/s); + if (bashMatch) { + command = bashMatch[1]; + args = [bashMatch[2], bashMatch[3]]; + } else { + // For other commands, put the whole thing as command + command = fullCommand; + args = []; + } + } else { + // Legacy format: { command: "bash", args: ["-c", "..."] } + command = hook.command || ''; + args = hook.args || []; + } + openHookCreateModal({ scope: scope, event: event, index: index, matcher: hook.matcher || '', - command: hook.command, - args: hook.args || [] + command: command, + args: args }); } });