@vetta-org/plugin-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +108 -0
- package/dist/agents-template.d.ts +16 -0
- package/dist/agents-template.d.ts.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +20181 -0
- package/dist/command.d.ts +101 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/hub-template.d.ts +18 -0
- package/dist/hub-template.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20204 -0
- package/dist/init.d.ts +31 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/npm-package.d.ts +27 -0
- package/dist/npm-package.d.ts.map +1 -0
- package/dist/sync.d.ts +50 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/workspace.d.ts +30 -0
- package/dist/workspace.d.ts.map +1 -0
- package/package.json +45 -0
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** 与脚手架一同落地的依赖范围;两个包各自独立发布,不要合成一个版本。 */
|
|
2
|
+
export declare const DEFAULT_SDK_RANGE = "^0.3.1";
|
|
3
|
+
export declare const DEFAULT_VITE_RANGE = "^0.2.0";
|
|
4
|
+
export interface InitPluginInput {
|
|
5
|
+
readonly targetDir: string;
|
|
6
|
+
readonly pluginId: string;
|
|
7
|
+
readonly displayName: string;
|
|
8
|
+
readonly sdkRange?: string;
|
|
9
|
+
readonly viteRange?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface InitPluginResult {
|
|
12
|
+
readonly root: string;
|
|
13
|
+
readonly pluginId: string;
|
|
14
|
+
readonly files: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function initPluginProject(input: InitPluginInput): InitPluginResult;
|
|
17
|
+
export interface InitHubInput {
|
|
18
|
+
readonly targetDir: string;
|
|
19
|
+
/** 市场名(slug)。 */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly repository: string;
|
|
22
|
+
/** 本市场的能力所支持的最老 Vetta 版本。 */
|
|
23
|
+
readonly minAppVersion: string;
|
|
24
|
+
}
|
|
25
|
+
export interface InitHubResult {
|
|
26
|
+
readonly root: string;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly files: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function initHubRepository(input: InitHubInput): InitHubResult;
|
|
31
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +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;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.1\";\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.0\",\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\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"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ResolvedNpmPluginArchive {
|
|
2
|
+
archivePath: string;
|
|
3
|
+
cleanup(): Promise<void>;
|
|
4
|
+
expectedSha256: string;
|
|
5
|
+
integrity?: string;
|
|
6
|
+
packageManifest: NpmPluginPackageManifest;
|
|
7
|
+
requestedSpec: string;
|
|
8
|
+
}
|
|
9
|
+
/** Public projection kept local so the bundled CLI has no published type dependency. */
|
|
10
|
+
export interface NpmPluginPackageManifest {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
vetta: {
|
|
14
|
+
schemaVersion: 1;
|
|
15
|
+
type: "desktop-plugin";
|
|
16
|
+
pluginId: string;
|
|
17
|
+
archive: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface NpmPackResult {
|
|
21
|
+
filename: string;
|
|
22
|
+
integrity?: string;
|
|
23
|
+
}
|
|
24
|
+
export type NpmPackRunner = (packageSpec: string, destination: string) => Promise<NpmPackResult>;
|
|
25
|
+
export declare const runNpmPack: NpmPackRunner;
|
|
26
|
+
export declare function resolveNpmPluginArchive(packageSpec: string, pack?: NpmPackRunner): Promise<ResolvedNpmPluginArchive>;
|
|
27
|
+
//# sourceMappingURL=npm-package.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"npm-package.d.ts","sourceRoot":"","sources":["../src/npm-package.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,wBAAwB;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,wBAAwB,CAAC;IAC1C,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,wFAAwF;AACxF,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE;QACN,aAAa,EAAE,CAAC,CAAC;QACjB,IAAI,EAAE,gBAAgB,CAAC;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC;CACF;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;AAyBjG,eAAO,MAAM,UAAU,EAAE,aA4BxB,CAAC;AA0CF,wBAAsB,uBAAuB,CAC5C,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE,aAA0B,GAC9B,OAAO,CAAC,wBAAwB,CAAC,CAsCnC","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { mkdtemp, readFile, realpath, rm, stat } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, isAbsolute, join, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseVettaNpmPluginPackage } from \"@vetta-org/plugin-sdk/npm-package\";\nimport npa from \"npm-package-arg\";\nimport { x as extractTar } from \"tar\";\n\nconst MAX_NPM_TARBALL_BYTES = 512 * 1024 * 1024;\nconst MAX_PLUGIN_ARCHIVE_BYTES = 512 * 1024 * 1024;\nconst REGISTRY_SPEC_TYPES = new Set([\"tag\", \"version\", \"range\"]);\n\nexport interface ResolvedNpmPluginArchive {\n\tarchivePath: string;\n\tcleanup(): Promise<void>;\n\texpectedSha256: string;\n\tintegrity?: string;\n\tpackageManifest: NpmPluginPackageManifest;\n\trequestedSpec: string;\n}\n\n/** Public projection kept local so the bundled CLI has no published type dependency. */\nexport interface NpmPluginPackageManifest {\n\tname: string;\n\tversion: string;\n\tvetta: {\n\t\tschemaVersion: 1;\n\t\ttype: \"desktop-plugin\";\n\t\tpluginId: string;\n\t\tarchive: string;\n\t};\n}\n\nexport interface NpmPackResult {\n\tfilename: string;\n\tintegrity?: string;\n}\n\nexport type NpmPackRunner = (packageSpec: string, destination: string) => Promise<NpmPackResult>;\n\nfunction parsePackOutput(stdout: string): NpmPackResult {\n\tconst parsed: unknown = JSON.parse(stdout);\n\tif (!Array.isArray(parsed) || parsed.length !== 1 || typeof parsed[0] !== \"object\" || parsed[0] === null) {\n\t\tthrow new Error(\"npm pack returned an invalid JSON response\");\n\t}\n\tconst entry = parsed[0] as Record<string, unknown>;\n\tif (typeof entry.filename !== \"string\" || entry.filename.length === 0 || basename(entry.filename) !== entry.filename) {\n\t\tthrow new Error(\"npm pack returned an invalid tarball filename\");\n\t}\n\treturn {\n\t\tfilename: entry.filename,\n\t\tintegrity: typeof entry.integrity === \"string\" ? entry.integrity : undefined,\n\t};\n}\n\nfunction npmInvocation(): { command: string; args: string[] } {\n\tconst npmExecPath = process.env.npm_execpath;\n\tif (npmExecPath) {\n\t\treturn { command: process.execPath, args: [npmExecPath] };\n\t}\n\treturn { command: process.platform === \"win32\" ? \"npm.cmd\" : \"npm\", args: [] };\n}\n\nexport const runNpmPack: NpmPackRunner = async (packageSpec, destination) => {\n\tconst invocation = npmInvocation();\n\tconst args = [\n\t\t...invocation.args,\n\t\t\"pack\",\n\t\tpackageSpec,\n\t\t\"--ignore-scripts\",\n\t\t\"--json\",\n\t\t\"--pack-destination\",\n\t\tdestination,\n\t];\n\tconst output = await new Promise<string>((resolvePromise, rejectPromise) => {\n\t\tconst child = spawn(invocation.command, args, { shell: false, windowsHide: true, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout.setEncoding(\"utf8\").on(\"data\", (chunk: string) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.stderr.setEncoding(\"utf8\").on(\"data\", (chunk: string) => {\n\t\t\tstderr += chunk;\n\t\t});\n\t\tchild.once(\"error\", rejectPromise);\n\t\tchild.once(\"exit\", (code, signal) => {\n\t\t\tif (code === 0) resolvePromise(stdout);\n\t\t\telse rejectPromise(new Error(stderr.trim() || `npm pack failed (code=${code}, signal=${signal})`));\n\t\t});\n\t});\n\treturn parsePackOutput(output);\n};\n\nfunction registryPackageName(packageSpec: string): string {\n\tconst parsed = npa(packageSpec);\n\tif (!parsed.registry || !parsed.name || !REGISTRY_SPEC_TYPES.has(parsed.type)) {\n\t\tthrow new Error(\"add accepts npm registry package names, tags, versions, or ranges\");\n\t}\n\treturn parsed.name;\n}\n\nfunction assertPathInside(root: string, candidate: string, label: string): void {\n\tconst rel = relative(root, candidate);\n\tif (!rel || rel.startsWith(\"..\") || isAbsolute(rel)) throw new Error(`${label} escapes the temporary package root`);\n}\n\nasync function extractRegularFile(tarballPath: string, destination: string, archivePath: string): Promise<string> {\n\tconst tarPath = `package/${archivePath.replace(/\\\\/g, \"/\")}`;\n\tlet matched = false;\n\tawait extractTar({\n\t\tfile: tarballPath,\n\t\tcwd: destination,\n\t\tstrict: true,\n\t\tfilter(path, entry) {\n\t\t\tif (path !== tarPath) return false;\n\t\t\tif (!(\"type\" in entry) || entry.type !== \"File\") {\n\t\t\t\tthrow new Error(`npm package entry must be a regular file: ${archivePath}`);\n\t\t\t}\n\t\t\tif (matched) throw new Error(`npm package entry is duplicated: ${archivePath}`);\n\t\t\tmatched = true;\n\t\t\treturn true;\n\t\t},\n\t\tstrip: 1,\n\t});\n\tif (!matched) throw new Error(`npm package entry is missing: ${archivePath}`);\n\tconst outputPath = resolve(destination, archivePath);\n\tassertPathInside(destination, outputPath, \"npm package entry\");\n\tconst [resolvedRoot, resolvedOutput, info] = await Promise.all([realpath(destination), realpath(outputPath), stat(outputPath)]);\n\tassertPathInside(resolvedRoot, resolvedOutput, \"npm package entry\");\n\tif (!info.isFile()) throw new Error(`npm package entry must be a regular file: ${archivePath}`);\n\treturn resolvedOutput;\n}\n\nexport async function resolveNpmPluginArchive(\n\tpackageSpec: string,\n\tpack: NpmPackRunner = runNpmPack,\n): Promise<ResolvedNpmPluginArchive> {\n\tconst requestedPackageName = registryPackageName(packageSpec);\n\tconst temporaryRoot = await mkdtemp(join(tmpdir(), \"vetta-plugin-add-\"));\n\tconst cleanup = () => rm(temporaryRoot, { recursive: true, force: true });\n\ttry {\n\t\tconst packed = await pack(packageSpec, temporaryRoot);\n\t\tconst tarballPath = resolve(temporaryRoot, packed.filename);\n\t\tassertPathInside(temporaryRoot, tarballPath, \"npm tarball\");\n\t\tconst tarballInfo = await stat(tarballPath);\n\t\tif (!tarballInfo.isFile() || tarballInfo.size > MAX_NPM_TARBALL_BYTES) {\n\t\t\tthrow new Error(\"npm plugin package tarball is missing or exceeds the 512 MB limit\");\n\t\t}\n\n\t\tconst packageJsonPath = await extractRegularFile(tarballPath, temporaryRoot, \"package.json\");\n\t\tconst packageManifest = parseVettaNpmPluginPackage(JSON.parse(await readFile(packageJsonPath, \"utf8\")) as unknown);\n\t\tif (packageManifest.name !== requestedPackageName) {\n\t\t\tthrow new Error(\n\t\t\t\t`npm package name mismatch: requested ${requestedPackageName}, received ${packageManifest.name}`,\n\t\t\t);\n\t\t}\n\t\tconst archivePath = await extractRegularFile(tarballPath, temporaryRoot, packageManifest.vetta.archive);\n\t\tconst archive = await readFile(archivePath);\n\t\tif (archive.length > MAX_PLUGIN_ARCHIVE_BYTES) {\n\t\t\tthrow new Error(\"Vetta plugin archive exceeds the 512 MB limit\");\n\t\t}\n\n\t\treturn {\n\t\t\tarchivePath,\n\t\t\tcleanup,\n\t\t\texpectedSha256: createHash(\"sha256\").update(archive).digest(\"hex\"),\n\t\t\tintegrity: packed.integrity,\n\t\t\tpackageManifest,\n\t\t\trequestedSpec: packageSpec,\n\t\t};\n\t} catch (error) {\n\t\tawait cleanup();\n\t\tthrow error;\n\t}\n}\n"]}
|
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对账能力市场索引(`.vetta/marketplace.json`)与各能力目录。
|
|
3
|
+
*
|
|
4
|
+
* 索引本来就是派生数据,却有一组会咬人的硬约束:条目的 slug/version 必须与能力目录里的身份
|
|
5
|
+
* 文件完全相等,否则宿主同步**直接失败**;`entry`/`styles` 指向的文件必须真实存在,否则本地
|
|
6
|
+
* 能装、市场上装不了;而改了任何内容却没换 `marketplaceVersion` 时,客户端按它判缓存——
|
|
7
|
+
* 既不报错也不更新,用户只是永远收不到新版本。
|
|
8
|
+
*
|
|
9
|
+
* 三种失败里有两种不在作者机器上复现,一种压根不报错。所以这件事必须是工具做的。
|
|
10
|
+
*/
|
|
11
|
+
export type SyncChangeKind = "version" | "api_version" | "permissions" | "commands" | "marketplaceVersion";
|
|
12
|
+
export interface SyncChange {
|
|
13
|
+
readonly slug: string;
|
|
14
|
+
readonly field: SyncChangeKind;
|
|
15
|
+
readonly from: unknown;
|
|
16
|
+
readonly to: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface SyncProblem {
|
|
19
|
+
readonly slug: string;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
}
|
|
22
|
+
export interface SyncResult {
|
|
23
|
+
readonly manifestPath: string;
|
|
24
|
+
readonly changes: readonly SyncChange[];
|
|
25
|
+
readonly problems: readonly SyncProblem[];
|
|
26
|
+
/** 目录里有身份文件、索引里却没有的能力。只报告,不擅自上架。 */
|
|
27
|
+
readonly unlisted: readonly string[];
|
|
28
|
+
/** apply 时是否真的写了盘。 */
|
|
29
|
+
readonly written: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface SyncInput {
|
|
32
|
+
readonly hubRoot: string;
|
|
33
|
+
readonly manifestPath: string;
|
|
34
|
+
/** false 时只报告不写盘(`--check`)。 */
|
|
35
|
+
readonly apply: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function syncMarketplaceIndex(input: SyncInput): SyncResult;
|
|
38
|
+
/**
|
|
39
|
+
* 单个能力的索引漂移,用于在开发命令里顺手提醒。
|
|
40
|
+
*
|
|
41
|
+
* 完整的 `sync` 是仓库根的事,但作者改完 version 往往立刻就装一次——那一刻提醒,比等他
|
|
42
|
+
* 某天想起来跑 CI 要早得多,也是 Agent 唯一能可靠接收到这件事的时机。
|
|
43
|
+
*/
|
|
44
|
+
export declare function describeIndexDrift(input: {
|
|
45
|
+
hubRoot: string;
|
|
46
|
+
manifestPath: string;
|
|
47
|
+
slug: string;
|
|
48
|
+
version: string;
|
|
49
|
+
}): string | undefined;
|
|
50
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +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;AAuDD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CA0DjE;AA+GD;;;;;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\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\") continue; // bundle 只组合成员,没有自己的身份文件。\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\twriteFileSync(input.manifestPath, `${JSON.stringify(manifest, null, \"\\t\")}\\n`, \"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\tconst config = typeof entry.config === \"object\" && entry.config !== null && !Array.isArray(entry.config)\n\t\t? (entry.config as Record<string, unknown>)\n\t\t: {};\n\tif (typeof manifest.pluginApiVersion === \"string\" && config.api_version !== manifest.pluginApiVersion) {\n\t\tchanges.push({ slug, field: \"api_version\", from: config.api_version, to: manifest.pluginApiVersion });\n\t\tconfig.api_version = manifest.pluginApiVersion;\n\t}\n\tconst permissions = stringArray(manifest.permissions) ?? [];\n\tif (!sameStringArray(config.permissions, permissions)) {\n\t\tchanges.push({ slug, field: \"permissions\", from: config.permissions, to: permissions });\n\t\tconfig.permissions = permissions;\n\t}\n\tconst commands = stringArray(manifest.commands) ?? [];\n\tif (commands.length > 0 || Array.isArray(config.commands)) {\n\t\tif (!sameStringArray(config.commands, commands)) {\n\t\t\tchanges.push({ slug, field: \"commands\", from: config.commands, to: commands });\n\t\t\tconfig.commands = commands;\n\t\t}\n\t}\n\tentry.config = config;\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"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface PluginProject {
|
|
2
|
+
/** 含 plugin.json 的目录。 */
|
|
3
|
+
readonly root: string;
|
|
4
|
+
readonly pluginId: string;
|
|
5
|
+
readonly version: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PluginHub {
|
|
8
|
+
/** 含 .vetta/marketplace.json 的仓库根。 */
|
|
9
|
+
readonly root: string;
|
|
10
|
+
readonly manifestPath: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 从 `from` 向上找最近的插件工程。
|
|
14
|
+
*
|
|
15
|
+
* 「最近」是刻意的:在 hub 里 `cd plugins/foo && … ` 命中 foo,而不是仓库根——用户站在
|
|
16
|
+
* 哪个插件里,操作就作用于哪个插件。
|
|
17
|
+
*/
|
|
18
|
+
export declare function findPluginProject(from: string): PluginProject | undefined;
|
|
19
|
+
/** 从 `from` 向上找最近的能力市场 hub(`.vetta/marketplace.json`)。 */
|
|
20
|
+
export declare function findPluginHub(from: string): PluginHub | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* 解析随 `@vetta-org/plugin-sdk` 发布的手册目录。
|
|
23
|
+
*
|
|
24
|
+
* 按 Node 的解析规则逐层找 node_modules,因此工作区把依赖提升到仓库根、或每个插件各装
|
|
25
|
+
* 一份,都能命中正确的那一份——也就是这个工程实际编译所针对的那个 SDK 版本的手册。
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveManualDir(from: string): string | undefined;
|
|
28
|
+
/** 手册所属的 SDK 版本;用于告诉调用方「这份手册对应哪个合同」。 */
|
|
29
|
+
export declare function readManualSdkVersion(manualDir: string): string | undefined;
|
|
30
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,aAAa;IAC7B,mCAAyB;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACzB,kDAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAsBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAiBzE;AAED,sFAA0D;AAC1D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAOjE;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED,gGAAwC;AACxC,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG1E","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\n\n/**\n * 定位「当前在开发哪个插件」与「手册在哪」。\n *\n * 这两件事都不能让调用方(尤其是 Agent)靠硬编码路径解决:插件既可能是一个独立工程,\n * 也可能是能力市场那种一仓多插件的 hub;依赖既可能装在插件目录下,也可能被工作区提升到\n * 仓库根。把它们收敛成一次向上查找,命令在任何子目录里执行都得到同一个答案。\n */\n\n/** 到此为止不再往上找:再往上就出了用户的项目,撞进无关仓库或用户主目录。 */\nconst BOUNDARY_MARKERS = [\".git\"] as const;\nconst MAX_WALK_DEPTH = 32;\n\nexport interface PluginProject {\n\t/** 含 plugin.json 的目录。 */\n\treadonly root: string;\n\treadonly pluginId: string;\n\treadonly version: string;\n}\n\nexport interface PluginHub {\n\t/** 含 .vetta/marketplace.json 的仓库根。 */\n\treadonly root: string;\n\treadonly manifestPath: string;\n}\n\nfunction* walkUp(from: string): Generator<string> {\n\tlet current = resolve(from);\n\tfor (let depth = 0; depth < MAX_WALK_DEPTH; depth += 1) {\n\t\tyield current;\n\t\tconst parent = dirname(current);\n\t\tif (parent === current) return;\n\t\tcurrent = parent;\n\t}\n}\n\nfunction readJson(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\n/**\n * 从 `from` 向上找最近的插件工程。\n *\n * 「最近」是刻意的:在 hub 里 `cd plugins/foo && … ` 命中 foo,而不是仓库根——用户站在\n * 哪个插件里,操作就作用于哪个插件。\n */\nexport function findPluginProject(from: string): PluginProject | undefined {\n\tfor (const dir of walkUp(from)) {\n\t\tconst manifestPath = join(dir, \"plugin.json\");\n\t\tif (!existsSync(manifestPath)) {\n\t\t\tif (isBoundary(dir)) return undefined;\n\t\t\tcontinue;\n\t\t}\n\t\tconst manifest = readJson(manifestPath);\n\t\tconst pluginId = typeof manifest?.id === \"string\" ? manifest.id : undefined;\n\t\tif (!pluginId) return undefined;\n\t\treturn {\n\t\t\troot: dir,\n\t\t\tpluginId,\n\t\t\tversion: typeof manifest?.version === \"string\" ? manifest.version : \"0.0.0\",\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** 从 `from` 向上找最近的能力市场 hub(`.vetta/marketplace.json`)。 */\nexport function findPluginHub(from: string): PluginHub | undefined {\n\tfor (const dir of walkUp(from)) {\n\t\tconst manifestPath = join(dir, \".vetta\", \"marketplace.json\");\n\t\tif (existsSync(manifestPath)) return { root: dir, manifestPath };\n\t\tif (isBoundary(dir)) return undefined;\n\t}\n\treturn undefined;\n}\n\nfunction isBoundary(dir: string): boolean {\n\treturn BOUNDARY_MARKERS.some((marker) => existsSync(join(dir, marker)));\n}\n\n/**\n * 解析随 `@vetta-org/plugin-sdk` 发布的手册目录。\n *\n * 按 Node 的解析规则逐层找 node_modules,因此工作区把依赖提升到仓库根、或每个插件各装\n * 一份,都能命中正确的那一份——也就是这个工程实际编译所针对的那个 SDK 版本的手册。\n */\nexport function resolveManualDir(from: string): string | undefined {\n\tfor (const dir of walkUp(from)) {\n\t\tconst candidate = join(dir, \"node_modules\", \"@vetta-org\", \"plugin-sdk\", \"docs\");\n\t\tif (existsSync(join(candidate, \"README.md\"))) return candidate;\n\t}\n\treturn undefined;\n}\n\n/** 手册所属的 SDK 版本;用于告诉调用方「这份手册对应哪个合同」。 */\nexport function readManualSdkVersion(manualDir: string): string | undefined {\n\tconst pkg = readJson(join(manualDir, \"..\", \"package.json\"));\n\treturn typeof pkg?.version === \"string\" ? pkg.version : undefined;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vetta-org/plugin-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install npm-distributed plugins into Vetta Desktop",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"vetta-plugin-cli": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"build": "bun run clean && tsgo -p tsconfig.build.json && bun build src/index.ts --target node --format esm --outfile dist/index.js && bun build src/cli.ts --target node --format esm --outfile dist/cli.js",
|
|
25
|
+
"typecheck": "tsgo --noEmit -p tsconfig.json",
|
|
26
|
+
"test": "bun ../../../scripts/quality/run-vitest.mjs --run"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.10.5",
|
|
30
|
+
"@types/npm-package-arg": "^6.1.4",
|
|
31
|
+
"@vetta-org/plugin-sdk": "workspace:*",
|
|
32
|
+
"@vetta/action-rpc": "workspace:*",
|
|
33
|
+
"npm-package-arg": "^14.0.0",
|
|
34
|
+
"tar": "^7.5.22",
|
|
35
|
+
"typescript": "^5.9.2",
|
|
36
|
+
"vitest": "^3.2.4"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|