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
Add plugin/adapter to CLI tool's pluggable arch via abstract base class.
symlink, copy, file-per-item, append-to-fileBase class establishes interface all plugins 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 identity + capabilities:
id: Used in --framework <id> + result reportingdisplayName: Shown in human-readable outstrategy: How content reaches targetcontentTypes: Filter which items adapter receivesBase class missing → create first. Pattern scales to any # of plugins.
→ Base class w/ static identity fields + abstract methods.
If err: Methods don't apply to all plugins (not all support audit) → default impls 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 → impl shape:
symlinkSync(source, target) — handle rel vs abs pathscpSync(source, target, { recursive: true }) — handle overwriteswriteFileSync(target, transform(content)) — maybe format convert→ Strategy picked w/ clear rationale based on how target discovers content.
If err: Unsure → check framework docs for how it discovers config/instruction files. Default symlink if framework reads arbitrary dirs.
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',
},
];
Strategies:
.claude/, .cursor/, .gemini/opencode.json, .aider.conf.ymlAGENTS.md, CONVENTIONS.md~/.openclaw/, ~/.hermes/Always return marker in result so users see why framework detected.
→ Detection rule reliably IDs framework, no false positives.
If err: No unique marker (generic dir) → combine markers or require explicit --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 };
}
Idempotency rules:
--force--force (remove then install)action: 'created'{ action, path, details? }→ Install creates at target, skips if present, respects --force + --dry-run.
If err: Symlink fails on Win/NTFS → fall back to dir junction or copy. Log 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:
→ Uninstall removes only plugin content, nothing else.
If err: Removal fails (perms, locked) → return err result, don't throw.
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}`),
};
}
→ List returns installed items + broken-link detection. Audit summarizes health.
If err: Target dir doesn't exist → empty results (not err — framework has nothing installed).
// In adapters/index.js
import { MyFrameworkAdapter } from './my-framework.js';
register(MyFrameworkAdapter);
Registration → adapter available for:
detectFrameworks() → getAdaptersForDetections())--framework my-framework)listAdapters())→ Adapter appears in tool detect out + targetable w/ --framework.
If err: Not appearing → verify static id matches detection rule id + register() 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);
});
});
Min: dry-run path, detection presence, content type support.
→ Adapter-specific tests confirm path + behavior.
If err: Framework not detected in CI (no marker dir) → use --framework explicitly.
id, displayName, strategy, contentTypes) setinstall() idempotent (skip if exists, respect --force)uninstall() removes only plugin contentlistInstalled() detects broken symlinksaudit() reports health accuratelytool detectmkdirSync(dir, { recursive: true }) before create.config/) may match multiple. Use specific file markers insidesupports() check: Installer calls supports(item.type) before dispatching. Wrong contentTypes → silently skipsscaffold-cli-command — build CLI cmds that use plugintest-cli-application — testing patterns for CLI + adapter testsdesign-cli-output — terminal out for install/uninstall results