# 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483)

This commit is contained in:
tanzhenxin
2025-09-01 14:48:55 +08:00
committed by GitHub
parent 1610c1586e
commit 2572faf726
292 changed files with 19401 additions and 5941 deletions

View File

@@ -5,14 +5,26 @@
*/
export async function readStdin(): Promise<string> {
const MAX_STDIN_SIZE = 8 * 1024 * 1024; // 8MB
return new Promise((resolve, reject) => {
let data = '';
let totalSize = 0;
process.stdin.setEncoding('utf8');
const onReadable = () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
if (totalSize + chunk.length > MAX_STDIN_SIZE) {
const remainingSize = MAX_STDIN_SIZE - totalSize;
data += chunk.slice(0, remainingSize);
console.warn(
`Warning: stdin input truncated to ${MAX_STDIN_SIZE} bytes.`,
);
process.stdin.destroy(); // Stop reading further
break;
}
data += chunk;
totalSize += chunk.length;
}
};

View File

@@ -63,7 +63,7 @@ const BUILTIN_SEATBELT_PROFILES = [
* @returns {Promise<boolean>} A promise that resolves to true if the current user's UID/GID should be used, false otherwise.
*/
async function shouldUseCurrentUserInSandbox(): Promise<boolean> {
const envVar = process.env.SANDBOX_SET_UID_GID?.toLowerCase().trim();
const envVar = process.env['SANDBOX_SET_UID_GID']?.toLowerCase().trim();
if (envVar === '1' || envVar === 'true') {
return true;
@@ -108,7 +108,7 @@ function parseImageName(image: string): string {
}
function ports(): string[] {
return (process.env.SANDBOX_PORTS ?? '')
return (process.env['SANDBOX_PORTS'] ?? '')
.split(',')
.filter((p) => p.trim())
.map((p) => p.trim());
@@ -121,8 +121,8 @@ function entrypoint(workdir: string): string[] {
const pathSeparator = isWindows ? ';' : ':';
let pathSuffix = '';
if (process.env.PATH) {
const paths = process.env.PATH.split(pathSeparator);
if (process.env['PATH']) {
const paths = process.env['PATH'].split(pathSeparator);
for (const p of paths) {
const containerPath = getContainerPath(p);
if (
@@ -137,8 +137,8 @@ function entrypoint(workdir: string): string[] {
}
let pythonPathSuffix = '';
if (process.env.PYTHONPATH) {
const paths = process.env.PYTHONPATH.split(pathSeparator);
if (process.env['PYTHONPATH']) {
const paths = process.env['PYTHONPATH'].split(pathSeparator);
for (const p of paths) {
const containerPath = getContainerPath(p);
if (
@@ -168,12 +168,12 @@ function entrypoint(workdir: string): string[] {
const cliArgs = process.argv.slice(2).map((arg) => quote([arg]));
const cliCmd =
process.env.NODE_ENV === 'development'
? process.env.DEBUG
process.env['NODE_ENV'] === 'development'
? process.env['DEBUG']
? 'npm run debug --'
: 'npm rebuild && npm run start --'
: process.env.DEBUG
? `node --inspect-brk=0.0.0.0:${process.env.DEBUG_PORT || '9229'} $(which qwen)`
: process.env['DEBUG']
? `node --inspect-brk=0.0.0.0:${process.env['DEBUG_PORT'] || '9229'} $(which qwen)`
: 'qwen';
const args = [...shellCmds, cliCmd, ...cliArgs];
@@ -187,7 +187,7 @@ export async function start_sandbox(
cliConfig?: Config,
) {
const patcher = new ConsolePatcher({
debugMode: cliConfig?.getDebugMode() || !!process.env.DEBUG,
debugMode: cliConfig?.getDebugMode() || !!process.env['DEBUG'],
stderr: true,
});
patcher.patch();
@@ -195,11 +195,11 @@ export async function start_sandbox(
try {
if (config.command === 'sandbox-exec') {
// disallow BUILD_SANDBOX
if (process.env.BUILD_SANDBOX) {
if (process.env['BUILD_SANDBOX']) {
console.error('ERROR: cannot BUILD_SANDBOX when using macOS Seatbelt');
process.exit(1);
}
const profile = (process.env.SEATBELT_PROFILE ??= 'permissive-open');
const profile = (process.env['SEATBELT_PROFILE'] ??= 'permissive-open');
let profileFile = new URL(`sandbox-macos-${profile}.sb`, import.meta.url)
.pathname;
// if profile name is not recognized, then look for file under project settings directory
@@ -219,7 +219,7 @@ export async function start_sandbox(
console.error(`using macos seatbelt (profile: ${profile}) ...`);
// if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS
const nodeOptions = [
...(process.env.DEBUG ? ['--inspect-brk'] : []),
...(process.env['DEBUG'] ? ['--inspect-brk'] : []),
...nodeArgs,
].join(' ');
@@ -275,22 +275,22 @@ export async function start_sandbox(
].join(' '),
);
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND;
const proxyCommand = process.env['GEMINI_SANDBOX_PROXY_COMMAND'];
let proxyProcess: ChildProcess | undefined = undefined;
let sandboxProcess: ChildProcess | undefined = undefined;
const sandboxEnv = { ...process.env };
if (proxyCommand) {
const proxy =
process.env.HTTPS_PROXY ||
process.env.https_proxy ||
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env['HTTPS_PROXY'] ||
process.env['https_proxy'] ||
process.env['HTTP_PROXY'] ||
process.env['http_proxy'] ||
'http://localhost:8877';
sandboxEnv['HTTPS_PROXY'] = proxy;
sandboxEnv['https_proxy'] = proxy; // lower-case can be required, e.g. for curl
sandboxEnv['HTTP_PROXY'] = proxy;
sandboxEnv['http_proxy'] = proxy;
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'];
if (noProxy) {
sandboxEnv['NO_PROXY'] = noProxy;
sandboxEnv['no_proxy'] = noProxy;
@@ -358,7 +358,7 @@ export async function start_sandbox(
// if BUILD_SANDBOX is set, then call scripts/build_sandbox.js under 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/')) {
console.error(
'ERROR: cannot build sandbox using installed gemini binary; ' +
@@ -408,8 +408,8 @@ export async function start_sandbox(
const args = ['run', '-i', '--rm', '--init', '--workdir', containerWorkdir];
// add custom flags from SANDBOX_FLAGS
if (process.env.SANDBOX_FLAGS) {
const flags = parse(process.env.SANDBOX_FLAGS, process.env).filter(
if (process.env['SANDBOX_FLAGS']) {
const flags = parse(process.env['SANDBOX_FLAGS'], process.env).filter(
(f): f is string => typeof f === 'string',
);
args.push(...flags);
@@ -456,8 +456,8 @@ export async function start_sandbox(
}
// mount ADC file if GOOGLE_APPLICATION_CREDENTIALS is set
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
const adcFile = process.env.GOOGLE_APPLICATION_CREDENTIALS;
if (process.env['GOOGLE_APPLICATION_CREDENTIALS']) {
const adcFile = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
if (fs.existsSync(adcFile)) {
args.push('--volume', `${adcFile}:${getContainerPath(adcFile)}:ro`);
args.push(
@@ -468,8 +468,8 @@ export async function start_sandbox(
}
// mount paths listed in SANDBOX_MOUNTS
if (process.env.SANDBOX_MOUNTS) {
for (let mount of process.env.SANDBOX_MOUNTS.split(',')) {
if (process.env['SANDBOX_MOUNTS']) {
for (let mount of process.env['SANDBOX_MOUNTS'].split(',')) {
if (mount.trim()) {
// parse mount as from:to:opts
let [from, to, opts] = mount.trim().split(':');
@@ -500,22 +500,22 @@ export async function start_sandbox(
ports().forEach((p) => args.push('--publish', `${p}:${p}`));
// if DEBUG is set, expose debugging port
if (process.env.DEBUG) {
const debugPort = process.env.DEBUG_PORT || '9229';
if (process.env['DEBUG']) {
const debugPort = process.env['DEBUG_PORT'] || '9229';
args.push(`--publish`, `${debugPort}:${debugPort}`);
}
// copy proxy environment variables, replacing localhost with SANDBOX_PROXY_NAME
// copy as both upper-case and lower-case as is required by some utilities
// GEMINI_SANDBOX_PROXY_COMMAND implies HTTPS_PROXY unless HTTP_PROXY is set
const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND;
const proxyCommand = process.env['GEMINI_SANDBOX_PROXY_COMMAND'];
if (proxyCommand) {
let proxy =
process.env.HTTPS_PROXY ||
process.env.https_proxy ||
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env['HTTPS_PROXY'] ||
process.env['https_proxy'] ||
process.env['HTTP_PROXY'] ||
process.env['http_proxy'] ||
'http://localhost:8877';
proxy = proxy.replace('localhost', SANDBOX_PROXY_NAME);
if (proxy) {
@@ -524,7 +524,7 @@ export async function start_sandbox(
args.push('--env', `HTTP_PROXY=${proxy}`);
args.push('--env', `http_proxy=${proxy}`);
}
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'];
if (noProxy) {
args.push('--env', `NO_PROXY=${noProxy}`);
args.push('--env', `no_proxy=${noProxy}`);
@@ -562,71 +562,71 @@ export async function start_sandbox(
args.push('--name', containerName, '--hostname', containerName);
// copy GEMINI_API_KEY(s)
if (process.env.GEMINI_API_KEY) {
args.push('--env', `GEMINI_API_KEY=${process.env.GEMINI_API_KEY}`);
if (process.env['GEMINI_API_KEY']) {
args.push('--env', `GEMINI_API_KEY=${process.env['GEMINI_API_KEY']}`);
}
if (process.env.GOOGLE_API_KEY) {
args.push('--env', `GOOGLE_API_KEY=${process.env.GOOGLE_API_KEY}`);
if (process.env['GOOGLE_API_KEY']) {
args.push('--env', `GOOGLE_API_KEY=${process.env['GOOGLE_API_KEY']}`);
}
// copy OPENAI_API_KEY and related env vars for Qwen
if (process.env.OPENAI_API_KEY) {
args.push('--env', `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}`);
if (process.env['OPENAI_API_KEY']) {
args.push('--env', `OPENAI_API_KEY=${process.env['OPENAI_API_KEY']}`);
}
// copy TAVILY_API_KEY for web search tool
if (process.env.TAVILY_API_KEY) {
args.push('--env', `TAVILY_API_KEY=${process.env.TAVILY_API_KEY}`);
if (process.env['TAVILY_API_KEY']) {
args.push('--env', `TAVILY_API_KEY=${process.env['TAVILY_API_KEY']}`);
}
if (process.env.OPENAI_BASE_URL) {
args.push('--env', `OPENAI_BASE_URL=${process.env.OPENAI_BASE_URL}`);
if (process.env['OPENAI_BASE_URL']) {
args.push('--env', `OPENAI_BASE_URL=${process.env['OPENAI_BASE_URL']}`);
}
if (process.env.OPENAI_MODEL) {
args.push('--env', `OPENAI_MODEL=${process.env.OPENAI_MODEL}`);
if (process.env['OPENAI_MODEL']) {
args.push('--env', `OPENAI_MODEL=${process.env['OPENAI_MODEL']}`);
}
// copy GOOGLE_GENAI_USE_VERTEXAI
if (process.env.GOOGLE_GENAI_USE_VERTEXAI) {
if (process.env['GOOGLE_GENAI_USE_VERTEXAI']) {
args.push(
'--env',
`GOOGLE_GENAI_USE_VERTEXAI=${process.env.GOOGLE_GENAI_USE_VERTEXAI}`,
`GOOGLE_GENAI_USE_VERTEXAI=${process.env['GOOGLE_GENAI_USE_VERTEXAI']}`,
);
}
// copy GOOGLE_GENAI_USE_GCA
if (process.env.GOOGLE_GENAI_USE_GCA) {
if (process.env['GOOGLE_GENAI_USE_GCA']) {
args.push(
'--env',
`GOOGLE_GENAI_USE_GCA=${process.env.GOOGLE_GENAI_USE_GCA}`,
`GOOGLE_GENAI_USE_GCA=${process.env['GOOGLE_GENAI_USE_GCA']}`,
);
}
// copy GOOGLE_CLOUD_PROJECT
if (process.env.GOOGLE_CLOUD_PROJECT) {
if (process.env['GOOGLE_CLOUD_PROJECT']) {
args.push(
'--env',
`GOOGLE_CLOUD_PROJECT=${process.env.GOOGLE_CLOUD_PROJECT}`,
`GOOGLE_CLOUD_PROJECT=${process.env['GOOGLE_CLOUD_PROJECT']}`,
);
}
// copy GOOGLE_CLOUD_LOCATION
if (process.env.GOOGLE_CLOUD_LOCATION) {
if (process.env['GOOGLE_CLOUD_LOCATION']) {
args.push(
'--env',
`GOOGLE_CLOUD_LOCATION=${process.env.GOOGLE_CLOUD_LOCATION}`,
`GOOGLE_CLOUD_LOCATION=${process.env['GOOGLE_CLOUD_LOCATION']}`,
);
}
// copy GEMINI_MODEL
if (process.env.GEMINI_MODEL) {
args.push('--env', `GEMINI_MODEL=${process.env.GEMINI_MODEL}`);
if (process.env['GEMINI_MODEL']) {
args.push('--env', `GEMINI_MODEL=${process.env['GEMINI_MODEL']}`);
}
// copy TERM and COLORTERM to try to maintain terminal setup
if (process.env.TERM) {
args.push('--env', `TERM=${process.env.TERM}`);
if (process.env['TERM']) {
args.push('--env', `TERM=${process.env['TERM']}`);
}
if (process.env.COLORTERM) {
args.push('--env', `COLORTERM=${process.env.COLORTERM}`);
if (process.env['COLORTERM']) {
args.push('--env', `COLORTERM=${process.env['COLORTERM']}`);
}
// Pass through IDE mode environment variables
@@ -645,7 +645,9 @@ export async function start_sandbox(
// sandbox can then set up this new VIRTUAL_ENV directory using sandbox.bashrc (see below)
// directory will be empty if not set up, which is still preferable to having host binaries
if (
process.env.VIRTUAL_ENV?.toLowerCase().startsWith(workdir.toLowerCase())
process.env['VIRTUAL_ENV']
?.toLowerCase()
.startsWith(workdir.toLowerCase())
) {
const sandboxVenvPath = path.resolve(
SETTINGS_DIRECTORY_NAME,
@@ -656,17 +658,17 @@ export async function start_sandbox(
}
args.push(
'--volume',
`${sandboxVenvPath}:${getContainerPath(process.env.VIRTUAL_ENV)}`,
`${sandboxVenvPath}:${getContainerPath(process.env['VIRTUAL_ENV'])}`,
);
args.push(
'--env',
`VIRTUAL_ENV=${getContainerPath(process.env.VIRTUAL_ENV)}`,
`VIRTUAL_ENV=${getContainerPath(process.env['VIRTUAL_ENV'])}`,
);
}
// copy additional environment variables from SANDBOX_ENV
if (process.env.SANDBOX_ENV) {
for (let env of process.env.SANDBOX_ENV.split(',')) {
if (process.env['SANDBOX_ENV']) {
for (let env of process.env['SANDBOX_ENV'].split(',')) {
if ((env = env.trim())) {
if (env.includes('=')) {
console.error(`SANDBOX_ENV: ${env}`);
@@ -682,7 +684,7 @@ export async function start_sandbox(
}
// copy NODE_OPTIONS
const existingNodeOptions = process.env.NODE_OPTIONS || '';
const existingNodeOptions = process.env['NODE_OPTIONS'] || '';
const allNodeOptions = [
...(existingNodeOptions ? [existingNodeOptions] : []),
...nodeArgs,
@@ -707,7 +709,7 @@ export async function start_sandbox(
let userFlag = '';
const finalEntrypoint = entrypoint(workdir);
if (process.env.GEMINI_CLI_INTEGRATION_TEST === 'true') {
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') {
args.push('--user', 'root');
userFlag = '--user root';
} else if (await shouldUseCurrentUserInSandbox()) {

View File

@@ -239,7 +239,7 @@ describe('SettingsUtils', () => {
expect(shouldShowInDialog('showMemoryUsage')).toBe(true);
expect(shouldShowInDialog('vimMode')).toBe(true);
expect(shouldShowInDialog('hideWindowTitle')).toBe(true);
expect(shouldShowInDialog('usageStatisticsEnabled')).toBe(true);
expect(shouldShowInDialog('usageStatisticsEnabled')).toBe(false);
});
it('should return false for settings marked to hide from dialog', () => {
@@ -286,7 +286,7 @@ describe('SettingsUtils', () => {
expect(allKeys).toContain('ideMode');
expect(allKeys).toContain('disableAutoUpdate');
expect(allKeys).toContain('showMemoryUsage');
expect(allKeys).toContain('usageStatisticsEnabled');
expect(allKeys).not.toContain('usageStatisticsEnabled');
expect(allKeys).not.toContain('selectedAuthType');
expect(allKeys).not.toContain('coreTools');
expect(allKeys).not.toContain('theme'); // Now hidden
@@ -302,7 +302,7 @@ describe('SettingsUtils', () => {
expect(keys).toContain('showMemoryUsage');
expect(keys).toContain('vimMode');
expect(keys).toContain('hideWindowTitle');
expect(keys).toContain('usageStatisticsEnabled');
expect(keys).not.toContain('usageStatisticsEnabled');
expect(keys).not.toContain('selectedAuthType'); // Advanced setting
expect(keys).not.toContain('useExternalAuth'); // Advanced setting
});
@@ -329,7 +329,7 @@ describe('SettingsUtils', () => {
expect(dialogKeys).toContain('showMemoryUsage');
expect(dialogKeys).toContain('vimMode');
expect(dialogKeys).toContain('hideWindowTitle');
expect(dialogKeys).toContain('usageStatisticsEnabled');
expect(dialogKeys).not.toContain('usageStatisticsEnabled');
expect(dialogKeys).toContain('ideMode');
expect(dialogKeys).toContain('disableAutoUpdate');
@@ -392,7 +392,7 @@ describe('SettingsUtils', () => {
new Set(),
updatedPendingSettings,
);
expect(displayValue).toBe('true*'); // Should show true with * indicating change
expect(displayValue).toBe('true'); // Should show true (no * since value matches default)
// Test that modified settings also show the * indicator
const modifiedSettings = new Set([key]);
@@ -602,7 +602,7 @@ describe('SettingsUtils', () => {
mergedSettings,
modifiedSettings,
);
expect(result).toBe('false'); // matches default, no *
expect(result).toBe('false*');
});
it('should show default value when setting is not in scope', () => {

View File

@@ -91,7 +91,10 @@ export function getRestartRequiredSettings(): string[] {
/**
* Recursively gets a value from a nested object using a key path array.
*/
function getNestedValue(obj: Record<string, unknown>, path: string[]): unknown {
export function getNestedValue(
obj: Record<string, unknown>,
path: string[],
): unknown {
const [first, ...rest] = path;
if (!first || !(first in obj)) {
return undefined;
@@ -332,6 +335,20 @@ export function setPendingSettingValue(
return newSettings;
}
/**
* Generic setter: Set a setting value (boolean, number, string, etc.) in the pending settings
*/
export function setPendingSettingValueAny(
key: string,
value: unknown,
pendingSettings: Settings,
): Settings {
const path = key.split('.');
const newSettings = structuredClone(pendingSettings);
setNestedValue(newSettings, path, value);
return newSettings;
}
/**
* Check if any modified settings require a restart
*/
@@ -382,11 +399,9 @@ export function saveModifiedSettings(
// We need to set the whole parent object.
const [parentKey] = path;
if (parentKey) {
// Ensure value is a boolean for setPendingSettingValue
const booleanValue = typeof value === 'boolean' ? value : false;
const newParentValue = setPendingSettingValue(
const newParentValue = setPendingSettingValueAny(
settingKey,
booleanValue,
value,
loadedSettings.forScope(scope).settings,
)[parentKey as keyof Settings];
@@ -431,11 +446,12 @@ export function getDisplayValue(
const isChangedFromDefault =
typeof defaultValue === 'boolean' ? value !== defaultValue : value === true;
const isInModifiedSettings = modifiedSettings.has(key);
const hasPendingChanges =
pendingSettings && settingExistsInScope(key, pendingSettings);
// Add * indicator when value differs from default, is in modified settings, or has pending changes
if (isChangedFromDefault || isInModifiedSettings || hasPendingChanges) {
// Mark as modified if setting exists in current scope OR is in modified settings
if (settingExistsInScope(key, settings) || isInModifiedSettings) {
return `${valueString}*`; // * indicates setting is set in current scope
}
if (isChangedFromDefault || isInModifiedSettings) {
return `${valueString}*`; // * indicates changed from default value
}

View File

@@ -8,5 +8,5 @@ import { getPackageJson } from './package.js';
export async function getCliVersion(): Promise<string> {
const pkgJson = await getPackageJson();
return process.env.CLI_VERSION || pkgJson?.version || 'unknown';
return process.env['CLI_VERSION'] || pkgJson?.version || 'unknown';
}