@vetta-org/plugin-cli 0.1.4 → 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/dist/index.js CHANGED
@@ -3646,7 +3646,7 @@ var require_npa = __commonJS((exports, module) => {
3646
3646
  });
3647
3647
 
3648
3648
  // src/command.ts
3649
- import { existsSync as existsSync4, statSync as statSync2 } from "node:fs";
3649
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
3650
3650
  import { join as join6, resolve as resolve5 } from "node:path";
3651
3651
  import { parseArgs } from "node:util";
3652
3652
 
@@ -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}$" }),
@@ -18919,107 +18921,93 @@ async function resolveNpmPluginArchive(packageSpec, pack = runNpmPack) {
18919
18921
  }
18920
18922
  }
18921
18923
 
18922
- // src/init.ts
18923
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18924
- import { dirname, join as join3, resolve as resolve2 } from "node:path";
18925
-
18926
18924
  // src/agents-template.ts
18925
+ var AGENTS_GUIDE_REVISION = 3;
18926
+ function readAgentsGuideRevision(content) {
18927
+ const match2 = /<!--\s*vetta-guide-revision:\s*(\d+)\s*-->/.exec(content);
18928
+ if (!match2)
18929
+ return;
18930
+ return Number(match2[1]);
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
+ }
18927
18952
  function renderAgentsGuide(input) {
18928
- return `# ${input.displayName}
18953
+ return `<!-- vetta-guide-revision: ${AGENTS_GUIDE_REVISION} -->
18954
+ # ${input.displayName}
18929
18955
 
18930
- Vetta 桌面插件工程。**先读手册再写代码**——不要凭记忆写 SDK API,这套合同变化很快。
18956
+ Vetta 桌面插件工程(插件 id:\`${input.pluginId}\`)。
18931
18957
 
18932
- ## 第一步:装依赖,然后找到手册
18958
+ **本文件不讲规则,只告诉你去哪读。** 规则在手册里,手册随 SDK 升级;写在这里的任何一条都会
18959
+ 停在这个工程创建那天。两者冲突时一律以手册为准。
18960
+
18961
+ ## 第一步:装依赖,找到手册
18933
18962
 
18934
18963
  \`\`\`bash
18935
18964
  npm install
18936
- npx vetta-plugin-cli docs
18965
+ npx vetta-plugin-cli docs --check-latest
18937
18966
  \`\`\`
18938
18967
 
18939
18968
  \`npm install\` 必须先跑:\`vetta-plugin-cli\` 是 \`@vetta-org/plugin-cli\` 的命令名,装完才在
18940
18969
  \`node_modules/.bin\` 里。还没装就想跑,用全名 \`npx @vetta-org/plugin-cli docs\`。
18941
18970
 
18942
- **手册会过期。** 它是随 SDK 装进 \`node_modules\` 的快照,工程不升级就永远停在初始化那天的
18943
- 版本——本文件同样是那天的快照。开工前先确认一次:
18944
-
18945
- \`\`\`bash
18946
- npx vetta-plugin-cli docs --check-latest
18947
- \`\`\`
18948
-
18949
- 落后就按它打印的命令升级 SDK 再重读手册。\`docs\` 的输出永远比本文件新(\`npx\` 默认取最新的
18950
- CLI),**两者冲突时以它为准**。
18971
+ 它打印手册目录的**绝对路径**、手册对应的 SDK 版本,以及本工程与所属 hub 的位置。
18972
+ **不要硬编码这个路径**:工作区可能把依赖提升到仓库根,一仓多插件时各插件还可能钉不同版本。
18951
18973
 
18952
- 它打印出随 \`@vetta-org/plugin-sdk\` 一起装进 \`node_modules\` 的手册目录**绝对路径**,以及
18953
- 这份手册对应的 SDK 版本。**不要硬编码这个路径**:工作区可能把依赖提升到仓库根,一仓多插件
18954
- 时各插件还可能钉不同的 SDK 版本。
18974
+ 输出里出现 \`Manual is behind\`\`This brief is stale\` 就按它给的命令升级,再重跑一次。
18955
18975
 
18956
- 拿到路径后,用 read 工具按这个顺序打开:
18976
+ ## 第二步:按顺序读手册
18957
18977
 
18958
18978
  | 顺序 | 文件 | 何时读 |
18959
18979
  | --- | --- | --- |
18960
- | 1 | \`README.md\` | 总是先读:能力矩阵、信任模型、导航 |
18980
+ | 1 | \`README.md\` | **总是先读**:能力矩阵、信任模型、**不可违反的红线**、导航 |
18961
18981
  | 2 | \`getting-started.md\` | 首次写代码、构建、安装调试 |
18962
18982
  | 3 | \`manifest.md\` | 写/改 \`plugin.json\`、贡献智能体与团队 |
18963
18983
  | 4 | \`permissions.md\` | 选定权限列表之前 |
18964
- | 5 | 按扩展点选读 | \`ui-slots.md\` / \`conversation-and-agent.md\` / \`message-cards.md\` / \`mcp.md\` / \`ai.md\` / \`browser.md\` / \`app-actions.md\` |
18984
+ | 5 | 按扩展点选读 | \`ui-slots.md\` / \`conversation-and-agent.md\` / \`message-cards.md\` / \`mcp.md\` / \`ai.md\` / \`browser.md\` / \`app-actions.md\` / \`styling-and-pitfalls.md\` |
18965
18985
 
18966
- 实现任一扩展点**之前**再读对应那章。手册是唯一真源,本文件与它冲突时以手册为准。
18986
+ 实现任一扩展点**之前**再读对应那章,不要凭记忆写 SDK API——这套合同变化很快。
18967
18987
 
18968
- ## 开发闭环
18988
+ ## 常用命令
18969
18989
 
18970
18990
  \`\`\`bash
18971
- npm run dev # Vite + Module Federation 开发服务器
18972
- npm run build # 产出 dist/
18973
- npm run install:vetta # 打包并装进正在运行的 Vetta(需要 Vetta 已启动)
18974
- npx vetta-plugin-cli watch # 开热更新:宿主改从本工程目录加载,改完即生效
18975
- npx vetta-plugin-cli uninstall # 卸载(省略 id 即本工程对应的插件)
18991
+ ${renderCommands(input.pluginId, input.scripts ?? ["dev", "build", "install:vetta"])}
18976
18992
  \`\`\`
18977
18993
 
18978
- \`install:vetta\` \`vetta-plugin-cli add .\`:它找到本工程打出来的归档,交给正在运行的
18979
- Desktop 校验、授权、安装。它**不会**直接写 \`~/.vetta/plugins\`。
18980
-
18981
- 装完若提示有 pending 版本,用 \`npx vetta-plugin-cli reload ${input.pluginId}\` 让宿主应用它。
18994
+ 细节都在 \`getting-started.md\`。\`docs\` 打印了 \`Marketplace index:\` 就说明这个目录之上有能力
18995
+ 市场索引,改完 \`version\` / \`permissions\` / \`pluginApiVersion\` 后要回仓库根跑 \`sync\`。
18982
18996
 
18983
- 开发期建议开热更新(\`watch\`):之后改源码即时生效,不用每次重新打包安装。改 \`plugin.json\`
18984
- 的权限或命令声明时仍需重新安装一次,让宿主把授权落盘。\`watch --stop\` 关闭。
18985
-
18986
- ## 如果这个目录之上有能力市场索引
18987
-
18988
- \`vetta-plugin-cli docs\` 会告诉你有没有(它会打印 \`Marketplace index:\`)。有的话,**改完
18989
- \`version\` / \`permissions\` / \`pluginApiVersion\` 之后要回仓库根跑一次**:
18990
-
18991
- \`\`\`bash
18992
- npx @vetta-org/plugin-cli sync # 从各能力目录回填索引,并推进 marketplaceVersion
18993
- npx @vetta-org/plugin-cli sync --check # 只报不写,CI 用
18994
- \`\`\`
18995
-
18996
- 索引里的 \`version\` 与 \`plugin.json\` 的 \`version\` 必须**完全相等**,否则宿主同步直接失败;
18997
- 而内容变了却不换 \`marketplaceVersion\` 时,客户端既不报错也不更新——用户只是永远收不到。
18998
- \`add .\` 装完若检测到索引还停在旧版本,会当场提醒你。
18999
-
19000
- ## 不可违反的几条
19001
-
19002
- - **样式只用 Tailwind \`className\`**。禁止新建业务 CSS、禁止在 \`style.css\` 里写 \`button\`/\`div\`/\`*\`
19003
- 这类选择器——插件与宿主共享同一个页面,全局选择器会污染整个 UI。
19004
- - **可能失败的路径必须上报**:读文件、解析、网络、外部库的 catch 里调用
19005
- \`ctx.ui.notify({ message, error })\`(无需权限)。禁止只写死「失败」文案并丢掉原始 error。
19006
- - **权限按需最小声明**。构建期会校验产物用到的能力与 \`plugin.json\` 的声明是否匹配,缺了直接
19007
- 构建失败。但 UI 槽位不在这条校验里——那类缺权限在运行时只是静默跳过,所以对着手册核对。
19008
- - **不要写 \`agent_mode\`**(已废弃,无运行时语义)。想收窄某个工具的使用场景,把「何时不该用它 +
19009
- 替代做法」写进该工具 description 的反向触发段。
19010
- - **顶层不要出现依赖共享 React 的 JSX**,放进组件或 \`activate\` 内(Module Federation 的加载时序)。
19011
- - 依赖用 registry 上已发布的 semver,不要 \`workspace:*\`。
19012
- - **\`dist/\` 要进版本库**。插件通过仓库目录分发时,宿主直接读 \`plugin.json\` 指向的 \`entry\`
19013
- 与 \`styles\`,它不会替你构建——目录里没有构建产物就装不上。
19014
18997
  ## 信息不足时
19015
18998
 
19016
18999
  插件 id、展示名、要用哪些权限、功能边界、是否立刻安装——**问用户**,不要自己假定。
19017
19000
  `;
19018
19001
  }
19019
19002
 
19003
+ // src/init.ts
19004
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19005
+ import { dirname, join as join3, resolve as resolve2 } from "node:path";
19006
+
19020
19007
  // src/hub-template.ts
19021
19008
  function renderHubAgentsGuide(input) {
19022
- return `# ${input.name}
19009
+ return `<!-- vetta-guide-revision: ${AGENTS_GUIDE_REVISION} -->
19010
+ # ${input.name}
19023
19011
 
19024
19012
  Vetta 能力市场仓库。本仓库索引若干**能力**(plugin / mcp / skill / scene / bundle),
19025
19013
  每个能力是 \`abilities/\` 下的一个自包含目录。
@@ -19053,16 +19041,25 @@ npx @vetta-org/plugin-cli init --id <slug> --name "<Display Name>" abilities/plu
19053
19041
 
19054
19042
  ## 索引由工具对账,不要手改派生字段
19055
19043
 
19056
- \`.vetta/marketplace.json\` 里能力的 \`version\`、\`config.api_version\`、\`config.permissions\`、
19057
- \`config.commands\` 全都是从能力包推导出来的。改完能力后:
19044
+ 改完能力后:
19058
19045
 
19059
19046
  \`\`\`bash
19060
- npx @vetta-org/plugin-cli sync # 回填派生字段,并推进 marketplaceVersion
19047
+ npx @vetta-org/plugin-cli sync # 对账并回填,看输出
19061
19048
  npx @vetta-org/plugin-cli sync --check # 只报不写,非零退出(CI 用)
19062
19049
  \`\`\`
19063
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
+
19064
19061
  要**手写**的只有身份与展示:\`slug\`、\`name\`、\`description\`、\`source.path\`、\`category\`、\`tags\`、
19065
- \`detail\`。新能力上架时手动加一条这样的条目,其余字段交给 \`sync\`。
19062
+ \`detail\`。新能力上架时手动加一条这样的条目。
19066
19063
 
19067
19064
  三条容易踩的约束,\`sync --check\` 会替你守住:
19068
19065
 
@@ -19072,7 +19069,7 @@ npx @vetta-org/plugin-cli sync --check # 只报不写,非零退出(CI 用
19072
19069
  | \`plugin.json\` 的 \`entry\` / \`styles\` 必须在已发布目录里真实存在 | 本地能装,市场上装不了 |
19073
19070
  | 改了任何内容必须换 \`marketplaceVersion\` | 客户端不报错、也不更新,用户永远收不到 |
19074
19071
 
19075
- 第三条最阴险——它不报错。
19072
+ 第三条最阴险——它不报错。\`sync\` 只在版本号是 semver 或整数时才推得动,其余格式要你自己换。
19076
19073
 
19077
19074
  ## 为什么插件目录里要提交 \`dist/\`
19078
19075
 
@@ -19240,26 +19237,80 @@ node_modules/
19240
19237
  }
19241
19238
  return { root, pluginId: input.pluginId, files: Object.keys(files).sort() };
19242
19239
  }
19243
- function refreshAgentsGuide(targetDir) {
19240
+ function refreshAgentsGuide(targetDir, options = {}) {
19244
19241
  const root = resolve2(targetDir);
19242
+ const file = join3(root, "AGENTS.md");
19245
19243
  const manifestPath = join3(root, "plugin.json");
19244
+ const hubManifestPath = join3(root, ".vetta", "marketplace.json");
19245
+ let kind;
19246
+ let content;
19246
19247
  if (existsSync(manifestPath)) {
19247
19248
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
19248
19249
  const pluginId = typeof manifest.id === "string" ? manifest.id : undefined;
19249
19250
  if (!pluginId)
19250
19251
  throw new Error(`plugin.json at ${root} has no id`);
19251
- const displayName = typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : pluginId;
19252
- writeFileSync(join3(root, "AGENTS.md"), renderAgentsGuide({ pluginId, displayName }), "utf8");
19253
- 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}`);
19254
19266
  }
19255
- const hubManifest = join3(root, ".vetta", "marketplace.json");
19256
- if (existsSync(hubManifest)) {
19257
- const manifest = JSON.parse(readFileSync(hubManifest, "utf8"));
19258
- const name = typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : "marketplace";
19259
- writeFileSync(join3(root, "AGENTS.md"), renderHubAgentsGuide({ name }), "utf8");
19260
- return { root, kind: "hub", file: join3(root, "AGENTS.md") };
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;
19295
+ }
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;
19261
19313
  }
19262
- throw new Error(`Not a plugin project or marketplace repository: ${root}`);
19263
19314
  }
19264
19315
  var HUB_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
19265
19316
  var APP_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
@@ -19319,7 +19370,7 @@ function detectJsonIndent(source) {
19319
19370
  const match2 = /\n([ \t]+)\S/.exec(source);
19320
19371
  return match2?.[1] ?? "\t";
19321
19372
  }
19322
- function readJsonFile(path) {
19373
+ function readJsonFile2(path) {
19323
19374
  try {
19324
19375
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
19325
19376
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
@@ -19363,7 +19414,7 @@ function nextMarketplaceVersion(current) {
19363
19414
  return;
19364
19415
  }
19365
19416
  function syncMarketplaceIndex(input) {
19366
- const manifest = readJsonFile(input.manifestPath);
19417
+ const manifest = readJsonFile2(input.manifestPath);
19367
19418
  if (!manifest)
19368
19419
  throw new Error(`Malformed marketplace manifest: ${input.manifestPath}`);
19369
19420
  const abilities = Array.isArray(manifest.abilities) ? manifest.abilities : [];
@@ -19432,7 +19483,7 @@ function syncMarketplaceIndex(input) {
19432
19483
  }
19433
19484
  function reconcilePlugin(context) {
19434
19485
  const { entry, slug, abilityDir, changes, problems } = context;
19435
- const manifest = readJsonFile(join4(abilityDir, "plugin.json"));
19486
+ const manifest = readJsonFile2(join4(abilityDir, "plugin.json"));
19436
19487
  if (!manifest) {
19437
19488
  problems.push({ slug, message: "plugin.json is missing or malformed" });
19438
19489
  return;
@@ -19473,7 +19524,7 @@ function reconcilePlugin(context) {
19473
19524
  }
19474
19525
  function reconcileIdentityFile(context) {
19475
19526
  const { entry, slug, abilityDir, fileName, changes, problems } = context;
19476
- const identity = readJsonFile(join4(abilityDir, fileName));
19527
+ const identity = readJsonFile2(join4(abilityDir, fileName));
19477
19528
  if (!identity) {
19478
19529
  problems.push({ slug, message: `${fileName} is missing or malformed` });
19479
19530
  return;
@@ -19516,7 +19567,7 @@ function findAbilityDirectories(hubRoot) {
19516
19567
  return found.sort();
19517
19568
  }
19518
19569
  function describeIndexDrift(input) {
19519
- const manifest = readJsonFile(input.manifestPath);
19570
+ const manifest = readJsonFile2(input.manifestPath);
19520
19571
  if (!manifest || !Array.isArray(manifest.abilities))
19521
19572
  return;
19522
19573
  const entry = manifest.abilities.find((item) => typeof item === "object" && item !== null && !Array.isArray(item) && item.slug === input.slug);
@@ -19606,7 +19657,7 @@ Usage:
19606
19657
  vetta-plugin-cli reload <plugin-id> [--json]
19607
19658
  vetta-plugin-cli docs [--check-latest] [--json]
19608
19659
  vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]
19609
- vetta-plugin-cli init --refresh-guide [dir] [--json]
19660
+ vetta-plugin-cli init --refresh-guide [dir] [--dry-run] [--force] [--json]
19610
19661
  vetta-plugin-cli init hub --name <slug> --repository <url> --min-app-version <x.y.z> [dir]
19611
19662
  vetta-plugin-cli watch [dir] [--stop] [--json]
19612
19663
  vetta-plugin-cli uninstall [plugin-id] [--json]
@@ -19707,7 +19758,9 @@ function parsePluginInitCommand(argv) {
19707
19758
  id: { type: "string" },
19708
19759
  name: { type: "string" },
19709
19760
  json: { type: "boolean" },
19710
- "refresh-guide": { type: "boolean" }
19761
+ "refresh-guide": { type: "boolean" },
19762
+ force: { type: "boolean" },
19763
+ "dry-run": { type: "boolean" }
19711
19764
  }
19712
19765
  });
19713
19766
  } catch (error) {
@@ -19717,7 +19770,13 @@ function parsePluginInitCommand(argv) {
19717
19770
  const [dir, extra] = parsed.positionals;
19718
19771
  if (extra)
19719
19772
  return { type: "error", message: `Unexpected argument: ${extra}` };
19720
- 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
+ };
19721
19780
  }
19722
19781
  const pluginId = parsed.values.id;
19723
19782
  if (typeof pluginId !== "string" || pluginId.length === 0) {
@@ -20060,6 +20119,7 @@ async function runDocsCommand(command, dependencies) {
20060
20119
  const sdkVersion = readManualSdkVersion(manualDir);
20061
20120
  const latestVersion = command.checkLatest ? await dependencies.readLatestSdkVersion?.() : undefined;
20062
20121
  const outdated = sdkVersion !== undefined && latestVersion !== undefined && compareSemver(sdkVersion, latestVersion) < 0;
20122
+ const guide = inspectAgentsGuide(project?.root ?? hub?.root ?? cwd);
20063
20123
  if (command.json) {
20064
20124
  dependencies.writeStdout(`${JSON.stringify({
20065
20125
  ok: true,
@@ -20067,6 +20127,7 @@ async function runDocsCommand(command, dependencies) {
20067
20127
  entry: join6(manualDir, "README.md"),
20068
20128
  sdkVersion,
20069
20129
  refreshCommand: SDK_REFRESH_COMMAND,
20130
+ guide,
20070
20131
  ...command.checkLatest ? { latestVersion, outdated } : {},
20071
20132
  project: project ? { root: project.root, pluginId: project.pluginId, version: project.version } : undefined,
20072
20133
  hub: hub ? {
@@ -20092,6 +20153,11 @@ async function runDocsCommand(command, dependencies) {
20092
20153
  } else {
20093
20154
  lines.push(`Manual follows the installed SDK. To refresh it: ${SDK_REFRESH_COMMAND}`);
20094
20155
  }
20156
+ if (guide.stale) {
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.`);
20160
+ }
20095
20161
  if (hub) {
20096
20162
  lines.push(`Marketplace index: ${hub.manifestPath}`);
20097
20163
  lines.push("After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.");
@@ -20102,6 +20168,24 @@ async function runDocsCommand(command, dependencies) {
20102
20168
  return 0;
20103
20169
  }
20104
20170
  var SDK_REFRESH_COMMAND = "npm i -D @vetta-org/plugin-sdk@latest && npx vetta-plugin-cli docs";
20171
+ var GUIDE_REFRESH_COMMAND = "npx @vetta-org/plugin-cli init --refresh-guide";
20172
+ function inspectAgentsGuide(root) {
20173
+ const path = join6(root, "AGENTS.md");
20174
+ if (!existsSync4(path))
20175
+ return { present: false, stale: false, unstamped: false };
20176
+ let revision;
20177
+ try {
20178
+ revision = readAgentsGuideRevision(readFileSync4(path, "utf8"));
20179
+ } catch {
20180
+ return { present: true, stale: false, unstamped: false };
20181
+ }
20182
+ return {
20183
+ present: true,
20184
+ ...revision === undefined ? {} : { revision },
20185
+ stale: revision !== undefined && revision < AGENTS_GUIDE_REVISION,
20186
+ unstamped: revision === undefined
20187
+ };
20188
+ }
20105
20189
  function compareSemver(left, right) {
20106
20190
  const parse2 = (value) => {
20107
20191
  const match2 = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(value.trim());
@@ -20122,11 +20206,18 @@ function compareSemver(left, right) {
20122
20206
  function runRefreshGuideCommand(command, dependencies) {
20123
20207
  const cwd = dependencies.cwd?.() ?? process.cwd();
20124
20208
  try {
20125
- const result = refreshAgentsGuide(resolve5(cwd, command.targetDir ?? "."));
20126
- dependencies.writeStdout(command.json ? `${JSON.stringify({ ok: true, ...result })}
20127
- ` : `Rewrote ${result.file}
20128
- 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 })}
20129
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}`);
20130
20221
  return 0;
20131
20222
  } catch (error) {
20132
20223
  const message = error instanceof Error ? error.message : String(error);
@@ -20354,6 +20445,7 @@ export {
20354
20445
  refreshAgentsGuide,
20355
20446
  readManualSdkVersion,
20356
20447
  readLatestNpmVersion,
20448
+ readAgentsGuideRevision,
20357
20449
  parsePluginWatchCommand,
20358
20450
  parsePluginUninstallCommand,
20359
20451
  parsePluginSyncCommand,
@@ -20366,5 +20458,6 @@ export {
20366
20458
  findPluginProject,
20367
20459
  findPluginHub,
20368
20460
  DEFAULT_VITE_RANGE,
20369
- DEFAULT_SDK_RANGE
20461
+ DEFAULT_SDK_RANGE,
20462
+ AGENTS_GUIDE_REVISION
20370
20463
  };
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.4",
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",