build-cli-plugin
关于
This skill guides developers in building CLI plugins or adapters using the abstract base class pattern. It covers defining the plugin contract, implementing idempotent install/uninstall operations, and choosing installation strategies like symlinking or file appending. Use it to add support for new frameworks to a CLI tool or to extend an existing plugin architecture.
快速安装
Claude Code
推荐npx skills add pjt222/agent-almanac -a claude-code/plugin add https://github.com/pjt222/agent-almanacgit clone https://github.com/pjt222/agent-almanac.git ~/.claude/skills/build-cli-plugin在 Claude Code 中复制并粘贴此命令以安装该技能
技能文档
Build a CLI Plugin
Añadir un nuevo plugin o adaptador a la arquitectura conectable de una herramienta CLI usando el patrón de clase base abstracta.
Cuándo Usar
- Añadir soporte para un nuevo framework objetivo a un instalador CLI
- Construir un sistema de plugins para una herramienta de línea de comandos multi-objetivo
- Extender una arquitectura de adaptadores existente con una nueva variante de estrategia
- Portar entrega de contenido a un framework que usa una disposición de archivos diferente
Entradas
- Requerido: Framework u objetivo que el plugin soporta (nombre, rutas de configuración, convenciones)
- Requerido: Ruta a la clase base o contrato del plugin
- Requerido: Estrategia de instalación:
symlink,copy,file-per-item, oappend-to-file - Opcional: Tipos de contenido que el plugin maneja (p. ej., solo skills, skills + agentes, soporte completo)
- Opcional: Soporte de scope (a nivel de proyecto, global, ambos)
Procedimiento
Paso 1: Definir el Contrato
La clase base establece la interfaz que todos los plugins deben implementar:
export class FrameworkAdapter {
static id = 'base'; // Unique identifier
static displayName = 'Base'; // Human-readable name
static strategy = 'symlink'; // Installation strategy
static contentTypes = ['skill']; // What this adapter handles
async detect(projectDir) { return false; }
getTargetPath(projectDir, scope) { throw new Error('Not implemented'); }
async install(item, projectDir, scope, options) { throw new Error('Not implemented'); }
async uninstall(item, projectDir, scope, options) { throw new Error('Not implemented'); }
async listInstalled(projectDir, scope) { return []; }
async audit(projectDir, scope) { return { framework: this.constructor.displayName, ok: [], warnings: [], errors: [] }; }
supports(contentType) { return this.constructor.contentTypes.includes(contentType); }
}
Campos estáticos definen la identidad y capacidades del plugin:
id: Usado en la opción--framework <id>y reporte de resultadosdisplayName: Mostrado en la salida legible para humanosstrategy: Determina cómo el contenido llega al objetivocontentTypes: Filtra qué items recibe este adaptador
Si la clase base no existe aún, crearla primero. El patrón escala a cualquier número de plugins.
Esperado: Una clase base con campos estáticos de identidad y métodos abstractos.
En caso de fallo: Si la clase base tiene métodos que no aplican a todos los plugins (p. ej., no todos los frameworks soportan audit), proporcionar implementaciones por defecto que retornen no-ops sensatos.
Paso 2: Elegir la Estrategia de Instalación
| Estrategia | Cuándo usar | Ejemplo |
|---|---|---|
| symlink | El objetivo lee archivos fuente directamente. Más barato, se mantiene en sincronía. | Claude Code lee symlinks .claude/skills/<name>/ |
| copy | El objetivo necesita archivos en su propio directorio. Las modificaciones no se propagan. | Algunos IDEs solo indexan sus propios directorios |
| file-per-item | El objetivo espera un archivo por item con formato específico. | Archivos de reglas .mdc de Cursor |
| append-to-file | El objetivo lee un único archivo de instrucciones. | CONVENTIONS.md de Aider, AGENTS.md de Codex |
La estrategia determina la forma de la implementación:
- Symlink:
symlinkSync(source, target)— manejar rutas relativas vs. absolutas - Copy:
cpSync(source, target, { recursive: true })— manejar sobrescrituras - File-per-item:
writeFileSync(target, transform(content))— puede necesitar conversión de formato - Append-to-file: Envolver contenido en marcadores para inserción/reemplazo/eliminación idempotente
Esperado: Estrategia seleccionada con justificación clara basada en cómo el framework objetivo descubre contenido.
En caso de fallo: Si no se está seguro, verificar la documentación del framework para ver cómo descubre archivos de configuración o instrucción. Por defecto symlink si el framework lee directorios arbitrarios.
Paso 3: Implementar la Detección
La detección le indica al CLI qué frameworks están presentes en un proyecto:
// In detector.js — each rule checks for a filesystem marker
const RULES = [
{
id: 'my-framework',
displayName: 'My Framework',
check: (dir) => existsSync(resolve(dir, '.myframework/')),
marker: '.myframework/',
scope: 'project',
},
];
Estrategias de detección:
- Presencia de directorio:
.claude/,.cursor/,.gemini/ - Archivo de configuración:
opencode.json,.aider.conf.yml - Archivo de instrucción:
AGENTS.md,CONVENTIONS.md - Marcadores globales:
~/.openclaw/,~/.hermes/
Siempre devolver el marcador en el resultado de detección para que los usuarios puedan entender por qué se detectó un framework.
Esperado: Una regla de detección que identifica el framework de manera confiable sin falsos positivos.
En caso de fallo: Si el framework no tiene un marcador único (nombre de directorio genérico), usar una combinación de marcadores o requerir especificación explícita de --framework.
Paso 4: Implementar Install con Idempotencia
async install(item, projectDir, scope, options) {
const targetDir = this.getTargetPath(projectDir, scope);
const targetPath = resolve(targetDir, item.id);
// Idempotency: skip if already installed (unless force)
if (existsSync(targetPath) && !options.force) {
return { action: 'skipped', path: targetPath };
}
if (options.dryRun) {
return { action: 'created', path: targetPath, details: 'dry-run' };
}
// Ensure parent directory exists
mkdirSync(targetDir, { recursive: true });
// Strategy-specific installation
if (this.constructor.strategy === 'symlink') {
const relPath = relative(targetDir, item.sourceDir);
symlinkSync(relPath, targetPath);
} else if (this.constructor.strategy === 'copy') {
cpSync(item.sourceDir, targetPath, { recursive: true });
}
return { action: 'created', path: targetPath };
}
Reglas de idempotencia:
- Saltar si el objetivo existe y
--forceno está establecido - Sobrescribir si
--forceestá establecido (eliminar primero, luego instalar) - Dry-run siempre tiene éxito con
action: 'created' - Valor de retorno debe ser siempre
{ action, path, details? }
Esperado: Install crea contenido en la ruta objetivo, salta si ya está presente, respeta --force y --dry-run.
En caso de fallo: Si la creación de symlink falla en Windows/NTFS, recurrir a junction de directorio o copia. Registrar el respaldo.
Paso 5: Implementar Uninstall con Limpieza
async uninstall(item, projectDir, scope, options) {
const targetDir = this.getTargetPath(projectDir, scope);
const targetPath = resolve(targetDir, item.id);
if (!existsSync(targetPath)) {
return { action: 'skipped', path: targetPath };
}
if (options.dryRun) {
return { action: 'removed', path: targetPath };
}
// Remove the installed content
rmSync(targetPath, { recursive: true });
return { action: 'removed', path: targetPath };
}
Consideraciones de limpieza:
- Eliminar solo lo que el plugin instaló — nunca borrar archivos creados por el usuario
- Para append-to-file: eliminar la sección marcada, no el archivo entero
- Dejar los directorios padres intactos (otros plugins pueden usarlos)
Esperado: Uninstall elimina solo el contenido del plugin y nada más.
En caso de fallo: Si la eliminación falla (permisos, archivo bloqueado), devolver un resultado de error en lugar de lanzar excepción.
Paso 6: Implementar Listado y Auditoría
async listInstalled(projectDir, scope) {
const targetDir = this.getTargetPath(projectDir, scope);
if (!existsSync(targetDir)) return [];
const entries = readdirSync(targetDir);
return entries.map(name => {
const fullPath = resolve(targetDir, name);
const broken = lstatSync(fullPath).isSymbolicLink()
&& !existsSync(fullPath);
return { id: name, type: 'skill', broken };
});
}
async audit(projectDir, scope) {
const items = await this.listInstalled(projectDir, scope);
const ok = items.filter(i => !i.broken);
const broken = items.filter(i => i.broken);
return {
framework: this.constructor.displayName,
ok: [`${ok.length} skills installed`],
warnings: [],
errors: broken.map(i => `Broken: ${i.id}`),
};
}
Esperado: El listado retorna todos los items instalados con detección de enlaces rotos. La auditoría resume la salud.
En caso de fallo: Si el directorio objetivo no existe, retornar resultados vacíos (no es un error — el framework simplemente no tiene nada instalado).
Paso 7: Registrar el Plugin
// In adapters/index.js
import { MyFrameworkAdapter } from './my-framework.js';
register(MyFrameworkAdapter);
El registro hace que el adaptador esté disponible para:
- Auto-detección (
detectFrameworks()→getAdaptersForDetections()) - Selección explícita (
--framework my-framework) - Listado (
listAdapters())
Esperado: El adaptador aparece en la salida de tool detect y puede ser objetivo de --framework.
En caso de fallo: Si el adaptador no aparece, verificar que static id coincida con el id de la regla de detección y que register() haya sido llamado.
Paso 8: Escribir Pruebas
describe('adapter: my-framework (dry-run)', () => {
it('targets the correct path', () => {
const out = run('install create-skill --framework my-framework --dry-run');
assert.match(out, /\.myframework/i);
});
});
Probar al mínimo: ruta de dry-run, presencia de detección y soporte de tipo de contenido.
Esperado: Pruebas específicas del adaptador confirman la ruta de instalación y el comportamiento.
En caso de fallo: Si el framework no se detecta en CI (sin directorio marcador), usar --framework explícitamente en las pruebas.
Validación
- El plugin extiende la clase base correctamente
- Los campos estáticos (
id,displayName,strategy,contentTypes) están establecidos - La regla de detección identifica el framework sin falsos positivos
-
install()es idempotente (saltar si existe, respetar--force) -
uninstall()elimina solo contenido creado por el plugin -
listInstalled()detecta symlinks rotos -
audit()reporta la salud con precisión - El plugin está registrado y aparece en
tool detect - Las pruebas de dry-run pasan
Errores Comunes
- Olvidar symlinks relativos vs. absolutos: Los symlinks de scope de proyecto deben ser relativos (portables). Los symlinks de scope global deben ser absolutos (no dependientes del cwd).
- No manejar directorios padres faltantes: Siempre
mkdirSync(dir, { recursive: true })antes de crear contenido. - Append-to-file sin marcadores: Sin marcadores idempotentes (
<!-- start:id -->/<!-- end:id -->), las instalaciones repetidas duplican contenido. Siempre envolver el contenido añadido. - Falsos positivos de detección: Un nombre de directorio genérico (p. ej.,
.config/) puede coincidir con múltiples frameworks. Usar marcadores de archivo específicos dentro del directorio. - Olvidar la verificación
supports(): El instalador llama asupports(item.type)antes de despachar. SicontentTypeses incorrecto, el adaptador salta items silenciosamente.
Habilidades Relacionadas
scaffold-cli-command— construir los comandos CLI que usan este plugintest-cli-application— patrones de prueba para herramientas CLI incluyendo pruebas de adaptadordesign-cli-output— salida del terminal para resultados de install/uninstall
GitHub 仓库
相关推荐技能
content-collections
元Content Collections 是一个 TypeScript 优先的构建工具,可将本地 Markdown/MDX 文件转换为类型安全的数据集合。它专为构建博客、文档站和内容密集型 Vite+React 应用而设计,提供基于 Zod 的自动模式验证。该工具涵盖从 Vite 插件配置、MDX 编译到生产环境部署的完整工作流。
polymarket
元这个Claude Skill为开发者提供完整的Polymarket预测市场开发支持,涵盖API调用、交易执行和市场数据分析。关键特性包括实时WebSocket数据流,可监控实时交易、订单和市场动态。开发者可用它构建预测市场应用、实施交易策略并集成实时市场预测功能。
creating-opencode-plugins
元该Skill帮助开发者创建OpenCode插件,用于接入命令、文件、LSP等25+种事件。它提供了插件结构、事件API规范和JavaScript/TypeScript实现模式,适合需要拦截操作、扩展功能或自定义事件处理的场景。开发者可通过它快速构建响应式模块来增强OpenCode AI助手的能力。
sglang
元SGLang是一个专为LLM设计的高性能推理框架,特别适用于需要结构化输出的场景。它通过RadixAttention前缀缓存技术,在处理JSON、正则表达式、工具调用等具有重复前缀的复杂工作流时,能实现极速生成。如果你正在构建智能体或多轮对话系统,并追求远超vLLM的推理性能,SGLang是理想选择。
