mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 04:30:48 +00:00
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:
parent
45df0e8b82
commit
7995c65571
82 changed files with 606 additions and 485 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
41
packages/cli/src/utils/stdioHelpers.ts
Normal file
41
packages/cli/src/utils/stdioHelpers.ts
Normal 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();
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue