Back to Tutorials
Advanced
60 minutes
Plugin Developer

Building Custom Plugins

Create your own plugins to extend Gemini CLI functionality for specific use cases and integrate with your development workflow.

Tutorial Progress

0 of 5 completed
Prerequisites
  • Completed Advanced Usage tutorial
  • Strong JavaScript/TypeScript knowledge
  • Understanding of Node.js and npm
  • Familiarity with CLI development concepts
1

Plugin Architecture

Understanding the Gemini CLI plugin system and how to structure your custom plugins.

Plugin Structure

A typical Gemini CLI plugin structure:

Plugin Directory Structure
my-gemini-plugin/
β”œβ”€β”€ package.json
β”œβ”€β”€ README.md
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ index.ts          # Main plugin entry point
β”‚   β”œβ”€β”€ commands/         # Command implementations
β”‚   β”‚   β”œβ”€β”€ analyze.ts
β”‚   β”‚   └── generate.ts
β”‚   β”œβ”€β”€ utils/           # Utility functions
β”‚   β”‚   └── helpers.ts
β”‚   └── types/           # TypeScript type definitions
β”‚       └── index.ts
β”œβ”€β”€ tests/               # Test files
β”‚   └── commands.test.ts
└── docs/                # Documentation
    └── usage.md

Plugin Manifest

The package.json defines your plugin metadata:

package.json
{
  "name": "gemini-plugin-myfeature",
  "version": "1.0.0",
  "description": "Custom plugin for specialized workflows",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "keywords": ["gemini-cli", "plugin", "ai", "development"],
  "gemini": {
    "plugin": true,
    "version": "^2.0.0",
    "commands": {
      "mycommand": {
        "description": "Custom command description",
        "usage": "gemini mycommand [options]",
        "examples": [
          "gemini mycommand --input file.js",
          "gemini mycommand --batch --pattern '**/*.ts'"
        ]
      }
    },
    "hooks": {
      "pre-generate": "./hooks/pre-generate.js",
      "post-analyze": "./hooks/post-analyze.js"
    }
  },
  "peerDependencies": {
    "@google/gemini-cli": "^2.0.0"
  }
}

Plugin Interface

Define TypeScript interfaces for your plugin:

Plugin Type Definitions
// src/types/index.ts
export interface PluginConfig {
  name: string;
  version: string;
  commands: Record<string, CommandDefinition>;
  hooks?: Record<string, string>;
}

export interface CommandDefinition {
  description: string;
  usage: string;
  examples: string[];
  handler: CommandHandler;
  options?: CommandOption[];
}

export interface CommandHandler {
  (args: CommandArgs, context: PluginContext): Promise<CommandResult>;
}

export interface CommandArgs {
  [key: string]: any;
  _: string[]; // Positional arguments
}

export interface PluginContext {
  gemini: GeminiClient;
  config: UserConfig;
  logger: Logger;
  fs: FileSystem;
}

export interface CommandResult {
  success: boolean;
  data?: any;
  error?: string;
  output?: string;
}
2

Creating a Basic Plugin

Let's create a simple plugin that adds custom code analysis capabilities.

Plugin Entry Point

src/index.ts
// src/index.ts
import { Plugin, PluginContext } from '@google/gemini-cli';
import { analyzeComplexity } from './commands/analyze-complexity';
import { generateTests } from './commands/generate-tests';

export default class MyCustomPlugin implements Plugin {
  name = 'my-custom-plugin';
  version = '1.0.0';

  async initialize(context: PluginContext): Promise<void> {
    context.logger.info('Initializing My Custom Plugin...');
    
    // Register commands
    context.registerCommand('analyze-complexity', analyzeComplexity);
    context.registerCommand('generate-tests', generateTests);
    
    // Register hooks
    context.registerHook('pre-analyze', this.preAnalyzeHook);
    context.registerHook('post-generate', this.postGenerateHook);
  }

  private async preAnalyzeHook(context: PluginContext, args: any): Promise<void> {
    context.logger.debug('Running pre-analyze hook');
    // Custom logic before analysis
  }

  private async postGenerateHook(context: PluginContext, result: any): Promise<void> {
    context.logger.debug('Running post-generate hook');
    // Custom logic after generation
  }
}

Command Implementation

src/commands/analyze-complexity.ts
// src/commands/analyze-complexity.ts
import { CommandHandler, CommandArgs, PluginContext, CommandResult } from '../types';
import * as fs from 'fs';
import * as path from 'path';

export const analyzeComplexity: CommandHandler = async (
  args: CommandArgs,
  context: PluginContext
): Promise<CommandResult> => {
  try {
    const { file, threshold = 10 } = args;
    
    if (!file) {
      return {
        success: false,
        error: 'File path is required'
      };
    }

    // Read file content
    const filePath = path.resolve(file);
    const content = fs.readFileSync(filePath, 'utf8');
    
    // Analyze with Gemini
    const analysisPrompt = `
Analyze the complexity of this code and provide:
1. Cyclomatic complexity score
2. Cognitive complexity score  
3. Maintainability index
4. Suggestions for improvement
5. Highlight functions with complexity > ${threshold}

Code:
${content}
`;

    const analysis = await context.gemini.generate({
      prompt: analysisPrompt,
      model: 'gemini-pro',
      temperature: 0.1
    });

    // Parse and format results
    const result = {
      file: filePath,
      analysis: analysis.text,
      timestamp: new Date().toISOString(),
      threshold
    };

    // Save results if requested
    if (args.output) {
      const outputPath = path.resolve(args.output);
      fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
      context.logger.info(`Analysis saved to ${outputPath}`);
    }

    return {
      success: true,
      data: result,
      output: analysis.text
    };

  } catch (error) {
    return {
      success: false,
      error: error.message
    };
  }
};

Utility Functions

src/utils/helpers.ts
// src/utils/helpers.ts
import * as fs from 'fs';
import * as path from 'path';
import { glob } from 'glob';

export class FileUtils {
  static async findFiles(pattern: string, options: { ignore?: string[] } = {}): Promise<string[]> {
    const files = await glob(pattern, {
      ignore: options.ignore || ['node_modules/**', 'dist/**', '*.test.*']
    });
    return files;
  }

  static readFile(filePath: string): string {
    return fs.readFileSync(path.resolve(filePath), 'utf8');
  }

  static writeFile(filePath: string, content: string): void {
    const dir = path.dirname(filePath);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    fs.writeFileSync(filePath, content);
  }

  static getFileExtension(filePath: string): string {
    return path.extname(filePath).toLowerCase();
  }

  static isCodeFile(filePath: string): boolean {
    const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.cs'];
    return codeExtensions.includes(this.getFileExtension(filePath));
  }
}

export class PromptUtils {
  static createContextualPrompt(basePrompt: string, context: {
    language?: string;
    framework?: string;
    style?: string;
  }): string {
    let prompt = basePrompt;
    
    if (context.language) {
      prompt += `
Language: ${context.language}`;
    }
    
    if (context.framework) {
      prompt += `
Framework: ${context.framework}`;
    }
    
    if (context.style) {
      prompt += `
Code style: ${context.style}`;
    }
    
    return prompt;
  }

  static formatCodeBlock(code: string, language: string): string {
    return ```${language}
${code}
````;
  }
}
3

Advanced Plugin Features

Implement advanced features like configuration management, caching, and interactive prompts.

Configuration Management

src/config/manager.ts
// src/config/manager.ts
import * as fs from 'fs';
import * as path from 'path';
import { PluginContext } from '../types';

export interface PluginConfig {
  complexity: {
    threshold: number;
    includeTests: boolean;
    outputFormat: 'json' | 'markdown' | 'html';
  };
  analysis: {
    includeMetrics: string[];
    excludePatterns: string[];
  };
  cache: {
    enabled: boolean;
    ttl: number; // Time to live in seconds
  };
}

export class ConfigManager {
  private config: PluginConfig;
  private configPath: string;

  constructor(private context: PluginContext) {
    this.configPath = path.join(process.cwd(), '.gemini-plugin.json');
    this.loadConfig();
  }

  private loadConfig(): void {
    const defaultConfig: PluginConfig = {
      complexity: {
        threshold: 10,
        includeTests: false,
        outputFormat: 'json'
      },
      analysis: {
        includeMetrics: ['complexity', 'maintainability', 'duplication'],
        excludePatterns: ['node_modules/**', '*.test.*', 'dist/**']
      },
      cache: {
        enabled: true,
        ttl: 3600
      }
    };

    try {
      if (fs.existsSync(this.configPath)) {
        const userConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
        this.config = { ...defaultConfig, ...userConfig };
      } else {
        this.config = defaultConfig;
        this.saveConfig();
      }
    } catch (error) {
      this.context.logger.warn('Failed to load config, using defaults');
      this.config = defaultConfig;
    }
  }

  private saveConfig(): void {
    try {
      fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
    } catch (error) {
      this.context.logger.error('Failed to save config:', error.message);
    }
  }

  get(key: string): any {
    return key.split('.').reduce((obj, k) => obj?.[k], this.config);
  }

  set(key: string, value: any): void {
    const keys = key.split('.');
    const lastKey = keys.pop()!;
    const target = keys.reduce((obj, k) => obj[k] = obj[k] || {}, this.config);
    target[lastKey] = value;
    this.saveConfig();
  }

  getConfig(): PluginConfig {
    return this.config;
  }
}

Caching System

src/cache/manager.ts
// src/cache/manager.ts
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';

export interface CacheEntry {
  data: any;
  timestamp: number;
  hash: string;
}

export class CacheManager {
  private cacheDir: string;

  constructor() {
    this.cacheDir = path.join(process.cwd(), '.gemini-cache');
    this.ensureCacheDir();
  }

  private ensureCacheDir(): void {
    if (!fs.existsSync(this.cacheDir)) {
      fs.mkdirSync(this.cacheDir, { recursive: true });
    }
  }

  private generateKey(input: string): string {
    return crypto.createHash('md5').update(input).digest('hex');
  }

  private getCachePath(key: string): string {
    return path.join(this.cacheDir, `${key}.json`);
  }

  async get(key: string, ttl: number = 3600): Promise<any | null> {
    try {
      const cachePath = this.getCachePath(key);

      if (!fs.existsSync(cachePath)) {
        return null;
      }

      const entry: CacheEntry = JSON.parse(fs.readFileSync(cachePath, 'utf8'));

      const now = Date.now();

      if (now - entry.timestamp > ttl * 1000) {
        fs.unlinkSync(cachePath);
        return null;
      }

      return entry.data;
    } catch (error) {
      return null;
    }
  }

  async set(key: string, data: any): Promise<void> {
    try {
      const cachePath = this.getCachePath(key);
      const entry: CacheEntry = {
        data,
        timestamp: Date.now(),
        hash: this.generateKey(JSON.stringify(data))
      };

      fs.writeFileSync(cachePath, JSON.stringify(entry));
    } catch (error) {
      // Silently fail cache writes
    }
  }

  async clear(): Promise<void> {
    try {
      const files = fs.readdirSync(this.cacheDir);
      for (const file of files) {
        fs.unlinkSync(path.join(this.cacheDir, file));
      }
    } catch (error) {
      // Silently fail cache clears
    }
  }
}

Interactive Prompts

src/interactive/prompts.ts
// src/interactive/prompts.ts
import * as readline from 'readline';

export class InteractivePrompts {
  private rl: readline.Interface;

  constructor() {
    this.rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });
  }

  async confirm(message: string): Promise<boolean> {
    const answer = await this.ask(`${message} (y/N): `);
    return ['y', 'yes', 'true', '1'].includes(answer.toLowerCase());
  }

  async select(message: string, options: string[]): Promise<string> {
    console.log(message);
    options.forEach((option, index) => {
      console.log(`  ${index + 1}. ${option}`);
    });

    while (true) {
      const answer = await this.ask('Select an option (number): ');
      const index = parseInt(answer) - 1;
      
      if (index >= 0 && index < options.length) {
        return options[index];
      }
      
      console.log('Invalid selection. Please try again.');
    }
  }

  async multiSelect(message: string, options: string[]): Promise<string[]> {
    console.log(message);
    options.forEach((option, index) => {
      console.log(`  ${index + 1}. ${option}`);
    });

    const answer = await this.ask('Select options (comma-separated numbers): ');
    const indices = answer.split(',')
      .map(s => parseInt(s.trim()) - 1)
      .filter(i => i >= 0 && i < options.length);

    return indices.map(i => options[i]);
  }

  close(): void {
    this.rl.close();
  }

  private ask(question: string): Promise<string> {
    return new Promise((resolve) => {
      this.rl.question(question, resolve);
    });
  }
}
4

Testing Your Plugin

Implement comprehensive testing to ensure your plugin works reliably across different scenarios.

Unit Tests

tests/commands.test.ts
// tests/commands.test.ts
import { analyzeComplexity } from '../src/commands/analyze-complexity';
import { PluginContext } from '../src/types';
import * as fs from 'fs';

// Mock dependencies
jest.mock('fs');
const mockFs = fs as jest.Mocked<typeof fs>;

describe('analyzeComplexity command', () => {
  let mockContext: PluginContext;

  beforeEach(() => {
    mockContext = {
      gemini: {
        generate: jest.fn().mockResolvedValue({
          text: 'Mock analysis result'
        })
      },
      logger: {
        info: jest.fn(),
        error: jest.fn(),
        debug: jest.fn()
      }
    } as any;

    mockFs.readFileSync.mockReturnValue('mock file content');
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should analyze file complexity successfully', async () => {
    const args = {
      file: 'test.js',
      threshold: 5
    };

    const result = await analyzeComplexity(args, mockContext);

    expect(result.success).toBe(true);
    expect(result.data).toBeDefined();
    expect(result.data.file).toContain('test.js');
    expect(mockContext.gemini.generate).toHaveBeenCalledWith({
      prompt: expect.stringContaining('Analyze the complexity'),
      model: 'gemini-pro',
      temperature: 0.1
    });
  });

  it('should return error when file is not provided', async () => {
    const args = {};

    const result = await analyzeComplexity(args, mockContext);

    expect(result.success).toBe(false);
    expect(result.error).toBe('File path is required');
  });

  it('should save output when requested', async () => {
    const args = {
      file: 'test.js',
      output: 'analysis.json'
    };

    mockFs.writeFileSync.mockImplementation(() => {});

    const result = await analyzeComplexity(args, mockContext);

    expect(result.success).toBe(true);
    expect(mockFs.writeFileSync).toHaveBeenCalledWith(
      expect.stringContaining('analysis.json'),
      expect.any(String)
    );
  });
});

Integration Tests

tests/integration.test.ts
// tests/integration.test.ts
import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';

describe('Plugin Integration Tests', () => {
  const testDir = path.join(__dirname, 'fixtures');
  const testFile = path.join(testDir, 'sample.js');

  beforeAll(() => {
    // Create test fixtures
    if (!fs.existsSync(testDir)) {
      fs.mkdirSync(testDir, { recursive: true });
    }

    fs.writeFileSync(testFile, `
function complexFunction(a, b, c) {
  if (a > 0) {
    if (b > 0) {
      if (c > 0) {
        return a + b + c;
      } else {
        return a + b;
      }
    } else {
      return a;
    }
  } else {
    return 0;
  }
}
`);
  });

  afterAll(() => {
    // Cleanup
    if (fs.existsSync(testFile)) {
      fs.unlinkSync(testFile);
    }
    if (fs.existsSync(testDir)) {
      fs.rmdirSync(testDir);
    }
  });

  it('should run complexity analysis via CLI', (done) => {
    const child = spawn('gemini', ['analyze-complexity', '--file', testFile], {
      stdio: 'pipe'
    });

    let output = '';
    child.stdout.on('data', (data) => {
      output += data.toString();
    });

    child.on('close', (code) => {
      expect(code).toBe(0);
      expect(output).toContain('complexity');
      done();
    });
  }, 10000);

  it('should handle invalid file paths gracefully', (done) => {
    const child = spawn('gemini', ['analyze-complexity', '--file', 'nonexistent.js'], {
      stdio: 'pipe'
    });

    let errorOutput = '';
    child.stderr.on('data', (data) => {
      errorOutput += data.toString();
    });

    child.on('close', (code) => {
      expect(code).not.toBe(0);
      expect(errorOutput).toContain('error');
      done();
    });
  });
});

Test Configuration

package.json (test configuration)
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:integration": "jest --testPathPattern=integration",
    "lint": "eslint src/**/*.ts",
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "jest": {
    "preset": "ts-jest",
    "testEnvironment": "node",
    "roots": ["<rootDir>/src", "<rootDir>/tests"],
    "testMatch": ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
    "collectCoverageFrom": [
      "src/**/*.ts",
      "!src/**/*.d.ts",
      "!src/index.ts"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}
5

Publishing Your Plugin

Package and distribute your plugin so others can use it in their Gemini CLI workflows.

Build Configuration

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

NPM Publishing

Publishing Commands
# Build the plugin
npm run build

# Run tests
npm test

# Update version
npm version patch  # or minor/major

# Publish to npm
npm publish

# Install globally for testing
npm install -g gemini-plugin-myfeature

# Register with Gemini CLI
gemini plugin install gemini-plugin-myfeature

Documentation

README.md
# My Custom Gemini CLI Plugin

A powerful plugin that extends Gemini CLI with advanced code analysis capabilities.

## Installation

```bash
npm install -g gemini-plugin-myfeature
gemini plugin install gemini-plugin-myfeature
```

## Commands

### analyze-complexity

Analyzes code complexity and provides detailed metrics.

```bash
gemini analyze-complexity --file src/app.js --threshold 10
```

**Options:**
- `--file` (required): Path to the file to analyze
- `--threshold` (optional): Complexity threshold (default: 10)
- `--output` (optional): Save results to file

### generate-tests

Generates comprehensive unit tests for your code.

```bash
gemini generate-tests --file src/utils.js --framework jest
```

**Options:**
- `--file` (required): Path to the file to generate tests for
- `--framework` (optional): Testing framework (jest, mocha, vitest)
- `--output` (optional): Output directory for test files

## Configuration

Create a `.gemini-plugin.json` file in your project root:

```json
{
  "complexity": {
    "threshold": 15,
    "includeTests": false
  },
  "analysis": {
    "includeMetrics": ["complexity", "maintainability"]
  }
}
```

## Examples

```bash
# Analyze a single file
gemini analyze-complexity --file src/app.js

# Generate tests for all components
gemini generate-tests --pattern "src/components/*.js" --framework jest

# Batch analysis with custom threshold
gemini analyze-complexity --pattern "src/**/*.js" --threshold 5 --output report.json
```

## Contributing

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Submit a pull request

## License

MIT

Next Steps

Congratulations! You've learned how to create, test, and publish custom Gemini CLI plugins. Here are some ideas for further development:

Plugin Marketplace
Explore existing plugins and share your creations
Advanced Examples
Study real-world plugin implementations
On this page