@vetta-org/plugin-cli 0.1.5 → 0.1.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  All notable changes to `@vetta-org/plugin-cli` are documented in this file.
4
4
 
5
+ ## [0.1.6] — 2026-09-14
6
+
7
+ ### Fixed
8
+
9
+ - **`init --refresh-guide` 不再覆盖手写的 `AGENTS.md`**。此前它无条件重写,而 `docs` 又把「没有
10
+ 版本戳」判成过期并给出这条命令——两者合起来是在引导用户删掉自己的文件。已知造成一个能力市场
11
+ 仓库根部 443 行手写市场规范被整份替换。
12
+
13
+ 没有 `vetta-guide-revision` 标记的文件现在一律拒绝覆盖(退出码 7),要覆盖得显式 `--force`;
14
+ 新增 `--dry-run` 把新模板打到 stdout 供人工合并。`docs` 对无标记文件改口为「看起来是手写的,
15
+ 请手动合并」,只有**带标记且落后**的才会被称作 stale 并给出刷新命令。
16
+
17
+ - 说明书标题不再印出未解析的本地化占位符。`plugin.json` 的 `name` 写成 `%plugin.name%` 时,按
18
+ `defaultLocale` 从 `locales/` 解析;解析不到退回插件 id。
19
+
20
+ - 说明书的命令清单改为读 `package.json` 的 `scripts`,只列真实存在的。老工程和自定义工程未必有
21
+ `dev` / `install:vetta`,照着跑只会得到一句 "Missing script";没有 `install:vetta` 时改列
22
+ `vetta-plugin-cli add .`。
23
+
24
+ - 更正 hub `AGENTS.md` 对 `sync` 的两处描述:`marketplaceVersion` 只在 semver 或纯整数时才推得动
25
+ (`YYYY.MM.DD-NN` 这类会报出来要手改),`config.api_version` / `permissions` / `commands`
26
+ **不回填**——宿主用 `plugin.json` 整个重算 `config`,`sync` 只在副本与真源不符时提醒删掉。
27
+
5
28
  ## [0.1.5] — 2026-09-14
6
29
 
7
30
  ### Changed
package/README.md CHANGED
@@ -28,6 +28,11 @@ is purely derived and holds no user content — leaving source, manifest and con
28
28
  the id and display name from the `plugin.json` already on disk. At a marketplace root it rewrites the
29
29
  hub brief instead.
30
30
 
31
+ It refuses to overwrite a brief that carries no `vetta-guide-revision` marker — that file is
32
+ indistinguishable from a hand-written one, and a marketplace root often holds a hand-written spec.
33
+ Use `--dry-run` to print the current template for manual merging, or `--force` to replace the file
34
+ outright.
35
+
31
36
  You will rarely have to remember this: the brief carries a revision stamp, and `docs` compares it on
32
37
  every run and says outright when it is behind. The brief itself holds no rules — those live in the
33
38
  manual, which travels with the SDK — so it changes seldom, and a project that refreshes once keeps
@@ -19,11 +19,13 @@
19
19
  * `docs` 读它来判断一份说明书是不是旧的——没有这个戳,「该不该刷新」就只能靠人记得,而这
20
20
  * 恰恰是它凝固的原因。**改动模板内容时必须一并推进它**,否则存量工程不会收到提示。
21
21
  */
22
- export declare const AGENTS_GUIDE_REVISION = 2;
22
+ export declare const AGENTS_GUIDE_REVISION = 3;
23
23
  /** 从一份 AGENTS.md 正文里读出版本戳;不是本模板生成的(或早于版本戳)时返回 undefined。 */
24
24
  export declare function readAgentsGuideRevision(content: string): number | undefined;
25
25
  export declare function renderAgentsGuide(input: {
26
26
  pluginId: string;
27
27
  displayName: string;
28
+ /** 工程 package.json 里实际存在的 script 名;缺省按脚手架的那套算。 */
29
+ scripts?: readonly string[];
28
30
  }): string;
29
31
  //# sourceMappingURL=agents-template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents-template.d.ts","sourceRoot":"","sources":["../src/agents-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,4HAA4D;AAC5D,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAwD1F","sourcesContent":["/**\n * 随脚手架落地的 Agent 说明书。\n *\n * 它替代的是「把开发知识写死在工作台插件的 skill 里」那套做法:那份 skill 只有 Vetta\n * 自己的 Agent 读得到,而且每加一个能力就要改一次、还要等 App 发版才到用户手里。这里\n * 反过来——工程自带说明书,说明书只说「去哪读手册」,手册随 SDK 版本进 node_modules。\n * 因此任何 Agent(Claude Code、Cursor、Vetta 自己)在任何陌生目录都能自举,而且读到的\n * 永远是这个工程实际编译所针对的那份合同。\n *\n * **这里刻意只留指引,不留知识。** 写进这个文件的每一条规则都会在所有存量工程里就地凝固:\n * 它是 `init` 当天的快照,之后既不会自更新,用户也没有理由回来看它。规则属于手册——手册\n * 随 SDK 升级一起到位。这个文件越薄,需要回头迁移老仓库的理由就越少。\n *\n * 单位是插件目录本身:外面是能力市场仓库、是单插件仓库、还是一堆别的东西,都不影响这里。\n */\n\n/**\n * 说明书的版本戳。\n *\n * `docs` 读它来判断一份说明书是不是旧的——没有这个戳,「该不该刷新」就只能靠人记得,而这\n * 恰恰是它凝固的原因。**改动模板内容时必须一并推进它**,否则存量工程不会收到提示。\n */\nexport const AGENTS_GUIDE_REVISION = 2;\n\n/** 从一份 AGENTS.md 正文里读出版本戳;不是本模板生成的(或早于版本戳)时返回 undefined。 */\nexport function readAgentsGuideRevision(content: string): number | undefined {\n\tconst match = /<!--\\s*vetta-guide-revision:\\s*(\\d+)\\s*-->/.exec(content);\n\tif (!match) return undefined;\n\treturn Number(match[1]);\n}\n\nexport function renderAgentsGuide(input: { pluginId: string; displayName: string }): string {\n\treturn `<!-- vetta-guide-revision: ${AGENTS_GUIDE_REVISION} -->\n# ${input.displayName}\n\nVetta 桌面插件工程(插件 id:\\`${input.pluginId}\\`)。\n\n**本文件不讲规则,只告诉你去哪读。** 规则在手册里,手册随 SDK 升级;写在这里的任何一条都会\n停在这个工程创建那天。两者冲突时一律以手册为准。\n\n## 第一步:装依赖,找到手册\n\n\\`\\`\\`bash\nnpm install\nnpx vetta-plugin-cli docs --check-latest\n\\`\\`\\`\n\n\\`npm install\\` 必须先跑:\\`vetta-plugin-cli\\` 是 \\`@vetta-org/plugin-cli\\` 的命令名,装完才在\n\\`node_modules/.bin\\` 里。还没装就想跑,用全名 \\`npx @vetta-org/plugin-cli docs\\`。\n\n它打印手册目录的**绝对路径**、手册对应的 SDK 版本,以及本工程与所属 hub 的位置。\n**不要硬编码这个路径**:工作区可能把依赖提升到仓库根,一仓多插件时各插件还可能钉不同版本。\n\n输出里出现 \\`Manual is behind\\` 或 \\`This brief is stale\\` 就按它给的命令升级,再重跑一次。\n\n## 第二步:按顺序读手册\n\n| 顺序 | 文件 | 何时读 |\n| --- | --- | --- |\n| 1 | \\`README.md\\` | **总是先读**:能力矩阵、信任模型、**不可违反的红线**、导航 |\n| 2 | \\`getting-started.md\\` | 首次写代码、构建、安装调试 |\n| 3 | \\`manifest.md\\` | 写/改 \\`plugin.json\\`、贡献智能体与团队 |\n| 4 | \\`permissions.md\\` | 选定权限列表之前 |\n| 5 | 按扩展点选读 | \\`ui-slots.md\\` / \\`conversation-and-agent.md\\` / \\`message-cards.md\\` / \\`mcp.md\\` / \\`ai.md\\` / \\`browser.md\\` / \\`app-actions.md\\` / \\`styling-and-pitfalls.md\\` |\n\n实现任一扩展点**之前**再读对应那章,不要凭记忆写 SDK API——这套合同变化很快。\n\n## 常用命令\n\n\\`\\`\\`bash\nnpm run dev # Vite + Module Federation 开发服务器\nnpm run build # 产出 dist/\nnpm run install:vetta # 打包并装进正在运行的 Vetta(需要 Vetta 已启动)\n\nnpx vetta-plugin-cli watch # 热更新:宿主改从本工程目录加载,改完即生效\nnpx vetta-plugin-cli reload ${input.pluginId} # 装完提示有 pending 版本时用它\nnpx vetta-plugin-cli uninstall # 卸载(省略 id 即本工程对应的插件)\nnpx vetta-plugin-cli sync # 在 hub 仓库根上跑:把索引与各能力目录对账\n\\`\\`\\`\n\n细节都在 \\`getting-started.md\\`。\\`docs\\` 打印了 \\`Marketplace index:\\` 就说明这个目录之上有能力\n市场索引,改完 \\`version\\` / \\`permissions\\` / \\`pluginApiVersion\\` 后要回仓库根跑 \\`sync\\`。\n\n## 信息不足时\n\n插件 id、展示名、要用哪些权限、功能边界、是否立刻安装——**问用户**,不要自己假定。\n`;\n}\n"]}
1
+ {"version":3,"file":"agents-template.d.ts","sourceRoot":"","sources":["../src/agents-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,4HAA4D;AAC5D,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI3E;AA4BD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,4FAAkD;IAClD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,GAAG,MAAM,CAiDT","sourcesContent":["/**\n * 随脚手架落地的 Agent 说明书。\n *\n * 它替代的是「把开发知识写死在工作台插件的 skill 里」那套做法:那份 skill 只有 Vetta\n * 自己的 Agent 读得到,而且每加一个能力就要改一次、还要等 App 发版才到用户手里。这里\n * 反过来——工程自带说明书,说明书只说「去哪读手册」,手册随 SDK 版本进 node_modules。\n * 因此任何 Agent(Claude Code、Cursor、Vetta 自己)在任何陌生目录都能自举,而且读到的\n * 永远是这个工程实际编译所针对的那份合同。\n *\n * **这里刻意只留指引,不留知识。** 写进这个文件的每一条规则都会在所有存量工程里就地凝固:\n * 它是 `init` 当天的快照,之后既不会自更新,用户也没有理由回来看它。规则属于手册——手册\n * 随 SDK 升级一起到位。这个文件越薄,需要回头迁移老仓库的理由就越少。\n *\n * 单位是插件目录本身:外面是能力市场仓库、是单插件仓库、还是一堆别的东西,都不影响这里。\n */\n\n/**\n * 说明书的版本戳。\n *\n * `docs` 读它来判断一份说明书是不是旧的——没有这个戳,「该不该刷新」就只能靠人记得,而这\n * 恰恰是它凝固的原因。**改动模板内容时必须一并推进它**,否则存量工程不会收到提示。\n */\nexport const AGENTS_GUIDE_REVISION = 3;\n\n/** 从一份 AGENTS.md 正文里读出版本戳;不是本模板生成的(或早于版本戳)时返回 undefined。 */\nexport function readAgentsGuideRevision(content: string): number | undefined {\n\tconst match = /<!--\\s*vetta-guide-revision:\\s*(\\d+)\\s*-->/.exec(content);\n\tif (!match) return undefined;\n\treturn Number(match[1]);\n}\n\n/**\n * 渲染常用命令块。\n *\n * 只列工程真有的 script:模板写死 `npm run dev` / `install:vetta`,老工程和自定义工程未必有,\n * 照着跑就是一句 \"Missing script\"。读不到 package.json 时退回 CLI 直连命令——它们不依赖工程脚本。\n */\nfunction renderCommands(pluginId: string, scripts: readonly string[]): string {\n\tconst known: readonly (readonly [string, string])[] = [\n\t\t[\"dev\", \"npm run dev # 开发服务器\"],\n\t\t[\"build\", \"npm run build # 产出 dist/\"],\n\t\t[\"install:vetta\", \"npm run install:vetta # 打包并装进正在运行的 Vetta(需要 Vetta 已启动)\"],\n\t];\n\tconst lines = known.filter(([name]) => scripts.includes(name)).map(([, line]) => line);\n\tif (!scripts.includes(\"install:vetta\")) {\n\t\tlines.push(\"npx vetta-plugin-cli add . # 打包并装进正在运行的 Vetta\");\n\t}\n\treturn [\n\t\t...lines,\n\t\t\"\",\n\t\t\"npx vetta-plugin-cli watch # 热更新:宿主改从本工程目录加载,改完即生效\",\n\t\t`npx vetta-plugin-cli reload ${pluginId} # 装完提示有 pending 版本时用它`,\n\t\t\"npx vetta-plugin-cli uninstall # 卸载(省略 id 即本工程对应的插件)\",\n\t\t\"npx vetta-plugin-cli sync # 在 hub 仓库根上跑:把索引与各能力目录对账\",\n\t].join(\"\\n\");\n}\n\nexport function renderAgentsGuide(input: {\n\tpluginId: string;\n\tdisplayName: string;\n\t/** 工程 package.json 里实际存在的 script 名;缺省按脚手架的那套算。 */\n\tscripts?: readonly string[];\n}): string {\n\treturn `<!-- vetta-guide-revision: ${AGENTS_GUIDE_REVISION} -->\n# ${input.displayName}\n\nVetta 桌面插件工程(插件 id:\\`${input.pluginId}\\`)。\n\n**本文件不讲规则,只告诉你去哪读。** 规则在手册里,手册随 SDK 升级;写在这里的任何一条都会\n停在这个工程创建那天。两者冲突时一律以手册为准。\n\n## 第一步:装依赖,找到手册\n\n\\`\\`\\`bash\nnpm install\nnpx vetta-plugin-cli docs --check-latest\n\\`\\`\\`\n\n\\`npm install\\` 必须先跑:\\`vetta-plugin-cli\\` 是 \\`@vetta-org/plugin-cli\\` 的命令名,装完才在\n\\`node_modules/.bin\\` 里。还没装就想跑,用全名 \\`npx @vetta-org/plugin-cli docs\\`。\n\n它打印手册目录的**绝对路径**、手册对应的 SDK 版本,以及本工程与所属 hub 的位置。\n**不要硬编码这个路径**:工作区可能把依赖提升到仓库根,一仓多插件时各插件还可能钉不同版本。\n\n输出里出现 \\`Manual is behind\\` 或 \\`This brief is stale\\` 就按它给的命令升级,再重跑一次。\n\n## 第二步:按顺序读手册\n\n| 顺序 | 文件 | 何时读 |\n| --- | --- | --- |\n| 1 | \\`README.md\\` | **总是先读**:能力矩阵、信任模型、**不可违反的红线**、导航 |\n| 2 | \\`getting-started.md\\` | 首次写代码、构建、安装调试 |\n| 3 | \\`manifest.md\\` | 写/改 \\`plugin.json\\`、贡献智能体与团队 |\n| 4 | \\`permissions.md\\` | 选定权限列表之前 |\n| 5 | 按扩展点选读 | \\`ui-slots.md\\` / \\`conversation-and-agent.md\\` / \\`message-cards.md\\` / \\`mcp.md\\` / \\`ai.md\\` / \\`browser.md\\` / \\`app-actions.md\\` / \\`styling-and-pitfalls.md\\` |\n\n实现任一扩展点**之前**再读对应那章,不要凭记忆写 SDK API——这套合同变化很快。\n\n## 常用命令\n\n\\`\\`\\`bash\n${renderCommands(input.pluginId, input.scripts ?? [\"dev\", \"build\", \"install:vetta\"])}\n\\`\\`\\`\n\n细节都在 \\`getting-started.md\\`。\\`docs\\` 打印了 \\`Marketplace index:\\` 就说明这个目录之上有能力\n市场索引,改完 \\`version\\` / \\`permissions\\` / \\`pluginApiVersion\\` 后要回仓库根跑 \\`sync\\`。\n\n## 信息不足时\n\n插件 id、展示名、要用哪些权限、功能边界、是否立刻安装——**问用户**,不要自己假定。\n`;\n}\n"]}
package/dist/cli.js CHANGED
@@ -15419,7 +15419,9 @@ var PluginAgentTeamMemberManifestSchema = Type.Object({
15419
15419
  agent: Type.Optional(NonWhitespaceStringSchema),
15420
15420
  role: Type.Optional(PluginAgentRoleSchema),
15421
15421
  responsibility: Type.String({ maxLength: 2000 }),
15422
- optional: Type.Optional(Type.Boolean())
15422
+ optional: Type.Optional(Type.Boolean()),
15423
+ instructions: Type.Optional(Type.String({ maxLength: 64000 })),
15424
+ instructionsPath: Type.Optional(NonWhitespaceStringSchema)
15423
15425
  }, { additionalProperties: false });
15424
15426
  var PluginAgentTeamManifestSchema = Type.Object({
15425
15427
  id: Type.String({ pattern: "^[a-z0-9][a-z0-9-]{0,63}$" }),
@@ -18921,13 +18923,33 @@ async function resolveNpmPluginArchive(packageSpec, pack = runNpmPack) {
18921
18923
  }
18922
18924
 
18923
18925
  // src/agents-template.ts
18924
- var AGENTS_GUIDE_REVISION = 2;
18926
+ var AGENTS_GUIDE_REVISION = 3;
18925
18927
  function readAgentsGuideRevision(content) {
18926
18928
  const match2 = /<!--\s*vetta-guide-revision:\s*(\d+)\s*-->/.exec(content);
18927
18929
  if (!match2)
18928
18930
  return;
18929
18931
  return Number(match2[1]);
18930
18932
  }
18933
+ function renderCommands(pluginId, scripts) {
18934
+ const known = [
18935
+ ["dev", "npm run dev # 开发服务器"],
18936
+ ["build", "npm run build # 产出 dist/"],
18937
+ ["install:vetta", "npm run install:vetta # 打包并装进正在运行的 Vetta(需要 Vetta 已启动)"]
18938
+ ];
18939
+ const lines = known.filter(([name]) => scripts.includes(name)).map(([, line]) => line);
18940
+ if (!scripts.includes("install:vetta")) {
18941
+ lines.push("npx vetta-plugin-cli add . # 打包并装进正在运行的 Vetta");
18942
+ }
18943
+ return [
18944
+ ...lines,
18945
+ "",
18946
+ "npx vetta-plugin-cli watch # 热更新:宿主改从本工程目录加载,改完即生效",
18947
+ `npx vetta-plugin-cli reload ${pluginId} # 装完提示有 pending 版本时用它`,
18948
+ "npx vetta-plugin-cli uninstall # 卸载(省略 id 即本工程对应的插件)",
18949
+ "npx vetta-plugin-cli sync # 在 hub 仓库根上跑:把索引与各能力目录对账"
18950
+ ].join(`
18951
+ `);
18952
+ }
18931
18953
  function renderAgentsGuide(input) {
18932
18954
  return `<!-- vetta-guide-revision: ${AGENTS_GUIDE_REVISION} -->
18933
18955
  # ${input.displayName}
@@ -18967,14 +18989,7 @@ npx vetta-plugin-cli docs --check-latest
18967
18989
  ## 常用命令
18968
18990
 
18969
18991
  \`\`\`bash
18970
- npm run dev # Vite + Module Federation 开发服务器
18971
- npm run build # 产出 dist/
18972
- npm run install:vetta # 打包并装进正在运行的 Vetta(需要 Vetta 已启动)
18973
-
18974
- npx vetta-plugin-cli watch # 热更新:宿主改从本工程目录加载,改完即生效
18975
- npx vetta-plugin-cli reload ${input.pluginId} # 装完提示有 pending 版本时用它
18976
- npx vetta-plugin-cli uninstall # 卸载(省略 id 即本工程对应的插件)
18977
- npx vetta-plugin-cli sync # 在 hub 仓库根上跑:把索引与各能力目录对账
18992
+ ${renderCommands(input.pluginId, input.scripts ?? ["dev", "build", "install:vetta"])}
18978
18993
  \`\`\`
18979
18994
 
18980
18995
  细节都在 \`getting-started.md\`。\`docs\` 打印了 \`Marketplace index:\` 就说明这个目录之上有能力
@@ -19027,16 +19042,25 @@ npx @vetta-org/plugin-cli init --id <slug> --name "<Display Name>" abilities/plu
19027
19042
 
19028
19043
  ## 索引由工具对账,不要手改派生字段
19029
19044
 
19030
- \`.vetta/marketplace.json\` 里能力的 \`version\`、\`config.api_version\`、\`config.permissions\`、
19031
- \`config.commands\` 全都是从能力包推导出来的。改完能力后:
19045
+ 改完能力后:
19032
19046
 
19033
19047
  \`\`\`bash
19034
- npx @vetta-org/plugin-cli sync # 回填派生字段,并推进 marketplaceVersion
19048
+ npx @vetta-org/plugin-cli sync # 对账并回填,看输出
19035
19049
  npx @vetta-org/plugin-cli sync --check # 只报不写,非零退出(CI 用)
19036
19050
  \`\`\`
19037
19051
 
19052
+ \`sync\` 到底做什么,分三档看清楚——它不是万能的:
19053
+
19054
+ | 字段 | \`sync\` 的行为 |
19055
+ | --- | --- |
19056
+ | 条目 \`version\` | **回填**成能力包里的版本 |
19057
+ | \`marketplaceVersion\` | 内容有变化时**尝试**推进。只认 semver(\`1.2.3\`)和纯整数;\`YYYY.MM.DD-NN\` 之类的格式推不动,会报出来要你手改 |
19058
+ | \`config.api_version\` / \`config.permissions\` / \`config.commands\` | **不写**。宿主建目录时用 \`plugin.json\` 整个重算 \`config\`,索引里的副本读都不读。已经存在且与真源不符时 \`sync\` 会提醒你删掉或改对 |
19059
+
19060
+ 所以 \`sync\` 跑完要看输出:它报出来的问题(尤其是推不动的 \`marketplaceVersion\`)没人会替你处理。
19061
+
19038
19062
  要**手写**的只有身份与展示:\`slug\`、\`name\`、\`description\`、\`source.path\`、\`category\`、\`tags\`、
19039
- \`detail\`。新能力上架时手动加一条这样的条目,其余字段交给 \`sync\`。
19063
+ \`detail\`。新能力上架时手动加一条这样的条目。
19040
19064
 
19041
19065
  三条容易踩的约束,\`sync --check\` 会替你守住:
19042
19066
 
@@ -19046,7 +19070,7 @@ npx @vetta-org/plugin-cli sync --check # 只报不写,非零退出(CI 用
19046
19070
  | \`plugin.json\` 的 \`entry\` / \`styles\` 必须在已发布目录里真实存在 | 本地能装,市场上装不了 |
19047
19071
  | 改了任何内容必须换 \`marketplaceVersion\` | 客户端不报错、也不更新,用户永远收不到 |
19048
19072
 
19049
- 第三条最阴险——它不报错。
19073
+ 第三条最阴险——它不报错。\`sync\` 只在版本号是 semver 或整数时才推得动,其余格式要你自己换。
19050
19074
 
19051
19075
  ## 为什么插件目录里要提交 \`dist/\`
19052
19076
 
@@ -19214,26 +19238,80 @@ node_modules/
19214
19238
  }
19215
19239
  return { root, pluginId: input.pluginId, files: Object.keys(files).sort() };
19216
19240
  }
19217
- function refreshAgentsGuide(targetDir) {
19241
+ function refreshAgentsGuide(targetDir, options = {}) {
19218
19242
  const root = resolve2(targetDir);
19243
+ const file = join3(root, "AGENTS.md");
19219
19244
  const manifestPath = join3(root, "plugin.json");
19245
+ const hubManifestPath = join3(root, ".vetta", "marketplace.json");
19246
+ let kind;
19247
+ let content;
19220
19248
  if (existsSync(manifestPath)) {
19221
19249
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
19222
19250
  const pluginId = typeof manifest.id === "string" ? manifest.id : undefined;
19223
19251
  if (!pluginId)
19224
19252
  throw new Error(`plugin.json at ${root} has no id`);
19225
- const displayName = typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : pluginId;
19226
- writeFileSync(join3(root, "AGENTS.md"), renderAgentsGuide({ pluginId, displayName }), "utf8");
19227
- return { root, kind: "plugin", file: join3(root, "AGENTS.md") };
19253
+ kind = "plugin";
19254
+ content = renderAgentsGuide({
19255
+ pluginId,
19256
+ displayName: resolveManifestText(manifest.name, root, pluginId),
19257
+ scripts: readPackageScripts(root)
19258
+ });
19259
+ } else if (existsSync(hubManifestPath)) {
19260
+ const manifest = JSON.parse(readFileSync(hubManifestPath, "utf8"));
19261
+ kind = "hub";
19262
+ content = renderHubAgentsGuide({
19263
+ name: typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : "marketplace"
19264
+ });
19265
+ } else {
19266
+ throw new Error(`Not a plugin project or marketplace repository: ${root}`);
19267
+ }
19268
+ if (!options.dryRun)
19269
+ assertSafeToOverwrite(file, options.force === true);
19270
+ if (options.dryRun)
19271
+ return { root, kind, file, content, written: false };
19272
+ writeFileSync(file, content, "utf8");
19273
+ return { root, kind, file, content, written: true };
19274
+ }
19275
+ function assertSafeToOverwrite(file, force) {
19276
+ if (force || !existsSync(file))
19277
+ return;
19278
+ if (readAgentsGuideRevision(readFileSync(file, "utf8")) !== undefined)
19279
+ return;
19280
+ throw new Error(`${file} has no vetta-guide-revision marker, so it looks hand-written rather than scaffolded. ` + "Refusing to overwrite it. Review the new template with `--dry-run`, merge what you want by hand, " + "or pass `--force` to replace the file.");
19281
+ }
19282
+ function resolveManifestText(raw2, root, fallback) {
19283
+ if (typeof raw2 !== "string" || raw2.length === 0)
19284
+ return fallback;
19285
+ const match2 = /^%([^%]+)%$/.exec(raw2);
19286
+ if (!match2)
19287
+ return raw2;
19288
+ const key = match2[1];
19289
+ const pluginManifest = readJsonFile(join3(root, "plugin.json"));
19290
+ const locale = typeof pluginManifest?.defaultLocale === "string" ? pluginManifest.defaultLocale : "zh";
19291
+ for (const candidate of [locale, "zh", "en"]) {
19292
+ const table = readJsonFile(join3(root, "locales", `${candidate}.json`));
19293
+ const value = table?.[key];
19294
+ if (typeof value === "string" && value.length > 0)
19295
+ return value;
19228
19296
  }
19229
- const hubManifest = join3(root, ".vetta", "marketplace.json");
19230
- if (existsSync(hubManifest)) {
19231
- const manifest = JSON.parse(readFileSync(hubManifest, "utf8"));
19232
- const name = typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : "marketplace";
19233
- writeFileSync(join3(root, "AGENTS.md"), renderHubAgentsGuide({ name }), "utf8");
19234
- return { root, kind: "hub", file: join3(root, "AGENTS.md") };
19297
+ return fallback;
19298
+ }
19299
+ function readPackageScripts(root) {
19300
+ const pkg = readJsonFile(join3(root, "package.json"));
19301
+ const scripts = pkg?.scripts;
19302
+ if (typeof scripts !== "object" || scripts === null || Array.isArray(scripts))
19303
+ return [];
19304
+ return Object.keys(scripts);
19305
+ }
19306
+ function readJsonFile(path) {
19307
+ if (!existsSync(path))
19308
+ return;
19309
+ try {
19310
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
19311
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
19312
+ } catch {
19313
+ return;
19235
19314
  }
19236
- throw new Error(`Not a plugin project or marketplace repository: ${root}`);
19237
19315
  }
19238
19316
  var HUB_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
19239
19317
  var APP_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
@@ -19293,7 +19371,7 @@ function detectJsonIndent(source) {
19293
19371
  const match2 = /\n([ \t]+)\S/.exec(source);
19294
19372
  return match2?.[1] ?? "\t";
19295
19373
  }
19296
- function readJsonFile(path) {
19374
+ function readJsonFile2(path) {
19297
19375
  try {
19298
19376
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
19299
19377
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
@@ -19337,7 +19415,7 @@ function nextMarketplaceVersion(current) {
19337
19415
  return;
19338
19416
  }
19339
19417
  function syncMarketplaceIndex(input) {
19340
- const manifest = readJsonFile(input.manifestPath);
19418
+ const manifest = readJsonFile2(input.manifestPath);
19341
19419
  if (!manifest)
19342
19420
  throw new Error(`Malformed marketplace manifest: ${input.manifestPath}`);
19343
19421
  const abilities = Array.isArray(manifest.abilities) ? manifest.abilities : [];
@@ -19406,7 +19484,7 @@ function syncMarketplaceIndex(input) {
19406
19484
  }
19407
19485
  function reconcilePlugin(context) {
19408
19486
  const { entry, slug, abilityDir, changes, problems } = context;
19409
- const manifest = readJsonFile(join4(abilityDir, "plugin.json"));
19487
+ const manifest = readJsonFile2(join4(abilityDir, "plugin.json"));
19410
19488
  if (!manifest) {
19411
19489
  problems.push({ slug, message: "plugin.json is missing or malformed" });
19412
19490
  return;
@@ -19447,7 +19525,7 @@ function reconcilePlugin(context) {
19447
19525
  }
19448
19526
  function reconcileIdentityFile(context) {
19449
19527
  const { entry, slug, abilityDir, fileName, changes, problems } = context;
19450
- const identity = readJsonFile(join4(abilityDir, fileName));
19528
+ const identity = readJsonFile2(join4(abilityDir, fileName));
19451
19529
  if (!identity) {
19452
19530
  problems.push({ slug, message: `${fileName} is missing or malformed` });
19453
19531
  return;
@@ -19490,7 +19568,7 @@ function findAbilityDirectories(hubRoot) {
19490
19568
  return found.sort();
19491
19569
  }
19492
19570
  function describeIndexDrift(input) {
19493
- const manifest = readJsonFile(input.manifestPath);
19571
+ const manifest = readJsonFile2(input.manifestPath);
19494
19572
  if (!manifest || !Array.isArray(manifest.abilities))
19495
19573
  return;
19496
19574
  const entry = manifest.abilities.find((item) => typeof item === "object" && item !== null && !Array.isArray(item) && item.slug === input.slug);
@@ -19580,7 +19658,7 @@ Usage:
19580
19658
  vetta-plugin-cli reload <plugin-id> [--json]
19581
19659
  vetta-plugin-cli docs [--check-latest] [--json]
19582
19660
  vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]
19583
- vetta-plugin-cli init --refresh-guide [dir] [--json]
19661
+ vetta-plugin-cli init --refresh-guide [dir] [--dry-run] [--force] [--json]
19584
19662
  vetta-plugin-cli init hub --name <slug> --repository <url> --min-app-version <x.y.z> [dir]
19585
19663
  vetta-plugin-cli watch [dir] [--stop] [--json]
19586
19664
  vetta-plugin-cli uninstall [plugin-id] [--json]
@@ -19681,7 +19759,9 @@ function parsePluginInitCommand(argv) {
19681
19759
  id: { type: "string" },
19682
19760
  name: { type: "string" },
19683
19761
  json: { type: "boolean" },
19684
- "refresh-guide": { type: "boolean" }
19762
+ "refresh-guide": { type: "boolean" },
19763
+ force: { type: "boolean" },
19764
+ "dry-run": { type: "boolean" }
19685
19765
  }
19686
19766
  });
19687
19767
  } catch (error) {
@@ -19691,7 +19771,13 @@ function parsePluginInitCommand(argv) {
19691
19771
  const [dir, extra] = parsed.positionals;
19692
19772
  if (extra)
19693
19773
  return { type: "error", message: `Unexpected argument: ${extra}` };
19694
- return { type: "refresh-guide", ...dir ? { targetDir: dir } : {}, json: parsed.values.json === true };
19774
+ return {
19775
+ type: "refresh-guide",
19776
+ ...dir ? { targetDir: dir } : {},
19777
+ json: parsed.values.json === true,
19778
+ force: parsed.values.force === true,
19779
+ dryRun: parsed.values["dry-run"] === true
19780
+ };
19695
19781
  }
19696
19782
  const pluginId = parsed.values.id;
19697
19783
  if (typeof pluginId !== "string" || pluginId.length === 0) {
@@ -20069,7 +20155,9 @@ async function runDocsCommand(command, dependencies) {
20069
20155
  lines.push(`Manual follows the installed SDK. To refresh it: ${SDK_REFRESH_COMMAND}`);
20070
20156
  }
20071
20157
  if (guide.stale) {
20072
- lines.push(`This brief is stale (AGENTS.md revision ${guide.revision ?? "unstamped"} < ${AGENTS_GUIDE_REVISION}). Refresh it with: ${GUIDE_REFRESH_COMMAND}`);
20158
+ lines.push(`This brief is stale (AGENTS.md revision ${guide.revision} < ${AGENTS_GUIDE_REVISION}). Refresh it with: ${GUIDE_REFRESH_COMMAND}`);
20159
+ } else if (guide.unstamped) {
20160
+ lines.push(`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.`);
20073
20161
  }
20074
20162
  if (hub) {
20075
20163
  lines.push(`Marketplace index: ${hub.manifestPath}`);
@@ -20085,17 +20173,18 @@ var GUIDE_REFRESH_COMMAND = "npx @vetta-org/plugin-cli init --refresh-guide";
20085
20173
  function inspectAgentsGuide(root) {
20086
20174
  const path = join6(root, "AGENTS.md");
20087
20175
  if (!existsSync4(path))
20088
- return { present: false, stale: false };
20176
+ return { present: false, stale: false, unstamped: false };
20089
20177
  let revision;
20090
20178
  try {
20091
20179
  revision = readAgentsGuideRevision(readFileSync4(path, "utf8"));
20092
20180
  } catch {
20093
- return { present: true, stale: false };
20181
+ return { present: true, stale: false, unstamped: false };
20094
20182
  }
20095
20183
  return {
20096
20184
  present: true,
20097
20185
  ...revision === undefined ? {} : { revision },
20098
- stale: revision === undefined || revision < AGENTS_GUIDE_REVISION
20186
+ stale: revision !== undefined && revision < AGENTS_GUIDE_REVISION,
20187
+ unstamped: revision === undefined
20099
20188
  };
20100
20189
  }
20101
20190
  function compareSemver(left, right) {
@@ -20118,11 +20207,18 @@ function compareSemver(left, right) {
20118
20207
  function runRefreshGuideCommand(command, dependencies) {
20119
20208
  const cwd = dependencies.cwd?.() ?? process.cwd();
20120
20209
  try {
20121
- const result = refreshAgentsGuide(resolve5(cwd, command.targetDir ?? "."));
20122
- dependencies.writeStdout(command.json ? `${JSON.stringify({ ok: true, ...result })}
20123
- ` : `Rewrote ${result.file}
20124
- Next: npx vetta-plugin-cli docs --check-latest
20210
+ const result = refreshAgentsGuide(resolve5(cwd, command.targetDir ?? "."), {
20211
+ force: command.force,
20212
+ dryRun: command.dryRun
20213
+ });
20214
+ if (command.json) {
20215
+ dependencies.writeStdout(`${JSON.stringify({ ok: true, ...result })}
20125
20216
  `);
20217
+ return 0;
20218
+ }
20219
+ dependencies.writeStdout(result.written ? `Rewrote ${result.file}
20220
+ Next: npx vetta-plugin-cli docs --check-latest
20221
+ ` : `${result.content}`);
20126
20222
  return 0;
20127
20223
  } catch (error) {
20128
20224
  const message = error instanceof Error ? error.message : String(error);
package/dist/command.d.ts CHANGED
@@ -44,6 +44,8 @@ export type PluginInitCommand = {
44
44
  type: "refresh-guide";
45
45
  targetDir?: string;
46
46
  json: boolean;
47
+ force: boolean;
48
+ dryRun: boolean;
47
49
  } | {
48
50
  type: "init-hub";
49
51
  targetDir?: string;
@@ -109,8 +111,14 @@ export interface AgentsGuideStatus {
109
111
  readonly present: boolean;
110
112
  /** 读到的版本戳;没有戳(模板早于版本戳,或是手写的)时缺省。 */
111
113
  readonly revision?: number;
112
- /** 落后于当前 CLI 的模板。没有 AGENTS.md 时为 false——那是「没有」,不是「旧」。 */
114
+ /**
115
+ * **带戳**且落后于当前 CLI 的模板——只有这一档能安全地一键重写。
116
+ *
117
+ * 没有 AGENTS.md 时为 false(那是「没有」,不是「旧」);没有戳时也为 false,见 {@link unstamped}。
118
+ */
113
119
  readonly stale: boolean;
120
+ /** 有文件但没有版本戳:可能是手写的,也可能是版本戳之前的模板,无从区分。 */
121
+ readonly unstamped: boolean;
114
122
  }
115
123
  export declare function runPluginCli(argv: string[]): Promise<number>;
116
124
  //# sourceMappingURL=command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiD,KAAK,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAMhH,MAAM,MAAM,gBAAgB,GACzB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAC5B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzD,MAAM,MAAM,iBAAiB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC3F;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEpH,MAAM,MAAM,kBAAkB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEjE,MAAM,MAAM,sBAAsB,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,iBAAiB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnD,MAAM,MAAM,aAAa,GACtB,gBAAgB,GAChB,iBAAiB,GACjB,sBAAsB,GACtB,mBAAmB,GACnB,iBAAiB,GACjB,iBAAiB,GACjB,kBAAkB,CAAC;AAEtB,MAAM,WAAW,yBAAyB;IACzC,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC1E,yGAAiD;IACjD,GAAG,CAAC,IAAI,MAAM,CAAC;IACf,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,wHAA4E;IAC5E,oBAAoB,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACrD;AAED,MAAM,MAAM,4BAA4B,GAAG,yBAAyB,CAAC;AAkCrE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAalF;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,mBAAmB,GAAG,SAAS,CAaxF;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,iBAAiB,GAAG,SAAS,CAqBpF;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,iBAAiB,GAAG,SAAS,CAuCpF;AA8CD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,kBAAkB,GAAG,SAAS,CAsBtF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,sBAAsB,GAAG,SAAS,CAkB9F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,iBAAiB,GAAG,SAAS,CAiBpF;AAyID,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,gBAAgB,EACzB,YAAY,GAAE,4BAAkD,GAC9D,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,wBAAsB,gBAAgB,CACrC,OAAO,EAAE,aAAa,EACtB,YAAY,GAAE,yBAA+C,GAC3D,OAAO,CAAC,MAAM,CAAC,CAuFjB;AAoGD,MAAM,WAAW,iBAAiB;IACjC,sCAAwB;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,8FAAoC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,iHAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AA0SD,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAUlE","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|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] [--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.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\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 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(\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"]}
package/dist/index.js CHANGED
@@ -15418,7 +15418,9 @@ var PluginAgentTeamMemberManifestSchema = Type.Object({
15418
15418
  agent: Type.Optional(NonWhitespaceStringSchema),
15419
15419
  role: Type.Optional(PluginAgentRoleSchema),
15420
15420
  responsibility: Type.String({ maxLength: 2000 }),
15421
- optional: Type.Optional(Type.Boolean())
15421
+ optional: Type.Optional(Type.Boolean()),
15422
+ instructions: Type.Optional(Type.String({ maxLength: 64000 })),
15423
+ instructionsPath: Type.Optional(NonWhitespaceStringSchema)
15422
15424
  }, { additionalProperties: false });
15423
15425
  var PluginAgentTeamManifestSchema = Type.Object({
15424
15426
  id: Type.String({ pattern: "^[a-z0-9][a-z0-9-]{0,63}$" }),
@@ -18920,13 +18922,33 @@ async function resolveNpmPluginArchive(packageSpec, pack = runNpmPack) {
18920
18922
  }
18921
18923
 
18922
18924
  // src/agents-template.ts
18923
- var AGENTS_GUIDE_REVISION = 2;
18925
+ var AGENTS_GUIDE_REVISION = 3;
18924
18926
  function readAgentsGuideRevision(content) {
18925
18927
  const match2 = /<!--\s*vetta-guide-revision:\s*(\d+)\s*-->/.exec(content);
18926
18928
  if (!match2)
18927
18929
  return;
18928
18930
  return Number(match2[1]);
18929
18931
  }
18932
+ function renderCommands(pluginId, scripts) {
18933
+ const known = [
18934
+ ["dev", "npm run dev # 开发服务器"],
18935
+ ["build", "npm run build # 产出 dist/"],
18936
+ ["install:vetta", "npm run install:vetta # 打包并装进正在运行的 Vetta(需要 Vetta 已启动)"]
18937
+ ];
18938
+ const lines = known.filter(([name]) => scripts.includes(name)).map(([, line]) => line);
18939
+ if (!scripts.includes("install:vetta")) {
18940
+ lines.push("npx vetta-plugin-cli add . # 打包并装进正在运行的 Vetta");
18941
+ }
18942
+ return [
18943
+ ...lines,
18944
+ "",
18945
+ "npx vetta-plugin-cli watch # 热更新:宿主改从本工程目录加载,改完即生效",
18946
+ `npx vetta-plugin-cli reload ${pluginId} # 装完提示有 pending 版本时用它`,
18947
+ "npx vetta-plugin-cli uninstall # 卸载(省略 id 即本工程对应的插件)",
18948
+ "npx vetta-plugin-cli sync # 在 hub 仓库根上跑:把索引与各能力目录对账"
18949
+ ].join(`
18950
+ `);
18951
+ }
18930
18952
  function renderAgentsGuide(input) {
18931
18953
  return `<!-- vetta-guide-revision: ${AGENTS_GUIDE_REVISION} -->
18932
18954
  # ${input.displayName}
@@ -18966,14 +18988,7 @@ npx vetta-plugin-cli docs --check-latest
18966
18988
  ## 常用命令
18967
18989
 
18968
18990
  \`\`\`bash
18969
- npm run dev # Vite + Module Federation 开发服务器
18970
- npm run build # 产出 dist/
18971
- npm run install:vetta # 打包并装进正在运行的 Vetta(需要 Vetta 已启动)
18972
-
18973
- npx vetta-plugin-cli watch # 热更新:宿主改从本工程目录加载,改完即生效
18974
- npx vetta-plugin-cli reload ${input.pluginId} # 装完提示有 pending 版本时用它
18975
- npx vetta-plugin-cli uninstall # 卸载(省略 id 即本工程对应的插件)
18976
- npx vetta-plugin-cli sync # 在 hub 仓库根上跑:把索引与各能力目录对账
18991
+ ${renderCommands(input.pluginId, input.scripts ?? ["dev", "build", "install:vetta"])}
18977
18992
  \`\`\`
18978
18993
 
18979
18994
  细节都在 \`getting-started.md\`。\`docs\` 打印了 \`Marketplace index:\` 就说明这个目录之上有能力
@@ -19026,16 +19041,25 @@ npx @vetta-org/plugin-cli init --id <slug> --name "<Display Name>" abilities/plu
19026
19041
 
19027
19042
  ## 索引由工具对账,不要手改派生字段
19028
19043
 
19029
- \`.vetta/marketplace.json\` 里能力的 \`version\`、\`config.api_version\`、\`config.permissions\`、
19030
- \`config.commands\` 全都是从能力包推导出来的。改完能力后:
19044
+ 改完能力后:
19031
19045
 
19032
19046
  \`\`\`bash
19033
- npx @vetta-org/plugin-cli sync # 回填派生字段,并推进 marketplaceVersion
19047
+ npx @vetta-org/plugin-cli sync # 对账并回填,看输出
19034
19048
  npx @vetta-org/plugin-cli sync --check # 只报不写,非零退出(CI 用)
19035
19049
  \`\`\`
19036
19050
 
19051
+ \`sync\` 到底做什么,分三档看清楚——它不是万能的:
19052
+
19053
+ | 字段 | \`sync\` 的行为 |
19054
+ | --- | --- |
19055
+ | 条目 \`version\` | **回填**成能力包里的版本 |
19056
+ | \`marketplaceVersion\` | 内容有变化时**尝试**推进。只认 semver(\`1.2.3\`)和纯整数;\`YYYY.MM.DD-NN\` 之类的格式推不动,会报出来要你手改 |
19057
+ | \`config.api_version\` / \`config.permissions\` / \`config.commands\` | **不写**。宿主建目录时用 \`plugin.json\` 整个重算 \`config\`,索引里的副本读都不读。已经存在且与真源不符时 \`sync\` 会提醒你删掉或改对 |
19058
+
19059
+ 所以 \`sync\` 跑完要看输出:它报出来的问题(尤其是推不动的 \`marketplaceVersion\`)没人会替你处理。
19060
+
19037
19061
  要**手写**的只有身份与展示:\`slug\`、\`name\`、\`description\`、\`source.path\`、\`category\`、\`tags\`、
19038
- \`detail\`。新能力上架时手动加一条这样的条目,其余字段交给 \`sync\`。
19062
+ \`detail\`。新能力上架时手动加一条这样的条目。
19039
19063
 
19040
19064
  三条容易踩的约束,\`sync --check\` 会替你守住:
19041
19065
 
@@ -19045,7 +19069,7 @@ npx @vetta-org/plugin-cli sync --check # 只报不写,非零退出(CI 用
19045
19069
  | \`plugin.json\` 的 \`entry\` / \`styles\` 必须在已发布目录里真实存在 | 本地能装,市场上装不了 |
19046
19070
  | 改了任何内容必须换 \`marketplaceVersion\` | 客户端不报错、也不更新,用户永远收不到 |
19047
19071
 
19048
- 第三条最阴险——它不报错。
19072
+ 第三条最阴险——它不报错。\`sync\` 只在版本号是 semver 或整数时才推得动,其余格式要你自己换。
19049
19073
 
19050
19074
  ## 为什么插件目录里要提交 \`dist/\`
19051
19075
 
@@ -19213,26 +19237,80 @@ node_modules/
19213
19237
  }
19214
19238
  return { root, pluginId: input.pluginId, files: Object.keys(files).sort() };
19215
19239
  }
19216
- function refreshAgentsGuide(targetDir) {
19240
+ function refreshAgentsGuide(targetDir, options = {}) {
19217
19241
  const root = resolve2(targetDir);
19242
+ const file = join3(root, "AGENTS.md");
19218
19243
  const manifestPath = join3(root, "plugin.json");
19244
+ const hubManifestPath = join3(root, ".vetta", "marketplace.json");
19245
+ let kind;
19246
+ let content;
19219
19247
  if (existsSync(manifestPath)) {
19220
19248
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
19221
19249
  const pluginId = typeof manifest.id === "string" ? manifest.id : undefined;
19222
19250
  if (!pluginId)
19223
19251
  throw new Error(`plugin.json at ${root} has no id`);
19224
- const displayName = typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : pluginId;
19225
- writeFileSync(join3(root, "AGENTS.md"), renderAgentsGuide({ pluginId, displayName }), "utf8");
19226
- return { root, kind: "plugin", file: join3(root, "AGENTS.md") };
19252
+ kind = "plugin";
19253
+ content = renderAgentsGuide({
19254
+ pluginId,
19255
+ displayName: resolveManifestText(manifest.name, root, pluginId),
19256
+ scripts: readPackageScripts(root)
19257
+ });
19258
+ } else if (existsSync(hubManifestPath)) {
19259
+ const manifest = JSON.parse(readFileSync(hubManifestPath, "utf8"));
19260
+ kind = "hub";
19261
+ content = renderHubAgentsGuide({
19262
+ name: typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : "marketplace"
19263
+ });
19264
+ } else {
19265
+ throw new Error(`Not a plugin project or marketplace repository: ${root}`);
19266
+ }
19267
+ if (!options.dryRun)
19268
+ assertSafeToOverwrite(file, options.force === true);
19269
+ if (options.dryRun)
19270
+ return { root, kind, file, content, written: false };
19271
+ writeFileSync(file, content, "utf8");
19272
+ return { root, kind, file, content, written: true };
19273
+ }
19274
+ function assertSafeToOverwrite(file, force) {
19275
+ if (force || !existsSync(file))
19276
+ return;
19277
+ if (readAgentsGuideRevision(readFileSync(file, "utf8")) !== undefined)
19278
+ return;
19279
+ throw new Error(`${file} has no vetta-guide-revision marker, so it looks hand-written rather than scaffolded. ` + "Refusing to overwrite it. Review the new template with `--dry-run`, merge what you want by hand, " + "or pass `--force` to replace the file.");
19280
+ }
19281
+ function resolveManifestText(raw2, root, fallback) {
19282
+ if (typeof raw2 !== "string" || raw2.length === 0)
19283
+ return fallback;
19284
+ const match2 = /^%([^%]+)%$/.exec(raw2);
19285
+ if (!match2)
19286
+ return raw2;
19287
+ const key = match2[1];
19288
+ const pluginManifest = readJsonFile(join3(root, "plugin.json"));
19289
+ const locale = typeof pluginManifest?.defaultLocale === "string" ? pluginManifest.defaultLocale : "zh";
19290
+ for (const candidate of [locale, "zh", "en"]) {
19291
+ const table = readJsonFile(join3(root, "locales", `${candidate}.json`));
19292
+ const value = table?.[key];
19293
+ if (typeof value === "string" && value.length > 0)
19294
+ return value;
19227
19295
  }
19228
- const hubManifest = join3(root, ".vetta", "marketplace.json");
19229
- if (existsSync(hubManifest)) {
19230
- const manifest = JSON.parse(readFileSync(hubManifest, "utf8"));
19231
- const name = typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : "marketplace";
19232
- writeFileSync(join3(root, "AGENTS.md"), renderHubAgentsGuide({ name }), "utf8");
19233
- return { root, kind: "hub", file: join3(root, "AGENTS.md") };
19296
+ return fallback;
19297
+ }
19298
+ function readPackageScripts(root) {
19299
+ const pkg = readJsonFile(join3(root, "package.json"));
19300
+ const scripts = pkg?.scripts;
19301
+ if (typeof scripts !== "object" || scripts === null || Array.isArray(scripts))
19302
+ return [];
19303
+ return Object.keys(scripts);
19304
+ }
19305
+ function readJsonFile(path) {
19306
+ if (!existsSync(path))
19307
+ return;
19308
+ try {
19309
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
19310
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
19311
+ } catch {
19312
+ return;
19234
19313
  }
19235
- throw new Error(`Not a plugin project or marketplace repository: ${root}`);
19236
19314
  }
19237
19315
  var HUB_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
19238
19316
  var APP_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
@@ -19292,7 +19370,7 @@ function detectJsonIndent(source) {
19292
19370
  const match2 = /\n([ \t]+)\S/.exec(source);
19293
19371
  return match2?.[1] ?? "\t";
19294
19372
  }
19295
- function readJsonFile(path) {
19373
+ function readJsonFile2(path) {
19296
19374
  try {
19297
19375
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
19298
19376
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
@@ -19336,7 +19414,7 @@ function nextMarketplaceVersion(current) {
19336
19414
  return;
19337
19415
  }
19338
19416
  function syncMarketplaceIndex(input) {
19339
- const manifest = readJsonFile(input.manifestPath);
19417
+ const manifest = readJsonFile2(input.manifestPath);
19340
19418
  if (!manifest)
19341
19419
  throw new Error(`Malformed marketplace manifest: ${input.manifestPath}`);
19342
19420
  const abilities = Array.isArray(manifest.abilities) ? manifest.abilities : [];
@@ -19405,7 +19483,7 @@ function syncMarketplaceIndex(input) {
19405
19483
  }
19406
19484
  function reconcilePlugin(context) {
19407
19485
  const { entry, slug, abilityDir, changes, problems } = context;
19408
- const manifest = readJsonFile(join4(abilityDir, "plugin.json"));
19486
+ const manifest = readJsonFile2(join4(abilityDir, "plugin.json"));
19409
19487
  if (!manifest) {
19410
19488
  problems.push({ slug, message: "plugin.json is missing or malformed" });
19411
19489
  return;
@@ -19446,7 +19524,7 @@ function reconcilePlugin(context) {
19446
19524
  }
19447
19525
  function reconcileIdentityFile(context) {
19448
19526
  const { entry, slug, abilityDir, fileName, changes, problems } = context;
19449
- const identity = readJsonFile(join4(abilityDir, fileName));
19527
+ const identity = readJsonFile2(join4(abilityDir, fileName));
19450
19528
  if (!identity) {
19451
19529
  problems.push({ slug, message: `${fileName} is missing or malformed` });
19452
19530
  return;
@@ -19489,7 +19567,7 @@ function findAbilityDirectories(hubRoot) {
19489
19567
  return found.sort();
19490
19568
  }
19491
19569
  function describeIndexDrift(input) {
19492
- const manifest = readJsonFile(input.manifestPath);
19570
+ const manifest = readJsonFile2(input.manifestPath);
19493
19571
  if (!manifest || !Array.isArray(manifest.abilities))
19494
19572
  return;
19495
19573
  const entry = manifest.abilities.find((item) => typeof item === "object" && item !== null && !Array.isArray(item) && item.slug === input.slug);
@@ -19579,7 +19657,7 @@ Usage:
19579
19657
  vetta-plugin-cli reload <plugin-id> [--json]
19580
19658
  vetta-plugin-cli docs [--check-latest] [--json]
19581
19659
  vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]
19582
- vetta-plugin-cli init --refresh-guide [dir] [--json]
19660
+ vetta-plugin-cli init --refresh-guide [dir] [--dry-run] [--force] [--json]
19583
19661
  vetta-plugin-cli init hub --name <slug> --repository <url> --min-app-version <x.y.z> [dir]
19584
19662
  vetta-plugin-cli watch [dir] [--stop] [--json]
19585
19663
  vetta-plugin-cli uninstall [plugin-id] [--json]
@@ -19680,7 +19758,9 @@ function parsePluginInitCommand(argv) {
19680
19758
  id: { type: "string" },
19681
19759
  name: { type: "string" },
19682
19760
  json: { type: "boolean" },
19683
- "refresh-guide": { type: "boolean" }
19761
+ "refresh-guide": { type: "boolean" },
19762
+ force: { type: "boolean" },
19763
+ "dry-run": { type: "boolean" }
19684
19764
  }
19685
19765
  });
19686
19766
  } catch (error) {
@@ -19690,7 +19770,13 @@ function parsePluginInitCommand(argv) {
19690
19770
  const [dir, extra] = parsed.positionals;
19691
19771
  if (extra)
19692
19772
  return { type: "error", message: `Unexpected argument: ${extra}` };
19693
- return { type: "refresh-guide", ...dir ? { targetDir: dir } : {}, json: parsed.values.json === true };
19773
+ return {
19774
+ type: "refresh-guide",
19775
+ ...dir ? { targetDir: dir } : {},
19776
+ json: parsed.values.json === true,
19777
+ force: parsed.values.force === true,
19778
+ dryRun: parsed.values["dry-run"] === true
19779
+ };
19694
19780
  }
19695
19781
  const pluginId = parsed.values.id;
19696
19782
  if (typeof pluginId !== "string" || pluginId.length === 0) {
@@ -20068,7 +20154,9 @@ async function runDocsCommand(command, dependencies) {
20068
20154
  lines.push(`Manual follows the installed SDK. To refresh it: ${SDK_REFRESH_COMMAND}`);
20069
20155
  }
20070
20156
  if (guide.stale) {
20071
- lines.push(`This brief is stale (AGENTS.md revision ${guide.revision ?? "unstamped"} < ${AGENTS_GUIDE_REVISION}). Refresh it with: ${GUIDE_REFRESH_COMMAND}`);
20157
+ lines.push(`This brief is stale (AGENTS.md revision ${guide.revision} < ${AGENTS_GUIDE_REVISION}). Refresh it with: ${GUIDE_REFRESH_COMMAND}`);
20158
+ } else if (guide.unstamped) {
20159
+ lines.push(`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.`);
20072
20160
  }
20073
20161
  if (hub) {
20074
20162
  lines.push(`Marketplace index: ${hub.manifestPath}`);
@@ -20084,17 +20172,18 @@ var GUIDE_REFRESH_COMMAND = "npx @vetta-org/plugin-cli init --refresh-guide";
20084
20172
  function inspectAgentsGuide(root) {
20085
20173
  const path = join6(root, "AGENTS.md");
20086
20174
  if (!existsSync4(path))
20087
- return { present: false, stale: false };
20175
+ return { present: false, stale: false, unstamped: false };
20088
20176
  let revision;
20089
20177
  try {
20090
20178
  revision = readAgentsGuideRevision(readFileSync4(path, "utf8"));
20091
20179
  } catch {
20092
- return { present: true, stale: false };
20180
+ return { present: true, stale: false, unstamped: false };
20093
20181
  }
20094
20182
  return {
20095
20183
  present: true,
20096
20184
  ...revision === undefined ? {} : { revision },
20097
- stale: revision === undefined || revision < AGENTS_GUIDE_REVISION
20185
+ stale: revision !== undefined && revision < AGENTS_GUIDE_REVISION,
20186
+ unstamped: revision === undefined
20098
20187
  };
20099
20188
  }
20100
20189
  function compareSemver(left, right) {
@@ -20117,11 +20206,18 @@ function compareSemver(left, right) {
20117
20206
  function runRefreshGuideCommand(command, dependencies) {
20118
20207
  const cwd = dependencies.cwd?.() ?? process.cwd();
20119
20208
  try {
20120
- const result = refreshAgentsGuide(resolve5(cwd, command.targetDir ?? "."));
20121
- dependencies.writeStdout(command.json ? `${JSON.stringify({ ok: true, ...result })}
20122
- ` : `Rewrote ${result.file}
20123
- Next: npx vetta-plugin-cli docs --check-latest
20209
+ const result = refreshAgentsGuide(resolve5(cwd, command.targetDir ?? "."), {
20210
+ force: command.force,
20211
+ dryRun: command.dryRun
20212
+ });
20213
+ if (command.json) {
20214
+ dependencies.writeStdout(`${JSON.stringify({ ok: true, ...result })}
20124
20215
  `);
20216
+ return 0;
20217
+ }
20218
+ dependencies.writeStdout(result.written ? `Rewrote ${result.file}
20219
+ Next: npx vetta-plugin-cli docs --check-latest
20220
+ ` : `${result.content}`);
20125
20221
  return 0;
20126
20222
  } catch (error) {
20127
20223
  const message = error instanceof Error ? error.message : String(error);
package/dist/init.d.ts CHANGED
@@ -14,10 +14,19 @@ export interface InitPluginResult {
14
14
  readonly files: readonly string[];
15
15
  }
16
16
  export declare function initPluginProject(input: InitPluginInput): InitPluginResult;
17
+ export interface RefreshGuideOptions {
18
+ /** 覆盖一份没有版本戳的 `AGENTS.md`。缺省拒绝——没有戳的多半是手写的。 */
19
+ readonly force?: boolean;
20
+ /** 只算出内容,不落盘。 */
21
+ readonly dryRun?: boolean;
22
+ }
17
23
  export interface RefreshGuideResult {
18
24
  readonly root: string;
19
25
  readonly kind: "plugin" | "hub";
20
26
  readonly file: string;
27
+ /** 这次生成的说明书正文。`dryRun` 时用它做人工合并。 */
28
+ readonly content: string;
29
+ readonly written: boolean;
21
30
  }
22
31
  /**
23
32
  * 在已有的工程或能力市场仓库里重写 `AGENTS.md`。
@@ -25,8 +34,12 @@ export interface RefreshGuideResult {
25
34
  * `init` 拒绝覆盖已有工程,所以老目录里那份说明书从落地起就再也没变过——它写于某个版本的
26
35
  * SDK,之后新增的约定一条都没有。这里只重写这一个文件:它是脚手架里唯一「纯派生、没有用户
27
36
  * 内容」的产物,其余文件都可能被改过,不该被一次刷新抹掉。
37
+ *
38
+ * **只有确实由脚手架生成的那份才算纯派生。** 没有版本戳的文件无法与手写内容区分——能力市场
39
+ * 仓库的根 `AGENTS.md` 往往是一整本手写的市场规范——所以一律拒绝覆盖,让调用方拿 `dryRun`
40
+ * 的内容去人工合并,或显式 `force`。宁可少刷新一份,也不能悄悄删掉别人写的东西。
28
41
  */
29
- export declare function refreshAgentsGuide(targetDir: string): RefreshGuideResult;
42
+ export declare function refreshAgentsGuide(targetDir: string, options?: RefreshGuideOptions): RefreshGuideResult;
30
43
  export interface InitHubInput {
31
44
  readonly targetDir: string;
32
45
  /** 市场名(slug)。 */
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAKA,0GAAwC;AACxC,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAC1C,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAI3C,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAUD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAkH1E;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAqBxE;AAKD,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6BAAiB;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2DAA6B;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,CA6CpE","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { renderAgentsGuide } from \"./agents-template.js\";\nimport { renderHubAgentsGuide, renderHubReadme, renderHubWorkflow } from \"./hub-template.js\";\n\n/** 与脚手架一同落地的依赖范围;两个包各自独立发布,不要合成一个版本。 */\nexport const DEFAULT_SDK_RANGE = \"^0.3.2\";\nexport const DEFAULT_VITE_RANGE = \"^0.2.0\";\n\nconst PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;\n\nexport interface InitPluginInput {\n\treadonly targetDir: string;\n\treadonly pluginId: string;\n\treadonly displayName: string;\n\treadonly sdkRange?: string;\n\treadonly viteRange?: string;\n}\n\nexport interface InitPluginResult {\n\treadonly root: string;\n\treadonly pluginId: string;\n\treadonly files: readonly string[];\n}\n\nfunction remoteNameFromId(pluginId: string): string {\n\treturn pluginId.replace(/-/g, \"_\").replace(/[^A-Za-z0-9_$]/g, \"_\");\n}\n\nfunction json(value: unknown): string {\n\treturn `${JSON.stringify(value, null, \"\\t\")}\\n`;\n}\n\nexport function initPluginProject(input: InitPluginInput): InitPluginResult {\n\tif (!PLUGIN_ID_PATTERN.test(input.pluginId)) {\n\t\tthrow new Error(`Invalid plugin id ${JSON.stringify(input.pluginId)}: use lowercase kebab-case starting with a letter`);\n\t}\n\tconst root = resolve(input.targetDir);\n\tif (existsSync(join(root, \"plugin.json\"))) {\n\t\tthrow new Error(`Refusing to overwrite an existing plugin at ${root}`);\n\t}\n\n\tconst remote = remoteNameFromId(input.pluginId);\n\n\tconst files: Record<string, string> = {\n\t\t\"plugin.json\": json({\n\t\t\tid: input.pluginId,\n\t\t\tname: input.displayName,\n\t\t\tversion: \"0.1.0\",\n\t\t\tpluginApiVersion: \"^2.0.0\",\n\t\t\tentry: \"dist/mf-manifest.json\",\n\t\t\tmoduleFederation: { remoteName: remote, expose: \"./plugin\" },\n\t\t\tstyles: [\"dist/style.css\"],\n\t\t\tpermissions: [],\n\t\t\tdescription: input.displayName,\n\t\t\tauthor: \"\",\n\t\t\ticon: \"solar:widget-add-bold\",\n\t\t\tguidingWords: [],\n\t\t}),\n\t\t\"package.json\": json({\n\t\t\tname: input.pluginId,\n\t\t\tversion: \"0.1.0\",\n\t\t\tprivate: true,\n\t\t\ttype: \"module\",\n\t\t\tscripts: {\n\t\t\t\tdev: \"vetta-plugin dev\",\n\t\t\t\tbuild: \"vite build\",\n\t\t\t\tcheck: \"tsc --noEmit\",\n\t\t\t\tpack: \"vetta-plugin pack\",\n\t\t\t\tvalidate: \"vetta-plugin validate\",\n\t\t\t\tdocs: \"vetta-plugin-cli docs\",\n\t\t\t\t// 一条命令走完「构建 → 打包 → 装进正在运行的 Vetta」。\n\t\t\t\t\"install:vetta\": \"vite build && vetta-plugin pack && vetta-plugin-cli add .\",\n\t\t\t},\n\t\t\tdevDependencies: {\n\t\t\t\t\"@tailwindcss/vite\": \"^4.1.12\",\n\t\t\t\t\"@types/react\": \"^19.1.1\",\n\t\t\t\t\"@types/react-dom\": \"^19.1.1\",\n\t\t\t\t\"@vetta-org/plugin-cli\": \"^0.1.1\",\n\t\t\t\t\"@vetta-org/plugin-sdk\": input.sdkRange ?? DEFAULT_SDK_RANGE,\n\t\t\t\t\"@vetta-org/plugin-vite\": input.viteRange ?? DEFAULT_VITE_RANGE,\n\t\t\t\treact: \"19.1.1\",\n\t\t\t\t\"react-dom\": \"19.1.1\",\n\t\t\t\ttailwindcss: \"^4.1.12\",\n\t\t\t\ttypescript: \"^5.9.2\",\n\t\t\t\tvite: \"^7.1.7\",\n\t\t\t},\n\t\t}),\n\t\t\"tsconfig.json\": json({\n\t\t\tcompilerOptions: {\n\t\t\t\ttarget: \"ES2022\",\n\t\t\t\tmodule: \"ESNext\",\n\t\t\t\tlib: [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n\t\t\t\tstrict: true,\n\t\t\t\tesModuleInterop: true,\n\t\t\t\tskipLibCheck: true,\n\t\t\t\tmoduleResolution: \"bundler\",\n\t\t\t\tjsx: \"react-jsx\",\n\t\t\t\tjsxImportSource: \"react\",\n\t\t\t\tnoEmit: true,\n\t\t\t},\n\t\t\tinclude: [\"src/**/*.ts\", \"src/**/*.tsx\"],\n\t\t}),\n\t\t\"vite.config.ts\": `import tailwindcss from \"@tailwindcss/vite\";\nimport { vettaPluginFederation } from \"@vetta-org/plugin-vite\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n\tplugins: [\n\t\ttailwindcss(),\n\t\tvettaPluginFederation({\n\t\t\tname: \"${remote}\",\n\t\t\tentry: \"./src/index.tsx\",\n\t\t}),\n\t],\n\tesbuild: { jsx: \"automatic\", jsxImportSource: \"react\" },\n});\n`,\n\t\t\"src/index.tsx\": `import { definePlugin } from \"@vetta-org/plugin-sdk\";\n// Tailwind pipeline only — business CSS here would leak into the host page.\nimport \"./style.css\";\n\nexport default definePlugin({\n\tactivate(ctx) {\n\t\t// Read the manual before adding contributions: npx vetta-plugin-cli docs\n\t\tvoid ctx;\n\t},\n});\n`,\n\t\t\"src/style.css\": `/* Tailwind entry only. No business selectors — they inject into the host page. */\n@layer theme, base, components, utilities;\n@import \"tailwindcss/theme.css\" layer(theme);\n@import \"tailwindcss/utilities.css\" layer(utilities);\n`,\n\t\t// dist/ 刻意不忽略:插件通过仓库目录分发时,宿主直接读 plugin.json 指向的 entry 与\n\t\t// styles,它不会替你构建——目录里没有构建产物就装不上,而且那是一个只在别人机器上\n\t\t// 复现的失败。\n\t\t\".gitignore\": \"release/\\nnode_modules/\\n\",\n\t\t\"AGENTS.md\": renderAgentsGuide({ pluginId: input.pluginId, displayName: input.displayName }),\n\t};\n\n\tmkdirSync(join(root, \"src\"), { recursive: true });\n\tfor (const [relativePath, content] of Object.entries(files)) {\n\t\twriteFileSync(join(root, relativePath), content, \"utf8\");\n\t}\n\n\treturn { root, pluginId: input.pluginId, files: Object.keys(files).sort() };\n}\n\nexport interface RefreshGuideResult {\n\treadonly root: string;\n\treadonly kind: \"plugin\" | \"hub\";\n\treadonly file: string;\n}\n\n/**\n * 在已有的工程或能力市场仓库里重写 `AGENTS.md`。\n *\n * `init` 拒绝覆盖已有工程,所以老目录里那份说明书从落地起就再也没变过——它写于某个版本的\n * SDK,之后新增的约定一条都没有。这里只重写这一个文件:它是脚手架里唯一「纯派生、没有用户\n * 内容」的产物,其余文件都可能被改过,不该被一次刷新抹掉。\n */\nexport function refreshAgentsGuide(targetDir: string): RefreshGuideResult {\n\tconst root = resolve(targetDir);\n\tconst manifestPath = join(root, \"plugin.json\");\n\tif (existsSync(manifestPath)) {\n\t\tconst manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as { id?: unknown; name?: unknown };\n\t\tconst pluginId = typeof manifest.id === \"string\" ? manifest.id : undefined;\n\t\tif (!pluginId) throw new Error(`plugin.json at ${root} has no id`);\n\t\tconst displayName = typeof manifest.name === \"string\" && manifest.name.length > 0 ? manifest.name : pluginId;\n\t\twriteFileSync(join(root, \"AGENTS.md\"), renderAgentsGuide({ pluginId, displayName }), \"utf8\");\n\t\treturn { root, kind: \"plugin\", file: join(root, \"AGENTS.md\") };\n\t}\n\n\tconst hubManifest = join(root, \".vetta\", \"marketplace.json\");\n\tif (existsSync(hubManifest)) {\n\t\tconst manifest = JSON.parse(readFileSync(hubManifest, \"utf8\")) as { name?: unknown };\n\t\tconst name = typeof manifest.name === \"string\" && manifest.name.length > 0 ? manifest.name : \"marketplace\";\n\t\twriteFileSync(join(root, \"AGENTS.md\"), renderHubAgentsGuide({ name }), \"utf8\");\n\t\treturn { root, kind: \"hub\", file: join(root, \"AGENTS.md\") };\n\t}\n\n\tthrow new Error(`Not a plugin project or marketplace repository: ${root}`);\n}\n\nconst HUB_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;\nconst APP_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/;\n\nexport interface InitHubInput {\n\treadonly targetDir: string;\n\t/** 市场名(slug)。 */\n\treadonly name: string;\n\treadonly repository: string;\n\t/** 本市场的能力所支持的最老 Vetta 版本。 */\n\treadonly minAppVersion: string;\n}\n\nexport interface InitHubResult {\n\treadonly root: string;\n\treadonly name: string;\n\treadonly files: readonly string[];\n}\n\n/** 能力按类型分目录;空目录留 .gitkeep,否则 git 不会带上它们,作者得自己猜该放哪。 */\nconst HUB_ABILITY_DIRS = [\"plugins\", \"mcp\", \"skills\", \"scenes\"] as const;\n\nexport function initHubRepository(input: InitHubInput): InitHubResult {\n\tif (!HUB_NAME_PATTERN.test(input.name)) {\n\t\tthrow new Error(`Invalid marketplace name ${JSON.stringify(input.name)}: use lowercase kebab-case`);\n\t}\n\tlet repository: URL;\n\ttry {\n\t\trepository = new URL(input.repository);\n\t} catch {\n\t\tthrow new Error(`Invalid repository URL: ${input.repository}`);\n\t}\n\tif (repository.protocol !== \"https:\") throw new Error(\"Repository URL must use https://\");\n\tif (!APP_VERSION_PATTERN.test(input.minAppVersion)) {\n\t\tthrow new Error(`Invalid --min-app-version ${JSON.stringify(input.minAppVersion)}: expected a version like 0.55.0`);\n\t}\n\n\tconst root = resolve(input.targetDir);\n\tconst manifestRelativePath = join(\".vetta\", \"marketplace.json\");\n\tif (existsSync(join(root, manifestRelativePath))) {\n\t\tthrow new Error(`Refusing to overwrite an existing marketplace at ${root}`);\n\t}\n\n\tconst files: Record<string, string> = {\n\t\t[manifestRelativePath]: json({\n\t\t\tschemaVersion: 2,\n\t\t\tname: input.name,\n\t\t\tmarketplaceVersion: \"1.0.0\",\n\t\t\trepository: repository.toString().replace(/\\/$/, \"\"),\n\t\t\tminAppVersion: input.minAppVersion,\n\t\t\tabilities: [],\n\t\t}),\n\t\t\"AGENTS.md\": renderHubAgentsGuide({ name: input.name }),\n\t\t\"README.md\": renderHubReadme({ name: input.name, repository: repository.toString().replace(/\\/$/, \"\") }),\n\t\t[join(\".github\", \"workflows\", \"marketplace.yml\")]: renderHubWorkflow(),\n\t\t// dist/ 刻意不忽略:客户端直接读能力目录安装,不会替作者构建。\n\t\t\".gitignore\": \"node_modules/\\nrelease/\\n\",\n\t};\n\tfor (const dir of HUB_ABILITY_DIRS) files[join(\"abilities\", dir, \".gitkeep\")] = \"\";\n\n\tfor (const [relativePath, content] of Object.entries(files)) {\n\t\tconst target = join(root, relativePath);\n\t\tmkdirSync(dirname(target), { recursive: true });\n\t\twriteFileSync(target, content, \"utf8\");\n\t}\n\n\treturn { root, name: input.name, files: Object.keys(files).sort() };\n}\n"]}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAKA,0GAAwC;AACxC,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAC1C,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAI3C,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAUD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAkH1E;AAED,MAAM,WAAW,mBAAmB;IACnC,uGAA+C;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,qCAAiB;IACjB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4EAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,kBAAkB,CAiC3G;AAwDD,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6BAAiB;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2DAA6B;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,CA6CpE","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { readAgentsGuideRevision, renderAgentsGuide } from \"./agents-template.js\";\nimport { renderHubAgentsGuide, renderHubReadme, renderHubWorkflow } from \"./hub-template.js\";\n\n/** 与脚手架一同落地的依赖范围;两个包各自独立发布,不要合成一个版本。 */\nexport const DEFAULT_SDK_RANGE = \"^0.3.2\";\nexport const DEFAULT_VITE_RANGE = \"^0.2.0\";\n\nconst PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;\n\nexport interface InitPluginInput {\n\treadonly targetDir: string;\n\treadonly pluginId: string;\n\treadonly displayName: string;\n\treadonly sdkRange?: string;\n\treadonly viteRange?: string;\n}\n\nexport interface InitPluginResult {\n\treadonly root: string;\n\treadonly pluginId: string;\n\treadonly files: readonly string[];\n}\n\nfunction remoteNameFromId(pluginId: string): string {\n\treturn pluginId.replace(/-/g, \"_\").replace(/[^A-Za-z0-9_$]/g, \"_\");\n}\n\nfunction json(value: unknown): string {\n\treturn `${JSON.stringify(value, null, \"\\t\")}\\n`;\n}\n\nexport function initPluginProject(input: InitPluginInput): InitPluginResult {\n\tif (!PLUGIN_ID_PATTERN.test(input.pluginId)) {\n\t\tthrow new Error(`Invalid plugin id ${JSON.stringify(input.pluginId)}: use lowercase kebab-case starting with a letter`);\n\t}\n\tconst root = resolve(input.targetDir);\n\tif (existsSync(join(root, \"plugin.json\"))) {\n\t\tthrow new Error(`Refusing to overwrite an existing plugin at ${root}`);\n\t}\n\n\tconst remote = remoteNameFromId(input.pluginId);\n\n\tconst files: Record<string, string> = {\n\t\t\"plugin.json\": json({\n\t\t\tid: input.pluginId,\n\t\t\tname: input.displayName,\n\t\t\tversion: \"0.1.0\",\n\t\t\tpluginApiVersion: \"^2.0.0\",\n\t\t\tentry: \"dist/mf-manifest.json\",\n\t\t\tmoduleFederation: { remoteName: remote, expose: \"./plugin\" },\n\t\t\tstyles: [\"dist/style.css\"],\n\t\t\tpermissions: [],\n\t\t\tdescription: input.displayName,\n\t\t\tauthor: \"\",\n\t\t\ticon: \"solar:widget-add-bold\",\n\t\t\tguidingWords: [],\n\t\t}),\n\t\t\"package.json\": json({\n\t\t\tname: input.pluginId,\n\t\t\tversion: \"0.1.0\",\n\t\t\tprivate: true,\n\t\t\ttype: \"module\",\n\t\t\tscripts: {\n\t\t\t\tdev: \"vetta-plugin dev\",\n\t\t\t\tbuild: \"vite build\",\n\t\t\t\tcheck: \"tsc --noEmit\",\n\t\t\t\tpack: \"vetta-plugin pack\",\n\t\t\t\tvalidate: \"vetta-plugin validate\",\n\t\t\t\tdocs: \"vetta-plugin-cli docs\",\n\t\t\t\t// 一条命令走完「构建 → 打包 → 装进正在运行的 Vetta」。\n\t\t\t\t\"install:vetta\": \"vite build && vetta-plugin pack && vetta-plugin-cli add .\",\n\t\t\t},\n\t\t\tdevDependencies: {\n\t\t\t\t\"@tailwindcss/vite\": \"^4.1.12\",\n\t\t\t\t\"@types/react\": \"^19.1.1\",\n\t\t\t\t\"@types/react-dom\": \"^19.1.1\",\n\t\t\t\t\"@vetta-org/plugin-cli\": \"^0.1.1\",\n\t\t\t\t\"@vetta-org/plugin-sdk\": input.sdkRange ?? DEFAULT_SDK_RANGE,\n\t\t\t\t\"@vetta-org/plugin-vite\": input.viteRange ?? DEFAULT_VITE_RANGE,\n\t\t\t\treact: \"19.1.1\",\n\t\t\t\t\"react-dom\": \"19.1.1\",\n\t\t\t\ttailwindcss: \"^4.1.12\",\n\t\t\t\ttypescript: \"^5.9.2\",\n\t\t\t\tvite: \"^7.1.7\",\n\t\t\t},\n\t\t}),\n\t\t\"tsconfig.json\": json({\n\t\t\tcompilerOptions: {\n\t\t\t\ttarget: \"ES2022\",\n\t\t\t\tmodule: \"ESNext\",\n\t\t\t\tlib: [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n\t\t\t\tstrict: true,\n\t\t\t\tesModuleInterop: true,\n\t\t\t\tskipLibCheck: true,\n\t\t\t\tmoduleResolution: \"bundler\",\n\t\t\t\tjsx: \"react-jsx\",\n\t\t\t\tjsxImportSource: \"react\",\n\t\t\t\tnoEmit: true,\n\t\t\t},\n\t\t\tinclude: [\"src/**/*.ts\", \"src/**/*.tsx\"],\n\t\t}),\n\t\t\"vite.config.ts\": `import tailwindcss from \"@tailwindcss/vite\";\nimport { vettaPluginFederation } from \"@vetta-org/plugin-vite\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n\tplugins: [\n\t\ttailwindcss(),\n\t\tvettaPluginFederation({\n\t\t\tname: \"${remote}\",\n\t\t\tentry: \"./src/index.tsx\",\n\t\t}),\n\t],\n\tesbuild: { jsx: \"automatic\", jsxImportSource: \"react\" },\n});\n`,\n\t\t\"src/index.tsx\": `import { definePlugin } from \"@vetta-org/plugin-sdk\";\n// Tailwind pipeline only — business CSS here would leak into the host page.\nimport \"./style.css\";\n\nexport default definePlugin({\n\tactivate(ctx) {\n\t\t// Read the manual before adding contributions: npx vetta-plugin-cli docs\n\t\tvoid ctx;\n\t},\n});\n`,\n\t\t\"src/style.css\": `/* Tailwind entry only. No business selectors — they inject into the host page. */\n@layer theme, base, components, utilities;\n@import \"tailwindcss/theme.css\" layer(theme);\n@import \"tailwindcss/utilities.css\" layer(utilities);\n`,\n\t\t// dist/ 刻意不忽略:插件通过仓库目录分发时,宿主直接读 plugin.json 指向的 entry 与\n\t\t// styles,它不会替你构建——目录里没有构建产物就装不上,而且那是一个只在别人机器上\n\t\t// 复现的失败。\n\t\t\".gitignore\": \"release/\\nnode_modules/\\n\",\n\t\t\"AGENTS.md\": renderAgentsGuide({ pluginId: input.pluginId, displayName: input.displayName }),\n\t};\n\n\tmkdirSync(join(root, \"src\"), { recursive: true });\n\tfor (const [relativePath, content] of Object.entries(files)) {\n\t\twriteFileSync(join(root, relativePath), content, \"utf8\");\n\t}\n\n\treturn { root, pluginId: input.pluginId, files: Object.keys(files).sort() };\n}\n\nexport interface RefreshGuideOptions {\n\t/** 覆盖一份没有版本戳的 `AGENTS.md`。缺省拒绝——没有戳的多半是手写的。 */\n\treadonly force?: boolean;\n\t/** 只算出内容,不落盘。 */\n\treadonly dryRun?: boolean;\n}\n\nexport interface RefreshGuideResult {\n\treadonly root: string;\n\treadonly kind: \"plugin\" | \"hub\";\n\treadonly file: string;\n\t/** 这次生成的说明书正文。`dryRun` 时用它做人工合并。 */\n\treadonly content: string;\n\treadonly written: boolean;\n}\n\n/**\n * 在已有的工程或能力市场仓库里重写 `AGENTS.md`。\n *\n * `init` 拒绝覆盖已有工程,所以老目录里那份说明书从落地起就再也没变过——它写于某个版本的\n * SDK,之后新增的约定一条都没有。这里只重写这一个文件:它是脚手架里唯一「纯派生、没有用户\n * 内容」的产物,其余文件都可能被改过,不该被一次刷新抹掉。\n *\n * **只有确实由脚手架生成的那份才算纯派生。** 没有版本戳的文件无法与手写内容区分——能力市场\n * 仓库的根 `AGENTS.md` 往往是一整本手写的市场规范——所以一律拒绝覆盖,让调用方拿 `dryRun`\n * 的内容去人工合并,或显式 `force`。宁可少刷新一份,也不能悄悄删掉别人写的东西。\n */\nexport function refreshAgentsGuide(targetDir: string, options: RefreshGuideOptions = {}): RefreshGuideResult {\n\tconst root = resolve(targetDir);\n\tconst file = join(root, \"AGENTS.md\");\n\tconst manifestPath = join(root, \"plugin.json\");\n\tconst hubManifestPath = join(root, \".vetta\", \"marketplace.json\");\n\n\tlet kind: \"plugin\" | \"hub\";\n\tlet content: string;\n\tif (existsSync(manifestPath)) {\n\t\tconst manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as { id?: unknown; name?: unknown };\n\t\tconst pluginId = typeof manifest.id === \"string\" ? manifest.id : undefined;\n\t\tif (!pluginId) throw new Error(`plugin.json at ${root} has no id`);\n\t\tkind = \"plugin\";\n\t\tcontent = renderAgentsGuide({\n\t\t\tpluginId,\n\t\t\t// 多语言插件的 name 是 `%plugin.name%`,直接当标题会把占位符印在文件开头。\n\t\t\tdisplayName: resolveManifestText(manifest.name, root, pluginId),\n\t\t\tscripts: readPackageScripts(root),\n\t\t});\n\t} else if (existsSync(hubManifestPath)) {\n\t\tconst manifest = JSON.parse(readFileSync(hubManifestPath, \"utf8\")) as { name?: unknown };\n\t\tkind = \"hub\";\n\t\tcontent = renderHubAgentsGuide({\n\t\t\tname: typeof manifest.name === \"string\" && manifest.name.length > 0 ? manifest.name : \"marketplace\",\n\t\t});\n\t} else {\n\t\tthrow new Error(`Not a plugin project or marketplace repository: ${root}`);\n\t}\n\n\tif (!options.dryRun) assertSafeToOverwrite(file, options.force === true);\n\tif (options.dryRun) return { root, kind, file, content, written: false };\n\twriteFileSync(file, content, \"utf8\");\n\treturn { root, kind, file, content, written: true };\n}\n\n/** 已有文件必须是本模板生成的(带版本戳)才允许重写。 */\nfunction assertSafeToOverwrite(file: string, force: boolean): void {\n\tif (force || !existsSync(file)) return;\n\tif (readAgentsGuideRevision(readFileSync(file, \"utf8\")) !== undefined) return;\n\tthrow new Error(\n\t\t`${file} has no vetta-guide-revision marker, so it looks hand-written rather than scaffolded. ` +\n\t\t\t\"Refusing to overwrite it. Review the new template with `--dry-run`, merge what you want by hand, \" +\n\t\t\t\"or pass `--force` to replace the file.\",\n\t);\n}\n\n/**\n * 解析 manifest 里的 `%key%` 占位;解析不到就退回插件 id。\n *\n * 规则与宿主一致:整串恰好是 `%key%` 才查表,否则原样返回。\n */\nfunction resolveManifestText(raw: unknown, root: string, fallback: string): string {\n\tif (typeof raw !== \"string\" || raw.length === 0) return fallback;\n\tconst match = /^%([^%]+)%$/.exec(raw);\n\tif (!match) return raw;\n\tconst key = match[1]!;\n\tconst pluginManifest = readJsonFile(join(root, \"plugin.json\")) as { defaultLocale?: unknown } | undefined;\n\tconst locale = typeof pluginManifest?.defaultLocale === \"string\" ? pluginManifest.defaultLocale : \"zh\";\n\tfor (const candidate of [locale, \"zh\", \"en\"]) {\n\t\tconst table = readJsonFile(join(root, \"locales\", `${candidate}.json`));\n\t\tconst value = table?.[key];\n\t\tif (typeof value === \"string\" && value.length > 0) return value;\n\t}\n\treturn fallback;\n}\n\n/** 工程实际有的 npm scripts;读不到就当没有。 */\nfunction readPackageScripts(root: string): string[] {\n\tconst pkg = readJsonFile(join(root, \"package.json\")) as { scripts?: unknown } | undefined;\n\tconst scripts = pkg?.scripts;\n\tif (typeof scripts !== \"object\" || scripts === null || Array.isArray(scripts)) return [];\n\treturn Object.keys(scripts as Record<string, unknown>);\n}\n\nfunction readJsonFile(path: string): Record<string, unknown> | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"));\n\t\treturn typeof parsed === \"object\" && parsed !== null && !Array.isArray(parsed)\n\t\t\t? (parsed as Record<string, unknown>)\n\t\t\t: undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nconst HUB_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;\nconst APP_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/;\n\nexport interface InitHubInput {\n\treadonly targetDir: string;\n\t/** 市场名(slug)。 */\n\treadonly name: string;\n\treadonly repository: string;\n\t/** 本市场的能力所支持的最老 Vetta 版本。 */\n\treadonly minAppVersion: string;\n}\n\nexport interface InitHubResult {\n\treadonly root: string;\n\treadonly name: string;\n\treadonly files: readonly string[];\n}\n\n/** 能力按类型分目录;空目录留 .gitkeep,否则 git 不会带上它们,作者得自己猜该放哪。 */\nconst HUB_ABILITY_DIRS = [\"plugins\", \"mcp\", \"skills\", \"scenes\"] as const;\n\nexport function initHubRepository(input: InitHubInput): InitHubResult {\n\tif (!HUB_NAME_PATTERN.test(input.name)) {\n\t\tthrow new Error(`Invalid marketplace name ${JSON.stringify(input.name)}: use lowercase kebab-case`);\n\t}\n\tlet repository: URL;\n\ttry {\n\t\trepository = new URL(input.repository);\n\t} catch {\n\t\tthrow new Error(`Invalid repository URL: ${input.repository}`);\n\t}\n\tif (repository.protocol !== \"https:\") throw new Error(\"Repository URL must use https://\");\n\tif (!APP_VERSION_PATTERN.test(input.minAppVersion)) {\n\t\tthrow new Error(`Invalid --min-app-version ${JSON.stringify(input.minAppVersion)}: expected a version like 0.55.0`);\n\t}\n\n\tconst root = resolve(input.targetDir);\n\tconst manifestRelativePath = join(\".vetta\", \"marketplace.json\");\n\tif (existsSync(join(root, manifestRelativePath))) {\n\t\tthrow new Error(`Refusing to overwrite an existing marketplace at ${root}`);\n\t}\n\n\tconst files: Record<string, string> = {\n\t\t[manifestRelativePath]: json({\n\t\t\tschemaVersion: 2,\n\t\t\tname: input.name,\n\t\t\tmarketplaceVersion: \"1.0.0\",\n\t\t\trepository: repository.toString().replace(/\\/$/, \"\"),\n\t\t\tminAppVersion: input.minAppVersion,\n\t\t\tabilities: [],\n\t\t}),\n\t\t\"AGENTS.md\": renderHubAgentsGuide({ name: input.name }),\n\t\t\"README.md\": renderHubReadme({ name: input.name, repository: repository.toString().replace(/\\/$/, \"\") }),\n\t\t[join(\".github\", \"workflows\", \"marketplace.yml\")]: renderHubWorkflow(),\n\t\t// dist/ 刻意不忽略:客户端直接读能力目录安装,不会替作者构建。\n\t\t\".gitignore\": \"node_modules/\\nrelease/\\n\",\n\t};\n\tfor (const dir of HUB_ABILITY_DIRS) files[join(\"abilities\", dir, \".gitkeep\")] = \"\";\n\n\tfor (const [relativePath, content] of Object.entries(files)) {\n\t\tconst target = join(root, relativePath);\n\t\tmkdirSync(dirname(target), { recursive: true });\n\t\twriteFileSync(target, content, \"utf8\");\n\t}\n\n\treturn { root, name: input.name, files: Object.keys(files).sort() };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vetta-org/plugin-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Install npm-distributed plugins into Vetta Desktop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",