diff --git a/src/main/commandRunner.ts b/src/main/commandRunner.ts index 613c13a..9bbe9e6 100644 --- a/src/main/commandRunner.ts +++ b/src/main/commandRunner.ts @@ -10,6 +10,19 @@ const ENV_WITH_DDEV_PATH = { const running = new Map() const cancelledIds = new Set() +// ddev colorizes its output unconditionally, regardless of TTY-ness, so a +// GUI panel rendering raw stdout/stderr ends up showing literal escape +// codes instead of color. Strip them before they ever reach the renderer. +const ANSI_PATTERN = new RegExp( + // eslint-disable-next-line no-control-regex -- intentional: matches ESC/BEL bytes in ddev's colorized output + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', + 'g' +) + +function stripAnsi(text: string): string { + return text.replace(ANSI_PATTERN, '') +} + export class CommandFailedError extends Error { constructor( message: string, @@ -37,7 +50,16 @@ export function runStreamed( options: { cwd?: string } = {} ): Promise { return new Promise((resolve, reject) => { - const child = spawn('ddev', args, { env: ENV_WITH_DDEV_PATH, cwd: options.cwd }) + // stdin must be closed, not just unused — an open-but-silent pipe + // leaves ddev blocked forever on any prompt it tries to read (e.g. the + // first-run telemetry opt-in), since a GUI-spawned child has no + // terminal to answer it. A closed stdin gets an immediate EOF instead, + // which ddev treats as "use the default" rather than hanging. + const child = spawn('ddev', args, { + env: ENV_WITH_DDEV_PATH, + cwd: options.cwd, + stdio: ['ignore', 'pipe', 'pipe'] + }) running.set(operationId, child) const tail: string[] = [] @@ -47,7 +69,7 @@ export function runStreamed( } child.stdout.on('data', (data: Buffer) => { - const chunk = data.toString() + const chunk = stripAnsi(data.toString()) trackTail(chunk) if (!sender.isDestroyed()) { sender.send('terminal:data', { operationId, stream: 'stdout', chunk }) @@ -55,7 +77,7 @@ export function runStreamed( }) child.stderr.on('data', (data: Buffer) => { - const chunk = data.toString() + const chunk = stripAnsi(data.toString()) trackTail(chunk) if (!sender.isDestroyed()) { sender.send('terminal:data', { operationId, stream: 'stderr', chunk }) @@ -105,12 +127,12 @@ export function cancelCommand(operationId: string): boolean { // stopping a log stream reuses cancelCommand — but pushes chunks over a // dedicated logs:data channel instead of terminal:data. export function startLogStream(operationId: string, args: string[], sender: WebContents): void { - const child = spawn('ddev', args, { env: ENV_WITH_DDEV_PATH }) + const child = spawn('ddev', args, { env: ENV_WITH_DDEV_PATH, stdio: ['ignore', 'pipe', 'pipe'] }) running.set(operationId, child) const forward = (stream: 'stdout' | 'stderr') => (data: Buffer) => { if (!sender.isDestroyed()) { - sender.send('logs:data', { operationId, stream, chunk: data.toString() }) + sender.send('logs:data', { operationId, stream, chunk: stripAnsi(data.toString()) }) } } child.stdout.on('data', forward('stdout')) diff --git a/src/renderer/src/components/create/CreateProjectModal.test.tsx b/src/renderer/src/components/create/projectName.test.ts similarity index 100% rename from src/renderer/src/components/create/CreateProjectModal.test.tsx rename to src/renderer/src/components/create/projectName.test.ts