fix(acp): rename --experimental-acp to --acp & remove Zed-specific refrences (#21171)

This commit is contained in:
Shreya Keshive
2026-03-05 14:57:28 -05:00
committed by GitHub
parent 9773a084c9
commit 0135b03c8a
24 changed files with 32 additions and 25 deletions
@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { debugLogger } from '@google/gemini-cli-core';
import type { Command } from './types.js';
export class CommandRegistry {
private readonly commands = new Map<string, Command>();
register(command: Command) {
if (this.commands.has(command.name)) {
debugLogger.warn(`Command ${command.name} already registered. Skipping.`);
return;
}
this.commands.set(command.name, command);
for (const subCommand of command.subCommands ?? []) {
this.register(subCommand);
}
}
get(commandName: string): Command | undefined {
return this.commands.get(commandName);
}
getAllCommands(): Command[] {
return [...this.commands.values()];
}
}
+428
View File
@@ -0,0 +1,428 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { listExtensions } from '@google/gemini-cli-core';
import { SettingScope } from '../../config/settings.js';
import {
ExtensionManager,
inferInstallMetadata,
} from '../../config/extension-manager.js';
import { getErrorMessage } from '../../utils/errors.js';
import { McpServerEnablementManager } from '../../config/mcp/mcpServerEnablement.js';
import { stat } from 'node:fs/promises';
import type {
Command,
CommandContext,
CommandExecutionResponse,
} from './types.js';
import type { Config } from '@google/gemini-cli-core';
export class ExtensionsCommand implements Command {
readonly name = 'extensions';
readonly description = 'Manage extensions.';
readonly subCommands = [
new ListExtensionsCommand(),
new ExploreExtensionsCommand(),
new EnableExtensionCommand(),
new DisableExtensionCommand(),
new InstallExtensionCommand(),
new LinkExtensionCommand(),
new UninstallExtensionCommand(),
new RestartExtensionCommand(),
new UpdateExtensionCommand(),
];
async execute(
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
return new ListExtensionsCommand().execute(context, _);
}
}
export class ListExtensionsCommand implements Command {
readonly name = 'extensions list';
readonly description = 'Lists all installed extensions.';
async execute(
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const extensions = listExtensions(context.config);
const data = extensions.length ? extensions : 'No extensions installed.';
return { name: this.name, data };
}
}
export class ExploreExtensionsCommand implements Command {
readonly name = 'extensions explore';
readonly description = 'Explore available extensions.';
async execute(
_context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const extensionsUrl = 'https://geminicli.com/extensions/';
return {
name: this.name,
data: `View or install available extensions at ${extensionsUrl}`,
};
}
}
function getEnableDisableContext(
config: Config,
args: string[],
invocationName: string,
) {
const extensionManager = config.getExtensionLoader();
if (!(extensionManager instanceof ExtensionManager)) {
return {
error: `Cannot ${invocationName} extensions in this environment.`,
};
}
if (args.length === 0) {
return {
error: `Usage: /extensions ${invocationName} <extension> [--scope=<user|workspace|session>]`,
};
}
let scope = SettingScope.User;
if (args.includes('--scope=workspace') || args.includes('workspace')) {
scope = SettingScope.Workspace;
} else if (args.includes('--scope=session') || args.includes('session')) {
scope = SettingScope.Session;
}
const name = args.filter(
(a) =>
!a.startsWith('--scope') && !['user', 'workspace', 'session'].includes(a),
)[0];
let names: string[] = [];
if (name === '--all') {
let extensions = extensionManager.getExtensions();
if (invocationName === 'enable') {
extensions = extensions.filter((ext) => !ext.isActive);
}
if (invocationName === 'disable') {
extensions = extensions.filter((ext) => ext.isActive);
}
names = extensions.map((ext) => ext.name);
} else if (name) {
names = [name];
} else {
return { error: 'No extension name provided.' };
}
return { extensionManager, names, scope };
}
export class EnableExtensionCommand implements Command {
readonly name = 'extensions enable';
readonly description = 'Enable an extension.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.config,
args,
'enable',
);
if ('error' in enableContext) {
return { name: this.name, data: enableContext.error };
}
const { names, scope, extensionManager } = enableContext;
const output: string[] = [];
for (const name of names) {
try {
await extensionManager.enableExtension(name, scope);
output.push(`Extension "${name}" enabled for scope "${scope}".`);
const extension = extensionManager
.getExtensions()
.find((e) => e.name === name);
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager = context.config.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers),
);
if (mcpClientManager && enabledServers.length > 0) {
const restartPromises = enabledServers.map((serverName) =>
mcpClientManager.restartServer(serverName).catch((error) => {
output.push(
`Failed to restart MCP server '${serverName}': ${getErrorMessage(error)}`,
);
}),
);
await Promise.all(restartPromises);
output.push(`Re-enabled MCP servers: ${enabledServers.join(', ')}`);
}
}
} catch (e) {
output.push(`Failed to enable "${name}": ${getErrorMessage(e)}`);
}
}
return { name: this.name, data: output.join('\n') || 'No action taken.' };
}
}
export class DisableExtensionCommand implements Command {
readonly name = 'extensions disable';
readonly description = 'Disable an extension.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.config,
args,
'disable',
);
if ('error' in enableContext) {
return { name: this.name, data: enableContext.error };
}
const { names, scope, extensionManager } = enableContext;
const output: string[] = [];
for (const name of names) {
try {
await extensionManager.disableExtension(name, scope);
output.push(`Extension "${name}" disabled for scope "${scope}".`);
} catch (e) {
output.push(`Failed to disable "${name}": ${getErrorMessage(e)}`);
}
}
return { name: this.name, data: output.join('\n') || 'No action taken.' };
}
}
export class InstallExtensionCommand implements Command {
readonly name = 'extensions install';
readonly description = 'Install an extension from a git repo or local path.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
data: 'Cannot install extensions in this environment.',
};
}
const source = args.join(' ').trim();
if (!source) {
return { name: this.name, data: `Usage: /extensions install <source>` };
}
if (/[;&|`'"]/.test(source)) {
return {
name: this.name,
data: `Invalid source: contains disallowed characters.`,
};
}
try {
const installMetadata = await inferInstallMetadata(source);
const extension =
await extensionLoader.installOrUpdateExtension(installMetadata);
return {
name: this.name,
data: `Extension "${extension.name}" installed successfully.`,
};
} catch (error) {
return {
name: this.name,
data: `Failed to install extension from "${source}": ${getErrorMessage(error)}`,
};
}
}
}
export class LinkExtensionCommand implements Command {
readonly name = 'extensions link';
readonly description = 'Link an extension from a local path.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
data: 'Cannot link extensions in this environment.',
};
}
const sourceFilepath = args.join(' ').trim();
if (!sourceFilepath) {
return { name: this.name, data: `Usage: /extensions link <source>` };
}
try {
await stat(sourceFilepath);
} catch (_error) {
return { name: this.name, data: `Invalid source: ${sourceFilepath}` };
}
try {
const extension = await extensionLoader.installOrUpdateExtension({
source: sourceFilepath,
type: 'link',
});
return {
name: this.name,
data: `Extension "${extension.name}" linked successfully.`,
};
} catch (error) {
return {
name: this.name,
data: `Failed to link extension: ${getErrorMessage(error)}`,
};
}
}
}
export class UninstallExtensionCommand implements Command {
readonly name = 'extensions uninstall';
readonly description = 'Uninstall an extension.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
data: 'Cannot uninstall extensions in this environment.',
};
}
const name = args.join(' ').trim();
if (!name) {
return {
name: this.name,
data: `Usage: /extensions uninstall <extension-name>`,
};
}
try {
await extensionLoader.uninstallExtension(name, false);
return {
name: this.name,
data: `Extension "${name}" uninstalled successfully.`,
};
} catch (error) {
return {
name: this.name,
data: `Failed to uninstall extension "${name}": ${getErrorMessage(error)}`,
};
}
}
}
export class RestartExtensionCommand implements Command {
readonly name = 'extensions restart';
readonly description = 'Restart an extension.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot restart extensions.' };
}
const all = args.includes('--all');
const names = all ? null : args.filter((a) => !!a);
if (!all && names?.length === 0) {
return {
name: this.name,
data: 'Usage: /extensions restart <extension-names>|--all',
};
}
let extensionsToRestart = extensionLoader
.getExtensions()
.filter((e) => e.isActive);
if (names) {
extensionsToRestart = extensionsToRestart.filter((e) =>
names.includes(e.name),
);
}
if (extensionsToRestart.length === 0) {
return {
name: this.name,
data: 'No active extensions matched the request.',
};
}
const output: string[] = [];
for (const extension of extensionsToRestart) {
try {
await extensionLoader.restartExtension(extension);
output.push(`Restarted "${extension.name}".`);
} catch (e) {
output.push(
`Failed to restart "${extension.name}": ${getErrorMessage(e)}`,
);
}
}
return { name: this.name, data: output.join('\n') };
}
}
export class UpdateExtensionCommand implements Command {
readonly name = 'extensions update';
readonly description = 'Update an extension.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot update extensions.' };
}
const all = args.includes('--all');
const names = all ? null : args.filter((a) => !!a);
if (!all && names?.length === 0) {
return {
name: this.name,
data: 'Usage: /extensions update <extension-names>|--all',
};
}
return {
name: this.name,
data: 'Headless extension updating requires internal UI dispatches. Please use `gemini extensions update` directly in the terminal.',
};
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { performInit } from '@google/gemini-cli-core';
import type {
Command,
CommandContext,
CommandExecutionResponse,
} from './types.js';
export class InitCommand implements Command {
name = 'init';
description = 'Analyzes the project and creates a tailored GEMINI.md file';
requiresWorkspace = true;
async execute(
context: CommandContext,
_args: string[] = [],
): Promise<CommandExecutionResponse> {
const targetDir = context.config.getTargetDir();
if (!targetDir) {
throw new Error('Command requires a workspace.');
}
const geminiMdPath = path.join(targetDir, 'GEMINI.md');
const result = performInit(fs.existsSync(geminiMdPath));
switch (result.type) {
case 'message':
return {
name: this.name,
data: result,
};
case 'submit_prompt':
fs.writeFileSync(geminiMdPath, '', 'utf8');
if (typeof result.content !== 'string') {
throw new Error('Init command content must be a string.');
}
// Inform the user since we can't trigger the UI-based interactive agent loop here directly.
// We output the prompt text they can use to re-trigger the generation manually,
// or just seed the GEMINI.md file as we've done above.
return {
name: this.name,
data: {
type: 'message',
messageType: 'info',
content: `A template GEMINI.md has been created at ${geminiMdPath}.\n\nTo populate it with project context, you can run the following prompt in a new chat:\n\n${result.content}`,
},
};
default:
throw new Error('Unknown result type from performInit');
}
}
}
+121
View File
@@ -0,0 +1,121 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
addMemory,
listMemoryFiles,
refreshMemory,
showMemory,
} from '@google/gemini-cli-core';
import type {
Command,
CommandContext,
CommandExecutionResponse,
} from './types.js';
const DEFAULT_SANITIZATION_CONFIG = {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
};
export class MemoryCommand implements Command {
readonly name = 'memory';
readonly description = 'Manage memory.';
readonly subCommands = [
new ShowMemoryCommand(),
new RefreshMemoryCommand(),
new ListMemoryCommand(),
new AddMemoryCommand(),
];
readonly requiresWorkspace = true;
async execute(
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
return new ShowMemoryCommand().execute(context, _);
}
}
export class ShowMemoryCommand implements Command {
readonly name = 'memory show';
readonly description = 'Shows the current memory contents.';
async execute(
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = showMemory(context.config);
return { name: this.name, data: result.content };
}
}
export class RefreshMemoryCommand implements Command {
readonly name = 'memory refresh';
readonly aliases = ['memory reload'];
readonly description = 'Refreshes the memory from the source.';
async execute(
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = await refreshMemory(context.config);
return { name: this.name, data: result.content };
}
}
export class ListMemoryCommand implements Command {
readonly name = 'memory list';
readonly description = 'Lists the paths of the GEMINI.md files in use.';
async execute(
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = listMemoryFiles(context.config);
return { name: this.name, data: result.content };
}
}
export class AddMemoryCommand implements Command {
readonly name = 'memory add';
readonly description = 'Add content to the memory.';
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const textToAdd = args.join(' ').trim();
const result = addMemory(textToAdd);
if (result.type === 'message') {
return { name: this.name, data: result.content };
}
const toolRegistry = context.config.getToolRegistry();
const tool = toolRegistry.getTool(result.toolName);
if (tool) {
const abortController = new AbortController();
const signal = abortController.signal;
await context.sendMessage(`Saving memory via ${result.toolName}...`);
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
});
await refreshMemory(context.config);
return {
name: this.name,
data: `Added memory: "${textToAdd}"`,
};
} else {
return {
name: this.name,
data: `Error: Tool ${result.toolName} not found.`,
};
}
}
}
+178
View File
@@ -0,0 +1,178 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
getCheckpointInfoList,
getToolCallDataSchema,
isNodeError,
performRestore,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import type {
Command,
CommandContext,
CommandExecutionResponse,
} from './types.js';
export class RestoreCommand implements Command {
readonly name = 'restore';
readonly description =
'Restore to a previous checkpoint, or list available checkpoints to restore. This will reset the conversation and file history to the state it was in when the checkpoint was created';
readonly requiresWorkspace = true;
readonly subCommands = [new ListCheckpointsCommand()];
async execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const { config, git: gitService } = context;
const argsStr = args.join(' ');
try {
if (!argsStr) {
return await new ListCheckpointsCommand().execute(context);
}
if (!config.getCheckpointingEnabled()) {
return {
name: this.name,
data: 'Checkpointing is not enabled. Please enable it in your settings (`general.checkpointing.enabled: true`) to use /restore.',
};
}
const selectedFile = argsStr.endsWith('.json')
? argsStr
: `${argsStr}.json`;
const checkpointDir = config.storage.getProjectTempCheckpointsDir();
const filePath = path.join(checkpointDir, selectedFile);
let data: string;
try {
data = await fs.readFile(filePath, 'utf-8');
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
return {
name: this.name,
data: `File not found: ${selectedFile}`,
};
}
throw error;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const toolCallData = JSON.parse(data);
const ToolCallDataSchema = getToolCallDataSchema();
const parseResult = ToolCallDataSchema.safeParse(toolCallData);
if (!parseResult.success) {
return {
name: this.name,
data: 'Checkpoint file is invalid or corrupted.',
};
}
const restoreResultGenerator = performRestore(
parseResult.data,
gitService,
);
const restoreResult = [];
for await (const result of restoreResultGenerator) {
restoreResult.push(result);
}
// Format the result nicely since Zed just dumps data
const formattedResult = restoreResult
.map((r) => {
if (r.type === 'message') {
return `[${r.messageType.toUpperCase()}] ${r.content}`;
} else if (r.type === 'load_history') {
return `Loaded history with ${r.clientHistory.length} messages.`;
}
return `Restored: ${JSON.stringify(r)}`;
})
.join('\n');
return {
name: this.name,
data: formattedResult,
};
} catch (error) {
return {
name: this.name,
data: `An unexpected error occurred during restore: ${error}`,
};
}
}
}
export class ListCheckpointsCommand implements Command {
readonly name = 'restore list';
readonly description = 'Lists all available checkpoints.';
async execute(context: CommandContext): Promise<CommandExecutionResponse> {
const { config } = context;
try {
if (!config.getCheckpointingEnabled()) {
return {
name: this.name,
data: 'Checkpointing is not enabled. Please enable it in your settings (`general.checkpointing.enabled: true`) to use /restore.',
};
}
const checkpointDir = config.storage.getProjectTempCheckpointsDir();
try {
await fs.mkdir(checkpointDir, { recursive: true });
} catch (_e) {
// Ignore
}
const files = await fs.readdir(checkpointDir);
const jsonFiles = files.filter((file) => file.endsWith('.json'));
if (jsonFiles.length === 0) {
return { name: this.name, data: 'No checkpoints found.' };
}
const checkpointFiles = new Map<string, string>();
for (const file of jsonFiles) {
const filePath = path.join(checkpointDir, file);
const data = await fs.readFile(filePath, 'utf-8');
checkpointFiles.set(file, data);
}
const checkpointInfoList = getCheckpointInfoList(checkpointFiles);
const formatted = checkpointInfoList
.map((info) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const i = info as Record<string, any>;
const fileName = String(i['fileName'] || 'Unknown');
const toolName = String(i['toolName'] || 'Unknown');
const status = String(i['status'] || 'Unknown');
const timestamp = new Date(
Number(i['timestamp']) || 0,
).toLocaleString();
return `- **${fileName}**: ${toolName} (Status: ${status}) [${timestamp}]`;
})
.join('\n');
return {
name: this.name,
data: `Available Checkpoints:\n${formatted}`,
};
} catch (_error) {
return {
name: this.name,
data: 'An unexpected error occurred while listing checkpoints.',
};
}
}
}
+40
View File
@@ -0,0 +1,40 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config, GitService } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../../config/settings.js';
export interface CommandContext {
config: Config;
settings: LoadedSettings;
git?: GitService;
sendMessage: (text: string) => Promise<void>;
}
export interface CommandArgument {
readonly name: string;
readonly description: string;
readonly isRequired?: boolean;
}
export interface Command {
readonly name: string;
readonly aliases?: string[];
readonly description: string;
readonly arguments?: CommandArgument[];
readonly subCommands?: Command[];
readonly requiresWorkspace?: boolean;
execute(
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse>;
}
export interface CommandExecutionResponse {
readonly name: string;
readonly data: unknown;
}