mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
fix(cli): add workspacePath to extension variables (#8482)
Co-authored-by: Taneja Hriday <hridayt@google.com>
This commit is contained in:
@@ -153,4 +153,5 @@ Gemini CLI extensions allow variable substitution in `gemini-extension.json`. Th
|
||||
| variable | description |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `${extensionPath}` | The fully-qualified path of the extension in the user's filesystem e.g., '/Users/username/.gemini/extensions/example-extension'. This will not unwrap symlinks. |
|
||||
| `${workspacePath}` | The fully-qualified path of the current workspace. |
|
||||
| `${/} or ${pathSeparator}` | The path separator (differs per OS). |
|
||||
|
||||
@@ -743,7 +743,10 @@ describe('extension tests', () => {
|
||||
});
|
||||
|
||||
const failed = await performWorkspaceExtensionMigration([
|
||||
loadExtension(ext1Path)!,
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
]);
|
||||
|
||||
expect(failed).toEqual(['ext1']);
|
||||
@@ -757,7 +760,12 @@ describe('extension tests', () => {
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await performWorkspaceExtensionMigration([loadExtension(ext1Path)!]);
|
||||
await performWorkspaceExtensionMigration([
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
]);
|
||||
|
||||
const userExtensionsDir = path.join(
|
||||
tempHomeDir,
|
||||
@@ -775,7 +783,12 @@ describe('extension tests', () => {
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await performWorkspaceExtensionMigration([loadExtension(ext1Path)!]);
|
||||
await performWorkspaceExtensionMigration([
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
]);
|
||||
const extensions = loadExtensions();
|
||||
|
||||
expect(extensions).toEqual([]);
|
||||
@@ -794,8 +807,14 @@ describe('extension tests', () => {
|
||||
version: '1.0.0',
|
||||
});
|
||||
const extensionsToMigrate: Extension[] = [
|
||||
loadExtension(ext1Path)!,
|
||||
loadExtension(ext2Path)!,
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
loadExtension({
|
||||
extensionDir: ext2Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
];
|
||||
const failed =
|
||||
await performWorkspaceExtensionMigration(extensionsToMigrate);
|
||||
@@ -828,7 +847,10 @@ describe('extension tests', () => {
|
||||
});
|
||||
|
||||
const extensions: Extension[] = [
|
||||
loadExtension(ext1Path)!,
|
||||
loadExtension({
|
||||
extensionDir: ext1Path,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
{
|
||||
path: '/ext/path/1',
|
||||
config: { name: 'ext2', version: '1.0.0' },
|
||||
@@ -869,7 +891,12 @@ describe('extension tests', () => {
|
||||
});
|
||||
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(targetExtDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir: targetExtDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -920,7 +947,12 @@ describe('extension tests', () => {
|
||||
const setExtensionUpdateState = vi.fn();
|
||||
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -951,7 +983,12 @@ describe('extension tests', () => {
|
||||
|
||||
const setExtensionUpdateState = vi.fn();
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -980,7 +1017,12 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -1007,7 +1049,12 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -1031,7 +1078,12 @@ describe('extension tests', () => {
|
||||
installMetadata: { source: '/local/path', type: 'local' },
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -1052,7 +1104,12 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -1077,7 +1134,12 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -1103,7 +1165,12 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -1125,7 +1192,12 @@ describe('extension tests', () => {
|
||||
version: '1.0.0',
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
@@ -1145,7 +1217,12 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
const extension = annotateActiveExtensions(
|
||||
[loadExtension(extensionDir)!],
|
||||
[
|
||||
loadExtension({
|
||||
extensionDir,
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
})!,
|
||||
],
|
||||
[],
|
||||
process.cwd(),
|
||||
)[0];
|
||||
|
||||
@@ -27,6 +27,7 @@ import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { ExtensionUpdateState } from '../ui/state/extensions.js';
|
||||
import type { LoadExtensionContext } from './extensions/variableSchema.js';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
|
||||
export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
|
||||
@@ -192,7 +193,7 @@ export function loadExtensionsFromDir(dir: string): Extension[] {
|
||||
for (const subdir of fs.readdirSync(extensionsDir)) {
|
||||
const extensionDir = path.join(extensionsDir, subdir);
|
||||
|
||||
const extension = loadExtension(extensionDir);
|
||||
const extension = loadExtension({ extensionDir, workspaceDir: dir });
|
||||
if (extension != null) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
@@ -200,7 +201,8 @@ export function loadExtensionsFromDir(dir: string): Extension[] {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
export function loadExtension(extensionDir: string): Extension | null {
|
||||
export function loadExtension(context: LoadExtensionContext): Extension | null {
|
||||
const { extensionDir, workspaceDir } = context;
|
||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
@@ -227,6 +229,7 @@ export function loadExtension(extensionDir: string): Extension | null {
|
||||
const configContent = fs.readFileSync(configFilePath, 'utf-8');
|
||||
let config = recursivelyHydrateStrings(JSON.parse(configContent), {
|
||||
extensionPath: extensionDir,
|
||||
workspacePath: workspaceDir,
|
||||
'/': path.sep,
|
||||
pathSeparator: path.sep,
|
||||
}) as unknown as ExtensionConfig;
|
||||
@@ -455,7 +458,10 @@ export async function installExtension(
|
||||
}
|
||||
|
||||
try {
|
||||
newExtensionConfig = await loadExtensionConfig(localSourcePath);
|
||||
newExtensionConfig = await loadExtensionConfig({
|
||||
extensionDir: localSourcePath,
|
||||
workspaceDir: cwd,
|
||||
});
|
||||
if (!newExtensionConfig) {
|
||||
throw new Error(
|
||||
`Invalid extension at ${installMetadata.source}. Please make sure it has a valid gemini-extension.json file.`,
|
||||
@@ -533,7 +539,10 @@ export async function installExtension(
|
||||
// Attempt to load config from the source path even if installation fails
|
||||
// to get the name and version for logging.
|
||||
if (!newExtensionConfig && localSourcePath) {
|
||||
newExtensionConfig = await loadExtensionConfig(localSourcePath);
|
||||
newExtensionConfig = await loadExtensionConfig({
|
||||
extensionDir: localSourcePath,
|
||||
workspaceDir: cwd,
|
||||
});
|
||||
}
|
||||
logger?.logExtensionInstallEvent(
|
||||
new ExtensionInstallEvent(
|
||||
@@ -548,8 +557,9 @@ export async function installExtension(
|
||||
}
|
||||
|
||||
export async function loadExtensionConfig(
|
||||
extensionDir: string,
|
||||
context: LoadExtensionContext,
|
||||
): Promise<ExtensionConfig | null> {
|
||||
const { extensionDir, workspaceDir } = context;
|
||||
const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME);
|
||||
if (!fs.existsSync(configFilePath)) {
|
||||
return null;
|
||||
@@ -558,6 +568,7 @@ export async function loadExtensionConfig(
|
||||
const configContent = fs.readFileSync(configFilePath, 'utf-8');
|
||||
const config = recursivelyHydrateStrings(JSON.parse(configContent), {
|
||||
extensionPath: extensionDir,
|
||||
workspacePath: workspaceDir,
|
||||
'/': path.sep,
|
||||
pathSeparator: path.sep,
|
||||
}) as unknown as ExtensionConfig;
|
||||
@@ -677,9 +688,10 @@ export async function updateExtension(
|
||||
);
|
||||
|
||||
const updatedExtensionStorage = new ExtensionStorage(extension.name);
|
||||
const updatedExtension = loadExtension(
|
||||
updatedExtensionStorage.getExtensionDir(),
|
||||
);
|
||||
const updatedExtension = loadExtension({
|
||||
extensionDir: updatedExtensionStorage.getExtensionDir(),
|
||||
workspaceDir: cwd,
|
||||
});
|
||||
if (!updatedExtension) {
|
||||
setExtensionUpdateState(ExtensionUpdateState.ERROR);
|
||||
throw new Error('Updated extension not found after installation.');
|
||||
|
||||
@@ -15,6 +15,11 @@ export interface VariableSchema {
|
||||
[key: string]: VariableDefinition;
|
||||
}
|
||||
|
||||
export interface LoadExtensionContext {
|
||||
extensionDir: string;
|
||||
workspaceDir: string;
|
||||
}
|
||||
|
||||
const PATH_SEPARATOR_DEFINITION = {
|
||||
type: 'string',
|
||||
description: 'The path separator.',
|
||||
@@ -25,6 +30,10 @@ export const VARIABLE_SCHEMA = {
|
||||
type: 'string',
|
||||
description: 'The path of the extension in the filesystem.',
|
||||
},
|
||||
workspacePath: {
|
||||
type: 'string',
|
||||
description: 'The absolute path of the current workspace.',
|
||||
},
|
||||
'/': PATH_SEPARATOR_DEFINITION,
|
||||
pathSeparator: PATH_SEPARATOR_DEFINITION,
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user