feat(cli): migrate console calls to debugLogger and stdioHelpers (M3 Phase 7-9)

Route CLI console.* calls to structured logging:
- Debug/internal diagnostics → debugLogger (logfile)
- User-facing output → writeStdoutLine/writeStderrLine/clearScreen (stdioHelpers)
- Add stdioHelpers.ts with writeStdoutLine, writeStderrLine, clearScreen
- Migrate pre-session files (gemini.tsx, sandbox.ts, config.ts) to stdioHelpers
- Migrate extension/MCP commands to stdioHelpers
- Migrate non-interactive session/control to debugLogger
- Migrate UI hooks and components to debugLogger
This commit is contained in:
tanzhenxin 2026-01-26 15:02:37 +08:00
parent 45df0e8b82
commit 7995c65571
82 changed files with 606 additions and 485 deletions

View file

@ -5,6 +5,7 @@
*/
import process from 'node:process';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
export enum AttentionNotificationReason {
ToolApproval = 'tool_approval',
@ -17,6 +18,7 @@ export interface TerminalNotificationOptions {
}
const TERMINAL_BELL = '\u0007';
const debugLogger = createDebugLogger('ATTENTION_NOTIFICATION');
/**
* Grabs the user's attention by emitting the terminal bell character.
@ -43,7 +45,7 @@ export function notifyTerminalAttention(
stream.write(TERMINAL_BELL);
return true;
} catch (error) {
console.warn('Failed to send terminal bell:', error);
debugLogger.warn('Failed to send terminal bell:', error);
return false;
}
}

View file

@ -6,6 +6,7 @@
import * as fs from 'node:fs';
import { parse, stringify } from 'comment-json';
import { writeStderrLine } from './stdioHelpers.js';
/**
* Updates a JSON file while preserving comments and formatting.
@ -25,8 +26,9 @@ export function updateSettingsFilePreservingFormat(
try {
parsed = parse(originalContent) as Record<string, unknown>;
} catch (error) {
console.error('Error parsing settings file:', error);
console.error(
writeStderrLine('Error parsing settings file.');
writeStderrLine(error instanceof Error ? error.message : String(error));
writeStderrLine(
'Settings file may be corrupted. Please check the JSON syntax.',
);
return;

View file

@ -12,7 +12,11 @@ import {
FatalTurnLimitedError,
FatalCancellationError,
ToolErrorType,
createDebugLogger,
} from '@qwen-code/qwen-code-core';
import { writeStderrLine } from './stdioHelpers.js';
const debugLogger = createDebugLogger('CLI_ERRORS');
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
@ -101,10 +105,10 @@ export function handleError(
errorCode,
);
console.error(formattedError);
writeStderrLine(formattedError);
process.exit(getNumericExitCode(errorCode));
} else {
console.error(errorMessage);
writeStderrLine(errorMessage);
throw error;
}
}
@ -143,12 +147,9 @@ export function handleToolError(
process.stderr.write(warningMessage);
}
// Always log detailed error in debug mode
if (config.getDebugMode()) {
console.error(
`Error executing tool ${toolName}: ${resultDisplay || toolError.message}`,
);
}
debugLogger.error(
`Error executing tool ${toolName}: ${resultDisplay || toolError.message}`,
);
}
/**
@ -164,10 +165,10 @@ export function handleCancellationError(config: Config): never {
cancellationError.exitCode,
);
console.error(formattedError);
writeStderrLine(formattedError);
process.exit(cancellationError.exitCode);
} else {
console.error(cancellationError.message);
writeStderrLine(cancellationError.message);
process.exit(cancellationError.exitCode);
}
}
@ -187,10 +188,10 @@ export function handleMaxTurnsExceededError(config: Config): never {
maxTurnsError.exitCode,
);
console.error(formattedError);
writeStderrLine(formattedError);
process.exit(maxTurnsError.exitCode);
} else {
console.error(maxTurnsError.message);
writeStderrLine(maxTurnsError.message);
process.exit(maxTurnsError.exitCode);
}
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { isGitRepository } from '@qwen-code/qwen-code-core';
import { createDebugLogger, isGitRepository } from '@qwen-code/qwen-code-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as childProcess from 'node:child_process';
@ -21,6 +21,8 @@ export enum PackageManager {
UNKNOWN = 'unknown',
}
const debugLogger = createDebugLogger('INSTALLATION_INFO');
export interface InstallationInfo {
packageManager: PackageManager;
isGlobal: boolean;
@ -170,7 +172,7 @@ export function getInstallationInfo(
: 'Installed with npm. Attempting to automatically update now...',
};
} catch (error) {
console.log(error);
debugLogger.error('Failed to detect installation info:', error);
return { packageManager: PackageManager.UNKNOWN, isGlobal: false };
}
}

View file

@ -13,6 +13,7 @@ import {
type ProviderModelConfig,
} from '@qwen-code/qwen-code-core';
import type { Settings } from '../config/settings.js';
import { writeStderrLine } from './stdioHelpers.js';
export interface CliGenerationConfigInputs {
argv: {
@ -131,7 +132,7 @@ export function resolveCliGenerationConfig(
// Log warnings if any
for (const warning of resolved.warnings) {
console.warn(warning);
writeStderrLine(warning);
}
// Resolve OpenAI logging config (CLI-specific, not part of core resolver)

View file

@ -211,12 +211,10 @@ async function loadSlashCommandNames(
// Extract command names and sort
return commands.map((cmd) => cmd.name).sort();
} catch (error) {
if (config.getDebugMode()) {
console.error(
'[buildSystemMessage] Failed to load slash commands:',
error,
);
}
debugLogger.error(
'[buildSystemMessage] Failed to load slash commands:',
error,
);
return [];
} finally {
controller.abort();
@ -272,9 +270,7 @@ export async function buildSystemMessage(
const subagents = await subagentManager.listSubagents();
agentNames = subagents.map((subagent) => subagent.name);
} catch (error) {
if (config.getDebugMode()) {
console.error('[buildSystemMessage] Failed to load subagents:', error);
}
debugLogger.error('[buildSystemMessage] Failed to load subagents:', error);
}
const systemMessage: CLISystemMessage = {

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { writeStderrLine } from './stdioHelpers.js';
export async function readStdin(): Promise<string> {
const MAX_STDIN_SIZE = 8 * 1024 * 1024; // 8MB
return new Promise((resolve, reject) => {
@ -30,7 +32,7 @@ export async function readStdin(): Promise<string> {
if (totalSize + chunk.length > MAX_STDIN_SIZE) {
const remainingSize = MAX_STDIN_SIZE - totalSize;
data += chunk.slice(0, remainingSize);
console.warn(
writeStderrLine(
`Warning: stdin input truncated to ${MAX_STDIN_SIZE} bytes.`,
);
process.stdin.destroy(); // Stop reading further

View file

@ -6,6 +6,7 @@
import { spawn } from 'node:child_process';
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
import { writeStderrLine } from './stdioHelpers.js';
export async function relaunchOnExitCode(runner: () => Promise<number>) {
while (true) {
@ -17,7 +18,8 @@ export async function relaunchOnExitCode(runner: () => Promise<number>) {
}
} catch (error) {
process.stdin.resume();
console.error('Fatal error: Failed to relaunch the CLI process.', error);
writeStderrLine('Fatal error: Failed to relaunch the CLI process.');
writeStderrLine(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}

View file

@ -19,6 +19,7 @@ import type { Config, SandboxConfig } from '@qwen-code/qwen-code-core';
import { FatalSandboxError } from '@qwen-code/qwen-code-core';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
import { randomBytes } from 'node:crypto';
import { writeStdoutLine, writeStderrLine } from './stdioHelpers.js';
const execAsync = promisify(exec);
@ -81,7 +82,7 @@ async function shouldUseCurrentUserInSandbox(): Promise<boolean> {
);
if (debugEnv) {
// Use stderr so it doesn't clutter normal STDOUT output (e.g. in `--prompt` runs).
console.error(
writeStderrLine(
'INFO: Using current user UID/GID in Linux sandbox. Set SANDBOX_SET_UID_GID=false to disable.',
);
}
@ -210,7 +211,7 @@ export async function start_sandbox(
);
}
// Log on STDERR so it doesn't clutter the output on STDOUT
console.error(`using macos seatbelt (profile: ${profile}) ...`);
writeStderrLine(`using macos seatbelt (profile: ${profile}) ...`);
// if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS
const nodeOptions = [
...(process.env['DEBUG'] ? ['--inspect-brk'] : []),
@ -299,7 +300,7 @@ export async function start_sandbox(
});
// install handlers to stop proxy on exit/signal
const stopProxy = () => {
console.log('stopping proxy ...');
writeStdoutLine('stopping proxy ...');
if (proxyProcess?.pid) {
process.kill(-proxyProcess.pid, 'SIGTERM');
}
@ -313,7 +314,7 @@ export async function start_sandbox(
// console.info(data.toString());
// });
proxyProcess.stderr?.on('data', (data) => {
console.error(data.toString());
writeStderrLine(data.toString());
});
proxyProcess.on('close', (code, signal) => {
if (sandboxProcess?.pid) {
@ -323,7 +324,7 @@ export async function start_sandbox(
`Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
);
});
console.log('waiting for proxy to start ...');
writeStdoutLine('waiting for proxy to start ...');
await execAsync(
`until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`,
);
@ -342,7 +343,7 @@ export async function start_sandbox(
});
}
console.error(`hopping into sandbox (command: ${config.command}) ...`);
writeStderrLine(`hopping into sandbox (command: ${config.command}) ...`);
// determine full path for gemini-cli to distinguish linked vs installed setting
const gcPath = fs.realpathSync(process.argv[1]);
@ -367,7 +368,7 @@ export async function start_sandbox(
'run `npm link ./packages/cli` under QwenCode-cli repo to switch to linked binary.',
);
} else {
console.error('building sandbox ...');
writeStderrLine('building sandbox ...');
const gcRoot = gcPath.split('/packages/')[0];
// if project folder has sandbox.Dockerfile under project settings folder, use that
let buildArgs = '';
@ -376,7 +377,7 @@ export async function start_sandbox(
'sandbox.Dockerfile',
);
if (isCustomProjectSandbox) {
console.error(`using ${projectSandboxDockerfile} for sandbox`);
writeStderrLine(`using ${projectSandboxDockerfile} for sandbox`);
buildArgs += `-f ${path.resolve(projectSandboxDockerfile)} -i ${image}`;
}
execSync(
@ -491,7 +492,7 @@ export async function start_sandbox(
`Missing mount path '${from}' listed in SANDBOX_MOUNTS`,
);
}
console.error(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
writeStderrLine(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
args.push('--volume', mount);
}
}
@ -557,7 +558,7 @@ export async function start_sandbox(
containerName = `gemini-cli-integration-test-${randomBytes(4).toString(
'hex',
)}`;
console.log(`ContainerName: ${containerName}`);
writeStdoutLine(`ContainerName: ${containerName}`);
} else {
let index = 0;
const containerNameCheck = execSync(
@ -569,7 +570,7 @@ export async function start_sandbox(
index++;
}
containerName = `${imageName}-${index}`;
console.log(`ContainerName (regular): ${containerName}`);
writeStdoutLine(`ContainerName (regular): ${containerName}`);
}
args.push('--name', containerName, '--hostname', containerName);
@ -691,7 +692,7 @@ export async function start_sandbox(
for (let env of process.env['SANDBOX_ENV'].split(',')) {
if ((env = env.trim())) {
if (env.includes('=')) {
console.error(`SANDBOX_ENV: ${env}`);
writeStderrLine(`SANDBOX_ENV: ${env}`);
args.push('--env', env);
} else {
throw new FatalSandboxError(
@ -795,7 +796,7 @@ export async function start_sandbox(
});
// install handlers to stop proxy on exit/signal
const stopProxy = () => {
console.log('stopping proxy container ...');
writeStdoutLine('stopping proxy container ...');
execSync(`${config.command} rm -f ${SANDBOX_PROXY_NAME}`);
};
process.on('exit', stopProxy);
@ -807,7 +808,7 @@ export async function start_sandbox(
// console.info(data.toString());
// });
proxyProcess.stderr?.on('data', (data) => {
console.error(data.toString().trim());
writeStderrLine(data.toString().trim());
});
proxyProcess.on('close', (code, signal) => {
if (sandboxProcess?.pid) {
@ -817,7 +818,7 @@ export async function start_sandbox(
`Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
);
});
console.log('waiting for proxy to start ...');
writeStdoutLine('waiting for proxy to start ...');
await execAsync(
`until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`,
);
@ -836,14 +837,14 @@ export async function start_sandbox(
return new Promise<number>((resolve, reject) => {
sandboxProcess.on('error', (err) => {
console.error('Sandbox process error:', err);
writeStderrLine(`Sandbox process error: ${err}`);
reject(err);
});
sandboxProcess?.on('close', (code, signal) => {
process.stdin.resume();
if (code !== 0 && code !== null) {
console.error(
writeStderrLine(
`Sandbox process exited with code: ${code}, signal: ${signal}`,
);
}
@ -869,7 +870,7 @@ async function imageExists(sandbox: string, image: string): Promise<boolean> {
}
checkProcess.on('error', (err) => {
console.warn(
writeStderrLine(
`Failed to start '${sandbox}' command for image check: ${err.message}`,
);
resolve(false);
@ -887,7 +888,7 @@ async function imageExists(sandbox: string, image: string): Promise<boolean> {
}
async function pullImage(sandbox: string, image: string): Promise<boolean> {
console.info(`Attempting to pull image ${image} using ${sandbox}...`);
writeStdoutLine(`Attempting to pull image ${image} using ${sandbox}...`);
return new Promise((resolve) => {
const args = ['pull', image];
const pullProcess = spawn(sandbox, args, { stdio: 'pipe' });
@ -895,16 +896,16 @@ async function pullImage(sandbox: string, image: string): Promise<boolean> {
let stderrData = '';
const onStdoutData = (data: Buffer) => {
console.info(data.toString().trim()); // Show pull progress
writeStdoutLine(data.toString().trim()); // Show pull progress
};
const onStderrData = (data: Buffer) => {
stderrData += data.toString();
console.error(data.toString().trim()); // Show pull errors/info from the command itself
writeStderrLine(data.toString().trim()); // Show pull errors/info from the command itself
};
const onError = (err: Error) => {
console.warn(
writeStderrLine(
`Failed to start '${sandbox} pull ${image}' command: ${err.message}`,
);
cleanup();
@ -913,11 +914,11 @@ async function pullImage(sandbox: string, image: string): Promise<boolean> {
const onClose = (code: number | null) => {
if (code === 0) {
console.info(`Successfully pulled image ${image}.`);
writeStdoutLine(`Successfully pulled image ${image}.`);
cleanup();
resolve(true);
} else {
console.warn(
writeStderrLine(
`Failed to pull image ${image}. '${sandbox} pull ${image}' exited with code ${code}.`,
);
if (stderrData.trim()) {
@ -957,13 +958,13 @@ async function ensureSandboxImageIsPresent(
sandbox: string,
image: string,
): Promise<boolean> {
console.info(`Checking for sandbox image: ${image}`);
writeStdoutLine(`Checking for sandbox image: ${image}`);
if (await imageExists(sandbox, image)) {
console.info(`Sandbox image ${image} found locally.`);
writeStdoutLine(`Sandbox image ${image} found locally.`);
return true;
}
console.info(`Sandbox image ${image} not found locally.`);
writeStdoutLine(`Sandbox image ${image} not found locally.`);
if (image === LOCAL_DEV_SANDBOX_IMAGE_NAME) {
// user needs to build the image themselves
return false;
@ -972,17 +973,17 @@ async function ensureSandboxImageIsPresent(
if (await pullImage(sandbox, image)) {
// After attempting to pull, check again to be certain
if (await imageExists(sandbox, image)) {
console.info(`Sandbox image ${image} is now available after pulling.`);
writeStdoutLine(`Sandbox image ${image} is now available after pulling.`);
return true;
} else {
console.warn(
writeStderrLine(
`Sandbox image ${image} still not found after a pull attempt. This might indicate an issue with the image name or registry, or the pull command reported success but failed to make the image available.`,
);
return false;
}
}
console.error(
writeStderrLine(
`Failed to obtain sandbox image ${image} after check and pull attempt.`,
);
return false; // Pull command failed or image still not present

View file

@ -0,0 +1,41 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Utility functions for writing to stdout/stderr in CLI commands.
*
* These helpers are used instead of console.log/console.error in standalone
* CLI commands (like `qwen extensions list`) where the output IS the user-facing
* result, not debug logging.
*
* For debug/diagnostic logging, use `createDebugLogger()` from @qwen-code/qwen-code-core.
*/
/**
* Writes a message to stdout with a trailing newline.
* Use for normal command output that the user expects to see.
* Avoids double newlines if the message already ends with one.
*/
export const writeStdoutLine = (message: string): void => {
process.stdout.write(message.endsWith('\n') ? message : `${message}\n`);
};
/**
* Writes a message to stderr with a trailing newline.
* Use for error messages in CLI commands.
* Avoids double newlines if the message already ends with one.
*/
export const writeStderrLine = (message: string): void => {
process.stderr.write(message.endsWith('\n') ? message : `${message}\n`);
};
/**
* Clears the terminal screen.
* Use instead of console.clear() to satisfy no-console lint rules.
*/
export const clearScreen = (): void => {
console.clear();
};