mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 21:07:00 -07:00
159 lines
4.3 KiB
TypeScript
159 lines
4.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
export interface ExtensionEnablementConfig {
|
|
overrides: string[];
|
|
}
|
|
|
|
export interface AllExtensionsEnablementConfig {
|
|
[extensionName: string]: ExtensionEnablementConfig;
|
|
}
|
|
|
|
/**
|
|
* Converts a glob pattern to a RegExp object.
|
|
* This is a simplified implementation that supports `*`.
|
|
*
|
|
* @param glob The glob pattern to convert.
|
|
* @returns A RegExp object.
|
|
*/
|
|
function globToRegex(glob: string): RegExp {
|
|
const regexString = glob
|
|
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special regex characters
|
|
.replace(/\*/g, '.*'); // Convert * to .*
|
|
|
|
return new RegExp(`^${regexString}$`);
|
|
}
|
|
|
|
/**
|
|
* Determines if an extension is enabled based on the configuration and current path.
|
|
* The last matching rule in the overrides list wins.
|
|
*
|
|
* @param config The enablement configuration for a single extension.
|
|
* @param currentPath The absolute path of the current working directory.
|
|
* @returns True if the extension is enabled, false otherwise.
|
|
*/
|
|
export class ExtensionEnablementManager {
|
|
private configFilePath: string;
|
|
private configDir: string;
|
|
|
|
constructor(configDir: string) {
|
|
this.configDir = configDir;
|
|
this.configFilePath = path.join(configDir, 'extension-enablement.json');
|
|
}
|
|
|
|
isEnabled(extensionName: string, currentPath: string): boolean {
|
|
const config = this.readConfig();
|
|
const extensionConfig = config[extensionName];
|
|
// Extensions are enabled by default.
|
|
let enabled = true;
|
|
|
|
for (const rule of extensionConfig?.overrides ?? []) {
|
|
const isDisableRule = rule.startsWith('!');
|
|
const globPattern = isDisableRule ? rule.substring(1) : rule;
|
|
const regex = globToRegex(globPattern);
|
|
if (regex.test(currentPath)) {
|
|
enabled = !isDisableRule;
|
|
}
|
|
}
|
|
|
|
return enabled;
|
|
}
|
|
|
|
readConfig(): AllExtensionsEnablementConfig {
|
|
try {
|
|
const content = fs.readFileSync(this.configFilePath, 'utf-8');
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
if (
|
|
error instanceof Error &&
|
|
'code' in error &&
|
|
error.code === 'ENOENT'
|
|
) {
|
|
return {};
|
|
}
|
|
console.error('Error reading extension enablement config:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
writeConfig(config: AllExtensionsEnablementConfig): void {
|
|
fs.mkdirSync(this.configDir, { recursive: true });
|
|
fs.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2));
|
|
}
|
|
|
|
enable(
|
|
extensionName: string,
|
|
includeSubdirs: boolean,
|
|
scopePath: string,
|
|
): void {
|
|
const config = this.readConfig();
|
|
if (!config[extensionName]) {
|
|
config[extensionName] = { overrides: [] };
|
|
}
|
|
|
|
const pathWithGlob = `${scopePath}*`;
|
|
const pathWithoutGlob = scopePath;
|
|
|
|
const newPath = includeSubdirs ? pathWithGlob : pathWithoutGlob;
|
|
const conflictingPath = includeSubdirs ? pathWithoutGlob : pathWithGlob;
|
|
|
|
config[extensionName].overrides = config[extensionName].overrides.filter(
|
|
(rule) =>
|
|
rule !== conflictingPath &&
|
|
rule !== `!${conflictingPath}` &&
|
|
rule !== `!${newPath}`,
|
|
);
|
|
|
|
if (!config[extensionName].overrides.includes(newPath)) {
|
|
config[extensionName].overrides.push(newPath);
|
|
}
|
|
|
|
this.writeConfig(config);
|
|
}
|
|
|
|
disable(
|
|
extensionName: string,
|
|
includeSubdirs: boolean,
|
|
scopePath: string,
|
|
): void {
|
|
const config = this.readConfig();
|
|
if (!config[extensionName]) {
|
|
config[extensionName] = { overrides: [] };
|
|
}
|
|
|
|
const pathWithGlob = `${scopePath}*`;
|
|
const pathWithoutGlob = scopePath;
|
|
|
|
const targetPath = includeSubdirs ? pathWithGlob : pathWithoutGlob;
|
|
const newRule = `!${targetPath}`;
|
|
const conflictingPath = includeSubdirs ? pathWithoutGlob : pathWithGlob;
|
|
|
|
config[extensionName].overrides = config[extensionName].overrides.filter(
|
|
(rule) =>
|
|
rule !== conflictingPath &&
|
|
rule !== `!${conflictingPath}` &&
|
|
rule !== targetPath,
|
|
);
|
|
|
|
if (!config[extensionName].overrides.includes(newRule)) {
|
|
config[extensionName].overrides.push(newRule);
|
|
}
|
|
|
|
this.writeConfig(config);
|
|
}
|
|
|
|
remove(extensionName: string): void {
|
|
const config = this.readConfig();
|
|
if (config[extensionName]) {
|
|
delete config[extensionName];
|
|
this.writeConfig(config);
|
|
}
|
|
}
|
|
}
|