@zshuangmu/agenthub 0.1.0

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/src/cli.js ADDED
@@ -0,0 +1,343 @@
1
+ /**
2
+ * AgentHub CLI
3
+ * AI Agent 打包与分发平台
4
+ *
5
+ * 根据 PRD v1.1 规范实现
6
+ */
7
+
8
+ import {
9
+ infoCommand,
10
+ installCommand,
11
+ packCommand,
12
+ publishCommand,
13
+ publishRemoteCommand,
14
+ serveCommand,
15
+ searchCommand,
16
+ apiCommand,
17
+ webCommand,
18
+ } from "./index.js";
19
+
20
+ const VERSION = "0.1.0";
21
+
22
+ function printHelp() {
23
+ console.log(`
24
+ AgentHub v${VERSION} - AI Agent 打包与分发平台
25
+
26
+ 用法:
27
+ agenthub <command> [options]
28
+
29
+ 命令:
30
+ pack 打包 OpenClaw 工作区为 Agent Bundle
31
+ publish 发布 Bundle 到本地 Registry
32
+ publish-remote 发布 Bundle 到远程服务器
33
+ install 安装 Agent 到目标工作区
34
+ search 搜索 Registry 中的 Agent
35
+ info 查看 Agent 详情
36
+ serve 启动 Web + API 服务
37
+
38
+ 选项:
39
+ --help, -h 显示帮助信息
40
+ --version, -v 显示版本号
41
+
42
+ 详细帮助:
43
+ agenthub <command> --help
44
+
45
+ 示例:
46
+ # 打包 Agent
47
+ agenthub pack --workspace ./my-workspace --config openclaw.json --output ./bundles
48
+
49
+ # 发布 Agent
50
+ agenthub publish ./bundles/my-agent.agent --registry ./.registry
51
+
52
+ # 安装 Agent
53
+ agenthub install my-agent --registry ./.registry --target-workspace ./workspace
54
+
55
+ # 搜索 Agent
56
+ agenthub search "code review" --registry ./.registry
57
+
58
+ # 启动完整服务(前端+后端)
59
+ agenthub serve --registry ./.registry --port 3000
60
+
61
+ `);
62
+ }
63
+
64
+ function printCommandHelp(command) {
65
+ const helps = {
66
+ pack: `
67
+ agenthub pack - 打包 Agent
68
+
69
+ 用法:
70
+ agenthub pack --workspace <dir> --config <file> --output <dir>
71
+
72
+ 选项:
73
+ --workspace <dir> OpenClaw 工作区目录 (必需)
74
+ --config <file> openclaw.json 配置文件路径 (必需)
75
+ --output <dir> 输出目录 (默认: ./bundles)
76
+ --tags <tags> 标签,逗号分隔
77
+ --category <cat> 分类
78
+
79
+ 示例:
80
+ agenthub pack --workspace ./my-agent --config ./openclaw.json
81
+ `,
82
+ publish: `
83
+ agenthub publish - 发布 Agent
84
+
85
+ 用法:
86
+ agenthub publish <bundle-dir> --registry <dir>
87
+
88
+ 选项:
89
+ --registry <dir> Registry 目录 (必需)
90
+
91
+ 示例:
92
+ agenthub publish ./bundles/my-agent.agent --registry ./.registry
93
+ `,
94
+ install: `
95
+ agenthub install - 安装 Agent
96
+
97
+ 用法:
98
+ agenthub install <agent-slug>[:version] --registry <dir> --target-workspace <dir>
99
+
100
+ 选项:
101
+ --registry <dir> Registry 目录 (必需)
102
+ --target-workspace <dir> 目标工作区目录 (必需)
103
+ --force 强制覆盖
104
+
105
+ 示例:
106
+ agenthub install code-review-assistant --registry ./.registry --target-workspace ./workspace
107
+ agenthub install my-agent:1.0.0 --registry ./.registry --target-workspace ./workspace
108
+ `,
109
+ search: `
110
+ agenthub search - 搜索 Agent
111
+
112
+ 用法:
113
+ agenthub search <query> --registry <dir>
114
+
115
+ 选项:
116
+ --registry <dir> Registry 目录 (必需)
117
+
118
+ 示例:
119
+ agenthub search "code review" --registry ./.registry
120
+ agenthub search "" --registry ./.registry # 列出所有
121
+ `,
122
+ serve: `
123
+ agenthub serve - 启动完整服务(前端+后端)
124
+
125
+ 用法:
126
+ agenthub serve --registry <dir> --port <port>
127
+
128
+ 选项:
129
+ --registry <dir> Registry 目录 (必需)
130
+ --port <port> 前端端口号 (默认: 3000)
131
+ --api-port <port> 后端 API 端口号 (默认: 3001)
132
+
133
+ 示例:
134
+ agenthub serve --registry ./.registry --port 3000
135
+ `,
136
+ };
137
+
138
+ console.log(helps[command] || `未知命令: ${command}`);
139
+ }
140
+
141
+ function parseArgs(argv) {
142
+ const positionals = [];
143
+ const options = {};
144
+ for (let index = 0; index < argv.length; index += 1) {
145
+ const token = argv[index];
146
+ if (token === "--help" || token === "-h") {
147
+ options.help = true;
148
+ } else if (token === "--version" || token === "-v") {
149
+ options.version = true;
150
+ } else if (token.startsWith("--")) {
151
+ const rawKey = token.slice(2);
152
+ const camelKey = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
153
+ const value = argv[index + 1];
154
+ if (value && !value.startsWith("-")) {
155
+ options[rawKey] = value;
156
+ options[camelKey] = value;
157
+ index += 1;
158
+ } else {
159
+ options[rawKey] = true;
160
+ options[camelKey] = true;
161
+ }
162
+ } else {
163
+ positionals.push(token);
164
+ }
165
+ }
166
+ return { positionals, options };
167
+ }
168
+
169
+ async function main() {
170
+ const { positionals, options } = parseArgs(process.argv.slice(2));
171
+ const [command, ...rest] = positionals;
172
+
173
+ // 全局选项
174
+ if (options.version) {
175
+ console.log(`AgentHub v${VERSION}`);
176
+ return;
177
+ }
178
+
179
+ if (!command) {
180
+ printHelp();
181
+ return;
182
+ }
183
+
184
+ // 命令帮助
185
+ if (options.help) {
186
+ printCommandHelp(command);
187
+ return;
188
+ }
189
+
190
+ // 命令路由
191
+ try {
192
+ switch (command) {
193
+ case "pack": {
194
+ if (!options.workspace || !options.config) {
195
+ console.error("错误: --workspace 和 --config 是必需的");
196
+ console.log("\n运行 'agenthub pack --help' 查看帮助");
197
+ process.exitCode = 1;
198
+ return;
199
+ }
200
+ const result = await packCommand(options);
201
+ console.log(`✓ 打包完成: ${result.bundleDir}`);
202
+ return;
203
+ }
204
+
205
+ case "publish": {
206
+ if (!rest[0]) {
207
+ console.error("错误: 需要指定 bundle 目录");
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+ const result = await publishCommand(rest[0], options);
212
+ console.log(`✓ 已发布 ${result.slug}@${result.version}`);
213
+ return;
214
+ }
215
+
216
+ case "publish-remote": {
217
+ if (!rest[0]) {
218
+ console.error("错误: 需要指定 bundle 目录");
219
+ process.exitCode = 1;
220
+ return;
221
+ }
222
+ const result = await publishRemoteCommand(rest[0], options);
223
+ console.log(`✓ 已发布到远程 ${result.slug}@${result.version}`);
224
+ return;
225
+ }
226
+
227
+ case "search": {
228
+ const results = await searchCommand(rest[0] || "", options);
229
+ if (results.length === 0) {
230
+ console.log("未找到匹配的 Agent");
231
+ } else {
232
+ console.log(`\n找到 ${results.length} 个 Agent:\n`);
233
+ for (const entry of results) {
234
+ console.log(` ${entry.slug}@${entry.version} - ${entry.description || ""}`);
235
+ }
236
+ }
237
+ return;
238
+ }
239
+
240
+ case "info": {
241
+ if (!rest[0]) {
242
+ console.error("错误: 需要指定 agent slug");
243
+ process.exitCode = 1;
244
+ return;
245
+ }
246
+ const result = await infoCommand(rest[0], options);
247
+ console.log(`\n📦 ${result.name} (${result.slug}@${result.version})`);
248
+ console.log(` ${result.description}`);
249
+ console.log(` Runtime: ${result.runtime?.type || "openclaw"} ${result.runtime?.version || ""}`);
250
+ const mem = result.includes?.memory || {};
251
+ if (mem.count > 0) {
252
+ console.log(` Memory: ${mem.count} 条 (public: ${mem.public}, portable: ${mem.portable})`);
253
+ }
254
+ console.log(`\n 安装命令: agenthub install ${result.slug}`);
255
+ return;
256
+ }
257
+
258
+ case "install": {
259
+ if (!rest[0]) {
260
+ console.error("错误: 需要指定 agent slug");
261
+ process.exitCode = 1;
262
+ return;
263
+ }
264
+ console.log(`\n📥 正在安装 ${rest[0]}...\n`);
265
+ const result = await installCommand(rest[0], options);
266
+ console.log(`✓ 已安装 ${result.manifest.slug}@${result.manifest.version}`);
267
+ console.log(` 位置: ${options.targetWorkspace || "当前目录"}`);
268
+ return;
269
+ }
270
+
271
+ case "serve": {
272
+ const result = await serveCommand(options);
273
+ console.log(`Server listening at ${result.baseUrl}`);
274
+ console.log(`\n🌐 AgentHub 服务已启动`);
275
+ console.log(` 地址: ${result.baseUrl}`);
276
+ console.log(` API: ${result.baseUrl}/api/agents`);
277
+ console.log(`\n按 Ctrl+C 停止服务\n`);
278
+
279
+ const shutdown = async () => {
280
+ process.off("SIGINT", shutdown);
281
+ process.off("SIGTERM", shutdown);
282
+ await result.close();
283
+ process.exit(0);
284
+ };
285
+ process.on("SIGINT", shutdown);
286
+ process.on("SIGTERM", shutdown);
287
+ return;
288
+ }
289
+
290
+ case "api": {
291
+ const port = options.port || "3001";
292
+ const result = await apiCommand({ ...options, port });
293
+ console.log(`Server listening at ${result.baseUrl}`);
294
+ console.log(`\n🔧 API 服务已启动`);
295
+ console.log(` 地址: ${result.baseUrl}`);
296
+ console.log(` 端点: ${result.baseUrl}/api/agents`);
297
+ console.log(` 统计: ${result.baseUrl}/api/stats`);
298
+ console.log(`\n按 Ctrl+C 停止服务\n`);
299
+
300
+ const shutdown = async () => {
301
+ process.off("SIGINT", shutdown);
302
+ process.off("SIGTERM", shutdown);
303
+ await result.close();
304
+ process.exit(0);
305
+ };
306
+ process.on("SIGINT", shutdown);
307
+ process.on("SIGTERM", shutdown);
308
+ return;
309
+ }
310
+
311
+ case "web": {
312
+ const port = options.port || "3000";
313
+ const apiBase = options.apiBase || "http://127.0.0.1:3001";
314
+ const result = await webCommand({ port, apiBase });
315
+ console.log(`Server listening at ${result.baseUrl}`);
316
+ console.log(`\n🌐 Web 服务已启动`);
317
+ console.log(` 地址: ${result.baseUrl}`);
318
+ console.log(` API: ${apiBase}`);
319
+ console.log(`\n按 Ctrl+C 停止服务\n`);
320
+
321
+ const shutdown = async () => {
322
+ process.off("SIGINT", shutdown);
323
+ process.off("SIGTERM", shutdown);
324
+ await result.close();
325
+ process.exit(0);
326
+ };
327
+ process.on("SIGINT", shutdown);
328
+ process.on("SIGTERM", shutdown);
329
+ return;
330
+ }
331
+
332
+ default:
333
+ console.error(`未知命令: ${command}`);
334
+ console.log("\n运行 'agenthub --help' 查看可用命令");
335
+ process.exitCode = 1;
336
+ }
337
+ } catch (error) {
338
+ console.error(`\n❌ 错误: ${error.message}`);
339
+ process.exitCode = 1;
340
+ }
341
+ }
342
+
343
+ main();
@@ -0,0 +1,8 @@
1
+ import { createApiServer } from "../api-server.js";
2
+
3
+ export async function apiCommand(options) {
4
+ const registry = options.registry || "./.registry";
5
+ const port = parseInt(options.port || "3001", 10);
6
+
7
+ return createApiServer({ registryDir: registry, port });
8
+ }
@@ -0,0 +1,6 @@
1
+ import path from "node:path";
2
+ import { readAgentInfo } from "../lib/registry.js";
3
+
4
+ export async function infoCommand(agentSpec, options) {
5
+ return readAgentInfo(path.resolve(options.registry), agentSpec);
6
+ }
@@ -0,0 +1,10 @@
1
+ import path from "node:path";
2
+ import { installBundle } from "../lib/install.js";
3
+
4
+ export async function installCommand(agentSpec, options) {
5
+ return installBundle({
6
+ registryDir: path.resolve(options.registry),
7
+ agentSpec,
8
+ targetWorkspace: path.resolve(options.targetWorkspace),
9
+ });
10
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * List Command
3
+ * 列出已安装的 Agent
4
+ */
5
+
6
+ import path from "node:path";
7
+ import { stat, readdir, readFile } from "node:fs/promises";
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
+ }
21
+
22
+ export async function listCommand(options = {}) {
23
+ // 检查多个可能的安装位置
24
+ const locations = [];
25
+
26
+ // 1. 当前目录的 .agenthub
27
+ const cwd = process.cwd();
28
+ const cwdAgenthub = path.join(cwd, ".agenthub");
29
+ if (await pathExists(cwdAgenthub)) {
30
+ const installs = await findInstallsInDir(cwd);
31
+ locations.push(...installs.map((i) => ({ ...i, location: cwd })));
32
+ }
33
+
34
+ // 2. 如果指定了 target-workspace
35
+ if (options.targetWorkspace) {
36
+ const targetPath = path.resolve(options.targetWorkspace);
37
+ const installs = await findInstallsInDir(targetPath);
38
+ locations.push(...installs.map((i) => ({ ...i, location: targetPath })));
39
+ }
40
+
41
+ return locations;
42
+ }
43
+
44
+ async function findInstallsInDir(dirPath) {
45
+ const installs = [];
46
+ const agenthubDir = path.join(dirPath, ".agenthub");
47
+ const installRecordPath = path.join(agenthubDir, "install.json");
48
+
49
+ if (await pathExists(installRecordPath)) {
50
+ try {
51
+ const record = await readJson(installRecordPath);
52
+ installs.push({
53
+ slug: record.slug,
54
+ version: record.version,
55
+ installedAt: record.installedAt || record.updatedAt,
56
+ rolledBackAt: record.rolledBackAt,
57
+ });
58
+ } catch {
59
+ // 无效的安装记录
60
+ }
61
+ }
62
+
63
+ return installs;
64
+ }
65
+
66
+ export function formatListOutput(installs) {
67
+ if (installs.length === 0) {
68
+ return "\n📭 暂无已安装的 Agent\n\n安装命令: agenthub install <agent-slug>\n";
69
+ }
70
+
71
+ const lines = [];
72
+ lines.push("\n📦 已安装的 Agent:\n");
73
+ lines.push("─".repeat(60));
74
+
75
+ for (const install of installs) {
76
+ const status = install.rolledBackAt ? "↩️ " : "✓ ";
77
+ lines.push(` ${status} ${install.slug}@${install.version}`);
78
+ if (install.location) {
79
+ lines.push(` 位置: ${install.location}`);
80
+ }
81
+ if (install.installedAt) {
82
+ lines.push(` 安装时间: ${install.installedAt}`);
83
+ }
84
+ lines.push("");
85
+ }
86
+
87
+ lines.push("─".repeat(60));
88
+ lines.push(`\n共 ${installs.length} 个已安装的 Agent\n`);
89
+
90
+ return lines.join("\n");
91
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Pack Command
3
+ * 打包 OpenClaw 工作区为 Agent Bundle
4
+ *
5
+ * 根据 PRD v1.1 Bundle 结构规范实现
6
+ */
7
+
8
+ import path from "node:path";
9
+ import { readdir, readFile, writeFile } from "node:fs/promises";
10
+ import { countFiles, copyDir, ensureDir, readJson, writeJson, pathExists } from "../lib/fs-utils.js";
11
+ import { createManifest, validateManifest, generateBundleId } from "../lib/manifest.js";
12
+ import { extractOpenClawTemplate } from "../lib/openclaw-config.js";
13
+
14
+ const WORKSPACE_CORE_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "IDENTITY.md", "TOOLS.md", "HEARTBEAT.md", "BOOTSTRAP.md"];
15
+
16
+ /**
17
+ * 扫描工作区文件
18
+ */
19
+ async function scanWorkspace(workspacePath) {
20
+ const files = {
21
+ core: [],
22
+ skills: [],
23
+ prompts: [],
24
+ canvas: false,
25
+ };
26
+
27
+ for (const filename of WORKSPACE_CORE_FILES) {
28
+ const filePath = path.join(workspacePath, filename);
29
+ if (await pathExists(filePath)) {
30
+ files.core.push(filename);
31
+ }
32
+ }
33
+
34
+ // 检查 skills 目录
35
+ const skillsPath = path.join(workspacePath, "skills");
36
+ if (await pathExists(skillsPath)) {
37
+ const entries = await readdir(skillsPath, { withFileTypes: true });
38
+ for (const entry of entries) {
39
+ if (entry.isDirectory()) {
40
+ files.skills.push(entry.name);
41
+ }
42
+ }
43
+ }
44
+
45
+ // 检查 PROMPTS 目录
46
+ const promptsPath = path.join(workspacePath, "PROMPTS");
47
+ if (await pathExists(promptsPath)) {
48
+ const entries = await readdir(promptsPath, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ if (entry.isFile() && entry.name.endsWith(".txt")) {
51
+ files.prompts.push(entry.name);
52
+ }
53
+ }
54
+ }
55
+
56
+ // 检查 canvas 目录
57
+ const canvasPath = path.join(workspacePath, "canvas");
58
+ files.canvas = await pathExists(canvasPath);
59
+
60
+ return files;
61
+ }
62
+
63
+ /**
64
+ * Pack 命令主函数
65
+ */
66
+ export async function packCommand(options) {
67
+ const workspace = path.resolve(options.workspace || process.cwd());
68
+ const output = path.resolve(options.output ?? "./bundles");
69
+ const configPath = options.config ? path.resolve(options.config) : null;
70
+
71
+ // 从工作区名称生成 slug
72
+ const slug = path.basename(workspace).toLowerCase().replace(/[^a-z0-9-]/g, "-");
73
+ const bundleDir = path.join(output, `${slug}-1.0.0.agent`);
74
+
75
+ // 1. 扫描工作区
76
+ const workspaceFiles = await scanWorkspace(workspace);
77
+
78
+ // 2. 统计 Memory 分层
79
+ const memoryCounts = {
80
+ public: await countFiles(path.join(workspace, "memory", "public")),
81
+ portable: await countFiles(path.join(workspace, "memory", "portable")),
82
+ private: await countFiles(path.join(workspace, "memory", "private")),
83
+ };
84
+
85
+ // 3. 读取并处理 OpenClaw 配置
86
+ let template = {};
87
+ if (configPath && (await pathExists(configPath))) {
88
+ const config = await readJson(configPath);
89
+ template = extractOpenClawTemplate(config);
90
+ }
91
+
92
+ // 4. 生成 MANIFEST
93
+ const manifest = createManifest({
94
+ slug,
95
+ name: path.basename(workspace),
96
+ memoryCounts,
97
+ openclawTemplate: template,
98
+ skills: workspaceFiles.skills,
99
+ prompts: workspaceFiles.prompts,
100
+ tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
101
+ category: options.category,
102
+ });
103
+
104
+ // 5. 验证 MANIFEST
105
+ const validation = validateManifest(manifest);
106
+ if (!validation.valid) {
107
+ throw new Error(`Invalid manifest: ${validation.errors.join(", ")}`);
108
+ }
109
+
110
+ // 6. 创建 Bundle 目录结构
111
+ await ensureDir(bundleDir);
112
+
113
+ // 6.1 复制 WORKSPACE
114
+ await copyDir(workspace, path.join(bundleDir, "WORKSPACE"));
115
+
116
+ // 6.2 写入 OPENCLAW.template.json
117
+ await writeJson(path.join(bundleDir, "OPENCLAW.template.json"), template);
118
+
119
+ // 6.3 写入 MANIFEST.json
120
+ await writeJson(path.join(bundleDir, "MANIFEST.json"), manifest);
121
+
122
+ // 6.4 生成 README.md
123
+ const readme = `# ${manifest.name}
124
+
125
+ ${manifest.description}
126
+
127
+ ## 安装
128
+
129
+ \`\`\`bash
130
+ agenthub install ${manifest.slug}
131
+ \`\`\`
132
+
133
+ ## 包含内容
134
+
135
+ - **记忆**: ${manifest.includes.memory.count} 条
136
+ - **技能**: ${manifest.includes.skills.join(", ") || "无"}
137
+ - **提示词**: ${manifest.includes.prompts} 个
138
+
139
+ ## 要求
140
+
141
+ - 运行时: OpenClaw ${manifest.runtime.version}
142
+ `;
143
+ await writeFile(path.join(bundleDir, "README.md"), readme, "utf8");
144
+
145
+ return {
146
+ bundleDir,
147
+ manifest,
148
+ validation,
149
+ bundleId: generateBundleId(manifest.slug, manifest.version),
150
+ };
151
+ }
@@ -0,0 +1,9 @@
1
+ import path from "node:path";
2
+ import { publishRemoteBundle } from "../lib/bundle-transfer.js";
3
+
4
+ export async function publishRemoteCommand(bundleDir, options) {
5
+ return publishRemoteBundle({
6
+ bundleDir: path.resolve(bundleDir),
7
+ serverUrl: options.server,
8
+ });
9
+ }
@@ -0,0 +1,7 @@
1
+ import path from "node:path";
2
+ import { publishBundle } from "../lib/registry.js";
3
+
4
+ export async function publishCommand(bundleDir, options) {
5
+ const registry = path.resolve(options.registry);
6
+ return publishBundle(path.resolve(bundleDir), registry);
7
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Rollback Command
3
+ * 回滚 Agent 到指定版本
4
+ */
5
+
6
+ import path from "node:path";
7
+ import { pathExists, readJson, writeJson } from "../lib/fs-utils.js";
8
+ import { installBundle } from "../lib/install.js";
9
+ import { versionsCommand } from "./versions.js";
10
+
11
+ export async function rollbackCommand(agentSpec, options) {
12
+ const registryDir = path.resolve(options.registry);
13
+ const targetWorkspace = options.targetWorkspace ? path.resolve(options.targetWorkspace) : null;
14
+ const targetVersion = options.to;
15
+
16
+ if (!targetVersion) {
17
+ throw new Error("请指定回滚版本: --to <version>");
18
+ }
19
+
20
+ const [slug] = agentSpec.split(":");
21
+
22
+ // 验证目标版本存在
23
+ const versions = await versionsCommand(slug, { registry: registryDir });
24
+ const versionExists = versions.some((v) => v.version === targetVersion);
25
+ if (!versionExists) {
26
+ throw new Error(`版本 ${targetVersion} 不存在`);
27
+ }
28
+
29
+ // 获取当前版本
30
+ let currentVersion = null;
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
+ }
38
+
39
+ // 执行回滚
40
+ const result = await installBundle({
41
+ registryDir,
42
+ agentSpec: `${slug}:${targetVersion}`,
43
+ targetWorkspace,
44
+ });
45
+
46
+ // 记录回滚
47
+ if (targetWorkspace) {
48
+ const installRecordPath = path.join(targetWorkspace, ".agenthub", "install.json");
49
+ await writeJson(installRecordPath, {
50
+ slug,
51
+ version: targetVersion,
52
+ rolledBackAt: new Date().toISOString(),
53
+ rolledBackFrom: currentVersion,
54
+ });
55
+ }
56
+
57
+ return {
58
+ rolledBack: true,
59
+ message: `已回滚 ${slug} 从 ${currentVersion || "未知"} 到 ${targetVersion}`,
60
+ currentVersion: targetVersion,
61
+ previousVersion: currentVersion,
62
+ manifest: result.manifest,
63
+ };
64
+ }
@@ -0,0 +1,7 @@
1
+ import path from "node:path";
2
+ import { searchRegistry } from "../lib/registry.js";
3
+
4
+ export async function searchCommand(query, options) {
5
+ const results = await searchRegistry(path.resolve(options.registry), query);
6
+ return results;
7
+ }