Standardize exit codes (#7055)

This commit is contained in:
Tommaso Sciortino
2025-08-25 21:44:45 -07:00
committed by GitHub
parent 415a36a195
commit 7e31577813
10 changed files with 116 additions and 97 deletions

View File

@@ -74,6 +74,18 @@ This guide provides solutions to common issues and debugging tips, including top
- **Cause:** The `DEBUG` and `DEBUG_MODE` variables are automatically excluded from project `.env` files to prevent interference with gemini-cli behavior. - **Cause:** The `DEBUG` and `DEBUG_MODE` variables are automatically excluded from project `.env` files to prevent interference with gemini-cli behavior.
- **Solution:** Use a `.gemini/.env` file instead, or configure the `excludedProjectEnvVars` setting in your `settings.json` to exclude fewer variables. - **Solution:** Use a `.gemini/.env` file instead, or configure the `excludedProjectEnvVars` setting in your `settings.json` to exclude fewer variables.
## Exit Codes
The Gemini CLI uses specific exit codes to indicate the reason for termination. This is especially useful for scripting and automation.
| Exit Code | Error Type | Description |
| --------- | -------------------------- | --------------------------------------------------------------------------------------------------- |
| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. |
| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) |
| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g., Docker, Podman, or Seatbelt). |
| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. |
| 53 | `FatalTurnLimitedError` | The maximum number of conversational turns for the session was reached. (non-interactive mode only) |
## Debugging Tips ## Debugging Tips
- **CLI debugging:** - **CLI debugging:**

View File

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

View File

@@ -5,6 +5,7 @@
*/ */
import type { SandboxConfig } from '@google/gemini-cli-core'; import type { SandboxConfig } from '@google/gemini-cli-core';
import { FatalSandboxError } from '@google/gemini-cli-core';
import commandExists from 'command-exists'; import commandExists from 'command-exists';
import * as os from 'node:os'; import * as os from 'node:os';
import { getPackageJson } from '../utils/package.js'; import { getPackageJson } from '../utils/package.js';
@@ -51,21 +52,19 @@ function getSandboxCommand(
if (typeof sandbox === 'string' && sandbox) { if (typeof sandbox === 'string' && sandbox) {
if (!isSandboxCommand(sandbox)) { if (!isSandboxCommand(sandbox)) {
console.error( throw new FatalSandboxError(
`ERROR: invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join( `Invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join(
', ', ', ',
)}`, )}`,
); );
process.exit(1);
} }
// confirm that specified command exists // confirm that specified command exists
if (commandExists.sync(sandbox)) { if (commandExists.sync(sandbox)) {
return sandbox; return sandbox;
} }
console.error( throw new FatalSandboxError(
`ERROR: missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`, `Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
); );
process.exit(1);
} }
// look for seatbelt, docker, or podman, in that order // look for seatbelt, docker, or podman, in that order
@@ -80,11 +79,10 @@ function getSandboxCommand(
// throw an error if user requested sandbox but no command was found // throw an error if user requested sandbox but no command was found
if (sandbox === true) { if (sandbox === true) {
console.error( throw new FatalSandboxError(
'ERROR: GEMINI_SANDBOX is true but failed to determine command for sandbox; ' + 'GEMINI_SANDBOX is true but failed to determine command for sandbox; ' +
'install docker or podman or specify command in GEMINI_SANDBOX', 'install docker or podman or specify command in GEMINI_SANDBOX',
); );
process.exit(1);
} }
return ''; return '';

View File

@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import stripAnsi from 'strip-ansi';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { import {
main, main,
@@ -16,6 +15,7 @@ import type { SettingsFile } from './config/settings.js';
import { LoadedSettings, loadSettings } from './config/settings.js'; import { LoadedSettings, loadSettings } from './config/settings.js';
import { appEvents, AppEvent } from './utils/events.js'; import { appEvents, AppEvent } from './utils/events.js';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import { FatalConfigError } from '@google/gemini-cli-core';
// Custom error to identify mock process.exit calls // Custom error to identify mock process.exit calls
class MockProcessExitError extends Error { class MockProcessExitError extends Error {
@@ -75,7 +75,6 @@ vi.mock('./utils/sandbox.js', () => ({
})); }));
describe('gemini.tsx main function', () => { describe('gemini.tsx main function', () => {
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let loadSettingsMock: ReturnType<typeof vi.mocked<typeof loadSettings>>; let loadSettingsMock: ReturnType<typeof vi.mocked<typeof loadSettings>>;
let originalEnvGeminiSandbox: string | undefined; let originalEnvGeminiSandbox: string | undefined;
let originalEnvSandbox: string | undefined; let originalEnvSandbox: string | undefined;
@@ -97,7 +96,6 @@ describe('gemini.tsx main function', () => {
delete process.env['GEMINI_SANDBOX']; delete process.env['GEMINI_SANDBOX'];
delete process.env['SANDBOX']; delete process.env['SANDBOX'];
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
initialUnhandledRejectionListeners = initialUnhandledRejectionListeners =
process.listeners('unhandledRejection'); process.listeners('unhandledRejection');
}); });
@@ -126,7 +124,7 @@ describe('gemini.tsx main function', () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
it('should call process.exit(1) if settings have errors', async () => { it('should throw InvalidConfigurationError if settings have errors', async () => {
const settingsError = { const settingsError = {
message: 'Test settings error', message: 'Test settings error',
path: '/test/settings.json', path: '/test/settings.json',
@@ -158,28 +156,7 @@ describe('gemini.tsx main function', () => {
loadSettingsMock.mockReturnValue(mockLoadedSettings); loadSettingsMock.mockReturnValue(mockLoadedSettings);
try { await expect(main()).rejects.toThrow(FatalConfigError);
await main();
// If main completes without throwing, the test should fail because process.exit was expected
expect.fail('main function did not exit as expected');
} catch (error) {
expect(error).toBeInstanceOf(MockProcessExitError);
if (error instanceof MockProcessExitError) {
expect(error.code).toBe(1);
}
}
// Verify console.error was called with the error message
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
expect(stripAnsi(String(consoleErrorSpy.mock.calls[0][0]))).toBe(
'Error in /test/settings.json: Test settings error',
);
expect(stripAnsi(String(consoleErrorSpy.mock.calls[1][0]))).toBe(
'Please fix /test/settings.json and try again.',
);
// Verify process.exit was called.
expect(processExitSpy).toHaveBeenCalledWith(1);
}); });
it('should log unhandled promise rejections and open debug console on first error', async () => { it('should log unhandled promise rejections and open debug console on first error', async () => {

View File

@@ -34,6 +34,7 @@ import {
logIdeConnection, logIdeConnection,
IdeConnectionEvent, IdeConnectionEvent,
IdeConnectionType, IdeConnectionType,
FatalConfigError,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { validateAuthMethod } from './config/auth.js'; import { validateAuthMethod } from './config/auth.js';
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js'; import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
@@ -173,15 +174,12 @@ export async function main() {
await cleanupCheckpoints(); await cleanupCheckpoints();
if (settings.errors.length > 0) { if (settings.errors.length > 0) {
for (const error of settings.errors) { const errorMessages = settings.errors.map(
let errorMessage = `Error in ${error.path}: ${error.message}`; (error) => `Error in ${error.path}: ${error.message}`,
if (!process.env['NO_COLOR']) { );
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`; throw new FatalConfigError(
} `${errorMessages.join('\n')}\nPlease fix the configuration file(s) and try again.`,
console.error(errorMessage); );
console.error(`Please fix ${error.path} and try again.`);
}
process.exit(1);
} }
const argv = await parseArguments(settings.merged); const argv = await parseArguments(settings.merged);

View File

@@ -38,7 +38,6 @@ describe('runNonInteractive', () => {
let mockCoreExecuteToolCall: vi.Mock; let mockCoreExecuteToolCall: vi.Mock;
let mockShutdownTelemetry: vi.Mock; let mockShutdownTelemetry: vi.Mock;
let consoleErrorSpy: vi.SpyInstance; let consoleErrorSpy: vi.SpyInstance;
let processExitSpy: vi.SpyInstance;
let processStdoutSpy: vi.SpyInstance; let processStdoutSpy: vi.SpyInstance;
let mockGeminiClient: { let mockGeminiClient: {
sendMessageStream: vi.Mock; sendMessageStream: vi.Mock;
@@ -49,9 +48,6 @@ describe('runNonInteractive', () => {
mockShutdownTelemetry = vi.mocked(shutdownTelemetry); mockShutdownTelemetry = vi.mocked(shutdownTelemetry);
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => {}) as (code?: number) => never);
processStdoutSpy = vi processStdoutSpy = vi
.spyOn(process.stdout, 'write') .spyOn(process.stdout, 'write')
.mockImplementation(() => true); .mockImplementation(() => true);
@@ -202,7 +198,6 @@ describe('runNonInteractive', () => {
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error executing tool errorTool: Execution failed', 'Error executing tool errorTool: Execution failed',
); );
expect(processExitSpy).not.toHaveBeenCalled();
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2); expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith( expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(
2, 2,
@@ -228,12 +223,9 @@ describe('runNonInteractive', () => {
throw apiError; throw apiError;
}); });
await runNonInteractive(mockConfig, 'Initial fail', 'prompt-id-4'); await expect(
runNonInteractive(mockConfig, 'Initial fail', 'prompt-id-4'),
expect(consoleErrorSpy).toHaveBeenCalledWith( ).rejects.toThrow(apiError);
'[API Error: API connection failed]',
);
expect(processExitSpy).toHaveBeenCalledWith(1);
}); });
it('should not exit if a tool is not found, and should send error back to model', async () => { it('should not exit if a tool is not found, and should send error back to model', async () => {
@@ -272,7 +264,6 @@ describe('runNonInteractive', () => {
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error executing tool nonexistentTool: Tool "nonexistentTool" not found in registry.', 'Error executing tool nonexistentTool: Tool "nonexistentTool" not found in registry.',
); );
expect(processExitSpy).not.toHaveBeenCalled();
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2); expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
expect(processStdoutSpy).toHaveBeenCalledWith( expect(processStdoutSpy).toHaveBeenCalledWith(
"Sorry, I can't find that tool.", "Sorry, I can't find that tool.",
@@ -281,9 +272,10 @@ describe('runNonInteractive', () => {
it('should exit when max session turns are exceeded', async () => { it('should exit when max session turns are exceeded', async () => {
vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(0); vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(0);
await runNonInteractive(mockConfig, 'Trigger loop', 'prompt-id-6'); await expect(
expect(consoleErrorSpy).toHaveBeenCalledWith( runNonInteractive(mockConfig, 'Trigger loop', 'prompt-id-6'),
'\n Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', ).rejects.toThrow(
'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
); );
}); });

View File

@@ -11,6 +11,8 @@ import {
isTelemetrySdkInitialized, isTelemetrySdkInitialized,
GeminiEventType, GeminiEventType,
parseAndFormatApiError, parseAndFormatApiError,
FatalInputError,
FatalTurnLimitedError,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { Content, Part } from '@google/genai'; import type { Content, Part } from '@google/genai';
@@ -53,8 +55,9 @@ export async function runNonInteractive(
if (!shouldProceed || !processedQuery) { if (!shouldProceed || !processedQuery) {
// An error occurred during @include processing (e.g., file not found). // An error occurred during @include processing (e.g., file not found).
// The error message is already logged by handleAtCommand. // The error message is already logged by handleAtCommand.
console.error('Exiting due to an error processing the @ command.'); throw new FatalInputError(
process.exit(1); 'Exiting due to an error processing the @ command.',
);
} }
let currentMessages: Content[] = [ let currentMessages: Content[] = [
@@ -68,10 +71,9 @@ export async function runNonInteractive(
config.getMaxSessionTurns() >= 0 && config.getMaxSessionTurns() >= 0 &&
turnCount > config.getMaxSessionTurns() turnCount > config.getMaxSessionTurns()
) { ) {
console.error( throw new FatalTurnLimitedError(
'\n Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', 'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
); );
return;
} }
const toolCallRequests: ToolCallRequestInfo[] = []; const toolCallRequests: ToolCallRequestInfo[] = [];
@@ -126,7 +128,7 @@ export async function runNonInteractive(
config.getContentGeneratorConfig()?.authType, config.getContentGeneratorConfig()?.authType,
), ),
); );
process.exit(1); throw error;
} finally { } finally {
consolePatcher.cleanup(); consolePatcher.cleanup();
if (isTelemetrySdkInitialized()) { if (isTelemetrySdkInitialized()) {

View File

@@ -17,6 +17,7 @@ import {
} from '../config/settings.js'; } from '../config/settings.js';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import type { Config, SandboxConfig } from '@google/gemini-cli-core'; import type { Config, SandboxConfig } from '@google/gemini-cli-core';
import { FatalSandboxError } from '@google/gemini-cli-core';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js'; import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -198,8 +199,9 @@ export async function start_sandbox(
if (config.command === 'sandbox-exec') { if (config.command === 'sandbox-exec') {
// disallow BUILD_SANDBOX // disallow BUILD_SANDBOX
if (process.env['BUILD_SANDBOX']) { if (process.env['BUILD_SANDBOX']) {
console.error('ERROR: cannot BUILD_SANDBOX when using macOS Seatbelt'); throw new FatalSandboxError(
process.exit(1); 'Cannot BUILD_SANDBOX when using macOS Seatbelt',
);
} }
const profile = (process.env['SEATBELT_PROFILE'] ??= 'permissive-open'); const profile = (process.env['SEATBELT_PROFILE'] ??= 'permissive-open');
@@ -214,10 +216,9 @@ export async function start_sandbox(
); );
} }
if (!fs.existsSync(profileFile)) { if (!fs.existsSync(profileFile)) {
console.error( throw new FatalSandboxError(
`ERROR: missing macos seatbelt profile file '${profileFile}'`, `Missing macos seatbelt profile file '${profileFile}'`,
); );
process.exit(1);
} }
// Log on STDERR so it doesn't clutter the output on STDOUT // Log on STDERR so it doesn't clutter the output on STDOUT
console.error(`using macos seatbelt (profile: ${profile}) ...`); console.error(`using macos seatbelt (profile: ${profile}) ...`);
@@ -325,13 +326,12 @@ export async function start_sandbox(
console.error(data.toString()); console.error(data.toString());
}); });
proxyProcess.on('close', (code, signal) => { proxyProcess.on('close', (code, signal) => {
console.error(
`ERROR: proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
);
if (sandboxProcess?.pid) { if (sandboxProcess?.pid) {
process.kill(-sandboxProcess.pid, 'SIGTERM'); process.kill(-sandboxProcess.pid, 'SIGTERM');
} }
process.exit(1); throw new FatalSandboxError(
`Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
);
}); });
console.log('waiting for proxy to start ...'); console.log('waiting for proxy to start ...');
await execAsync( await execAsync(
@@ -366,11 +366,10 @@ export async function start_sandbox(
// note this can only be done with binary linked from gemini-cli repo // note this can only be done with binary linked from gemini-cli repo
if (process.env['BUILD_SANDBOX']) { if (process.env['BUILD_SANDBOX']) {
if (!gcPath.includes('gemini-cli/packages/')) { if (!gcPath.includes('gemini-cli/packages/')) {
console.error( throw new FatalSandboxError(
'ERROR: cannot build sandbox using installed gemini binary; ' + 'Cannot build sandbox using installed gemini binary; ' +
'run `npm link ./packages/cli` under gemini-cli repo to switch to linked binary.', 'run `npm link ./packages/cli` under gemini-cli repo to switch to linked binary.',
); );
process.exit(1);
} else { } else {
console.error('building sandbox ...'); console.error('building sandbox ...');
const gcRoot = gcPath.split('/packages/')[0]; const gcRoot = gcPath.split('/packages/')[0];
@@ -403,10 +402,9 @@ export async function start_sandbox(
image === LOCAL_DEV_SANDBOX_IMAGE_NAME image === LOCAL_DEV_SANDBOX_IMAGE_NAME
? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.' ? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
: 'Please check the image name, your network connection, or notify gemini-cli-dev@google.com if the issue persists.'; : 'Please check the image name, your network connection, or notify gemini-cli-dev@google.com if the issue persists.';
console.error( throw new FatalSandboxError(
`ERROR: Sandbox image '${image}' is missing or could not be pulled. ${remedy}`, `Sandbox image '${image}' is missing or could not be pulled. ${remedy}`,
); );
process.exit(1);
} }
// use interactive mode and auto-remove container on exit // use interactive mode and auto-remove container on exit
@@ -484,17 +482,15 @@ export async function start_sandbox(
mount = `${from}:${to}:${opts}`; mount = `${from}:${to}:${opts}`;
// check that from path is absolute // check that from path is absolute
if (!path.isAbsolute(from)) { if (!path.isAbsolute(from)) {
console.error( throw new FatalSandboxError(
`ERROR: path '${from}' listed in SANDBOX_MOUNTS must be absolute`, `Path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
); );
process.exit(1);
} }
// check that from path exists on host // check that from path exists on host
if (!fs.existsSync(from)) { if (!fs.existsSync(from)) {
console.error( throw new FatalSandboxError(
`ERROR: missing mount path '${from}' listed in SANDBOX_MOUNTS`, `Missing mount path '${from}' listed in SANDBOX_MOUNTS`,
); );
process.exit(1);
} }
console.error(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`); console.error(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
args.push('--volume', mount); args.push('--volume', mount);
@@ -665,10 +661,9 @@ export async function start_sandbox(
console.error(`SANDBOX_ENV: ${env}`); console.error(`SANDBOX_ENV: ${env}`);
args.push('--env', env); args.push('--env', env);
} else { } else {
console.error( throw new FatalSandboxError(
'ERROR: SANDBOX_ENV must be a comma-separated list of key=value pairs', 'SANDBOX_ENV must be a comma-separated list of key=value pairs',
); );
process.exit(1);
} }
} }
} }
@@ -776,13 +771,12 @@ export async function start_sandbox(
console.error(data.toString().trim()); console.error(data.toString().trim());
}); });
proxyProcess.on('close', (code, signal) => { proxyProcess.on('close', (code, signal) => {
console.error(
`ERROR: proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
);
if (sandboxProcess?.pid) { if (sandboxProcess?.pid) {
process.kill(-sandboxProcess.pid, 'SIGTERM'); process.kill(-sandboxProcess.pid, 'SIGTERM');
} }
process.exit(1); throw new FatalSandboxError(
`Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
);
}); });
console.log('waiting for proxy to start ...'); console.log('waiting for proxy to start ...');
await execAsync( await execAsync(

View File

@@ -18,7 +18,7 @@ import open from 'open';
import path from 'node:path'; import path from 'node:path';
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { getErrorMessage } from '../utils/errors.js'; import { getErrorMessage, FatalAuthenticationError } from '../utils/errors.js';
import { UserAccountManager } from '../utils/userAccountManager.js'; import { UserAccountManager } from '../utils/userAccountManager.js';
import { AuthType } from '../core/contentGenerator.js'; import { AuthType } from '../core/contentGenerator.js';
import readline from 'node:readline'; import readline from 'node:readline';
@@ -142,7 +142,9 @@ async function initOauthClient(
} }
} }
if (!success) { if (!success) {
process.exit(1); throw new FatalAuthenticationError(
'Failed to authenticate with user code.',
);
} }
} else { } else {
const webLogin = await authWithWeb(client); const webLogin = await authWithWeb(client);
@@ -166,7 +168,7 @@ async function initOauthClient(
console.error( console.error(
'Failed to open browser automatically. Please try running again with NO_BROWSER=true set.', 'Failed to open browser automatically. Please try running again with NO_BROWSER=true set.',
); );
process.exit(1); throw new FatalAuthenticationError('Failed to open browser.');
}); });
} catch (err) { } catch (err) {
console.error( console.error(
@@ -174,7 +176,7 @@ async function initOauthClient(
err, err,
'\nPlease try running again with NO_BROWSER=true set.', '\nPlease try running again with NO_BROWSER=true set.',
); );
process.exit(1); throw new FatalAuthenticationError('Failed to open browser.');
} }
console.log('Waiting for authentication...'); console.log('Waiting for authentication...');

View File

@@ -25,6 +25,41 @@ export function getErrorMessage(error: unknown): string {
} }
} }
export class FatalError extends Error {
constructor(
message: string,
readonly exitCode: number,
) {
super(message);
}
}
export class FatalAuthenticationError extends FatalError {
constructor(message: string) {
super(message, 41);
}
}
export class FatalInputError extends FatalError {
constructor(message: string) {
super(message, 42);
}
}
export class FatalSandboxError extends FatalError {
constructor(message: string) {
super(message, 44);
}
}
export class FatalConfigError extends FatalError {
constructor(message: string) {
super(message, 52);
}
}
export class FatalTurnLimitedError extends FatalError {
constructor(message: string) {
super(message, 53);
}
}
export class ForbiddenError extends Error {} export class ForbiddenError extends Error {}
export class UnauthorizedError extends Error {} export class UnauthorizedError extends Error {}
export class BadRequestError extends Error {} export class BadRequestError extends Error {}