mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(cli): add support for the ${extensionPath} variable in extension TOML commands (#22681)
This commit is contained in:
committed by
Jerop Kipruto
parent
d78af8202a
commit
39361bce1f
@@ -267,6 +267,25 @@ When you run `/review FileCommandLoader.ts`, the `@{docs/best-practices.md}`
|
|||||||
placeholder is replaced by the content of that file, and `{{args}}` is replaced
|
placeholder is replaced by the content of that file, and `{{args}}` is replaced
|
||||||
by the text you provided, before the final prompt is sent to the model.
|
by the text you provided, before the final prompt is sent to the model.
|
||||||
|
|
||||||
|
### 5. Using extension paths with `${extensionPath}`
|
||||||
|
|
||||||
|
If you are authoring a custom command as part of an
|
||||||
|
[extension](../extensions/index.md), you can use the `${extensionPath}` variable
|
||||||
|
in your prompt. This allows your command to reference files or resources bundled
|
||||||
|
within the extension directory, regardless of where the extension is installed.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
|
||||||
|
- `${extensionPath}` is replaced with the absolute path to your extension's root
|
||||||
|
directory before any other processing (like `!{...}` or `@{...}`) occurs.
|
||||||
|
|
||||||
|
**Example (`/my-extension.toml`):**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
description = "Uses a local template from the extension."
|
||||||
|
prompt = "Please format the code according to this template: ${extensionPath}/templates/code-style.md"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Example: A "Pure Function" refactoring command
|
## Example: A "Pure Function" refactoring command
|
||||||
|
|||||||
@@ -895,6 +895,66 @@ describe('FileCommandLoader', () => {
|
|||||||
expect(command.extensionName).toBe('my-test-ext');
|
expect(command.extensionName).toBe('my-test-ext');
|
||||||
expect(command.extensionId).toBe(extensionId);
|
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)', () => {
|
describe('Argument Handling Integration (via ShellProcessor)', () => {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ interface CommandDirectory {
|
|||||||
kind: CommandKind;
|
kind: CommandKind;
|
||||||
extensionName?: string;
|
extensionName?: string;
|
||||||
extensionId?: string;
|
extensionId?: string;
|
||||||
|
extensionPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,6 +115,7 @@ export class FileCommandLoader implements ICommandLoader {
|
|||||||
dirInfo.kind,
|
dirInfo.kind,
|
||||||
dirInfo.extensionName,
|
dirInfo.extensionName,
|
||||||
dirInfo.extensionId,
|
dirInfo.extensionId,
|
||||||
|
dirInfo.extensionPath,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -175,6 +177,7 @@ export class FileCommandLoader implements ICommandLoader {
|
|||||||
kind: CommandKind.EXTENSION_FILE,
|
kind: CommandKind.EXTENSION_FILE,
|
||||||
extensionName: ext.name,
|
extensionName: ext.name,
|
||||||
extensionId: ext.id,
|
extensionId: ext.id,
|
||||||
|
extensionPath: ext.path,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
dirs.push(...extensionCommandDirs);
|
dirs.push(...extensionCommandDirs);
|
||||||
@@ -197,6 +200,7 @@ export class FileCommandLoader implements ICommandLoader {
|
|||||||
kind: CommandKind,
|
kind: CommandKind,
|
||||||
extensionName?: string,
|
extensionName?: string,
|
||||||
extensionId?: string,
|
extensionId?: string,
|
||||||
|
extensionPath?: string,
|
||||||
): Promise<SlashCommand | null> {
|
): Promise<SlashCommand | null> {
|
||||||
let fileContent: string;
|
let fileContent: string;
|
||||||
try {
|
try {
|
||||||
@@ -235,6 +239,14 @@ export class FileCommandLoader implements ICommandLoader {
|
|||||||
|
|
||||||
const validDef = validationResult.data;
|
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 relativePathWithExt = path.relative(baseDir, filePath);
|
||||||
const relativePath = relativePathWithExt.substring(
|
const relativePath = relativePathWithExt.substring(
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ PLAN MODE PROTOCOL: This setup process runs entirely within Plan Mode. While in
|
|||||||
### 2.4 Select Guides (Interactive)
|
### 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.
|
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:**
|
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:**
|
- **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.
|
- **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:
|
- **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).
|
- **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.
|
- **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.
|
4. **Continue:** Immediately proceed to the next section.
|
||||||
|
|
||||||
### 2.5 Select Workflow (Interactive)
|
### 2.5 Select Workflow (Interactive)
|
||||||
1. **Copy Initial Workflow:**
|
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.
|
2. **Determine Mode:** Use the `ask_user` tool to let the user choose their preferred workflow.
|
||||||
- **questions:**
|
- **questions:**
|
||||||
- **header:** "Workflow"
|
- **header:** "Workflow"
|
||||||
|
|||||||
Reference in New Issue
Block a user