build-cli-plugin
について
このスキルは、抽象基底クラスパターンを使用してCLIプラグインやアダプターを構築する方法を開発者に指南します。プラグイン契約の定義、冪等性を持つインストール/アンインストール操作の実装、シンボリックリンクやファイル追記などのインストール戦略の選択について網羅しています。CLIツールに新しいフレームワークのサポートを追加したり、既存のプラグインアーキテクチャを拡張する際にご活用ください。
クイックインストール
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(Markdown/MDXファイルを型安全なデータコレクションに変換するTypeScriptファーストのツール)の本番環境でテストされた設定を提供します。Zodバリデーションによる型安全性を実現し、ブログ、ドキュメントサイト、コンテンツ重視のVite + Reactアプリケーション構築時にご利用ください。Viteプラグインの設定、MDXコンパイルから、デプロイ最適化、スキーマバリデーションまで、すべてを網羅しています。
polymarket
メタこのスキルは、開発者がPolymarket予測市場プラットフォームを活用したアプリケーション構築を可能にします。API統合による取引や市場データの取得に加え、WebSocketを介したリアルタイムデータストリーミングにより、ライブ取引や市場活動を監視できます。取引戦略の実装や、ライブ市場更新を処理するツールの作成にご利用ください。
creating-opencode-plugins
メタこのスキルは、開発者がコマンド、ファイル、LSP操作など25種類以上のイベントタイプにフックするOpenCodeプラグインを作成することを支援します。JavaScript/TypeScriptモジュール向けに、プラグイン構造、イベントAPI仕様、および実装パターンを提供します。カスタムイベント駆動ロジックでOpenCode AIアシスタントのライフサイクルをインターセプト、監視、または拡張する必要がある場合にご利用ください。
sglang
メタSGLangは、高性能なLLMサービングフレームワークであり、RadixAttentionプレフィックスキャッシュを活用したJSON、正規表現、エージェントワークフロー向けの高速で構造化された生成を特長とします。特にプレフィックスが繰り返されるタスクにおいて、大幅に高速な推論を実現し、複雑な構造化出力やマルチターン対話に最適です。制約付きデコードが必要な場合や、広範なプレフィックス共有を伴うアプリケーションを構築する場合は、vLLMなどの代替案ではなくSGLangを選択してください。
