Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 54 additions & 6 deletions core/tools/implementations/runTerminalCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import iconv from "iconv-lite";
import childProcess from "node:child_process";
import os from "node:os";
import { ContinueError, ContinueErrorReason } from "../../util/errors";
import * as path from "node:path";

// Default timeout for terminal commands (2 minutes)
const DEFAULT_TOOL_TIMEOUT_MS = 120_000;
Expand All @@ -21,8 +22,42 @@ function getDecodedOutput(data: Buffer): string {
} else {
return data.toString();
}
} // Simple helper function to use login shell on Unix/macOS and PowerShell on Windows
function getShellCommand(command: string): { shell: string; args: string[] } {
}

// Simple helper function to use login shell on Unix/macOS and PowerShell on Windows
// Detects WSL paths and uses the appropriate WSL shell
function getShellCommand(
command: string,
cwd?: string,
): { shell: string; args: string[] } {
// Check if the working directory is in WSL on Intellij
const isWslPath =
cwd && (cwd.includes("\\wsl.localhost\\") || cwd.includes("\\wsl$\\"));

if (isWslPath) {
// Extract the WSL distribution name from the path
// Path format: \\wsl.localhost\UbuntuAny\home\user\project
// or: \\wsl$\UbuntuAny\home\user\project
let distroName = "Ubuntu"; // Default fallback
try {
const wslPathSegments = cwd.split(/\\+/);
// Find the segment after wsl.localhost or wsl$
const wslIndex = wslPathSegments.findIndex(
(seg) => seg.includes("wsl.localhost") || seg.includes("wsl$"),
);
if (wslIndex !== -1 && wslIndex + 1 < wslPathSegments.length) {
distroName = wslPathSegments[wslIndex + 1];
}
} catch {
// If parsing fails, use default Ubuntu
}
// Use wsl.exe to run commands in the specific distribution
return {
shell: "wsl.exe",
args: ["-d", distroName, "-e", "bash", "-c", command],
};
}

if (process.platform === "win32") {
// Windows: Use PowerShell
return {
Expand Down Expand Up @@ -52,6 +87,17 @@ import { getBooleanArg, getStringArg } from "../parseArgs";
* Falls back to home directory or temp directory if no workspace is available.
*/
function resolveWorkingDirectory(workspaceDirs: string[]): string {
// Check for valid existing local paths first (IntelliJ support) so \\wsl$\ or \\wsl.local\ can pass
for (const dir of workspaceDirs) {
try {
if (dir.startsWith("file:////wsl")) {
return path.normalize(dir.replace("file://", ""));
}
} catch {
// Ignore errors (e.g. invalid path formats)
}
}

// Handle file:// URIs (local workspaces)
const fileWorkspaceDir = workspaceDirs.find((dir) =>
dir.startsWith("file:/"),
Expand Down Expand Up @@ -145,7 +191,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => {
}

// Use spawn with color environment
const { shell, args } = getShellCommand(command);
const { shell, args } = getShellCommand(command, cwd);
const childProc = childProcess.spawn(shell, args, {
cwd,
env: getColorEnv(), // Add enhanced environment for colors
Expand Down Expand Up @@ -376,7 +422,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => {
try {
// Use spawn approach for consistency with streaming version
const { shell: nonStreamingShell, args: nonStreamingArgs } =
getShellCommand(command);
getShellCommand(command, cwd);
const output = await new Promise<{ stdout: string; stderr: string }>(
(resolve, reject) => {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
Expand Down Expand Up @@ -503,8 +549,10 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => {
// but don't attach any listeners other than error
try {
// Use spawn with color environment
const { shell: detachedShell, args: detachedArgs } =
getShellCommand(command);
const { shell: detachedShell, args: detachedArgs } = getShellCommand(
command,
cwd,
);
const childProc = childProcess.spawn(detachedShell, detachedArgs, {
cwd,
env: getColorEnv(), // Add color environment
Expand Down
9 changes: 9 additions & 0 deletions extensions/intellij/.run/Run Continue WSL (CE).run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Continue WSL (CE)" type="CompoundRunConfigurationType">
<toRun name="Run Extension WSL (use TCP)" type="GradleRunConfiguration" />
<toRun name="Prompt Logs" type="ShConfigurationType" />
<toRun name="Start Core Dev Server (CE)" type="ShConfigurationType" />
<toRun name="Start GUI Dev Server" type="ShConfigurationType" />
<method v="2" />
</configuration>
</component>
10 changes: 10 additions & 0 deletions extensions/intellij/.run/Run Continue WSL.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Continue WSL" type="CompoundRunConfigurationType">
<toRun name="Run Extension WSL (use TCP)" type="GradleRunConfiguration" />
<toRun name="Start Core Dev Server" type="NodeJSConfigurationType" />
<toRun name="IDE Logs" type="ShConfigurationType" />
<toRun name="Prompt Logs" type="ShConfigurationType" />
<toRun name="Start GUI Dev Server" type="ShConfigurationType" />
<method v="2" />
</configuration>
</component>
31 changes: 31 additions & 0 deletions extensions/intellij/.run/Run Extension WSL (use TCP).run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Extension WSL (use TCP)" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="GUI_URL" value="http://localhost:5173/jetbrains_index.html" />
<entry key="WSL_KERNEL" value="Ubuntu" />
<entry key="USE_TCP" value="true" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/extensions/intellij" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runIde" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>
74 changes: 73 additions & 1 deletion extensions/intellij/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,49 @@ tasks {
}

runIde {
val openProject = "$projectDir/../../manual-testing-sandbox"
val wslKernel = environment("WSL_KERNEL").getOrElse("")
val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1")
val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024
val currentDrive = projectDir.absolutePath.first().lowercase()

val openProject = if (wslKernel.isNotEmpty()) {
//If gradle.properties >= 2025.X -- can direct run mount from WSL windows path in Intellij
// instead of copy to /tmp -- IE \\wsl?\Ubuntu\mnt\c\Users
if (majorVersion >= 2025) {
"\\\\wsl$\\$wslKernel\\mnt\\$currentDrive" + projectDir.absolutePath.substring(2).replace("/extensions/intellij/", "")
.replace("/", "\\") + "\\..\\..\\manual-testing-sandbox"
}
//For now we must copy the test file to WSL filesystem to be cleaned up at exit
else {
"\\\\wsl$\\$wslKernel\\tmp\\manual-testing-sandbox"
}
} else {
"$projectDir/../../manual-testing-sandbox"
}

doFirst {
if (wslKernel.isNotEmpty() && majorVersion < 2025) {
val wslSourcePath = "/mnt/$currentDrive" + projectDir.absolutePath.substring(2)
.replace("\\", "/") + "/../../manual-testing-sandbox"
val wslDestPath = "/tmp/manual-testing-sandbox"
val command = listOf("wsl", "-d", wslKernel, "-e", "cp", "-r", wslSourcePath, wslDestPath)
val process = ProcessBuilder(command).start()
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
val exitCode = process.waitFor()
if (exitCode != 0) {
throw GradleException(
"Failed to copy $wslSourcePath to $wslKernel:$wslDestPath\" via WSL: ${
process.errorStream.bufferedReader().readText()
}"
)
} else {
logger.lifecycle("Successfully copied $wslSourcePath to $wslKernel:$wslDestPath")
}
}
}
argumentProviders += CommandLineArgumentProvider {
listOf(openProject, "$openProject/test.kt")
}
finalizedBy("cleanupAfterRunIde")
}

test {
Expand All @@ -132,6 +171,39 @@ tasks {
}
}

tasks.register("cleanupAfterRunIde") {
doLast {
val pluginVersion = providers.gradleProperty("platformVersion").getOrElse("2024.1")
val majorVersion = pluginVersion.split(".").firstOrNull()?.toIntOrNull() ?: 2024
//cleanup the temp file created in runIde if WSL test was used
if (majorVersion < 2025) {
val wslKernel = environment("WSL_KERNEL").getOrElse("")
if (wslKernel.isNotEmpty()) {
val wslDestPath = "/tmp/manual-testing-sandbox"
logger.lifecycle("Cleaning up WSL temp file: $wslDestPath in $wslKernel")
val command = listOf("wsl", "-d", wslKernel, "-e", "rm", "-rf", wslDestPath)
try {
val process = ProcessBuilder(command).start()
val exitCode = process.waitFor()
if (exitCode != 0) {
logger.lifecycle(
"Warning: Failed to cleanup WSL temp file: ${
process.errorStream.bufferedReader().readText()
}"
)
} else {
logger.lifecycle("Successfully cleaned up $wslDestPath")
}
} catch (e: Exception) {
logger.lifecycle("Error during cleanup: ${e.message}")
}

}
logger.lifecycle("IDE stopped. Performing cleanup...")
}
}
}

val testIntegration = task<Test>("testIntegration") {
val integrationTestSourceSet = sourceSets.getByName("testIntegration")
testClassesDirs = integrationTestSourceSet.output.classesDirs
Expand Down
Loading