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