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
Add plugin/adapter to CLI tool's pluggable arch via abstract base class.
Use When
- Add support for new target framework to CLI installer
- Build plugin system for multi-target CLI tool
- Extend existing adapter arch w/ new strategy variant
- Port content delivery to framework w/ diff file layout
In
- Required: Framework/target plugin supports (name, config paths, conventions)
- Required: Path to base class or plugin contract
- Required: Install strategy:
symlink,copy,file-per-item,append-to-file - Optional: Content types plugin handles (skills only, skills+agents, full)
- Optional: Scope support (project, global, both)
Do
Step 1: Define Contract
Base 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 receives
Base 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.
Step 2: Pick Strategy
| 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:
- Symlink:
symlinkSync(source, target)— handle rel vs abs paths - Copy:
cpSync(source, target, { recursive: true })— handle overwrites - File-per-item:
writeFileSync(target, transform(content))— maybe format convert - Append-to-file: Wrap in markers for idempotent insert/replace/remove
→ 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.
Step 3: Impl 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',
},
];
Strategies:
- Dir 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 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.
Step 4: Impl Install w/ 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 + no
--force - Overwrite if
--force(remove then install) - Dry-run always succeeds w/
action: 'created' - Return always
{ 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.
Step 5: Impl Uninstall w/ 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:
- Remove only what plugin installed — never user files
- Append-to-file: remove marked section, not full file
- Leave parent dirs (other plugins may use)
→ Uninstall removes only plugin content, nothing else.
If err: Removal fails (perms, locked) → return err result, don't throw.
Step 6: Impl List + 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}`),
};
}
→ 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).
Step 7: Register Plugin
// In adapters/index.js
import { MyFrameworkAdapter } from './my-framework.js';
register(MyFrameworkAdapter);
Registration → adapter available for:
- Auto-detection (
detectFrameworks()→getAdaptersForDetections()) - Explicit select (
--framework my-framework) - Listing (
listAdapters())
→ Adapter appears in tool detect out + targetable w/ --framework.
If err: Not appearing → verify static id matches detection rule id + register() called.
Step 8: 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);
});
});
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.
Check
- Extends base class correctly
- Static fields (
id,displayName,strategy,contentTypes) set - Detection rule IDs framework, no false positives
-
install()idempotent (skip if exists, respect--force) -
uninstall()removes only plugin content -
listInstalled()detects broken symlinks -
audit()reports health accurately - Plugin registered + appears in
tool detect - Dry-run tests pass
Traps
- Relative vs abs symlinks: Project-scope relative (portable). Global-scope absolute (not cwd-dep)
- Missing parent dirs: Always
mkdirSync(dir, { recursive: true })before create - Append w/o markers: No idempotent markers → repeats duplicate. Always wrap
- Detection false positives: Generic dir name (
.config/) may match multiple. Use specific file markers inside - Skip
supports()check: Installer callssupports(item.type)before dispatching. WrongcontentTypes→ silently skips
→
scaffold-cli-command— build CLI cmds that use plugintest-cli-application— testing patterns for CLI + adapter testsdesign-cli-output— terminal out for install/uninstall results
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を選択してください。
