mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-02 13:40:46 +00:00
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:
parent
cfe142e9a3
commit
41f71ab7e7
19 changed files with 750 additions and 72 deletions
|
|
@ -69,6 +69,7 @@ export async function handleQwenAuth(
|
|||
prompt: undefined,
|
||||
promptInteractive: undefined,
|
||||
yolo: undefined,
|
||||
bare: undefined,
|
||||
approvalMode: undefined,
|
||||
telemetry: undefined,
|
||||
checkpointing: undefined,
|
||||
|
|
|
|||
|
|
@ -579,6 +579,12 @@ describe('parseArguments', () => {
|
|||
const argv = await parseArguments();
|
||||
expect(argv.extensions).toEqual(['ext1', 'ext2']);
|
||||
});
|
||||
|
||||
it('should parse --bare', async () => {
|
||||
process.argv = ['node', 'script.js', '--bare'];
|
||||
const argv = await parseArguments();
|
||||
expect(argv.bare).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig', () => {
|
||||
|
|
@ -1242,6 +1248,17 @@ describe('Approval mode tool exclusion logic', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('should keep the bare toolset available in non-interactive bare mode', async () => {
|
||||
process.argv = ['node', 'script.js', '--bare', '-p', 'test'];
|
||||
const argv = await parseArguments();
|
||||
const config = await loadCliConfig({}, argv, undefined, []);
|
||||
|
||||
const excludedTools = config.getPermissionsDeny();
|
||||
expect(excludedTools).not.toContain(ToolNames.SHELL);
|
||||
expect(excludedTools).not.toContain(ToolNames.EDIT);
|
||||
expect(excludedTools).not.toContain(ToolNames.WRITE_FILE);
|
||||
});
|
||||
|
||||
it('should merge approval mode exclusions with settings exclusions in auto-edit mode', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
|
|
@ -1648,6 +1665,90 @@ describe('loadCliConfig with includeDirectories', () => {
|
|||
expected.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore implicit startup context inputs in bare mode', async () => {
|
||||
const mockCwd = path.resolve(path.sep, 'home', 'user', 'project');
|
||||
const cliPath = path.resolve(path.sep, 'cli', 'path1');
|
||||
const settingsPath = path.resolve(path.sep, 'settings', 'path1');
|
||||
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--bare',
|
||||
'--include-directories',
|
||||
cliPath,
|
||||
];
|
||||
const argv = await parseArguments();
|
||||
const settings: Settings = {
|
||||
context: {
|
||||
includeDirectories: [settingsPath],
|
||||
},
|
||||
};
|
||||
|
||||
const config = await loadCliConfig(settings, argv, undefined, []);
|
||||
|
||||
expect(config.getWorkspaceContext().getDirectories()).toEqual([
|
||||
mockCwd,
|
||||
cliPath,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should force minimal startup behavior in bare mode', async () => {
|
||||
process.argv = ['node', 'script.js', '--bare'];
|
||||
const argv = await parseArguments();
|
||||
const settings: Settings = {
|
||||
tools: {
|
||||
core: [ToolNames.WEB_FETCH],
|
||||
allowed: [ToolNames.WEB_FETCH],
|
||||
exclude: [ToolNames.ASK_USER_QUESTION],
|
||||
},
|
||||
hooks: {
|
||||
PreToolUse: [],
|
||||
} as Record<string, unknown>,
|
||||
memory: {
|
||||
enableManagedAutoMemory: true,
|
||||
},
|
||||
security: {
|
||||
allowedHttpHookUrls: ['https://hooks.example.com/*'],
|
||||
},
|
||||
mcp: {
|
||||
allowed: ['test-server'],
|
||||
},
|
||||
mcpServers: {
|
||||
'test-server': {
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const config = await loadCliConfig(settings, argv, undefined, []);
|
||||
|
||||
expect(config.getCoreTools()).toEqual([
|
||||
ToolNames.READ_FILE,
|
||||
ToolNames.EDIT,
|
||||
ToolNames.SHELL,
|
||||
]);
|
||||
expect(config.getDisableAllHooks()).toBe(true);
|
||||
expect(config.getManagedAutoMemoryEnabled()).toBe(false);
|
||||
expect(config.getToolDiscoveryCommand()).toBeUndefined();
|
||||
expect(config.getToolCallCommand()).toBeUndefined();
|
||||
expect(config.getMcpServers()).toEqual({});
|
||||
expect(config.getWebSearchConfig()).toBeUndefined();
|
||||
expect(config.isLspEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore coreTools overrides in bare mode', async () => {
|
||||
process.argv = ['node', 'script.js', '--bare', '--core-tools', 'web_fetch'];
|
||||
const argv = await parseArguments();
|
||||
const config = await loadCliConfig({}, argv, undefined, []);
|
||||
|
||||
expect(config.getCoreTools()).toEqual([
|
||||
ToolNames.READ_FILE,
|
||||
ToolNames.EDIT,
|
||||
ToolNames.SHELL,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig chatCompression', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
validateDnsResolutionOrder,
|
||||
startInteractiveUI,
|
||||
} from './gemini.js';
|
||||
import type { CliArgs } from './config/config.js';
|
||||
import { type LoadedSettings } from './config/settings.js';
|
||||
import { appEvents, AppEvent } from './utils/events.js';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
|
|
@ -40,6 +41,7 @@ vi.mock('./config/settings.js', async (importOriginal) => {
|
|||
return {
|
||||
...actual,
|
||||
loadSettings: vi.fn(),
|
||||
createMinimalSettings: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -109,6 +111,7 @@ vi.mock('./core/initializer.js', () => ({
|
|||
describe('gemini.tsx main function', () => {
|
||||
let originalEnvGeminiSandbox: string | undefined;
|
||||
let originalEnvSandbox: string | undefined;
|
||||
let originalEnvQwenCodeSimple: string | undefined;
|
||||
let initialUnhandledRejectionListeners: NodeJS.UnhandledRejectionListener[] =
|
||||
[];
|
||||
|
||||
|
|
@ -116,8 +119,10 @@ describe('gemini.tsx main function', () => {
|
|||
// Store and clear sandbox-related env variables to ensure a consistent test environment
|
||||
originalEnvGeminiSandbox = process.env['QWEN_SANDBOX'];
|
||||
originalEnvSandbox = process.env['SANDBOX'];
|
||||
originalEnvQwenCodeSimple = process.env['QWEN_CODE_SIMPLE'];
|
||||
delete process.env['QWEN_SANDBOX'];
|
||||
delete process.env['SANDBOX'];
|
||||
delete process.env['QWEN_CODE_SIMPLE'];
|
||||
|
||||
initialUnhandledRejectionListeners =
|
||||
process.listeners('unhandledRejection');
|
||||
|
|
@ -135,6 +140,11 @@ describe('gemini.tsx main function', () => {
|
|||
} else {
|
||||
delete process.env['SANDBOX'];
|
||||
}
|
||||
if (originalEnvQwenCodeSimple !== undefined) {
|
||||
process.env['QWEN_CODE_SIMPLE'] = originalEnvQwenCodeSimple;
|
||||
} else {
|
||||
delete process.env['QWEN_CODE_SIMPLE'];
|
||||
}
|
||||
|
||||
const currentListeners = process.listeners('unhandledRejection');
|
||||
const addedListener = currentListeners.find(
|
||||
|
|
@ -215,6 +225,87 @@ describe('gemini.tsx main function', () => {
|
|||
processExitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should skip full settings discovery in bare mode', async () => {
|
||||
const originalArgv = process.argv;
|
||||
process.argv = ['node', 'script.js', '--bare'];
|
||||
|
||||
const { loadCliConfig, parseArguments } = await import(
|
||||
'./config/config.js'
|
||||
);
|
||||
const { loadSettings, createMinimalSettings } = await import(
|
||||
'./config/settings.js'
|
||||
);
|
||||
const { loadSandboxConfig } = await import('./config/sandboxConfig.js');
|
||||
const { relaunchAppInChildProcess } = await import('./utils/relaunch.js');
|
||||
const nonInteractiveModule = await import('./nonInteractiveCli.js');
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((code) => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
const minimalSettings = {
|
||||
errors: [],
|
||||
merged: {},
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
migrationWarnings: [],
|
||||
getUserHooks: () => undefined,
|
||||
getProjectHooks: () => undefined,
|
||||
};
|
||||
const configStub = {
|
||||
isInteractive: () => false,
|
||||
getQuestion: () => 'bare prompt',
|
||||
getSandbox: () => false,
|
||||
getDebugMode: () => false,
|
||||
getListExtensions: () => false,
|
||||
getMcpServers: () => ({}),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getIdeMode: () => false,
|
||||
getExperimentalZedIntegration: () => false,
|
||||
getScreenReader: () => false,
|
||||
getGeminiMdFileCount: () => 0,
|
||||
getProjectRoot: () => '/',
|
||||
getOutputFormat: () => OutputFormat.TEXT,
|
||||
getWarnings: () => [],
|
||||
getModelsConfig: () => ({ getCurrentAuthType: () => null }),
|
||||
getSessionId: () => 'test-session-id',
|
||||
} as unknown as Config;
|
||||
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
bare: true,
|
||||
} as unknown as CliArgs);
|
||||
vi.mocked(createMinimalSettings).mockReturnValue(minimalSettings as never);
|
||||
vi.mocked(loadSandboxConfig).mockResolvedValue(undefined);
|
||||
vi.mocked(relaunchAppInChildProcess).mockResolvedValue(undefined);
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(configStub);
|
||||
vi.spyOn(nonInteractiveModule, 'runNonInteractive').mockResolvedValue();
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
if (!(error instanceof MockProcessExitError)) {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
process.argv = originalArgv;
|
||||
processExitSpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(createMinimalSettings).toHaveBeenCalledOnce();
|
||||
expect(loadSettings).not.toHaveBeenCalled();
|
||||
expect(loadCliConfig).toHaveBeenCalledWith(
|
||||
{},
|
||||
expect.objectContaining({ bare: true }),
|
||||
process.cwd(),
|
||||
undefined,
|
||||
{
|
||||
userHooks: undefined,
|
||||
projectHooks: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should log unhandled promise rejections and open debug console on first error', async () => {
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
|
|
@ -483,6 +574,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||
appendSystemPrompt: undefined,
|
||||
query: undefined,
|
||||
yolo: undefined,
|
||||
bare: undefined,
|
||||
approvalMode: undefined,
|
||||
telemetry: undefined,
|
||||
checkpointing: undefined,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import {
|
|||
AuthType,
|
||||
InputFormat,
|
||||
isDebugLoggingDegraded,
|
||||
isBareMode,
|
||||
logUserPrompt,
|
||||
QWEN_CODE_SIMPLE_ENV_VAR,
|
||||
Storage,
|
||||
type Config,
|
||||
createDebugLogger,
|
||||
|
|
@ -23,7 +25,11 @@ import { validateAuthMethod } from './config/auth.js';
|
|||
import * as cliConfig from './config/config.js';
|
||||
import { loadCliConfig, parseArguments } from './config/config.js';
|
||||
import type { DnsResolutionOrder, LoadedSettings } from './config/settings.js';
|
||||
import { getSettingsWarnings, loadSettings } from './config/settings.js';
|
||||
import {
|
||||
createMinimalSettings,
|
||||
getSettingsWarnings,
|
||||
loadSettings,
|
||||
} from './config/settings.js';
|
||||
import {
|
||||
initializeApp,
|
||||
type InitializationResult,
|
||||
|
|
@ -290,13 +296,24 @@ export async function startInteractiveUI(
|
|||
export async function main() {
|
||||
profileCheckpoint('main_entry');
|
||||
setupUnhandledRejectionHandler();
|
||||
const settings = loadSettings();
|
||||
await cleanupCheckpoints();
|
||||
profileCheckpoint('after_load_settings');
|
||||
|
||||
if (process.argv.includes('--bare')) {
|
||||
process.env[QWEN_CODE_SIMPLE_ENV_VAR] = '1';
|
||||
}
|
||||
|
||||
let argv = await parseArguments();
|
||||
profileCheckpoint('after_parse_arguments');
|
||||
|
||||
if (isBareMode(argv.bare)) {
|
||||
process.env[QWEN_CODE_SIMPLE_ENV_VAR] = '1';
|
||||
}
|
||||
|
||||
const settings = isBareMode(argv.bare)
|
||||
? createMinimalSettings()
|
||||
: loadSettings();
|
||||
await cleanupCheckpoints();
|
||||
profileCheckpoint('after_load_settings');
|
||||
|
||||
// Check for invalid input combinations early to prevent crashes
|
||||
if (argv.promptInteractive && !process.stdin.isTTY) {
|
||||
writeStderrLine(
|
||||
|
|
@ -439,7 +456,9 @@ export async function main() {
|
|||
profileCheckpoint('after_sandbox_check');
|
||||
|
||||
// Initialize output language file before config loads to ensure it's included in context
|
||||
initializeLlmOutputLanguage(settings.merged.general?.outputLanguage);
|
||||
if (!isBareMode(argv.bare)) {
|
||||
initializeLlmOutputLanguage(settings.merged.general?.outputLanguage);
|
||||
}
|
||||
|
||||
{
|
||||
const config = await loadCliConfig(
|
||||
|
|
|
|||
|
|
@ -55,6 +55,20 @@ describe('BundledSkillLoader', () => {
|
|||
expect(commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array in bare mode', async () => {
|
||||
const skill = makeSkill();
|
||||
mockSkillManager.listSkills.mockResolvedValue([skill]);
|
||||
(
|
||||
mockConfig as Config & { getBareMode: ReturnType<typeof vi.fn> }
|
||||
).getBareMode = vi.fn().mockReturnValue(true);
|
||||
|
||||
const loader = new BundledSkillLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
|
||||
expect(commands).toEqual([]);
|
||||
expect(mockSkillManager.listSkills).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should load bundled skills as slash commands', async () => {
|
||||
const skill = makeSkill();
|
||||
mockSkillManager.listSkills.mockResolvedValue([skill]);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ export class BundledSkillLoader implements ICommandLoader {
|
|||
constructor(private readonly config: Config | null) {}
|
||||
|
||||
async loadCommands(_signal: AbortSignal): Promise<SlashCommand[]> {
|
||||
if (this.config?.getBareMode?.()) {
|
||||
debugLogger.debug('Bare mode enabled, skipping bundled skills');
|
||||
return [];
|
||||
}
|
||||
|
||||
const skillManager = this.config?.getSkillManager();
|
||||
if (!skillManager) {
|
||||
debugLogger.debug('SkillManager not available, skipping bundled skills');
|
||||
|
|
|
|||
|
|
@ -308,6 +308,33 @@ describe('FileCommandLoader', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('skips auto-discovered commands in bare mode', async () => {
|
||||
const userCommandsDir = Storage.getUserCommandsDir();
|
||||
const projectCommandsDir = new Storage(
|
||||
process.cwd(),
|
||||
).getProjectCommandsDir();
|
||||
mock({
|
||||
[userCommandsDir]: {
|
||||
'user.toml': 'prompt = "User prompt"',
|
||||
},
|
||||
[projectCommandsDir]: {
|
||||
'project.toml': 'prompt = "Project prompt"',
|
||||
},
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getProjectRoot: vi.fn(() => process.cwd()),
|
||||
getExtensions: vi.fn(() => []),
|
||||
getFolderTrustFeature: vi.fn(() => false),
|
||||
getFolderTrust: vi.fn(() => false),
|
||||
getBareMode: vi.fn(() => true),
|
||||
} as unknown as Config;
|
||||
const loader = new FileCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
|
||||
expect(commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores files with TOML syntax errors', async () => {
|
||||
const userCommandsDir = Storage.getUserCommandsDir();
|
||||
mock({
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ export class FileCommandLoader implements ICommandLoader {
|
|||
* @returns A promise that resolves to an array of all loaded SlashCommands.
|
||||
*/
|
||||
async loadCommands(signal: AbortSignal): Promise<SlashCommand[]> {
|
||||
if (this.config?.getBareMode?.()) {
|
||||
debugLogger.debug('Bare mode enabled, skipping auto-discovered commands');
|
||||
return [];
|
||||
}
|
||||
|
||||
const allCommands: SlashCommand[] = [];
|
||||
const globOptions = {
|
||||
nodir: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue