summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/config/config.test.ts142
-rw-r--r--packages/cli/src/config/config.ts31
-rw-r--r--packages/cli/src/gemini.tsx61
-rw-r--r--packages/cli/src/nonInteractiveCli.ts1
4 files changed, 174 insertions, 61 deletions
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index 1d83ccbc..701ae267 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -8,6 +8,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
+import { ShellTool, EditTool, WriteFileTool } from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments } from './config.js';
import { Settings } from './settings.js';
import { Extension } from './extension.js';
@@ -561,6 +562,17 @@ describe('mergeMcpServers', () => {
});
describe('mergeExcludeTools', () => {
+ const defaultExcludes = [ShellTool.Name, EditTool.Name, WriteFileTool.Name];
+ const originalIsTTY = process.stdin.isTTY;
+
+ beforeEach(() => {
+ process.stdin.isTTY = true;
+ });
+
+ afterEach(() => {
+ process.stdin.isTTY = originalIsTTY;
+ });
+
it('should merge excludeTools from settings and extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const extensions: Extension[] = [
@@ -655,7 +667,8 @@ describe('mergeExcludeTools', () => {
expect(config.getExcludeTools()).toHaveLength(4);
});
- it('should return an empty array when no excludeTools are specified', async () => {
+ it('should return an empty array when no excludeTools are specified and it is interactive', async () => {
+ process.stdin.isTTY = true;
const settings: Settings = {};
const extensions: Extension[] = [];
process.argv = ['node', 'script.js'];
@@ -669,6 +682,21 @@ describe('mergeExcludeTools', () => {
expect(config.getExcludeTools()).toEqual([]);
});
+ it('should return default excludes when no excludeTools are specified and it is not interactive', async () => {
+ process.stdin.isTTY = false;
+ const settings: Settings = {};
+ const extensions: Extension[] = [];
+ process.argv = ['node', 'script.js', '-p', 'test'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig(
+ settings,
+ extensions,
+ 'test-session',
+ argv,
+ );
+ expect(config.getExcludeTools()).toEqual(defaultExcludes);
+ });
+
it('should handle settings with excludeTools but no extensions', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
@@ -1214,3 +1242,115 @@ describe('loadCliConfig chatCompression', () => {
expect(config.getChatCompression()).toBeUndefined();
});
});
+
+describe('loadCliConfig tool exclusions', () => {
+ const originalArgv = process.argv;
+ const originalEnv = { ...process.env };
+ const originalIsTTY = process.stdin.isTTY;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ process.env.GEMINI_API_KEY = 'test-api-key';
+ process.stdin.isTTY = true;
+ });
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ process.env = originalEnv;
+ process.stdin.isTTY = originalIsTTY;
+ vi.restoreAllMocks();
+ });
+
+ it('should not exclude interactive tools in interactive mode without YOLO', async () => {
+ process.stdin.isTTY = true;
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.getExcludeTools()).not.toContain('run_shell_command');
+ expect(config.getExcludeTools()).not.toContain('replace');
+ expect(config.getExcludeTools()).not.toContain('write_file');
+ });
+
+ it('should not exclude interactive tools in interactive mode with YOLO', async () => {
+ process.stdin.isTTY = true;
+ process.argv = ['node', 'script.js', '--yolo'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.getExcludeTools()).not.toContain('run_shell_command');
+ expect(config.getExcludeTools()).not.toContain('replace');
+ expect(config.getExcludeTools()).not.toContain('write_file');
+ });
+
+ it('should exclude interactive tools in non-interactive mode without YOLO', async () => {
+ process.stdin.isTTY = false;
+ process.argv = ['node', 'script.js', '-p', 'test'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.getExcludeTools()).toContain('run_shell_command');
+ expect(config.getExcludeTools()).toContain('replace');
+ expect(config.getExcludeTools()).toContain('write_file');
+ });
+
+ it('should not exclude interactive tools in non-interactive mode with YOLO', async () => {
+ process.stdin.isTTY = false;
+ process.argv = ['node', 'script.js', '-p', 'test', '--yolo'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.getExcludeTools()).not.toContain('run_shell_command');
+ expect(config.getExcludeTools()).not.toContain('replace');
+ expect(config.getExcludeTools()).not.toContain('write_file');
+ });
+});
+
+describe('loadCliConfig interactive', () => {
+ const originalArgv = process.argv;
+ const originalEnv = { ...process.env };
+ const originalIsTTY = process.stdin.isTTY;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ process.env.GEMINI_API_KEY = 'test-api-key';
+ process.stdin.isTTY = true;
+ });
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ process.env = originalEnv;
+ process.stdin.isTTY = originalIsTTY;
+ vi.restoreAllMocks();
+ });
+
+ it('should be interactive if isTTY and no prompt', async () => {
+ process.stdin.isTTY = true;
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.isInteractive()).toBe(true);
+ });
+
+ it('should be interactive if prompt-interactive is set', async () => {
+ process.stdin.isTTY = false;
+ process.argv = ['node', 'script.js', '--prompt-interactive', 'test'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.isInteractive()).toBe(true);
+ });
+
+ it('should not be interactive if not isTTY and no prompt', async () => {
+ process.stdin.isTTY = false;
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.isInteractive()).toBe(false);
+ });
+
+ it('should not be interactive if prompt is set', async () => {
+ process.stdin.isTTY = true;
+ process.argv = ['node', 'script.js', '--prompt', 'test'];
+ const argv = await parseArguments();
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.isInteractive()).toBe(false);
+ });
+});
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 3104e4c1..d142bd12 100644
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -23,6 +23,9 @@ import {
FileDiscoveryService,
TelemetryTarget,
FileFilteringOptions,
+ ShellTool,
+ EditTool,
+ WriteFileTool,
} from '@google/gemini-cli-core';
import { Settings } from './settings.js';
@@ -365,7 +368,22 @@ export async function loadCliConfig(
);
let mcpServers = mergeMcpServers(settings, activeExtensions);
- const excludeTools = mergeExcludeTools(settings, activeExtensions);
+ const question = argv.promptInteractive || argv.prompt || '';
+ const approvalMode =
+ argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT;
+ const interactive =
+ !!argv.promptInteractive || (process.stdin.isTTY && question.length === 0);
+ // In non-interactive and non-yolo mode, exclude interactive built in tools.
+ const extraExcludes =
+ !interactive && approvalMode !== ApprovalMode.YOLO
+ ? [ShellTool.Name, EditTool.Name, WriteFileTool.Name]
+ : undefined;
+
+ const excludeTools = mergeExcludeTools(
+ settings,
+ activeExtensions,
+ extraExcludes,
+ );
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
if (!argv.allowedMcpServerNames) {
@@ -427,7 +445,7 @@ export async function loadCliConfig(
settings.loadMemoryFromIncludeDirectories ||
false,
debugMode,
- question: argv.promptInteractive || argv.prompt || '',
+ question,
fullContext: argv.allFiles || argv.all_files || false,
coreTools: settings.coreTools || undefined,
excludeTools,
@@ -437,7 +455,7 @@ export async function loadCliConfig(
mcpServers,
userMemory: memoryContent,
geminiMdFileCount: fileCount,
- approvalMode: argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT,
+ approvalMode,
showMemoryUsage:
argv.showMemoryUsage ||
argv.show_memory_usage ||
@@ -486,6 +504,7 @@ export async function loadCliConfig(
ideModeFeature,
chatCompression: settings.chatCompression,
folderTrustFeature,
+ interactive,
folderTrust,
});
}
@@ -514,8 +533,12 @@ function mergeMcpServers(settings: Settings, extensions: Extension[]) {
function mergeExcludeTools(
settings: Settings,
extensions: Extension[],
+ extraExcludes?: string[] | undefined,
): string[] {
- const allExcludeTools = new Set(settings.excludeTools || []);
+ const allExcludeTools = new Set([
+ ...(settings.excludeTools || []),
+ ...(extraExcludes || []),
+ ]);
for (const extension of extensions) {
for (const tool of extension.config.excludeTools || []) {
allExcludeTools.add(tool);
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 48dbd271..771fcacb 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { render } from 'ink';
import { AppWrapper } from './ui/App.js';
-import { loadCliConfig, parseArguments, CliArgs } from './config/config.js';
+import { loadCliConfig, parseArguments } from './config/config.js';
import { readStdin } from './utils/readStdin.js';
import { basename } from 'node:path';
import v8 from 'node:v8';
@@ -25,15 +25,11 @@ import { themeManager } from './ui/themes/theme-manager.js';
import { getStartupWarnings } from './utils/startupWarnings.js';
import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
import { runNonInteractive } from './nonInteractiveCli.js';
-import { loadExtensions, Extension } from './config/extension.js';
+import { loadExtensions } from './config/extension.js';
import { cleanupCheckpoints, registerCleanup } from './utils/cleanup.js';
import { getCliVersion } from './utils/version.js';
import {
- ApprovalMode,
Config,
- EditTool,
- ShellTool,
- WriteFileTool,
sessionId,
logUserPrompt,
AuthType,
@@ -255,11 +251,8 @@ export async function main() {
...(await getUserStartupWarnings(workspaceRoot)),
];
- const shouldBeInteractive =
- !!argv.promptInteractive || (process.stdin.isTTY && input?.length === 0);
-
// Render UI, passing necessary config values. Check that there is no command line question.
- if (shouldBeInteractive) {
+ if (config.isInteractive()) {
const version = await getCliVersion();
setWindowTitle(basename(workspaceRoot), settings);
const instance = render(
@@ -308,12 +301,10 @@ export async function main() {
prompt_length: input.length,
});
- // Non-interactive mode handled by runNonInteractive
- const nonInteractiveConfig = await loadNonInteractiveConfig(
+ const nonInteractiveConfig = await validateNonInteractiveAuth(
+ settings.merged.selectedAuthType,
+ settings.merged.useExternalAuth,
config,
- extensions,
- settings,
- argv,
);
await runNonInteractive(nonInteractiveConfig, input, prompt_id);
@@ -334,43 +325,3 @@ function setWindowTitle(title: string, settings: LoadedSettings) {
});
}
}
-
-async function loadNonInteractiveConfig(
- config: Config,
- extensions: Extension[],
- settings: LoadedSettings,
- argv: CliArgs,
-) {
- let finalConfig = config;
- if (config.getApprovalMode() !== ApprovalMode.YOLO) {
- // Everything is not allowed, ensure that only read-only tools are configured.
- const existingExcludeTools = settings.merged.excludeTools || [];
- const interactiveTools = [
- ShellTool.Name,
- EditTool.Name,
- WriteFileTool.Name,
- ];
-
- const newExcludeTools = [
- ...new Set([...existingExcludeTools, ...interactiveTools]),
- ];
-
- const nonInteractiveSettings = {
- ...settings.merged,
- excludeTools: newExcludeTools,
- };
- finalConfig = await loadCliConfig(
- nonInteractiveSettings,
- extensions,
- config.getSessionId(),
- argv,
- );
- await finalConfig.initialize();
- }
-
- return await validateNonInteractiveAuth(
- settings.merged.selectedAuthType,
- settings.merged.useExternalAuth,
- finalConfig,
- );
-}
diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts
index 8b056a28..95ed70cf 100644
--- a/packages/cli/src/nonInteractiveCli.ts
+++ b/packages/cli/src/nonInteractiveCli.ts
@@ -30,7 +30,6 @@ export async function runNonInteractive(
});
try {
- await config.initialize();
consolePatcher.patch();
// Handle EPIPE errors when the output is piped to a command that closes early.
process.stdout.on('error', (err: NodeJS.ErrnoException) => {