Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0

This commit is contained in:
mingholy.lmh
2025-09-10 21:01:40 +08:00
583 changed files with 30160 additions and 10770 deletions

View File

@@ -9,13 +9,15 @@ import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { quote, parse } from 'shell-quote';
import {
USER_SETTINGS_DIR,
SETTINGS_DIRECTORY_NAME,
} from '../config/settings.js';
import { promisify } from 'util';
import { Config, SandboxConfig } from '@qwen-code/qwen-code-core';
import { promisify } from 'node:util';
import type { Config, SandboxConfig } from '@qwen-code/qwen-code-core';
import { FatalSandboxError } from '@qwen-code/qwen-code-core';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
const execAsync = promisify(exec);
@@ -24,6 +26,7 @@ function getContainerPath(hostPath: string): string {
if (os.platform() !== 'win32') {
return hostPath;
}
const withForwardSlashes = hostPath.replace(/\\/g, '/');
const match = withForwardSlashes.match(/^([A-Z]):\/(.*)/i);
if (match) {
@@ -114,7 +117,7 @@ function ports(): string[] {
.map((p) => p.trim());
}
function entrypoint(workdir: string): string[] {
function entrypoint(workdir: string, cliArgs: string[]): string[] {
const isWindows = os.platform() === 'win32';
const containerWorkdir = getContainerPath(workdir);
const shellCmds = [];
@@ -166,7 +169,7 @@ function entrypoint(workdir: string): string[] {
),
);
const cliArgs = process.argv.slice(2).map((arg) => quote([arg]));
const quotedCliArgs = cliArgs.slice(2).map((arg) => quote([arg]));
const cliCmd =
process.env['NODE_ENV'] === 'development'
? process.env['DEBUG']
@@ -176,8 +179,7 @@ function entrypoint(workdir: string): string[] {
? `node --inspect-brk=0.0.0.0:${process.env['DEBUG_PORT'] || '9229'} $(which qwen)`
: 'qwen';
const args = [...shellCmds, cliCmd, ...cliArgs];
const args = [...shellCmds, cliCmd, ...quotedCliArgs];
return ['bash', '-c', args.join(' ')];
}
@@ -185,6 +187,7 @@ export async function start_sandbox(
config: SandboxConfig,
nodeArgs: string[] = [],
cliConfig?: Config,
cliArgs: string[] = [],
) {
const patcher = new ConsolePatcher({
debugMode: cliConfig?.getDebugMode() || !!process.env['DEBUG'],
@@ -196,12 +199,15 @@ export async function start_sandbox(
if (config.command === 'sandbox-exec') {
// disallow BUILD_SANDBOX
if (process.env['BUILD_SANDBOX']) {
console.error('ERROR: cannot BUILD_SANDBOX when using macOS Seatbelt');
process.exit(1);
throw new FatalSandboxError(
'Cannot BUILD_SANDBOX when using macOS Seatbelt',
);
}
const profile = (process.env['SEATBELT_PROFILE'] ??= 'permissive-open');
let profileFile = new URL(`sandbox-macos-${profile}.sb`, import.meta.url)
.pathname;
let profileFile = fileURLToPath(
new URL(`sandbox-macos-${profile}.sb`, import.meta.url),
);
// if profile name is not recognized, then look for file under project settings directory
if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) {
profileFile = path.join(
@@ -210,10 +216,9 @@ export async function start_sandbox(
);
}
if (!fs.existsSync(profileFile)) {
console.error(
`ERROR: missing macos seatbelt profile file '${profileFile}'`,
throw new FatalSandboxError(
`Missing macos seatbelt profile file '${profileFile}'`,
);
process.exit(1);
}
// Log on STDERR so it doesn't clutter the output on STDOUT
console.error(`using macos seatbelt (profile: ${profile}) ...`);
@@ -263,6 +268,8 @@ export async function start_sandbox(
args.push('-D', `INCLUDE_DIR_${i}=${dirPath}`);
}
const finalArgv = cliArgs;
args.push(
'-f',
profileFile,
@@ -271,7 +278,7 @@ export async function start_sandbox(
[
`SANDBOX=sandbox-exec`,
`NODE_OPTIONS="${nodeOptions}"`,
...process.argv.map((arg) => quote([arg])),
...finalArgv.map((arg) => quote([arg])),
].join(' '),
);
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
@@ -319,13 +326,12 @@ export async function start_sandbox(
console.error(data.toString());
});
proxyProcess.on('close', (code, signal) => {
console.error(
`ERROR: proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
);
if (sandboxProcess?.pid) {
process.kill(-sandboxProcess.pid, 'SIGTERM');
}
process.exit(1);
throw new FatalSandboxError(
`Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
);
});
console.log('waiting for proxy to start ...');
await execAsync(
@@ -360,11 +366,10 @@ export async function start_sandbox(
// note this can only be done with binary linked from gemini-cli repo
if (process.env['BUILD_SANDBOX']) {
if (!gcPath.includes('gemini-cli/packages/')) {
console.error(
'ERROR: cannot build sandbox using installed gemini binary; ' +
throw new FatalSandboxError(
'Cannot build sandbox using installed gemini binary; ' +
'run `npm link ./packages/cli` under gemini-cli repo to switch to linked binary.',
);
process.exit(1);
} else {
console.error('building sandbox ...');
const gcRoot = gcPath.split('/packages/')[0];
@@ -397,10 +402,9 @@ export async function start_sandbox(
image === LOCAL_DEV_SANDBOX_IMAGE_NAME
? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
: 'Please check the image name, your network connection, or notify gemini-cli-dev@google.com if the issue persists.';
console.error(
`ERROR: Sandbox image '${image}' is missing or could not be pulled. ${remedy}`,
throw new FatalSandboxError(
`Sandbox image '${image}' is missing or could not be pulled. ${remedy}`,
);
process.exit(1);
}
// use interactive mode and auto-remove container on exit
@@ -478,17 +482,15 @@ export async function start_sandbox(
mount = `${from}:${to}:${opts}`;
// check that from path is absolute
if (!path.isAbsolute(from)) {
console.error(
`ERROR: path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
throw new FatalSandboxError(
`Path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
);
process.exit(1);
}
// check that from path exists on host
if (!fs.existsSync(from)) {
console.error(
`ERROR: missing mount path '${from}' listed in SANDBOX_MOUNTS`,
throw new FatalSandboxError(
`Missing mount path '${from}' listed in SANDBOX_MOUNTS`,
);
process.exit(1);
}
console.error(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
args.push('--volume', mount);
@@ -674,10 +676,9 @@ export async function start_sandbox(
console.error(`SANDBOX_ENV: ${env}`);
args.push('--env', env);
} else {
console.error(
'ERROR: SANDBOX_ENV must be a comma-separated list of key=value pairs',
throw new FatalSandboxError(
'SANDBOX_ENV must be a comma-separated list of key=value pairs',
);
process.exit(1);
}
}
}
@@ -707,7 +708,7 @@ export async function start_sandbox(
// Determine if the current user's UID/GID should be passed to the sandbox.
// See shouldUseCurrentUserInSandbox for more details.
let userFlag = '';
const finalEntrypoint = entrypoint(workdir);
const finalEntrypoint = entrypoint(workdir, cliArgs);
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') {
args.push('--user', 'root');
@@ -785,13 +786,12 @@ export async function start_sandbox(
console.error(data.toString().trim());
});
proxyProcess.on('close', (code, signal) => {
console.error(
`ERROR: proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
);
if (sandboxProcess?.pid) {
process.kill(-sandboxProcess.pid, 'SIGTERM');
}
process.exit(1);
throw new FatalSandboxError(
`Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
);
});
console.log('waiting for proxy to start ...');
await execAsync(