diff --git a/workspaces/ballerina/ballerina-extension/src/core/extension.ts b/workspaces/ballerina/ballerina-extension/src/core/extension.ts index 7565c7ee696..385a28af002 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extension.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extension.ts @@ -40,6 +40,7 @@ import { outputChannel, isWindows, isWSL, + quoteShellPath, isSupportedVersion, VERSION, isSupportedSLVersion, @@ -1819,7 +1820,8 @@ export class BallerinaExtension { debug("[VERSION] Non-Windows platform detected, no extension needed"); } - let ballerinaCommand = distPath + 'bal' + exeExtension + ' version'; + // Build the executable path separately so we can quote it for shell execution + let balExecutablePath = distPath + 'bal' + exeExtension; // Handle WSL environment - prefer native Linux installation over Windows .bat files if (isWSL()) { @@ -1829,26 +1831,28 @@ export class BallerinaExtension { // Check if 'bal' command is available in PATH execSync('which bal', { encoding: 'utf8', timeout: 5000 }); // If we get here, 'bal' is available, use it instead of .bat - ballerinaCommand = 'bal version'; + balExecutablePath = 'bal'; debug("[VERSION] WSL detected native 'bal' command, using it instead of .bat file"); } catch (error) { debug("[VERSION] No native 'bal' command found in WSL, will try .bat file"); // If the path contains Windows-style paths, we need to handle them properly - if (ballerinaCommand.includes('\\') || ballerinaCommand.match(/^[A-Za-z]:/)) { + if (balExecutablePath.includes('\\') || balExecutablePath.match(/^[A-Za-z]:/)) { debug("[VERSION] WSL detected with Windows path, attempting to convert to WSL path"); // Try to convert Windows path to WSL path - const wslPath = ballerinaCommand.replace(/^([A-Za-z]):/, '/mnt/$1').replace(/\\/g, '/').toLowerCase(); - debug(`[VERSION] Converted Windows path to WSL path: ${wslPath}`); - ballerinaCommand = wslPath; + balExecutablePath = balExecutablePath.replace(/^([A-Za-z]):/, '/mnt/$1').replace(/\\/g, '/').toLowerCase(); + debug(`[VERSION] Converted Windows path to WSL path: ${balExecutablePath}`); } } } else { // We have a native Linux installation, use it directly - ballerinaCommand = 'bal version'; + balExecutablePath = 'bal'; debug("[VERSION] WSL detected with native Linux installation, using 'bal version'"); } } + // Quote the executable path to handle spaces in directory names + let ballerinaCommand = `${quoteShellPath(balExecutablePath)} version`; + debug(`[VERSION] Executing command: '${ballerinaCommand}'`); let ballerinaExecutor = ''; diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts index b92930a7272..4c9774c9834 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts @@ -19,6 +19,7 @@ import { z } from 'zod'; import child_process from 'child_process'; import { CopilotEventHandler } from '../../utils/events'; import { extension } from '../../../../BalExtensionContext'; +import { quoteShellPath } from '../../../../utils/config'; import { DIAGNOSTICS_TOOL_NAME } from './diagnostics'; export const TEST_RUNNER_TOOL_NAME = "runTests"; @@ -99,7 +100,7 @@ function parseTestSummary(output: string): string { async function runBallerinaTests(cwd: string): Promise { return new Promise((resolve) => { const balCmd = extension.ballerinaExtInstance.getBallerinaCmd(); - const command = `${balCmd} test`; + const command = `${quoteShellPath(balCmd)} test`; console.log(`[TestRunner] Running: ${command} in ${cwd}`); diff --git a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts index fbe1b8472e6..03941c2a0d3 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts @@ -48,6 +48,7 @@ import { createVersionNumber } from "../../utils"; import { getProjectWorkingDirectory } from "../../utils/file-utils"; +import { quoteShellPath } from "../../utils/config"; import { decimal, ExecutableOptions } from 'vscode-languageclient/node'; import { BAL_NOTEBOOK, getTempFile, NOTEBOOK_CELL_SCHEME } from '../../views/notebook'; import fileUriToPath from 'file-uri-to-path'; @@ -603,7 +604,8 @@ class BallerinaDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFa } getScriptPath(args: string[]): string { args.push('start-debugger-adapter'); - return extension.ballerinaExtInstance.getBallerinaCmd(); + // Quote the path to handle spaces in directory names (used with shell: true) + return quoteShellPath(extension.ballerinaExtInstance.getBallerinaCmd()); } getCurrentWorkingDir(): string { return path.join(extension.ballerinaExtInstance.ballerinaHome, "bin"); @@ -662,7 +664,7 @@ class BIRunAdapter extends LoggingDebugSession { task: 'run' }; - let runCommand: string = `${extension.ballerinaExtInstance.getBallerinaCmd()} run`; + let runCommand: string = `${quoteShellPath(extension.ballerinaExtInstance.getBallerinaCmd())} run`; const programArgs = (args as any).programArgs; if (programArgs && programArgs.length > 0) { diff --git a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts index ec8018823d2..52383dae9fe 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts @@ -18,7 +18,7 @@ import { BallerinaProject } from "@wso2/ballerina-core"; import { Terminal, window, workspace } from "vscode"; -import { isSupportedSLVersion, isWindows, createVersionNumber } from "../../../utils"; +import { isSupportedSLVersion, isWindows, createVersionNumber, quoteShellPath } from "../../../utils"; import { extension } from "../../../BalExtensionContext"; import { TracerMachine } from "../../../features/tracing"; @@ -128,7 +128,7 @@ export function runCommandWithConf(file: BallerinaProject | string, executor: st } let commandText; if (cmd === BALLERINA_COMMANDS.OTHER) { - commandText = `${executor} ${argsList}`; + commandText = `${quoteShellPath(executor)} ${argsList}`; terminal = window.createTerminal({ name: TERMINAL_NAME }); } else { let env = {}; @@ -164,7 +164,7 @@ export function runCommandWithConf(file: BallerinaProject | string, executor: st } } - commandText = `${executor} ${cmd} ${argsList}`; + commandText = `${quoteShellPath(executor)} ${cmd} ${argsList}`; if (confPath !== '') { const configs = env['BAL_CONFIG_FILES'] ? `${env['BAL_CONFIG_FILES']}:${confPath}` : confPath; Object.assign(env, { BAL_CONFIG_FILES: configs }); diff --git a/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts b/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts index b136c9560ae..877f05992d2 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts @@ -34,6 +34,7 @@ import { BALLERINA_COMMANDS } from "../project"; import { discoverTests, gatherTestItems } from "./discover"; import { testController, projectRoot } from "./activator"; import { extension } from "../../BalExtensionContext"; +import { quoteShellPath } from "../../utils/config"; enum EXEC_ARG { TESTS = '--tests', @@ -72,7 +73,7 @@ export function runHandler(request: TestRunRequest, cancellation: CancellationTo try { // execute test const executor = extension.ballerinaExtInstance.getBallerinaCmd(); - const commandText = `${executor} ${BALLERINA_COMMANDS.TEST} ${EXEC_ARG.TESTS} ${testNames} ${EXEC_ARG.COVERAGE}`; + const commandText = `${quoteShellPath(executor)} ${BALLERINA_COMMANDS.TEST} ${EXEC_ARG.TESTS} ${testNames} ${EXEC_ARG.COVERAGE}`; await runCommand(commandText, projectRoot); } catch { diff --git a/workspaces/ballerina/ballerina-extension/src/utils/config.ts b/workspaces/ballerina/ballerina-extension/src/utils/config.ts index de73c71053e..c802db2727d 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/config.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/config.ts @@ -73,6 +73,27 @@ export function isWSL(): boolean { ); } +/** + * Wraps a file path in double quotes if it contains spaces, + * so it can be safely used in shell command strings. + * Handles already-quoted paths and escapes embedded double quotes. + */ +export function quoteShellPath(filePath: string): string { + // Strip existing surrounding quotes to normalize + let normalized = filePath; + if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) { + normalized = normalized.slice(1, -1); + } + + if (!normalized.includes(' ')) { + return normalized; + } + + // Escape any embedded double quotes + const escaped = normalized.replace(/"/g, '\\"'); + return `"${escaped}"`; +} + export function isSupportedVersion(ballerinaExtInstance: BallerinaExtension, supportedRelease: VERSION, supportedVersion: number): boolean { const ballerinaVersion: string = ballerinaExtInstance.ballerinaVersion.toLocaleLowerCase(); diff --git a/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts b/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts index 02da6e72759..317fcb0f163 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts @@ -22,6 +22,7 @@ import { extension } from "../BalExtensionContext"; import { RPCLayer } from "../RPCLayer"; import { VisualizerWebview } from "../views/visualizer/webview"; import { debug } from "./logger"; +import { quoteShellPath } from "./config"; const PROGRESS_COMPLETE = 100; @@ -53,7 +54,7 @@ export async function pullMigrationTool(migrationToolName: string, version: stri } const ballerinaCmd = extension.ballerinaExtInstance.getBallerinaCmd(); - const command = `${ballerinaCmd} tool pull ${migrationToolName}:${version}`; + const command = `${quoteShellPath(ballerinaCmd)} tool pull ${migrationToolName}:${version}`; debug(`Executing migration tool pull command: ${command}`); // 2. This function now returns a promise that wraps the exec lifecycle