返回技能列表

build-cli-plugin

pjt222
更新于 Yesterday
2 次查看
17
2
17
在 GitHub 上查看
design

关于

This skill guides developers in building CLI plugins or adapters using the abstract base class pattern. It covers defining the plugin contract, implementing idempotent install/uninstall operations, and choosing installation strategies like symlink or copy. Use it to add support for new frameworks to a CLI installer or to extend an existing plugin architecture.

快速安装

Claude Code

推荐
主要方式
npx skills add pjt222/agent-almanac -a claude-code
插件命令备选方式
/plugin add https://github.com/pjt222/agent-almanac
Git 克隆备选方式
git 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 reporting
  • displayName: Shown in human-readable out
  • strategy: How content reaches target
  • contentTypes: 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

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 → 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 calls supports(item.type) before dispatching. Wrong contentTypes → silently skips

  • scaffold-cli-command — build CLI cmds that use plugin
  • test-cli-application — testing patterns for CLI + adapter tests
  • design-cli-output — terminal out for install/uninstall results

GitHub 仓库

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

相关推荐技能

content-collections

Content Collections 是一个 TypeScript 优先的构建工具,可将本地 Markdown/MDX 文件转换为类型安全的数据集合。它专为构建博客、文档站和内容密集型 Vite+React 应用而设计,提供基于 Zod 的自动模式验证。该工具涵盖从 Vite 插件配置、MDX 编译到生产环境部署的完整工作流。

查看技能

polymarket

这个Claude Skill为开发者提供完整的Polymarket预测市场开发支持,涵盖API调用、交易执行和市场数据分析。关键特性包括实时WebSocket数据流,可监控实时交易、订单和市场动态。开发者可用它构建预测市场应用、实施交易策略并集成实时市场预测功能。

查看技能

creating-opencode-plugins

该Skill帮助开发者创建OpenCode插件,用于接入命令、文件、LSP等25+种事件。它提供了插件结构、事件API规范和JavaScript/TypeScript实现模式,适合需要拦截操作、扩展功能或自定义事件处理的场景。开发者可通过它快速构建响应式模块来增强OpenCode AI助手的能力。

查看技能

sglang

SGLang是一个专为LLM设计的高性能推理框架,特别适用于需要结构化输出的场景。它通过RadixAttention前缀缓存技术,在处理JSON、正则表达式、工具调用等具有重复前缀的复杂工作流时,能实现极速生成。如果你正在构建智能体或多轮对话系统,并追求远超vLLM的推理性能,SGLang是理想选择。

查看技能