22.5 插件发布与维护
约 2408 字大约 8 分钟
22.5.1 插件打包
基本打包配置
// package.json
{
"name": "my-claude-plugin",
"version": "1.0.0",
"description": "My Claude Code Plugin",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"plugin.yaml"
],
"scripts": {
"build": "tsc",
"prepack": "npm run build",
"pack": "npm pack",
"publish": "npm publish"
},
"keywords": [
"claude-code",
"plugin"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"@claude-code/plugin-sdk": "^1.0.0",
"typescript": "^4.9.0"
}
}TypeScript 配置
json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
### 插件清单
# plugin.yaml
name: my-claude-plugin
version: 1.0.0
description: My Claude Code Plugin
author: Your Name
license: MIT
homepage: https://github.com/yourname/my-claude-plugin
repository: https://github.com/yourname/my-claude-plugin
# 插件入口
main: dist/index.js
types: dist/index.d.ts
# 插件权限
permissions:
file:
- read: "/"
network:
- https: ["api.example.com"]
# 插件依赖
dependencies:
claude-code: ">=1.0.0"
# 插件元数据
metadata:
category: development
tags:
- code-generation
- productivity
keywords:
- plugin
- claude-code打包脚本
bash
#!/bin/bash
# scripts/build.sh
echo "Building plugin..."
# 清理构建目录
rm -rf dist
# 编译 TypeScript
npm run build
# 复制插件清单
cp plugin.yaml dist/
# 复制 README
cp README.md dist/
# 复制 LICENSE
cp LICENSE dist/ 2>/dev/null || true
echo "Build complete!"
#!/bin/bash
# scripts/pack.sh
echo "Packing plugin..."
# 构建
./scripts/build.sh
# 打包
cd dist
npm pack
# 移动包到项目根目录
mv *.tgz ../
echo "Pack complete!"22.5.2 版本管理
语义化版本
typescript
// src/version.ts
/**
* 语义化版本
*/
export class SemanticVersion {
major: number;
minor: number;
patch: number;
prerelease?: string;
build?: string;
constructor(version: string) {
const parsed = this.parse(version);
this.major = parsed.major;
this.minor = parsed.minor;
this.patch = parsed.patch;
this.prerelease = parsed.prerelease;
this.build = parsed.build;
}
/**
* 解析版本字符串
*/
private parse(version: string): {
major: number;
minor: number;
patch: number;
prerelease?: string;
build?: string;
} {
const regex = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+))?(?:\+([0-9A-Za-z-]+))?$/;
const match = version.match(regex);
if (!match) {
throw new Error(`Invalid version: ${version}`);
}
return {
major: parseInt(match[1]),
minor: parseInt(match[2]),
patch: parseInt(match[3]),
prerelease: match[4],
build: match[5]
};
}
/**
* 转换为字符串
*/
toString(): string {
let version = `${this.major}.${this.minor}.${this.patch}`;
if (this.prerelease) {
version += `-${this.prerelease}`;
}
if (this.build) {
version += `+${this.build}`;
}
return version;
}
/**
* 主版本升级
*/
bumpMajor(): SemanticVersion {
return new SemanticVersion(
`${this.major + 1}.0.0`
);
}
/**
* 次版本升级
*/
bumpMinor(): SemanticVersion {
return new SemanticVersion(
`${this.major}.${this.minor + 1}.0`
);
}
/**
* 补丁版本升级
*/
bumpPatch(): SemanticVersion {
return new SemanticVersion(
`${this.major}.${this.minor}.${this.patch + 1}`
);
}
/**
* 比较版本
*/
compare(other: SemanticVersion): number {
if (this.major !== other.major) {
return this.major - other.major;
}
if (this.minor !== other.minor) {
return this.minor - other.minor;
}
if (this.patch !== other.patch) {
return this.patch - other.patch;
}
return 0;
}
/**
* 检查是否大于
*/
greaterThan(other: SemanticVersion): boolean {
return this.compare(other) > 0;
}
/**
* 检查是否小于
*/
lessThan(other: SemanticVersion): boolean {
return this.compare(other) < 0;
}
/**
* 检查是否等于
*/
equals(other: SemanticVersion): boolean {
return this.compare(other) === 0;
}
}
// 使用示例
const version = new SemanticVersion('1.2.3');
console.log(version.toString()); // 1.2.3
const nextMajor = version.bumpMajor();
console.log(nextMajor.toString()); // 2.0.0
const nextMinor = version.bumpMinor();
console.log(nextMinor.toString()); // 1.3.0
const nextPatch = version.bumpPatch();
console.log(nextPatch.toString()); // 1.2.4
const v1 = new SemanticVersion('1.2.3');
const v2 = new SemanticVersion('1.2.4');
console.log(v1.lessThan(v2)); // true
console.log(v2.greaterThan(v1)); // true
### 版本发布脚本
#!/bin/bash
# scripts/release.sh
VERSION=$1
if [ -z "$VERSION" ]; then
echo "Usage: ./scripts/release.sh <version>"
exit 1
fi
echo "Releasing version $VERSION..."
# 更新 package.json
npm version $VERSION --no-git-tag-version
# 更新 plugin.yaml
sed -i.bak "s/^version: .*/version: $VERSION/" plugin.yaml
rm plugin.yaml.bak
# 构建和打包
./scripts/pack.sh
# 提交更改
git add package.json plugin.yaml
git commit -m "Release version $VERSION"
# 创建标签
git tag -a "v$VERSION" -m "Release version $VERSION"
# 推送到远程
git push origin main
git push origin "v$VERSION"
# 发布到 npm
npm publish
echo "Release $VERSION complete!"22.5.3 文档生成
API 文档生成
typescript
// scripts/generate-docs.ts
import { Project, TSConfigReader, TypeDocReader } from 'typedoc';
import { MarkdownRenderer } from 'typedoc-plugin-markdown';
/**
* 生成 API 文档
*/
async function generateDocs() {
const project = new Project({
tsconfig: 'tsconfig.json',
entryPoints: ['src/index.ts'],
readme: 'README.md',
out: 'docs/api',
plugin: [TypeDocReader, MarkdownRenderer],
exclude: ['**/*.test.ts', '**/node_modules/**'],
theme: 'markdown',
gitRevision: 'main'
});
await project.generate();
}
generateDocs().catch(console.error);
### README 模板
# My Claude Code Plugin
[](https://www.npmjs.com/package/my-claude-plugin)
[](LICENSE)
My Claude Code Plugin is a powerful plugin for Claude Code that provides [brief description].
## Features
- Feature 1
- Feature 2
- Feature 3
## Installation
````bash
`bash
claude plugin install my-claude-plugin
```> Or install from npm:Usage
Basic Usage
````typescript
import { MyPlugin } from 'my-claude-plugin';
const plugin = new MyPlugin();
await plugin.initialize({});
await plugin.start();
```### Advanced Usagetypescript
const plugin = new MyPlugin({ option1: 'value1', option2: 'value2' });
await plugin.initialize(config); await plugin.start();
Configuration
The plugin can be configured with the following options:
| Option | Type | Default | Description |
|---|---|---|---|
| option1 | string | 'default' | Description of option1 |
| option2 | number | 100 | Description of option2 |
API
Methods
initialize(config: PluginConfig): Promise<void>
Initializes the plugin with the given configuration.
start(): Promise<void>
Starts the plugin.
stop(): Promise<void>
Stops the plugin.
Development
Prerequisites
- Node.js >= 14
- npm >= 6
Setup
````bash
# Clone the repository
git clone https://github.com/yourname/my-claude-plugin.git
cd my-claude-plugin
# Install dependencies
npm install
# Build the project
npm run build
```### Testing
```
bash
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
### Building
````bash
````bash
# Build the project
npm run build
# Pack the project
npm run pack
```## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
MIT © [Your Name]
## Support
For support, please open an issue on GitHub or contact [your-email@example.com].
```
## 22.5.4 持续集成
### GitHub Actions 配置
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Generate coverage
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
files: ./coverage/lcov.info
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '18.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Pack
run: npm run pack
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: package
path: '*.tgz'
```
### 发布工作流
```
yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '18.x'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run tests
run: npm test
- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
## 22.5.5 错误监控
### 错误追踪集成
// src/monitoring/error-tracker.ts
/**
* 错误追踪器
*/
export class ErrorTracker {
private errors: ErrorReport[] = [];
private maxErrors: number = 100;
/**
* 追踪错误
*/
track(error: Error, context?: ErrorContext): void {
const report: ErrorReport = {
id: this.generateId(),
message: error.message,
stack: error.stack,
timestamp: new Date(),
context: context || {}
};
this.errors.push(report);
// 限制错误数量
if (this.errors.length > this.maxErrors) {
this.errors.shift();
}
// 发送到错误监控服务
this.sendToMonitoringService(report);
}
/**
* 生成 ID
*/
private generateId(): string {
return `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 发送到监控服务
*/
private sendToMonitoringService(report: ErrorReport): void {
// 集成到 Sentry、Bugsnag 等
console.log('Sending error to monitoring service:', report.id);
}
/**
* 获取错误报告
*/
getReports(limit?: number): ErrorReport[] {
if (limit) {
return this.errors.slice(-limit);
}
return [...this.errors];
}
/**
* 清除错误报告
*/
clearReports(): void {
this.errors = [];
}
/**
* 获取错误统计
*/
getStats(): ErrorStats {
const stats: ErrorStats = {
total: this.errors.length,
byType: {},
byHour: {}
};
for (const error of this.errors) {
// 按类型统计
const type = error.context.type || 'unknown';
stats.byType[type] = (stats.byType[type] || 0) + 1;
// 按小时统计
const hour = error.timestamp.getHours();
stats.byHour[hour] = (stats.byHour[hour] || 0) + 1;
}
return stats;
}
}
/**
* 错误报告
*/
interface ErrorReport {
id: string;
message: string;
stack?: string;
timestamp: Date;
context: ErrorContext;
}
/**
* 错误上下文
*/
interface ErrorContext {
type?: string;
plugin?: string;
user?: string;
[key: string]: any;
}
/**
* 错误统计
*/
interface ErrorStats {
total: number;
byType: Record<string, number>;
byHour: Record<number, number>;
}
// 使用示例
const tracker = new ErrorTracker();
try {
// 可能出错的代码
throw new Error('Something went wrong');
} catch (error) {
tracker.track(error, {
type: 'runtime',
plugin: 'my-plugin',
user: 'user123'
});
}
// 获取错误报告
const reports = tracker.getReports(10);
console.log('Recent errors:', reports);
// 获取错误统计
const stats = tracker.getStats();
console.log('Error stats:', stats);
```
### 性能监控
```
typescript
// src/monitoring/performance-monitor.ts
/**
* 性能监控器
*/
export class PerformanceMonitor {
private metrics: Map<string, Metric[]> = new Map();
private maxMetrics: number = 1000;
/**
* 记录指标
*/
record(name: string, value: number, tags?: Record<string, string>): void {
const metric: Metric = {
name,
value,
timestamp: new Date(),
tags: tags || {}
};
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
const metrics = this.metrics.get(name)!;
metrics.push(metric);
// 限制指标数量
if (metrics.length > this.maxMetrics) {
metrics.shift();
}
}
/**
* 测量函数执行时间
*/
async measure<T>(name: string, fn: () => Promise<T>, tags?: Record<string, string>): Promise<T> {
const start = Date.now();
try {
const result = await fn();
const duration = Date.now() - start;
this.record(name, duration, tags);
return result;
} catch (error) {
const duration = Date.now() - start;
this.record(`${name}.error`, duration, {
...tags,
error: error.message
});
throw error;
}
```
- 获取指标
*/
getMetrics(name: string, limit?: number): Metric[] {
const metrics = this.metrics.get(name);
- 获取指标统计
*/
getStats(name: string): MetricStats | null {
const metrics = this.metrics.get(name);
- 清除指标
*/
clearMetrics(name?: string): void {
if (name) {
this.metrics.delete(name);
} else {
this.metrics.clear();
}
}
}
- 指标
*/
interface Metric {
name: string;
value: number;
timestamp: Date;
tags: Record<string, string>;
}
- 指标统计
*/
interface MetricStats {
count: number;
sum: number;
avg: number;
min: number;
max: number;
p50: number;
p95: number;
p99: number;
}
// 使用示例
const monitor = new PerformanceMonitor();
// 记录指标
monitor.record('request.duration', 123, {
method: 'GET',
endpoint: '/api/users'
});
// 测量函数执行时间
const result = await monitor.measure('database.query', async () => {
// 数据库查询
return { id: 1, name: 'John' };
});
// 获取指标统计
const stats = monitor.getStats('request.duration');
console.log('Stats:', stats);
## 22.5.6 用户反馈#
### 反馈收集#
// src/feedback/feedback-collector.ts
/**
- 反馈收集器
/
export class FeedbackCollector {
private feedbacks: Feedback[] = [];
/*
- 收集反馈
/
collect(feedback: Omit<Feedback, 'id' | 'timestamp'>): string {
const newFeedback: Feedback = {
id: this.generateId(),
timestamp: new Date(),
...feedback
};
this.feedbacks.push(newFeedback);
// 发送到反馈服务
this.sendToFeedbackService(newFeedback);
return newFeedback.id;
}
/*
- 生成 ID
/
private generateId(): string {
return feedback-${Date.now()}-${Math.random().toString(36).substr(2, 9)};
}
/*
- 发送到反馈服务
/
private sendToFeedbackService(feedback: Feedback): void {
// 发送到反馈服务
console.log('Sending feedback:', feedback.id);
}
/*
- 获取反馈
/
getFeedbacks(limit?: number): Feedback[] {
if (limit) {
return this.feedbacks.slice(-limit);
}
return [...this.feedbacks];
}
/*
- 获取反馈统计
/
getStats(): FeedbackStats {
const stats: FeedbackStats = {
total: this.feedbacks.length,
byType: {},
byRating: {},
averageRating: 0
};
let totalRating = 0;
let ratingCount = 0;
for (const feedback of this.feedbacks) {
// 按类型统计
stats.byType[feedback.type] = (stats.byType[feedback.type] || 0) + 1;
// 按评分统计
if (feedback.rating) {
stats.byRating[feedback.rating] = (stats.byRating[feedback.rating] || 0) + 1;
totalRating += feedback.rating;
ratingCount++;
}
}
// 计算平均评分
if (ratingCount > 0) {
stats.averageRating = totalRating / ratingCount;
}
return stats;
}
}
/*
- 反馈
/
interface Feedback {
id: string;
type: 'bug' | 'feature' | 'improvement' | 'other';
rating?: number;
title: string;
description: string;
user?: string;
timestamp: Date;
}
/*
- 反馈统计
*/
interface FeedbackStats {
total: number;
byType: Record<string, number>;
byRating: Record<number, number>;
averageRating: number;
}
// 使用示例
const collector = new FeedbackCollector();
// 收集反馈
const feedbackId = collector.collect({
type: 'feature',
rating: 5,
title: 'Add new feature',
description: 'Please add this feature',
user: 'user123'
});
console.log('Feedback ID:', feedbackId);
// 获取反馈统计
const stats = collector.getStats();
console.log('Feedback stats:', stats);
typescript
// src/feedback/survey.ts
- 用户调查
*/
export class UserSurvey {
private questions: SurveyQuestion[] = [];
private responses: SurveyResponse[] = [];
- 添加问题
*/
addQuestion(question: SurveyQuestion): void {
this.questions.push(question);
}
- 提交响应
*/
submitResponse(response: Omit<SurveyResponse, 'id' | 'timestamp'>): string {
const newResponse: SurveyResponse = {
id: this.generateId(),
timestamp: new Date(),
...response
};
- 生成 ID
*/
private generateId(): string {
return response-${Date.now()}-${Math.random().toString(36).substr(2, 9)};
}
- 发送到调查服务
*/
private sendToSurveyService(response: SurveyResponse): void {
// 发送到调查服务
console.log('Sending survey response:', response.id);
}
- 获取响应
*/
getResponses(limit?: number): SurveyResponse[] {
if (limit) {
return this.responses.slice(-limit);
}
return [...this.responses];
}
- 分析响应
*/
analyzeResponses(): SurveyAnalysis {
const analysis: SurveyAnalysis = {
totalResponses: this.responses.length,
averageRating: 0,
byQuestion: {}
};
- 调查问题
*/
interface SurveyQuestion {
id: string;
type: 'text' | 'rating' | 'choice' | 'multiple';
question: string;
options?: string[];
required: boolean;
}
- 调查响应
*/
interface SurveyResponse {
id: string;
userId?: string;
answers: Record<string, any>;
timestamp: Date;
}
- 调查分析
*/
interface SurveyAnalysis {
totalResponses: number;
averageRating: number;
byQuestion: Record<string, any>;
}
// 使用示例
const survey = new UserSurvey();
// 添加问题
survey.addQuestion({
id: 'q1',
type: 'rating',
question: 'How satisfied are you with the plugin?',
required: true
});
survey.addQuestion({
id: 'q2',
type: 'text',
question: 'What do you like most about the plugin?',
required: false
});
survey.addQuestion({
id: 'q3',
type: 'choice',
question: 'How often do you use the plugin?',
options: ['Daily', 'Weekly', 'Monthly', 'Rarely'],
required: true
});
// 提交响应
const responseId = survey.submitResponse({
userId: 'user123',
answers: {
q1: 5,
q2: 'Easy to use and powerful',
q3: 'Daily'
}
});
console.log('Response ID:', responseId);
// 分析响应
const analysis = survey.analyzeResponses();
console.log('Survey analysis:', analysis);