diff --git a/pom.xml b/pom.xml index 41ca9862..35a46541 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.21.28-SNAPSHOT + 0.21.29-SNAPSHOT nbm Netbeans Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java b/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java index 53b62f40..d44b97a1 100644 --- a/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java +++ b/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java @@ -177,7 +177,7 @@ public PtyProcess start(String workingDir, ClaudeProfile profile, String extraCl appendSessionFlags(cmd, workingDir, env, extraCliArgs, mode, resumeSessionId); - lastCommand = String.join(" ", cmd); + lastCommand = toShellCommand(cmd); LOG.info("Claude command: " + lastCommand); PtyProcessBuilder builder = new PtyProcessBuilder(cmd.toArray(new String[0])) .setEnvironment(env) @@ -616,6 +616,37 @@ static Map buildEnv(ClaudeProfile profile, java.nio.file.Path pr * Builds a minimal {@code settings.local.json} from scratch for the given port. * Used as a fallback when the existing file cannot be parsed. */ + /** + * Converts a command list to a shell-pasteable string. + * Arguments that contain spaces, quotes, or other shell-special characters + * are quoted so the result can be pasted directly into a terminal. + * On Windows, double-quotes inside an argument are doubled (""). + * On other platforms, they are backslash-escaped (\"). + */ + static String toShellCommand(List cmd) { + boolean windows = System.getProperty("os.name", "").toLowerCase().contains("win"); + return toShellCommand(cmd, windows); + } + + static String toShellCommand(List cmd, boolean windows) { + StringBuilder sb = new StringBuilder(); + for (String arg : cmd) { + if (sb.length() > 0) sb.append(' '); + boolean needsQuoting = arg.isEmpty() || arg.chars().anyMatch(c -> + c == ' ' || c == '\t' || c == '"' || c == '\'' || c == '\\' || + c == '{' || c == '}' || c == '(' || c == ')' || + c == '&' || c == '|' || c == '<' || c == '>'); + if (!needsQuoting) { + sb.append(arg); + } else if (windows) { + sb.append('"').append(arg.replace("\"", "\"\"")).append('"'); + } else { + sb.append('"').append(arg.replace("\"", "\\\"")).append('"'); + } + } + return sb.toString(); + } + /** * Builds the JSON string to pass as {@code --mcp-config} to the Claude CLI. */ diff --git a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java index ee679571..849ef7ac 100644 --- a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java +++ b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java @@ -3,6 +3,7 @@ import com.pty4j.PtyProcess; import io.github.nbclaudecodegui.settings.ClaudeProfile; import java.io.File; +import java.util.List; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -170,6 +171,52 @@ void testCleanupRemovesApiKeyHelperButKeepsUserContent() { assertTrue(cleaned.contains("myCustomKey"), "cleaned should preserve user content"); } + // ------------------------------------------------------------------------- + // toShellCommand + // ------------------------------------------------------------------------- + + @Test + void testToShellCommandNoQuoting() { + List cmd = List.of("claude", "--continue"); + // plain args need no quoting on either platform + assertEquals("claude --continue", ClaudeProcess.toShellCommand(cmd, false)); + assertEquals("claude --continue", ClaudeProcess.toShellCommand(cmd, true)); + } + + @Test + void testToShellCommandQuotesJsonArgUnix() { + String json = ClaudeProcess.buildMcpConfigJson(28991); + List cmd = List.of("claude", "--mcp-config", json, "--continue"); + String result = ClaudeProcess.toShellCommand(cmd, false); + // Expected: claude --mcp-config "{\"mcpServers\":...}" --continue + assertTrue(result.startsWith("claude --mcp-config \""), "should quote JSON arg"); + assertTrue(result.endsWith("\" --continue"), "should close quote before next arg"); + String inner = result.substring("claude --mcp-config \"".length(), + result.lastIndexOf("\" --continue")); + // On Unix all " inside must be escaped as \" + for (int i = 0; i < inner.length(); i++) { + if (inner.charAt(i) == '"') { + assertTrue(i > 0 && inner.charAt(i - 1) == '\\', + "unescaped double-quote at position " + i + " in: " + inner); + } + } + } + + @Test + void testToShellCommandQuotesJsonArgWindows() { + String json = ClaudeProcess.buildMcpConfigJson(28991); + List cmd = List.of("claude", "--mcp-config", json, "--continue"); + String result = ClaudeProcess.toShellCommand(cmd, true); + // Expected: claude --mcp-config "{""mcpServers"":...}" --continue + assertTrue(result.startsWith("claude --mcp-config \""), "should quote JSON arg"); + assertTrue(result.endsWith("\" --continue"), "should close quote before next arg"); + String inner = result.substring("claude --mcp-config \"".length(), + result.lastIndexOf("\" --continue")); + // On Windows all " inside must be doubled as "" + assertFalse(inner.contains("\\\""), "Windows quoting must not use backslash-escape"); + assertTrue(inner.contains("\"\""), "Windows quoting must double double-quotes"); + } + // ------------------------------------------------------------------------- // buildMcpConfigJson // -------------------------------------------------------------------------