fix(cli)!: Default to interactive mode for positional arguments (#16329)

Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
Ishaan Gupta
2026-01-23 05:08:53 +05:30
committed by GitHub
parent a060e6149a
commit beacc4f6fd
3 changed files with 53 additions and 24 deletions

View File

@@ -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(),

View File

@@ -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);

View File

@@ -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(