@vetta-org/plugin-cli 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,aAAa,GAAG,aAAa,GAAG,UAAU,GAAG,oBAAoB,CAAC;AAE3G,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,8FAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,wCAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,oDAAgC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAmED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CA+EjE;AAiHD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,GAAG,SAAS,CAUrB","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { join, relative, resolve, sep } from \"node:path\";\n\n/**\n * 对账能力市场索引(`.vetta/marketplace.json`)与各能力目录。\n *\n * 索引本来就是派生数据,却有一组会咬人的硬约束:条目的 slug/version 必须与能力目录里的身份\n * 文件完全相等,否则宿主同步**直接失败**;`entry`/`styles` 指向的文件必须真实存在,否则本地\n * 能装、市场上装不了;而改了任何内容却没换 `marketplaceVersion` 时,客户端按它判缓存——\n * 既不报错也不更新,用户只是永远收不到新版本。\n *\n * 三种失败里有两种不在作者机器上复现,一种压根不报错。所以这件事必须是工具做的。\n */\n\nexport type SyncChangeKind = \"version\" | \"api_version\" | \"permissions\" | \"commands\" | \"marketplaceVersion\";\n\nexport interface SyncChange {\n\treadonly slug: string;\n\treadonly field: SyncChangeKind;\n\treadonly from: unknown;\n\treadonly to: unknown;\n}\n\nexport interface SyncProblem {\n\treadonly slug: string;\n\treadonly message: string;\n}\n\nexport interface SyncResult {\n\treadonly manifestPath: string;\n\treadonly changes: readonly SyncChange[];\n\treadonly problems: readonly SyncProblem[];\n\t/** 目录里有身份文件、索引里却没有的能力。只报告,不擅自上架。 */\n\treadonly unlisted: readonly string[];\n\t/** apply 时是否真的写了盘。 */\n\treadonly written: boolean;\n}\n\nexport interface SyncInput {\n\treadonly hubRoot: string;\n\treadonly manifestPath: string;\n\t/** false 时只报告不写盘(`--check`)。 */\n\treadonly apply: boolean;\n}\n\nconst SCAN_IGNORED = new Set([\"node_modules\", \".git\", \"dist\", \"release\", \".vetta\", \"assets\", \"test\", \"src\"]);\nconst SCAN_MAX_DEPTH = 5;\n\n/**\n * 探测 JSON 文件用的缩进,回写时沿用。\n *\n * 不这么做的代价很具体:索引文件本是 2 空格,工具按 Tab 重排就会把整份文件写成一个大 diff,\n * 与仓库里其它写这份文件的脚本来回拉锯,任何并发提交都升级成整文件冲突。对账工具只该改它\n * 要改的那几个字段。\n */\nfunction detectJsonIndent(source: string): string {\n\tconst match = /\\n([ \\t]+)\\S/.exec(source);\n\treturn match?.[1] ?? \"\\t\";\n}\n\nfunction readJsonFile(path: string): Record<string, unknown> | undefined {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"));\n\t\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return undefined;\n\t\treturn parsed as Record<string, unknown>;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction stringArray(value: unknown): string[] | undefined {\n\tif (!Array.isArray(value)) return undefined;\n\treturn value.every((item) => typeof item === \"string\") ? [...(value as string[])] : undefined;\n}\n\nfunction sameStringArray(left: unknown, right: readonly string[]): boolean {\n\tconst current = stringArray(left) ?? [];\n\treturn current.length === right.length && current.every((item, index) => item === right[index]);\n}\n\n/** `source.path` 必须留在仓库内;索引是仓库里的文件,不该能指到仓库外面去。 */\nfunction resolveAbilityDir(hubRoot: string, sourcePath: string): string | undefined {\n\tconst target = resolve(hubRoot, sourcePath);\n\tconst fromRoot = relative(hubRoot, target);\n\tif (fromRoot === \"\" || fromRoot === \"..\" || fromRoot.startsWith(`..${sep}`)) return undefined;\n\treturn existsSync(target) && statSync(target).isDirectory() ? target : undefined;\n}\n\n/** 能力目录内的相对路径同理,且必须指向真实存在的文件。 */\nfunction packagedFileExists(abilityDir: string, path: string): boolean {\n\tconst target = resolve(abilityDir, path);\n\tconst fromDir = relative(abilityDir, target);\n\tif (fromDir === \"\" || fromDir === \"..\" || fromDir.startsWith(`..${sep}`)) return false;\n\treturn existsSync(target) && statSync(target).isFile();\n}\n\n/**\n * 推进 `marketplaceVersion`。\n *\n * 只认得两种写法:semver 和纯整数。其它写法(日期、commit 短号……)由作者自己决定下一个是\n * 什么,工具猜一个反而更危险——这个值一旦回退或重复,客户端就不会拉新快照。\n */\nfunction nextMarketplaceVersion(current: unknown): string | undefined {\n\tif (typeof current !== \"string\") return undefined;\n\tconst semver = /^(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(current.trim());\n\tif (semver) return `${semver[1]}.${semver[2]}.${Number(semver[3]) + 1}`;\n\tif (/^\\d+$/.test(current.trim())) return String(Number(current.trim()) + 1);\n\treturn undefined;\n}\n\nexport function syncMarketplaceIndex(input: SyncInput): SyncResult {\n\tconst manifest = readJsonFile(input.manifestPath);\n\tif (!manifest) throw new Error(`Malformed marketplace manifest: ${input.manifestPath}`);\n\tconst abilities = Array.isArray(manifest.abilities) ? (manifest.abilities as unknown[]) : [];\n\n\tconst changes: SyncChange[] = [];\n\tconst problems: SyncProblem[] = [];\n\tconst listedDirs = new Set<string>();\n\n\tfor (const raw of abilities) {\n\t\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) continue;\n\t\tconst entry = raw as Record<string, unknown>;\n\t\tconst slug = typeof entry.slug === \"string\" ? entry.slug : \"(unnamed)\";\n\t\tconst type = typeof entry.type === \"string\" ? entry.type : \"\";\n\t\tif (type === \"bundle\") {\n\t\t\t// bundle 成员刻意不独立上架(索引的 abilities 是「独立上架条目」)。把它们的目录\n\t\t\t// 记为已登记,否则会被当成漏登记的能力报出来。\n\t\t\tconst bundleConfig =\n\t\t\t\ttypeof entry.config === \"object\" && entry.config !== null && !Array.isArray(entry.config)\n\t\t\t\t\t? (entry.config as Record<string, unknown>)\n\t\t\t\t\t: undefined;\n\t\t\tfor (const member of Array.isArray(bundleConfig?.members) ? (bundleConfig.members as unknown[]) : []) {\n\t\t\t\tif (typeof member !== \"object\" || member === null || Array.isArray(member)) continue;\n\t\t\t\tconst memberSource = (member as Record<string, unknown>).source;\n\t\t\t\tconst memberPath =\n\t\t\t\t\ttypeof memberSource === \"object\" && memberSource !== null && !Array.isArray(memberSource)\n\t\t\t\t\t\t? (memberSource as Record<string, unknown>).path\n\t\t\t\t\t\t: undefined;\n\t\t\t\tif (typeof memberPath !== \"string\") continue;\n\t\t\t\tconst dir = resolveAbilityDir(input.hubRoot, memberPath);\n\t\t\t\tif (dir) listedDirs.add(dir);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst source = typeof entry.source === \"object\" && entry.source !== null ? (entry.source as Record<string, unknown>) : undefined;\n\t\tconst sourcePath = typeof source?.path === \"string\" ? source.path : undefined;\n\t\tif (!sourcePath) {\n\t\t\tproblems.push({ slug, message: \"entry has no source.path\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst abilityDir = resolveAbilityDir(input.hubRoot, sourcePath);\n\t\tif (!abilityDir) {\n\t\t\tproblems.push({ slug, message: `source.path does not resolve to a directory inside the repository: ${sourcePath}` });\n\t\t\tcontinue;\n\t\t}\n\t\tlistedDirs.add(abilityDir);\n\n\t\tif (type === \"plugin\") reconcilePlugin({ entry, slug, abilityDir, changes, problems });\n\t\telse if (type === \"mcp\") reconcileIdentityFile({ entry, slug, abilityDir, fileName: \"mcp.json\", changes, problems });\n\t\t// skill / scene 目录里没有身份文件,目录存在即算通过。\n\t}\n\n\t// 未登记的目录只报告、不作为 problem:作者可能正在开发一个还不打算上架的东西。\n\tconst unlisted = findAbilityDirectories(input.hubRoot)\n\t\t.filter((dir) => !listedDirs.has(dir))\n\t\t.map((dir) => relative(input.hubRoot, dir).split(sep).join(\"/\"))\n\t\t.sort();\n\n\tlet written = false;\n\tif (changes.length > 0 && input.apply) {\n\t\tconst bumped = nextMarketplaceVersion(manifest.marketplaceVersion);\n\t\tif (bumped) {\n\t\t\tchanges.push({ slug: \"(manifest)\", field: \"marketplaceVersion\", from: manifest.marketplaceVersion, to: bumped });\n\t\t\tmanifest.marketplaceVersion = bumped;\n\t\t} else {\n\t\t\tproblems.push({\n\t\t\t\tslug: \"(manifest)\",\n\t\t\t\tmessage:\n\t\t\t\t\t\"content changed but marketplaceVersion could not be bumped automatically (expected semver or an integer). Set it manually — clients skip the update when it does not change.\",\n\t\t\t});\n\t\t}\n\t\tconst source = readFileSync(input.manifestPath, \"utf8\");\n\t\tconst serialized = JSON.stringify(manifest, null, detectJsonIndent(source));\n\t\twriteFileSync(input.manifestPath, source.endsWith(\"\\n\") ? `${serialized}\\n` : serialized, \"utf8\");\n\t\twritten = true;\n\t}\n\n\treturn { manifestPath: input.manifestPath, changes, problems, unlisted, written };\n}\n\nfunction reconcilePlugin(context: {\n\tentry: Record<string, unknown>;\n\tslug: string;\n\tabilityDir: string;\n\tchanges: SyncChange[];\n\tproblems: SyncProblem[];\n}): void {\n\tconst { entry, slug, abilityDir, changes, problems } = context;\n\tconst manifest = readJsonFile(join(abilityDir, \"plugin.json\"));\n\tif (!manifest) {\n\t\tproblems.push({ slug, message: \"plugin.json is missing or malformed\" });\n\t\treturn;\n\t}\n\tif (manifest.id !== slug) {\n\t\t// id 由作者决定,slug 是上架身份;改哪个都有副作用,不擅自动手。\n\t\tproblems.push({ slug, message: `plugin.json id ${JSON.stringify(manifest.id)} does not match the ability slug` });\n\t\treturn;\n\t}\n\tif (typeof manifest.version === \"string\" && entry.version !== manifest.version) {\n\t\tchanges.push({ slug, field: \"version\", from: entry.version, to: manifest.version });\n\t\tentry.version = manifest.version;\n\t}\n\n\t// api_version / permissions / commands 刻意不回填:宿主在建目录时用 plugin.json 推导的值\n\t// 整个覆盖 config,索引里写什么都会被重算掉。写进去只会多一份会漂移的副本,所以只在它\n\t// 已经存在且与真源不符时提醒作者删掉或改对。\n\tconst config =\n\t\ttypeof entry.config === \"object\" && entry.config !== null && !Array.isArray(entry.config)\n\t\t\t? (entry.config as Record<string, unknown>)\n\t\t\t: undefined;\n\tif (config) {\n\t\tif (typeof config.api_version === \"string\" && config.api_version !== manifest.pluginApiVersion) {\n\t\t\tproblems.push({\n\t\t\t\tslug,\n\t\t\t\tmessage: `config.api_version ${JSON.stringify(config.api_version)} disagrees with plugin.json (${JSON.stringify(manifest.pluginApiVersion)}); the host derives this field, so drop the copy or fix it`,\n\t\t\t});\n\t\t}\n\t\tconst declared = stringArray(manifest.permissions) ?? [];\n\t\tif (Array.isArray(config.permissions) && !sameStringArray(config.permissions, declared)) {\n\t\t\tproblems.push({\n\t\t\t\tslug,\n\t\t\t\tmessage: \"config.permissions disagrees with plugin.json; the host derives this field, so drop the copy or fix it\",\n\t\t\t});\n\t\t}\n\t}\n\n\t// 宿主按 plugin.json 直接读目录,不会替作者构建:产物不在仓库里就是装不上。\n\tconst entryFile = typeof manifest.entry === \"string\" ? manifest.entry : undefined;\n\tif (!entryFile || !packagedFileExists(abilityDir, entryFile)) {\n\t\tproblems.push({ slug, message: `built entry is missing from the published directory: ${entryFile ?? \"(entry not declared)\"}` });\n\t}\n\tfor (const style of stringArray(manifest.styles) ?? []) {\n\t\tif (!packagedFileExists(abilityDir, style)) {\n\t\t\tproblems.push({ slug, message: `declared style is missing from the published directory: ${style}` });\n\t\t}\n\t}\n}\n\nfunction reconcileIdentityFile(context: {\n\tentry: Record<string, unknown>;\n\tslug: string;\n\tabilityDir: string;\n\tfileName: string;\n\tchanges: SyncChange[];\n\tproblems: SyncProblem[];\n}): void {\n\tconst { entry, slug, abilityDir, fileName, changes, problems } = context;\n\tconst identity = readJsonFile(join(abilityDir, fileName));\n\tif (!identity) {\n\t\tproblems.push({ slug, message: `${fileName} is missing or malformed` });\n\t\treturn;\n\t}\n\tif (typeof identity.slug === \"string\" && identity.slug !== slug) {\n\t\tproblems.push({ slug, message: `${fileName} slug ${JSON.stringify(identity.slug)} does not match the ability slug` });\n\t\treturn;\n\t}\n\tif (typeof identity.version === \"string\" && entry.version !== identity.version) {\n\t\tchanges.push({ slug, field: \"version\", from: entry.version, to: identity.version });\n\t\tentry.version = identity.version;\n\t}\n}\n\n/** 扫描仓库里带身份文件的能力目录。深度与忽略名单是为了不爬进依赖和构建产物。 */\nfunction findAbilityDirectories(hubRoot: string): string[] {\n\tconst found: string[] = [];\n\tconst walk = (dir: string, depth: number): void => {\n\t\tif (depth > SCAN_MAX_DEPTH) return;\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (entries.includes(\"plugin.json\") || entries.includes(\"mcp.json\")) {\n\t\t\tfound.push(dir);\n\t\t\treturn; // 能力目录内部不再深挖。\n\t\t}\n\t\tfor (const name of entries) {\n\t\t\tif (name.startsWith(\".\") || SCAN_IGNORED.has(name)) continue;\n\t\t\tconst child = join(dir, name);\n\t\t\ttry {\n\t\t\t\tif (statSync(child).isDirectory()) walk(child, depth + 1);\n\t\t\t} catch {\n\t\t\t\t// 断链或权限不足:跳过即可,扫描不该因为一个目录中断。\n\t\t\t}\n\t\t}\n\t};\n\twalk(hubRoot, 0);\n\treturn found.sort();\n}\n\n/**\n * 单个能力的索引漂移,用于在开发命令里顺手提醒。\n *\n * 完整的 `sync` 是仓库根的事,但作者改完 version 往往立刻就装一次——那一刻提醒,比等他\n * 某天想起来跑 CI 要早得多,也是 Agent 唯一能可靠接收到这件事的时机。\n */\nexport function describeIndexDrift(input: {\n\thubRoot: string;\n\tmanifestPath: string;\n\tslug: string;\n\tversion: string;\n}): string | undefined {\n\tconst manifest = readJsonFile(input.manifestPath);\n\tif (!manifest || !Array.isArray(manifest.abilities)) return undefined;\n\tconst entry = (manifest.abilities as unknown[]).find(\n\t\t(item) =>\n\t\t\ttypeof item === \"object\" && item !== null && !Array.isArray(item) && (item as Record<string, unknown>).slug === input.slug,\n\t) as Record<string, unknown> | undefined;\n\tif (!entry) return undefined;\n\tif (entry.version === input.version) return undefined;\n\treturn `Marketplace index still lists ${input.slug} ${JSON.stringify(entry.version)} (this project is ${JSON.stringify(input.version)}). Run \\`vetta-plugin-cli sync\\` at ${input.hubRoot} — the host refuses to sync an entry whose version does not match.`;\n}\n"]}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,aAAa,GAAG,aAAa,GAAG,UAAU,GAAG,oBAAoB,CAAC;AAE3G,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,8FAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,wCAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,oDAAgC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAmED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAkGjE;AAkMD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,GAAG,SAAS,CAUrB","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { join, relative, resolve, sep } from \"node:path\";\n\n/**\n * 对账能力市场索引(`.vetta/marketplace.json`)与各能力目录。\n *\n * 索引本来就是派生数据,却有一组会咬人的硬约束:条目的 slug/version 必须与能力目录里的身份\n * 文件完全相等,否则宿主同步**直接失败**;`entry`/`styles` 指向的文件必须真实存在,否则本地\n * 能装、市场上装不了;而改了任何内容却没换 `marketplaceVersion` 时,客户端按它判缓存——\n * 既不报错也不更新,用户只是永远收不到新版本。\n *\n * 三种失败里有两种不在作者机器上复现,一种压根不报错。所以这件事必须是工具做的。\n */\n\nexport type SyncChangeKind = \"version\" | \"api_version\" | \"permissions\" | \"commands\" | \"marketplaceVersion\";\n\nexport interface SyncChange {\n\treadonly slug: string;\n\treadonly field: SyncChangeKind;\n\treadonly from: unknown;\n\treadonly to: unknown;\n}\n\nexport interface SyncProblem {\n\treadonly slug: string;\n\treadonly message: string;\n}\n\nexport interface SyncResult {\n\treadonly manifestPath: string;\n\treadonly changes: readonly SyncChange[];\n\treadonly problems: readonly SyncProblem[];\n\t/** 目录里有身份文件、索引里却没有的能力。只报告,不擅自上架。 */\n\treadonly unlisted: readonly string[];\n\t/** apply 时是否真的写了盘。 */\n\treadonly written: boolean;\n}\n\nexport interface SyncInput {\n\treadonly hubRoot: string;\n\treadonly manifestPath: string;\n\t/** false 时只报告不写盘(`--check`)。 */\n\treadonly apply: boolean;\n}\n\nconst SCAN_IGNORED = new Set([\"node_modules\", \".git\", \"dist\", \"release\", \".vetta\", \"assets\", \"test\", \"src\"]);\nconst SCAN_MAX_DEPTH = 5;\n\n/**\n * 探测 JSON 文件用的缩进,回写时沿用。\n *\n * 不这么做的代价很具体:索引文件本是 2 空格,工具按 Tab 重排就会把整份文件写成一个大 diff,\n * 与仓库里其它写这份文件的脚本来回拉锯,任何并发提交都升级成整文件冲突。对账工具只该改它\n * 要改的那几个字段。\n */\nfunction detectJsonIndent(source: string): string {\n\tconst match = /\\n([ \\t]+)\\S/.exec(source);\n\treturn match?.[1] ?? \"\\t\";\n}\n\nfunction readJsonFile(path: string): Record<string, unknown> | undefined {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"));\n\t\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return undefined;\n\t\treturn parsed as Record<string, unknown>;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction stringArray(value: unknown): string[] | undefined {\n\tif (!Array.isArray(value)) return undefined;\n\treturn value.every((item) => typeof item === \"string\") ? [...(value as string[])] : undefined;\n}\n\nfunction sameStringArray(left: unknown, right: readonly string[]): boolean {\n\tconst current = stringArray(left) ?? [];\n\treturn current.length === right.length && current.every((item, index) => item === right[index]);\n}\n\n/** `source.path` 必须留在仓库内;索引是仓库里的文件,不该能指到仓库外面去。 */\nfunction resolveAbilityDir(hubRoot: string, sourcePath: string): string | undefined {\n\tconst target = resolve(hubRoot, sourcePath);\n\tconst fromRoot = relative(hubRoot, target);\n\tif (fromRoot === \"\" || fromRoot === \"..\" || fromRoot.startsWith(`..${sep}`)) return undefined;\n\treturn existsSync(target) && statSync(target).isDirectory() ? target : undefined;\n}\n\n/** 能力目录内的相对路径同理,且必须指向真实存在的文件。 */\nfunction packagedFileExists(abilityDir: string, path: string): boolean {\n\tconst target = resolve(abilityDir, path);\n\tconst fromDir = relative(abilityDir, target);\n\tif (fromDir === \"\" || fromDir === \"..\" || fromDir.startsWith(`..${sep}`)) return false;\n\treturn existsSync(target) && statSync(target).isFile();\n}\n\n/**\n * 推进 `marketplaceVersion`。\n *\n * 只认得两种写法:semver 和纯整数。其它写法(日期、commit 短号……)由作者自己决定下一个是\n * 什么,工具猜一个反而更危险——这个值一旦回退或重复,客户端就不会拉新快照。\n */\nfunction nextMarketplaceVersion(current: unknown): string | undefined {\n\tif (typeof current !== \"string\") return undefined;\n\tconst semver = /^(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(current.trim());\n\tif (semver) return `${semver[1]}.${semver[2]}.${Number(semver[3]) + 1}`;\n\tif (/^\\d+$/.test(current.trim())) return String(Number(current.trim()) + 1);\n\treturn undefined;\n}\n\nexport function syncMarketplaceIndex(input: SyncInput): SyncResult {\n\tconst manifest = readJsonFile(input.manifestPath);\n\tif (!manifest) throw new Error(`Malformed marketplace manifest: ${input.manifestPath}`);\n\tconst abilities = Array.isArray(manifest.abilities) ? (manifest.abilities as unknown[]) : [];\n\n\tconst changes: SyncChange[] = [];\n\tconst problems: SyncProblem[] = [];\n\tconst listedDirs = new Set<string>();\n\n\tfor (const raw of abilities) {\n\t\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) continue;\n\t\tconst entry = raw as Record<string, unknown>;\n\t\tconst slug = typeof entry.slug === \"string\" ? entry.slug : \"(unnamed)\";\n\t\tconst type = typeof entry.type === \"string\" ? entry.type : \"\";\n\t\tif (type === \"bundle\") {\n\t\t\t// bundle 成员刻意不独立上架(索引的 abilities 是「独立上架条目」)。把它们的目录\n\t\t\t// 记为已登记,否则会被当成漏登记的能力报出来。\n\t\t\tconst bundleConfig =\n\t\t\t\ttypeof entry.config === \"object\" && entry.config !== null && !Array.isArray(entry.config)\n\t\t\t\t\t? (entry.config as Record<string, unknown>)\n\t\t\t\t\t: undefined;\n\t\t\tfor (const member of Array.isArray(bundleConfig?.members) ? (bundleConfig.members as unknown[]) : []) {\n\t\t\t\tif (typeof member !== \"object\" || member === null || Array.isArray(member)) continue;\n\t\t\t\tconst memberEntry = member as Record<string, unknown>;\n\t\t\t\tconst memberSource = memberEntry.source;\n\t\t\t\tconst memberPath =\n\t\t\t\t\ttypeof memberSource === \"object\" && memberSource !== null && !Array.isArray(memberSource)\n\t\t\t\t\t\t? (memberSource as Record<string, unknown>).path\n\t\t\t\t\t\t: undefined;\n\t\t\t\tif (typeof memberPath !== \"string\") continue;\n\t\t\t\tconst dir = resolveAbilityDir(input.hubRoot, memberPath);\n\t\t\t\tif (dir) {\n\t\t\t\t\tlistedDirs.add(dir);\n\t\t\t\t\tif (manifest.schemaVersion === 3 && memberEntry.type === \"plugin\") {\n\t\t\t\t\t\tconst descriptor = readJsonFile(join(dir, \"ability.json\"));\n\t\t\t\t\t\tconst memberChanges: SyncChange[] = [];\n\t\t\t\t\t\treconcilePlugin({\n\t\t\t\t\t\t\tentry: { ...memberEntry, version: descriptor?.version },\n\t\t\t\t\t\t\tslug: typeof memberEntry.slug === \"string\" ? memberEntry.slug : \"(unnamed)\",\n\t\t\t\t\t\t\tabilityDir: dir,\n\t\t\t\t\t\t\tschemaVersion: manifest.schemaVersion,\n\t\t\t\t\t\t\tminAppVersion: manifest.minAppVersion,\n\t\t\t\t\t\t\tchanges: memberChanges,\n\t\t\t\t\t\t\tproblems,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfor (const change of memberChanges) {\n\t\t\t\t\t\t\tproblems.push({ slug: change.slug, message: `ability.json version does not match latest release: ${change.to}` });\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst source = typeof entry.source === \"object\" && entry.source !== null ? (entry.source as Record<string, unknown>) : undefined;\n\t\tconst sourcePath = typeof source?.path === \"string\" ? source.path : undefined;\n\t\tif (!sourcePath) {\n\t\t\tproblems.push({ slug, message: \"entry has no source.path\" });\n\t\t\tcontinue;\n\t\t}\n\t\tconst abilityDir = resolveAbilityDir(input.hubRoot, sourcePath);\n\t\tif (!abilityDir) {\n\t\t\tproblems.push({ slug, message: `source.path does not resolve to a directory inside the repository: ${sourcePath}` });\n\t\t\tcontinue;\n\t\t}\n\t\tlistedDirs.add(abilityDir);\n\n\t\tif (type === \"plugin\") reconcilePlugin({ entry, slug, abilityDir, schemaVersion: manifest.schemaVersion, minAppVersion: manifest.minAppVersion, changes, problems });\n\t\telse if (type === \"mcp\") reconcileIdentityFile({ entry, slug, abilityDir, fileName: \"mcp.json\", changes, problems });\n\t\t// skill / scene 目录里没有身份文件,目录存在即算通过。\n\t}\n\n\t// 未登记的目录只报告、不作为 problem:作者可能正在开发一个还不打算上架的东西。\n\tconst unlisted = findAbilityDirectories(input.hubRoot)\n\t\t.filter((dir) => !listedDirs.has(dir))\n\t\t.map((dir) => relative(input.hubRoot, dir).split(sep).join(\"/\"))\n\t\t.sort();\n\n\tlet written = false;\n\tif (changes.length > 0 && input.apply) {\n\t\tconst bumped = nextMarketplaceVersion(manifest.marketplaceVersion);\n\t\tif (bumped) {\n\t\t\tchanges.push({ slug: \"(manifest)\", field: \"marketplaceVersion\", from: manifest.marketplaceVersion, to: bumped });\n\t\t\tmanifest.marketplaceVersion = bumped;\n\t\t} else {\n\t\t\tproblems.push({\n\t\t\t\tslug: \"(manifest)\",\n\t\t\t\tmessage:\n\t\t\t\t\t\"content changed but marketplaceVersion could not be bumped automatically (expected semver or an integer). Set it manually — clients skip the update when it does not change.\",\n\t\t\t});\n\t\t}\n\t\tconst source = readFileSync(input.manifestPath, \"utf8\");\n\t\tconst serialized = JSON.stringify(manifest, null, detectJsonIndent(source));\n\t\twriteFileSync(input.manifestPath, source.endsWith(\"\\n\") ? `${serialized}\\n` : serialized, \"utf8\");\n\t\twritten = true;\n\t}\n\n\treturn { manifestPath: input.manifestPath, changes, problems, unlisted, written };\n}\n\nfunction reconcilePlugin(context: {\n\tentry: Record<string, unknown>;\n\tslug: string;\n\tabilityDir: string;\n\tschemaVersion: unknown;\n\tminAppVersion: unknown;\n\tchanges: SyncChange[];\n\tproblems: SyncProblem[];\n}): void {\n\tconst { entry, slug, abilityDir, schemaVersion, minAppVersion, changes, problems } = context;\n\tif (schemaVersion === 3 && !(\"releases\" in entry)) {\n\t\tproblems.push({ slug, message: \"schemaVersion 3 plugin requires versioned releases\" });\n\t\treturn;\n\t}\n\tif (\"releases\" in entry) {\n\t\tif (schemaVersion !== 3) {\n\t\t\tproblems.push({ slug, message: \"versioned plugin releases require marketplace schemaVersion 3\" });\n\t\t\treturn;\n\t\t}\n\t\tconst releases = entry.releases;\n\t\tif (!Array.isArray(releases) || releases.length === 0) {\n\t\t\tproblems.push({ slug, message: \"plugin releases must be a nonempty array\" });\n\t\t\treturn;\n\t\t}\n\t\tlet latest: { version: string; parts: [number, number, number] } | undefined;\n\t\tconst seen = new Set<string>();\n\t\tfor (const raw of releases) {\n\t\t\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n\t\t\t\tproblems.push({ slug, message: \"plugin release must be an object\" });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst release = raw as Record<string, unknown>;\n\t\t\tconst match = typeof release.version === \"string\" ? /^(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(release.version) : null;\n\t\t\tif (!match || seen.has(release.version as string)) {\n\t\t\t\tproblems.push({ slug, message: `invalid or duplicate plugin release version: ${String(release.version)}` });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tseen.add(release.version as string);\n\t\t\tconst minimum = typeof release.minAppVersion === \"string\" ? /^(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(release.minAppVersion) : null;\n\t\t\tif (!minimum || typeof minAppVersion !== \"string\" || !/^(\\d+)\\.(\\d+)\\.(\\d+)$/.test(minAppVersion)) {\n\t\t\t\tproblems.push({ slug, message: `release ${release.version} has an invalid minAppVersion` });\n\t\t\t} else {\n\t\t\t\tconst marketMinimum = /^(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(minAppVersion);\n\t\t\t\tif (marketMinimum && compareVersionParts(minimum.slice(1).map(Number), marketMinimum.slice(1).map(Number)) < 0) {\n\t\t\t\t\tproblems.push({ slug, message: `release ${release.version} requires an app older than the marketplace` });\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (typeof release.pluginApiVersion !== \"string\" || !/^\\^\\d+\\.\\d+\\.\\d+$/.test(release.pluginApiVersion)) {\n\t\t\t\tproblems.push({ slug, message: `release ${release.version} has an invalid pluginApiVersion` });\n\t\t\t}\n\t\t\tfor (const field of [\"permissions\", \"commands\"] as const) {\n\t\t\t\tif (release[field] !== undefined && !stringArray(release[field])) {\n\t\t\t\t\tproblems.push({ slug, message: `release ${release.version} has invalid ${field}` });\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst parts: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])];\n\t\t\tif (!latest || compareVersionParts(parts, latest.parts) > 0) {\n\t\t\t\tlatest = { version: release.version as string, parts };\n\t\t\t}\n\t\t\tconst artifact = release.artifact;\n\t\t\tif (typeof artifact !== \"object\" || artifact === null || Array.isArray(artifact)) {\n\t\t\t\tproblems.push({ slug, message: `release ${release.version} has no artifact` });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst { url, sha256 } = artifact as Record<string, unknown>;\n\t\t\tlet validUrl = false;\n\t\t\ttry {\n\t\t\t\tconst parsed = new URL(String(url));\n\t\t\t\tvalidUrl = parsed.protocol === \"https:\" && !parsed.username && !parsed.password && !parsed.hash;\n\t\t\t} catch {\n\t\t\t\t// Report the malformed URL below.\n\t\t\t}\n\t\t\tif (!validUrl || typeof sha256 !== \"string\" || !/^[a-f0-9]{64}$/.test(sha256)) {\n\t\t\t\tproblems.push({ slug, message: `release ${release.version} has an invalid HTTPS artifact or SHA-256` });\n\t\t\t}\n\t\t}\n\t\tif (latest && entry.version !== latest.version) {\n\t\t\tchanges.push({ slug, field: \"version\", from: entry.version, to: latest.version });\n\t\t\tentry.version = latest.version;\n\t\t}\n\t\treturn;\n\t}\n\tconst manifest = readJsonFile(join(abilityDir, \"plugin.json\"));\n\tif (!manifest) {\n\t\tproblems.push({ slug, message: \"plugin.json is missing or malformed\" });\n\t\treturn;\n\t}\n\tif (manifest.id !== slug) {\n\t\t// id 由作者决定,slug 是上架身份;改哪个都有副作用,不擅自动手。\n\t\tproblems.push({ slug, message: `plugin.json id ${JSON.stringify(manifest.id)} does not match the ability slug` });\n\t\treturn;\n\t}\n\tif (typeof manifest.version === \"string\" && entry.version !== manifest.version) {\n\t\tchanges.push({ slug, field: \"version\", from: entry.version, to: manifest.version });\n\t\tentry.version = manifest.version;\n\t}\n\n\t// api_version / permissions / commands 刻意不回填:宿主在建目录时用 plugin.json 推导的值\n\t// 整个覆盖 config,索引里写什么都会被重算掉。写进去只会多一份会漂移的副本,所以只在它\n\t// 已经存在且与真源不符时提醒作者删掉或改对。\n\tconst config =\n\t\ttypeof entry.config === \"object\" && entry.config !== null && !Array.isArray(entry.config)\n\t\t\t? (entry.config as Record<string, unknown>)\n\t\t\t: undefined;\n\tif (config) {\n\t\tif (typeof config.api_version === \"string\" && config.api_version !== manifest.pluginApiVersion) {\n\t\t\tproblems.push({\n\t\t\t\tslug,\n\t\t\t\tmessage: `config.api_version ${JSON.stringify(config.api_version)} disagrees with plugin.json (${JSON.stringify(manifest.pluginApiVersion)}); the host derives this field, so drop the copy or fix it`,\n\t\t\t});\n\t\t}\n\t\tconst declared = stringArray(manifest.permissions) ?? [];\n\t\tif (Array.isArray(config.permissions) && !sameStringArray(config.permissions, declared)) {\n\t\t\tproblems.push({\n\t\t\t\tslug,\n\t\t\t\tmessage: \"config.permissions disagrees with plugin.json; the host derives this field, so drop the copy or fix it\",\n\t\t\t});\n\t\t}\n\t}\n\n\t// 宿主按 plugin.json 直接读目录,不会替作者构建:产物不在仓库里就是装不上。\n\tconst entryFile = typeof manifest.entry === \"string\" ? manifest.entry : undefined;\n\tif (!entryFile || !packagedFileExists(abilityDir, entryFile)) {\n\t\tproblems.push({ slug, message: `built entry is missing from the published directory: ${entryFile ?? \"(entry not declared)\"}` });\n\t}\n\tfor (const style of stringArray(manifest.styles) ?? []) {\n\t\tif (!packagedFileExists(abilityDir, style)) {\n\t\t\tproblems.push({ slug, message: `declared style is missing from the published directory: ${style}` });\n\t\t}\n\t}\n}\n\nfunction compareVersionParts(left: readonly number[], right: readonly number[]): number {\n\tfor (let index = 0; index < 3; index += 1) {\n\t\tif (left[index] !== right[index]) return (left[index] ?? 0) - (right[index] ?? 0);\n\t}\n\treturn 0;\n}\n\nfunction reconcileIdentityFile(context: {\n\tentry: Record<string, unknown>;\n\tslug: string;\n\tabilityDir: string;\n\tfileName: string;\n\tchanges: SyncChange[];\n\tproblems: SyncProblem[];\n}): void {\n\tconst { entry, slug, abilityDir, fileName, changes, problems } = context;\n\tconst identity = readJsonFile(join(abilityDir, fileName));\n\tif (!identity) {\n\t\tproblems.push({ slug, message: `${fileName} is missing or malformed` });\n\t\treturn;\n\t}\n\tif (typeof identity.slug === \"string\" && identity.slug !== slug) {\n\t\tproblems.push({ slug, message: `${fileName} slug ${JSON.stringify(identity.slug)} does not match the ability slug` });\n\t\treturn;\n\t}\n\tif (typeof identity.version === \"string\" && entry.version !== identity.version) {\n\t\tchanges.push({ slug, field: \"version\", from: entry.version, to: identity.version });\n\t\tentry.version = identity.version;\n\t}\n}\n\n/** 扫描仓库里带身份文件的能力目录。深度与忽略名单是为了不爬进依赖和构建产物。 */\nfunction findAbilityDirectories(hubRoot: string): string[] {\n\tconst found: string[] = [];\n\tconst walk = (dir: string, depth: number): void => {\n\t\tif (depth > SCAN_MAX_DEPTH) return;\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (entries.includes(\"plugin.json\") || entries.includes(\"mcp.json\")) {\n\t\t\tfound.push(dir);\n\t\t\treturn; // 能力目录内部不再深挖。\n\t\t}\n\t\tfor (const name of entries) {\n\t\t\tif (name.startsWith(\".\") || SCAN_IGNORED.has(name)) continue;\n\t\t\tconst child = join(dir, name);\n\t\t\ttry {\n\t\t\t\tif (statSync(child).isDirectory()) walk(child, depth + 1);\n\t\t\t} catch {\n\t\t\t\t// 断链或权限不足:跳过即可,扫描不该因为一个目录中断。\n\t\t\t}\n\t\t}\n\t};\n\twalk(hubRoot, 0);\n\treturn found.sort();\n}\n\n/**\n * 单个能力的索引漂移,用于在开发命令里顺手提醒。\n *\n * 完整的 `sync` 是仓库根的事,但作者改完 version 往往立刻就装一次——那一刻提醒,比等他\n * 某天想起来跑 CI 要早得多,也是 Agent 唯一能可靠接收到这件事的时机。\n */\nexport function describeIndexDrift(input: {\n\thubRoot: string;\n\tmanifestPath: string;\n\tslug: string;\n\tversion: string;\n}): string | undefined {\n\tconst manifest = readJsonFile(input.manifestPath);\n\tif (!manifest || !Array.isArray(manifest.abilities)) return undefined;\n\tconst entry = (manifest.abilities as unknown[]).find(\n\t\t(item) =>\n\t\t\ttypeof item === \"object\" && item !== null && !Array.isArray(item) && (item as Record<string, unknown>).slug === input.slug,\n\t) as Record<string, unknown> | undefined;\n\tif (!entry) return undefined;\n\tif (entry.version === input.version) return undefined;\n\treturn `Marketplace index still lists ${input.slug} ${JSON.stringify(entry.version)} (this project is ${JSON.stringify(input.version)}). Run \\`vetta-plugin-cli sync\\` at ${input.hubRoot} — the host refuses to sync an entry whose version does not match.`;\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.7",
4
4
  "description": "Install npm-distributed plugins into Vetta Desktop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",