Skip to content

Commit dfa82ec

Browse files
committed
refactor(@angular/cli): implement native stream line-buffering & VT removal for MCP logs
Refactor the process log capturing mechanism in host.ts and devserver.ts to natively line-buffer and sanitize process stdout and stderr streams using Node's native readline `createInterface` API and `util.stripVTControlCharacters`. This ensures all command and devserver logs are cleanly line-split, trimmed, and stripped of VT/ANSI color sequences and carriage returns.
1 parent d32bfd9 commit dfa82ec

3 files changed

Lines changed: 45 additions & 19 deletions

File tree

packages/angular/cli/src/commands/mcp/devserver.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import type { ChildProcess } from 'child_process';
10-
import type { Host } from './host';
10+
import { type Host, processStreamLines } from './host';
1111

1212
// Log messages that we want to catch to identify the build status.
1313

@@ -122,13 +122,10 @@ export class LocalDevserver implements Devserver {
122122
stdio: 'pipe',
123123
cwd: this.workspacePath,
124124
});
125-
this.devserverProcess.stdout?.on('data', (data) => {
126-
this.addLog(data.toString());
127-
});
128-
this.devserverProcess.stderr?.on('data', (data) => {
129-
this.addLog(data.toString());
130-
});
131-
this.devserverProcess.stderr?.on('close', () => {
125+
processStreamLines(this.devserverProcess.stdout, (line) => this.addLog(line));
126+
processStreamLines(this.devserverProcess.stderr, (line) => this.addLog(line));
127+
128+
this.devserverProcess.on('close', () => {
132129
this.stop();
133130
});
134131
this.buildInProgress = true;

packages/angular/cli/src/commands/mcp/host.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { glob as nodeGlob, readFile as nodeReadFile, stat } from 'node:fs/promis
2020
import { createRequire } from 'node:module';
2121
import { createServer } from 'node:net';
2222
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
23+
import { createInterface } from 'node:readline';
24+
import { stripVTControlCharacters } from 'node:util';
2325

2426
/**
2527
* An error thrown when a command fails to execute.
@@ -191,8 +193,8 @@ export const LocalWorkspaceHost: Host = {
191193
});
192194

193195
const logs: string[] = [];
194-
childProcess.stdout?.on('data', (data) => logs.push(data.toString()));
195-
childProcess.stderr?.on('data', (data) => logs.push(data.toString()));
196+
processStreamLines(childProcess.stdout, (line) => logs.push(line));
197+
processStreamLines(childProcess.stderr, (line) => logs.push(line));
196198

197199
childProcess.on('close', (code) => {
198200
if (code === 0) {
@@ -381,3 +383,25 @@ export function createRootRestrictedHost(
381383
},
382384
};
383385
}
386+
387+
/**
388+
* Binds a readline interface to the given stream to process each line.
389+
* Sanitizes lines by removing VT/ANSI control characters, trimming trailing whitespace,
390+
* and preserving leading indentation.
391+
*/
392+
export function processStreamLines(
393+
stream: NodeJS.ReadableStream | undefined | null,
394+
lineCallback: (line: string) => void,
395+
): void {
396+
if (!stream) {
397+
return;
398+
}
399+
400+
const rl = createInterface({ input: stream, terminal: false });
401+
rl.on('line', (line) => {
402+
const cleanLine = stripVTControlCharacters(line).trimEnd();
403+
if (cleanLine.length > 0) {
404+
lineCallback(cleanLine);
405+
}
406+
});
407+
}

packages/angular/cli/src/commands/mcp/tools/devserver/devserver_spec.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ import { startDevserver } from './devserver-start';
1818
import { stopDevserver } from './devserver-stop';
1919
import { WATCH_DELAY, waitForDevserverBuild } from './devserver-wait-for-build';
2020

21+
class MockStream extends EventEmitter {
22+
resume = jasmine.createSpy('resume').and.returnValue(this);
23+
pause = jasmine.createSpy('pause').and.returnValue(this);
24+
}
25+
2126
class MockChildProcess extends EventEmitter {
22-
stdout = new EventEmitter();
23-
stderr = new EventEmitter();
27+
stdout = new MockStream();
28+
stderr = new MockStream();
2429
kill = jasmine.createSpy('kill');
2530
}
2631

@@ -95,10 +100,10 @@ describe('Serve Tools', () => {
95100
const waitPromise = waitForDevserverBuild({ timeout: 10 }, mockContext);
96101

97102
// Simulate build logs.
98-
mockProcess.stdout.emit('data', '... building ...');
99-
mockProcess.stdout.emit('data', '✔ Changes detected. Rebuilding...');
100-
mockProcess.stdout.emit('data', '... more logs ...');
101-
mockProcess.stdout.emit('data', 'Application bundle generation complete.');
103+
mockProcess.stdout.emit('data', '... building ...\n');
104+
mockProcess.stdout.emit('data', '✔ Changes detected. Rebuilding...\n');
105+
mockProcess.stdout.emit('data', '... more logs ...\n');
106+
mockProcess.stdout.emit('data', 'Application bundle generation complete.\n');
102107

103108
const waitResult = await waitPromise;
104109
expect(waitResult.structuredContent.status).toBe('success');
@@ -161,7 +166,7 @@ describe('Serve Tools', () => {
161166
await startDevserver({ project: 'crash-app' }, mockContext);
162167

163168
// Simulate a crash with exit code 1
164-
mockProcess.stdout.emit('data', 'Fatal error.');
169+
mockProcess.stdout.emit('data', 'Fatal error.\n');
165170
mockProcess.emit('close', 1);
166171

167172
const stopResult = await stopDevserver({ project: 'crash-app' }, mockContext);
@@ -185,7 +190,7 @@ describe('Serve Tools', () => {
185190
await startDevserver({}, mockContext);
186191

187192
// Immediately simulate a build starting so isBuilding() is true.
188-
mockProcess.stdout.emit('data', '❯ Changes detected. Rebuilding...');
193+
mockProcess.stdout.emit('data', '❯ Changes detected. Rebuilding...\n');
189194

190195
const waitPromise = waitForDevserverBuild({ timeout: 5 * WATCH_DELAY }, mockContext);
191196

@@ -199,7 +204,7 @@ describe('Serve Tools', () => {
199204
jasmine.clock().tick(WATCH_DELAY + 1);
200205

201206
// Now finish the build.
202-
mockProcess.stdout.emit('data', 'Application bundle generation complete.');
207+
mockProcess.stdout.emit('data', 'Application bundle generation complete.\n');
203208

204209
// Tick past another debounce to exit the loop.
205210
jasmine.clock().tick(WATCH_DELAY + 1);

0 commit comments

Comments
 (0)