feat(cli): add bare startup mode (#3448)

* feat(cli): add bare startup mode

Skip implicit startup discovery in bare mode while keeping explicit inputs such as include directories and extension overrides.

Add a repository plan document and targeted tests for config, startup, skills, extensions, and memory discovery.

* fix(bare): enforce explicit-only startup behavior

* fix(cli): preserve bare tools in non-interactive mode

* chore(docs): remove bare mode planning note
This commit is contained in:
易良 2026-04-20 10:01:59 +08:00 committed by GitHub
parent cfe142e9a3
commit 41f71ab7e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 750 additions and 72 deletions

View file

@ -28,6 +28,7 @@ import {
NativeLspClient,
createDebugLogger,
NativeLspService,
isBareMode,
isToolEnabled,
} from '@qwen-code/qwen-code-core';
import { extensionsCommand } from '../commands/extensions.js';
@ -115,6 +116,7 @@ export interface CliArgs {
systemPrompt: string | undefined;
appendSystemPrompt: string | undefined;
yolo: boolean | undefined;
bare: boolean | undefined;
approvalMode: string | undefined;
telemetry: boolean | undefined;
checkpointing: boolean | undefined;
@ -260,6 +262,12 @@ export async function parseArguments(): Promise<CliArgs> {
description: 'Run in debug mode?',
default: false,
})
.option('bare', {
type: 'boolean',
description:
'Minimal mode: skip implicit startup auto-discovery and only honor explicitly provided CLI inputs.',
default: false,
})
.option('proxy', {
type: 'string',
description: 'Proxy for Qwen Code, like schema://user:password@host:port',
@ -737,6 +745,7 @@ export async function loadCliConfig(
},
): Promise<Config> {
const debugMode = isDebugMode(argv);
const bareMode = isBareMode(argv.bare);
// Set runtime output directory from settings (env var QWEN_RUNTIME_DIR
// is auto-detected inside getRuntimeBaseDir() at each call site).
@ -771,20 +780,24 @@ export async function loadCliConfig(
);
let outputLanguageFilePath: string | undefined;
if (fs.existsSync(projectOutputLanguagePath)) {
outputLanguageFilePath = projectOutputLanguagePath;
} else if (fs.existsSync(globalOutputLanguagePath)) {
outputLanguageFilePath = globalOutputLanguagePath;
if (!bareMode) {
if (fs.existsSync(projectOutputLanguagePath)) {
outputLanguageFilePath = projectOutputLanguagePath;
} else if (fs.existsSync(globalOutputLanguagePath)) {
outputLanguageFilePath = globalOutputLanguagePath;
}
}
const fileService = new FileDiscoveryService(cwd);
const includeDirectories = (settings.context?.includeDirectories || [])
const includeDirectories = (
bareMode ? [] : (settings.context?.includeDirectories ?? [])
)
.map(resolvePath)
.concat((argv.includeDirectories || []).map(resolvePath));
// LSP configuration: enabled only via --experimental-lsp flag
const lspEnabled = argv.experimentalLsp === true;
const lspEnabled = !bareMode && argv.experimentalLsp === true;
let lspClient: LspClient | undefined;
const question = argv.promptInteractive || argv.prompt || '';
const inputFormat: InputFormat =
@ -810,7 +823,7 @@ export async function loadCliConfig(
approvalMode = parseApprovalModeValue(argv.approvalMode);
} else if (argv.yolo) {
approvalMode = ApprovalMode.YOLO;
} else if (settings.tools?.approvalMode) {
} else if (!bareMode && settings.tools?.approvalMode) {
approvalMode = parseApprovalModeValue(settings.tools.approvalMode);
} else {
approvalMode = ApprovalMode.DEFAULT;
@ -888,17 +901,19 @@ export async function loadCliConfig(
// not auto-approve semantics. They are passed via the `coreTools` Config param
// and handled by PermissionManager.coreToolsAllowList.
const resolvedCoreTools: string[] = [
...(argv.coreTools ?? []),
...(settings.tools?.core ?? []),
...(bareMode ? [] : (argv.coreTools ?? [])),
...(bareMode ? [] : (settings.tools?.core ?? [])),
];
const mergedAllow: string[] = [
...(settings.permissions?.allow ?? []),
...(settings.tools?.allowed ?? []),
...(bareMode ? [] : (settings.permissions?.allow ?? [])),
...(bareMode ? [] : (settings.tools?.allowed ?? [])),
];
const mergedAsk: string[] = [
...(bareMode ? [] : (settings.permissions?.ask ?? [])),
];
const mergedAsk: string[] = [...(settings.permissions?.ask ?? [])];
const mergedDeny: string[] = [
...(settings.permissions?.deny ?? []),
...(settings.tools?.exclude ?? []),
...(bareMode ? [] : (settings.permissions?.deny ?? [])),
...(bareMode ? [] : (settings.tools?.exclude ?? [])),
];
// argv.allowedTools adds allow rules (auto-approve).
@ -941,7 +956,12 @@ export async function loadCliConfig(
// the caller has explicitly allowed them. Stream-JSON input is excluded from
// this logic because approval can be sent programmatically via JSON messages.
const isAcpMode = argv.acp || argv.experimentalAcp;
if (!interactive && !isAcpMode && inputFormat !== InputFormat.STREAM_JSON) {
if (
!bareMode &&
!interactive &&
!isAcpMode &&
inputFormat !== InputFormat.STREAM_JSON
) {
const denyUnlessAllowed = (toolName: ToolName): void => {
if (!isExplicitlyAllowed(toolName)) {
const name = toolName as string;
@ -974,7 +994,7 @@ export async function loadCliConfig(
if (argv.allowedMcpServerNames) {
allowedMcpServers = new Set(argv.allowedMcpServerNames.filter(Boolean));
excludedMcpServers = undefined;
} else {
} else if (!bareMode) {
allowedMcpServers = settings.mcp?.allowed
? new Set(settings.mcp.allowed.filter(Boolean))
: undefined;
@ -985,7 +1005,7 @@ export async function loadCliConfig(
const selectedAuthType =
(argv.authType as AuthType | undefined) ||
settings.security?.auth?.selectedType ||
(bareMode ? undefined : settings.security?.auth?.selectedType) ||
/* getAuthTypeFromEnv means no authType was explicitly provided, we infer the authType from env vars */
getAuthTypeFromEnv();
@ -1005,7 +1025,10 @@ export async function loadCliConfig(
const { model: resolvedModel } = resolvedCliConfig;
const sandboxConfig = await loadSandboxConfig(settings, argv);
const sandboxConfig = await loadSandboxConfig(
bareMode ? ({} as Settings) : settings,
argv,
);
const screenReader =
argv.screenReader !== undefined
? argv.screenReader
@ -1054,16 +1077,21 @@ export async function loadCliConfig(
sandbox: sandboxConfig,
targetDir: cwd,
includeDirectories,
loadMemoryFromIncludeDirectories:
settings.context?.loadFromIncludeDirectories || false,
loadMemoryFromIncludeDirectories: bareMode
? includeDirectories.length > 0
: (settings.context?.loadFromIncludeDirectories ?? false),
importFormat: settings.context?.importFormat || 'tree',
debugMode,
question,
systemPrompt: argv.systemPrompt,
appendSystemPrompt: argv.appendSystemPrompt,
// Legacy fields kept for backward compatibility with getCoreTools() etc.
coreTools: argv.coreTools || settings.tools?.core || undefined,
allowedTools: argv.allowedTools || settings.tools?.allowed || undefined,
coreTools: bareMode
? undefined
: argv.coreTools || settings.tools?.core || undefined,
allowedTools: bareMode
? argv.allowedTools || undefined
: argv.allowedTools || settings.tools?.allowed || undefined,
excludeTools: mergedDeny,
// New unified permissions (PermissionManager source of truth).
permissions: {
@ -1085,10 +1113,12 @@ export async function loadCliConfig(
currentSettings.setValue(settingScope, key, [...currentRules, rule]);
}
},
toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand,
mcpServerCommand: settings.mcp?.serverCommand,
mcpServers: settings.mcpServers || {},
toolDiscoveryCommand: bareMode
? undefined
: settings.tools?.discoveryCommand,
toolCallCommand: bareMode ? undefined : settings.tools?.callCommand,
mcpServerCommand: bareMode ? undefined : settings.mcp?.serverCommand,
mcpServers: bareMode ? {} : settings.mcpServers || {},
allowedMcpServers: allowedMcpServers
? Array.from(allowedMcpServers)
: undefined,
@ -1133,9 +1163,14 @@ export async function loadCliConfig(
generationConfigSources: resolvedCliConfig.sources,
generationConfig: resolvedCliConfig.generationConfig,
warnings: resolvedCliConfig.warnings,
allowedHttpHookUrls: settings.security?.allowedHttpHookUrls ?? [],
bareMode,
allowedHttpHookUrls: bareMode
? []
: (settings.security?.allowedHttpHookUrls ?? []),
cliVersion: await getCliVersion(),
webSearch: buildWebSearchConfig(argv, settings, selectedAuthType),
webSearch: bareMode
? undefined
: buildWebSearchConfig(argv, settings, selectedAuthType),
ideMode,
chatCompression: settings.model?.chatCompression,
folderTrust,
@ -1154,14 +1189,18 @@ export async function loadCliConfig(
output: {
format: outputSettingsFormat,
},
enableManagedAutoMemory: settings.memory?.enableManagedAutoMemory ?? true,
enableManagedAutoMemory: bareMode
? false
: (settings.memory?.enableManagedAutoMemory ?? true),
enableManagedAutoDream: settings.memory?.enableManagedAutoDream ?? false,
fastModel: settings.fastModel || undefined,
// Use separated hooks if provided, otherwise fall back to merged hooks
userHooks: hooksConfig?.userHooks ?? settings.hooks,
projectHooks: hooksConfig?.projectHooks,
hooks: settings.hooks, // Keep for backward compatibility
disableAllHooks: settings.disableAllHooks ?? false,
userHooks: bareMode
? undefined
: (hooksConfig?.userHooks ?? settings.hooks),
projectHooks: bareMode ? undefined : hooksConfig?.projectHooks,
hooks: bareMode ? undefined : settings.hooks, // Keep for backward compatibility
disableAllHooks: bareMode ? true : (settings.disableAllHooks ?? false),
channel: argv.channel,
// CLI flag wins over settings.json. `--json-fd` is fd-only (no settings
// equivalent — fd passing is a spawn-time concern). `--json-file` and