@zshuangmu/agenthub 0.1.6 → 0.1.7
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/package.json +1 -1
- package/src/api-server.js +29 -1
- package/src/cli.js +64 -46
- package/src/commands/info.js +12 -3
- package/src/commands/list.js +1 -14
- package/src/commands/rollback.js +12 -27
- package/src/commands/search.js +10 -3
- package/src/commands/stats.js +43 -28
- package/src/commands/update.js +7 -30
- package/src/commands/verify.js +94 -0
- package/src/commands/versions.js +25 -13
- package/src/index.js +2 -0
- package/src/lib/http.js +144 -0
- package/src/lib/install.js +5 -156
- package/src/lib/registry.js +32 -1
- package/src/lib/remote.js +11 -0
- package/src/lib/version-manager.js +77 -0
- package/src/server.js +3 -20
- package/src/web-server.js +8 -11
- package/src/lib/download-stats.js +0 -77
- package/src/lib/skill-md.js +0 -17
package/package.json
CHANGED
package/src/api-server.js
CHANGED
|
@@ -23,6 +23,30 @@ const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || "100", 10); // 100
|
|
|
23
23
|
|
|
24
24
|
// 简单的速率限制器
|
|
25
25
|
const rateLimiter = new Map();
|
|
26
|
+
let cleanupInterval = null;
|
|
27
|
+
|
|
28
|
+
function startRateLimiterCleanup() {
|
|
29
|
+
if (cleanupInterval) return;
|
|
30
|
+
cleanupInterval = setInterval(() => {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const windowStart = now - RATE_LIMIT_WINDOW;
|
|
33
|
+
for (const [ip, requests] of rateLimiter.entries()) {
|
|
34
|
+
const recentRequests = requests.filter(t => t > windowStart);
|
|
35
|
+
if (recentRequests.length === 0) {
|
|
36
|
+
rateLimiter.delete(ip);
|
|
37
|
+
} else if (recentRequests.length !== requests.length) {
|
|
38
|
+
rateLimiter.set(ip, recentRequests);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, RATE_LIMIT_WINDOW);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stopRateLimiterCleanup() {
|
|
45
|
+
if (cleanupInterval) {
|
|
46
|
+
clearInterval(cleanupInterval);
|
|
47
|
+
cleanupInterval = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
26
50
|
|
|
27
51
|
function checkRateLimit(ip) {
|
|
28
52
|
const now = Date.now();
|
|
@@ -203,6 +227,7 @@ export async function createApiServer({ registryDir, port = 3000 }) {
|
|
|
203
227
|
}
|
|
204
228
|
});
|
|
205
229
|
|
|
230
|
+
startRateLimiterCleanup();
|
|
206
231
|
await new Promise((resolve) => server.listen(port, "0.0.0.0", resolve));
|
|
207
232
|
const address = server.address();
|
|
208
233
|
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
@@ -211,6 +236,9 @@ export async function createApiServer({ registryDir, port = 3000 }) {
|
|
|
211
236
|
server,
|
|
212
237
|
port: actualPort,
|
|
213
238
|
baseUrl: `http://127.0.0.1:${actualPort}`,
|
|
214
|
-
close: () => new Promise((resolve, reject) =>
|
|
239
|
+
close: () => new Promise((resolve, reject) => {
|
|
240
|
+
stopRateLimiterCleanup();
|
|
241
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
242
|
+
}),
|
|
215
243
|
};
|
|
216
244
|
}
|
package/src/cli.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import {
|
|
10
10
|
infoCommand,
|
|
11
11
|
installCommand,
|
|
12
|
+
verifyCommand,
|
|
13
|
+
formatVerifyOutput,
|
|
12
14
|
packCommand,
|
|
13
15
|
publishCommand,
|
|
14
16
|
publishRemoteCommand,
|
|
@@ -26,7 +28,24 @@ import {
|
|
|
26
28
|
formatVersionsOutput,
|
|
27
29
|
} from "./index.js";
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
import { createRequire } from "node:module";
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const VERSION = require("../package.json").version;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 验证必需参数,如果缺失则打印错误并设置退出码
|
|
37
|
+
* @param {string} arg - 要验证的参数
|
|
38
|
+
* @param {string} message - 错误消息
|
|
39
|
+
* @returns {boolean} 参数是否存在
|
|
40
|
+
*/
|
|
41
|
+
function requireArg(arg, message) {
|
|
42
|
+
if (!arg) {
|
|
43
|
+
console.error(message);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
30
49
|
|
|
31
50
|
function printHelp() {
|
|
32
51
|
console.log(`
|
|
@@ -43,6 +62,7 @@ AgentHub v${VERSION} - AI Agent 打包与分发平台
|
|
|
43
62
|
search 搜索 Registry 中的 Agent
|
|
44
63
|
info 查看 Agent 详情
|
|
45
64
|
list 列出当前目录/指定目录的已安装 Agent
|
|
65
|
+
verify 校验已安装 Agent 是否完整可用
|
|
46
66
|
versions 查看 Agent 版本历史
|
|
47
67
|
update 更新已安装 Agent 到最新版
|
|
48
68
|
rollback 回滚已安装 Agent 到指定版本
|
|
@@ -147,45 +167,66 @@ agenthub list - 列出已安装 Agent
|
|
|
147
167
|
示例:
|
|
148
168
|
agenthub list
|
|
149
169
|
agenthub list --target-workspace ./my-workspace
|
|
170
|
+
`,
|
|
171
|
+
verify: `
|
|
172
|
+
agenthub verify - 校验已安装 Agent
|
|
173
|
+
|
|
174
|
+
用法:
|
|
175
|
+
agenthub verify [agent-slug] [--registry <dir> | --server <url>] [--target-workspace <dir>]
|
|
176
|
+
|
|
177
|
+
选项:
|
|
178
|
+
--registry <dir> 本地 Registry 目录(与 --server 二选一)
|
|
179
|
+
--server <url> 远程服务器地址(默认: https://agenthub.cyou)
|
|
180
|
+
--target-workspace <dir> 目标工作区目录(默认当前目录)
|
|
181
|
+
|
|
182
|
+
示例:
|
|
183
|
+
agenthub verify workspace --registry ./.registry --target-workspace ./my-workspace
|
|
184
|
+
agenthub verify workspace --server https://agenthub.cyou --target-workspace ./my-workspace
|
|
150
185
|
`,
|
|
151
186
|
versions: `
|
|
152
187
|
agenthub versions - 查看版本历史
|
|
153
188
|
|
|
154
189
|
用法:
|
|
155
|
-
agenthub versions <agent-slug> --registry <dir>
|
|
190
|
+
agenthub versions <agent-slug> [--registry <dir> | --server <url>]
|
|
156
191
|
|
|
157
192
|
选项:
|
|
158
|
-
--registry <dir>
|
|
193
|
+
--registry <dir> 本地 Registry 目录(与 --server 二选一)
|
|
194
|
+
--server <url> 远程服务器地址(默认: https://agenthub.cyou)
|
|
159
195
|
|
|
160
196
|
示例:
|
|
161
197
|
agenthub versions workspace --registry ./.registry
|
|
198
|
+
agenthub versions workspace --server https://agenthub.cyou
|
|
162
199
|
`,
|
|
163
200
|
update: `
|
|
164
201
|
agenthub update - 更新 Agent 到最新版
|
|
165
202
|
|
|
166
203
|
用法:
|
|
167
|
-
agenthub update <agent-slug> --registry <dir> --target-workspace <dir>
|
|
204
|
+
agenthub update <agent-slug> [--registry <dir> | --server <url>] --target-workspace <dir>
|
|
168
205
|
|
|
169
206
|
选项:
|
|
170
|
-
--registry <dir> Registry
|
|
207
|
+
--registry <dir> 本地 Registry 目录(与 --server 二选一)
|
|
208
|
+
--server <url> 远程服务器地址(默认: https://agenthub.cyou)
|
|
171
209
|
--target-workspace <dir> 目标工作区目录 (必需)
|
|
172
210
|
|
|
173
211
|
示例:
|
|
174
212
|
agenthub update workspace --registry ./.registry --target-workspace ./my-workspace
|
|
213
|
+
agenthub update workspace --server https://agenthub.cyou --target-workspace ./my-workspace
|
|
175
214
|
`,
|
|
176
215
|
rollback: `
|
|
177
216
|
agenthub rollback - 回滚 Agent 到指定版本
|
|
178
217
|
|
|
179
218
|
用法:
|
|
180
|
-
agenthub rollback <agent-slug> --to <version> --registry <dir> --target-workspace <dir>
|
|
219
|
+
agenthub rollback <agent-slug> --to <version> [--registry <dir> | --server <url>] --target-workspace <dir>
|
|
181
220
|
|
|
182
221
|
选项:
|
|
183
222
|
--to <version> 目标版本 (必需)
|
|
184
|
-
--registry <dir> Registry
|
|
223
|
+
--registry <dir> 本地 Registry 目录(与 --server 二选一)
|
|
224
|
+
--server <url> 远程服务器地址(默认: https://agenthub.cyou)
|
|
185
225
|
--target-workspace <dir> 目标工作区目录 (必需)
|
|
186
226
|
|
|
187
227
|
示例:
|
|
188
228
|
agenthub rollback workspace --to 1.0.0 --registry ./.registry --target-workspace ./my-workspace
|
|
229
|
+
agenthub rollback workspace --to 1.0.0 --server https://agenthub.cyou --target-workspace ./my-workspace
|
|
189
230
|
`,
|
|
190
231
|
stats: `
|
|
191
232
|
agenthub stats - 查看 Agent 统计信息
|
|
@@ -283,22 +324,14 @@ async function main() {
|
|
|
283
324
|
}
|
|
284
325
|
|
|
285
326
|
case "publish": {
|
|
286
|
-
if (!rest[0])
|
|
287
|
-
console.error("错误: 需要指定 bundle 目录");
|
|
288
|
-
process.exitCode = 1;
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
327
|
+
if (!requireArg(rest[0], "错误: 需要指定 bundle 目录")) return;
|
|
291
328
|
const result = await publishCommand(rest[0], options);
|
|
292
329
|
console.log(`✓ 已发布 ${result.slug}@${result.version}`);
|
|
293
330
|
return;
|
|
294
331
|
}
|
|
295
332
|
|
|
296
333
|
case "publish-remote": {
|
|
297
|
-
if (!rest[0])
|
|
298
|
-
console.error("错误: 需要指定 bundle 目录");
|
|
299
|
-
process.exitCode = 1;
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
334
|
+
if (!requireArg(rest[0], "错误: 需要指定 bundle 目录")) return;
|
|
302
335
|
const result = await publishRemoteCommand(rest[0], options);
|
|
303
336
|
console.log(`✓ 已发布到远程 ${result.slug}@${result.version}`);
|
|
304
337
|
return;
|
|
@@ -318,11 +351,7 @@ async function main() {
|
|
|
318
351
|
}
|
|
319
352
|
|
|
320
353
|
case "info": {
|
|
321
|
-
if (!rest[0])
|
|
322
|
-
console.error("错误: 需要指定 agent slug");
|
|
323
|
-
process.exitCode = 1;
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
354
|
+
if (!requireArg(rest[0], "错误: 需要指定 agent slug")) return;
|
|
326
355
|
const result = await infoCommand(rest[0], options);
|
|
327
356
|
console.log(`\n📦 ${result.name} (${result.slug}@${result.version})`);
|
|
328
357
|
console.log(` ${result.description}`);
|
|
@@ -336,11 +365,7 @@ async function main() {
|
|
|
336
365
|
}
|
|
337
366
|
|
|
338
367
|
case "install": {
|
|
339
|
-
if (!rest[0])
|
|
340
|
-
console.error("错误: 需要指定 agent slug");
|
|
341
|
-
process.exitCode = 1;
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
368
|
+
if (!requireArg(rest[0], "错误: 需要指定 agent slug")) return;
|
|
344
369
|
console.log(`\n📥 正在安装 ${rest[0]}...\n`);
|
|
345
370
|
const installResult = await installCommand(rest[0], options);
|
|
346
371
|
console.log(`✓ 已安装 ${installResult.manifest.slug}@${installResult.manifest.version}`);
|
|
@@ -354,45 +379,38 @@ async function main() {
|
|
|
354
379
|
return;
|
|
355
380
|
}
|
|
356
381
|
|
|
357
|
-
case "
|
|
358
|
-
|
|
359
|
-
|
|
382
|
+
case "verify": {
|
|
383
|
+
const result = await verifyCommand(rest[0], options);
|
|
384
|
+
console.log(formatVerifyOutput(result));
|
|
385
|
+
if (!result.verified) {
|
|
360
386
|
process.exitCode = 1;
|
|
361
|
-
return;
|
|
362
387
|
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case "versions": {
|
|
392
|
+
if (!requireArg(rest[0], "错误: 需要指定 agent slug")) return;
|
|
363
393
|
const versions = await versionsCommand(rest[0], options);
|
|
364
394
|
console.log(formatVersionsOutput(rest[0], versions));
|
|
365
395
|
return;
|
|
366
396
|
}
|
|
367
397
|
|
|
368
398
|
case "update": {
|
|
369
|
-
if (!rest[0])
|
|
370
|
-
console.error("错误: 需要指定 agent slug");
|
|
371
|
-
process.exitCode = 1;
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
399
|
+
if (!requireArg(rest[0], "错误: 需要指定 agent slug")) return;
|
|
374
400
|
const updateResult = await updateCommand(rest[0], options);
|
|
375
401
|
console.log(updateResult.message);
|
|
376
402
|
return;
|
|
377
403
|
}
|
|
378
404
|
|
|
379
405
|
case "rollback": {
|
|
380
|
-
if (!rest[0])
|
|
381
|
-
console.error("错误: 需要指定 agent slug");
|
|
382
|
-
process.exitCode = 1;
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
406
|
+
if (!requireArg(rest[0], "错误: 需要指定 agent slug")) return;
|
|
385
407
|
const rollbackResult = await rollbackCommand(rest[0], options);
|
|
386
408
|
console.log(rollbackResult.message);
|
|
387
409
|
return;
|
|
388
410
|
}
|
|
389
411
|
|
|
390
412
|
case "stats": {
|
|
391
|
-
if (!rest[0])
|
|
392
|
-
console.error("错误: 需要指定 agent slug");
|
|
393
|
-
process.exitCode = 1;
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
413
|
+
if (!requireArg(rest[0], "错误: 需要指定 agent slug")) return;
|
|
396
414
|
const stats = await statsCommand(rest[0], options);
|
|
397
415
|
console.log(formatStatsOutput(stats));
|
|
398
416
|
return;
|
package/src/commands/info.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { readAgentInfo } from "../lib/registry.js";
|
|
2
|
+
import { readAgentInfo, parseSpec } from "../lib/registry.js";
|
|
3
|
+
import { fetchRemoteJson } from "../lib/remote.js";
|
|
3
4
|
|
|
4
|
-
export async function infoCommand(agentSpec, options) {
|
|
5
|
-
|
|
5
|
+
export async function infoCommand(agentSpec, options = {}) {
|
|
6
|
+
if (options.registry) {
|
|
7
|
+
return readAgentInfo(path.resolve(options.registry), agentSpec);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { slug, version } = parseSpec(agentSpec);
|
|
11
|
+
const params = new URLSearchParams();
|
|
12
|
+
if (version) params.set("version", version);
|
|
13
|
+
const qs = params.toString();
|
|
14
|
+
return fetchRemoteJson(`/api/agents/${slug}${qs ? `?${qs}` : ""}`, options);
|
|
6
15
|
}
|
package/src/commands/list.js
CHANGED
|
@@ -4,20 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
async function pathExists(targetPath) {
|
|
10
|
-
try {
|
|
11
|
-
await stat(targetPath);
|
|
12
|
-
return true;
|
|
13
|
-
} catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function readJson(filePath) {
|
|
19
|
-
return JSON.parse(await readFile(filePath, "utf8"));
|
|
20
|
-
}
|
|
7
|
+
import { pathExists, readJson } from "../lib/fs-utils.js";
|
|
21
8
|
|
|
22
9
|
export async function listCommand(options = {}) {
|
|
23
10
|
// 检查多个可能的安装位置
|
package/src/commands/rollback.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { getCurrentVersion, updateInstallRecord, buildInstallOptions } from "../lib/version-manager.js";
|
|
8
8
|
import { installBundle } from "../lib/install.js";
|
|
9
|
+
import { parseSpec } from "../lib/registry.js";
|
|
9
10
|
import { versionsCommand } from "./versions.js";
|
|
10
11
|
|
|
11
|
-
export async function rollbackCommand(agentSpec, options) {
|
|
12
|
-
const registryDir = path.resolve(options.registry);
|
|
12
|
+
export async function rollbackCommand(agentSpec, options = {}) {
|
|
13
13
|
const targetWorkspace = options.targetWorkspace ? path.resolve(options.targetWorkspace) : null;
|
|
14
14
|
const targetVersion = options.to;
|
|
15
15
|
|
|
@@ -17,42 +17,27 @@ export async function rollbackCommand(agentSpec, options) {
|
|
|
17
17
|
throw new Error("请指定回滚版本: --to <version>");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const { slug } = parseSpec(agentSpec);
|
|
21
21
|
|
|
22
22
|
// 验证目标版本存在
|
|
23
|
-
const versions = await versionsCommand(slug,
|
|
23
|
+
const versions = await versionsCommand(slug, options);
|
|
24
24
|
const versionExists = versions.some((v) => v.version === targetVersion);
|
|
25
25
|
if (!versionExists) {
|
|
26
26
|
throw new Error(`版本 ${targetVersion} 不存在`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// 获取当前版本
|
|
30
|
-
|
|
31
|
-
if (targetWorkspace) {
|
|
32
|
-
const installRecordPath = path.join(targetWorkspace, ".agenthub", "install.json");
|
|
33
|
-
if (await pathExists(installRecordPath)) {
|
|
34
|
-
const record = await readJson(installRecordPath);
|
|
35
|
-
currentVersion = record.version;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
30
|
+
const currentVersion = await getCurrentVersion(targetWorkspace);
|
|
38
31
|
|
|
39
32
|
// 执行回滚
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
agentSpec: `${slug}:${targetVersion}`,
|
|
43
|
-
targetWorkspace,
|
|
44
|
-
});
|
|
33
|
+
const installOptions = buildInstallOptions(slug, targetVersion, targetWorkspace, options);
|
|
34
|
+
const result = await installBundle(installOptions);
|
|
45
35
|
|
|
46
36
|
// 记录回滚
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
version: targetVersion,
|
|
52
|
-
rolledBackAt: new Date().toISOString(),
|
|
53
|
-
rolledBackFrom: currentVersion,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
37
|
+
await updateInstallRecord(targetWorkspace, slug, targetVersion, {
|
|
38
|
+
rolledBackAt: new Date().toISOString(),
|
|
39
|
+
rolledBackFrom: currentVersion,
|
|
40
|
+
});
|
|
56
41
|
|
|
57
42
|
return {
|
|
58
43
|
rolledBack: true,
|
package/src/commands/search.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { searchRegistry } from "../lib/registry.js";
|
|
3
|
+
import { fetchRemoteJson } from "../lib/remote.js";
|
|
3
4
|
|
|
4
|
-
export async function searchCommand(query, options) {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export async function searchCommand(query, options = {}) {
|
|
6
|
+
if (options.registry) {
|
|
7
|
+
return searchRegistry(path.resolve(options.registry), query);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const params = new URLSearchParams();
|
|
11
|
+
if (query) params.set("q", query);
|
|
12
|
+
const result = await fetchRemoteJson(`/api/agents?${params.toString()}`, options);
|
|
13
|
+
return result.agents || [];
|
|
7
14
|
}
|
package/src/commands/stats.js
CHANGED
|
@@ -5,11 +5,49 @@
|
|
|
5
5
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { pathExists, readJson } from "../lib/fs-utils.js";
|
|
8
|
+
import { fetchRemoteJson } from "../lib/remote.js";
|
|
9
|
+
import { parseSpec, getLatestVersion } from "../lib/registry.js";
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* 构建统计返回对象的辅助函数
|
|
13
|
+
*/
|
|
14
|
+
function buildStatsResult(slug, manifest, agentEntries) {
|
|
15
|
+
const latestEntry = getLatestVersion(agentEntries);
|
|
16
|
+
return {
|
|
17
|
+
slug,
|
|
18
|
+
name: manifest.name,
|
|
19
|
+
latestVersion: latestEntry.version,
|
|
20
|
+
totalVersions: agentEntries.length,
|
|
21
|
+
description: manifest.description,
|
|
22
|
+
author: manifest.author,
|
|
23
|
+
runtime: manifest.runtime,
|
|
24
|
+
includes: manifest.includes,
|
|
25
|
+
requirements: manifest.requirements,
|
|
26
|
+
metadata: manifest.metadata,
|
|
27
|
+
downloads: manifest.downloads || 0,
|
|
28
|
+
stars: manifest.stats?.stars || 0,
|
|
29
|
+
rating: manifest.stats?.rating || null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function statsCommand(agentSpec, options = {}) {
|
|
34
|
+
const { slug } = parseSpec(agentSpec);
|
|
35
|
+
|
|
36
|
+
if (!options.registry) {
|
|
37
|
+
const [manifest, agentsResult] = await Promise.all([
|
|
38
|
+
fetchRemoteJson(`/api/agents/${slug}`, options),
|
|
39
|
+
fetchRemoteJson(`/api/agents?q=${encodeURIComponent(slug)}`, options),
|
|
40
|
+
]);
|
|
12
41
|
|
|
42
|
+
const agentEntries = (agentsResult.agents || []).filter((entry) => entry.slug === slug);
|
|
43
|
+
if (agentEntries.length === 0) {
|
|
44
|
+
throw new Error(`Agent not found: ${slug}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return buildStatsResult(slug, manifest, agentEntries);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const registryDir = path.resolve(options.registry);
|
|
13
51
|
const indexPath = path.join(registryDir, "index.json");
|
|
14
52
|
if (!(await pathExists(indexPath))) {
|
|
15
53
|
throw new Error(`Agent not found: ${slug}`);
|
|
@@ -22,12 +60,7 @@ export async function statsCommand(agentSpec, options) {
|
|
|
22
60
|
throw new Error(`Agent not found: ${slug}`);
|
|
23
61
|
}
|
|
24
62
|
|
|
25
|
-
|
|
26
|
-
const latestEntry = agentEntries.sort((a, b) =>
|
|
27
|
-
b.version.localeCompare(a.version, undefined, { numeric: true })
|
|
28
|
-
)[0];
|
|
29
|
-
|
|
30
|
-
// 读取完整 MANIFEST
|
|
63
|
+
const latestEntry = getLatestVersion(agentEntries);
|
|
31
64
|
const manifestPath = path.join(
|
|
32
65
|
registryDir,
|
|
33
66
|
"agents",
|
|
@@ -37,25 +70,7 @@ export async function statsCommand(agentSpec, options) {
|
|
|
37
70
|
);
|
|
38
71
|
const manifest = await readJson(manifestPath);
|
|
39
72
|
|
|
40
|
-
|
|
41
|
-
const stats = {
|
|
42
|
-
slug,
|
|
43
|
-
name: manifest.name,
|
|
44
|
-
latestVersion: latestEntry.version,
|
|
45
|
-
totalVersions: agentEntries.length,
|
|
46
|
-
description: manifest.description,
|
|
47
|
-
author: manifest.author,
|
|
48
|
-
runtime: manifest.runtime,
|
|
49
|
-
includes: manifest.includes,
|
|
50
|
-
requirements: manifest.requirements,
|
|
51
|
-
metadata: manifest.metadata,
|
|
52
|
-
// 以下数据在实际系统中会从数据库获取
|
|
53
|
-
downloads: manifest.stats?.installs || 0,
|
|
54
|
-
stars: manifest.stats?.stars || 0,
|
|
55
|
-
rating: manifest.stats?.rating || null,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
return stats;
|
|
73
|
+
return buildStatsResult(slug, manifest, agentEntries);
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
export function formatStatsOutput(stats) {
|
package/src/commands/update.js
CHANGED
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { getCurrentVersion, performVersionChange } from "../lib/version-manager.js";
|
|
8
|
+
import { parseSpec } from "../lib/registry.js";
|
|
9
9
|
import { versionsCommand } from "./versions.js";
|
|
10
10
|
|
|
11
|
-
export async function updateCommand(agentSpec, options) {
|
|
12
|
-
const registryDir = path.resolve(options.registry);
|
|
11
|
+
export async function updateCommand(agentSpec, options = {}) {
|
|
13
12
|
const targetWorkspace = options.targetWorkspace ? path.resolve(options.targetWorkspace) : null;
|
|
14
|
-
const
|
|
13
|
+
const { slug } = parseSpec(agentSpec);
|
|
15
14
|
|
|
16
15
|
// 获取可用版本
|
|
17
|
-
const versions = await versionsCommand(slug,
|
|
16
|
+
const versions = await versionsCommand(slug, options);
|
|
18
17
|
if (versions.length === 0) {
|
|
19
18
|
throw new Error(`Agent not found: ${slug}`);
|
|
20
19
|
}
|
|
@@ -22,14 +21,7 @@ export async function updateCommand(agentSpec, options) {
|
|
|
22
21
|
const latestVersion = versions[0].version;
|
|
23
22
|
|
|
24
23
|
// 获取当前安装版本
|
|
25
|
-
|
|
26
|
-
if (targetWorkspace) {
|
|
27
|
-
const installRecordPath = path.join(targetWorkspace, ".agenthub", "install.json");
|
|
28
|
-
if (await pathExists(installRecordPath)) {
|
|
29
|
-
const record = await readJson(installRecordPath);
|
|
30
|
-
currentVersion = record.version;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
24
|
+
const currentVersion = await getCurrentVersion(targetWorkspace);
|
|
33
25
|
|
|
34
26
|
if (currentVersion === latestVersion) {
|
|
35
27
|
return {
|
|
@@ -41,22 +33,7 @@ export async function updateCommand(agentSpec, options) {
|
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
// 执行更新
|
|
44
|
-
const result = await
|
|
45
|
-
registryDir,
|
|
46
|
-
agentSpec: `${slug}:${latestVersion}`,
|
|
47
|
-
targetWorkspace,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// 记录更新
|
|
51
|
-
if (targetWorkspace) {
|
|
52
|
-
const installRecordPath = path.join(targetWorkspace, ".agenthub", "install.json");
|
|
53
|
-
await writeJson(installRecordPath, {
|
|
54
|
-
slug,
|
|
55
|
-
version: latestVersion,
|
|
56
|
-
updatedAt: new Date().toISOString(),
|
|
57
|
-
previousVersion: currentVersion,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
36
|
+
const result = await performVersionChange(slug, latestVersion, currentVersion, targetWorkspace, options);
|
|
60
37
|
|
|
61
38
|
return {
|
|
62
39
|
updated: true,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathExists, readJson } from "../lib/fs-utils.js";
|
|
3
|
+
import { parseSpec } from "../lib/registry.js";
|
|
4
|
+
import { infoCommand } from "./info.js";
|
|
5
|
+
|
|
6
|
+
export async function verifyCommand(agentSpec, options = {}) {
|
|
7
|
+
const targetWorkspace = path.resolve(options.targetWorkspace || process.cwd());
|
|
8
|
+
const installRecordPath = path.join(targetWorkspace, ".agenthub", "install.json");
|
|
9
|
+
const appliedConfigPath = path.join(targetWorkspace, ".agenthub", "OPENCLAW.applied.json");
|
|
10
|
+
|
|
11
|
+
const checks = [];
|
|
12
|
+
|
|
13
|
+
if (!(await pathExists(installRecordPath))) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
verified: false,
|
|
17
|
+
targetWorkspace,
|
|
18
|
+
reason: "install record missing",
|
|
19
|
+
checks: [{ name: "install_record", ok: false, detail: installRecordPath }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const installRecord = await readJson(installRecordPath);
|
|
24
|
+
const expectedSlug = agentSpec ? parseSpec(agentSpec).slug : installRecord.slug;
|
|
25
|
+
if (expectedSlug && installRecord.slug !== expectedSlug) {
|
|
26
|
+
checks.push({
|
|
27
|
+
name: "slug_match",
|
|
28
|
+
ok: false,
|
|
29
|
+
detail: `installed=${installRecord.slug}, expected=${expectedSlug}`,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
verified: false,
|
|
34
|
+
targetWorkspace,
|
|
35
|
+
installRecord,
|
|
36
|
+
checks,
|
|
37
|
+
reason: "installed slug mismatch",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
checks.push({ name: "install_record", ok: true, detail: installRecordPath });
|
|
42
|
+
|
|
43
|
+
const configExists = await pathExists(appliedConfigPath);
|
|
44
|
+
checks.push({ name: "applied_config", ok: configExists, detail: appliedConfigPath });
|
|
45
|
+
|
|
46
|
+
let manifest = null;
|
|
47
|
+
try {
|
|
48
|
+
manifest = await infoCommand(`${installRecord.slug}:${installRecord.version}`, options);
|
|
49
|
+
checks.push({ name: "manifest_lookup", ok: true, detail: `${installRecord.slug}@${installRecord.version}` });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
checks.push({
|
|
52
|
+
name: "manifest_lookup",
|
|
53
|
+
ok: false,
|
|
54
|
+
detail: error.message,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const requiredFiles = ["AGENTS.md", "SOUL.md", "USER.md"];
|
|
59
|
+
for (const fileName of requiredFiles) {
|
|
60
|
+
const filePath = path.join(targetWorkspace, fileName);
|
|
61
|
+
checks.push({
|
|
62
|
+
name: `workspace_file:${fileName}`,
|
|
63
|
+
ok: await pathExists(filePath),
|
|
64
|
+
detail: filePath,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const verified = checks.every((check) => check.ok);
|
|
69
|
+
return {
|
|
70
|
+
ok: verified,
|
|
71
|
+
verified,
|
|
72
|
+
targetWorkspace,
|
|
73
|
+
installRecord,
|
|
74
|
+
manifest,
|
|
75
|
+
checks,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatVerifyOutput(result) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push(`\n${result.verified ? "✅" : "❌"} Verify ${result.verified ? "passed" : "failed"}\n`);
|
|
82
|
+
if (result.installRecord) {
|
|
83
|
+
lines.push(`Agent: ${result.installRecord.slug}@${result.installRecord.version}`);
|
|
84
|
+
}
|
|
85
|
+
lines.push(`Workspace: ${result.targetWorkspace}`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
for (const check of result.checks || []) {
|
|
88
|
+
lines.push(`- ${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`);
|
|
89
|
+
}
|
|
90
|
+
if (result.reason) {
|
|
91
|
+
lines.push(`\nReason: ${result.reason}`);
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|