mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
fix(cli)!: Default to interactive mode for positional arguments (#16329)
Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
@@ -371,6 +371,21 @@ describe('parseArguments', () => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it('should include a startup message when converting positional query to interactive prompt', async () => {
|
||||
const originalIsTTY = process.stdin.isTTY;
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', 'hello'];
|
||||
|
||||
try {
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
expect(argv.startupMessages).toContain(
|
||||
'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.',
|
||||
);
|
||||
} finally {
|
||||
process.stdin.isTTY = originalIsTTY;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -1953,7 +1968,7 @@ describe('loadCliConfig interactive', () => {
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be interactive if positional prompt words are provided with other flags', async () => {
|
||||
it('should be interactive if positional prompt words are provided with other flags', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-pro', 'Hello'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
@@ -1962,10 +1977,10 @@ describe('loadCliConfig interactive', () => {
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
expect(config.isInteractive()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be interactive if positional prompt words are provided with multiple flags', async () => {
|
||||
it('should be interactive if positional prompt words are provided with multiple flags', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = [
|
||||
'node',
|
||||
@@ -1981,13 +1996,13 @@ describe('loadCliConfig interactive', () => {
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
expect(config.isInteractive()).toBe(true);
|
||||
// Verify the question is preserved for one-shot execution
|
||||
expect(argv.prompt).toBe('Hello world');
|
||||
expect(argv.promptInteractive).toBeUndefined();
|
||||
expect(argv.prompt).toBeUndefined();
|
||||
expect(argv.promptInteractive).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('should not be interactive if positional prompt words are provided with extensions flag', async () => {
|
||||
it('should be interactive if positional prompt words are provided with extensions flag', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', '-e', 'none', 'hello'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
@@ -1996,8 +2011,9 @@ describe('loadCliConfig interactive', () => {
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
expect(config.isInteractive()).toBe(true);
|
||||
expect(argv.query).toBe('hello');
|
||||
expect(argv.promptInteractive).toBe('hello');
|
||||
expect(argv.extensions).toEqual(['none']);
|
||||
});
|
||||
|
||||
@@ -2010,9 +2026,9 @@ describe('loadCliConfig interactive', () => {
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
expect(config.isInteractive()).toBe(true);
|
||||
expect(argv.query).toBe('hello world how are you');
|
||||
expect(argv.prompt).toBe('hello world how are you');
|
||||
expect(argv.promptInteractive).toBe('hello world how are you');
|
||||
});
|
||||
|
||||
it('should handle multiple positional words with flags', async () => {
|
||||
@@ -2035,8 +2051,9 @@ describe('loadCliConfig interactive', () => {
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
expect(config.isInteractive()).toBe(true);
|
||||
expect(argv.query).toBe('write a function to sort array');
|
||||
expect(argv.promptInteractive).toBe('write a function to sort array');
|
||||
expect(argv.model).toBe('gemini-2.5-pro');
|
||||
});
|
||||
|
||||
@@ -2072,8 +2089,9 @@ describe('loadCliConfig interactive', () => {
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.isInteractive()).toBe(false);
|
||||
expect(config.isInteractive()).toBe(true);
|
||||
expect(argv.query).toBe('hello world how are you');
|
||||
expect(argv.promptInteractive).toBe('hello world how are you');
|
||||
expect(argv.extensions).toEqual(['none']);
|
||||
});
|
||||
|
||||
@@ -2708,9 +2726,9 @@ describe('PolicyEngine nonInteractive wiring', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should set nonInteractive to true in one-shot mode', async () => {
|
||||
it('should set nonInteractive to true when -p flag is used', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', 'echo hello']; // Positional query makes it one-shot
|
||||
process.argv = ['node', 'script.js', '-p', 'echo hello'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings(),
|
||||
|
||||
@@ -84,6 +84,7 @@ export interface CliArgs {
|
||||
outputFormat: string | undefined;
|
||||
fakeResponses: string | undefined;
|
||||
recordResponses: string | undefined;
|
||||
startupMessages?: string[];
|
||||
rawOutput: boolean | undefined;
|
||||
acceptRawOutputRisk: boolean | undefined;
|
||||
isCommand: boolean | undefined;
|
||||
@@ -93,11 +94,12 @@ export async function parseArguments(
|
||||
settings: MergedSettings,
|
||||
): Promise<CliArgs> {
|
||||
const rawArgv = hideBin(process.argv);
|
||||
const startupMessages: string[] = [];
|
||||
const yargsInstance = yargs(rawArgv)
|
||||
.locale('en')
|
||||
.scriptName('gemini')
|
||||
.usage(
|
||||
'Usage: gemini [options] [command]\n\nGemini CLI - Launch an interactive CLI, use -p/--prompt for non-interactive mode',
|
||||
'Usage: gemini [options] [command]\n\nGemini CLI - Defaults to interactive mode. Use -p/--prompt for non-interactive (headless) mode.',
|
||||
)
|
||||
.option('debug', {
|
||||
alias: 'd',
|
||||
@@ -109,7 +111,7 @@ export async function parseArguments(
|
||||
yargsInstance
|
||||
.positional('query', {
|
||||
description:
|
||||
'Positional prompt. Defaults to one-shot; use -i/--prompt-interactive for interactive.',
|
||||
'Initial prompt. Runs in interactive mode by default; use -p/--prompt for non-interactive.',
|
||||
})
|
||||
.option('model', {
|
||||
alias: 'm',
|
||||
@@ -121,7 +123,8 @@ export async function parseArguments(
|
||||
alias: 'p',
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
description: 'Prompt. Appended to input on stdin (if any).',
|
||||
description:
|
||||
'Run in non-interactive (headless) mode with the given prompt. Appended to input on stdin (if any).',
|
||||
})
|
||||
.option('prompt-interactive', {
|
||||
alias: 'i',
|
||||
@@ -342,11 +345,12 @@ export async function parseArguments(
|
||||
? queryArg.join(' ')
|
||||
: queryArg;
|
||||
|
||||
// Route positional args: explicit -i flag -> interactive; else -> one-shot (even for @commands)
|
||||
// -p/--prompt forces non-interactive mode; positional args default to interactive in TTY
|
||||
if (q && !result['prompt']) {
|
||||
const hasExplicitInteractive =
|
||||
result['promptInteractive'] === '' || !!result['promptInteractive'];
|
||||
if (hasExplicitInteractive) {
|
||||
if (process.stdin.isTTY) {
|
||||
startupMessages.push(
|
||||
'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.',
|
||||
);
|
||||
result['promptInteractive'] = q;
|
||||
} else {
|
||||
result['prompt'] = q;
|
||||
@@ -355,6 +359,7 @@ export async function parseArguments(
|
||||
|
||||
// Keep CliArgs.query as a string for downstream typing
|
||||
(result as Record<string, unknown>)['query'] = q || undefined;
|
||||
(result as Record<string, unknown>)['startupMessages'] = startupMessages;
|
||||
|
||||
// The import format is now only controlled by settings.memoryImportFormat
|
||||
// We no longer accept it as a CLI argument
|
||||
@@ -573,12 +578,12 @@ export async function loadCliConfig(
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Interactive mode: explicit -i flag or (TTY + no args + no -p flag)
|
||||
const hasQuery = !!argv.query;
|
||||
// -p/--prompt forces non-interactive (headless) mode
|
||||
// -i/--prompt-interactive forces interactive mode with an initial prompt
|
||||
const interactive =
|
||||
!!argv.promptInteractive ||
|
||||
!!argv.experimentalAcp ||
|
||||
(process.stdin.isTTY && !hasQuery && !argv.prompt && !argv.isCommand);
|
||||
(process.stdin.isTTY && !argv.query && !argv.prompt && !argv.isCommand);
|
||||
|
||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||
const allowedToolsSet = new Set(allowedTools);
|
||||
|
||||
@@ -324,6 +324,12 @@ export async function main() {
|
||||
const argv = await parseArguments(settings.merged);
|
||||
parseArgsHandle?.end();
|
||||
|
||||
if (argv.startupMessages) {
|
||||
argv.startupMessages.forEach((msg) => {
|
||||
coreEvents.emitFeedback('info', msg);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for invalid input combinations early to prevent crashes
|
||||
if (argv.promptInteractive && !process.stdin.isTTY) {
|
||||
writeToStderr(
|
||||
|
||||
Reference in New Issue
Block a user