MCP HubMCP Hub
Volver a habilidades

build-cli-plugin

pjt222
Actualizado Yesterday
3 vistas
17
2
17
Ver en GitHub
Metadesign

Acerca de

Esta habilidad guía a los desarrolladores en la construcción de plugins o adaptadores para CLI utilizando el patrón de clase base abstracta. Cubre la definición del contrato del plugin, la implementación de operaciones idempotentes de instalación/desinstalación, y la elección de estrategias de instalación como enlaces simbólicos o copias. Úsala para agregar soporte a nuevos frameworks en una herramienta CLI o para extender una arquitectura de plugins existente.

Instalación rápida

Claude Code

Recomendado
Principal
npx skills add pjt222/agent-almanac -a claude-code
Comando PluginAlternativo
/plugin add https://github.com/pjt222/agent-almanac
Git CloneAlternativo
git clone https://github.com/pjt222/agent-almanac.git ~/.claude/skills/build-cli-plugin

Copia y pega este comando en Claude Code para instalar esta habilidad

Documentación

Build a CLI Plugin

Add new plugin or adapter to CLI tool's pluggable architecture using abstract base class pattern.

When Use

  • Adding support for new target framework to CLI installer
  • Building plugin system for multi-target command-line tool
  • Extending existing adapter architecture with new strategy variant
  • Porting content delivery to framework using different file layout

Inputs

  • Required: Framework or target plugin supports (name, config paths, conventions)
  • Required: Path to base class or plugin contract
  • Required: Installation strategy: symlink, copy, file-per-item, or append-to-file
  • Optional: Content types plugin handles (e.g., skills only, skills + agents, full support)
  • Optional: Scope support (project-level, global, both)

Steps

Step 1: Define the Contract

Base class establishes interface all plugins must implement:

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); }
}

Static fields define plugin's identity and capabilities:

  • id: Used in --framework <id> option and result reporting
  • displayName: Shown in human-readable output
  • strategy: Determines how content reaches target
  • contentTypes: Filters which items this adapter receives

Base class doesn't exist yet? Create it first. Pattern scales to any number of plugins.

Got: Base class with static identity fields and abstract methods.

If fail: Base class has methods that don't apply to all plugins (e.g., not all frameworks support audit)? Provide default implementations returning sensible no-ops.

Step 2: Choose the Installation Strategy

StrategyWhen to useExample
symlinkTarget reads source files directly. Cheapest, stays in sync.Claude Code reads .claude/skills/<name>/ symlinks
copyTarget needs files in its own directory. Modifications don't propagate.Some IDEs index only their own dirs
file-per-itemTarget expects one file per item with specific format.Cursor .mdc rules files
append-to-fileTarget reads a single instructions file.Aider CONVENTIONS.md, Codex AGENTS.md

Strategy determines implementation shape:

  • Symlink: symlinkSync(source, target) — handle relative vs absolute paths
  • Copy: cpSync(source, target, { recursive: true }) — handle overwrites
  • File-per-item: writeFileSync(target, transform(content)) — may need format conversion
  • Append-to-file: Wrap content in markers for idempotent insert/replace/remove

Got: Strategy selected with clear rationale based on how target framework discovers content.

If fail: Unsure? Check framework's documentation for how it discovers configuration or instruction files. Default to symlink if framework reads arbitrary directories.

Step 3: Implement Detection

Detection tells CLI which frameworks present in project:

// 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',
  },
];

Detection strategies:

  • Directory presence: .claude/, .cursor/, .gemini/
  • Config file: opencode.json, .aider.conf.yml
  • Instruction file: AGENTS.md, CONVENTIONS.md
  • Global markers: ~/.openclaw/, ~/.hermes/

Always return marker in detection result so users can understand why framework was detected.

Got: Detection rule reliably identifies framework without false positives.

If fail: Framework has no unique marker (generic directory name)? Use combination of markers or require explicit --framework specification.

Step 4: Implement Install with Idempotency

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 };
}

Idempotency rules:

  • Skip if target exists and --force not set
  • Overwrite if --force set (remove first, then install)
  • Dry-run always succeeds with action: 'created'
  • Return value must always be { action, path, details? }

Got: Install creates content at target path, skips if already present, respects --force and --dry-run.

If fail: Symlink creation fails on Windows/NTFS? Fall back to directory junction or copy. Log the fallback.

Step 5: Implement Uninstall with Cleanup

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 };
}

Cleanup considerations:

  • Remove only what plugin installed — never delete user-created files
  • For append-to-file: remove marked section, not entire file
  • Leave parent directories intact (other plugins may use them)

Got: Uninstall removes only plugin's content and nothing else.

If fail: Removal fails (permissions, locked file)? Return error result instead of throwing.

Step 6: Implement Listing and Audit

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}`),
  };
}

Got: Listing returns all installed items with broken-link detection. Audit summarizes health.

If fail: Target directory doesn't exist? Return empty results (not error — framework just has nothing installed).

Step 7: Register the Plugin

// In adapters/index.js
import { MyFrameworkAdapter } from './my-framework.js';
register(MyFrameworkAdapter);

Registration makes adapter available to:

  • Auto-detection (detectFrameworks()getAdaptersForDetections())
  • Explicit selection (--framework my-framework)
  • Listing (listAdapters())

Got: Adapter appears in tool detect output, can be targeted with --framework.

If fail: Adapter doesn't appear? Verify static id matches detection rule's id and that register() was called.

Step 8: Write Tests

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);
  });
});

Test at minimum: dry-run path, detection presence, content type support.

Got: Adapter-specific tests confirm installation path and behavior.

If fail: Framework isn't detected in CI (no marker directory)? Use --framework explicitly in tests.

Checks

  • Plugin extends base class correctly
  • Static fields (id, displayName, strategy, contentTypes) set
  • Detection rule identifies framework without false positives
  • install() idempotent (skip if exists, respect --force)
  • uninstall() removes only plugin-created content
  • listInstalled() detects broken symlinks
  • audit() reports health accurately
  • Plugin registered, appears in tool detect
  • Dry-run tests pass

Pitfalls

  • Forgetting relative vs absolute symlinks: Project-scope symlinks should be relative (portable). Global-scope symlinks should be absolute (not dependent on cwd).
  • Not handling missing parent directories: Always mkdirSync(dir, { recursive: true }) before creating content.
  • Append-to-file without markers: Without idempotent markers (<!-- start:id --> / <!-- end:id -->), repeated installs duplicate content. Always wrap appended content.
  • Detection false positives: Generic directory name (e.g., .config/) may match multiple frameworks. Use specific file markers inside directory.
  • Forgetting supports() check: Installer calls supports(item.type) before dispatching. Wrong contentTypes? Adapter silently skips items.

See Also

  • scaffold-cli-command — build CLI commands using this plugin
  • test-cli-application — testing patterns for CLI tools including adapter tests
  • design-cli-output — terminal output for install/uninstall results

Repositorio GitHub

pjt222/agent-almanac
Ruta: i18n/caveman/skills/build-cli-plugin
0
agentsagentskillsai-assisted-developmentclaude-codeskillsteams

Habilidades relacionadas

content-collections

Meta

Esta habilidad proporciona una configuración probada en producción para Content Collections, una herramienta centrada en TypeScript que transforma archivos Markdown/MDX en colecciones de datos con tipado seguro mediante validación Zod. Úsala al construir blogs, sitios de documentación o aplicaciones Vite + React con mucho contenido para garantizar seguridad de tipos y validación automática de contenido. Abarca todo, desde la configuración del plugin de Vite y compilación MDX hasta la optimización de despliegue y validación de esquemas.

Ver habilidad

polymarket

Meta

Esta habilidad permite a los desarrolladores crear aplicaciones con la plataforma de mercados de predicción Polymarket, incluyendo la integración de API para operaciones y datos de mercado. También proporciona transmisión de datos en tiempo real a través de WebSocket para monitorear operaciones en vivo y actividad del mercado. Úsela para implementar estrategias de trading o crear herramientas que procesen actualizaciones de mercado en tiempo real.

Ver habilidad

creating-opencode-plugins

Meta

Esta habilidad ayuda a los desarrolladores a crear complementos de OpenCode que se conectan a más de 25 tipos de eventos, como comandos, archivos y operaciones LSP. Proporciona la estructura del complemento, las especificaciones de la API de eventos y los patrones de implementación para módulos en JavaScript/TypeScript. Úsala cuando necesites interceptar, monitorear o extender el ciclo de vida del asistente de IA de OpenCode con lógica personalizada basada en eventos.

Ver habilidad

sglang

Meta

SGLang es un framework de alto rendimiento para el servicio de LLM que se especializa en generación rápida y estructurada para JSON, expresiones regulares y flujos de trabajo de agentes utilizando su caché de prefijos RadixAttention. Ofrece una inferencia significativamente más rápida, especialmente para tareas con prefijos repetidos, lo que lo hace ideal para salidas complejas y estructuradas, y conversaciones multiturno. Elige SGLang sobre alternativas como vLLM cuando necesites decodificación restringida o estés construyendo aplicaciones con uso extensivo de prefijos compartidos.

Ver habilidad