@42ailab/42plugin 0.1.0-beta.1 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -68
- package/package.json +12 -7
- package/src/api.ts +447 -0
- package/src/cli.ts +39 -16
- package/src/commands/auth.ts +83 -69
- package/src/commands/check.ts +118 -0
- package/src/commands/completion.ts +210 -0
- package/src/commands/index.ts +13 -0
- package/src/commands/install-helper.ts +71 -0
- package/src/commands/install.ts +219 -300
- package/src/commands/list.ts +42 -66
- package/src/commands/publish.ts +121 -0
- package/src/commands/search.ts +89 -85
- package/src/commands/setup.ts +158 -0
- package/src/commands/uninstall.ts +53 -44
- package/src/config.ts +27 -36
- package/src/db.ts +593 -0
- package/src/errors.ts +40 -0
- package/src/index.ts +4 -31
- package/src/services/packager.ts +177 -0
- package/src/services/publisher.ts +237 -0
- package/src/services/upload.ts +52 -0
- package/src/services/version-manager.ts +65 -0
- package/src/types.ts +396 -0
- package/src/utils.ts +128 -0
- package/src/validators/plugin-validator.ts +635 -0
- package/src/commands/version.ts +0 -20
- package/src/db/client.ts +0 -180
- package/src/services/api.ts +0 -128
- package/src/services/auth.ts +0 -46
- package/src/services/cache.ts +0 -101
- package/src/services/download.ts +0 -148
- package/src/services/link.ts +0 -86
- package/src/services/project.ts +0 -179
- package/src/types/api.ts +0 -115
- package/src/types/db.ts +0 -31
- package/src/utils/errors.ts +0 -40
- package/src/utils/platform.ts +0 -6
- package/src/utils/target.ts +0 -114
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件打包器
|
|
3
|
+
*
|
|
4
|
+
* 负责:
|
|
5
|
+
* - 计算内容哈希(基于源文件)
|
|
6
|
+
* - 打包为 .tar.gz
|
|
7
|
+
* - 计算包哈希(基于打包后文件)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import * as tar from 'tar';
|
|
14
|
+
import { config } from '../config';
|
|
15
|
+
import type { PackageResult } from '../types';
|
|
16
|
+
|
|
17
|
+
// 需要保留的功能性隐藏文件
|
|
18
|
+
const ALLOWED_HIDDEN_FILES = [
|
|
19
|
+
'.mcp.json',
|
|
20
|
+
'.tool.json',
|
|
21
|
+
'.plugin.json',
|
|
22
|
+
'.claude',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export class Packager {
|
|
26
|
+
/**
|
|
27
|
+
* 打包插件
|
|
28
|
+
*/
|
|
29
|
+
async pack(sourcePath: string, pluginName: string): Promise<PackageResult> {
|
|
30
|
+
const stat = await fs.stat(sourcePath);
|
|
31
|
+
const isFile = stat.isFile();
|
|
32
|
+
|
|
33
|
+
// 计算内容哈希
|
|
34
|
+
const contentHash = isFile
|
|
35
|
+
? await this.hashFile(sourcePath)
|
|
36
|
+
: await this.hashDirectory(sourcePath);
|
|
37
|
+
|
|
38
|
+
// 创建临时目录
|
|
39
|
+
const tempDir = path.join(config.cacheDir, 'publish', crypto.randomUUID());
|
|
40
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
41
|
+
|
|
42
|
+
const tarballPath = path.join(tempDir, `${pluginName}.tar.gz`);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// 打包
|
|
46
|
+
if (isFile) {
|
|
47
|
+
await this.packFile(sourcePath, tarballPath);
|
|
48
|
+
} else {
|
|
49
|
+
await this.packDirectory(sourcePath, tarballPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 计算包哈希和大小
|
|
53
|
+
const tarballBuffer = await fs.readFile(tarballPath);
|
|
54
|
+
const packageHash = 'sha256:' + crypto.createHash('sha256').update(tarballBuffer).digest('hex');
|
|
55
|
+
const sizeBytes = tarballBuffer.length;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
tarballPath,
|
|
59
|
+
contentHash,
|
|
60
|
+
packageHash,
|
|
61
|
+
sizeBytes,
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// 清理临时文件
|
|
65
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 清理打包临时文件
|
|
72
|
+
*/
|
|
73
|
+
async cleanup(tarballPath: string): Promise<void> {
|
|
74
|
+
const tempDir = path.dirname(tarballPath);
|
|
75
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 计算单文件哈希
|
|
80
|
+
*/
|
|
81
|
+
private async hashFile(filePath: string): Promise<string> {
|
|
82
|
+
const content = await fs.readFile(filePath);
|
|
83
|
+
return 'sha256:' + crypto.createHash('sha256').update(content).digest('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 计算目录内容哈希(类似 Git tree hash)
|
|
88
|
+
*/
|
|
89
|
+
private async hashDirectory(dirPath: string): Promise<string> {
|
|
90
|
+
const files = await this.walkDir(dirPath);
|
|
91
|
+
|
|
92
|
+
// 按路径排序以保证一致性
|
|
93
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
94
|
+
|
|
95
|
+
// 构建哈希输入:每个文件的哈希+相对路径
|
|
96
|
+
const hashInput = files.map((f) => `${f.hash}:${f.relativePath}`).join('\n');
|
|
97
|
+
|
|
98
|
+
return 'sha256:' + crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 遍历目录,计算每个文件的哈希
|
|
103
|
+
*/
|
|
104
|
+
private async walkDir(
|
|
105
|
+
dirPath: string,
|
|
106
|
+
basePath: string = dirPath
|
|
107
|
+
): Promise<Array<{ relativePath: string; hash: string }>> {
|
|
108
|
+
const results: Array<{ relativePath: string; hash: string }> = [];
|
|
109
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
110
|
+
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
// 跳过 node_modules
|
|
113
|
+
if (entry.name === 'node_modules') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 跳过非功能性隐藏文件(保留 .mcp.json 等)
|
|
118
|
+
if (entry.name.startsWith('.') && !ALLOWED_HIDDEN_FILES.includes(entry.name)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
123
|
+
const relativePath = path.relative(basePath, fullPath);
|
|
124
|
+
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
const subResults = await this.walkDir(fullPath, basePath);
|
|
127
|
+
results.push(...subResults);
|
|
128
|
+
} else if (entry.isFile()) {
|
|
129
|
+
const content = await fs.readFile(fullPath);
|
|
130
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
131
|
+
results.push({ relativePath, hash });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 打包单文件
|
|
140
|
+
*/
|
|
141
|
+
private async packFile(filePath: string, tarballPath: string): Promise<void> {
|
|
142
|
+
const dir = path.dirname(filePath);
|
|
143
|
+
const file = path.basename(filePath);
|
|
144
|
+
|
|
145
|
+
await tar.create(
|
|
146
|
+
{
|
|
147
|
+
gzip: true,
|
|
148
|
+
file: tarballPath,
|
|
149
|
+
cwd: dir,
|
|
150
|
+
},
|
|
151
|
+
[file]
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 打包目录
|
|
157
|
+
*/
|
|
158
|
+
private async packDirectory(dirPath: string, tarballPath: string): Promise<void> {
|
|
159
|
+
const entries = await fs.readdir(dirPath);
|
|
160
|
+
|
|
161
|
+
// 过滤掉非功能性隐藏文件和 node_modules
|
|
162
|
+
const filesToPack = entries.filter((e) => {
|
|
163
|
+
if (e === 'node_modules') return false;
|
|
164
|
+
if (e.startsWith('.') && !ALLOWED_HIDDEN_FILES.includes(e)) return false;
|
|
165
|
+
return true;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await tar.create(
|
|
169
|
+
{
|
|
170
|
+
gzip: true,
|
|
171
|
+
file: tarballPath,
|
|
172
|
+
cwd: dirPath,
|
|
173
|
+
},
|
|
174
|
+
filesToPack
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发布流程编排器
|
|
3
|
+
*
|
|
4
|
+
* 协调各个服务完成发布流程:
|
|
5
|
+
* 1. 验证插件结构
|
|
6
|
+
* 2. 计算版本
|
|
7
|
+
* 3. 打包
|
|
8
|
+
* 4. 上传
|
|
9
|
+
* 5. 确认发布
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import ora from 'ora';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { confirm } from '@inquirer/prompts';
|
|
15
|
+
import { api } from '../api';
|
|
16
|
+
import { PluginValidator } from '../validators/plugin-validator';
|
|
17
|
+
import { Packager } from './packager';
|
|
18
|
+
import { VersionManager } from './version-manager';
|
|
19
|
+
import { Uploader } from './upload';
|
|
20
|
+
import { formatBytes } from '../utils';
|
|
21
|
+
import { ValidationError } from '../errors';
|
|
22
|
+
import type {
|
|
23
|
+
PublishOptions,
|
|
24
|
+
PublishResult,
|
|
25
|
+
PackageResult,
|
|
26
|
+
ValidationResult,
|
|
27
|
+
} from '../types';
|
|
28
|
+
|
|
29
|
+
export class Publisher {
|
|
30
|
+
private validator: PluginValidator;
|
|
31
|
+
private packager: Packager;
|
|
32
|
+
private versionManager: VersionManager;
|
|
33
|
+
private uploader: Uploader;
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
this.validator = new PluginValidator();
|
|
37
|
+
this.packager = new Packager();
|
|
38
|
+
this.versionManager = new VersionManager();
|
|
39
|
+
this.uploader = new Uploader();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 执行发布流程
|
|
44
|
+
*/
|
|
45
|
+
async publish(options: PublishOptions): Promise<PublishResult> {
|
|
46
|
+
const spinner = ora('准备发布...').start();
|
|
47
|
+
let pkg: PackageResult | null = null;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Phase 1: 完整验证
|
|
51
|
+
spinner.text = '验证插件...';
|
|
52
|
+
const validation = await this.validator.validateFull(options.path, options.name);
|
|
53
|
+
|
|
54
|
+
// 显示验证结果
|
|
55
|
+
spinner.stop();
|
|
56
|
+
this.displayValidationResult(validation);
|
|
57
|
+
|
|
58
|
+
// 如果有错误,阻止发布
|
|
59
|
+
if (!validation.valid) {
|
|
60
|
+
throw new ValidationError('验证失败,请修复上述错误后重试', 'VALIDATION_FAILED');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const metadata = validation.metadata;
|
|
64
|
+
console.log(chalk.green(`✔ 验证通过: ${metadata.name} (${metadata.type})`));
|
|
65
|
+
|
|
66
|
+
// Phase 2: 打包
|
|
67
|
+
spinner.start('打包中...');
|
|
68
|
+
pkg = await this.packager.pack(metadata.sourcePath, metadata.name);
|
|
69
|
+
spinner.succeed(`打包完成: ${formatBytes(pkg.sizeBytes)}`);
|
|
70
|
+
|
|
71
|
+
// Phase 3: 版本决策
|
|
72
|
+
spinner.start('检查版本...');
|
|
73
|
+
const versionDecision = await this.versionManager.getNextVersion(
|
|
74
|
+
metadata.name,
|
|
75
|
+
pkg.contentHash
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// 处理不同的版本决策
|
|
79
|
+
if (versionDecision.action === 'skip' && !options.force) {
|
|
80
|
+
spinner.info('插件内容未变化,无需发布');
|
|
81
|
+
await this.packager.cleanup(pkg.tarballPath);
|
|
82
|
+
return {
|
|
83
|
+
fullName: `${await this.getUsername()}/${metadata.name}`,
|
|
84
|
+
version: versionDecision.version,
|
|
85
|
+
storageKey: '',
|
|
86
|
+
type: metadata.type,
|
|
87
|
+
action: 'unchanged',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (versionDecision.action === 'update') {
|
|
92
|
+
spinner.stop();
|
|
93
|
+
// 询问确认更新
|
|
94
|
+
const shouldUpdate = await confirm({
|
|
95
|
+
message: `发现已存在版本,是否更新到 v${versionDecision.version}?`,
|
|
96
|
+
default: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!shouldUpdate) {
|
|
100
|
+
console.log(chalk.yellow('已取消发布'));
|
|
101
|
+
await this.packager.cleanup(pkg.tarballPath);
|
|
102
|
+
return {
|
|
103
|
+
fullName: `${await this.getUsername()}/${metadata.name}`,
|
|
104
|
+
version: versionDecision.version,
|
|
105
|
+
storageKey: '',
|
|
106
|
+
type: metadata.type,
|
|
107
|
+
action: 'unchanged',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
spinner.succeed(
|
|
111
|
+
`版本: v${versionDecision.version} (更新)`
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
spinner.succeed(
|
|
115
|
+
`版本: v${versionDecision.version} (${versionDecision.action === 'create' ? '新建' : '更新'})`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Dry run 模式
|
|
120
|
+
if (options.dryRun) {
|
|
121
|
+
console.log();
|
|
122
|
+
console.log(chalk.cyan('=== Dry Run 预览 ==='));
|
|
123
|
+
console.log(` 插件名: ${metadata.name}`);
|
|
124
|
+
console.log(` 类型: ${metadata.type}`);
|
|
125
|
+
console.log(` 版本: v${versionDecision.version}`);
|
|
126
|
+
console.log(` 大小: ${formatBytes(pkg.sizeBytes)}`);
|
|
127
|
+
console.log(` 内容哈希: ${pkg.contentHash.slice(0, 20)}...`);
|
|
128
|
+
console.log(` 可见性: ${options.visibility || 'self'}`);
|
|
129
|
+
console.log(chalk.cyan('=================='));
|
|
130
|
+
|
|
131
|
+
await this.packager.cleanup(pkg.tarballPath);
|
|
132
|
+
return {
|
|
133
|
+
fullName: `${await this.getUsername()}/${metadata.name}`,
|
|
134
|
+
version: versionDecision.version,
|
|
135
|
+
storageKey: '',
|
|
136
|
+
type: metadata.type,
|
|
137
|
+
action: versionDecision.action === 'create' ? 'created' : 'updated',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Phase 4: 上传
|
|
142
|
+
spinner.start('上传中...');
|
|
143
|
+
const uploadResult = await this.uploader.upload(metadata, pkg, versionDecision.version);
|
|
144
|
+
spinner.succeed('上传完成');
|
|
145
|
+
|
|
146
|
+
// Phase 5: 确认发布
|
|
147
|
+
spinner.start('确认发布...');
|
|
148
|
+
const result = await api.confirmPublish({
|
|
149
|
+
name: metadata.name,
|
|
150
|
+
type: metadata.type,
|
|
151
|
+
version: versionDecision.version,
|
|
152
|
+
contentHash: pkg.contentHash,
|
|
153
|
+
packageHash: pkg.packageHash,
|
|
154
|
+
storageKey: uploadResult.storageKey,
|
|
155
|
+
sizeBytes: pkg.sizeBytes,
|
|
156
|
+
title: metadata.title,
|
|
157
|
+
description: metadata.description,
|
|
158
|
+
tags: metadata.tags,
|
|
159
|
+
visibility: options.visibility || 'self',
|
|
160
|
+
});
|
|
161
|
+
spinner.succeed('发布成功');
|
|
162
|
+
|
|
163
|
+
// 清理临时文件
|
|
164
|
+
await this.packager.cleanup(pkg.tarballPath);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
fullName: result.plugin.fullName,
|
|
168
|
+
version: result.plugin.version,
|
|
169
|
+
storageKey: uploadResult.storageKey,
|
|
170
|
+
type: metadata.type,
|
|
171
|
+
action: result.action,
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
spinner.fail('发布失败');
|
|
175
|
+
// 清理临时文件
|
|
176
|
+
if (pkg) {
|
|
177
|
+
await this.packager.cleanup(pkg.tarballPath);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 显示验证结果
|
|
185
|
+
*/
|
|
186
|
+
private displayValidationResult(result: ValidationResult): void {
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(chalk.bold('📋 验证结果'));
|
|
189
|
+
console.log();
|
|
190
|
+
|
|
191
|
+
// 显示元信息
|
|
192
|
+
const { metadata } = result;
|
|
193
|
+
console.log(` 名称: ${metadata.name || chalk.gray('(未指定)')}`);
|
|
194
|
+
console.log(` 类型: ${metadata.type}`);
|
|
195
|
+
if (metadata.description) {
|
|
196
|
+
const desc =
|
|
197
|
+
metadata.description.length > 50
|
|
198
|
+
? metadata.description.slice(0, 50) + '...'
|
|
199
|
+
: metadata.description;
|
|
200
|
+
console.log(` 描述: ${desc}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 显示错误
|
|
204
|
+
if (result.errors.length > 0) {
|
|
205
|
+
console.log();
|
|
206
|
+
console.log(chalk.red.bold(`❌ 错误 (${result.errors.length}):`));
|
|
207
|
+
for (const error of result.errors) {
|
|
208
|
+
console.log(chalk.red(` [${error.code}] ${error.message}`));
|
|
209
|
+
if (error.suggestion) {
|
|
210
|
+
console.log(chalk.gray(` 💡 ${error.suggestion}`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 显示警告
|
|
216
|
+
if (result.warnings.length > 0) {
|
|
217
|
+
console.log();
|
|
218
|
+
console.log(chalk.yellow.bold(`⚠️ 警告 (${result.warnings.length}):`));
|
|
219
|
+
for (const warning of result.warnings) {
|
|
220
|
+
console.log(chalk.yellow(` [${warning.code}] ${warning.message}`));
|
|
221
|
+
if (warning.suggestion) {
|
|
222
|
+
console.log(chalk.gray(` 💡 ${warning.suggestion}`));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 获取当前登录用户的用户名
|
|
232
|
+
*/
|
|
233
|
+
private async getUsername(): Promise<string> {
|
|
234
|
+
const session = await api.getSession();
|
|
235
|
+
return session.user.username || session.user.name || 'unknown';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件上传服务
|
|
3
|
+
*
|
|
4
|
+
* 使用预签名 URL 直接上传到 COS
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import { api } from '../api';
|
|
9
|
+
import { UploadError } from '../errors';
|
|
10
|
+
import type { PluginMetadata, PackageResult, UploadUrlResponse } from '../types';
|
|
11
|
+
|
|
12
|
+
export class Uploader {
|
|
13
|
+
/**
|
|
14
|
+
* 上传插件包到 COS
|
|
15
|
+
*/
|
|
16
|
+
async upload(
|
|
17
|
+
metadata: PluginMetadata,
|
|
18
|
+
pkg: PackageResult,
|
|
19
|
+
version: string
|
|
20
|
+
): Promise<UploadUrlResponse> {
|
|
21
|
+
// 1. 获取上传签名 URL
|
|
22
|
+
const uploadInfo = await api.getUploadUrl({
|
|
23
|
+
name: metadata.name,
|
|
24
|
+
type: metadata.type,
|
|
25
|
+
version,
|
|
26
|
+
contentHash: pkg.contentHash,
|
|
27
|
+
sizeBytes: pkg.sizeBytes,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 2. 上传文件到 COS
|
|
31
|
+
const tarballBuffer = await fs.readFile(pkg.tarballPath);
|
|
32
|
+
|
|
33
|
+
const response = await fetch(uploadInfo.uploadUrl, {
|
|
34
|
+
method: 'PUT',
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/gzip',
|
|
37
|
+
'Content-Length': String(pkg.sizeBytes),
|
|
38
|
+
},
|
|
39
|
+
body: tarballBuffer,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const text = await response.text().catch(() => '');
|
|
44
|
+
throw new UploadError(
|
|
45
|
+
`上传失败: ${response.status} ${text}`,
|
|
46
|
+
response.status
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return uploadInfo;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本管理器
|
|
3
|
+
*
|
|
4
|
+
* 策略:
|
|
5
|
+
* - 新插件:从 1.0.0 开始
|
|
6
|
+
* - 内容变化:patch 版本自动递增(1.0.0 → 1.0.1)
|
|
7
|
+
* - 内容未变化:跳过发布
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { api } from '../api';
|
|
11
|
+
import type { VersionDecision } from '../types';
|
|
12
|
+
|
|
13
|
+
export class VersionManager {
|
|
14
|
+
/**
|
|
15
|
+
* 计算下一个版本号
|
|
16
|
+
*/
|
|
17
|
+
async getNextVersion(
|
|
18
|
+
pluginName: string,
|
|
19
|
+
contentHash: string
|
|
20
|
+
): Promise<VersionDecision> {
|
|
21
|
+
// 查询插件当前状态
|
|
22
|
+
const status = await api.getPluginStatus(pluginName);
|
|
23
|
+
|
|
24
|
+
// 新插件
|
|
25
|
+
if (!status.exists) {
|
|
26
|
+
return {
|
|
27
|
+
version: '1.0.0',
|
|
28
|
+
action: 'create',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 内容未变化
|
|
33
|
+
if (status.contentHash === contentHash) {
|
|
34
|
+
return {
|
|
35
|
+
version: status.version!,
|
|
36
|
+
action: 'skip',
|
|
37
|
+
existingHash: status.contentHash,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 内容变化,递增 patch 版本
|
|
42
|
+
const newVersion = this.incrementPatch(status.version || '1.0.0');
|
|
43
|
+
return {
|
|
44
|
+
version: newVersion,
|
|
45
|
+
action: 'update',
|
|
46
|
+
existingHash: status.contentHash,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 递增 patch 版本
|
|
52
|
+
* 1.0.0 → 1.0.1
|
|
53
|
+
* 1.2.3 → 1.2.4
|
|
54
|
+
*/
|
|
55
|
+
private incrementPatch(version: string): string {
|
|
56
|
+
const parts = version.split('.');
|
|
57
|
+
if (parts.length !== 3) {
|
|
58
|
+
return '1.0.1'; // fallback
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [major, minor, patch] = parts;
|
|
62
|
+
const newPatch = parseInt(patch, 10) + 1;
|
|
63
|
+
return `${major}.${minor}.${newPatch}`;
|
|
64
|
+
}
|
|
65
|
+
}
|