This guide explains how to create plugins for gh-please to extend its functionality.
The gh-please plugin system allows developers to add new commands and features without modifying the core codebase. Plugins are npm packages that implement the GhPleasePlugin interface and register commander.js commands.
All plugins must implement the GhPleasePlugin interface:
export interface GhPleasePlugin {
name: string
version: string
type: PluginType
registerCommands: () => Command[]
init?: () => Promise<void>
metadata?: PluginMetadata
}
export type PluginType = 'command-group' | 'provider' | 'utility'
export interface PluginMetadata {
author: string
description: string
homepage?: string
premium?: boolean
license?: string
keywords?: string[]
}- command-group: Adds a group of related commands (e.g., AI commands)
- provider: Provides backend implementations or services (e.g., AI providers)
- utility: Provides utility functions or enhancements
Create a new npm package:
mkdir gh-please-myplugin
cd gh-please-myplugin
npm init -yAdd plugin metadata to package.json:
{
"name": "@your-org/gh-please-myplugin",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"ghPleasePlugin": {
"name": "myplugin",
"type": "command-group",
"enabled": true
},
"peerDependencies": {
"@pleaseai/gh-please": ">=0.3.0"
},
"dependencies": {
"commander": "^12.1.0"
}
}Create src/index.ts:
import type { GhPleasePlugin } from '@pleaseai/gh-please/plugins'
import { Command } from 'commander'
const plugin: GhPleasePlugin = {
name: 'myplugin',
version: '1.0.0',
type: 'command-group',
metadata: {
author: 'Your Name',
description: 'My custom plugin for gh-please',
homepage: 'https://github.com/yourorg/gh-please-myplugin',
license: 'MIT',
keywords: ['gh-please', 'plugin'],
},
registerCommands() {
const cmd = new Command('myplugin')
.description('My custom plugin commands')
cmd
.command('hello')
.description('Say hello')
.action(() => {
console.log('Hello from my plugin!')
})
return [cmd]
},
async init() {
// Optional: Plugin initialization
// Check dependencies, validate config, etc.
},
}
export default pluginCommands should follow the gh please <plugin-name> <command> pattern:
const plugin: GhPleasePlugin = {
name: 'myplugin',
version: '1.0.0',
type: 'command-group',
registerCommands() {
const mainCmd = new Command('myplugin')
.description('My plugin commands')
// gh please myplugin hello
mainCmd
.command('hello <name>')
.option('-l, --loud', 'Print in uppercase')
.action((name: string, options: { loud?: boolean }) => {
const message = `Hello, ${name}!`
console.log(options.loud ? message.toUpperCase() : message)
})
// gh please myplugin status
mainCmd
.command('status')
.description('Show plugin status')
.action(() => {
console.log('Plugin is active!')
})
return [mainCmd]
},
}Add TypeScript configuration tsconfig.json:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext"],
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true
},
"include": ["src/**/*"]
}Build script in package.json:
{
"scripts": {
"build": "tsc",
"prepublish": "npm run build"
}
}The plugin system discovers plugins in two ways:
Plugins installed via npm are automatically discovered in node_modules:
npm install @your-org/gh-please-mypluginThe system scans node_modules for packages with ghPleasePlugin metadata.
Plugins can also be placed in ~/.gh-please/plugins/:
mkdir -p ~/.gh-please/plugins/myplugin
ln -s /path/to/your/plugin ~/.gh-please/plugins/mypluginAlways handle errors gracefully:
cmd
.command('mycommand')
.action(async (options) => {
try {
await performAction(options)
}
catch (error) {
console.error('Error:', error instanceof Error ? error.message : 'Unknown error')
process.exit(1)
}
})Support multiple languages if possible:
type Language = 'ko' | 'en'
const messages = {
ko: { success: '성공!' },
en: { success: 'Success!' }
}
function detectLanguage(): Language {
const lang = process.env.LANG || ''
return lang.startsWith('ko') ? 'ko' : 'en'
}Use the gh CLI for GitHub API calls:
async function callGitHubAPI(endpoint: string) {
const proc = Bun.spawn(['gh', 'api', endpoint], {
stdout: 'pipe',
stderr: 'pipe',
})
const output = await new Response(proc.stdout).text()
const exitCode = await proc.exited
if (exitCode !== 0) {
throw new Error('GitHub API call failed')
}
return JSON.parse(output)
}Write tests for your plugin:
import { describe, expect, test } from 'bun:test'
import plugin from './index'
describe('MyPlugin', () => {
test('should register commands', () => {
const commands = plugin.registerCommands()
expect(commands).toHaveLength(1)
expect(commands[0].name()).toBe('myplugin')
})
})npm publishnpm publish --registry=https://npm.pkg.github.comconst plugin: GhPleasePlugin = {
name: 'utils',
version: '1.0.0',
type: 'utility',
registerCommands() {
const cmd = new Command('utils')
cmd
.command('format <file>')
.description('Format a file')
.action((file: string) => {
console.log(`Formatting ${file}...`)
})
return [cmd]
},
}const plugin: GhPleasePlugin = {
name: 'jira',
version: '1.0.0',
type: 'integration',
metadata: {
author: 'Your Org',
description: 'JIRA integration for gh-please',
},
async init() {
// Check JIRA credentials
const token = process.env.JIRA_TOKEN
if (!token) {
console.warn('Warning: JIRA_TOKEN not set')
}
},
registerCommands() {
const cmd = new Command('jira')
cmd
.command('link <issue>')
.description('Link GitHub issue to JIRA')
.action((issue: string) => {
console.log(`Linking to JIRA: ${issue}`)
})
return [cmd]
},
}- Check
package.jsonhasghPleasePluginmetadata - Verify plugin exports default object implementing
GhPleasePlugin - Check plugin is in
node_modulesor~/.gh-please/plugins/ - Run
gh please plugin listto see discovered plugins
- Verify
registerCommands()returns array of Command objects - Check command name doesn't conflict with existing commands
- Ensure plugin
init()doesn't throw errors
- Install peer dependencies:
npm install @pleaseai/gh-please commander - Ensure TypeScript version >= 5.0
- Check
tsconfig.jsonhas correct module settings
For premium plugins distributed via GitHub releases:
Create a tarball with the following structure:
gh-please-ai-v0.1.0.tar.gz
├── package.json
├── dist/
│ └── index.js
├── dist/
│ └── index.d.ts
├── README.md
├── LICENSE
└── docs/
└── CONFIGURATION.md
# 1. Build the plugin
bun run build
# 2. Create distribution directory
mkdir -p dist
# 3. Copy files
cp package.json dist/
cp -r src dist/ # or dist/ if already compiled
cp README.md dist/
cp LICENSE dist/
# 4. Create tarball
cd dist
tar -czf ../gh-please-ai-v0.1.0.tar.gz \
package.json \
index.js \
index.d.ts \
README.md \
LICENSE \
docs/
# 5. Verify tarball
tar -tzf gh-please-ai-v0.1.0.tar.gz# 1. Tag the release
git tag -a v0.1.0 -m "Release v0.1.0
Features:
- Feature 1
- Feature 2
Breaking Changes:
- Change 1"
# 2. Push the tag
git push origin v0.1.0
# 3. Create GitHub release with asset
gh release create v0.1.0 \
gh-please-ai-v0.1.0.tar.gz \
--title "v0.1.0 Release Title" \
--notes "Release notes and changelog"Create .github/workflows/release.yml:
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Build plugin
run: bun run build
- name: Create tarball
run: |
mkdir -p dist
cp package.json dist/
cp -r src dist/
cp README.md LICENSE dist/
cd dist
tar -czf ../gh-please-plugin-${{ github.ref_name }}.tar.gz \
package.json src/ README.md LICENSE
- name: Create release
uses: softprops/action-gh-release@v1
with:
files: gh-please-plugin-${{ github.ref_name }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Then simply push a tag to trigger the release:
git tag -a v0.1.0 -m "Release v0.1.0"
git push origin v0.1.0Users can then install your premium plugin with:
gh please plugin install pluginname --premium- Commander.js Documentation
- Bun Documentation
- GitHub CLI Manual
- GitHub Actions Documentation
- Example Plugin: @pleaseai/gh-please-ai
- Premium Plugin Installation Guide
For plugin development help:
- Open an issue: https://github.com/pleaseai/gh-please/issues
- Check examples: https://github.com/pleaseai/gh-please/tree/main/plugins
- Read the Premium Plugin Installation Guide: ./PREMIUM_PLUGIN_INSTALLATION.md
- Report bugs: https://github.com/pleaseai/gh-please-ai/issues