mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Standardize exit codes (#7055)
This commit is contained in:
committed by
GitHub
parent
415a36a195
commit
7e31577813
@@ -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:**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 '';
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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...');
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
Reference in New Issue
Block a user