skills/build-cli-plugin/SKILL.md
Build 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 framework to a CLI installer, building a plugin system for any multi-target tool, or extending an existing adapter architecture.
npx skillsauth add pjt222/agent-almanac build-cli-pluginInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Add a new plugin or adapter to a CLI tool's pluggable architecture using the abstract base class pattern.
symlink, copy, file-per-item, or append-to-fileThe base class establishes the 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 the plugin's identity and capabilities:
id: Used in --framework <id> option and result reportingdisplayName: Shown in human-readable outputstrategy: Determines how content reaches the targetcontentTypes: Filters which items this adapter receivesIf the base class does not exist yet, create it first. The pattern scales to any number of plugins.
Expected: A base class with static identity fields and abstract methods.
On failure: If the base class has methods that don't apply to all plugins (e.g., not all frameworks support audit), provide default implementations that return sensible no-ops.
| Strategy | When to use | Example |
|----------|------------|---------|
| symlink | Target reads source files directly. Cheapest, stays in sync. | Claude Code reads .claude/skills/<name>/ symlinks |
| copy | Target needs files in its own directory. Modifications don't propagate. | Some IDEs index only their own dirs |
| file-per-item | Target expects one file per item with specific format. | Cursor .mdc rules files |
| append-to-file | Target reads a single instructions file. | Aider CONVENTIONS.md, Codex AGENTS.md |
Strategy determines the implementation shape:
symlinkSync(source, target) — handle relative vs. absolute pathscpSync(source, target, { recursive: true }) — handle overwriteswriteFileSync(target, transform(content)) — may need format conversionExpected: Strategy selected with clear rationale based on how the target framework discovers content.
On failure: If unsure, check the framework's documentation for how it discovers configuration or instruction files. Default to symlink if the framework reads arbitrary directories.
Detection tells the CLI which frameworks are present in a 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:
.claude/, .cursor/, .gemini/opencode.json, .aider.conf.ymlAGENTS.md, CONVENTIONS.md~/.openclaw/, ~/.hermes/Always return the marker in the detection result so users can understand why a framework was detected.
Expected: A detection rule that reliably identifies the framework without false positives.
On failure: If the framework has no unique marker (generic directory name), use a combination of markers or require explicit --framework specification.
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:
--force is not set--force is set (remove first, then install)action: 'created'{ action, path, details? }Expected: Install creates content at the target path, skips if already present, respects --force and --dry-run.
On failure: If symlink creation fails on Windows/NTFS, fall back to directory junction or copy. Log the fallback.
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:
Expected: Uninstall removes only the plugin's content and nothing else.
On failure: If removal fails (permissions, locked file), return an error result instead of throwing.
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}`),
};
}
Expected: Listing returns all installed items with broken-link detection. Audit summarizes health.
On failure: If the target directory doesn't exist, return empty results (not an error — the framework just has nothing installed).
// In adapters/index.js
import { MyFrameworkAdapter } from './my-framework.js';
register(MyFrameworkAdapter);
Registration makes the adapter available to:
detectFrameworks() → getAdaptersForDetections())--framework my-framework)listAdapters())Expected: The adapter appears in tool detect output and can be targeted with --framework.
On failure: If the adapter doesn't appear, verify static id matches the detection rule's id and that register() was called.
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, and content type support.
Expected: Adapter-specific tests confirm the installation path and behavior.
On failure: If the framework isn't detected in CI (no marker directory), use --framework explicitly in tests.
id, displayName, strategy, contentTypes) are setinstall() is idempotent (skip if exists, respect --force)uninstall() removes only plugin-created contentlistInstalled() detects broken symlinksaudit() reports health accuratelytool detectmkdirSync(dir, { recursive: true }) before creating content.<!-- start:id --> / <!-- end:id -->), repeated installs duplicate content. Always wrap appended content..config/) may match multiple frameworks. Use specific file markers inside the directory.supports() check: The installer calls supports(item.type) before dispatching. If contentTypes is wrong, the adapter silently skips items.scaffold-cli-command — build the CLI commands that use this plugintest-cli-application — testing patterns for CLI tools including adapter testsdesign-cli-output — terminal output for install/uninstall resultstesting
Launch all available agents in parallel waves for open-ended hypothesis generation on problems where the correct domain is unknown. Use when facing a cross-domain problem with no clear starting point, when single-agent approaches have stalled, or when diverse perspectives are more valuable than deep expertise. Produces a ranked hypothesis set with convergence analysis and adversarial refinement.
tools
Write integration tests for a Node.js CLI application using the built-in node:test module. Covers the exec helper pattern, output assertions, filesystem state verification, cleanup hooks, JSON output parsing, error case testing, and state restoration after destructive tests. Use when adding tests to an existing CLI, testing a new command, verifying adapter behavior across frameworks, or setting up CI for a CLI tool.
development
Screen a proposed trademark for conflicts and distinctiveness before filing. Covers trademark database searches (TMview, WIPO Global Brand Database, USPTO TESS), distinctiveness analysis using the Abercrombie spectrum, likelihood of confusion assessment using DuPont factors and EUIPO relative grounds, common law rights evaluation, and goods/services overlap analysis. Produces a conflict report with a risk matrix. Use before adopting a new brand name, logo, or slogan — distinct from patent prior art search, which uses different databases, legal frameworks, and analysis methods.
tools
Scaffold a new CLI command using Commander.js with options, action handler, three output modes (human-readable, quiet, JSON), and optional ceremony variant. Covers command naming, option design, shared context patterns, error handling, and integration testing. Use when adding a command to an existing Commander.js CLI, designing a new CLI tool from scratch, or standardizing command structure across a multi-command CLI.