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