feat(cli): add support for the ${extensionPath} variable in extension TOML commands (#22681)

This commit is contained in:
Moisés Gana Obregón
2026-03-17 08:31:52 -07:00
committed by Jerop Kipruto
parent 2add0df893
commit cb6542b2e8
4 changed files with 94 additions and 3 deletions
@@ -895,6 +895,66 @@ describe('FileCommandLoader', () => {
expect(command.extensionName).toBe('my-test-ext');
expect(command.extensionId).toBe(extensionId);
});
it('correctly injects ${extensionPath} into extension commands', async () => {
const extensionDir = path.join(
process.cwd(),
GEMINI_DIR,
'extensions',
'path-test-ext',
);
mock({
[extensionDir]: {
'gemini-extension.json': JSON.stringify({
name: 'path-test-ext',
version: '1.0.0',
}),
commands: {
'path-cmd.toml': 'prompt = "Path: ${extensionPath}/templates"',
},
},
});
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => [
{
name: 'path-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];
expect(command.name).toBe('path-cmd');
const result = await command.action?.(
createMockCommandContext({
invocation: {
raw: '/path-cmd',
name: 'path-cmd',
args: '',
},
}),
'',
);
if (result?.type === 'submit_prompt') {
expect(result.content).toEqual([
{ text: `Path: ${extensionDir}/templates` },
]);
} else {
assert.fail('Incorrect action type');
}
});
});
describe('Argument Handling Integration (via ShellProcessor)', () => {
@@ -39,6 +39,7 @@ interface CommandDirectory {
kind: CommandKind;
extensionName?: string;
extensionId?: string;
extensionPath?: string;
}
/**
@@ -114,6 +115,7 @@ export class FileCommandLoader implements ICommandLoader {
dirInfo.kind,
dirInfo.extensionName,
dirInfo.extensionId,
dirInfo.extensionPath,
),
);
@@ -175,6 +177,7 @@ export class FileCommandLoader implements ICommandLoader {
kind: CommandKind.EXTENSION_FILE,
extensionName: ext.name,
extensionId: ext.id,
extensionPath: ext.path,
}));
dirs.push(...extensionCommandDirs);
@@ -197,6 +200,7 @@ export class FileCommandLoader implements ICommandLoader {
kind: CommandKind,
extensionName?: string,
extensionId?: string,
extensionPath?: string,
): Promise<SlashCommand | null> {
let fileContent: string;
try {
@@ -235,6 +239,14 @@ 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,
);
}
const relativePathWithExt = path.relative(baseDir, filePath);
const relativePath = relativePathWithExt.substring(
0,
@@ -285,7 +285,7 @@ PLAN MODE PROTOCOL: This setup process runs entirely within Plan Mode. While in
### 2.4 Select Guides (Interactive)
1. **Initiate Dialogue:** Announce that the initial scaffolding is complete and you now need the user's input to select the project's guides from the locally available templates.
2. **Select Code Style Guides:**
- List the available style guides by using the `run_shell_command` tool to execute `ls ~/.gemini/extensions/conductor/templates/code_styleguides/`. **CRITICAL: You MUST use `run_shell_command` for this step. Do NOT use the `list_directory` tool, as the templates directory resides outside of your allowed workspace and the call will fail.**
- List the available style guides by using the `run_shell_command` tool to execute `ls ${extensionPath}/templates/code_styleguides/`. **CRITICAL: You MUST use `run_shell_command` for this step. Do NOT use the `list_directory` tool, as the templates directory resides outside of your allowed workspace and the call will fail.**
- **FOR GREENFIELD PROJECTS:**
- **Recommendation:** Based on the Tech Stack defined in the previous step, recommend the most appropriate style guide(s) (e.g., "python.md" for a Python project) and explain why.
- **Determine Mode:** Use the `ask_user` tool:
@@ -322,12 +322,12 @@ PLAN MODE PROTOCOL: This setup process runs entirely within Plan Mode. While in
- **Action:** Announce "I'll present the additional guides. Please select all that apply." Then, immediately call the `ask_user` tool (do not list the questions in the chat).
- **Method:** Use a single `ask_user` tool call. Dynamically split the available guides into batches of 4 options max. Create one `multiSelect: true` question for each batch.
3. **Action:** Construct and execute a command to create the directory and copy all selected files. For example: `mkdir -p conductor/code_styleguides && cp ~/.gemini/extensions/conductor/templates/code_styleguides/python.md ~/.gemini/extensions/conductor/templates/code_styleguides/javascript.md conductor/code_styleguides/`
3. **Action:** Construct and execute a command to create the directory and copy all selected files. For example: `mkdir -p conductor/code_styleguides && cp ${extensionPath}/templates/code_styleguides/python.md ${extensionPath}/templates/code_styleguides/javascript.md conductor/code_styleguides/`
4. **Continue:** Immediately proceed to the next section.
### 2.5 Select Workflow (Interactive)
1. **Copy Initial Workflow:**
- Copy `~/.gemini/extensions/conductor/templates/workflow.md` to `conductor/workflow.md`.
- Copy `${extensionPath}/templates/workflow.md` to `conductor/workflow.md`.
2. **Determine Mode:** Use the `ask_user` tool to let the user choose their preferred workflow.
- **questions:**
- **header:** "Workflow"