executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
build-cli-pluginBuild a plugin or adapter for a CLI tool using the abstract base class pattern. Covers defining the contract (static fields, required methods), choosing an installation strategy (symlink, copy, append-to-file), implementing detection, install/uninstall with idempotency, listing, auditing, and registering the plugin. Use when adding support for a new framewo…
Independent GitHub snapshot · Automated classification · Original documentation governs
Añadir un nuevo plugin o adaptador a la arquitectura conectable de una herramienta CLI usando el patrón de clase base abstracta.
symlink, copy, file-per-item, o append-to-fileLa 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 adaptadorSi 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.
| 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:
symlinkSync(source, target) — manejar rutas relativas vs. absolutascpSync(source, target, { recursive: true }) — manejar sobrescrituraswriteFileSync(target, transform(content)) — puede necesitar conversión de formatoEsperado: 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.
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:
.claude/, .cursor/, .gemini/opencode.json, .aider.conf.ymlAGENTS.md, CONVENTIONS.md~/.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.
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:
--force no está establecido--force está establecido (eliminar primero, luego instalar)action: 'created'{ 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.
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:
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.
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).
// In adapters/index.js
import { MyFrameworkAdapter } from './my-framework.js';
register(MyFrameworkAdapter);
El registro hace que el adaptador esté disponible para:
detectFrameworks() → getAdaptersForDetections())--framework my-framework)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.
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.
id, displayName, strategy, contentTypes) están establecidosinstall() es idempotente (saltar si existe, respetar --force)uninstall() elimina solo contenido creado por el pluginlistInstalled() detecta symlinks rotosaudit() reporta la salud con precisióntool detectmkdirSync(dir, { recursive: true }) antes de crear contenido.<!-- start:id --> / <!-- end:id -->), las instalaciones repetidas duplican contenido. Siempre envolver el contenido añadido..config/) puede coincidir con múltiples frameworks. Usar marcadores de archivo específicos dentro del directorio.supports(): El instalador llama a supports(item.type) antes de despachar. Si contentTypes es incorrecto, el adaptador salta items silenciosamente.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