mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 10:01:29 -07:00
feat(cli): integrate conductor as a built-in extension
- Introduce support for bundled 'builtin' extensions in ExtensionManager - Migrate conductor extension to packages/core/src/extensions/builtin/conductor - Add automatic migration logic to cleanup manual conductor installations - Make extension versioning optional for built-ins, defaulting to CLI version - Update build and bundle scripts to include builtin extensions - Enhance documentation to highlight conductor as a core feature
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import chalk from 'chalk';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
import { type MergedSettings, SettingScope } from './settings.js';
|
||||
@@ -458,8 +459,8 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
newExtensionConfig.name,
|
||||
hashValue(newExtensionConfig.name),
|
||||
getExtensionId(newExtensionConfig, installMetadata),
|
||||
newExtensionConfig.version,
|
||||
previousExtensionConfig.version,
|
||||
newExtensionConfig.version ?? 'unknown',
|
||||
previousExtensionConfig.version ?? 'unknown',
|
||||
installMetadata.type,
|
||||
CoreToolCallStatus.Success,
|
||||
),
|
||||
@@ -483,7 +484,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
newExtensionConfig.name,
|
||||
hashValue(newExtensionConfig.name),
|
||||
getExtensionId(newExtensionConfig, installMetadata),
|
||||
newExtensionConfig.version,
|
||||
newExtensionConfig.version ?? 'unknown',
|
||||
installMetadata.type,
|
||||
CoreToolCallStatus.Success,
|
||||
),
|
||||
@@ -520,8 +521,8 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
config?.name ?? '',
|
||||
hashValue(config?.name ?? ''),
|
||||
extensionId ?? '',
|
||||
newExtensionConfig?.version ?? '',
|
||||
previousExtensionConfig.version,
|
||||
newExtensionConfig?.version ?? 'unknown',
|
||||
previousExtensionConfig.version ?? 'unknown',
|
||||
installMetadata.type,
|
||||
CoreToolCallStatus.Error,
|
||||
),
|
||||
@@ -533,7 +534,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
newExtensionConfig?.name ?? '',
|
||||
hashValue(newExtensionConfig?.name ?? ''),
|
||||
extensionId ?? '',
|
||||
newExtensionConfig?.version ?? '',
|
||||
newExtensionConfig?.version ?? 'unknown',
|
||||
installMetadata.type,
|
||||
CoreToolCallStatus.Error,
|
||||
),
|
||||
@@ -636,6 +637,68 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
(ext): ext is GeminiCLIExtension => ext !== null,
|
||||
);
|
||||
|
||||
let builtinExtensionsDir = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'extensions',
|
||||
'builtin',
|
||||
);
|
||||
if (!fs.existsSync(builtinExtensionsDir)) {
|
||||
builtinExtensionsDir = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'core',
|
||||
'src',
|
||||
'extensions',
|
||||
'builtin',
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env['VITEST'] && fs.existsSync(builtinExtensionsDir)) {
|
||||
const builtinSubdirs =
|
||||
await fs.promises.readdir(builtinExtensionsDir);
|
||||
const builtinPromises = builtinSubdirs.map((subdir) => {
|
||||
const extensionDir = path.join(builtinExtensionsDir, subdir);
|
||||
return this._buildExtension(extensionDir, true);
|
||||
});
|
||||
const builtinResolved = await Promise.all(builtinPromises);
|
||||
|
||||
for (const builtinExt of builtinResolved) {
|
||||
if (builtinExt) {
|
||||
const existingIdx = builtExtensions.findIndex(
|
||||
(e) => e.name === builtinExt.name,
|
||||
);
|
||||
if (existingIdx !== -1) {
|
||||
// If the user has a manually installed version of the builtin extension, we migrate them.
|
||||
const manualExt = builtExtensions[existingIdx];
|
||||
const storage = new ExtensionStorage(
|
||||
manualExt.installMetadata?.type === 'link'
|
||||
? manualExt.name
|
||||
: path.basename(manualExt.path),
|
||||
);
|
||||
try {
|
||||
await fs.promises.rm(storage.getExtensionDir(), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
debugLogger.debug(
|
||||
`Migrated to built-in extension: ${builtinExt.name}. Removed manual installation.`,
|
||||
);
|
||||
} catch (e) {
|
||||
debugLogger.warn(
|
||||
`Failed to clean up manual installation of ${builtinExt.name}: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
builtExtensions[existingIdx] = builtinExt;
|
||||
} else {
|
||||
builtExtensions.push(builtinExt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const seenNames = new Set<string>();
|
||||
for (const ext of builtExtensions) {
|
||||
if (seenNames.has(ext.name)) {
|
||||
@@ -705,6 +768,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
*/
|
||||
private async _buildExtension(
|
||||
extensionDir: string,
|
||||
isBuiltin = false,
|
||||
): Promise<GeminiCLIExtension | null> {
|
||||
try {
|
||||
const stats = await fs.promises.stat(extensionDir);
|
||||
@@ -959,7 +1023,9 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
return {
|
||||
name: config.name,
|
||||
version: config.version,
|
||||
version:
|
||||
config.version ??
|
||||
(isBuiltin ? this.telemetryConfig.getClientVersion() : 'unknown'),
|
||||
path: effectiveExtensionPath,
|
||||
contextFiles,
|
||||
installMetadata,
|
||||
@@ -1028,9 +1094,9 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
const configContent = await fs.promises.readFile(configFilePath, 'utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const rawConfig = JSON.parse(configContent) as ExtensionConfig;
|
||||
if (!rawConfig.name || !rawConfig.version) {
|
||||
if (!rawConfig.name) {
|
||||
throw new Error(
|
||||
`Invalid configuration in ${configFilePath}: missing ${!rawConfig.name ? '"name"' : '"version"'}`,
|
||||
`Invalid configuration in ${configFilePath}: missing "name"`,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { ExtensionSetting } from './extensions/extensionSettings.js';
|
||||
*/
|
||||
export interface ExtensionConfig {
|
||||
name: string;
|
||||
version: string;
|
||||
version?: string;
|
||||
mcpServers?: Record<string, MCPServerConfig>;
|
||||
contextFileName?: string | string[];
|
||||
excludeTools?: string[];
|
||||
|
||||
Reference in New Issue
Block a user