diff --git a/packages/cli/src/services/FileCommandLoader.test.ts b/packages/cli/src/services/FileCommandLoader.test.ts index 6645fe6f09..04f3c6fabe 100644 --- a/packages/cli/src/services/FileCommandLoader.test.ts +++ b/packages/cli/src/services/FileCommandLoader.test.ts @@ -955,6 +955,64 @@ describe('FileCommandLoader', () => { assert.fail('Incorrect action type'); } }); + + it('correctly injects ${workspacePath} into extension commands', async () => { + const workspaceDir = process.cwd(); + const extensionDir = path.join( + workspaceDir, + GEMINI_DIR, + 'extensions', + 'ws-test-ext', + ); + + mock({ + [extensionDir]: { + 'gemini-extension.json': JSON.stringify({ + name: 'ws-test-ext', + version: '1.0.0', + }), + commands: { + 'ws-cmd.toml': 'prompt = "WS: ${workspacePath}"', + }, + }, + }); + + const mockConfig = { + getProjectRoot: vi.fn(() => workspaceDir), + getExtensions: vi.fn(() => [ + { + name: 'ws-test-ext', + version: '1.0.0', + isActive: true, + path: extensionDir, + }, + ]), + getFolderTrust: vi.fn(() => false), + isTrustedFolder: vi.fn(() => false), + } as unknown as Config; + const loader = new FileCommandLoader(mockConfig); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + const command = commands[0]; + + const result = await command.action?.( + createMockCommandContext({ + invocation: { + raw: '/ws-cmd', + name: 'ws-cmd', + args: '', + }, + }), + '', + ); + + if (result?.type === 'submit_prompt') { + expect(result.content).toEqual([{ text: `WS: ${workspaceDir}` }]); + } else { + assert.fail('Incorrect action type'); + } + }); }); describe('Argument Handling Integration (via ShellProcessor)', () => { diff --git a/packages/cli/src/services/FileCommandLoader.ts b/packages/cli/src/services/FileCommandLoader.ts index ee08383263..83dddd2d93 100644 --- a/packages/cli/src/services/FileCommandLoader.ts +++ b/packages/cli/src/services/FileCommandLoader.ts @@ -33,6 +33,7 @@ import { } from './prompt-processors/shellProcessor.js'; import { AtFileProcessor } from './prompt-processors/atFileProcessor.js'; import { sanitizeForDisplay } from '../ui/utils/textUtils.js'; +import { hydrateString } from '../config/extensions/variables.js'; interface CommandDirectory { path: string; @@ -239,13 +240,13 @@ export class FileCommandLoader implements ICommandLoader { const validDef = validationResult.data; - // Hydrate extensionPath if this is an extension command - if (extensionPath) { - validDef.prompt = validDef.prompt.replace( - /\$\{extensionPath\}/g, - () => extensionPath, - ); - } + // Hydrate variables in the prompt + validDef.prompt = hydrateString(validDef.prompt, { + extensionPath, + workspacePath: this.projectRoot, + '/': path.sep, + pathSeparator: path.sep, + }); const relativePathWithExt = path.relative(baseDir, filePath); const relativePath = relativePathWithExt.substring(