Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.
Merged
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
31 changes: 9 additions & 22 deletions src/shared/utils/ddev-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,7 @@ export class DdevUtils {
*/
public static isDdevRunning(workspacePath: string): boolean {
try {
execSync('ddev exec echo "test"', {
cwd: workspacePath,
stdio: 'ignore'
});
this.execDdev('echo "test"', workspacePath);
return true;
} catch (error) {
return false;
Expand All @@ -83,10 +80,7 @@ export class DdevUtils {
*/
public static isToolInstalled(toolName: string, workspacePath: string): boolean {
try {
execSync(`ddev exec ${toolName} --version`, {
cwd: workspacePath,
stdio: 'ignore'
});
this.execDdev(`${toolName} --version`, workspacePath);
return true;
} catch (error) {
return false;
Expand All @@ -112,25 +106,14 @@ export class DdevUtils {

// Try to run the tool
try {
execSync(`ddev exec ${toolName} --version`, {
cwd: workspacePath,
stdio: 'ignore'
});
this.execDdev(`${toolName} --version`, workspacePath);

return {
isValid: true
};
} catch (error: any) {
// Try to get more specific error information
let errorDetails = '';
try {
execSync(`ddev exec ${toolName} --version`, {
cwd: workspacePath,
encoding: 'utf-8'
});
} catch (execError: any) {
errorDetails = execError.message || execError.stderr || '';
}
const errorDetails = error.message || error.stderr || '';

// Build concise but informative error message
let userMessage = `${toolName.toUpperCase()} not available`;
Expand Down Expand Up @@ -201,8 +184,12 @@ export class DdevUtils {
* @throws Error if the command fails
*/
public static execDdev(command: string, workspacePath: string): string {
// Escape single quotes in the command to prevent breaking the bash -c string
const escapedCommand = command.replace(/'/g, "'\\''");
const wrappedCommand = `bash -c 'XDEBUG_MODE=off ${escapedCommand}'`;

try {
return execSync(`ddev exec ${command}`, {
return execSync(`ddev exec ${wrappedCommand}`, {
cwd: workspacePath,
encoding: 'utf-8'
});
Expand Down
33 changes: 26 additions & 7 deletions src/test/ddev-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ suite('DdevUtils Test Suite', () => {

assert.strictEqual(result, true);
assert.strictEqual(execSyncStub.calledOnce, true);
// Verify it uses execDdev wrapper
const callArgs = execSyncStub.firstCall.args;
assert.ok(callArgs[0].includes("bash -c 'XDEBUG_MODE=off echo \"test\"'"));
});

test('isDdevRunning returns false when DDEV container is not running', () => {
Expand All @@ -78,6 +81,9 @@ suite('DdevUtils Test Suite', () => {

assert.strictEqual(result, true);
assert.strictEqual(execSyncStub.calledOnce, true);
// Verify it uses execDdev wrapper
const callArgs = execSyncStub.firstCall.args;
assert.ok(callArgs[0].includes("bash -c 'XDEBUG_MODE=off phpmd --version'"));
});

test('isToolInstalled returns false when tool is not available', () => {
Expand Down Expand Up @@ -113,19 +119,17 @@ suite('DdevUtils Test Suite', () => {
test('validateDdevTool returns error message for DDEV issues', () => {
// First call (hasDdevProject) succeeds
execSyncStub.onFirstCall().returns('exists\n');
// Second call (tool version check) fails
execSyncStub.onSecondCall().throws(new Error('Tool not available'));
// Third call (error details) returns error
execSyncStub.onThirdCall().throws({
message: 'DDEV project not currently running',
stderr: 'not currently running'
});
// Second call (tool version check) fails with specific error
const error = new Error('DDEV project not currently running') as any;
error.stderr = 'not currently running';
execSyncStub.onSecondCall().throws(error);

const result = DdevUtils.validateDdevTool('phpmd', '/test/workspace');

assert.strictEqual(result.isValid, false);
assert.strictEqual(result.errorType, 'unknown');
assert.ok(result.userMessage?.includes('PHPMD not available'));
assert.ok(result.userMessage?.includes('DDEV appears to be stopped'));
});

test('execDdev returns output when command succeeds', () => {
Expand All @@ -136,6 +140,9 @@ suite('DdevUtils Test Suite', () => {

assert.strictEqual(result, expectedOutput);
assert.strictEqual(execSyncStub.calledOnce, true);

const callArgs = execSyncStub.firstCall.args;
assert.ok(callArgs[0].includes("bash -c 'XDEBUG_MODE=off phpmd test.php json cleancode'"));
});

test('execDdev returns stdout when command fails but has output', () => {
Expand All @@ -156,4 +163,16 @@ suite('DdevUtils Test Suite', () => {
DdevUtils.execDdev('phpmd test.php json cleancode', '/test/workspace');
}, /Command failed/);
});

test('execDdev escapes single quotes in command', () => {
execSyncStub.returns('output');

DdevUtils.execDdev("echo 'hello'", '/test/workspace');

assert.strictEqual(execSyncStub.calledOnce, true);
const callArgs = execSyncStub.firstCall.args;
// echo 'hello' -> echo '\''hello'\''
// wrapped: bash -c 'XDEBUG_MODE=off echo '\''hello'\'''
assert.ok(callArgs[0].includes("echo '\\''hello'\\''"));
});
});