refactor(logging): Centralize all console messaging to a shared logger (part 1) (#11537)

This commit is contained in:
Abhi
2025-10-20 18:16:47 -04:00
committed by GitHub
parent d5a06d3cd2
commit 995ae717cc
18 changed files with 145 additions and 143 deletions

View File

@@ -8,7 +8,7 @@
import './src/gemini.js';
import { main } from './src/gemini.js';
import { FatalError } from '@google/gemini-cli-core';
import { debugLogger, FatalError } from '@google/gemini-cli-core';
// --- Global Entry Point ---
main().catch((error) => {
@@ -17,14 +17,14 @@ main().catch((error) => {
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
}
console.error(errorMessage);
debugLogger.error(errorMessage);
process.exit(error.exitCode);
}
console.error('An unexpected critical error occurred:');
debugLogger.error('An unexpected critical error occurred:');
if (error instanceof Error) {
console.error(error.stack);
debugLogger.error(error.stack);
} else {
console.error(String(error));
debugLogger.error(String(error));
}
process.exit(1);
});

View File

@@ -8,6 +8,7 @@ import { type CommandModule } from 'yargs';
import { disableExtension } from '../../config/extension.js';
import { SettingScope } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
import { debugLogger } from '@google/gemini-cli-core';
interface DisableArgs {
name: string;
@@ -21,11 +22,11 @@ export function handleDisable(args: DisableArgs) {
} else {
disableExtension(args.name, SettingScope.User);
}
console.log(
debugLogger.log(
`Extension "${args.name}" successfully disabled for scope "${args.scope}".`,
);
} catch (error) {
console.error(getErrorMessage(error));
debugLogger.error(getErrorMessage(error));
process.exit(1);
}
}

View File

@@ -10,7 +10,10 @@ import {
installOrUpdateExtension,
requestConsentNonInteractive,
} from '../../config/extension.js';
import type { ExtensionInstallMetadata } from '@google/gemini-cli-core';
import {
debugLogger,
type ExtensionInstallMetadata,
} from '@google/gemini-cli-core';
import { getErrorMessage } from '../../utils/errors.js';
import { stat } from 'node:fs/promises';
@@ -60,16 +63,16 @@ export async function handleInstall(args: InstallArgs) {
? () => Promise.resolve(true)
: requestConsentNonInteractive;
if (args.consent) {
console.log('You have consented to the following:');
console.log(INSTALL_WARNING_MESSAGE);
debugLogger.log('You have consented to the following:');
debugLogger.log(INSTALL_WARNING_MESSAGE);
}
const name = await installOrUpdateExtension(
installMetadata,
requestConsent,
);
console.log(`Extension "${name}" installed successfully and enabled.`);
debugLogger.log(`Extension "${name}" installed successfully and enabled.`);
} catch (error) {
console.error(getErrorMessage(error));
debugLogger.error(getErrorMessage(error));
process.exit(1);
}
}

View File

@@ -9,7 +9,10 @@ import {
installOrUpdateExtension,
requestConsentNonInteractive,
} from '../../config/extension.js';
import type { ExtensionInstallMetadata } from '@google/gemini-cli-core';
import {
debugLogger,
type ExtensionInstallMetadata,
} from '@google/gemini-cli-core';
import { getErrorMessage } from '../../utils/errors.js';
@@ -27,11 +30,11 @@ export async function handleLink(args: InstallArgs) {
installMetadata,
requestConsentNonInteractive,
);
console.log(
debugLogger.log(
`Extension "${extensionName}" linked successfully and enabled.`,
);
} catch (error) {
console.error(getErrorMessage(error));
debugLogger.error(getErrorMessage(error));
process.exit(1);
}
}

View File

@@ -8,6 +8,7 @@ import type { CommandModule } from 'yargs';
import { loadExtensions, toOutputString } from '../../config/extension.js';
import { getErrorMessage } from '../../utils/errors.js';
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
import { debugLogger } from '@google/gemini-cli-core';
export async function handleList() {
try {
@@ -16,16 +17,16 @@ export async function handleList() {
process.cwd(),
);
if (extensions.length === 0) {
console.log('No extensions installed.');
debugLogger.log('No extensions installed.');
return;
}
console.log(
debugLogger.log(
extensions
.map((extension, _): string => toOutputString(extension, process.cwd()))
.join('\n\n'),
);
} catch (error) {
console.error(getErrorMessage(error));
debugLogger.error(getErrorMessage(error));
process.exit(1);
}
}

View File

@@ -8,7 +8,7 @@ import { access, cp, mkdir, readdir, writeFile } from 'node:fs/promises';
import { join, dirname, basename } from 'node:path';
import type { CommandModule } from 'yargs';
import { fileURLToPath } from 'node:url';
import { getErrorMessage } from '../../utils/errors.js';
import { debugLogger } from '@google/gemini-cli-core';
interface NewArgs {
path: string;
@@ -49,32 +49,27 @@ async function copyDirectory(template: string, path: string) {
}
async function handleNew(args: NewArgs) {
try {
if (args.template) {
await copyDirectory(args.template, args.path);
console.log(
`Successfully created new extension from template "${args.template}" at ${args.path}.`,
);
} else {
await createDirectory(args.path);
const extensionName = basename(args.path);
const manifest = {
name: extensionName,
version: '1.0.0',
};
await writeFile(
join(args.path, 'gemini-extension.json'),
JSON.stringify(manifest, null, 2),
);
console.log(`Successfully created new extension at ${args.path}.`);
}
console.log(
`You can install this using "gemini extensions link ${args.path}" to test it out.`,
if (args.template) {
await copyDirectory(args.template, args.path);
debugLogger.log(
`Successfully created new extension from template "${args.template}" at ${args.path}.`,
);
} catch (error) {
console.error(getErrorMessage(error));
throw error;
} else {
await createDirectory(args.path);
const extensionName = basename(args.path);
const manifest = {
name: extensionName,
version: '1.0.0',
};
await writeFile(
join(args.path, 'gemini-extension.json'),
JSON.stringify(manifest, null, 2),
);
debugLogger.log(`Successfully created new extension at ${args.path}.`);
}
debugLogger.log(
`You can install this using "gemini extensions link ${args.path}" to test it out.`,
);
}
async function getBoilerplateChoices() {

View File

@@ -7,6 +7,7 @@
import type { CommandModule } from 'yargs';
import { uninstallExtension } from '../../config/extension.js';
import { getErrorMessage } from '../../utils/errors.js';
import { debugLogger } from '@google/gemini-cli-core';
interface UninstallArgs {
name: string; // can be extension name or source URL.
@@ -15,9 +16,9 @@ interface UninstallArgs {
export async function handleUninstall(args: UninstallArgs) {
try {
await uninstallExtension(args.name, false);
console.log(`Extension "${args.name}" successfully uninstalled.`);
debugLogger.log(`Extension "${args.name}" successfully uninstalled.`);
} catch (error) {
console.error(getErrorMessage(error));
debugLogger.error(getErrorMessage(error));
process.exit(1);
}
}

View File

@@ -20,6 +20,7 @@ import { checkForExtensionUpdate } from '../../config/extensions/github.js';
import { getErrorMessage } from '../../utils/errors.js';
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
import { debugLogger } from '@google/gemini-cli-core';
interface UpdateArgs {
name?: string;
@@ -48,18 +49,18 @@ export async function handleUpdate(args: UpdateArgs) {
(extension) => extension.name === args.name,
);
if (!extension) {
console.log(`Extension "${args.name}" not found.`);
debugLogger.log(`Extension "${args.name}" not found.`);
return;
}
if (!extension.installMetadata) {
console.log(
debugLogger.log(
`Unable to install extension "${args.name}" due to missing install metadata`,
);
return;
}
const updateState = await checkForExtensionUpdate(extension);
if (updateState !== ExtensionUpdateState.UPDATE_AVAILABLE) {
console.log(`Extension "${args.name}" is already up to date.`);
debugLogger.log(`Extension "${args.name}" is already up to date.`);
return;
}
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
@@ -74,14 +75,14 @@ export async function handleUpdate(args: UpdateArgs) {
updatedExtensionInfo.originalVersion !==
updatedExtensionInfo.updatedVersion
) {
console.log(
debugLogger.log(
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion}${updatedExtensionInfo.updatedVersion}.`,
);
} else {
console.log(`Extension "${args.name}" is already up to date.`);
debugLogger.log(`Extension "${args.name}" is already up to date.`);
}
} catch (error) {
console.error(getErrorMessage(error));
debugLogger.error(getErrorMessage(error));
}
}
if (args.all) {
@@ -109,12 +110,12 @@ export async function handleUpdate(args: UpdateArgs) {
(info) => info.originalVersion !== info.updatedVersion,
);
if (updateInfos.length === 0) {
console.log('No extensions to update.');
debugLogger.log('No extensions to update.');
return;
}
console.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
debugLogger.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
} catch (error) {
console.error(getErrorMessage(error));
debugLogger.error(getErrorMessage(error));
}
}
}

View File

@@ -7,7 +7,7 @@
// File for 'gemini mcp add' command
import type { CommandModule } from 'yargs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import type { MCPServerConfig } from '@google/gemini-cli-core';
import { debugLogger, type MCPServerConfig } from '@google/gemini-cli-core';
async function addMcpServer(
name: string,
@@ -41,7 +41,7 @@ async function addMcpServer(
const inHome = settings.workspace.path === settings.user.path;
if (scope === 'project' && inHome) {
console.error(
debugLogger.error(
'Error: Please use --scope user to edit settings in the home directory.',
);
process.exit(1);
@@ -116,7 +116,7 @@ async function addMcpServer(
const isExistingServer = !!mcpServers[name];
if (isExistingServer) {
console.log(
debugLogger.log(
`MCP server "${name}" is already configured within ${scope} settings.`,
);
}
@@ -126,9 +126,9 @@ async function addMcpServer(
settings.setValue(settingsScope, 'mcpServers', mcpServers);
if (isExistingServer) {
console.log(`MCP server "${name}" updated in ${scope} settings.`);
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
} else {
console.log(
debugLogger.log(
`MCP server "${name}" added to ${scope} settings. (${transport})`,
);
}

View File

@@ -36,6 +36,7 @@ import {
FatalConfigError,
getPty,
EDIT_TOOL_NAME,
debugLogger,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
@@ -49,16 +50,6 @@ import { isWorkspaceTrusted } from './trustedFolders.js';
import { createPolicyEngineConfig } from './policy.js';
import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
// Simple console logger for now - replace with actual logger if available
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug: (...args: any[]) => console.debug('[DEBUG]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
warn: (...args: any[]) => console.warn('[WARN]', ...args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: (...args: any[]) => console.error('[ERROR]', ...args),
};
export interface CliArgs {
query: string | undefined;
model: string | undefined;
@@ -210,7 +201,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
)
// Ensure validation flows through .fail() for clean UX
.fail((msg, err, yargs) => {
console.error(msg || err?.message || 'Unknown error');
debugLogger.error(msg || err?.message || 'Unknown error');
yargs.showHelp();
process.exit(1);
})
@@ -313,7 +304,7 @@ export async function loadHierarchicalGeminiMemory(
const effectiveCwd = isHomeDirectory ? '' : currentWorkingDirectory;
if (debugMode) {
logger.debug(
debugLogger.debug(
`CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory} (memoryImportFormat: ${memoryImportFormat})`,
);
}
@@ -470,7 +461,7 @@ export async function loadCliConfig(
// Force approval mode to default if the folder is not trusted.
if (!trustedFolder && approvalMode !== ApprovalMode.DEFAULT) {
logger.warn(
debugLogger.warn(
`Approval mode overridden to "default" because the current folder is not trusted.`,
);
approvalMode = ApprovalMode.DEFAULT;
@@ -700,7 +691,7 @@ function mergeMcpServers(settings: Settings, extensions: GeminiCLIExtension[]) {
for (const extension of extensions) {
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
if (mcpServers[key]) {
logger.warn(
debugLogger.warn(
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
);
return;

View File

@@ -23,6 +23,7 @@ import {
logExtensionUninstall,
logExtensionUpdateEvent,
logExtensionDisable,
debugLogger,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
@@ -233,7 +234,7 @@ export function loadExtension(
id,
};
} catch (e) {
console.error(
debugLogger.error(
`Warning: Skipping extension in ${effectiveExtensionPath}: ${getErrorMessage(
e,
)}`,
@@ -324,7 +325,7 @@ export function annotateActiveExtensions(
export async function requestConsentNonInteractive(
consentDescription: string,
): Promise<boolean> {
console.info(consentDescription);
debugLogger.log(consentDescription);
const result = await promptForConsentNonInteractive(
'Do you want to continue? [Y/n]: ',
);

View File

@@ -6,9 +6,10 @@
import { simpleGit } from 'simple-git';
import { getErrorMessage } from '../../utils/errors.js';
import type {
ExtensionInstallMetadata,
GeminiCLIExtension,
import {
debugLogger,
type ExtensionInstallMetadata,
type GeminiCLIExtension,
} from '@google/gemini-cli-core';
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
import * as os from 'node:os';
@@ -160,7 +161,7 @@ export async function checkForExtensionUpdate(
workspaceDir: cwd,
});
if (!newExtension) {
console.error(
debugLogger.error(
`Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}`,
);
return ExtensionUpdateState.ERROR;
@@ -182,12 +183,14 @@ export async function checkForExtensionUpdate(
const git = simpleGit(extension.path);
const remotes = await git.getRemotes(true);
if (remotes.length === 0) {
console.error('No git remotes found.');
debugLogger.error('No git remotes found.');
return ExtensionUpdateState.ERROR;
}
const remoteUrl = remotes[0].refs.fetch;
if (!remoteUrl) {
console.error(`No fetch URL found for git remote ${remotes[0].name}.`);
debugLogger.error(
`No fetch URL found for git remote ${remotes[0].name}.`,
);
return ExtensionUpdateState.ERROR;
}
@@ -197,7 +200,7 @@ export async function checkForExtensionUpdate(
const lsRemoteOutput = await git.listRemote([remoteUrl, refToCheck]);
if (typeof lsRemoteOutput !== 'string' || lsRemoteOutput.trim() === '') {
console.error(`Git ref ${refToCheck} not found.`);
debugLogger.error(`Git ref ${refToCheck} not found.`);
return ExtensionUpdateState.ERROR;
}
@@ -205,7 +208,7 @@ export async function checkForExtensionUpdate(
const localHash = await git.revparse(['HEAD']);
if (!remoteHash) {
console.error(
debugLogger.error(
`Unable to parse hash from git ls-remote output "${lsRemoteOutput}"`,
);
return ExtensionUpdateState.ERROR;
@@ -217,12 +220,12 @@ export async function checkForExtensionUpdate(
} else {
const { source, releaseTag } = installMetadata;
if (!source) {
console.error(`No "source" provided for extension.`);
debugLogger.error(`No "source" provided for extension.`);
return ExtensionUpdateState.ERROR;
}
const repoInfo = tryParseGithubUrl(source);
if (!repoInfo) {
console.error(
debugLogger.error(
`Source is not a valid GitHub repository for release checks: ${source}`,
);
return ExtensionUpdateState.ERROR;
@@ -244,7 +247,7 @@ export async function checkForExtensionUpdate(
return ExtensionUpdateState.UP_TO_DATE;
}
} catch (error) {
console.error(
debugLogger.error(
`Failed to check for updates for extension "${installMetadata.source}": ${getErrorMessage(error)}`,
);
return ExtensionUpdateState.ERROR;

View File

@@ -18,7 +18,7 @@ import {
loadExtensionConfig,
} from '../extension.js';
import { checkForExtensionUpdate } from './github.js';
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
import { debugLogger, type GeminiCLIExtension } from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import { getErrorMessage } from '../../utils/errors.js';
@@ -101,7 +101,7 @@ export async function updateExtension(
updatedVersion,
};
} catch (e) {
console.error(
debugLogger.error(
`Error updating extension, rolling back. ${getErrorMessage(e)}`,
);
dispatchExtensionStateUpdate({

View File

@@ -40,6 +40,7 @@ import {
AuthType,
getOauthClient,
UserPromptEvent,
debugLogger,
} from '@google/gemini-cli-core';
import {
initializeApp,
@@ -79,7 +80,7 @@ export function validateDnsResolutionOrder(
return order;
}
// We don't want to throw here, just warn and use the default.
console.warn(
debugLogger.warn(
`Invalid value for dnsResolutionOrder in settings: "${order}". Using default "${defaultValue}".`,
);
return defaultValue;
@@ -95,7 +96,7 @@ function getNodeMemoryArgs(isDebugMode: boolean): string[] {
// Set target to 50% of total memory
const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);
if (isDebugMode) {
console.debug(
debugLogger.debug(
`Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`,
);
}
@@ -106,7 +107,7 @@ function getNodeMemoryArgs(isDebugMode: boolean): string[] {
if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
if (isDebugMode) {
console.debug(
debugLogger.debug(
`Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`,
);
}
@@ -213,7 +214,7 @@ export async function startInteractiveUI(
.catch((err) => {
// Silently ignore update check errors.
if (config.getDebugMode()) {
console.error('Update check failed:', err);
debugLogger.warn('Update check failed:', err);
}
});
@@ -230,7 +231,7 @@ export async function main() {
// Check for invalid input combinations early to prevent crashes
if (argv.promptInteractive && !process.stdin.isTTY) {
console.error(
debugLogger.error(
'Error: The --prompt-interactive flag cannot be used when input is piped from stdin.',
);
process.exit(1);
@@ -266,7 +267,9 @@ export async function main() {
if (!themeManager.setActiveTheme(settings.merged.ui?.theme)) {
// If the theme is not found during initial load, log a warning and continue.
// The useThemeCommand hook in AppContainer.tsx will handle opening the dialog.
console.warn(`Warning: Theme "${settings.merged.ui?.theme}" not found.`);
debugLogger.warn(
`Warning: Theme "${settings.merged.ui?.theme}" not found.`,
);
}
}
@@ -308,7 +311,7 @@ export async function main() {
settings.merged.security.auth.selectedType,
);
} catch (err) {
console.error('Error authenticating:', err);
debugLogger.error('Error authenticating:', err);
process.exit(1);
}
}
@@ -373,9 +376,9 @@ export async function main() {
await cleanupExpiredSessions(config, settings.merged);
if (config.getListExtensions()) {
console.log('Installed extensions:');
debugLogger.log('Installed extensions:');
for (const extension of extensions) {
console.log(`- ${extension.name}`);
debugLogger.log(`- ${extension.name}`);
}
process.exit(0);
}
@@ -443,7 +446,7 @@ export async function main() {
}
}
if (!input) {
console.error(
debugLogger.error(
`No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option.`,
);
process.exit(1);
@@ -468,7 +471,7 @@ export async function main() {
);
if (config.getDebugMode()) {
console.log('Session ID: %s', sessionId);
debugLogger.log('Session ID: %s', sessionId);
}
await runNonInteractive(nonInteractiveConfig, settings, input, prompt_id);

View File

@@ -23,6 +23,7 @@ import {
StreamJsonFormatter,
JsonStreamEventType,
uiTelemetryService,
debugLogger,
} from '@google/gemini-cli-core';
import type { Content, Part } from '@google/genai';
@@ -255,7 +256,7 @@ export async function runNonInteractive(
.getChat()
.recordCompletedToolCalls(currentModel, completedToolCalls);
} catch (error) {
console.error(
debugLogger.error(
`Error recording completed tool call information: ${error}`,
);
}

View File

@@ -5,6 +5,7 @@
*/
import {
debugLogger,
flatMapTextParts,
readPathFromWorkspace,
} from '@google/gemini-cli-core';
@@ -68,8 +69,9 @@ export class AtFileProcessor implements IPromptProcessor {
error instanceof Error ? error.message : String(error);
const uiMessage = `Failed to inject content for '@{${pathStr}}': ${message}`;
console.error(
`[AtFileProcessor] ${uiMessage}. Leaving placeholder in prompt.`,
// `context.invocation` should always be present at this point.
debugLogger.error(
`Error while loading custom command (${context.invocation!.name}) ${uiMessage}. Leaving placeholder in prompt.`,
);
context.ui.addItem(
{ type: MessageType.ERROR, text: uiMessage },

View File

@@ -5,7 +5,7 @@
*/
import type { Config } from '@google/gemini-cli-core';
import { AuthType, OutputFormat } from '@google/gemini-cli-core';
import { AuthType, debugLogger, OutputFormat } from '@google/gemini-cli-core';
import { USER_SETTINGS_PATH } from './config/settings.js';
import { validateAuthMethod } from './config/auth.js';
import { type LoadedSettings } from './config/settings.js';
@@ -65,7 +65,7 @@ export async function validateNonInteractiveAuth(
1,
);
} else {
console.error(error instanceof Error ? error.message : String(error));
debugLogger.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}

View File

@@ -5,6 +5,7 @@
*/
import { execSync, spawn, spawnSync } from 'node:child_process';
import { debugLogger } from './debugLogger.js';
export type EditorType =
| 'vscode'
@@ -168,50 +169,45 @@ export async function openDiff(
): Promise<void> {
const diffCommand = getDiffCommand(oldPath, newPath, editor);
if (!diffCommand) {
console.error('No diff tool available. Install a supported editor.');
debugLogger.error('No diff tool available. Install a supported editor.');
return;
}
try {
const isTerminalEditor = ['vim', 'emacs', 'neovim'].includes(editor);
const isTerminalEditor = ['vim', 'emacs', 'neovim'].includes(editor);
if (isTerminalEditor) {
try {
const result = spawnSync(diffCommand.command, diffCommand.args, {
stdio: 'inherit',
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`${editor} exited with code ${result.status}`);
}
} finally {
onEditorClose();
}
return;
}
return new Promise<void>((resolve, reject) => {
const childProcess = spawn(diffCommand.command, diffCommand.args, {
if (isTerminalEditor) {
try {
const result = spawnSync(diffCommand.command, diffCommand.args, {
stdio: 'inherit',
shell: process.platform === 'win32',
});
childProcess.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`${editor} exited with code ${code}`));
}
});
childProcess.on('error', (error) => {
reject(error);
});
});
} catch (error) {
console.error(error);
throw error;
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`${editor} exited with code ${result.status}`);
}
} finally {
onEditorClose();
}
return;
}
return new Promise<void>((resolve, reject) => {
const childProcess = spawn(diffCommand.command, diffCommand.args, {
stdio: 'inherit',
shell: process.platform === 'win32',
});
childProcess.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`${editor} exited with code ${code}`));
}
});
childProcess.on('error', (error) => {
reject(error);
});
});
}