Skip to content

Commit 254808a

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 5dd4daf commit 254808a

3 files changed

Lines changed: 60 additions & 19 deletions

File tree

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

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

99
import type { ChildProcess } from 'child_process';
10-
import type { Host } from './host';
10+
import { createInterface } from 'node:readline';
11+
import { stripVTControlCharacters } from 'node:util';
12+
import { type Host } from './host';
1113

1214
// Log messages that we want to catch to identify the build status.
1315

@@ -122,13 +124,27 @@ export class LocalDevserver implements Devserver {
122124
stdio: 'pipe',
123125
cwd: this.workspacePath,
124126
});
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', () => {
127+
if (this.devserverProcess.stdout) {
128+
const rl = createInterface({ input: this.devserverProcess.stdout, terminal: false });
129+
rl.on('line', (line) => {
130+
const cleanLine = stripVTControlCharacters(line).replace(/\r/g, '').trim();
131+
if (cleanLine.length > 0) {
132+
this.addLog(cleanLine);
133+
}
134+
});
135+
}
136+
137+
if (this.devserverProcess.stderr) {
138+
const rl = createInterface({ input: this.devserverProcess.stderr, terminal: false });
139+
rl.on('line', (line) => {
140+
const cleanLine = stripVTControlCharacters(line).replace(/\r/g, '').trim();
141+
if (cleanLine.length > 0) {
142+
this.addLog(cleanLine);
143+
}
144+
});
145+
}
146+
147+
this.devserverProcess.on('close', () => {
132148
this.stop();
133149
});
134150
this.buildInProgress = true;

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

Lines changed: 22 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,26 @@ 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+
197+
if (childProcess.stdout) {
198+
const rl = createInterface({ input: childProcess.stdout, terminal: false });
199+
rl.on('line', (line) => {
200+
const cleanLine = stripVTControlCharacters(line).replace(/\r/g, '').trim();
201+
if (cleanLine.length > 0) {
202+
logs.push(cleanLine);
203+
}
204+
});
205+
}
206+
207+
if (childProcess.stderr) {
208+
const rl = createInterface({ input: childProcess.stderr, terminal: false });
209+
rl.on('line', (line) => {
210+
const cleanLine = stripVTControlCharacters(line).replace(/\r/g, '').trim();
211+
if (cleanLine.length > 0) {
212+
logs.push(cleanLine);
213+
}
214+
});
215+
}
196216

197217
childProcess.on('close', (code) => {
198218
if (code === 0) {

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)