mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 10:34:35 -07:00
remove support for workspace extensions and migrations (#11324)
This commit is contained in:
@@ -5,12 +5,16 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { loadUserExtensions, toOutputString } from '../../config/extension.js';
|
||||
import { loadExtensions, toOutputString } from '../../config/extension.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
|
||||
export async function handleList() {
|
||||
try {
|
||||
const extensions = loadUserExtensions();
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(),
|
||||
process.cwd(),
|
||||
);
|
||||
if (extensions.length === 0) {
|
||||
console.log('No extensions installed.');
|
||||
return;
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { CommandModule } from 'yargs';
|
||||
import {
|
||||
loadExtensions,
|
||||
annotateActiveExtensions,
|
||||
ExtensionStorage,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extension.js';
|
||||
import {
|
||||
@@ -33,7 +32,6 @@ const updateOutput = (info: ExtensionUpdateInfo) =>
|
||||
export async function handleUpdate(args: UpdateArgs) {
|
||||
const workingDir = process.cwd();
|
||||
const extensionEnablementManager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
// Force enable named extensions, otherwise we will only update the enabled
|
||||
// ones.
|
||||
args.name ? [args.name] : [],
|
||||
|
||||
@@ -10,7 +10,7 @@ import { loadSettings } from '../../config/settings.js';
|
||||
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import { MCPServerStatus, createTransport } from '@google/gemini-cli-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { ExtensionStorage, loadExtensions } from '../../config/extension.js';
|
||||
import { loadExtensions } from '../../config/extension.js';
|
||||
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
|
||||
|
||||
const COLOR_GREEN = '\u001b[32m';
|
||||
@@ -22,9 +22,7 @@ async function getMcpServersFromConfig(): Promise<
|
||||
Record<string, MCPServerConfig>
|
||||
> {
|
||||
const settings = loadSettings();
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
const mcpServers = { ...(settings.merged.mcpServers || {}) };
|
||||
for (const extension of extensions) {
|
||||
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,6 @@ import {
|
||||
loadExtension,
|
||||
loadExtensionConfig,
|
||||
loadExtensions,
|
||||
performWorkspaceExtensionMigration,
|
||||
uninstallExtension,
|
||||
} from './extension.js';
|
||||
import {
|
||||
@@ -155,9 +154,7 @@ describe('extension tests', () => {
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].path).toBe(extensionDir);
|
||||
expect(extensions[0].name).toBe('test-extension');
|
||||
@@ -176,9 +173,7 @@ describe('extension tests', () => {
|
||||
version: '2.0.0',
|
||||
});
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
|
||||
expect(extensions).toHaveLength(2);
|
||||
const ext1 = extensions.find((e) => e.name === 'ext1');
|
||||
@@ -198,9 +193,7 @@ describe('extension tests', () => {
|
||||
contextFileName: 'my-context-file.md',
|
||||
});
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
const ext1 = extensions.find((e) => e.name === 'ext1');
|
||||
@@ -225,9 +218,7 @@ describe('extension tests', () => {
|
||||
SettingScope.User,
|
||||
tempWorkspaceDir,
|
||||
);
|
||||
const manager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
);
|
||||
const manager = new ExtensionEnablementManager();
|
||||
const extensions = loadExtensions(manager);
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
@@ -252,9 +243,7 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
expect(extensions).toHaveLength(1);
|
||||
const expectedCwd = path.join(
|
||||
userExtensionsDir,
|
||||
@@ -282,9 +271,7 @@ describe('extension tests', () => {
|
||||
);
|
||||
|
||||
expect(extensionName).toEqual('my-linked-extension');
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
expect(extensions).toHaveLength(1);
|
||||
|
||||
const linkedExt = extensions[0];
|
||||
@@ -333,11 +320,7 @@ describe('extension tests', () => {
|
||||
};
|
||||
fs.writeFileSync(configPath, JSON.stringify(extensionConfig));
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
const extension = extensions[0];
|
||||
@@ -388,9 +371,7 @@ describe('extension tests', () => {
|
||||
JSON.stringify(extensionConfig),
|
||||
);
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
const extension = extensions[0];
|
||||
@@ -418,9 +399,7 @@ describe('extension tests', () => {
|
||||
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
@@ -452,9 +431,7 @@ describe('extension tests', () => {
|
||||
const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' }));
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].name).toBe('good-ext');
|
||||
@@ -482,9 +459,7 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
const extensions = loadExtensions(new ExtensionEnablementManager());
|
||||
expect(extensions).toHaveLength(1);
|
||||
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
|
||||
});
|
||||
@@ -698,7 +673,7 @@ describe('extension tests', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(activeExtensions).toHaveLength(3);
|
||||
expect(activeExtensions.every((e) => e.isActive)).toBe(true);
|
||||
@@ -708,10 +683,7 @@ describe('extension tests', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
['ext1', 'ext3'],
|
||||
),
|
||||
new ExtensionEnablementManager(['ext1', 'ext3']),
|
||||
);
|
||||
expect(activeExtensions).toHaveLength(3);
|
||||
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
|
||||
@@ -729,10 +701,7 @@ describe('extension tests', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
['none'],
|
||||
),
|
||||
new ExtensionEnablementManager(['none']),
|
||||
);
|
||||
expect(activeExtensions).toHaveLength(3);
|
||||
expect(activeExtensions.every((e) => !e.isActive)).toBe(true);
|
||||
@@ -742,10 +711,7 @@ describe('extension tests', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
['EXT1'],
|
||||
),
|
||||
new ExtensionEnablementManager(['EXT1']),
|
||||
);
|
||||
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
|
||||
true,
|
||||
@@ -759,10 +725,7 @@ describe('extension tests', () => {
|
||||
annotateActiveExtensions(
|
||||
extensions,
|
||||
'/path/to/workspace',
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
['ext4'],
|
||||
),
|
||||
new ExtensionEnablementManager(['ext4']),
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4');
|
||||
consoleSpy.mockRestore();
|
||||
@@ -773,9 +736,7 @@ describe('extension tests', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
),
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(
|
||||
activeExtensions.every(
|
||||
@@ -797,9 +758,7 @@ describe('extension tests', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensionsWithAutoUpdate,
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
),
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(
|
||||
activeExtensions.every((e) => e.installMetadata?.autoUpdate === true),
|
||||
@@ -843,9 +802,7 @@ describe('extension tests', () => {
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensionsWithAutoUpdate,
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
),
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
expect(
|
||||
activeExtensions.find((e) => e.name === 'ext1')?.installMetadata
|
||||
@@ -1073,9 +1030,7 @@ describe('extension tests', () => {
|
||||
});
|
||||
|
||||
it(`should ${isUpdate ? 'not ' : ''} alter the extension enablement configuration`, async () => {
|
||||
const enablementManager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
);
|
||||
const enablementManager = new ExtensionEnablementManager();
|
||||
enablementManager.enable('my-local-extension', true, '/some/scope');
|
||||
|
||||
await installOrUpdateExtension(
|
||||
@@ -1457,13 +1412,7 @@ This extension will run the following MCP servers:
|
||||
await uninstallExtension('my-local-extension', false);
|
||||
|
||||
expect(fs.existsSync(sourceExtDir)).toBe(false);
|
||||
expect(
|
||||
loadExtensions(
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
),
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(loadExtensions(new ExtensionEnablementManager())).toHaveLength(1);
|
||||
expect(fs.existsSync(otherExtDir)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1501,9 +1450,7 @@ This extension will run the following MCP servers:
|
||||
name: 'test-extension',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const enablementManager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
);
|
||||
const enablementManager = new ExtensionEnablementManager();
|
||||
enablementManager.enable('test-extension', true, '/some/scope');
|
||||
|
||||
await uninstallExtension('test-extension', isUpdate);
|
||||
@@ -1557,182 +1504,6 @@ This extension will run the following MCP servers:
|
||||
});
|
||||
});
|
||||
|
||||
describe('performWorkspaceExtensionMigration', () => {
|
||||
let workspaceExtensionsDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceExtensionsDir = path.join(
|
||||
tempWorkspaceDir,
|
||||
EXTENSIONS_DIRECTORY_NAME,
|
||||
);
|
||||
fs.mkdirSync(workspaceExtensionsDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(workspaceExtensionsDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('folder trust', () => {
|
||||
it('refuses to install extensions from untrusted folders', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: false,
|
||||
source: undefined,
|
||||
});
|
||||
const ext1Path = createExtension({
|
||||
extensionsDir: workspaceExtensionsDir,
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const failed = await performWorkspaceExtensionMigration(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
async () => true,
|
||||
);
|
||||
|
||||
expect(failed).toEqual(['ext1']);
|
||||
});
|
||||
|
||||
it('does not copy extensions to the user dir', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: false,
|
||||
source: undefined,
|
||||
});
|
||||
const ext1Path = createExtension({
|
||||
extensionsDir: workspaceExtensionsDir,
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await performWorkspaceExtensionMigration(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
async (_) => true,
|
||||
);
|
||||
|
||||
const userExtensionsDir = path.join(
|
||||
tempHomeDir,
|
||||
GEMINI_DIR,
|
||||
'extensions',
|
||||
);
|
||||
expect(fs.readdirSync(userExtensionsDir).length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not load any extensions in the workspace config', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: false,
|
||||
source: undefined,
|
||||
});
|
||||
const ext1Path = createExtension({
|
||||
extensionsDir: workspaceExtensionsDir,
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await performWorkspaceExtensionMigration(
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
async (_) => true,
|
||||
);
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(extensions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should install the extensions in the user directory', async () => {
|
||||
const ext1Path = createExtension({
|
||||
extensionsDir: workspaceExtensionsDir,
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const ext2Path = createExtension({
|
||||
extensionsDir: workspaceExtensionsDir,
|
||||
name: 'ext2',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const extensionsToMigrate: GeminiCLIExtension[] = [
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
loadExtension({
|
||||
extensionDir: ext2Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
];
|
||||
const failed = await performWorkspaceExtensionMigration(
|
||||
extensionsToMigrate,
|
||||
async (_) => true,
|
||||
);
|
||||
|
||||
expect(failed).toEqual([]);
|
||||
|
||||
const userExtensionsDir = path.join(
|
||||
tempHomeDir,
|
||||
GEMINI_DIR,
|
||||
'extensions',
|
||||
);
|
||||
const userExt1Path = path.join(userExtensionsDir, 'ext1');
|
||||
const extensions = loadExtensions(
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
);
|
||||
|
||||
expect(extensions).toHaveLength(2);
|
||||
const metadataPath = path.join(userExt1Path, INSTALL_METADATA_FILENAME);
|
||||
expect(fs.existsSync(metadataPath)).toBe(true);
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
||||
expect(metadata).toEqual({
|
||||
source: ext1Path,
|
||||
type: 'local',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the names of failed installations', async () => {
|
||||
const ext1Path = createExtension({
|
||||
extensionsDir: workspaceExtensionsDir,
|
||||
name: 'ext1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const extensions: GeminiCLIExtension[] = [
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
{
|
||||
path: '/ext/path/1',
|
||||
name: 'ext2',
|
||||
version: '1.0.0',
|
||||
contextFiles: [],
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
const failed = await performWorkspaceExtensionMigration(
|
||||
extensions,
|
||||
async (_) => true,
|
||||
);
|
||||
expect(failed).toEqual(['ext2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableExtension', () => {
|
||||
it('should disable an extension at the user scope', () => {
|
||||
createExtension({
|
||||
@@ -1745,7 +1516,6 @@ This extension will run the following MCP servers:
|
||||
expect(
|
||||
isEnabled({
|
||||
name: 'my-extension',
|
||||
configDir: userExtensionsDir,
|
||||
enabledForPath: tempWorkspaceDir,
|
||||
}),
|
||||
).toBe(false);
|
||||
@@ -1766,14 +1536,12 @@ This extension will run the following MCP servers:
|
||||
expect(
|
||||
isEnabled({
|
||||
name: 'my-extension',
|
||||
configDir: userExtensionsDir,
|
||||
enabledForPath: tempHomeDir,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isEnabled({
|
||||
name: 'my-extension',
|
||||
configDir: userExtensionsDir,
|
||||
enabledForPath: tempWorkspaceDir,
|
||||
}),
|
||||
).toBe(false);
|
||||
@@ -1791,7 +1559,6 @@ This extension will run the following MCP servers:
|
||||
expect(
|
||||
isEnabled({
|
||||
name: 'my-extension',
|
||||
configDir: userExtensionsDir,
|
||||
enabledForPath: tempWorkspaceDir,
|
||||
}),
|
||||
).toBe(false);
|
||||
@@ -1826,9 +1593,7 @@ This extension will run the following MCP servers:
|
||||
});
|
||||
|
||||
const getActiveExtensions = (): GeminiCLIExtension[] => {
|
||||
const manager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
);
|
||||
const manager = new ExtensionEnablementManager();
|
||||
const extensions = loadExtensions(manager);
|
||||
const activeExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
@@ -1888,11 +1653,7 @@ This extension will run the following MCP servers:
|
||||
});
|
||||
});
|
||||
|
||||
function isEnabled(options: {
|
||||
name: string;
|
||||
configDir: string;
|
||||
enabledForPath: string;
|
||||
}) {
|
||||
const manager = new ExtensionEnablementManager(options.configDir);
|
||||
function isEnabled(options: { name: string; enabledForPath: string }) {
|
||||
const manager = new ExtensionEnablementManager();
|
||||
return manager.isEnabled(options.name, options.enabledForPath);
|
||||
}
|
||||
|
||||
@@ -104,16 +104,6 @@ export class ExtensionStorage {
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspaceExtensions(
|
||||
workspaceDir: string,
|
||||
): GeminiCLIExtension[] {
|
||||
// If the workspace dir is the user extensions dir, there are no workspace extensions.
|
||||
if (path.resolve(workspaceDir) === path.resolve(os.homedir())) {
|
||||
return [];
|
||||
}
|
||||
return loadExtensionsFromDir(workspaceDir);
|
||||
}
|
||||
|
||||
export async function copyExtension(
|
||||
source: string,
|
||||
destination: string,
|
||||
@@ -121,26 +111,6 @@ export async function copyExtension(
|
||||
await fs.promises.cp(source, destination, { recursive: true });
|
||||
}
|
||||
|
||||
export async function performWorkspaceExtensionMigration(
|
||||
extensions: GeminiCLIExtension[],
|
||||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
): Promise<string[]> {
|
||||
const failedInstallNames: string[] = [];
|
||||
|
||||
for (const extension of extensions) {
|
||||
try {
|
||||
const installMetadata: ExtensionInstallMetadata = {
|
||||
source: extension.path,
|
||||
type: 'local',
|
||||
};
|
||||
await installOrUpdateExtension(installMetadata, requestConsent);
|
||||
} catch (_) {
|
||||
failedInstallNames.push(extension.name);
|
||||
}
|
||||
}
|
||||
return failedInstallNames;
|
||||
}
|
||||
|
||||
function getTelemetryConfig(cwd: string) {
|
||||
const settings = loadSettings(cwd);
|
||||
const config = new Config({
|
||||
@@ -159,20 +129,27 @@ export function loadExtensions(
|
||||
extensionEnablementManager: ExtensionEnablementManager,
|
||||
workspaceDir: string = process.cwd(),
|
||||
): GeminiCLIExtension[] {
|
||||
const settings = loadSettings(workspaceDir).merged;
|
||||
const allExtensions = [...loadUserExtensions()];
|
||||
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
isWorkspaceTrusted(settings).isTrusted &&
|
||||
// Default management setting to true
|
||||
!(settings.experimental?.extensionManagement ?? true)
|
||||
) {
|
||||
allExtensions.push(...getWorkspaceExtensions(workspaceDir));
|
||||
const extensions: GeminiCLIExtension[] = [];
|
||||
for (const subdir of fs.readdirSync(extensionsDir)) {
|
||||
const extensionDir = path.join(extensionsDir, subdir);
|
||||
|
||||
const extension = loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir,
|
||||
});
|
||||
if (extension != null) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueExtensions = new Map<string, GeminiCLIExtension>();
|
||||
|
||||
for (const extension of allExtensions) {
|
||||
for (const extension of extensions) {
|
||||
if (
|
||||
!uniqueExtensions.has(extension.name) &&
|
||||
extensionEnablementManager.isEnabled(extension.name, workspaceDir)
|
||||
@@ -184,38 +161,6 @@ export function loadExtensions(
|
||||
return Array.from(uniqueExtensions.values());
|
||||
}
|
||||
|
||||
export function loadUserExtensions(): GeminiCLIExtension[] {
|
||||
const userExtensions = loadExtensionsFromDir(os.homedir());
|
||||
|
||||
const uniqueExtensions = new Map<string, GeminiCLIExtension>();
|
||||
for (const extension of userExtensions) {
|
||||
if (!uniqueExtensions.has(extension.name)) {
|
||||
uniqueExtensions.set(extension.name, extension);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueExtensions.values());
|
||||
}
|
||||
|
||||
export function loadExtensionsFromDir(dir: string): GeminiCLIExtension[] {
|
||||
const storage = new Storage(dir);
|
||||
const extensionsDir = storage.getExtensionsDir();
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const extensions: GeminiCLIExtension[] = [];
|
||||
for (const subdir of fs.readdirSync(extensionsDir)) {
|
||||
const extensionDir = path.join(extensionsDir, subdir);
|
||||
|
||||
const extension = loadExtension({ extensionDir, workspaceDir: dir });
|
||||
if (extension != null) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
export function loadExtension(
|
||||
context: LoadExtensionContext,
|
||||
): GeminiCLIExtension | null {
|
||||
@@ -538,7 +483,10 @@ export async function installOrUpdateExtension(
|
||||
|
||||
const newExtensionName = newExtensionConfig.name;
|
||||
if (!isUpdate) {
|
||||
const installedExtensions = loadUserExtensions();
|
||||
const installedExtensions = loadExtensions(
|
||||
new ExtensionEnablementManager(),
|
||||
cwd,
|
||||
);
|
||||
if (
|
||||
installedExtensions.some(
|
||||
(installed) => installed.name === newExtensionName,
|
||||
@@ -762,7 +710,10 @@ export async function uninstallExtension(
|
||||
isUpdate: boolean,
|
||||
cwd: string = process.cwd(),
|
||||
): Promise<void> {
|
||||
const installedExtensions = loadUserExtensions();
|
||||
const installedExtensions = loadExtensions(
|
||||
new ExtensionEnablementManager(),
|
||||
cwd,
|
||||
);
|
||||
const extensionName = installedExtensions.find(
|
||||
(installed) =>
|
||||
installed.name.toLowerCase() === extensionIdentifier.toLowerCase() ||
|
||||
@@ -783,10 +734,7 @@ export async function uninstallExtension(
|
||||
// uninstalls related to updates.
|
||||
if (isUpdate) return;
|
||||
|
||||
const manager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
[extensionName],
|
||||
);
|
||||
const manager = new ExtensionEnablementManager([extensionName]);
|
||||
manager.remove(extensionName);
|
||||
|
||||
const telemetryConfig = getTelemetryConfig(cwd);
|
||||
@@ -800,9 +748,7 @@ export function toOutputString(
|
||||
extension: GeminiCLIExtension,
|
||||
workspaceDir: string,
|
||||
): string {
|
||||
const manager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
);
|
||||
const manager = new ExtensionEnablementManager();
|
||||
const userEnabled = manager.isEnabled(extension.name, os.homedir());
|
||||
const workspaceEnabled = manager.isEnabled(extension.name, workspaceDir);
|
||||
|
||||
@@ -855,10 +801,7 @@ export function disableExtension(
|
||||
throw new Error(`Extension with name ${name} does not exist.`);
|
||||
}
|
||||
|
||||
const manager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
[name],
|
||||
);
|
||||
const manager = new ExtensionEnablementManager([name]);
|
||||
const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir();
|
||||
manager.disable(name, true, scopePath);
|
||||
logExtensionDisable(config, new ExtensionDisableEvent(name, scope));
|
||||
@@ -876,9 +819,7 @@ export function enableExtension(
|
||||
if (!extension) {
|
||||
throw new Error(`Extension with name ${name} does not exist.`);
|
||||
}
|
||||
const manager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
);
|
||||
const manager = new ExtensionEnablementManager();
|
||||
const scopePath = scope === SettingScope.Workspace ? cwd : os.homedir();
|
||||
manager.enable(name, true, scopePath);
|
||||
const config = getTelemetryConfig(cwd);
|
||||
|
||||
@@ -6,12 +6,20 @@
|
||||
|
||||
import * as path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import * as os from 'node:os';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ExtensionEnablementManager, Override } from './extensionEnablement.js';
|
||||
|
||||
import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const mockedOs = await importOriginal<typeof os>();
|
||||
return {
|
||||
...mockedOs,
|
||||
homedir: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Helper to create a temporary directory for testing
|
||||
function createTestDir() {
|
||||
const dirPath = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-test-'));
|
||||
@@ -22,14 +30,13 @@ function createTestDir() {
|
||||
}
|
||||
|
||||
let testDir: { path: string; cleanup: () => void };
|
||||
let configDir: string;
|
||||
let manager: ExtensionEnablementManager;
|
||||
|
||||
describe('ExtensionEnablementManager', () => {
|
||||
beforeEach(() => {
|
||||
testDir = createTestDir();
|
||||
configDir = path.join(testDir.path, GEMINI_DIR);
|
||||
manager = new ExtensionEnablementManager(configDir);
|
||||
vi.mocked(os.homedir).mockReturnValue(path.join(testDir.path, GEMINI_DIR));
|
||||
manager = new ExtensionEnablementManager();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -230,7 +237,7 @@ describe('ExtensionEnablementManager', () => {
|
||||
|
||||
describe('extension overrides (-e <name>)', () => {
|
||||
beforeEach(() => {
|
||||
manager = new ExtensionEnablementManager(configDir, ['ext-test']);
|
||||
manager = new ExtensionEnablementManager(['ext-test']);
|
||||
});
|
||||
|
||||
it('can enable extensions, case-insensitive', () => {
|
||||
@@ -238,29 +245,29 @@ describe('ExtensionEnablementManager', () => {
|
||||
expect(manager.isEnabled('ext-test', '/')).toBe(true);
|
||||
expect(manager.isEnabled('Ext-Test', '/')).toBe(true);
|
||||
// Double check that it would have been disabled otherwise
|
||||
expect(
|
||||
new ExtensionEnablementManager(configDir).isEnabled('ext-test', '/'),
|
||||
).toBe(false);
|
||||
expect(new ExtensionEnablementManager().isEnabled('ext-test', '/')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('disable all other extensions', () => {
|
||||
manager = new ExtensionEnablementManager(configDir, ['ext-test']);
|
||||
manager = new ExtensionEnablementManager(['ext-test']);
|
||||
manager.enable('ext-test-2', true, '/');
|
||||
expect(manager.isEnabled('ext-test-2', '/')).toBe(false);
|
||||
// Double check that it would have been enabled otherwise
|
||||
expect(
|
||||
new ExtensionEnablementManager(configDir).isEnabled('ext-test-2', '/'),
|
||||
new ExtensionEnablementManager().isEnabled('ext-test-2', '/'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('none disables all extensions', () => {
|
||||
manager = new ExtensionEnablementManager(configDir, ['none']);
|
||||
manager = new ExtensionEnablementManager(['none']);
|
||||
manager.enable('ext-test', true, '/');
|
||||
expect(manager.isEnabled('ext-test', '/path/to/dir')).toBe(false);
|
||||
// Double check that it would have been enabled otherwise
|
||||
expect(
|
||||
new ExtensionEnablementManager(configDir).isEnabled('ext-test', '/'),
|
||||
).toBe(true);
|
||||
expect(new ExtensionEnablementManager().isEnabled('ext-test', '/')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -276,16 +283,13 @@ describe('ExtensionEnablementManager', () => {
|
||||
});
|
||||
|
||||
it('should not log an error if enabledExtensionNamesOverride is empty', () => {
|
||||
const manager = new ExtensionEnablementManager(configDir, []);
|
||||
const manager = new ExtensionEnablementManager([]);
|
||||
manager.validateExtensionOverrides([]);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not log an error if all enabledExtensionNamesOverride are valid', () => {
|
||||
const manager = new ExtensionEnablementManager(configDir, [
|
||||
'ext-one',
|
||||
'ext-two',
|
||||
]);
|
||||
const manager = new ExtensionEnablementManager(['ext-one', 'ext-two']);
|
||||
const extensions = [
|
||||
{ name: 'ext-one' },
|
||||
{ name: 'ext-two' },
|
||||
@@ -295,7 +299,7 @@ describe('ExtensionEnablementManager', () => {
|
||||
});
|
||||
|
||||
it('should log an error for each invalid extension name in enabledExtensionNamesOverride', () => {
|
||||
const manager = new ExtensionEnablementManager(configDir, [
|
||||
const manager = new ExtensionEnablementManager([
|
||||
'ext-one',
|
||||
'ext-invalid',
|
||||
'ext-another-invalid',
|
||||
@@ -315,7 +319,7 @@ describe('ExtensionEnablementManager', () => {
|
||||
});
|
||||
|
||||
it('should not log an error if "none" is in enabledExtensionNamesOverride', () => {
|
||||
const manager = new ExtensionEnablementManager(configDir, ['none']);
|
||||
const manager = new ExtensionEnablementManager(['none']);
|
||||
manager.validateExtensionOverrides([]);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { ExtensionStorage } from '../extension.js';
|
||||
|
||||
export interface ExtensionEnablementConfig {
|
||||
overrides: string[];
|
||||
@@ -112,9 +113,12 @@ export class ExtensionEnablementManager {
|
||||
// only the ones in this list.
|
||||
private enabledExtensionNamesOverride: string[];
|
||||
|
||||
constructor(configDir: string, enabledExtensionNames?: string[]) {
|
||||
this.configDir = configDir;
|
||||
this.configFilePath = path.join(configDir, 'extension-enablement.json');
|
||||
constructor(enabledExtensionNames?: string[]) {
|
||||
this.configDir = ExtensionStorage.getUserExtensionsDir();
|
||||
this.configFilePath = path.join(
|
||||
this.configDir,
|
||||
'extension-enablement.json',
|
||||
);
|
||||
this.enabledExtensionNamesOverride =
|
||||
enabledExtensionNames?.map((name) => name.toLowerCase()) ?? [];
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
ExtensionStorage,
|
||||
INSTALL_METADATA_FILENAME,
|
||||
annotateActiveExtensions,
|
||||
loadExtension,
|
||||
@@ -137,7 +136,7 @@ describe('update tests', () => {
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const updateInfo = await updateExtension(
|
||||
extension,
|
||||
@@ -194,7 +193,7 @@ describe('update tests', () => {
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
await updateExtension(
|
||||
extension,
|
||||
@@ -244,7 +243,7 @@ describe('update tests', () => {
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
await expect(
|
||||
updateExtension(
|
||||
@@ -292,7 +291,7 @@ describe('update tests', () => {
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
@@ -334,7 +333,7 @@ describe('update tests', () => {
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
|
||||
mockGit.getRemotes.mockResolvedValue([
|
||||
@@ -380,7 +379,7 @@ describe('update tests', () => {
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
@@ -419,7 +418,7 @@ describe('update tests', () => {
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
const dispatch = vi.fn();
|
||||
await checkForAllExtensionUpdates(
|
||||
@@ -454,7 +453,7 @@ describe('update tests', () => {
|
||||
})!,
|
||||
],
|
||||
process.cwd(),
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
|
||||
mockGit.getRemotes.mockRejectedValue(new Error('Git error'));
|
||||
|
||||
@@ -26,7 +26,7 @@ import { getStartupWarnings } from './utils/startupWarnings.js';
|
||||
import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
|
||||
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
||||
import { runNonInteractive } from './nonInteractiveCli.js';
|
||||
import { ExtensionStorage, loadExtensions } from './config/extension.js';
|
||||
import { loadExtensions } from './config/extension.js';
|
||||
import {
|
||||
cleanupCheckpoints,
|
||||
registerCleanup,
|
||||
@@ -286,7 +286,7 @@ export async function main() {
|
||||
const partialConfig = await loadCliConfig(
|
||||
settings.merged,
|
||||
[],
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
sessionId,
|
||||
argv,
|
||||
);
|
||||
@@ -358,7 +358,6 @@ export async function main() {
|
||||
// may have side effects.
|
||||
{
|
||||
const extensionEnablementManager = new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
argv.extensions,
|
||||
);
|
||||
const extensions = loadExtensions(extensionEnablementManager);
|
||||
|
||||
@@ -73,7 +73,6 @@ vi.mock('./hooks/useFolderTrust.js');
|
||||
vi.mock('./hooks/useIdeTrustListener.js');
|
||||
vi.mock('./hooks/useMessageQueue.js');
|
||||
vi.mock('./hooks/useAutoAcceptIndicator.js');
|
||||
vi.mock('./hooks/useWorkspaceMigration.js');
|
||||
vi.mock('./hooks/useGitBranchName.js');
|
||||
vi.mock('./contexts/VimModeContext.js');
|
||||
vi.mock('./contexts/SessionContext.js');
|
||||
@@ -100,7 +99,6 @@ import { useFolderTrust } from './hooks/useFolderTrust.js';
|
||||
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
|
||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||
import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
|
||||
import { useGitBranchName } from './hooks/useGitBranchName.js';
|
||||
import { useVimMode } from './contexts/VimModeContext.js';
|
||||
import { useSessionStats } from './contexts/SessionContext.js';
|
||||
@@ -132,7 +130,6 @@ describe('AppContainer State Management', () => {
|
||||
const mockedUseIdeTrustListener = useIdeTrustListener as Mock;
|
||||
const mockedUseMessageQueue = useMessageQueue as Mock;
|
||||
const mockedUseAutoAcceptIndicator = useAutoAcceptIndicator as Mock;
|
||||
const mockedUseWorkspaceMigration = useWorkspaceMigration as Mock;
|
||||
const mockedUseGitBranchName = useGitBranchName as Mock;
|
||||
const mockedUseVimMode = useVimMode as Mock;
|
||||
const mockedUseSessionStats = useSessionStats as Mock;
|
||||
@@ -236,12 +233,6 @@ describe('AppContainer State Management', () => {
|
||||
getQueuedMessagesText: vi.fn().mockReturnValue(''),
|
||||
});
|
||||
mockedUseAutoAcceptIndicator.mockReturnValue(false);
|
||||
mockedUseWorkspaceMigration.mockReturnValue({
|
||||
showWorkspaceMigrationDialog: false,
|
||||
workspaceExtensions: [],
|
||||
onWorkspaceMigrationDialogOpen: vi.fn(),
|
||||
onWorkspaceMigrationDialogClose: vi.fn(),
|
||||
});
|
||||
mockedUseGitBranchName.mockReturnValue('main');
|
||||
mockedUseVimMode.mockReturnValue({
|
||||
isVimEnabled: false,
|
||||
|
||||
@@ -86,7 +86,6 @@ import { ConsolePatcher } from './utils/ConsolePatcher.js';
|
||||
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||
import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
|
||||
import { useSessionStats } from './contexts/SessionContext.js';
|
||||
import { useGitBranchName } from './hooks/useGitBranchName.js';
|
||||
import { useExtensionUpdates } from './hooks/useExtensionUpdates.js';
|
||||
@@ -448,13 +447,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
const { isModelDialogOpen, openModelDialog, closeModelDialog } =
|
||||
useModelCommand();
|
||||
|
||||
const {
|
||||
showWorkspaceMigrationDialog,
|
||||
workspaceExtensions,
|
||||
onWorkspaceMigrationDialogOpen,
|
||||
onWorkspaceMigrationDialogClose,
|
||||
} = useWorkspaceMigration(settings);
|
||||
|
||||
const { toggleVimEnabled } = useVimMode();
|
||||
|
||||
const slashCommandActions = useMemo(
|
||||
@@ -1073,7 +1065,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
const nightly = props.version.includes('nightly');
|
||||
|
||||
const dialogsVisible =
|
||||
showWorkspaceMigrationDialog ||
|
||||
shouldShowIdePrompt ||
|
||||
isFolderTrustDialogOpen ||
|
||||
!!shellConfirmationRequest ||
|
||||
@@ -1151,8 +1142,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
messageQueue,
|
||||
queueErrorMessage,
|
||||
showAutoAcceptIndicator,
|
||||
showWorkspaceMigrationDialog,
|
||||
workspaceExtensions,
|
||||
currentModel,
|
||||
userTier,
|
||||
proQuotaRequest,
|
||||
@@ -1233,8 +1222,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
messageQueue,
|
||||
queueErrorMessage,
|
||||
showAutoAcceptIndicator,
|
||||
showWorkspaceMigrationDialog,
|
||||
workspaceExtensions,
|
||||
userTier,
|
||||
proQuotaRequest,
|
||||
contextFileNames,
|
||||
@@ -1293,8 +1280,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
refreshStatic,
|
||||
handleFinalSubmit,
|
||||
handleClearScreen,
|
||||
onWorkspaceMigrationDialogOpen,
|
||||
onWorkspaceMigrationDialogClose,
|
||||
handleProQuotaChoice,
|
||||
setQueueErrorMessage,
|
||||
popAllMessages,
|
||||
@@ -1320,8 +1305,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
refreshStatic,
|
||||
handleFinalSubmit,
|
||||
handleClearScreen,
|
||||
onWorkspaceMigrationDialogOpen,
|
||||
onWorkspaceMigrationDialogClose,
|
||||
handleProQuotaChoice,
|
||||
setQueueErrorMessage,
|
||||
popAllMessages,
|
||||
|
||||
@@ -16,7 +16,6 @@ import { AuthInProgress } from '../auth/AuthInProgress.js';
|
||||
import { AuthDialog } from '../auth/AuthDialog.js';
|
||||
import { EditorSettingsDialog } from './EditorSettingsDialog.js';
|
||||
import { PrivacyNotice } from '../privacy/PrivacyNotice.js';
|
||||
import { WorkspaceMigrationDialog } from './WorkspaceMigrationDialog.js';
|
||||
import { ProQuotaDialog } from './ProQuotaDialog.js';
|
||||
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
||||
import { ModelDialog } from './ModelDialog.js';
|
||||
@@ -50,15 +49,6 @@ export const DialogManager = ({
|
||||
if (uiState.showIdeRestartPrompt) {
|
||||
return <IdeTrustChangeDialog reason={uiState.ideTrustRestartReason} />;
|
||||
}
|
||||
if (uiState.showWorkspaceMigrationDialog) {
|
||||
return (
|
||||
<WorkspaceMigrationDialog
|
||||
workspaceExtensions={uiState.workspaceExtensions}
|
||||
onOpen={uiActions.onWorkspaceMigrationDialogOpen}
|
||||
onClose={uiActions.onWorkspaceMigrationDialogClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.proQuotaRequest) {
|
||||
return (
|
||||
<ProQuotaDialog
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { performWorkspaceExtensionMigration } from '../../config/extension.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function WorkspaceMigrationDialog(props: {
|
||||
workspaceExtensions: GeminiCLIExtension[];
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { workspaceExtensions, onOpen, onClose } = props;
|
||||
const [migrationComplete, setMigrationComplete] = useState(false);
|
||||
const [failedExtensions, setFailedExtensions] = useState<string[]>([]);
|
||||
onOpen();
|
||||
const onMigrate = async () => {
|
||||
const failed = await performWorkspaceExtensionMigration(
|
||||
workspaceExtensions,
|
||||
// We aren't updating extensions, just moving them around, don't need to ask for consent.
|
||||
async (_) => true,
|
||||
);
|
||||
setFailedExtensions(failed);
|
||||
setMigrationComplete(true);
|
||||
};
|
||||
|
||||
useInput((input) => {
|
||||
if (migrationComplete && input === 'q') {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
if (migrationComplete) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
padding={1}
|
||||
>
|
||||
{failedExtensions.length > 0 ? (
|
||||
<>
|
||||
<Text color={theme.text.primary}>
|
||||
The following extensions failed to migrate. Please try installing
|
||||
them manually. To see other changes, Gemini CLI must be restarted.
|
||||
Press 'q' to quit.
|
||||
</Text>
|
||||
<Box flexDirection="column" marginTop={1} marginLeft={2}>
|
||||
{failedExtensions.map((failed) => (
|
||||
<Text key={failed}>- {failed}</Text>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Text color={theme.text.primary}>
|
||||
Migration complete. To see changes, Gemini CLI must be restarted.
|
||||
Press 'q' to quit.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
padding={1}
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Workspace-level extensions are deprecated{'\n'}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
Would you like to install them at the user level?
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
The extension definition will remain in your workspace directory.
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
If you opt to skip, you can install them manually using the extensions
|
||||
install command.
|
||||
</Text>
|
||||
|
||||
<Box flexDirection="column" marginTop={1} marginLeft={2}>
|
||||
{workspaceExtensions.map((extension) => (
|
||||
<Text key={extension.name}>- {extension.name}</Text>
|
||||
))}
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<RadioButtonSelect
|
||||
items={[
|
||||
{ label: 'Install all', value: 'migrate', key: 'migrate' },
|
||||
{ label: 'Skip', value: 'skip', key: 'skip' },
|
||||
]}
|
||||
onSelect={(value: string) => {
|
||||
if (value === 'migrate') {
|
||||
onMigrate();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -42,8 +42,6 @@ export interface UIActions {
|
||||
refreshStatic: () => void;
|
||||
handleFinalSubmit: (value: string) => void;
|
||||
handleClearScreen: () => void;
|
||||
onWorkspaceMigrationDialogOpen: () => void;
|
||||
onWorkspaceMigrationDialogClose: () => void;
|
||||
handleProQuotaChoice: (choice: 'auth' | 'continue') => void;
|
||||
setQueueErrorMessage: (message: string | null) => void;
|
||||
popAllMessages: (onPop: (messages: string | undefined) => void) => void;
|
||||
|
||||
@@ -91,9 +91,6 @@ export interface UIState {
|
||||
messageQueue: string[];
|
||||
queueErrorMessage: string | null;
|
||||
showAutoAcceptIndicator: ApprovalMode;
|
||||
showWorkspaceMigrationDialog: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
workspaceExtensions: any[]; // Extension[]
|
||||
// Quota-related state
|
||||
userTier: UserTierId | undefined;
|
||||
proQuotaRequest: ProQuotaDialogRequest | null;
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
ExtensionStorage,
|
||||
annotateActiveExtensions,
|
||||
loadExtension,
|
||||
} from '../../config/extension.js';
|
||||
@@ -117,7 +116,7 @@ describe('useExtensionUpdates', () => {
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension({ extensionDir, workspaceDir: tempHomeDir })!],
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
)[0];
|
||||
|
||||
const addItem = vi.fn();
|
||||
@@ -190,7 +189,7 @@ describe('useExtensionUpdates', () => {
|
||||
})!,
|
||||
],
|
||||
tempHomeDir,
|
||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||
new ExtensionEnablementManager(),
|
||||
);
|
||||
|
||||
const addItem = vi.fn();
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { getWorkspaceExtensions } from '../../config/extension.js';
|
||||
import { type LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import process from 'node:process';
|
||||
|
||||
export function useWorkspaceMigration(settings: LoadedSettings) {
|
||||
const [showWorkspaceMigrationDialog, setShowWorkspaceMigrationDialog] =
|
||||
useState(false);
|
||||
const [workspaceExtensions, setWorkspaceExtensions] = useState<
|
||||
GeminiCLIExtension[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Default to true if not set.
|
||||
if (!(settings.merged.experimental?.extensionManagement ?? true)) {
|
||||
return;
|
||||
}
|
||||
const cwd = process.cwd();
|
||||
const extensions = getWorkspaceExtensions(cwd);
|
||||
if (
|
||||
extensions.length > 0 &&
|
||||
!settings.merged.extensions?.workspacesWithMigrationNudge?.includes(cwd)
|
||||
) {
|
||||
setWorkspaceExtensions(extensions);
|
||||
setShowWorkspaceMigrationDialog(true);
|
||||
console.log(settings.merged.extensions);
|
||||
}
|
||||
}, [
|
||||
settings.merged.extensions,
|
||||
settings.merged.experimental?.extensionManagement,
|
||||
]);
|
||||
|
||||
const onWorkspaceMigrationDialogOpen = useCallback(() => {
|
||||
const userSettings = settings.forScope(SettingScope.User);
|
||||
const extensionSettings = userSettings.settings.extensions || {
|
||||
disabled: [],
|
||||
};
|
||||
const workspacesWithMigrationNudge =
|
||||
extensionSettings.workspacesWithMigrationNudge || [];
|
||||
|
||||
const cwd = process.cwd();
|
||||
if (!workspacesWithMigrationNudge.includes(cwd)) {
|
||||
workspacesWithMigrationNudge.push(cwd);
|
||||
}
|
||||
|
||||
extensionSettings.workspacesWithMigrationNudge =
|
||||
workspacesWithMigrationNudge;
|
||||
settings.setValue(SettingScope.User, 'extensions', extensionSettings);
|
||||
}, [settings]);
|
||||
|
||||
const onWorkspaceMigrationDialogClose = useCallback(() => {
|
||||
setShowWorkspaceMigrationDialog(false);
|
||||
}, [setShowWorkspaceMigrationDialog]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
showWorkspaceMigrationDialog,
|
||||
workspaceExtensions,
|
||||
onWorkspaceMigrationDialogOpen,
|
||||
onWorkspaceMigrationDialogClose,
|
||||
}),
|
||||
[
|
||||
showWorkspaceMigrationDialog,
|
||||
workspaceExtensions,
|
||||
onWorkspaceMigrationDialogOpen,
|
||||
onWorkspaceMigrationDialogClose,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,6 @@ import * as path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { ExtensionStorage } from '../config/extension.js';
|
||||
import type { CliArgs } from '../config/config.js';
|
||||
import { loadCliConfig } from '../config/config.js';
|
||||
import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js';
|
||||
@@ -207,10 +206,7 @@ class GeminiAgent {
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
this.extensions,
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
this.argv.extensions,
|
||||
),
|
||||
new ExtensionEnablementManager(this.argv.extensions),
|
||||
sessionId,
|
||||
this.argv,
|
||||
cwd,
|
||||
|
||||
Reference in New Issue
Block a user