@vetta-org/plugin-cli 0.1.5 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { ActionRpcError, createActionRpcClient, readActionRpcEndpoint } from \"@vetta/action-rpc\";\nimport { readLatestNpmVersion, resolveNpmPluginArchive, type ResolvedNpmPluginArchive } from \"./npm-package.js\";\nimport { AGENTS_GUIDE_REVISION, readAgentsGuideRevision } from \"./agents-template.js\";\nimport { initHubRepository, initPluginProject, refreshAgentsGuide } from \"./init.js\";\nimport { describeIndexDrift, syncMarketplaceIndex } from \"./sync.js\";\nimport { findPluginHub, findPluginProject, type PluginProject, readManualSdkVersion, resolveManualDir } from \"./workspace.js\";\n\nexport type PluginAddCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"add\"; source: string; json: boolean };\n\nexport type PluginReloadCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"reload\"; pluginId: string; json: boolean };\n\nexport type PluginDocsCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"docs\"; json: boolean; checkLatest: boolean };\n\nexport type PluginInitCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"init\"; targetDir?: string; pluginId: string; displayName?: string; json: boolean }\n\t| { type: \"refresh-guide\"; targetDir?: string; json: boolean }\n\t| { type: \"init-hub\"; targetDir?: string; name: string; repository: string; minAppVersion: string; json: boolean };\n\nexport type PluginWatchCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"watch\"; dir?: string; stop: boolean; json: boolean };\n\nexport type PluginUninstallCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"uninstall\"; pluginId?: string; json: boolean };\n\nexport type PluginSyncCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"sync\"; check: boolean; json: boolean };\n\nexport type PluginCommand =\n\t| PluginAddCommand\n\t| PluginSyncCommand\n\t| PluginUninstallCommand\n\t| PluginReloadCommand\n\t| PluginDocsCommand\n\t| PluginInitCommand\n\t| PluginWatchCommand;\n\nexport interface PluginCommandDependencies {\n\tresolveNpmArchive(packageSpec: string): Promise<ResolvedNpmPluginArchive>;\n\t/** 命令执行时所在目录;缺省用 process.cwd(),测试与非交互调用方可以覆盖。 */\n\tcwd?(): string;\n\trunAction(actionId: string, input: unknown): Promise<unknown>;\n\twriteStdout(value: string): void;\n\twriteStderr(value: string): void;\n\t/** `docs --check-latest` 查询 registry 上最新的 SDK 版本;查不到(离线、私服)返回 undefined。 */\n\treadLatestSdkVersion?(): Promise<string | undefined>;\n}\n\nexport type PluginAddCommandDependencies = PluginCommandDependencies;\n\nconst HELP_TEXT = `Vetta plugin manager\n\nUsage:\n vetta-plugin-cli add <npm-package|zip-path|http-url> [--json]\n vetta-plugin-cli reload <plugin-id> [--json]\n vetta-plugin-cli docs [--check-latest] [--json]\n vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]\n vetta-plugin-cli init --refresh-guide [dir] [--json]\n vetta-plugin-cli init hub --name <slug> --repository <url> --min-app-version <x.y.z> [dir]\n vetta-plugin-cli watch [dir] [--stop] [--json]\n vetta-plugin-cli uninstall [plugin-id] [--json]\n vetta-plugin-cli sync [--check] [--json]\n\nExamples:\n npx @vetta-org/plugin-cli add @example/vetta-plugin-demo\n npx @vetta-org/plugin-cli add @example/vetta-plugin-demo@1.2.0\n npx @vetta-org/plugin-cli add . # 当前插件工程(先 pack)\n npx @vetta-org/plugin-cli add ./release/demo-1.2.0.zip\n npx @vetta-org/plugin-cli reload demo\n npx @vetta-org/plugin-cli docs\n npx @vetta-org/plugin-cli init --id my-plugin --name \"My Plugin\"\n npx @vetta-org/plugin-cli init hub --name my-market --repository https://github.com/me/my-market --min-app-version 0.55.0\n npx @vetta-org/plugin-cli watch # 让宿主改从工程目录加载,改完即生效\n npx @vetta-org/plugin-cli uninstall # 卸载当前插件工程对应的插件\n npx @vetta-org/plugin-cli sync # 在市场仓库根对账 .vetta/marketplace.json\n npx @vetta-org/plugin-cli sync --check # 只报不写,给 CI 用\n`;\n\nfunction formatParseError(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nexport function parsePluginAddCommand(argv: string[]): PluginAddCommand | undefined {\n\tif (argv[0] !== \"add\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({ args: argv.slice(1), allowPositionals: true, strict: true, options: { json: { type: \"boolean\" } } });\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [source, unexpected] = parsed.positionals;\n\tif (!source) return { type: \"error\", message: \"Missing <npm-package|zip-path|http-url>\" };\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"add\", source, json: parsed.values.json === true };\n}\n\nexport function parsePluginReloadCommand(argv: string[]): PluginReloadCommand | undefined {\n\tif (argv[0] !== \"reload\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({ args: argv.slice(1), allowPositionals: true, strict: true, options: { json: { type: \"boolean\" } } });\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [pluginId, unexpected] = parsed.positionals;\n\tif (!pluginId) return { type: \"error\", message: \"Missing <plugin-id>\" };\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"reload\", pluginId, json: parsed.values.json === true };\n}\n\nexport function parsePluginDocsCommand(argv: string[]): PluginDocsCommand | undefined {\n\tif (argv[0] !== \"docs\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, \"check-latest\": { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"docs\",\n\t\tjson: parsed.values.json === true,\n\t\tcheckLatest: parsed.values[\"check-latest\"] === true,\n\t};\n}\n\nexport function parsePluginInitCommand(argv: string[]): PluginInitCommand | undefined {\n\tif (argv[0] !== \"init\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tif (argv[1] === \"hub\") return parseInitHubCommand(argv.slice(2));\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: {\n\t\t\t\tid: { type: \"string\" },\n\t\t\t\tname: { type: \"string\" },\n\t\t\t\tjson: { type: \"boolean\" },\n\t\t\t\t\"refresh-guide\": { type: \"boolean\" },\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tif (parsed.values[\"refresh-guide\"] === true) {\n\t\tconst [dir, extra] = parsed.positionals;\n\t\tif (extra) return { type: \"error\", message: `Unexpected argument: ${extra}` };\n\t\t// 刷新是就地重写,工程的 id 和展示名从磁盘上读,不再由命令行给。\n\t\treturn { type: \"refresh-guide\", ...(dir ? { targetDir: dir } : {}), json: parsed.values.json === true };\n\t}\n\tconst pluginId = parsed.values.id;\n\tif (typeof pluginId !== \"string\" || pluginId.length === 0) {\n\t\treturn { type: \"error\", message: \"Missing --id <plugin-id>\" };\n\t}\n\tconst [targetDir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"init\",\n\t\t...(targetDir ? { targetDir } : {}),\n\t\tpluginId,\n\t\t...(typeof parsed.values.name === \"string\" ? { displayName: parsed.values.name } : {}),\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nfunction parseInitHubCommand(argv: string[]): PluginInitCommand {\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv,\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: {\n\t\t\t\tname: { type: \"string\" },\n\t\t\t\trepository: { type: \"string\" },\n\t\t\t\t\"min-app-version\": { type: \"string\" },\n\t\t\t\tjson: { type: \"boolean\" },\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst name = parsed.values.name;\n\tif (typeof name !== \"string\" || name.length === 0) return { type: \"error\", message: \"Missing --name <slug>\" };\n\tconst repository = parsed.values.repository;\n\tif (typeof repository !== \"string\" || repository.length === 0) {\n\t\treturn { type: \"error\", message: \"Missing --repository <https url>\" };\n\t}\n\t// 刻意不给默认值:太低会让装不动新 schema 的旧客户端也去激活快照,太高则部分用户直接\n\t// 看不到这个市场。这是发布决定,不该由工具替作者猜。\n\tconst minAppVersion = parsed.values[\"min-app-version\"];\n\tif (typeof minAppVersion !== \"string\" || minAppVersion.length === 0) {\n\t\treturn {\n\t\t\ttype: \"error\",\n\t\t\tmessage: \"Missing --min-app-version <x.y.z> (the oldest Vetta Desktop version your abilities support)\",\n\t\t};\n\t}\n\tconst [targetDir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"init-hub\",\n\t\t...(targetDir ? { targetDir } : {}),\n\t\tname,\n\t\trepository,\n\t\tminAppVersion,\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nexport function parsePluginWatchCommand(argv: string[]): PluginWatchCommand | undefined {\n\tif (argv[0] !== \"watch\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, stop: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [dir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"watch\",\n\t\t...(dir ? { dir } : {}),\n\t\tstop: parsed.values.stop === true,\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nexport function parsePluginUninstallCommand(argv: string[]): PluginUninstallCommand | undefined {\n\tif (argv[0] !== \"uninstall\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [pluginId, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\t// 省略 id 时按 cwd 推断,语义与 add . / watch 一致:站在哪个插件里就作用于哪个。\n\treturn { type: \"uninstall\", ...(pluginId ? { pluginId } : {}), json: parsed.values.json === true };\n}\n\nexport function parsePluginSyncCommand(argv: string[]): PluginSyncCommand | undefined {\n\tif (argv[0] !== \"sync\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, check: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"sync\", check: parsed.values.check === true, json: parsed.values.json === true };\n}\n\nasync function defaultRunAction(actionId: string, input: unknown): Promise<unknown> {\n\tconst client = createActionRpcClient(await readActionRpcEndpoint());\n\treturn client.run(actionId, input);\n}\n\nconst defaultDependencies: PluginCommandDependencies = {\n\tresolveNpmArchive: resolveNpmPluginArchive,\n\tcwd: () => process.cwd(),\n\trunAction: defaultRunAction,\n\twriteStdout: (value) => process.stdout.write(value),\n\twriteStderr: (value) => process.stderr.write(value),\n\treadLatestSdkVersion: () => readLatestNpmVersion(\"@vetta-org/plugin-sdk\"),\n};\n\nfunction isHttpUrl(source: string): boolean {\n\ttry {\n\t\tconst url = new URL(source);\n\t\treturn url.protocol === \"http:\" || url.protocol === \"https:\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction isLocalZip(source: string): boolean {\n\tif (source.toLowerCase().endsWith(\".zip\")) return true;\n\tconst path = resolve(source);\n\t// 目录不是压缩包:它是一个插件工程,走 resolveProjectArchive 先找它打出来的产物。\n\treturn existsSync(path) && !statSync(path).isDirectory();\n}\n\nfunction isDirectorySource(source: string): boolean {\n\tconst path = resolve(source);\n\treturn existsSync(path) && statSync(path).isDirectory();\n}\n\n/**\n * 把「装当前这个工程」翻译成一个具体的归档路径。\n *\n * 这条路径是给 `install:vetta` 这类脚本用的:作者(或 Agent)在插件目录里跑一条命令就\n * 装进 Vetta,不必记住产物叫什么名字。找不到产物时给出该跑的那条命令,而不是报一个\n * 「文件不存在」让人自己猜。\n */\nfunction resolveProjectArchive(source: string): { archivePath: string; project: PluginProject } {\n\tconst from = resolve(source);\n\tconst project = findPluginProject(from);\n\tif (!project) {\n\t\tconst hub = findPluginHub(from);\n\t\tif (hub) {\n\t\t\tthrow new Error(\n\t\t\t\t`${from} indexes plugins but is not one itself. Run this from a plugin directory, or pass its path: vetta-plugin-cli add ./path/to/plugin`,\n\t\t\t);\n\t\t}\n\t\tthrow new Error(`No plugin.json found in ${from} or any parent directory.`);\n\t}\n\tconst archivePath = join(project.root, \"release\", `${project.pluginId}-${project.version}.zip`);\n\tif (!existsSync(archivePath)) {\n\t\tthrow new Error(\n\t\t\t`Packaged archive not found: ${archivePath}\\nBuild it first: npm run build && npx vetta-plugin pack`,\n\t\t);\n\t}\n\treturn { archivePath, project };\n}\n\n/** 装完立刻检查索引是否还停在旧版本;不在市场仓库里时什么也不说。 */\nfunction indexDriftHint(project: PluginProject): string | undefined {\n\tconst hub = findPluginHub(project.root);\n\tif (!hub) return undefined;\n\treturn describeIndexDrift({\n\t\thubRoot: hub.root,\n\t\tmanifestPath: hub.manifestPath,\n\t\tslug: project.pluginId,\n\t\tversion: project.version,\n\t});\n}\n\nfunction npmInstallInput(resolved: ResolvedNpmPluginArchive): Record<string, unknown> {\n\treturn {\n\t\toperation: \"install-from-path\",\n\t\tpath: resolved.archivePath,\n\t\tenable: true,\n\t\tsource: \"npm\",\n\t\texpectedSha256: resolved.expectedSha256,\n\t\texpectedId: resolved.packageManifest.vetta.pluginId,\n\t\texpectedVersion: resolved.packageManifest.version,\n\t\tnpm: {\n\t\t\tpackageName: resolved.packageManifest.name,\n\t\t\trequestedSpec: resolved.requestedSpec,\n\t\t\tresolvedVersion: resolved.packageManifest.version,\n\t\t\t...(resolved.integrity ? { integrity: resolved.integrity } : {}),\n\t\t},\n\t};\n}\n\nfunction resultSummary(result: unknown): string {\n\tif (typeof result !== \"object\" || result === null || Array.isArray(result)) return \"Plugin installed.\\n\";\n\tconst response = result as Record<string, unknown>;\n\tconst plugin =\n\t\ttypeof response.plugin === \"object\" && response.plugin !== null && !Array.isArray(response.plugin)\n\t\t\t? (response.plugin as Record<string, unknown>)\n\t\t\t: undefined;\n\tif (!plugin) return \"Plugin installed.\\n\";\n\tconst id = typeof plugin.id === \"string\" ? plugin.id : \"plugin\";\n\tconst version = typeof plugin.version === \"string\" ? `@${plugin.version}` : \"\";\n\tconst pending = typeof plugin.pendingVersion === \"string\"\n\t\t? ` Update ${plugin.pendingVersion} is pending reload. Run \\`vetta-plugin-cli reload ${id}\\` to apply it.`\n\t\t: \"\";\n\treturn `Installed ${id}${version}.${pending}\\n`;\n}\n\nfunction reloadResultSummary(result: unknown, requestedPluginId: string): string {\n\tif (typeof result !== \"object\" || result === null || Array.isArray(result)) {\n\t\treturn `Reloaded ${requestedPluginId}.\\n`;\n\t}\n\tconst response = result as Record<string, unknown>;\n\tconst plugin =\n\t\ttypeof response.plugin === \"object\" && response.plugin !== null && !Array.isArray(response.plugin)\n\t\t\t? (response.plugin as Record<string, unknown>)\n\t\t\t: undefined;\n\tconst id = typeof plugin?.id === \"string\" ? plugin.id : requestedPluginId;\n\tconst version = typeof plugin?.activeVersion === \"string\" ? `@${plugin.activeVersion}` : \"\";\n\treturn `Reloaded ${id}${version}.\\n`;\n}\n\nfunction isConnectionError(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst code = (error as NodeJS.ErrnoException).code;\n\treturn (\n\t\tcode === \"ENOENT\" ||\n\t\tcode === \"ECONNREFUSED\" ||\n\t\tcode === \"ECONNRESET\" ||\n\t\terror.message.includes(\"ECONNREFUSED\") ||\n\t\terror.message.includes(\"fetch failed\")\n\t);\n}\n\nexport async function runPluginAddCommand(\n\tcommand: PluginAddCommand,\n\tdependencies: PluginAddCommandDependencies = defaultDependencies,\n): Promise<number> {\n\treturn runPluginCommand(command, dependencies);\n}\n\nexport async function runPluginCommand(\n\tcommand: PluginCommand,\n\tdependencies: PluginCommandDependencies = defaultDependencies,\n): Promise<number> {\n\tif (command.type === \"help\") {\n\t\tdependencies.writeStdout(HELP_TEXT);\n\t\treturn 0;\n\t}\n\tif (command.type === \"error\") {\n\t\tdependencies.writeStderr(`${command.message}\\n`);\n\t\treturn 2;\n\t}\n\n\tif (command.type === \"docs\") {\n\t\treturn await runDocsCommand(command, dependencies);\n\t}\n\tif (command.type === \"init\") {\n\t\treturn runInitCommand(command, dependencies);\n\t}\n\tif (command.type === \"refresh-guide\") {\n\t\treturn runRefreshGuideCommand(command, dependencies);\n\t}\n\tif (command.type === \"init-hub\") {\n\t\treturn runInitHubCommand(command, dependencies);\n\t}\n\tif (command.type === \"sync\") {\n\t\treturn runSyncCommand(command, dependencies);\n\t}\n\n\tif (command.type === \"watch\") {\n\t\treturn runWatchCommand(command, dependencies);\n\t}\n\tif (command.type === \"uninstall\") {\n\t\treturn runUninstallCommand(command, dependencies);\n\t}\n\n\tlet resolvedNpm: ResolvedNpmPluginArchive | undefined;\n\tlet driftHint: string | undefined;\n\ttry {\n\t\tlet result: unknown;\n\t\tif (command.type === \"reload\") {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"reload\",\n\t\t\t\tid: command.pluginId,\n\t\t\t});\n\t\t} else if (isHttpUrl(command.source)) {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-url\",\n\t\t\t\turl: command.source,\n\t\t\t});\n\t\t} else if (isDirectorySource(command.source)) {\n\t\t\tconst { archivePath, project } = resolveProjectArchive(command.source);\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-path\",\n\t\t\t\tpath: archivePath,\n\t\t\t\tenable: true,\n\t\t\t});\n\t\t\tdriftHint = indexDriftHint(project);\n\t\t} else if (isLocalZip(command.source)) {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-path\",\n\t\t\t\tpath: resolve(command.source),\n\t\t\t\tenable: true,\n\t\t\t});\n\t\t} else {\n\t\t\tresolvedNpm = await dependencies.resolveNpmArchive(command.source);\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", npmInstallInput(resolvedNpm));\n\t\t}\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, result, ...(driftHint ? { warning: driftHint } : {}) })}\\n`\n\t\t\t\t: command.type === \"reload\"\n\t\t\t\t\t? reloadResultSummary(result, command.pluginId)\n\t\t\t\t\t: `${resultSummary(result)}${driftHint ? `${driftHint}\\n` : \"\"}`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : command.type === \"reload\" ? \"PLUGIN_RELOAD_FAILED\" : \"PLUGIN_ADD_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t} finally {\n\t\tawait resolvedNpm?.cleanup();\n\t}\n}\n\n/**\n * 打印随 SDK 发布的手册目录。\n *\n * 存在的理由是「不要让任何人硬编码 node_modules 路径」:工作区会把依赖提升到仓库根,\n * 一仓多插件的 hub 里每个插件也可能各装一份。Agent 只需记住这一条命令,拿回来的永远是\n * 当前工程实际编译所针对的那个 SDK 版本的手册。\n */\nasync function runDocsCommand(\n\tcommand: { json: boolean; checkLatest: boolean },\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst manualDir = resolveManualDir(cwd);\n\tif (!manualDir) {\n\t\t// 能力市场仓库的根目录通常没装 SDK,手册在各能力目录里。直接说「装 SDK」会把人引到\n\t\t// 仓库根去装一份用不上的依赖。\n\t\tconst inHubRoot = findPluginHub(cwd) !== undefined && findPluginProject(cwd) === undefined;\n\t\tconst message = inHubRoot\n\t\t\t? \"Plugin manual not found at the hub root. cd into an ability directory (abilities/plugins/<slug>), then run npm install.\\n\"\n\t\t\t: \"Plugin manual not found. Install the SDK first: npm i -D @vetta-org/plugin-sdk\\n\";\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: \"MANUAL_NOT_FOUND\", message: message.trim() } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(message);\n\t\t}\n\t\treturn 6;\n\t}\n\tconst project = findPluginProject(cwd);\n\tconst hub = findPluginHub(cwd);\n\tconst sdkVersion = readManualSdkVersion(manualDir);\n\tconst latestVersion = command.checkLatest ? await dependencies.readLatestSdkVersion?.() : undefined;\n\tconst outdated = sdkVersion !== undefined && latestVersion !== undefined && compareSemver(sdkVersion, latestVersion) < 0;\n\tconst guide = inspectAgentsGuide(project?.root ?? hub?.root ?? cwd);\n\n\tif (command.json) {\n\t\tdependencies.writeStdout(\n\t\t\t`${JSON.stringify({\n\t\t\t\tok: true,\n\t\t\t\tmanualDir,\n\t\t\t\tentry: join(manualDir, \"README.md\"),\n\t\t\t\tsdkVersion,\n\t\t\t\trefreshCommand: SDK_REFRESH_COMMAND,\n\t\t\t\tguide,\n\t\t\t\t...(command.checkLatest ? { latestVersion, outdated } : {}),\n\t\t\t\tproject: project ? { root: project.root, pluginId: project.pluginId, version: project.version } : undefined,\n\t\t\t\thub: hub\n\t\t\t\t\t? {\n\t\t\t\t\t\t\troot: hub.root,\n\t\t\t\t\t\t\tmanifestPath: hub.manifestPath,\n\t\t\t\t\t\t\tsyncHint: \"After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t})}\\n`,\n\t\t);\n\t\treturn 0;\n\t}\n\tconst lines = [\n\t\t`Plugin manual (@vetta-org/plugin-sdk${sdkVersion ? `@${sdkVersion}` : \"\"}):`,\n\t\t` ${manualDir}`,\n\t\t`Start here: ${join(manualDir, \"README.md\")}`,\n\t];\n\tif (project) lines.push(`Current plugin: ${project.pluginId} (${project.root})`);\n\tif (outdated) {\n\t\tlines.push(`Manual is behind: ${sdkVersion} → ${latestVersion}. Refresh it with: ${SDK_REFRESH_COMMAND}`);\n\t} else if (command.checkLatest && latestVersion === undefined) {\n\t\tlines.push(`Could not reach the registry; cannot tell whether ${sdkVersion ?? \"this manual\"} is current.`);\n\t} else {\n\t\t// 手册是随 SDK 装进 node_modules 的快照,工程不升级它就永远停在初始化那天的版本。\n\t\t// 这条命令必须每次都打印:读到它的 Agent 手上的 AGENTS.md 往往也是同一天的快照。\n\t\tlines.push(`Manual follows the installed SDK. To refresh it: ${SDK_REFRESH_COMMAND}`);\n\t}\n\tif (guide.stale) {\n\t\t// 说明书同样是快照,而且用户没有理由回头看它。这里是唯一会被读到的位置。\n\t\tlines.push(`This brief is stale (AGENTS.md revision ${guide.revision ?? \"unstamped\"} < ${AGENTS_GUIDE_REVISION}). Refresh it with: ${GUIDE_REFRESH_COMMAND}`);\n\t}\n\tif (hub) {\n\t\tlines.push(`Marketplace index: ${hub.manifestPath}`);\n\t\t// Agent 几乎一定会先跑 docs,所以这是告诉它「索引要对账」的最佳时机。\n\t\tlines.push(\"After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.\");\n\t}\n\tdependencies.writeStdout(`${lines.join(\"\\n\")}\\n`);\n\treturn 0;\n}\n\n/**\n * 刷新手册的命令。\n *\n * 手册不从网络现取,而是随 SDK 进 `node_modules`——Agent 读到的合同因此与工程实际编译的\n * 版本一致。代价是它不会自己变新,所以「怎么变新」必须由 CLI 每次说一遍:`npx` 默认取最新的\n * CLI,它的输出是这条链路上唯一不会过期的位置。\n */\nconst SDK_REFRESH_COMMAND = \"npm i -D @vetta-org/plugin-sdk@latest && npx vetta-plugin-cli docs\";\n\n/** 刷新说明书的命令。与手册各刷各的:一个随 SDK 走,一个随 CLI 走。 */\nconst GUIDE_REFRESH_COMMAND = \"npx @vetta-org/plugin-cli init --refresh-guide\";\n\nexport interface AgentsGuideStatus {\n\t/** 本工程有没有 AGENTS.md。 */\n\treadonly present: boolean;\n\t/** 读到的版本戳;没有戳(模板早于版本戳,或是手写的)时缺省。 */\n\treadonly revision?: number;\n\t/** 落后于当前 CLI 的模板。没有 AGENTS.md 时为 false——那是「没有」,不是「旧」。 */\n\treadonly stale: boolean;\n}\n\n/**\n * 判断工程里的 AGENTS.md 是不是旧模板。\n *\n * 说明书凝固在 `init` 那天,而用户没有理由回头看它——所以「它旧了」这件事只能由每次都会被\n * 跑到的 `docs` 说出来。没有版本戳的一律当作旧的:那是版本戳出现之前的模板。\n */\nfunction inspectAgentsGuide(root: string): AgentsGuideStatus {\n\tconst path = join(root, \"AGENTS.md\");\n\tif (!existsSync(path)) return { present: false, stale: false };\n\tlet revision: number | undefined;\n\ttry {\n\t\trevision = readAgentsGuideRevision(readFileSync(path, \"utf8\"));\n\t} catch {\n\t\treturn { present: true, stale: false };\n\t}\n\treturn {\n\t\tpresent: true,\n\t\t...(revision === undefined ? {} : { revision }),\n\t\tstale: revision === undefined || revision < AGENTS_GUIDE_REVISION,\n\t};\n}\n\n/** 够用的 semver 比较:只看 major.minor.patch,预发布后缀一律当作小于正式版。 */\nfunction compareSemver(left: string, right: string): number {\n\tconst parse = (value: string): readonly [number, number, number, boolean] => {\n\t\tconst match = /^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$/.exec(value.trim());\n\t\tif (!match) return [0, 0, 0, false];\n\t\treturn [Number(match[1]), Number(match[2]), Number(match[3]), match[4] !== undefined];\n\t};\n\tconst a = parse(left);\n\tconst b = parse(right);\n\tfor (let index = 0; index < 3; index += 1) {\n\t\tif (a[index] !== b[index]) return a[index]! < b[index]! ? -1 : 1;\n\t}\n\tif (a[3] === b[3]) return 0;\n\treturn a[3] ? -1 : 1;\n}\n\n/**\n * 在已有工程里把 AGENTS.md 重写成当前 CLI 的版本。\n *\n * `init` 拒绝覆盖已有工程,所以老目录里那份说明书从落地起就停在原地。它是纯派生产物,重写\n * 它不会碰用户写过的任何东西——这也是唯一一个能这么做的脚手架文件。\n */\nfunction runRefreshGuideCommand(\n\tcommand: Extract<PluginCommand, { type: \"refresh-guide\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = refreshAgentsGuide(resolve(cwd, command.targetDir ?? \".\"));\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: `Rewrote ${result.file}\\nNext: npx vetta-plugin-cli docs --check-latest\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: \"GUIDE_REFRESH_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 7;\n\t}\n}\n\n/** 在陌生目录里生成一个可直接开工的插件工程,并留下让任意 Agent 自举的 AGENTS.md。 */\nfunction runInitCommand(\n\tcommand: Extract<PluginCommand, { type: \"init\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = initPluginProject({\n\t\t\ttargetDir: resolve(cwd, command.targetDir ?? command.pluginId),\n\t\t\tpluginId: command.pluginId,\n\t\t\tdisplayName: command.displayName ?? command.pluginId,\n\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: [\n\t\t\t\t\t\t`Created ${result.pluginId} at ${result.root}`,\n\t\t\t\t\t\t\"Next: npm install && npm run install:vetta\",\n\t\t\t\t\t\t\"The agent brief is in AGENTS.md; after npm install, run `npx vetta-plugin-cli docs` for the manual.\",\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t\t.concat(\"\\n\"),\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"PLUGIN_INIT_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\n/**\n * 让宿主改从工程目录加载本插件,之后改源码即时生效,不必每次 build → pack → install。\n *\n * 目标插件按 cwd 向上找,理由同 `add .`:一仓多插件时「我正站在哪个插件里」是唯一不会\n * 弄错的意图,而 id 靠人重复输入迟早会错配到另一个插件上。\n */\nasync function runWatchCommand(\n\tcommand: Extract<PluginCommand, { type: \"watch\" }>,\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst from = resolve(cwd, command.dir ?? \".\");\n\ttry {\n\t\tconst project = findPluginProject(from);\n\t\tif (!project) {\n\t\t\tconst hub = findPluginHub(from);\n\t\t\tthrow new Error(\n\t\t\t\thub\n\t\t\t\t\t? `${from} indexes plugins but is not one itself. Run this from a plugin directory, or pass its path.`\n\t\t\t\t\t: `No plugin.json found in ${from} or any parent directory.`,\n\t\t\t);\n\t\t}\n\t\tconst result = command.stop\n\t\t\t? await dependencies.runAction(\"plugins.manage\", { operation: \"dev-watch-stop\", id: project.pluginId })\n\t\t\t: await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\t\toperation: \"dev-watch\",\n\t\t\t\t\tid: project.pluginId,\n\t\t\t\t\tprojectDir: project.root,\n\t\t\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, result })}\\n`\n\t\t\t\t: command.stop\n\t\t\t\t\t? `Stopped hot reload for ${project.pluginId}.\\n`\n\t\t\t\t\t: `Hot reload on for ${project.pluginId}. Vetta now loads it from ${project.root}.\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : \"PLUGIN_WATCH_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t}\n}\n\n/**\n * 卸载一个插件。省略 id 时按 cwd 推断,语义与 `add .` / `watch` 一致。\n *\n * 刻意不在这里做二次确认:宿主自己会为写操作弹审批,CLI 再问一遍只是噪音。系统插件由\n * 宿主拒绝,这里不重复判断——那份名单不该有第二个真相源。\n */\nasync function runUninstallCommand(\n\tcommand: Extract<PluginCommand, { type: \"uninstall\" }>,\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tlet pluginId = command.pluginId;\n\t\tif (!pluginId) {\n\t\t\tconst project = findPluginProject(cwd);\n\t\t\tif (!project) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No plugin.json found in ${cwd} or any parent directory. Pass the id: vetta-plugin-cli uninstall <plugin-id>`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tpluginId = project.pluginId;\n\t\t}\n\t\tconst result = await dependencies.runAction(\"plugins.manage\", { operation: \"uninstall\", id: pluginId });\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json ? `${JSON.stringify({ ok: true, result })}\\n` : `Uninstalled ${pluginId}.\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : \"PLUGIN_UNINSTALL_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t}\n}\n\n/**\n * 对账能力市场索引。定位靠向上找 `.vetta/marketplace.json`,因此在仓库任何位置都能跑。\n *\n * `--check` 只报不写并以非零退出,给 CI 用:索引漂移的三种后果里,两种不在作者机器上复现,\n * 一种压根不报错,光靠人自觉看不住。\n */\nfunction runSyncCommand(\n\tcommand: Extract<PluginCommand, { type: \"sync\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst hub = findPluginHub(cwd);\n\tif (!hub) {\n\t\tconst message = `No .vetta/marketplace.json found in ${cwd} or any parent directory. sync is for marketplace repositories.\\n`;\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"HUB_NOT_FOUND\", message: message.trim() } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(message);\n\t\t}\n\t\treturn 6;\n\t}\n\ttry {\n\t\tconst result = syncMarketplaceIndex({ hubRoot: hub.root, manifestPath: hub.manifestPath, apply: !command.check });\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: result.problems.length === 0, ...result })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStdout(formatSyncReport(result, command.check));\n\t\t}\n\t\tif (result.problems.length > 0) return 7;\n\t\t// --check 的职责就是「有漂移就红」,否则 CI 拦不住任何东西。\n\t\treturn command.check && result.changes.length > 0 ? 7 : 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"SYNC_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\nfunction formatSyncReport(result: ReturnType<typeof syncMarketplaceIndex>, check: boolean): string {\n\tconst lines: string[] = [];\n\tfor (const change of result.changes) {\n\t\tlines.push(` ${change.slug}: ${change.field} ${JSON.stringify(change.from)} -> ${JSON.stringify(change.to)}`);\n\t}\n\tif (lines.length > 0) {\n\t\tlines.unshift(check ? \"Index is out of date:\" : \"Updated the index:\");\n\t}\n\tif (result.problems.length > 0) {\n\t\tlines.push(\"Problems:\");\n\t\tfor (const problem of result.problems) lines.push(` ${problem.slug}: ${problem.message}`);\n\t}\n\tif (result.unlisted.length > 0) {\n\t\tlines.push(\"Ability directories not listed in the index (add them by hand when ready to publish):\");\n\t\tfor (const dir of result.unlisted) lines.push(` ${dir}`);\n\t}\n\tif (lines.length === 0) return \"Index is in sync.\\n\";\n\tif (check && result.changes.length > 0) lines.push(\"Run `vetta-plugin-cli sync` to apply.\");\n\treturn `${lines.join(\"\\n\")}\\n`;\n}\n\n/** 生成一个合规的能力市场仓库骨架,连同仓库级 AGENTS.md 与对账用的 CI。 */\nfunction runInitHubCommand(\n\tcommand: Extract<PluginCommand, { type: \"init-hub\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = initHubRepository({\n\t\t\ttargetDir: resolve(cwd, command.targetDir ?? command.name),\n\t\t\tname: command.name,\n\t\t\trepository: command.repository,\n\t\t\tminAppVersion: command.minAppVersion,\n\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: [\n\t\t\t\t\t\t`Created marketplace ${result.name} at ${result.root}`,\n\t\t\t\t\t\t\"Add an ability: npx @vetta-org/plugin-cli init --id <slug> --name \\\"<Display>\\\" abilities/plugins/<slug>\",\n\t\t\t\t\t\t\"Then list it in .vetta/marketplace.json and run: npx @vetta-org/plugin-cli sync\",\n\t\t\t\t\t\t\"The working agreement for agents is in AGENTS.md.\",\n\t\t\t\t\t].join(\"\\n\") + \"\\n\",\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"HUB_INIT_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\nexport async function runPluginCli(argv: string[]): Promise<number> {\n\tif (argv.length === 0 || argv[0] === \"-h\" || argv[0] === \"--help\") {\n\t\treturn runPluginAddCommand({ type: \"help\" });\n\t}\n\tconst command = parsePluginAddCommand(argv) ?? parsePluginReloadCommand(argv) ?? parsePluginDocsCommand(argv) ?? parsePluginInitCommand(argv) ?? parsePluginWatchCommand(argv) ?? parsePluginUninstallCommand(argv) ?? parsePluginSyncCommand(argv);\n\tif (!command) {\n\t\tprocess.stderr.write(`Unknown command: ${argv[0]}\\n`);\n\t\treturn 2;\n\t}\n\treturn runPluginCommand(command);\n}\n"]}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { ActionRpcError, createActionRpcClient, readActionRpcEndpoint } from \"@vetta/action-rpc\";\nimport { readLatestNpmVersion, resolveNpmPluginArchive, type ResolvedNpmPluginArchive } from \"./npm-package.js\";\nimport { AGENTS_GUIDE_REVISION, readAgentsGuideRevision } from \"./agents-template.js\";\nimport { initHubRepository, initPluginProject, refreshAgentsGuide } from \"./init.js\";\nimport { describeIndexDrift, syncMarketplaceIndex } from \"./sync.js\";\nimport { findPluginHub, findPluginProject, type PluginProject, readManualSdkVersion, resolveManualDir } from \"./workspace.js\";\n\nexport type PluginAddCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"add\"; source: string; json: boolean };\n\nexport type PluginReloadCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"reload\"; pluginId: string; json: boolean };\n\nexport type PluginDocsCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"docs\"; json: boolean; checkLatest: boolean };\n\nexport type PluginInitCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"init\"; targetDir?: string; pluginId: string; displayName?: string; json: boolean }\n\t| { type: \"refresh-guide\"; targetDir?: string; json: boolean; force: boolean; dryRun: boolean }\n\t| { type: \"init-hub\"; targetDir?: string; name: string; repository: string; minAppVersion: string; json: boolean };\n\nexport type PluginWatchCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"watch\"; dir?: string; stop: boolean; json: boolean };\n\nexport type PluginUninstallCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"uninstall\"; pluginId?: string; json: boolean };\n\nexport type PluginSyncCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"sync\"; check: boolean; json: boolean };\n\nexport type PluginCommand =\n\t| PluginAddCommand\n\t| PluginSyncCommand\n\t| PluginUninstallCommand\n\t| PluginReloadCommand\n\t| PluginDocsCommand\n\t| PluginInitCommand\n\t| PluginWatchCommand;\n\nexport interface PluginCommandDependencies {\n\tresolveNpmArchive(packageSpec: string): Promise<ResolvedNpmPluginArchive>;\n\t/** 命令执行时所在目录;缺省用 process.cwd(),测试与非交互调用方可以覆盖。 */\n\tcwd?(): string;\n\trunAction(actionId: string, input: unknown): Promise<unknown>;\n\twriteStdout(value: string): void;\n\twriteStderr(value: string): void;\n\t/** `docs --check-latest` 查询 registry 上最新的 SDK 版本;查不到(离线、私服)返回 undefined。 */\n\treadLatestSdkVersion?(): Promise<string | undefined>;\n}\n\nexport type PluginAddCommandDependencies = PluginCommandDependencies;\n\nconst HELP_TEXT = `Vetta plugin manager\n\nUsage:\n vetta-plugin-cli add <npm-package|package-path|http-url> [--json]\n vetta-plugin-cli reload <plugin-id> [--json]\n vetta-plugin-cli docs [--check-latest] [--json]\n vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]\n vetta-plugin-cli init --refresh-guide [dir] [--dry-run] [--force] [--json]\n vetta-plugin-cli init hub --name <slug> --repository <url> --min-app-version <x.y.z> [dir]\n vetta-plugin-cli watch [dir] [--stop] [--json]\n vetta-plugin-cli uninstall [plugin-id] [--json]\n vetta-plugin-cli sync [--check] [--json]\n\nExamples:\n npx @vetta-org/plugin-cli add @example/vetta-plugin-demo\n npx @vetta-org/plugin-cli add @example/vetta-plugin-demo@1.2.0\n npx @vetta-org/plugin-cli add . # 当前插件工程(先 pack)\n npx @vetta-org/plugin-cli add ./release/demo-1.2.0.vettapkg\n npx @vetta-org/plugin-cli reload demo\n npx @vetta-org/plugin-cli docs\n npx @vetta-org/plugin-cli init --id my-plugin --name \"My Plugin\"\n npx @vetta-org/plugin-cli init hub --name my-market --repository https://github.com/me/my-market --min-app-version 0.55.0\n npx @vetta-org/plugin-cli watch # 让宿主改从工程目录加载,改完即生效\n npx @vetta-org/plugin-cli uninstall # 卸载当前插件工程对应的插件\n npx @vetta-org/plugin-cli sync # 在市场仓库根对账 .vetta/marketplace.json\n npx @vetta-org/plugin-cli sync --check # 只报不写,给 CI 用\n`;\n\nfunction formatParseError(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nexport function parsePluginAddCommand(argv: string[]): PluginAddCommand | undefined {\n\tif (argv[0] !== \"add\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({ args: argv.slice(1), allowPositionals: true, strict: true, options: { json: { type: \"boolean\" } } });\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [source, unexpected] = parsed.positionals;\n\tif (!source) return { type: \"error\", message: \"Missing <npm-package|package-path|http-url>\" };\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"add\", source, json: parsed.values.json === true };\n}\n\nexport function parsePluginReloadCommand(argv: string[]): PluginReloadCommand | undefined {\n\tif (argv[0] !== \"reload\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({ args: argv.slice(1), allowPositionals: true, strict: true, options: { json: { type: \"boolean\" } } });\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [pluginId, unexpected] = parsed.positionals;\n\tif (!pluginId) return { type: \"error\", message: \"Missing <plugin-id>\" };\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"reload\", pluginId, json: parsed.values.json === true };\n}\n\nexport function parsePluginDocsCommand(argv: string[]): PluginDocsCommand | undefined {\n\tif (argv[0] !== \"docs\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, \"check-latest\": { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"docs\",\n\t\tjson: parsed.values.json === true,\n\t\tcheckLatest: parsed.values[\"check-latest\"] === true,\n\t};\n}\n\nexport function parsePluginInitCommand(argv: string[]): PluginInitCommand | undefined {\n\tif (argv[0] !== \"init\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tif (argv[1] === \"hub\") return parseInitHubCommand(argv.slice(2));\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: {\n\t\t\t\tid: { type: \"string\" },\n\t\t\t\tname: { type: \"string\" },\n\t\t\t\tjson: { type: \"boolean\" },\n\t\t\t\t\"refresh-guide\": { type: \"boolean\" },\n\t\t\t\tforce: { type: \"boolean\" },\n\t\t\t\t\"dry-run\": { type: \"boolean\" },\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tif (parsed.values[\"refresh-guide\"] === true) {\n\t\tconst [dir, extra] = parsed.positionals;\n\t\tif (extra) return { type: \"error\", message: `Unexpected argument: ${extra}` };\n\t\t// 刷新是就地重写,工程的 id 和展示名从磁盘上读,不再由命令行给。\n\t\treturn {\n\t\t\ttype: \"refresh-guide\",\n\t\t\t...(dir ? { targetDir: dir } : {}),\n\t\t\tjson: parsed.values.json === true,\n\t\t\tforce: parsed.values.force === true,\n\t\t\tdryRun: parsed.values[\"dry-run\"] === true,\n\t\t};\n\t}\n\tconst pluginId = parsed.values.id;\n\tif (typeof pluginId !== \"string\" || pluginId.length === 0) {\n\t\treturn { type: \"error\", message: \"Missing --id <plugin-id>\" };\n\t}\n\tconst [targetDir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"init\",\n\t\t...(targetDir ? { targetDir } : {}),\n\t\tpluginId,\n\t\t...(typeof parsed.values.name === \"string\" ? { displayName: parsed.values.name } : {}),\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nfunction parseInitHubCommand(argv: string[]): PluginInitCommand {\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv,\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: {\n\t\t\t\tname: { type: \"string\" },\n\t\t\t\trepository: { type: \"string\" },\n\t\t\t\t\"min-app-version\": { type: \"string\" },\n\t\t\t\tjson: { type: \"boolean\" },\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst name = parsed.values.name;\n\tif (typeof name !== \"string\" || name.length === 0) return { type: \"error\", message: \"Missing --name <slug>\" };\n\tconst repository = parsed.values.repository;\n\tif (typeof repository !== \"string\" || repository.length === 0) {\n\t\treturn { type: \"error\", message: \"Missing --repository <https url>\" };\n\t}\n\t// 刻意不给默认值:太低会让装不动新 schema 的旧客户端也去激活快照,太高则部分用户直接\n\t// 看不到这个市场。这是发布决定,不该由工具替作者猜。\n\tconst minAppVersion = parsed.values[\"min-app-version\"];\n\tif (typeof minAppVersion !== \"string\" || minAppVersion.length === 0) {\n\t\treturn {\n\t\t\ttype: \"error\",\n\t\t\tmessage: \"Missing --min-app-version <x.y.z> (the oldest Vetta Desktop version your abilities support)\",\n\t\t};\n\t}\n\tconst [targetDir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"init-hub\",\n\t\t...(targetDir ? { targetDir } : {}),\n\t\tname,\n\t\trepository,\n\t\tminAppVersion,\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nexport function parsePluginWatchCommand(argv: string[]): PluginWatchCommand | undefined {\n\tif (argv[0] !== \"watch\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, stop: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [dir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"watch\",\n\t\t...(dir ? { dir } : {}),\n\t\tstop: parsed.values.stop === true,\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nexport function parsePluginUninstallCommand(argv: string[]): PluginUninstallCommand | undefined {\n\tif (argv[0] !== \"uninstall\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [pluginId, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\t// 省略 id 时按 cwd 推断,语义与 add . / watch 一致:站在哪个插件里就作用于哪个。\n\treturn { type: \"uninstall\", ...(pluginId ? { pluginId } : {}), json: parsed.values.json === true };\n}\n\nexport function parsePluginSyncCommand(argv: string[]): PluginSyncCommand | undefined {\n\tif (argv[0] !== \"sync\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, check: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"sync\", check: parsed.values.check === true, json: parsed.values.json === true };\n}\n\nasync function defaultRunAction(actionId: string, input: unknown): Promise<unknown> {\n\tconst client = createActionRpcClient(await readActionRpcEndpoint());\n\treturn client.run(actionId, input);\n}\n\nconst defaultDependencies: PluginCommandDependencies = {\n\tresolveNpmArchive: resolveNpmPluginArchive,\n\tcwd: () => process.cwd(),\n\trunAction: defaultRunAction,\n\twriteStdout: (value) => process.stdout.write(value),\n\twriteStderr: (value) => process.stderr.write(value),\n\treadLatestSdkVersion: () => readLatestNpmVersion(\"@vetta-org/plugin-sdk\"),\n};\n\nfunction isHttpUrl(source: string): boolean {\n\ttry {\n\t\tconst url = new URL(source);\n\t\treturn url.protocol === \"http:\" || url.protocol === \"https:\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction isLocalPackage(source: string): boolean {\n\tconst lower = source.toLowerCase();\n\tif (lower.endsWith(\".vettapkg\") || lower.endsWith(\".zip\")) return true;\n\tconst path = resolve(source);\n\t// 目录不是压缩包:它是一个插件工程,走 resolveProjectArchive 先找它打出来的产物。\n\treturn existsSync(path) && !statSync(path).isDirectory();\n}\n\nfunction isDirectorySource(source: string): boolean {\n\tconst path = resolve(source);\n\treturn existsSync(path) && statSync(path).isDirectory();\n}\n\n/**\n * 把「装当前这个工程」翻译成一个具体的归档路径。\n *\n * 这条路径是给 `install:vetta` 这类脚本用的:作者(或 Agent)在插件目录里跑一条命令就\n * 装进 Vetta,不必记住产物叫什么名字。找不到产物时给出该跑的那条命令,而不是报一个\n * 「文件不存在」让人自己猜。\n */\nfunction resolveProjectArchive(source: string): { archivePath: string; project: PluginProject } {\n\tconst from = resolve(source);\n\tconst project = findPluginProject(from);\n\tif (!project) {\n\t\tconst hub = findPluginHub(from);\n\t\tif (hub) {\n\t\t\tthrow new Error(\n\t\t\t\t`${from} indexes plugins but is not one itself. Run this from a plugin directory, or pass its path: vetta-plugin-cli add ./path/to/plugin`,\n\t\t\t);\n\t\t}\n\t\tthrow new Error(`No plugin.json found in ${from} or any parent directory.`);\n\t}\n\tconst archivePath = join(project.root, \"release\", `${project.pluginId}-${project.version}.vettapkg`);\n\tif (!existsSync(archivePath)) {\n\t\tthrow new Error(\n\t\t\t`Packaged archive not found: ${archivePath}\\nBuild it first: npm run build && npx vetta-plugin pack`,\n\t\t);\n\t}\n\treturn { archivePath, project };\n}\n\n/** 装完立刻检查索引是否还停在旧版本;不在市场仓库里时什么也不说。 */\nfunction indexDriftHint(project: PluginProject): string | undefined {\n\tconst hub = findPluginHub(project.root);\n\tif (!hub) return undefined;\n\treturn describeIndexDrift({\n\t\thubRoot: hub.root,\n\t\tmanifestPath: hub.manifestPath,\n\t\tslug: project.pluginId,\n\t\tversion: project.version,\n\t});\n}\n\nfunction npmInstallInput(resolved: ResolvedNpmPluginArchive): Record<string, unknown> {\n\treturn {\n\t\toperation: \"install-from-path\",\n\t\tinitiator: \"plugin-cli\",\n\t\tpath: resolved.archivePath,\n\t\tenable: true,\n\t\tsource: \"npm\",\n\t\texpectedSha256: resolved.expectedSha256,\n\t\texpectedId: resolved.packageManifest.vetta.pluginId,\n\t\texpectedVersion: resolved.packageManifest.version,\n\t\tnpm: {\n\t\t\tpackageName: resolved.packageManifest.name,\n\t\t\trequestedSpec: resolved.requestedSpec,\n\t\t\tresolvedVersion: resolved.packageManifest.version,\n\t\t\t...(resolved.integrity ? { integrity: resolved.integrity } : {}),\n\t\t},\n\t};\n}\n\nfunction resultSummary(result: unknown): string {\n\tif (typeof result !== \"object\" || result === null || Array.isArray(result)) return \"Plugin installed.\\n\";\n\tconst response = result as Record<string, unknown>;\n\tconst plugin =\n\t\ttypeof response.plugin === \"object\" && response.plugin !== null && !Array.isArray(response.plugin)\n\t\t\t? (response.plugin as Record<string, unknown>)\n\t\t\t: undefined;\n\tif (!plugin) return \"Plugin installed.\\n\";\n\tconst id = typeof plugin.id === \"string\" ? plugin.id : \"plugin\";\n\tconst version = typeof plugin.version === \"string\" ? `@${plugin.version}` : \"\";\n\tconst pending = typeof plugin.pendingVersion === \"string\"\n\t\t? ` Update ${plugin.pendingVersion} is pending reload. Run \\`vetta-plugin-cli reload ${id}\\` to apply it.`\n\t\t: \"\";\n\treturn `Installed ${id}${version}.${pending}\\n`;\n}\n\nfunction reloadResultSummary(result: unknown, requestedPluginId: string): string {\n\tif (typeof result !== \"object\" || result === null || Array.isArray(result)) {\n\t\treturn `Reloaded ${requestedPluginId}.\\n`;\n\t}\n\tconst response = result as Record<string, unknown>;\n\tconst plugin =\n\t\ttypeof response.plugin === \"object\" && response.plugin !== null && !Array.isArray(response.plugin)\n\t\t\t? (response.plugin as Record<string, unknown>)\n\t\t\t: undefined;\n\tconst id = typeof plugin?.id === \"string\" ? plugin.id : requestedPluginId;\n\tconst version = typeof plugin?.activeVersion === \"string\" ? `@${plugin.activeVersion}` : \"\";\n\treturn `Reloaded ${id}${version}.\\n`;\n}\n\nfunction isConnectionError(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst code = (error as NodeJS.ErrnoException).code;\n\treturn (\n\t\tcode === \"ENOENT\" ||\n\t\tcode === \"ECONNREFUSED\" ||\n\t\tcode === \"ECONNRESET\" ||\n\t\terror.message.includes(\"ECONNREFUSED\") ||\n\t\terror.message.includes(\"fetch failed\")\n\t);\n}\n\nexport async function runPluginAddCommand(\n\tcommand: PluginAddCommand,\n\tdependencies: PluginAddCommandDependencies = defaultDependencies,\n): Promise<number> {\n\treturn runPluginCommand(command, dependencies);\n}\n\nexport async function runPluginCommand(\n\tcommand: PluginCommand,\n\tdependencies: PluginCommandDependencies = defaultDependencies,\n): Promise<number> {\n\tif (command.type === \"help\") {\n\t\tdependencies.writeStdout(HELP_TEXT);\n\t\treturn 0;\n\t}\n\tif (command.type === \"error\") {\n\t\tdependencies.writeStderr(`${command.message}\\n`);\n\t\treturn 2;\n\t}\n\n\tif (command.type === \"docs\") {\n\t\treturn await runDocsCommand(command, dependencies);\n\t}\n\tif (command.type === \"init\") {\n\t\treturn runInitCommand(command, dependencies);\n\t}\n\tif (command.type === \"refresh-guide\") {\n\t\treturn runRefreshGuideCommand(command, dependencies);\n\t}\n\tif (command.type === \"init-hub\") {\n\t\treturn runInitHubCommand(command, dependencies);\n\t}\n\tif (command.type === \"sync\") {\n\t\treturn runSyncCommand(command, dependencies);\n\t}\n\n\tif (command.type === \"watch\") {\n\t\treturn runWatchCommand(command, dependencies);\n\t}\n\tif (command.type === \"uninstall\") {\n\t\treturn runUninstallCommand(command, dependencies);\n\t}\n\n\tlet resolvedNpm: ResolvedNpmPluginArchive | undefined;\n\tlet driftHint: string | undefined;\n\ttry {\n\t\tlet result: unknown;\n\t\tif (command.type === \"reload\") {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"reload\",\n\t\t\t\tid: command.pluginId,\n\t\t\t});\n\t\t} else if (isHttpUrl(command.source)) {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-url\",\n\t\t\t\tinitiator: \"plugin-cli\",\n\t\t\t\turl: command.source,\n\t\t\t});\n\t\t} else if (isDirectorySource(command.source)) {\n\t\t\tconst { archivePath, project } = resolveProjectArchive(command.source);\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-path\",\n\t\t\t\tinitiator: \"plugin-cli\",\n\t\t\t\tpath: archivePath,\n\t\t\t\tenable: true,\n\t\t\t});\n\t\t\tdriftHint = indexDriftHint(project);\n\t\t} else if (isLocalPackage(command.source)) {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-path\",\n\t\t\t\tinitiator: \"plugin-cli\",\n\t\t\t\tpath: resolve(command.source),\n\t\t\t\tenable: true,\n\t\t\t});\n\t\t} else {\n\t\t\tresolvedNpm = await dependencies.resolveNpmArchive(command.source);\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", npmInstallInput(resolvedNpm));\n\t\t}\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, result, ...(driftHint ? { warning: driftHint } : {}) })}\\n`\n\t\t\t\t: command.type === \"reload\"\n\t\t\t\t\t? reloadResultSummary(result, command.pluginId)\n\t\t\t\t\t: `${resultSummary(result)}${driftHint ? `${driftHint}\\n` : \"\"}`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : command.type === \"reload\" ? \"PLUGIN_RELOAD_FAILED\" : \"PLUGIN_ADD_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t} finally {\n\t\tawait resolvedNpm?.cleanup();\n\t}\n}\n\n/**\n * 打印随 SDK 发布的手册目录。\n *\n * 存在的理由是「不要让任何人硬编码 node_modules 路径」:工作区会把依赖提升到仓库根,\n * 一仓多插件的 hub 里每个插件也可能各装一份。Agent 只需记住这一条命令,拿回来的永远是\n * 当前工程实际编译所针对的那个 SDK 版本的手册。\n */\nasync function runDocsCommand(\n\tcommand: { json: boolean; checkLatest: boolean },\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst manualDir = resolveManualDir(cwd);\n\tif (!manualDir) {\n\t\t// 能力市场仓库的根目录通常没装 SDK,手册在各能力目录里。直接说「装 SDK」会把人引到\n\t\t// 仓库根去装一份用不上的依赖。\n\t\tconst inHubRoot = findPluginHub(cwd) !== undefined && findPluginProject(cwd) === undefined;\n\t\tconst message = inHubRoot\n\t\t\t? \"Plugin manual not found at the hub root. cd into an ability directory (abilities/plugins/<slug>), then run npm install.\\n\"\n\t\t\t: \"Plugin manual not found. Install the SDK first: npm i -D @vetta-org/plugin-sdk\\n\";\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: \"MANUAL_NOT_FOUND\", message: message.trim() } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(message);\n\t\t}\n\t\treturn 6;\n\t}\n\tconst project = findPluginProject(cwd);\n\tconst hub = findPluginHub(cwd);\n\tconst sdkVersion = readManualSdkVersion(manualDir);\n\tconst latestVersion = command.checkLatest ? await dependencies.readLatestSdkVersion?.() : undefined;\n\tconst outdated = sdkVersion !== undefined && latestVersion !== undefined && compareSemver(sdkVersion, latestVersion) < 0;\n\tconst guide = inspectAgentsGuide(project?.root ?? hub?.root ?? cwd);\n\n\tif (command.json) {\n\t\tdependencies.writeStdout(\n\t\t\t`${JSON.stringify({\n\t\t\t\tok: true,\n\t\t\t\tmanualDir,\n\t\t\t\tentry: join(manualDir, \"README.md\"),\n\t\t\t\tsdkVersion,\n\t\t\t\trefreshCommand: SDK_REFRESH_COMMAND,\n\t\t\t\tguide,\n\t\t\t\t...(command.checkLatest ? { latestVersion, outdated } : {}),\n\t\t\t\tproject: project ? { root: project.root, pluginId: project.pluginId, version: project.version } : undefined,\n\t\t\t\thub: hub\n\t\t\t\t\t? {\n\t\t\t\t\t\t\troot: hub.root,\n\t\t\t\t\t\t\tmanifestPath: hub.manifestPath,\n\t\t\t\t\t\t\tsyncHint: \"After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t})}\\n`,\n\t\t);\n\t\treturn 0;\n\t}\n\tconst lines = [\n\t\t`Plugin manual (@vetta-org/plugin-sdk${sdkVersion ? `@${sdkVersion}` : \"\"}):`,\n\t\t` ${manualDir}`,\n\t\t`Start here: ${join(manualDir, \"README.md\")}`,\n\t];\n\tif (project) lines.push(`Current plugin: ${project.pluginId} (${project.root})`);\n\tif (outdated) {\n\t\tlines.push(`Manual is behind: ${sdkVersion} → ${latestVersion}. Refresh it with: ${SDK_REFRESH_COMMAND}`);\n\t} else if (command.checkLatest && latestVersion === undefined) {\n\t\tlines.push(`Could not reach the registry; cannot tell whether ${sdkVersion ?? \"this manual\"} is current.`);\n\t} else {\n\t\t// 手册是随 SDK 装进 node_modules 的快照,工程不升级它就永远停在初始化那天的版本。\n\t\t// 这条命令必须每次都打印:读到它的 Agent 手上的 AGENTS.md 往往也是同一天的快照。\n\t\tlines.push(`Manual follows the installed SDK. To refresh it: ${SDK_REFRESH_COMMAND}`);\n\t}\n\tif (guide.stale) {\n\t\t// 说明书同样是快照,而且用户没有理由回头看它。这里是唯一会被读到的位置。\n\t\tlines.push(\n\t\t\t`This brief is stale (AGENTS.md revision ${guide.revision} < ${AGENTS_GUIDE_REVISION}). Refresh it with: ${GUIDE_REFRESH_COMMAND}`,\n\t\t);\n\t} else if (guide.unstamped) {\n\t\t// 没有版本戳的文件与手写内容无从区分——能力市场仓库的根 AGENTS.md 往往是一整本手写的\n\t\t// 市场规范。绝不能把它引导成一条覆盖命令。\n\t\tlines.push(\n\t\t\t`AGENTS.md has no revision marker, so it looks hand-written. Review the current template with \\`${GUIDE_REFRESH_COMMAND} --dry-run\\` and merge by hand; do not overwrite it blindly.`,\n\t\t);\n\t}\n\tif (hub) {\n\t\tlines.push(`Marketplace index: ${hub.manifestPath}`);\n\t\t// Agent 几乎一定会先跑 docs,所以这是告诉它「索引要对账」的最佳时机。\n\t\tlines.push(\"After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.\");\n\t}\n\tdependencies.writeStdout(`${lines.join(\"\\n\")}\\n`);\n\treturn 0;\n}\n\n/**\n * 刷新手册的命令。\n *\n * 手册不从网络现取,而是随 SDK 进 `node_modules`——Agent 读到的合同因此与工程实际编译的\n * 版本一致。代价是它不会自己变新,所以「怎么变新」必须由 CLI 每次说一遍:`npx` 默认取最新的\n * CLI,它的输出是这条链路上唯一不会过期的位置。\n */\nconst SDK_REFRESH_COMMAND = \"npm i -D @vetta-org/plugin-sdk@latest && npx vetta-plugin-cli docs\";\n\n/** 刷新说明书的命令。与手册各刷各的:一个随 SDK 走,一个随 CLI 走。 */\nconst GUIDE_REFRESH_COMMAND = \"npx @vetta-org/plugin-cli init --refresh-guide\";\n\nexport interface AgentsGuideStatus {\n\t/** 本工程有没有 AGENTS.md。 */\n\treadonly present: boolean;\n\t/** 读到的版本戳;没有戳(模板早于版本戳,或是手写的)时缺省。 */\n\treadonly revision?: number;\n\t/**\n\t * **带戳**且落后于当前 CLI 的模板——只有这一档能安全地一键重写。\n\t *\n\t * 没有 AGENTS.md 时为 false(那是「没有」,不是「旧」);没有戳时也为 false,见 {@link unstamped}。\n\t */\n\treadonly stale: boolean;\n\t/** 有文件但没有版本戳:可能是手写的,也可能是版本戳之前的模板,无从区分。 */\n\treadonly unstamped: boolean;\n}\n\n/**\n * 判断工程里的 AGENTS.md 是不是旧模板。\n *\n * 说明书凝固在 `init` 那天,而用户没有理由回头看它——所以「它旧了」这件事只能由每次都会被\n * 跑到的 `docs` 说出来。\n *\n * **没有版本戳的不算「旧」,只算「来路不明」。** 早先把它判成 stale 并引导去跑刷新命令,等于\n * 教用户覆盖自己手写的文件——能力市场仓库的根 `AGENTS.md` 常常是一整本手写的市场规范。\n */\nfunction inspectAgentsGuide(root: string): AgentsGuideStatus {\n\tconst path = join(root, \"AGENTS.md\");\n\tif (!existsSync(path)) return { present: false, stale: false, unstamped: false };\n\tlet revision: number | undefined;\n\ttry {\n\t\trevision = readAgentsGuideRevision(readFileSync(path, \"utf8\"));\n\t} catch {\n\t\treturn { present: true, stale: false, unstamped: false };\n\t}\n\treturn {\n\t\tpresent: true,\n\t\t...(revision === undefined ? {} : { revision }),\n\t\tstale: revision !== undefined && revision < AGENTS_GUIDE_REVISION,\n\t\tunstamped: revision === undefined,\n\t};\n}\n\n/** 够用的 semver 比较:只看 major.minor.patch,预发布后缀一律当作小于正式版。 */\nfunction compareSemver(left: string, right: string): number {\n\tconst parse = (value: string): readonly [number, number, number, boolean] => {\n\t\tconst match = /^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$/.exec(value.trim());\n\t\tif (!match) return [0, 0, 0, false];\n\t\treturn [Number(match[1]), Number(match[2]), Number(match[3]), match[4] !== undefined];\n\t};\n\tconst a = parse(left);\n\tconst b = parse(right);\n\tfor (let index = 0; index < 3; index += 1) {\n\t\tif (a[index] !== b[index]) return a[index]! < b[index]! ? -1 : 1;\n\t}\n\tif (a[3] === b[3]) return 0;\n\treturn a[3] ? -1 : 1;\n}\n\n/**\n * 在已有工程里把 AGENTS.md 重写成当前 CLI 的版本。\n *\n * `init` 拒绝覆盖已有工程,所以老目录里那份说明书从落地起就停在原地。它是纯派生产物,重写\n * 它不会碰用户写过的任何东西——这也是唯一一个能这么做的脚手架文件。\n */\nfunction runRefreshGuideCommand(\n\tcommand: Extract<PluginCommand, { type: \"refresh-guide\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = refreshAgentsGuide(resolve(cwd, command.targetDir ?? \".\"), {\n\t\t\tforce: command.force,\n\t\t\tdryRun: command.dryRun,\n\t\t});\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: true, ...result })}\\n`);\n\t\t\treturn 0;\n\t\t}\n\t\tdependencies.writeStdout(\n\t\t\tresult.written\n\t\t\t\t? `Rewrote ${result.file}\\nNext: npx vetta-plugin-cli docs --check-latest\\n`\n\t\t\t\t// dry-run 把正文直接吐到 stdout,人工合并时可以重定向成文件再 diff。\n\t\t\t\t: `${result.content}`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: \"GUIDE_REFRESH_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 7;\n\t}\n}\n\n/** 在陌生目录里生成一个可直接开工的插件工程,并留下让任意 Agent 自举的 AGENTS.md。 */\nfunction runInitCommand(\n\tcommand: Extract<PluginCommand, { type: \"init\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = initPluginProject({\n\t\t\ttargetDir: resolve(cwd, command.targetDir ?? command.pluginId),\n\t\t\tpluginId: command.pluginId,\n\t\t\tdisplayName: command.displayName ?? command.pluginId,\n\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: [\n\t\t\t\t\t\t`Created ${result.pluginId} at ${result.root}`,\n\t\t\t\t\t\t\"Next: npm install && npm run install:vetta\",\n\t\t\t\t\t\t\"The agent brief is in AGENTS.md; after npm install, run `npx vetta-plugin-cli docs` for the manual.\",\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t\t.concat(\"\\n\"),\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"PLUGIN_INIT_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\n/**\n * 让宿主改从工程目录加载本插件,之后改源码即时生效,不必每次 build → pack → install。\n *\n * 目标插件按 cwd 向上找,理由同 `add .`:一仓多插件时「我正站在哪个插件里」是唯一不会\n * 弄错的意图,而 id 靠人重复输入迟早会错配到另一个插件上。\n */\nasync function runWatchCommand(\n\tcommand: Extract<PluginCommand, { type: \"watch\" }>,\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst from = resolve(cwd, command.dir ?? \".\");\n\ttry {\n\t\tconst project = findPluginProject(from);\n\t\tif (!project) {\n\t\t\tconst hub = findPluginHub(from);\n\t\t\tthrow new Error(\n\t\t\t\thub\n\t\t\t\t\t? `${from} indexes plugins but is not one itself. Run this from a plugin directory, or pass its path.`\n\t\t\t\t\t: `No plugin.json found in ${from} or any parent directory.`,\n\t\t\t);\n\t\t}\n\t\tconst result = command.stop\n\t\t\t? await dependencies.runAction(\"plugins.manage\", { operation: \"dev-watch-stop\", id: project.pluginId })\n\t\t\t: await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\t\toperation: \"dev-watch\",\n\t\t\t\t\tid: project.pluginId,\n\t\t\t\t\tprojectDir: project.root,\n\t\t\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, result })}\\n`\n\t\t\t\t: command.stop\n\t\t\t\t\t? `Stopped hot reload for ${project.pluginId}.\\n`\n\t\t\t\t\t: `Hot reload on for ${project.pluginId}. Vetta now loads it from ${project.root}.\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : \"PLUGIN_WATCH_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t}\n}\n\n/**\n * 卸载一个插件。省略 id 时按 cwd 推断,语义与 `add .` / `watch` 一致。\n *\n * 刻意不在这里做二次确认:宿主自己会为写操作弹审批,CLI 再问一遍只是噪音。系统插件由\n * 宿主拒绝,这里不重复判断——那份名单不该有第二个真相源。\n */\nasync function runUninstallCommand(\n\tcommand: Extract<PluginCommand, { type: \"uninstall\" }>,\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tlet pluginId = command.pluginId;\n\t\tif (!pluginId) {\n\t\t\tconst project = findPluginProject(cwd);\n\t\t\tif (!project) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No plugin.json found in ${cwd} or any parent directory. Pass the id: vetta-plugin-cli uninstall <plugin-id>`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tpluginId = project.pluginId;\n\t\t}\n\t\tconst result = await dependencies.runAction(\"plugins.manage\", { operation: \"uninstall\", id: pluginId });\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json ? `${JSON.stringify({ ok: true, result })}\\n` : `Uninstalled ${pluginId}.\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : \"PLUGIN_UNINSTALL_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t}\n}\n\n/**\n * 对账能力市场索引。定位靠向上找 `.vetta/marketplace.json`,因此在仓库任何位置都能跑。\n *\n * `--check` 只报不写并以非零退出,给 CI 用:索引漂移的三种后果里,两种不在作者机器上复现,\n * 一种压根不报错,光靠人自觉看不住。\n */\nfunction runSyncCommand(\n\tcommand: Extract<PluginCommand, { type: \"sync\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst hub = findPluginHub(cwd);\n\tif (!hub) {\n\t\tconst message = `No .vetta/marketplace.json found in ${cwd} or any parent directory. sync is for marketplace repositories.\\n`;\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"HUB_NOT_FOUND\", message: message.trim() } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(message);\n\t\t}\n\t\treturn 6;\n\t}\n\ttry {\n\t\tconst result = syncMarketplaceIndex({ hubRoot: hub.root, manifestPath: hub.manifestPath, apply: !command.check });\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: result.problems.length === 0, ...result })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStdout(formatSyncReport(result, command.check));\n\t\t}\n\t\tif (result.problems.length > 0) return 7;\n\t\t// --check 的职责就是「有漂移就红」,否则 CI 拦不住任何东西。\n\t\treturn command.check && result.changes.length > 0 ? 7 : 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"SYNC_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\nfunction formatSyncReport(result: ReturnType<typeof syncMarketplaceIndex>, check: boolean): string {\n\tconst lines: string[] = [];\n\tfor (const change of result.changes) {\n\t\tlines.push(` ${change.slug}: ${change.field} ${JSON.stringify(change.from)} -> ${JSON.stringify(change.to)}`);\n\t}\n\tif (lines.length > 0) {\n\t\tlines.unshift(check ? \"Index is out of date:\" : \"Updated the index:\");\n\t}\n\tif (result.problems.length > 0) {\n\t\tlines.push(\"Problems:\");\n\t\tfor (const problem of result.problems) lines.push(` ${problem.slug}: ${problem.message}`);\n\t}\n\tif (result.unlisted.length > 0) {\n\t\tlines.push(\"Ability directories not listed in the index (add them by hand when ready to publish):\");\n\t\tfor (const dir of result.unlisted) lines.push(` ${dir}`);\n\t}\n\tif (lines.length === 0) return \"Index is in sync.\\n\";\n\tif (check && result.changes.length > 0) lines.push(\"Run `vetta-plugin-cli sync` to apply.\");\n\treturn `${lines.join(\"\\n\")}\\n`;\n}\n\n/** 生成一个合规的能力市场仓库骨架,连同仓库级 AGENTS.md 与对账用的 CI。 */\nfunction runInitHubCommand(\n\tcommand: Extract<PluginCommand, { type: \"init-hub\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = initHubRepository({\n\t\t\ttargetDir: resolve(cwd, command.targetDir ?? command.name),\n\t\t\tname: command.name,\n\t\t\trepository: command.repository,\n\t\t\tminAppVersion: command.minAppVersion,\n\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: [\n\t\t\t\t\t\t`Created marketplace ${result.name} at ${result.root}`,\n\t\t\t\t\t\t\"Add an ability: npx @vetta-org/plugin-cli init --id <slug> --name \\\"<Display>\\\" abilities/plugins/<slug>\",\n\t\t\t\t\t\t\"Then list it in .vetta/marketplace.json and run: npx @vetta-org/plugin-cli sync\",\n\t\t\t\t\t\t\"The working agreement for agents is in AGENTS.md.\",\n\t\t\t\t\t].join(\"\\n\") + \"\\n\",\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"HUB_INIT_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\nexport async function runPluginCli(argv: string[]): Promise<number> {\n\tif (argv.length === 0 || argv[0] === \"-h\" || argv[0] === \"--help\") {\n\t\treturn runPluginAddCommand({ type: \"help\" });\n\t}\n\tconst command = parsePluginAddCommand(argv) ?? parsePluginReloadCommand(argv) ?? parsePluginDocsCommand(argv) ?? parsePluginInitCommand(argv) ?? parsePluginWatchCommand(argv) ?? parsePluginUninstallCommand(argv) ?? parsePluginSyncCommand(argv);\n\tif (!command) {\n\t\tprocess.stderr.write(`Unknown command: ${argv[0]}\\n`);\n\t\treturn 2;\n\t}\n\treturn runPluginCommand(command);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"hub-template.d.ts","sourceRoot":"","sources":["../src/hub-template.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAoEpE;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAmB1C;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAYnF","sourcesContent":["import { AGENTS_GUIDE_REVISION } from \"./agents-template.js\";\n\n/**\n * 能力市场仓库的骨架。\n *\n * 手写一个合规的 hub 成本不低:`.vetta/marketplace.json` 的必填字段、目录约定、以及那几条\n * 只在别人机器上复现的发布约束,都得先读一遍文档才知道。这里把它变成一条命令。\n *\n * 仓库级 `AGENTS.md` 是关键的一半:落在仓库根的 Agent 需要知道「能力目录才是开发单位、\n * 索引由 sync 对账」,否则它会去手改 marketplace.json。\n */\n\nexport function renderHubAgentsGuide(input: { name: string }): string {\n\treturn `<!-- vetta-guide-revision: ${AGENTS_GUIDE_REVISION} -->\n# ${input.name}\n\nVetta 能力市场仓库。本仓库索引若干**能力**(plugin / mcp / skill / scene / bundle),\n每个能力是 \\`abilities/\\` 下的一个自包含目录。\n\n## 开发时站在能力目录里,不是站在这里\n\n> 仓库根没有 \\`node_modules\\`,所以在根上执行时用全名 \\`@vetta-org/plugin-cli\\`;进了能力目录、\n> \\`npm install\\` 之后,裸命令 \\`vetta-plugin-cli\\` 才在 \\`node_modules/.bin\\` 里。\n\n\\`\\`\\`bash\ncd abilities/plugins/<slug> # ← 开发单位是这个目录\nnpm install\nnpx vetta-plugin-cli docs --check-latest # 手册(随该目录装的 SDK 版本;顺带查是否落后)\nnpm run install:vetta # 装进正在运行的 Vetta\nnpx vetta-plugin-cli watch # 热更新\n\\`\\`\\`\n\n每个插件目录自带 \\`AGENTS.md\\`,里面有该读哪些手册、以及不可违反的几条。**先 \\`cd\\` 进去再动手**:\n所有开发命令都作用于「最近的那个能力目录」,站在仓库根上它们不知道你指的是哪一个。\n\n手册与各目录的 \\`AGENTS.md\\` 都是那次 \\`init\\` 当天的快照,各能力还可能钉着不同的 SDK 版本。\n动手前先跑 \\`docs --check-latest\\`,它的输出永远比这两份文件新,冲突时以它为准。\n\n新建一个插件:\n\n\\`\\`\\`bash\nnpx @vetta-org/plugin-cli init --id <slug> --name \"<Display Name>\" abilities/plugins/<slug>\n\\`\\`\\`\n\n它只创建目录,**不会**动索引——新能力什么时候上架是人的决定。想好了再按下面的方式登记。\n\n## 索引由工具对账,不要手改派生字段\n\n\\`.vetta/marketplace.json\\` 里能力的 \\`version\\`、\\`config.api_version\\`、\\`config.permissions\\`、\n\\`config.commands\\` 全都是从能力包推导出来的。改完能力后:\n\n\\`\\`\\`bash\nnpx @vetta-org/plugin-cli sync # 回填派生字段,并推进 marketplaceVersion\nnpx @vetta-org/plugin-cli sync --check # 只报不写,非零退出(CI 用)\n\\`\\`\\`\n\n要**手写**的只有身份与展示:\\`slug\\`、\\`name\\`、\\`description\\`、\\`source.path\\`、\\`category\\`、\\`tags\\`、\n\\`detail\\`。新能力上架时手动加一条这样的条目,其余字段交给 \\`sync\\`。\n\n三条容易踩的约束,\\`sync --check\\` 会替你守住:\n\n| 约束 | 漏了会怎样 |\n| --- | --- |\n| 条目 \\`version\\` 必须 == 能力包里的版本 | 宿主同步**直接失败** |\n| \\`plugin.json\\` 的 \\`entry\\` / \\`styles\\` 必须在已发布目录里真实存在 | 本地能装,市场上装不了 |\n| 改了任何内容必须换 \\`marketplaceVersion\\` | 客户端不报错、也不更新,用户永远收不到 |\n\n第三条最阴险——它不报错。\n\n## 为什么插件目录里要提交 \\`dist/\\`\n\n客户端按 \\`source.path\\` 直接读目录并安装,**它不会替你构建**。所以构建产物必须在仓库里。\n脚手架生成的插件目录因此不忽略 \\`dist/\\`。\n\n## 发布\n\n1. 改能力 → 在能力目录里 build\n2. 回仓库根 \\`npx @vetta-org/plugin-cli sync\\`\n3. 提交并推送;客户端在 \\`marketplaceVersion\\` 变化时拉新快照\n`;\n}\n\nexport function renderHubWorkflow(): string {\n\treturn `name: marketplace\n\non:\n pull_request:\n push:\n branches: [main]\n\njobs:\n index:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n # 索引与能力包漂移的后果有两种不在作者机器上复现、一种压根不报错,所以在这里拦。\n - run: npx --yes @vetta-org/plugin-cli sync --check\n`;\n}\n\nexport function renderHubReadme(input: { name: string; repository: string }): string {\n\treturn `# ${input.name}\n\nA Vetta ability marketplace. Add it in Vetta Desktop under **能力市场 → 添加来源**:\n\n\\`\\`\\`\n${input.repository}\n\\`\\`\\`\n\nAbilities live under \\`abilities/\\`. The index is \\`.vetta/marketplace.json\\`; its derived fields are\nmaintained by \\`npx @vetta-org/plugin-cli sync\\`. See \\`AGENTS.md\\` for the working agreement.\n`;\n}\n"]}
1
+ {"version":3,"file":"hub-template.d.ts","sourceRoot":"","sources":["../src/hub-template.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA6EpE;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAmB1C;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAYnF","sourcesContent":["import { AGENTS_GUIDE_REVISION } from \"./agents-template.js\";\n\n/**\n * 能力市场仓库的骨架。\n *\n * 手写一个合规的 hub 成本不低:`.vetta/marketplace.json` 的必填字段、目录约定、以及那几条\n * 只在别人机器上复现的发布约束,都得先读一遍文档才知道。这里把它变成一条命令。\n *\n * 仓库级 `AGENTS.md` 是关键的一半:落在仓库根的 Agent 需要知道「能力目录才是开发单位、\n * 索引由 sync 对账」,否则它会去手改 marketplace.json。\n */\n\nexport function renderHubAgentsGuide(input: { name: string }): string {\n\treturn `<!-- vetta-guide-revision: ${AGENTS_GUIDE_REVISION} -->\n# ${input.name}\n\nVetta 能力市场仓库。本仓库索引若干**能力**(plugin / mcp / skill / scene / bundle),\n每个能力是 \\`abilities/\\` 下的一个自包含目录。\n\n## 开发时站在能力目录里,不是站在这里\n\n> 仓库根没有 \\`node_modules\\`,所以在根上执行时用全名 \\`@vetta-org/plugin-cli\\`;进了能力目录、\n> \\`npm install\\` 之后,裸命令 \\`vetta-plugin-cli\\` 才在 \\`node_modules/.bin\\` 里。\n\n\\`\\`\\`bash\ncd abilities/plugins/<slug> # ← 开发单位是这个目录\nnpm install\nnpx vetta-plugin-cli docs --check-latest # 手册(随该目录装的 SDK 版本;顺带查是否落后)\nnpm run install:vetta # 装进正在运行的 Vetta\nnpx vetta-plugin-cli watch # 热更新\n\\`\\`\\`\n\n每个插件目录自带 \\`AGENTS.md\\`,里面有该读哪些手册、以及不可违反的几条。**先 \\`cd\\` 进去再动手**:\n所有开发命令都作用于「最近的那个能力目录」,站在仓库根上它们不知道你指的是哪一个。\n\n手册与各目录的 \\`AGENTS.md\\` 都是那次 \\`init\\` 当天的快照,各能力还可能钉着不同的 SDK 版本。\n动手前先跑 \\`docs --check-latest\\`,它的输出永远比这两份文件新,冲突时以它为准。\n\n新建一个插件:\n\n\\`\\`\\`bash\nnpx @vetta-org/plugin-cli init --id <slug> --name \"<Display Name>\" abilities/plugins/<slug>\n\\`\\`\\`\n\n它只创建目录,**不会**动索引——新能力什么时候上架是人的决定。想好了再按下面的方式登记。\n\n## 索引由工具对账,不要手改派生字段\n\n改完能力后:\n\n\\`\\`\\`bash\nnpx @vetta-org/plugin-cli sync # 对账并回填,看输出\nnpx @vetta-org/plugin-cli sync --check # 只报不写,非零退出(CI 用)\n\\`\\`\\`\n\n\\`sync\\` 到底做什么,分三档看清楚——它不是万能的:\n\n| 字段 | \\`sync\\` 的行为 |\n| --- | --- |\n| 条目 \\`version\\` | **回填**成能力包里的版本 |\n| \\`marketplaceVersion\\` | 内容有变化时**尝试**推进。只认 semver(\\`1.2.3\\`)和纯整数;\\`YYYY.MM.DD-NN\\` 之类的格式推不动,会报出来要你手改 |\n| \\`config.api_version\\` / \\`config.permissions\\` / \\`config.commands\\` | **不写**。宿主建目录时用 \\`plugin.json\\` 整个重算 \\`config\\`,索引里的副本读都不读。已经存在且与真源不符时 \\`sync\\` 会提醒你删掉或改对 |\n\n所以 \\`sync\\` 跑完要看输出:它报出来的问题(尤其是推不动的 \\`marketplaceVersion\\`)没人会替你处理。\n\n要**手写**的只有身份与展示:\\`slug\\`、\\`name\\`、\\`description\\`、\\`source.path\\`、\\`category\\`、\\`tags\\`、\n\\`detail\\`。新能力上架时手动加一条这样的条目。\n\n三条容易踩的约束,\\`sync --check\\` 会替你守住:\n\n| 约束 | 漏了会怎样 |\n| --- | --- |\n| 条目 \\`version\\` 必须 == 能力包里的版本 | 宿主同步**直接失败** |\n| \\`plugin.json\\` 的 \\`entry\\` / \\`styles\\` 必须在已发布目录里真实存在 | 本地能装,市场上装不了 |\n| 改了任何内容必须换 \\`marketplaceVersion\\` | 客户端不报错、也不更新,用户永远收不到 |\n\n第三条最阴险——它不报错。\\`sync\\` 只在版本号是 semver 或整数时才推得动,其余格式要你自己换。\n\n## 为什么插件目录里要提交 \\`dist/\\`\n\n客户端按 \\`source.path\\` 直接读目录并安装,**它不会替你构建**。所以构建产物必须在仓库里。\n脚手架生成的插件目录因此不忽略 \\`dist/\\`。\n\n## 发布\n\n1. 改能力 → 在能力目录里 build\n2. 回仓库根 \\`npx @vetta-org/plugin-cli sync\\`\n3. 提交并推送;客户端在 \\`marketplaceVersion\\` 变化时拉新快照\n`;\n}\n\nexport function renderHubWorkflow(): string {\n\treturn `name: marketplace\n\non:\n pull_request:\n push:\n branches: [main]\n\njobs:\n index:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n # 索引与能力包漂移的后果有两种不在作者机器上复现、一种压根不报错,所以在这里拦。\n - run: npx --yes @vetta-org/plugin-cli sync --check\n`;\n}\n\nexport function renderHubReadme(input: { name: string; repository: string }): string {\n\treturn `# ${input.name}\n\nA Vetta ability marketplace. Add it in Vetta Desktop under **能力市场 → 添加来源**:\n\n\\`\\`\\`\n${input.repository}\n\\`\\`\\`\n\nAbilities live under \\`abilities/\\`. The index is \\`.vetta/marketplace.json\\`; its derived fields are\nmaintained by \\`npx @vetta-org/plugin-cli sync\\`. See \\`AGENTS.md\\` for the working agreement.\n`;\n}\n"]}