@vetta-org/plugin-cli 0.1.6 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `@vetta-org/plugin-cli` are documented in this file.
4
4
 
5
+ ## Unreleased
6
+
7
+ ### Changed
8
+
9
+ - `add` and `add .` now use the dedicated `.vettapkg` plugin package format. Existing `.zip` files remain accepted as a compatibility import path.
10
+
11
+ - `add` now identifies itself to Desktop so ability lifecycle logs distinguish CLI installs from manual package imports and marketplace installs.
12
+
13
+ - `sync --check` accepts schema v3 plugin releases without local `dist/` and validates release metadata for independently listed and bundle-only plugins. The marketplace publication gate separately verifies the referenced App releases and artifact digests.
14
+
5
15
  ## [0.1.6] — 2026-09-14
6
16
 
7
17
  ### Fixed
package/README.md CHANGED
@@ -69,6 +69,13 @@ remotely: the host refuses to sync an entry whose version differs from the packa
69
69
  install a plugin whose built entry is missing from the published directory, and clients silently
70
70
  skip an update when `marketplaceVersion` did not change. `sync` reconciles all three.
71
71
 
72
+ For marketplace schema v3, a plugin may instead list immutable `releases[]` with HTTPS `.vettapkg` URLs
73
+ and SHA-256 digests. Its `source.path` then contains presentation files only. `sync --check`
74
+ checks release metadata and reconciles the catalog version with the highest release; the
75
+ Desktop installation verifies the downloaded package. Before advancing a stable marketplace ref,
76
+ run the publication check from a fixed `open-vetta` checkout as described in
77
+ [`docs/open-marketplace.md`](../../../docs/open-marketplace.md).
78
+
72
79
  ## Find the manual
73
80
 
74
81
  ```bash
@@ -101,8 +108,8 @@ The npm package is fetched with lifecycle scripts disabled. The CLI extracts onl
101
108
  Local archives and HTTP(S) archives use the same command:
102
109
 
103
110
  ```bash
104
- npx @vetta-org/plugin-cli add ./release/demo-1.0.0.zip
105
- npx @vetta-org/plugin-cli add https://example.com/demo-1.0.0.zip
111
+ npx @vetta-org/plugin-cli add ./release/demo-1.0.0.vettapkg
112
+ npx @vetta-org/plugin-cli add https://example.com/demo-1.0.0.vettapkg
106
113
  ```
107
114
 
108
115
  When an update is installed as a pending version, apply it through the running Desktop host instead of
@@ -125,12 +132,12 @@ The published plugin package must include a standard Desktop plugin archive and
125
132
  {
126
133
  "name": "@example/vetta-plugin-demo",
127
134
  "version": "1.0.0",
128
- "files": ["release/vetta-plugin.zip"],
135
+ "files": ["release/vetta-plugin.vettapkg"],
129
136
  "vetta": {
130
137
  "schemaVersion": 1,
131
138
  "type": "desktop-plugin",
132
139
  "pluginId": "demo",
133
- "archive": "release/vetta-plugin.zip"
140
+ "archive": "release/vetta-plugin.vettapkg"
134
141
  }
135
142
  }
136
143
  ```
package/dist/cli.js CHANGED
@@ -11905,6 +11905,18 @@ var agentExperimentalSettingsUpdateType = Type.Object({
11905
11905
  promptPrediction: Type.Optional(Type.Boolean()),
11906
11906
  agentSkills: Type.Optional(Type.Boolean())
11907
11907
  }, { additionalProperties: false, minProperties: 1 });
11908
+ var imageGenerationSettingsType = Type.Object({
11909
+ textToImageProviderId: Type.Optional(Type.String({ minLength: 1, maxLength: 129 })),
11910
+ textToImageModelId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
11911
+ imageToImageProviderId: Type.Optional(Type.String({ minLength: 1, maxLength: 129 })),
11912
+ imageToImageModelId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 }))
11913
+ }, { additionalProperties: false });
11914
+ var imageGenerationSettingsUpdateType = Type.Object({
11915
+ textToImageProviderId: Type.Optional(Type.Union([Type.String({ minLength: 1, maxLength: 129 }), Type.Null()])),
11916
+ textToImageModelId: Type.Optional(Type.Union([Type.String({ minLength: 1, maxLength: 256 }), Type.Null()])),
11917
+ imageToImageProviderId: Type.Optional(Type.Union([Type.String({ minLength: 1, maxLength: 129 }), Type.Null()])),
11918
+ imageToImageModelId: Type.Optional(Type.Union([Type.String({ minLength: 1, maxLength: 256 }), Type.Null()]))
11919
+ }, { additionalProperties: false, minProperties: 1 });
11908
11920
  var agentSettingsEmptyInputSchema = defineCapabilityInputSchema(agentSettingsEmptyInputType);
11909
11921
  var agentExperimentalSettingsSchema = defineCapabilityOutputSchema(agentExperimentalSettingsType, { clean: true });
11910
11922
  var agentExperimentalSettingsUpdateSchema = defineCapabilityInputSchema(agentExperimentalSettingsUpdateType);
@@ -11924,6 +11936,22 @@ var DOMAIN_AGENT_SETTINGS_CAPABILITIES = {
11924
11936
  version: 1,
11925
11937
  input: agentExperimentalSettingsUpdateSchema,
11926
11938
  output: agentExperimentalSettingsSchema
11939
+ }),
11940
+ GET_IMAGE_GENERATION: defineCapability({
11941
+ id: "cap.domain.vetta.agent-settings.image-generation.get",
11942
+ kind: "query",
11943
+ layer: CAPABILITY_LAYERS.DOMAIN,
11944
+ version: 1,
11945
+ input: agentSettingsEmptyInputSchema,
11946
+ output: defineCapabilityOutputSchema(imageGenerationSettingsType, { clean: true })
11947
+ }),
11948
+ SET_IMAGE_GENERATION: defineCapability({
11949
+ id: "cap.domain.vetta.agent-settings.image-generation.set",
11950
+ kind: "command",
11951
+ layer: CAPABILITY_LAYERS.DOMAIN,
11952
+ version: 1,
11953
+ input: defineCapabilityInputSchema(imageGenerationSettingsUpdateType),
11954
+ output: defineCapabilityOutputSchema(imageGenerationSettingsType, { clean: true })
11927
11955
  })
11928
11956
  };
11929
11957
  var DOMAIN_AGENT_SETTINGS_CAPABILITY_CATALOG = createCapabilityCatalog(Object.values(DOMAIN_AGENT_SETTINGS_CAPABILITIES));
@@ -13029,7 +13057,7 @@ var FOUNDATION_JOB_CAPABILITIES = {
13029
13057
  var FOUNDATION_JOB_CAPABILITY_CATALOG = createCapabilityCatalog(Object.values(FOUNDATION_JOB_CAPABILITIES));
13030
13058
 
13031
13059
  // ../../capability-sdk/dist/domain/media.js
13032
- var MEDIA_PROTOCOL_VERSION = 4;
13060
+ var MEDIA_PROTOCOL_VERSION = 5;
13033
13061
  var MEDIA_OPERATIONS = {
13034
13062
  GENERATE: "generate",
13035
13063
  COMPOSE: "compose",
@@ -13128,6 +13156,16 @@ var mediaGenerationModeCapabilityType = Type.Object({
13128
13156
  aspectRatioPolicy: Type.Optional(Type.Union([Type.Literal("configurable"), Type.Literal("input-derived")])),
13129
13157
  audioGeneration: Type.Optional(Type.Union([Type.Literal("none"), Type.Literal("always"), Type.Literal("optional")]))
13130
13158
  }, { additionalProperties: false });
13159
+ var mediaGenerationModelDescriptorType = Type.Object({
13160
+ id: requiredStringType2,
13161
+ displayName: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
13162
+ sourceId: Type.Optional(requiredStringType2),
13163
+ sourceDisplayName: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
13164
+ modes: Type.Array(mediaGenerationModeType, { minItems: 1 }),
13165
+ aspectRatios: Type.Optional(Type.Array(requiredStringType2)),
13166
+ resolutions: Type.Optional(Type.Array(requiredStringType2)),
13167
+ defaultResolution: Type.Optional(requiredStringType2)
13168
+ }, { additionalProperties: false });
13131
13169
  var mediaGenerateCapabilityType = Type.Object({
13132
13170
  operation: Type.Literal(MEDIA_OPERATIONS.GENERATE),
13133
13171
  kind: mediaGenerationKindType,
@@ -13135,6 +13173,8 @@ var mediaGenerateCapabilityType = Type.Object({
13135
13173
  aspectRatios: Type.Optional(Type.Array(requiredStringType2)),
13136
13174
  resolutions: Type.Optional(Type.Array(requiredStringType2)),
13137
13175
  defaultResolution: Type.Optional(requiredStringType2),
13176
+ models: Type.Optional(Type.Array(mediaGenerationModelDescriptorType, { minItems: 1 })),
13177
+ defaultModelId: Type.Optional(requiredStringType2),
13138
13178
  durationsSeconds: Type.Optional(Type.Array(Type.Number({ exclusiveMinimum: 0 }))),
13139
13179
  modeCapabilities: Type.Optional(Type.Array(mediaGenerationModeCapabilityType))
13140
13180
  }, { additionalProperties: false });
@@ -13318,6 +13358,8 @@ var modelProviderUpsertModelType = Type.Object({
13318
13358
  name: Type.Optional(Type.String()),
13319
13359
  api: Type.Optional(Type.String()),
13320
13360
  reasoning: Type.Optional(Type.Boolean()),
13361
+ reasoningLevels: modelDefinitionDetailType.properties.reasoningLevels,
13362
+ defaultReasoningLevel: modelDefinitionDetailType.properties.defaultReasoningLevel,
13321
13363
  contextWindow: Type.Optional(Type.Number()),
13322
13364
  maxTokens: Type.Optional(Type.Number())
13323
13365
  }, { additionalProperties: false });
@@ -19433,13 +19475,31 @@ function syncMarketplaceIndex(input) {
19433
19475
  for (const member of Array.isArray(bundleConfig?.members) ? bundleConfig.members : []) {
19434
19476
  if (typeof member !== "object" || member === null || Array.isArray(member))
19435
19477
  continue;
19436
- const memberSource = member.source;
19478
+ const memberEntry = member;
19479
+ const memberSource = memberEntry.source;
19437
19480
  const memberPath = typeof memberSource === "object" && memberSource !== null && !Array.isArray(memberSource) ? memberSource.path : undefined;
19438
19481
  if (typeof memberPath !== "string")
19439
19482
  continue;
19440
19483
  const dir = resolveAbilityDir(input.hubRoot, memberPath);
19441
- if (dir)
19484
+ if (dir) {
19442
19485
  listedDirs.add(dir);
19486
+ if (manifest.schemaVersion === 3 && memberEntry.type === "plugin") {
19487
+ const descriptor2 = readJsonFile2(join4(dir, "ability.json"));
19488
+ const memberChanges = [];
19489
+ reconcilePlugin({
19490
+ entry: { ...memberEntry, version: descriptor2?.version },
19491
+ slug: typeof memberEntry.slug === "string" ? memberEntry.slug : "(unnamed)",
19492
+ abilityDir: dir,
19493
+ schemaVersion: manifest.schemaVersion,
19494
+ minAppVersion: manifest.minAppVersion,
19495
+ changes: memberChanges,
19496
+ problems
19497
+ });
19498
+ for (const change of memberChanges) {
19499
+ problems.push({ slug: change.slug, message: `ability.json version does not match latest release: ${change.to}` });
19500
+ }
19501
+ }
19502
+ }
19443
19503
  }
19444
19504
  continue;
19445
19505
  }
@@ -19456,7 +19516,7 @@ function syncMarketplaceIndex(input) {
19456
19516
  }
19457
19517
  listedDirs.add(abilityDir);
19458
19518
  if (type === "plugin")
19459
- reconcilePlugin({ entry, slug, abilityDir, changes, problems });
19519
+ reconcilePlugin({ entry, slug, abilityDir, schemaVersion: manifest.schemaVersion, minAppVersion: manifest.minAppVersion, changes, problems });
19460
19520
  else if (type === "mcp")
19461
19521
  reconcileIdentityFile({ entry, slug, abilityDir, fileName: "mcp.json", changes, problems });
19462
19522
  }
@@ -19483,7 +19543,77 @@ function syncMarketplaceIndex(input) {
19483
19543
  return { manifestPath: input.manifestPath, changes, problems, unlisted, written };
19484
19544
  }
19485
19545
  function reconcilePlugin(context) {
19486
- const { entry, slug, abilityDir, changes, problems } = context;
19546
+ const { entry, slug, abilityDir, schemaVersion, minAppVersion, changes, problems } = context;
19547
+ if (schemaVersion === 3 && !("releases" in entry)) {
19548
+ problems.push({ slug, message: "schemaVersion 3 plugin requires versioned releases" });
19549
+ return;
19550
+ }
19551
+ if ("releases" in entry) {
19552
+ if (schemaVersion !== 3) {
19553
+ problems.push({ slug, message: "versioned plugin releases require marketplace schemaVersion 3" });
19554
+ return;
19555
+ }
19556
+ const releases = entry.releases;
19557
+ if (!Array.isArray(releases) || releases.length === 0) {
19558
+ problems.push({ slug, message: "plugin releases must be a nonempty array" });
19559
+ return;
19560
+ }
19561
+ let latest;
19562
+ const seen = new Set;
19563
+ for (const raw2 of releases) {
19564
+ if (typeof raw2 !== "object" || raw2 === null || Array.isArray(raw2)) {
19565
+ problems.push({ slug, message: "plugin release must be an object" });
19566
+ continue;
19567
+ }
19568
+ const release = raw2;
19569
+ const match2 = typeof release.version === "string" ? /^(\d+)\.(\d+)\.(\d+)$/.exec(release.version) : null;
19570
+ if (!match2 || seen.has(release.version)) {
19571
+ problems.push({ slug, message: `invalid or duplicate plugin release version: ${String(release.version)}` });
19572
+ continue;
19573
+ }
19574
+ seen.add(release.version);
19575
+ const minimum = typeof release.minAppVersion === "string" ? /^(\d+)\.(\d+)\.(\d+)$/.exec(release.minAppVersion) : null;
19576
+ if (!minimum || typeof minAppVersion !== "string" || !/^(\d+)\.(\d+)\.(\d+)$/.test(minAppVersion)) {
19577
+ problems.push({ slug, message: `release ${release.version} has an invalid minAppVersion` });
19578
+ } else {
19579
+ const marketMinimum = /^(\d+)\.(\d+)\.(\d+)$/.exec(minAppVersion);
19580
+ if (marketMinimum && compareVersionParts(minimum.slice(1).map(Number), marketMinimum.slice(1).map(Number)) < 0) {
19581
+ problems.push({ slug, message: `release ${release.version} requires an app older than the marketplace` });
19582
+ }
19583
+ }
19584
+ if (typeof release.pluginApiVersion !== "string" || !/^\^\d+\.\d+\.\d+$/.test(release.pluginApiVersion)) {
19585
+ problems.push({ slug, message: `release ${release.version} has an invalid pluginApiVersion` });
19586
+ }
19587
+ for (const field of ["permissions", "commands"]) {
19588
+ if (release[field] !== undefined && !stringArray(release[field])) {
19589
+ problems.push({ slug, message: `release ${release.version} has invalid ${field}` });
19590
+ }
19591
+ }
19592
+ const parts = [Number(match2[1]), Number(match2[2]), Number(match2[3])];
19593
+ if (!latest || compareVersionParts(parts, latest.parts) > 0) {
19594
+ latest = { version: release.version, parts };
19595
+ }
19596
+ const artifact = release.artifact;
19597
+ if (typeof artifact !== "object" || artifact === null || Array.isArray(artifact)) {
19598
+ problems.push({ slug, message: `release ${release.version} has no artifact` });
19599
+ continue;
19600
+ }
19601
+ const { url, sha256 } = artifact;
19602
+ let validUrl = false;
19603
+ try {
19604
+ const parsed = new URL(String(url));
19605
+ validUrl = parsed.protocol === "https:" && !parsed.username && !parsed.password && !parsed.hash;
19606
+ } catch {}
19607
+ if (!validUrl || typeof sha256 !== "string" || !/^[a-f0-9]{64}$/.test(sha256)) {
19608
+ problems.push({ slug, message: `release ${release.version} has an invalid HTTPS artifact or SHA-256` });
19609
+ }
19610
+ }
19611
+ if (latest && entry.version !== latest.version) {
19612
+ changes.push({ slug, field: "version", from: entry.version, to: latest.version });
19613
+ entry.version = latest.version;
19614
+ }
19615
+ return;
19616
+ }
19487
19617
  const manifest = readJsonFile2(join4(abilityDir, "plugin.json"));
19488
19618
  if (!manifest) {
19489
19619
  problems.push({ slug, message: "plugin.json is missing or malformed" });
@@ -19523,6 +19653,13 @@ function reconcilePlugin(context) {
19523
19653
  }
19524
19654
  }
19525
19655
  }
19656
+ function compareVersionParts(left, right) {
19657
+ for (let index = 0;index < 3; index += 1) {
19658
+ if (left[index] !== right[index])
19659
+ return (left[index] ?? 0) - (right[index] ?? 0);
19660
+ }
19661
+ return 0;
19662
+ }
19526
19663
  function reconcileIdentityFile(context) {
19527
19664
  const { entry, slug, abilityDir, fileName, changes, problems } = context;
19528
19665
  const identity = readJsonFile2(join4(abilityDir, fileName));
@@ -19654,7 +19791,7 @@ function readManualSdkVersion(manualDir) {
19654
19791
  var HELP_TEXT = `Vetta plugin manager
19655
19792
 
19656
19793
  Usage:
19657
- vetta-plugin-cli add <npm-package|zip-path|http-url> [--json]
19794
+ vetta-plugin-cli add <npm-package|package-path|http-url> [--json]
19658
19795
  vetta-plugin-cli reload <plugin-id> [--json]
19659
19796
  vetta-plugin-cli docs [--check-latest] [--json]
19660
19797
  vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]
@@ -19668,7 +19805,7 @@ Examples:
19668
19805
  npx @vetta-org/plugin-cli add @example/vetta-plugin-demo
19669
19806
  npx @vetta-org/plugin-cli add @example/vetta-plugin-demo@1.2.0
19670
19807
  npx @vetta-org/plugin-cli add . # 当前插件工程(先 pack)
19671
- npx @vetta-org/plugin-cli add ./release/demo-1.2.0.zip
19808
+ npx @vetta-org/plugin-cli add ./release/demo-1.2.0.vettapkg
19672
19809
  npx @vetta-org/plugin-cli reload demo
19673
19810
  npx @vetta-org/plugin-cli docs
19674
19811
  npx @vetta-org/plugin-cli init --id my-plugin --name "My Plugin"
@@ -19694,7 +19831,7 @@ function parsePluginAddCommand(argv) {
19694
19831
  }
19695
19832
  const [source, unexpected] = parsed.positionals;
19696
19833
  if (!source)
19697
- return { type: "error", message: "Missing <npm-package|zip-path|http-url>" };
19834
+ return { type: "error", message: "Missing <npm-package|package-path|http-url>" };
19698
19835
  if (unexpected)
19699
19836
  return { type: "error", message: `Unexpected argument: ${unexpected}` };
19700
19837
  return { type: "add", source, json: parsed.values.json === true };
@@ -19925,8 +20062,9 @@ function isHttpUrl(source) {
19925
20062
  return false;
19926
20063
  }
19927
20064
  }
19928
- function isLocalZip(source) {
19929
- if (source.toLowerCase().endsWith(".zip"))
20065
+ function isLocalPackage(source) {
20066
+ const lower = source.toLowerCase();
20067
+ if (lower.endsWith(".vettapkg") || lower.endsWith(".zip"))
19930
20068
  return true;
19931
20069
  const path = resolve5(source);
19932
20070
  return existsSync4(path) && !statSync2(path).isDirectory();
@@ -19945,7 +20083,7 @@ function resolveProjectArchive(source) {
19945
20083
  }
19946
20084
  throw new Error(`No plugin.json found in ${from} or any parent directory.`);
19947
20085
  }
19948
- const archivePath = join6(project.root, "release", `${project.pluginId}-${project.version}.zip`);
20086
+ const archivePath = join6(project.root, "release", `${project.pluginId}-${project.version}.vettapkg`);
19949
20087
  if (!existsSync4(archivePath)) {
19950
20088
  throw new Error(`Packaged archive not found: ${archivePath}
19951
20089
  Build it first: npm run build && npx vetta-plugin pack`);
@@ -19966,6 +20104,7 @@ function indexDriftHint(project) {
19966
20104
  function npmInstallInput(resolved) {
19967
20105
  return {
19968
20106
  operation: "install-from-path",
20107
+ initiator: "plugin-cli",
19969
20108
  path: resolved.archivePath,
19970
20109
  enable: true,
19971
20110
  source: "npm",
@@ -20059,19 +20198,22 @@ async function runPluginCommand(command, dependencies = defaultDependencies) {
20059
20198
  } else if (isHttpUrl(command.source)) {
20060
20199
  result = await dependencies.runAction("plugins.manage", {
20061
20200
  operation: "install-from-url",
20201
+ initiator: "plugin-cli",
20062
20202
  url: command.source
20063
20203
  });
20064
20204
  } else if (isDirectorySource(command.source)) {
20065
20205
  const { archivePath, project } = resolveProjectArchive(command.source);
20066
20206
  result = await dependencies.runAction("plugins.manage", {
20067
20207
  operation: "install-from-path",
20208
+ initiator: "plugin-cli",
20068
20209
  path: archivePath,
20069
20210
  enable: true
20070
20211
  });
20071
20212
  driftHint = indexDriftHint(project);
20072
- } else if (isLocalZip(command.source)) {
20213
+ } else if (isLocalPackage(command.source)) {
20073
20214
  result = await dependencies.runAction("plugins.manage", {
20074
20215
  operation: "install-from-path",
20216
+ initiator: "plugin-cli",
20075
20217
  path: resolve5(command.source),
20076
20218
  enable: true
20077
20219
  });
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { ActionRpcError, createActionRpcClient, readActionRpcEndpoint } from \"@vetta/action-rpc\";\nimport { readLatestNpmVersion, resolveNpmPluginArchive, type ResolvedNpmPluginArchive } from \"./npm-package.js\";\nimport { AGENTS_GUIDE_REVISION, readAgentsGuideRevision } from \"./agents-template.js\";\nimport { initHubRepository, initPluginProject, refreshAgentsGuide } from \"./init.js\";\nimport { describeIndexDrift, syncMarketplaceIndex } from \"./sync.js\";\nimport { findPluginHub, findPluginProject, type PluginProject, readManualSdkVersion, resolveManualDir } from \"./workspace.js\";\n\nexport type PluginAddCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"add\"; source: string; json: boolean };\n\nexport type PluginReloadCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"reload\"; pluginId: string; json: boolean };\n\nexport type PluginDocsCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"docs\"; json: boolean; checkLatest: boolean };\n\nexport type PluginInitCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"init\"; targetDir?: string; pluginId: string; displayName?: string; json: boolean }\n\t| { type: \"refresh-guide\"; targetDir?: string; json: boolean; force: boolean; dryRun: boolean }\n\t| { type: \"init-hub\"; targetDir?: string; name: string; repository: string; minAppVersion: string; json: boolean };\n\nexport type PluginWatchCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"watch\"; dir?: string; stop: boolean; json: boolean };\n\nexport type PluginUninstallCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"uninstall\"; pluginId?: string; json: boolean };\n\nexport type PluginSyncCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"sync\"; check: boolean; json: boolean };\n\nexport type PluginCommand =\n\t| PluginAddCommand\n\t| PluginSyncCommand\n\t| PluginUninstallCommand\n\t| PluginReloadCommand\n\t| PluginDocsCommand\n\t| PluginInitCommand\n\t| PluginWatchCommand;\n\nexport interface PluginCommandDependencies {\n\tresolveNpmArchive(packageSpec: string): Promise<ResolvedNpmPluginArchive>;\n\t/** 命令执行时所在目录;缺省用 process.cwd(),测试与非交互调用方可以覆盖。 */\n\tcwd?(): string;\n\trunAction(actionId: string, input: unknown): Promise<unknown>;\n\twriteStdout(value: string): void;\n\twriteStderr(value: string): void;\n\t/** `docs --check-latest` 查询 registry 上最新的 SDK 版本;查不到(离线、私服)返回 undefined。 */\n\treadLatestSdkVersion?(): Promise<string | undefined>;\n}\n\nexport type PluginAddCommandDependencies = PluginCommandDependencies;\n\nconst HELP_TEXT = `Vetta plugin manager\n\nUsage:\n vetta-plugin-cli add <npm-package|zip-path|http-url> [--json]\n vetta-plugin-cli reload <plugin-id> [--json]\n vetta-plugin-cli docs [--check-latest] [--json]\n vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]\n vetta-plugin-cli init --refresh-guide [dir] [--dry-run] [--force] [--json]\n vetta-plugin-cli init hub --name <slug> --repository <url> --min-app-version <x.y.z> [dir]\n vetta-plugin-cli watch [dir] [--stop] [--json]\n vetta-plugin-cli uninstall [plugin-id] [--json]\n vetta-plugin-cli sync [--check] [--json]\n\nExamples:\n npx @vetta-org/plugin-cli add @example/vetta-plugin-demo\n npx @vetta-org/plugin-cli add @example/vetta-plugin-demo@1.2.0\n npx @vetta-org/plugin-cli add . # 当前插件工程(先 pack)\n npx @vetta-org/plugin-cli add ./release/demo-1.2.0.zip\n npx @vetta-org/plugin-cli reload demo\n npx @vetta-org/plugin-cli docs\n npx @vetta-org/plugin-cli init --id my-plugin --name \"My Plugin\"\n npx @vetta-org/plugin-cli init hub --name my-market --repository https://github.com/me/my-market --min-app-version 0.55.0\n npx @vetta-org/plugin-cli watch # 让宿主改从工程目录加载,改完即生效\n npx @vetta-org/plugin-cli uninstall # 卸载当前插件工程对应的插件\n npx @vetta-org/plugin-cli sync # 在市场仓库根对账 .vetta/marketplace.json\n npx @vetta-org/plugin-cli sync --check # 只报不写,给 CI 用\n`;\n\nfunction formatParseError(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nexport function parsePluginAddCommand(argv: string[]): PluginAddCommand | undefined {\n\tif (argv[0] !== \"add\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({ args: argv.slice(1), allowPositionals: true, strict: true, options: { json: { type: \"boolean\" } } });\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [source, unexpected] = parsed.positionals;\n\tif (!source) return { type: \"error\", message: \"Missing <npm-package|zip-path|http-url>\" };\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"add\", source, json: parsed.values.json === true };\n}\n\nexport function parsePluginReloadCommand(argv: string[]): PluginReloadCommand | undefined {\n\tif (argv[0] !== \"reload\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({ args: argv.slice(1), allowPositionals: true, strict: true, options: { json: { type: \"boolean\" } } });\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [pluginId, unexpected] = parsed.positionals;\n\tif (!pluginId) return { type: \"error\", message: \"Missing <plugin-id>\" };\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"reload\", pluginId, json: parsed.values.json === true };\n}\n\nexport function parsePluginDocsCommand(argv: string[]): PluginDocsCommand | undefined {\n\tif (argv[0] !== \"docs\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, \"check-latest\": { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"docs\",\n\t\tjson: parsed.values.json === true,\n\t\tcheckLatest: parsed.values[\"check-latest\"] === true,\n\t};\n}\n\nexport function parsePluginInitCommand(argv: string[]): PluginInitCommand | undefined {\n\tif (argv[0] !== \"init\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tif (argv[1] === \"hub\") return parseInitHubCommand(argv.slice(2));\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: {\n\t\t\t\tid: { type: \"string\" },\n\t\t\t\tname: { type: \"string\" },\n\t\t\t\tjson: { type: \"boolean\" },\n\t\t\t\t\"refresh-guide\": { type: \"boolean\" },\n\t\t\t\tforce: { type: \"boolean\" },\n\t\t\t\t\"dry-run\": { type: \"boolean\" },\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tif (parsed.values[\"refresh-guide\"] === true) {\n\t\tconst [dir, extra] = parsed.positionals;\n\t\tif (extra) return { type: \"error\", message: `Unexpected argument: ${extra}` };\n\t\t// 刷新是就地重写,工程的 id 和展示名从磁盘上读,不再由命令行给。\n\t\treturn {\n\t\t\ttype: \"refresh-guide\",\n\t\t\t...(dir ? { targetDir: dir } : {}),\n\t\t\tjson: parsed.values.json === true,\n\t\t\tforce: parsed.values.force === true,\n\t\t\tdryRun: parsed.values[\"dry-run\"] === true,\n\t\t};\n\t}\n\tconst pluginId = parsed.values.id;\n\tif (typeof pluginId !== \"string\" || pluginId.length === 0) {\n\t\treturn { type: \"error\", message: \"Missing --id <plugin-id>\" };\n\t}\n\tconst [targetDir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"init\",\n\t\t...(targetDir ? { targetDir } : {}),\n\t\tpluginId,\n\t\t...(typeof parsed.values.name === \"string\" ? { displayName: parsed.values.name } : {}),\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nfunction parseInitHubCommand(argv: string[]): PluginInitCommand {\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv,\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: {\n\t\t\t\tname: { type: \"string\" },\n\t\t\t\trepository: { type: \"string\" },\n\t\t\t\t\"min-app-version\": { type: \"string\" },\n\t\t\t\tjson: { type: \"boolean\" },\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst name = parsed.values.name;\n\tif (typeof name !== \"string\" || name.length === 0) return { type: \"error\", message: \"Missing --name <slug>\" };\n\tconst repository = parsed.values.repository;\n\tif (typeof repository !== \"string\" || repository.length === 0) {\n\t\treturn { type: \"error\", message: \"Missing --repository <https url>\" };\n\t}\n\t// 刻意不给默认值:太低会让装不动新 schema 的旧客户端也去激活快照,太高则部分用户直接\n\t// 看不到这个市场。这是发布决定,不该由工具替作者猜。\n\tconst minAppVersion = parsed.values[\"min-app-version\"];\n\tif (typeof minAppVersion !== \"string\" || minAppVersion.length === 0) {\n\t\treturn {\n\t\t\ttype: \"error\",\n\t\t\tmessage: \"Missing --min-app-version <x.y.z> (the oldest Vetta Desktop version your abilities support)\",\n\t\t};\n\t}\n\tconst [targetDir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"init-hub\",\n\t\t...(targetDir ? { targetDir } : {}),\n\t\tname,\n\t\trepository,\n\t\tminAppVersion,\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nexport function parsePluginWatchCommand(argv: string[]): PluginWatchCommand | undefined {\n\tif (argv[0] !== \"watch\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, stop: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [dir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"watch\",\n\t\t...(dir ? { dir } : {}),\n\t\tstop: parsed.values.stop === true,\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nexport function parsePluginUninstallCommand(argv: string[]): PluginUninstallCommand | undefined {\n\tif (argv[0] !== \"uninstall\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [pluginId, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\t// 省略 id 时按 cwd 推断,语义与 add . / watch 一致:站在哪个插件里就作用于哪个。\n\treturn { type: \"uninstall\", ...(pluginId ? { pluginId } : {}), json: parsed.values.json === true };\n}\n\nexport function parsePluginSyncCommand(argv: string[]): PluginSyncCommand | undefined {\n\tif (argv[0] !== \"sync\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, check: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"sync\", check: parsed.values.check === true, json: parsed.values.json === true };\n}\n\nasync function defaultRunAction(actionId: string, input: unknown): Promise<unknown> {\n\tconst client = createActionRpcClient(await readActionRpcEndpoint());\n\treturn client.run(actionId, input);\n}\n\nconst defaultDependencies: PluginCommandDependencies = {\n\tresolveNpmArchive: resolveNpmPluginArchive,\n\tcwd: () => process.cwd(),\n\trunAction: defaultRunAction,\n\twriteStdout: (value) => process.stdout.write(value),\n\twriteStderr: (value) => process.stderr.write(value),\n\treadLatestSdkVersion: () => readLatestNpmVersion(\"@vetta-org/plugin-sdk\"),\n};\n\nfunction isHttpUrl(source: string): boolean {\n\ttry {\n\t\tconst url = new URL(source);\n\t\treturn url.protocol === \"http:\" || url.protocol === \"https:\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction isLocalZip(source: string): boolean {\n\tif (source.toLowerCase().endsWith(\".zip\")) return true;\n\tconst path = resolve(source);\n\t// 目录不是压缩包:它是一个插件工程,走 resolveProjectArchive 先找它打出来的产物。\n\treturn existsSync(path) && !statSync(path).isDirectory();\n}\n\nfunction isDirectorySource(source: string): boolean {\n\tconst path = resolve(source);\n\treturn existsSync(path) && statSync(path).isDirectory();\n}\n\n/**\n * 把「装当前这个工程」翻译成一个具体的归档路径。\n *\n * 这条路径是给 `install:vetta` 这类脚本用的:作者(或 Agent)在插件目录里跑一条命令就\n * 装进 Vetta,不必记住产物叫什么名字。找不到产物时给出该跑的那条命令,而不是报一个\n * 「文件不存在」让人自己猜。\n */\nfunction resolveProjectArchive(source: string): { archivePath: string; project: PluginProject } {\n\tconst from = resolve(source);\n\tconst project = findPluginProject(from);\n\tif (!project) {\n\t\tconst hub = findPluginHub(from);\n\t\tif (hub) {\n\t\t\tthrow new Error(\n\t\t\t\t`${from} indexes plugins but is not one itself. Run this from a plugin directory, or pass its path: vetta-plugin-cli add ./path/to/plugin`,\n\t\t\t);\n\t\t}\n\t\tthrow new Error(`No plugin.json found in ${from} or any parent directory.`);\n\t}\n\tconst archivePath = join(project.root, \"release\", `${project.pluginId}-${project.version}.zip`);\n\tif (!existsSync(archivePath)) {\n\t\tthrow new Error(\n\t\t\t`Packaged archive not found: ${archivePath}\\nBuild it first: npm run build && npx vetta-plugin pack`,\n\t\t);\n\t}\n\treturn { archivePath, project };\n}\n\n/** 装完立刻检查索引是否还停在旧版本;不在市场仓库里时什么也不说。 */\nfunction indexDriftHint(project: PluginProject): string | undefined {\n\tconst hub = findPluginHub(project.root);\n\tif (!hub) return undefined;\n\treturn describeIndexDrift({\n\t\thubRoot: hub.root,\n\t\tmanifestPath: hub.manifestPath,\n\t\tslug: project.pluginId,\n\t\tversion: project.version,\n\t});\n}\n\nfunction npmInstallInput(resolved: ResolvedNpmPluginArchive): Record<string, unknown> {\n\treturn {\n\t\toperation: \"install-from-path\",\n\t\tpath: resolved.archivePath,\n\t\tenable: true,\n\t\tsource: \"npm\",\n\t\texpectedSha256: resolved.expectedSha256,\n\t\texpectedId: resolved.packageManifest.vetta.pluginId,\n\t\texpectedVersion: resolved.packageManifest.version,\n\t\tnpm: {\n\t\t\tpackageName: resolved.packageManifest.name,\n\t\t\trequestedSpec: resolved.requestedSpec,\n\t\t\tresolvedVersion: resolved.packageManifest.version,\n\t\t\t...(resolved.integrity ? { integrity: resolved.integrity } : {}),\n\t\t},\n\t};\n}\n\nfunction resultSummary(result: unknown): string {\n\tif (typeof result !== \"object\" || result === null || Array.isArray(result)) return \"Plugin installed.\\n\";\n\tconst response = result as Record<string, unknown>;\n\tconst plugin =\n\t\ttypeof response.plugin === \"object\" && response.plugin !== null && !Array.isArray(response.plugin)\n\t\t\t? (response.plugin as Record<string, unknown>)\n\t\t\t: undefined;\n\tif (!plugin) return \"Plugin installed.\\n\";\n\tconst id = typeof plugin.id === \"string\" ? plugin.id : \"plugin\";\n\tconst version = typeof plugin.version === \"string\" ? `@${plugin.version}` : \"\";\n\tconst pending = typeof plugin.pendingVersion === \"string\"\n\t\t? ` Update ${plugin.pendingVersion} is pending reload. Run \\`vetta-plugin-cli reload ${id}\\` to apply it.`\n\t\t: \"\";\n\treturn `Installed ${id}${version}.${pending}\\n`;\n}\n\nfunction reloadResultSummary(result: unknown, requestedPluginId: string): string {\n\tif (typeof result !== \"object\" || result === null || Array.isArray(result)) {\n\t\treturn `Reloaded ${requestedPluginId}.\\n`;\n\t}\n\tconst response = result as Record<string, unknown>;\n\tconst plugin =\n\t\ttypeof response.plugin === \"object\" && response.plugin !== null && !Array.isArray(response.plugin)\n\t\t\t? (response.plugin as Record<string, unknown>)\n\t\t\t: undefined;\n\tconst id = typeof plugin?.id === \"string\" ? plugin.id : requestedPluginId;\n\tconst version = typeof plugin?.activeVersion === \"string\" ? `@${plugin.activeVersion}` : \"\";\n\treturn `Reloaded ${id}${version}.\\n`;\n}\n\nfunction isConnectionError(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst code = (error as NodeJS.ErrnoException).code;\n\treturn (\n\t\tcode === \"ENOENT\" ||\n\t\tcode === \"ECONNREFUSED\" ||\n\t\tcode === \"ECONNRESET\" ||\n\t\terror.message.includes(\"ECONNREFUSED\") ||\n\t\terror.message.includes(\"fetch failed\")\n\t);\n}\n\nexport async function runPluginAddCommand(\n\tcommand: PluginAddCommand,\n\tdependencies: PluginAddCommandDependencies = defaultDependencies,\n): Promise<number> {\n\treturn runPluginCommand(command, dependencies);\n}\n\nexport async function runPluginCommand(\n\tcommand: PluginCommand,\n\tdependencies: PluginCommandDependencies = defaultDependencies,\n): Promise<number> {\n\tif (command.type === \"help\") {\n\t\tdependencies.writeStdout(HELP_TEXT);\n\t\treturn 0;\n\t}\n\tif (command.type === \"error\") {\n\t\tdependencies.writeStderr(`${command.message}\\n`);\n\t\treturn 2;\n\t}\n\n\tif (command.type === \"docs\") {\n\t\treturn await runDocsCommand(command, dependencies);\n\t}\n\tif (command.type === \"init\") {\n\t\treturn runInitCommand(command, dependencies);\n\t}\n\tif (command.type === \"refresh-guide\") {\n\t\treturn runRefreshGuideCommand(command, dependencies);\n\t}\n\tif (command.type === \"init-hub\") {\n\t\treturn runInitHubCommand(command, dependencies);\n\t}\n\tif (command.type === \"sync\") {\n\t\treturn runSyncCommand(command, dependencies);\n\t}\n\n\tif (command.type === \"watch\") {\n\t\treturn runWatchCommand(command, dependencies);\n\t}\n\tif (command.type === \"uninstall\") {\n\t\treturn runUninstallCommand(command, dependencies);\n\t}\n\n\tlet resolvedNpm: ResolvedNpmPluginArchive | undefined;\n\tlet driftHint: string | undefined;\n\ttry {\n\t\tlet result: unknown;\n\t\tif (command.type === \"reload\") {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"reload\",\n\t\t\t\tid: command.pluginId,\n\t\t\t});\n\t\t} else if (isHttpUrl(command.source)) {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-url\",\n\t\t\t\turl: command.source,\n\t\t\t});\n\t\t} else if (isDirectorySource(command.source)) {\n\t\t\tconst { archivePath, project } = resolveProjectArchive(command.source);\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-path\",\n\t\t\t\tpath: archivePath,\n\t\t\t\tenable: true,\n\t\t\t});\n\t\t\tdriftHint = indexDriftHint(project);\n\t\t} else if (isLocalZip(command.source)) {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-path\",\n\t\t\t\tpath: resolve(command.source),\n\t\t\t\tenable: true,\n\t\t\t});\n\t\t} else {\n\t\t\tresolvedNpm = await dependencies.resolveNpmArchive(command.source);\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", npmInstallInput(resolvedNpm));\n\t\t}\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, result, ...(driftHint ? { warning: driftHint } : {}) })}\\n`\n\t\t\t\t: command.type === \"reload\"\n\t\t\t\t\t? reloadResultSummary(result, command.pluginId)\n\t\t\t\t\t: `${resultSummary(result)}${driftHint ? `${driftHint}\\n` : \"\"}`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : command.type === \"reload\" ? \"PLUGIN_RELOAD_FAILED\" : \"PLUGIN_ADD_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t} finally {\n\t\tawait resolvedNpm?.cleanup();\n\t}\n}\n\n/**\n * 打印随 SDK 发布的手册目录。\n *\n * 存在的理由是「不要让任何人硬编码 node_modules 路径」:工作区会把依赖提升到仓库根,\n * 一仓多插件的 hub 里每个插件也可能各装一份。Agent 只需记住这一条命令,拿回来的永远是\n * 当前工程实际编译所针对的那个 SDK 版本的手册。\n */\nasync function runDocsCommand(\n\tcommand: { json: boolean; checkLatest: boolean },\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst manualDir = resolveManualDir(cwd);\n\tif (!manualDir) {\n\t\t// 能力市场仓库的根目录通常没装 SDK,手册在各能力目录里。直接说「装 SDK」会把人引到\n\t\t// 仓库根去装一份用不上的依赖。\n\t\tconst inHubRoot = findPluginHub(cwd) !== undefined && findPluginProject(cwd) === undefined;\n\t\tconst message = inHubRoot\n\t\t\t? \"Plugin manual not found at the hub root. cd into an ability directory (abilities/plugins/<slug>), then run npm install.\\n\"\n\t\t\t: \"Plugin manual not found. Install the SDK first: npm i -D @vetta-org/plugin-sdk\\n\";\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: \"MANUAL_NOT_FOUND\", message: message.trim() } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(message);\n\t\t}\n\t\treturn 6;\n\t}\n\tconst project = findPluginProject(cwd);\n\tconst hub = findPluginHub(cwd);\n\tconst sdkVersion = readManualSdkVersion(manualDir);\n\tconst latestVersion = command.checkLatest ? await dependencies.readLatestSdkVersion?.() : undefined;\n\tconst outdated = sdkVersion !== undefined && latestVersion !== undefined && compareSemver(sdkVersion, latestVersion) < 0;\n\tconst guide = inspectAgentsGuide(project?.root ?? hub?.root ?? cwd);\n\n\tif (command.json) {\n\t\tdependencies.writeStdout(\n\t\t\t`${JSON.stringify({\n\t\t\t\tok: true,\n\t\t\t\tmanualDir,\n\t\t\t\tentry: join(manualDir, \"README.md\"),\n\t\t\t\tsdkVersion,\n\t\t\t\trefreshCommand: SDK_REFRESH_COMMAND,\n\t\t\t\tguide,\n\t\t\t\t...(command.checkLatest ? { latestVersion, outdated } : {}),\n\t\t\t\tproject: project ? { root: project.root, pluginId: project.pluginId, version: project.version } : undefined,\n\t\t\t\thub: hub\n\t\t\t\t\t? {\n\t\t\t\t\t\t\troot: hub.root,\n\t\t\t\t\t\t\tmanifestPath: hub.manifestPath,\n\t\t\t\t\t\t\tsyncHint: \"After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t})}\\n`,\n\t\t);\n\t\treturn 0;\n\t}\n\tconst lines = [\n\t\t`Plugin manual (@vetta-org/plugin-sdk${sdkVersion ? `@${sdkVersion}` : \"\"}):`,\n\t\t` ${manualDir}`,\n\t\t`Start here: ${join(manualDir, \"README.md\")}`,\n\t];\n\tif (project) lines.push(`Current plugin: ${project.pluginId} (${project.root})`);\n\tif (outdated) {\n\t\tlines.push(`Manual is behind: ${sdkVersion} → ${latestVersion}. Refresh it with: ${SDK_REFRESH_COMMAND}`);\n\t} else if (command.checkLatest && latestVersion === undefined) {\n\t\tlines.push(`Could not reach the registry; cannot tell whether ${sdkVersion ?? \"this manual\"} is current.`);\n\t} else {\n\t\t// 手册是随 SDK 装进 node_modules 的快照,工程不升级它就永远停在初始化那天的版本。\n\t\t// 这条命令必须每次都打印:读到它的 Agent 手上的 AGENTS.md 往往也是同一天的快照。\n\t\tlines.push(`Manual follows the installed SDK. To refresh it: ${SDK_REFRESH_COMMAND}`);\n\t}\n\tif (guide.stale) {\n\t\t// 说明书同样是快照,而且用户没有理由回头看它。这里是唯一会被读到的位置。\n\t\tlines.push(\n\t\t\t`This brief is stale (AGENTS.md revision ${guide.revision} < ${AGENTS_GUIDE_REVISION}). Refresh it with: ${GUIDE_REFRESH_COMMAND}`,\n\t\t);\n\t} else if (guide.unstamped) {\n\t\t// 没有版本戳的文件与手写内容无从区分——能力市场仓库的根 AGENTS.md 往往是一整本手写的\n\t\t// 市场规范。绝不能把它引导成一条覆盖命令。\n\t\tlines.push(\n\t\t\t`AGENTS.md has no revision marker, so it looks hand-written. Review the current template with \\`${GUIDE_REFRESH_COMMAND} --dry-run\\` and merge by hand; do not overwrite it blindly.`,\n\t\t);\n\t}\n\tif (hub) {\n\t\tlines.push(`Marketplace index: ${hub.manifestPath}`);\n\t\t// Agent 几乎一定会先跑 docs,所以这是告诉它「索引要对账」的最佳时机。\n\t\tlines.push(\"After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.\");\n\t}\n\tdependencies.writeStdout(`${lines.join(\"\\n\")}\\n`);\n\treturn 0;\n}\n\n/**\n * 刷新手册的命令。\n *\n * 手册不从网络现取,而是随 SDK 进 `node_modules`——Agent 读到的合同因此与工程实际编译的\n * 版本一致。代价是它不会自己变新,所以「怎么变新」必须由 CLI 每次说一遍:`npx` 默认取最新的\n * CLI,它的输出是这条链路上唯一不会过期的位置。\n */\nconst SDK_REFRESH_COMMAND = \"npm i -D @vetta-org/plugin-sdk@latest && npx vetta-plugin-cli docs\";\n\n/** 刷新说明书的命令。与手册各刷各的:一个随 SDK 走,一个随 CLI 走。 */\nconst GUIDE_REFRESH_COMMAND = \"npx @vetta-org/plugin-cli init --refresh-guide\";\n\nexport interface AgentsGuideStatus {\n\t/** 本工程有没有 AGENTS.md。 */\n\treadonly present: boolean;\n\t/** 读到的版本戳;没有戳(模板早于版本戳,或是手写的)时缺省。 */\n\treadonly revision?: number;\n\t/**\n\t * **带戳**且落后于当前 CLI 的模板——只有这一档能安全地一键重写。\n\t *\n\t * 没有 AGENTS.md 时为 false(那是「没有」,不是「旧」);没有戳时也为 false,见 {@link unstamped}。\n\t */\n\treadonly stale: boolean;\n\t/** 有文件但没有版本戳:可能是手写的,也可能是版本戳之前的模板,无从区分。 */\n\treadonly unstamped: boolean;\n}\n\n/**\n * 判断工程里的 AGENTS.md 是不是旧模板。\n *\n * 说明书凝固在 `init` 那天,而用户没有理由回头看它——所以「它旧了」这件事只能由每次都会被\n * 跑到的 `docs` 说出来。\n *\n * **没有版本戳的不算「旧」,只算「来路不明」。** 早先把它判成 stale 并引导去跑刷新命令,等于\n * 教用户覆盖自己手写的文件——能力市场仓库的根 `AGENTS.md` 常常是一整本手写的市场规范。\n */\nfunction inspectAgentsGuide(root: string): AgentsGuideStatus {\n\tconst path = join(root, \"AGENTS.md\");\n\tif (!existsSync(path)) return { present: false, stale: false, unstamped: false };\n\tlet revision: number | undefined;\n\ttry {\n\t\trevision = readAgentsGuideRevision(readFileSync(path, \"utf8\"));\n\t} catch {\n\t\treturn { present: true, stale: false, unstamped: false };\n\t}\n\treturn {\n\t\tpresent: true,\n\t\t...(revision === undefined ? {} : { revision }),\n\t\tstale: revision !== undefined && revision < AGENTS_GUIDE_REVISION,\n\t\tunstamped: revision === undefined,\n\t};\n}\n\n/** 够用的 semver 比较:只看 major.minor.patch,预发布后缀一律当作小于正式版。 */\nfunction compareSemver(left: string, right: string): number {\n\tconst parse = (value: string): readonly [number, number, number, boolean] => {\n\t\tconst match = /^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$/.exec(value.trim());\n\t\tif (!match) return [0, 0, 0, false];\n\t\treturn [Number(match[1]), Number(match[2]), Number(match[3]), match[4] !== undefined];\n\t};\n\tconst a = parse(left);\n\tconst b = parse(right);\n\tfor (let index = 0; index < 3; index += 1) {\n\t\tif (a[index] !== b[index]) return a[index]! < b[index]! ? -1 : 1;\n\t}\n\tif (a[3] === b[3]) return 0;\n\treturn a[3] ? -1 : 1;\n}\n\n/**\n * 在已有工程里把 AGENTS.md 重写成当前 CLI 的版本。\n *\n * `init` 拒绝覆盖已有工程,所以老目录里那份说明书从落地起就停在原地。它是纯派生产物,重写\n * 它不会碰用户写过的任何东西——这也是唯一一个能这么做的脚手架文件。\n */\nfunction runRefreshGuideCommand(\n\tcommand: Extract<PluginCommand, { type: \"refresh-guide\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = refreshAgentsGuide(resolve(cwd, command.targetDir ?? \".\"), {\n\t\t\tforce: command.force,\n\t\t\tdryRun: command.dryRun,\n\t\t});\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: true, ...result })}\\n`);\n\t\t\treturn 0;\n\t\t}\n\t\tdependencies.writeStdout(\n\t\t\tresult.written\n\t\t\t\t? `Rewrote ${result.file}\\nNext: npx vetta-plugin-cli docs --check-latest\\n`\n\t\t\t\t// dry-run 把正文直接吐到 stdout,人工合并时可以重定向成文件再 diff。\n\t\t\t\t: `${result.content}`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: \"GUIDE_REFRESH_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 7;\n\t}\n}\n\n/** 在陌生目录里生成一个可直接开工的插件工程,并留下让任意 Agent 自举的 AGENTS.md。 */\nfunction runInitCommand(\n\tcommand: Extract<PluginCommand, { type: \"init\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = initPluginProject({\n\t\t\ttargetDir: resolve(cwd, command.targetDir ?? command.pluginId),\n\t\t\tpluginId: command.pluginId,\n\t\t\tdisplayName: command.displayName ?? command.pluginId,\n\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: [\n\t\t\t\t\t\t`Created ${result.pluginId} at ${result.root}`,\n\t\t\t\t\t\t\"Next: npm install && npm run install:vetta\",\n\t\t\t\t\t\t\"The agent brief is in AGENTS.md; after npm install, run `npx vetta-plugin-cli docs` for the manual.\",\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t\t.concat(\"\\n\"),\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"PLUGIN_INIT_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\n/**\n * 让宿主改从工程目录加载本插件,之后改源码即时生效,不必每次 build → pack → install。\n *\n * 目标插件按 cwd 向上找,理由同 `add .`:一仓多插件时「我正站在哪个插件里」是唯一不会\n * 弄错的意图,而 id 靠人重复输入迟早会错配到另一个插件上。\n */\nasync function runWatchCommand(\n\tcommand: Extract<PluginCommand, { type: \"watch\" }>,\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst from = resolve(cwd, command.dir ?? \".\");\n\ttry {\n\t\tconst project = findPluginProject(from);\n\t\tif (!project) {\n\t\t\tconst hub = findPluginHub(from);\n\t\t\tthrow new Error(\n\t\t\t\thub\n\t\t\t\t\t? `${from} indexes plugins but is not one itself. Run this from a plugin directory, or pass its path.`\n\t\t\t\t\t: `No plugin.json found in ${from} or any parent directory.`,\n\t\t\t);\n\t\t}\n\t\tconst result = command.stop\n\t\t\t? await dependencies.runAction(\"plugins.manage\", { operation: \"dev-watch-stop\", id: project.pluginId })\n\t\t\t: await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\t\toperation: \"dev-watch\",\n\t\t\t\t\tid: project.pluginId,\n\t\t\t\t\tprojectDir: project.root,\n\t\t\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, result })}\\n`\n\t\t\t\t: command.stop\n\t\t\t\t\t? `Stopped hot reload for ${project.pluginId}.\\n`\n\t\t\t\t\t: `Hot reload on for ${project.pluginId}. Vetta now loads it from ${project.root}.\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : \"PLUGIN_WATCH_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t}\n}\n\n/**\n * 卸载一个插件。省略 id 时按 cwd 推断,语义与 `add .` / `watch` 一致。\n *\n * 刻意不在这里做二次确认:宿主自己会为写操作弹审批,CLI 再问一遍只是噪音。系统插件由\n * 宿主拒绝,这里不重复判断——那份名单不该有第二个真相源。\n */\nasync function runUninstallCommand(\n\tcommand: Extract<PluginCommand, { type: \"uninstall\" }>,\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tlet pluginId = command.pluginId;\n\t\tif (!pluginId) {\n\t\t\tconst project = findPluginProject(cwd);\n\t\t\tif (!project) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No plugin.json found in ${cwd} or any parent directory. Pass the id: vetta-plugin-cli uninstall <plugin-id>`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tpluginId = project.pluginId;\n\t\t}\n\t\tconst result = await dependencies.runAction(\"plugins.manage\", { operation: \"uninstall\", id: pluginId });\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json ? `${JSON.stringify({ ok: true, result })}\\n` : `Uninstalled ${pluginId}.\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : \"PLUGIN_UNINSTALL_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t}\n}\n\n/**\n * 对账能力市场索引。定位靠向上找 `.vetta/marketplace.json`,因此在仓库任何位置都能跑。\n *\n * `--check` 只报不写并以非零退出,给 CI 用:索引漂移的三种后果里,两种不在作者机器上复现,\n * 一种压根不报错,光靠人自觉看不住。\n */\nfunction runSyncCommand(\n\tcommand: Extract<PluginCommand, { type: \"sync\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst hub = findPluginHub(cwd);\n\tif (!hub) {\n\t\tconst message = `No .vetta/marketplace.json found in ${cwd} or any parent directory. sync is for marketplace repositories.\\n`;\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"HUB_NOT_FOUND\", message: message.trim() } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(message);\n\t\t}\n\t\treturn 6;\n\t}\n\ttry {\n\t\tconst result = syncMarketplaceIndex({ hubRoot: hub.root, manifestPath: hub.manifestPath, apply: !command.check });\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: result.problems.length === 0, ...result })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStdout(formatSyncReport(result, command.check));\n\t\t}\n\t\tif (result.problems.length > 0) return 7;\n\t\t// --check 的职责就是「有漂移就红」,否则 CI 拦不住任何东西。\n\t\treturn command.check && result.changes.length > 0 ? 7 : 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"SYNC_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\nfunction formatSyncReport(result: ReturnType<typeof syncMarketplaceIndex>, check: boolean): string {\n\tconst lines: string[] = [];\n\tfor (const change of result.changes) {\n\t\tlines.push(` ${change.slug}: ${change.field} ${JSON.stringify(change.from)} -> ${JSON.stringify(change.to)}`);\n\t}\n\tif (lines.length > 0) {\n\t\tlines.unshift(check ? \"Index is out of date:\" : \"Updated the index:\");\n\t}\n\tif (result.problems.length > 0) {\n\t\tlines.push(\"Problems:\");\n\t\tfor (const problem of result.problems) lines.push(` ${problem.slug}: ${problem.message}`);\n\t}\n\tif (result.unlisted.length > 0) {\n\t\tlines.push(\"Ability directories not listed in the index (add them by hand when ready to publish):\");\n\t\tfor (const dir of result.unlisted) lines.push(` ${dir}`);\n\t}\n\tif (lines.length === 0) return \"Index is in sync.\\n\";\n\tif (check && result.changes.length > 0) lines.push(\"Run `vetta-plugin-cli sync` to apply.\");\n\treturn `${lines.join(\"\\n\")}\\n`;\n}\n\n/** 生成一个合规的能力市场仓库骨架,连同仓库级 AGENTS.md 与对账用的 CI。 */\nfunction runInitHubCommand(\n\tcommand: Extract<PluginCommand, { type: \"init-hub\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = initHubRepository({\n\t\t\ttargetDir: resolve(cwd, command.targetDir ?? command.name),\n\t\t\tname: command.name,\n\t\t\trepository: command.repository,\n\t\t\tminAppVersion: command.minAppVersion,\n\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: [\n\t\t\t\t\t\t`Created marketplace ${result.name} at ${result.root}`,\n\t\t\t\t\t\t\"Add an ability: npx @vetta-org/plugin-cli init --id <slug> --name \\\"<Display>\\\" abilities/plugins/<slug>\",\n\t\t\t\t\t\t\"Then list it in .vetta/marketplace.json and run: npx @vetta-org/plugin-cli sync\",\n\t\t\t\t\t\t\"The working agreement for agents is in AGENTS.md.\",\n\t\t\t\t\t].join(\"\\n\") + \"\\n\",\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"HUB_INIT_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\nexport async function runPluginCli(argv: string[]): Promise<number> {\n\tif (argv.length === 0 || argv[0] === \"-h\" || argv[0] === \"--help\") {\n\t\treturn runPluginAddCommand({ type: \"help\" });\n\t}\n\tconst command = parsePluginAddCommand(argv) ?? parsePluginReloadCommand(argv) ?? parsePluginDocsCommand(argv) ?? parsePluginInitCommand(argv) ?? parsePluginWatchCommand(argv) ?? parsePluginUninstallCommand(argv) ?? parsePluginSyncCommand(argv);\n\tif (!command) {\n\t\tprocess.stderr.write(`Unknown command: ${argv[0]}\\n`);\n\t\treturn 2;\n\t}\n\treturn runPluginCommand(command);\n}\n"]}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { ActionRpcError, createActionRpcClient, readActionRpcEndpoint } from \"@vetta/action-rpc\";\nimport { readLatestNpmVersion, resolveNpmPluginArchive, type ResolvedNpmPluginArchive } from \"./npm-package.js\";\nimport { AGENTS_GUIDE_REVISION, readAgentsGuideRevision } from \"./agents-template.js\";\nimport { initHubRepository, initPluginProject, refreshAgentsGuide } from \"./init.js\";\nimport { describeIndexDrift, syncMarketplaceIndex } from \"./sync.js\";\nimport { findPluginHub, findPluginProject, type PluginProject, readManualSdkVersion, resolveManualDir } from \"./workspace.js\";\n\nexport type PluginAddCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"add\"; source: string; json: boolean };\n\nexport type PluginReloadCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"reload\"; pluginId: string; json: boolean };\n\nexport type PluginDocsCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"docs\"; json: boolean; checkLatest: boolean };\n\nexport type PluginInitCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"init\"; targetDir?: string; pluginId: string; displayName?: string; json: boolean }\n\t| { type: \"refresh-guide\"; targetDir?: string; json: boolean; force: boolean; dryRun: boolean }\n\t| { type: \"init-hub\"; targetDir?: string; name: string; repository: string; minAppVersion: string; json: boolean };\n\nexport type PluginWatchCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"watch\"; dir?: string; stop: boolean; json: boolean };\n\nexport type PluginUninstallCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"uninstall\"; pluginId?: string; json: boolean };\n\nexport type PluginSyncCommand =\n\t| { type: \"help\" }\n\t| { type: \"error\"; message: string }\n\t| { type: \"sync\"; check: boolean; json: boolean };\n\nexport type PluginCommand =\n\t| PluginAddCommand\n\t| PluginSyncCommand\n\t| PluginUninstallCommand\n\t| PluginReloadCommand\n\t| PluginDocsCommand\n\t| PluginInitCommand\n\t| PluginWatchCommand;\n\nexport interface PluginCommandDependencies {\n\tresolveNpmArchive(packageSpec: string): Promise<ResolvedNpmPluginArchive>;\n\t/** 命令执行时所在目录;缺省用 process.cwd(),测试与非交互调用方可以覆盖。 */\n\tcwd?(): string;\n\trunAction(actionId: string, input: unknown): Promise<unknown>;\n\twriteStdout(value: string): void;\n\twriteStderr(value: string): void;\n\t/** `docs --check-latest` 查询 registry 上最新的 SDK 版本;查不到(离线、私服)返回 undefined。 */\n\treadLatestSdkVersion?(): Promise<string | undefined>;\n}\n\nexport type PluginAddCommandDependencies = PluginCommandDependencies;\n\nconst HELP_TEXT = `Vetta plugin manager\n\nUsage:\n vetta-plugin-cli add <npm-package|package-path|http-url> [--json]\n vetta-plugin-cli reload <plugin-id> [--json]\n vetta-plugin-cli docs [--check-latest] [--json]\n vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]\n vetta-plugin-cli init --refresh-guide [dir] [--dry-run] [--force] [--json]\n vetta-plugin-cli init hub --name <slug> --repository <url> --min-app-version <x.y.z> [dir]\n vetta-plugin-cli watch [dir] [--stop] [--json]\n vetta-plugin-cli uninstall [plugin-id] [--json]\n vetta-plugin-cli sync [--check] [--json]\n\nExamples:\n npx @vetta-org/plugin-cli add @example/vetta-plugin-demo\n npx @vetta-org/plugin-cli add @example/vetta-plugin-demo@1.2.0\n npx @vetta-org/plugin-cli add . # 当前插件工程(先 pack)\n npx @vetta-org/plugin-cli add ./release/demo-1.2.0.vettapkg\n npx @vetta-org/plugin-cli reload demo\n npx @vetta-org/plugin-cli docs\n npx @vetta-org/plugin-cli init --id my-plugin --name \"My Plugin\"\n npx @vetta-org/plugin-cli init hub --name my-market --repository https://github.com/me/my-market --min-app-version 0.55.0\n npx @vetta-org/plugin-cli watch # 让宿主改从工程目录加载,改完即生效\n npx @vetta-org/plugin-cli uninstall # 卸载当前插件工程对应的插件\n npx @vetta-org/plugin-cli sync # 在市场仓库根对账 .vetta/marketplace.json\n npx @vetta-org/plugin-cli sync --check # 只报不写,给 CI 用\n`;\n\nfunction formatParseError(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nexport function parsePluginAddCommand(argv: string[]): PluginAddCommand | undefined {\n\tif (argv[0] !== \"add\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({ args: argv.slice(1), allowPositionals: true, strict: true, options: { json: { type: \"boolean\" } } });\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [source, unexpected] = parsed.positionals;\n\tif (!source) return { type: \"error\", message: \"Missing <npm-package|package-path|http-url>\" };\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"add\", source, json: parsed.values.json === true };\n}\n\nexport function parsePluginReloadCommand(argv: string[]): PluginReloadCommand | undefined {\n\tif (argv[0] !== \"reload\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({ args: argv.slice(1), allowPositionals: true, strict: true, options: { json: { type: \"boolean\" } } });\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [pluginId, unexpected] = parsed.positionals;\n\tif (!pluginId) return { type: \"error\", message: \"Missing <plugin-id>\" };\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"reload\", pluginId, json: parsed.values.json === true };\n}\n\nexport function parsePluginDocsCommand(argv: string[]): PluginDocsCommand | undefined {\n\tif (argv[0] !== \"docs\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, \"check-latest\": { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"docs\",\n\t\tjson: parsed.values.json === true,\n\t\tcheckLatest: parsed.values[\"check-latest\"] === true,\n\t};\n}\n\nexport function parsePluginInitCommand(argv: string[]): PluginInitCommand | undefined {\n\tif (argv[0] !== \"init\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tif (argv[1] === \"hub\") return parseInitHubCommand(argv.slice(2));\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: {\n\t\t\t\tid: { type: \"string\" },\n\t\t\t\tname: { type: \"string\" },\n\t\t\t\tjson: { type: \"boolean\" },\n\t\t\t\t\"refresh-guide\": { type: \"boolean\" },\n\t\t\t\tforce: { type: \"boolean\" },\n\t\t\t\t\"dry-run\": { type: \"boolean\" },\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tif (parsed.values[\"refresh-guide\"] === true) {\n\t\tconst [dir, extra] = parsed.positionals;\n\t\tif (extra) return { type: \"error\", message: `Unexpected argument: ${extra}` };\n\t\t// 刷新是就地重写,工程的 id 和展示名从磁盘上读,不再由命令行给。\n\t\treturn {\n\t\t\ttype: \"refresh-guide\",\n\t\t\t...(dir ? { targetDir: dir } : {}),\n\t\t\tjson: parsed.values.json === true,\n\t\t\tforce: parsed.values.force === true,\n\t\t\tdryRun: parsed.values[\"dry-run\"] === true,\n\t\t};\n\t}\n\tconst pluginId = parsed.values.id;\n\tif (typeof pluginId !== \"string\" || pluginId.length === 0) {\n\t\treturn { type: \"error\", message: \"Missing --id <plugin-id>\" };\n\t}\n\tconst [targetDir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"init\",\n\t\t...(targetDir ? { targetDir } : {}),\n\t\tpluginId,\n\t\t...(typeof parsed.values.name === \"string\" ? { displayName: parsed.values.name } : {}),\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nfunction parseInitHubCommand(argv: string[]): PluginInitCommand {\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv,\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: {\n\t\t\t\tname: { type: \"string\" },\n\t\t\t\trepository: { type: \"string\" },\n\t\t\t\t\"min-app-version\": { type: \"string\" },\n\t\t\t\tjson: { type: \"boolean\" },\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst name = parsed.values.name;\n\tif (typeof name !== \"string\" || name.length === 0) return { type: \"error\", message: \"Missing --name <slug>\" };\n\tconst repository = parsed.values.repository;\n\tif (typeof repository !== \"string\" || repository.length === 0) {\n\t\treturn { type: \"error\", message: \"Missing --repository <https url>\" };\n\t}\n\t// 刻意不给默认值:太低会让装不动新 schema 的旧客户端也去激活快照,太高则部分用户直接\n\t// 看不到这个市场。这是发布决定,不该由工具替作者猜。\n\tconst minAppVersion = parsed.values[\"min-app-version\"];\n\tif (typeof minAppVersion !== \"string\" || minAppVersion.length === 0) {\n\t\treturn {\n\t\t\ttype: \"error\",\n\t\t\tmessage: \"Missing --min-app-version <x.y.z> (the oldest Vetta Desktop version your abilities support)\",\n\t\t};\n\t}\n\tconst [targetDir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"init-hub\",\n\t\t...(targetDir ? { targetDir } : {}),\n\t\tname,\n\t\trepository,\n\t\tminAppVersion,\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nexport function parsePluginWatchCommand(argv: string[]): PluginWatchCommand | undefined {\n\tif (argv[0] !== \"watch\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, stop: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [dir, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn {\n\t\ttype: \"watch\",\n\t\t...(dir ? { dir } : {}),\n\t\tstop: parsed.values.stop === true,\n\t\tjson: parsed.values.json === true,\n\t};\n}\n\nexport function parsePluginUninstallCommand(argv: string[]): PluginUninstallCommand | undefined {\n\tif (argv[0] !== \"uninstall\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [pluginId, unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\t// 省略 id 时按 cwd 推断,语义与 add . / watch 一致:站在哪个插件里就作用于哪个。\n\treturn { type: \"uninstall\", ...(pluginId ? { pluginId } : {}), json: parsed.values.json === true };\n}\n\nexport function parsePluginSyncCommand(argv: string[]): PluginSyncCommand | undefined {\n\tif (argv[0] !== \"sync\") return undefined;\n\tif (argv[1] === \"-h\" || argv[1] === \"--help\") return { type: \"help\" };\n\tlet parsed: ReturnType<typeof parseArgs>;\n\ttry {\n\t\tparsed = parseArgs({\n\t\t\targs: argv.slice(1),\n\t\t\tallowPositionals: true,\n\t\t\tstrict: true,\n\t\t\toptions: { json: { type: \"boolean\" }, check: { type: \"boolean\" } },\n\t\t});\n\t} catch (error) {\n\t\treturn { type: \"error\", message: formatParseError(error) };\n\t}\n\tconst [unexpected] = parsed.positionals;\n\tif (unexpected) return { type: \"error\", message: `Unexpected argument: ${unexpected}` };\n\treturn { type: \"sync\", check: parsed.values.check === true, json: parsed.values.json === true };\n}\n\nasync function defaultRunAction(actionId: string, input: unknown): Promise<unknown> {\n\tconst client = createActionRpcClient(await readActionRpcEndpoint());\n\treturn client.run(actionId, input);\n}\n\nconst defaultDependencies: PluginCommandDependencies = {\n\tresolveNpmArchive: resolveNpmPluginArchive,\n\tcwd: () => process.cwd(),\n\trunAction: defaultRunAction,\n\twriteStdout: (value) => process.stdout.write(value),\n\twriteStderr: (value) => process.stderr.write(value),\n\treadLatestSdkVersion: () => readLatestNpmVersion(\"@vetta-org/plugin-sdk\"),\n};\n\nfunction isHttpUrl(source: string): boolean {\n\ttry {\n\t\tconst url = new URL(source);\n\t\treturn url.protocol === \"http:\" || url.protocol === \"https:\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction isLocalPackage(source: string): boolean {\n\tconst lower = source.toLowerCase();\n\tif (lower.endsWith(\".vettapkg\") || lower.endsWith(\".zip\")) return true;\n\tconst path = resolve(source);\n\t// 目录不是压缩包:它是一个插件工程,走 resolveProjectArchive 先找它打出来的产物。\n\treturn existsSync(path) && !statSync(path).isDirectory();\n}\n\nfunction isDirectorySource(source: string): boolean {\n\tconst path = resolve(source);\n\treturn existsSync(path) && statSync(path).isDirectory();\n}\n\n/**\n * 把「装当前这个工程」翻译成一个具体的归档路径。\n *\n * 这条路径是给 `install:vetta` 这类脚本用的:作者(或 Agent)在插件目录里跑一条命令就\n * 装进 Vetta,不必记住产物叫什么名字。找不到产物时给出该跑的那条命令,而不是报一个\n * 「文件不存在」让人自己猜。\n */\nfunction resolveProjectArchive(source: string): { archivePath: string; project: PluginProject } {\n\tconst from = resolve(source);\n\tconst project = findPluginProject(from);\n\tif (!project) {\n\t\tconst hub = findPluginHub(from);\n\t\tif (hub) {\n\t\t\tthrow new Error(\n\t\t\t\t`${from} indexes plugins but is not one itself. Run this from a plugin directory, or pass its path: vetta-plugin-cli add ./path/to/plugin`,\n\t\t\t);\n\t\t}\n\t\tthrow new Error(`No plugin.json found in ${from} or any parent directory.`);\n\t}\n\tconst archivePath = join(project.root, \"release\", `${project.pluginId}-${project.version}.vettapkg`);\n\tif (!existsSync(archivePath)) {\n\t\tthrow new Error(\n\t\t\t`Packaged archive not found: ${archivePath}\\nBuild it first: npm run build && npx vetta-plugin pack`,\n\t\t);\n\t}\n\treturn { archivePath, project };\n}\n\n/** 装完立刻检查索引是否还停在旧版本;不在市场仓库里时什么也不说。 */\nfunction indexDriftHint(project: PluginProject): string | undefined {\n\tconst hub = findPluginHub(project.root);\n\tif (!hub) return undefined;\n\treturn describeIndexDrift({\n\t\thubRoot: hub.root,\n\t\tmanifestPath: hub.manifestPath,\n\t\tslug: project.pluginId,\n\t\tversion: project.version,\n\t});\n}\n\nfunction npmInstallInput(resolved: ResolvedNpmPluginArchive): Record<string, unknown> {\n\treturn {\n\t\toperation: \"install-from-path\",\n\t\tinitiator: \"plugin-cli\",\n\t\tpath: resolved.archivePath,\n\t\tenable: true,\n\t\tsource: \"npm\",\n\t\texpectedSha256: resolved.expectedSha256,\n\t\texpectedId: resolved.packageManifest.vetta.pluginId,\n\t\texpectedVersion: resolved.packageManifest.version,\n\t\tnpm: {\n\t\t\tpackageName: resolved.packageManifest.name,\n\t\t\trequestedSpec: resolved.requestedSpec,\n\t\t\tresolvedVersion: resolved.packageManifest.version,\n\t\t\t...(resolved.integrity ? { integrity: resolved.integrity } : {}),\n\t\t},\n\t};\n}\n\nfunction resultSummary(result: unknown): string {\n\tif (typeof result !== \"object\" || result === null || Array.isArray(result)) return \"Plugin installed.\\n\";\n\tconst response = result as Record<string, unknown>;\n\tconst plugin =\n\t\ttypeof response.plugin === \"object\" && response.plugin !== null && !Array.isArray(response.plugin)\n\t\t\t? (response.plugin as Record<string, unknown>)\n\t\t\t: undefined;\n\tif (!plugin) return \"Plugin installed.\\n\";\n\tconst id = typeof plugin.id === \"string\" ? plugin.id : \"plugin\";\n\tconst version = typeof plugin.version === \"string\" ? `@${plugin.version}` : \"\";\n\tconst pending = typeof plugin.pendingVersion === \"string\"\n\t\t? ` Update ${plugin.pendingVersion} is pending reload. Run \\`vetta-plugin-cli reload ${id}\\` to apply it.`\n\t\t: \"\";\n\treturn `Installed ${id}${version}.${pending}\\n`;\n}\n\nfunction reloadResultSummary(result: unknown, requestedPluginId: string): string {\n\tif (typeof result !== \"object\" || result === null || Array.isArray(result)) {\n\t\treturn `Reloaded ${requestedPluginId}.\\n`;\n\t}\n\tconst response = result as Record<string, unknown>;\n\tconst plugin =\n\t\ttypeof response.plugin === \"object\" && response.plugin !== null && !Array.isArray(response.plugin)\n\t\t\t? (response.plugin as Record<string, unknown>)\n\t\t\t: undefined;\n\tconst id = typeof plugin?.id === \"string\" ? plugin.id : requestedPluginId;\n\tconst version = typeof plugin?.activeVersion === \"string\" ? `@${plugin.activeVersion}` : \"\";\n\treturn `Reloaded ${id}${version}.\\n`;\n}\n\nfunction isConnectionError(error: unknown): boolean {\n\tif (!(error instanceof Error)) return false;\n\tconst code = (error as NodeJS.ErrnoException).code;\n\treturn (\n\t\tcode === \"ENOENT\" ||\n\t\tcode === \"ECONNREFUSED\" ||\n\t\tcode === \"ECONNRESET\" ||\n\t\terror.message.includes(\"ECONNREFUSED\") ||\n\t\terror.message.includes(\"fetch failed\")\n\t);\n}\n\nexport async function runPluginAddCommand(\n\tcommand: PluginAddCommand,\n\tdependencies: PluginAddCommandDependencies = defaultDependencies,\n): Promise<number> {\n\treturn runPluginCommand(command, dependencies);\n}\n\nexport async function runPluginCommand(\n\tcommand: PluginCommand,\n\tdependencies: PluginCommandDependencies = defaultDependencies,\n): Promise<number> {\n\tif (command.type === \"help\") {\n\t\tdependencies.writeStdout(HELP_TEXT);\n\t\treturn 0;\n\t}\n\tif (command.type === \"error\") {\n\t\tdependencies.writeStderr(`${command.message}\\n`);\n\t\treturn 2;\n\t}\n\n\tif (command.type === \"docs\") {\n\t\treturn await runDocsCommand(command, dependencies);\n\t}\n\tif (command.type === \"init\") {\n\t\treturn runInitCommand(command, dependencies);\n\t}\n\tif (command.type === \"refresh-guide\") {\n\t\treturn runRefreshGuideCommand(command, dependencies);\n\t}\n\tif (command.type === \"init-hub\") {\n\t\treturn runInitHubCommand(command, dependencies);\n\t}\n\tif (command.type === \"sync\") {\n\t\treturn runSyncCommand(command, dependencies);\n\t}\n\n\tif (command.type === \"watch\") {\n\t\treturn runWatchCommand(command, dependencies);\n\t}\n\tif (command.type === \"uninstall\") {\n\t\treturn runUninstallCommand(command, dependencies);\n\t}\n\n\tlet resolvedNpm: ResolvedNpmPluginArchive | undefined;\n\tlet driftHint: string | undefined;\n\ttry {\n\t\tlet result: unknown;\n\t\tif (command.type === \"reload\") {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"reload\",\n\t\t\t\tid: command.pluginId,\n\t\t\t});\n\t\t} else if (isHttpUrl(command.source)) {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-url\",\n\t\t\t\tinitiator: \"plugin-cli\",\n\t\t\t\turl: command.source,\n\t\t\t});\n\t\t} else if (isDirectorySource(command.source)) {\n\t\t\tconst { archivePath, project } = resolveProjectArchive(command.source);\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-path\",\n\t\t\t\tinitiator: \"plugin-cli\",\n\t\t\t\tpath: archivePath,\n\t\t\t\tenable: true,\n\t\t\t});\n\t\t\tdriftHint = indexDriftHint(project);\n\t\t} else if (isLocalPackage(command.source)) {\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\toperation: \"install-from-path\",\n\t\t\t\tinitiator: \"plugin-cli\",\n\t\t\t\tpath: resolve(command.source),\n\t\t\t\tenable: true,\n\t\t\t});\n\t\t} else {\n\t\t\tresolvedNpm = await dependencies.resolveNpmArchive(command.source);\n\t\t\tresult = await dependencies.runAction(\"plugins.manage\", npmInstallInput(resolvedNpm));\n\t\t}\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, result, ...(driftHint ? { warning: driftHint } : {}) })}\\n`\n\t\t\t\t: command.type === \"reload\"\n\t\t\t\t\t? reloadResultSummary(result, command.pluginId)\n\t\t\t\t\t: `${resultSummary(result)}${driftHint ? `${driftHint}\\n` : \"\"}`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : command.type === \"reload\" ? \"PLUGIN_RELOAD_FAILED\" : \"PLUGIN_ADD_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t} finally {\n\t\tawait resolvedNpm?.cleanup();\n\t}\n}\n\n/**\n * 打印随 SDK 发布的手册目录。\n *\n * 存在的理由是「不要让任何人硬编码 node_modules 路径」:工作区会把依赖提升到仓库根,\n * 一仓多插件的 hub 里每个插件也可能各装一份。Agent 只需记住这一条命令,拿回来的永远是\n * 当前工程实际编译所针对的那个 SDK 版本的手册。\n */\nasync function runDocsCommand(\n\tcommand: { json: boolean; checkLatest: boolean },\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst manualDir = resolveManualDir(cwd);\n\tif (!manualDir) {\n\t\t// 能力市场仓库的根目录通常没装 SDK,手册在各能力目录里。直接说「装 SDK」会把人引到\n\t\t// 仓库根去装一份用不上的依赖。\n\t\tconst inHubRoot = findPluginHub(cwd) !== undefined && findPluginProject(cwd) === undefined;\n\t\tconst message = inHubRoot\n\t\t\t? \"Plugin manual not found at the hub root. cd into an ability directory (abilities/plugins/<slug>), then run npm install.\\n\"\n\t\t\t: \"Plugin manual not found. Install the SDK first: npm i -D @vetta-org/plugin-sdk\\n\";\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: \"MANUAL_NOT_FOUND\", message: message.trim() } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(message);\n\t\t}\n\t\treturn 6;\n\t}\n\tconst project = findPluginProject(cwd);\n\tconst hub = findPluginHub(cwd);\n\tconst sdkVersion = readManualSdkVersion(manualDir);\n\tconst latestVersion = command.checkLatest ? await dependencies.readLatestSdkVersion?.() : undefined;\n\tconst outdated = sdkVersion !== undefined && latestVersion !== undefined && compareSemver(sdkVersion, latestVersion) < 0;\n\tconst guide = inspectAgentsGuide(project?.root ?? hub?.root ?? cwd);\n\n\tif (command.json) {\n\t\tdependencies.writeStdout(\n\t\t\t`${JSON.stringify({\n\t\t\t\tok: true,\n\t\t\t\tmanualDir,\n\t\t\t\tentry: join(manualDir, \"README.md\"),\n\t\t\t\tsdkVersion,\n\t\t\t\trefreshCommand: SDK_REFRESH_COMMAND,\n\t\t\t\tguide,\n\t\t\t\t...(command.checkLatest ? { latestVersion, outdated } : {}),\n\t\t\t\tproject: project ? { root: project.root, pluginId: project.pluginId, version: project.version } : undefined,\n\t\t\t\thub: hub\n\t\t\t\t\t? {\n\t\t\t\t\t\t\troot: hub.root,\n\t\t\t\t\t\t\tmanifestPath: hub.manifestPath,\n\t\t\t\t\t\t\tsyncHint: \"After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t})}\\n`,\n\t\t);\n\t\treturn 0;\n\t}\n\tconst lines = [\n\t\t`Plugin manual (@vetta-org/plugin-sdk${sdkVersion ? `@${sdkVersion}` : \"\"}):`,\n\t\t` ${manualDir}`,\n\t\t`Start here: ${join(manualDir, \"README.md\")}`,\n\t];\n\tif (project) lines.push(`Current plugin: ${project.pluginId} (${project.root})`);\n\tif (outdated) {\n\t\tlines.push(`Manual is behind: ${sdkVersion} → ${latestVersion}. Refresh it with: ${SDK_REFRESH_COMMAND}`);\n\t} else if (command.checkLatest && latestVersion === undefined) {\n\t\tlines.push(`Could not reach the registry; cannot tell whether ${sdkVersion ?? \"this manual\"} is current.`);\n\t} else {\n\t\t// 手册是随 SDK 装进 node_modules 的快照,工程不升级它就永远停在初始化那天的版本。\n\t\t// 这条命令必须每次都打印:读到它的 Agent 手上的 AGENTS.md 往往也是同一天的快照。\n\t\tlines.push(`Manual follows the installed SDK. To refresh it: ${SDK_REFRESH_COMMAND}`);\n\t}\n\tif (guide.stale) {\n\t\t// 说明书同样是快照,而且用户没有理由回头看它。这里是唯一会被读到的位置。\n\t\tlines.push(\n\t\t\t`This brief is stale (AGENTS.md revision ${guide.revision} < ${AGENTS_GUIDE_REVISION}). Refresh it with: ${GUIDE_REFRESH_COMMAND}`,\n\t\t);\n\t} else if (guide.unstamped) {\n\t\t// 没有版本戳的文件与手写内容无从区分——能力市场仓库的根 AGENTS.md 往往是一整本手写的\n\t\t// 市场规范。绝不能把它引导成一条覆盖命令。\n\t\tlines.push(\n\t\t\t`AGENTS.md has no revision marker, so it looks hand-written. Review the current template with \\`${GUIDE_REFRESH_COMMAND} --dry-run\\` and merge by hand; do not overwrite it blindly.`,\n\t\t);\n\t}\n\tif (hub) {\n\t\tlines.push(`Marketplace index: ${hub.manifestPath}`);\n\t\t// Agent 几乎一定会先跑 docs,所以这是告诉它「索引要对账」的最佳时机。\n\t\tlines.push(\"After changing version/permissions, run `vetta-plugin-cli sync` at the repository root.\");\n\t}\n\tdependencies.writeStdout(`${lines.join(\"\\n\")}\\n`);\n\treturn 0;\n}\n\n/**\n * 刷新手册的命令。\n *\n * 手册不从网络现取,而是随 SDK 进 `node_modules`——Agent 读到的合同因此与工程实际编译的\n * 版本一致。代价是它不会自己变新,所以「怎么变新」必须由 CLI 每次说一遍:`npx` 默认取最新的\n * CLI,它的输出是这条链路上唯一不会过期的位置。\n */\nconst SDK_REFRESH_COMMAND = \"npm i -D @vetta-org/plugin-sdk@latest && npx vetta-plugin-cli docs\";\n\n/** 刷新说明书的命令。与手册各刷各的:一个随 SDK 走,一个随 CLI 走。 */\nconst GUIDE_REFRESH_COMMAND = \"npx @vetta-org/plugin-cli init --refresh-guide\";\n\nexport interface AgentsGuideStatus {\n\t/** 本工程有没有 AGENTS.md。 */\n\treadonly present: boolean;\n\t/** 读到的版本戳;没有戳(模板早于版本戳,或是手写的)时缺省。 */\n\treadonly revision?: number;\n\t/**\n\t * **带戳**且落后于当前 CLI 的模板——只有这一档能安全地一键重写。\n\t *\n\t * 没有 AGENTS.md 时为 false(那是「没有」,不是「旧」);没有戳时也为 false,见 {@link unstamped}。\n\t */\n\treadonly stale: boolean;\n\t/** 有文件但没有版本戳:可能是手写的,也可能是版本戳之前的模板,无从区分。 */\n\treadonly unstamped: boolean;\n}\n\n/**\n * 判断工程里的 AGENTS.md 是不是旧模板。\n *\n * 说明书凝固在 `init` 那天,而用户没有理由回头看它——所以「它旧了」这件事只能由每次都会被\n * 跑到的 `docs` 说出来。\n *\n * **没有版本戳的不算「旧」,只算「来路不明」。** 早先把它判成 stale 并引导去跑刷新命令,等于\n * 教用户覆盖自己手写的文件——能力市场仓库的根 `AGENTS.md` 常常是一整本手写的市场规范。\n */\nfunction inspectAgentsGuide(root: string): AgentsGuideStatus {\n\tconst path = join(root, \"AGENTS.md\");\n\tif (!existsSync(path)) return { present: false, stale: false, unstamped: false };\n\tlet revision: number | undefined;\n\ttry {\n\t\trevision = readAgentsGuideRevision(readFileSync(path, \"utf8\"));\n\t} catch {\n\t\treturn { present: true, stale: false, unstamped: false };\n\t}\n\treturn {\n\t\tpresent: true,\n\t\t...(revision === undefined ? {} : { revision }),\n\t\tstale: revision !== undefined && revision < AGENTS_GUIDE_REVISION,\n\t\tunstamped: revision === undefined,\n\t};\n}\n\n/** 够用的 semver 比较:只看 major.minor.patch,预发布后缀一律当作小于正式版。 */\nfunction compareSemver(left: string, right: string): number {\n\tconst parse = (value: string): readonly [number, number, number, boolean] => {\n\t\tconst match = /^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$/.exec(value.trim());\n\t\tif (!match) return [0, 0, 0, false];\n\t\treturn [Number(match[1]), Number(match[2]), Number(match[3]), match[4] !== undefined];\n\t};\n\tconst a = parse(left);\n\tconst b = parse(right);\n\tfor (let index = 0; index < 3; index += 1) {\n\t\tif (a[index] !== b[index]) return a[index]! < b[index]! ? -1 : 1;\n\t}\n\tif (a[3] === b[3]) return 0;\n\treturn a[3] ? -1 : 1;\n}\n\n/**\n * 在已有工程里把 AGENTS.md 重写成当前 CLI 的版本。\n *\n * `init` 拒绝覆盖已有工程,所以老目录里那份说明书从落地起就停在原地。它是纯派生产物,重写\n * 它不会碰用户写过的任何东西——这也是唯一一个能这么做的脚手架文件。\n */\nfunction runRefreshGuideCommand(\n\tcommand: Extract<PluginCommand, { type: \"refresh-guide\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = refreshAgentsGuide(resolve(cwd, command.targetDir ?? \".\"), {\n\t\t\tforce: command.force,\n\t\t\tdryRun: command.dryRun,\n\t\t});\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: true, ...result })}\\n`);\n\t\t\treturn 0;\n\t\t}\n\t\tdependencies.writeStdout(\n\t\t\tresult.written\n\t\t\t\t? `Rewrote ${result.file}\\nNext: npx vetta-plugin-cli docs --check-latest\\n`\n\t\t\t\t// dry-run 把正文直接吐到 stdout,人工合并时可以重定向成文件再 diff。\n\t\t\t\t: `${result.content}`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: \"GUIDE_REFRESH_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 7;\n\t}\n}\n\n/** 在陌生目录里生成一个可直接开工的插件工程,并留下让任意 Agent 自举的 AGENTS.md。 */\nfunction runInitCommand(\n\tcommand: Extract<PluginCommand, { type: \"init\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = initPluginProject({\n\t\t\ttargetDir: resolve(cwd, command.targetDir ?? command.pluginId),\n\t\t\tpluginId: command.pluginId,\n\t\t\tdisplayName: command.displayName ?? command.pluginId,\n\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: [\n\t\t\t\t\t\t`Created ${result.pluginId} at ${result.root}`,\n\t\t\t\t\t\t\"Next: npm install && npm run install:vetta\",\n\t\t\t\t\t\t\"The agent brief is in AGENTS.md; after npm install, run `npx vetta-plugin-cli docs` for the manual.\",\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t\t.concat(\"\\n\"),\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"PLUGIN_INIT_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\n/**\n * 让宿主改从工程目录加载本插件,之后改源码即时生效,不必每次 build → pack → install。\n *\n * 目标插件按 cwd 向上找,理由同 `add .`:一仓多插件时「我正站在哪个插件里」是唯一不会\n * 弄错的意图,而 id 靠人重复输入迟早会错配到另一个插件上。\n */\nasync function runWatchCommand(\n\tcommand: Extract<PluginCommand, { type: \"watch\" }>,\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst from = resolve(cwd, command.dir ?? \".\");\n\ttry {\n\t\tconst project = findPluginProject(from);\n\t\tif (!project) {\n\t\t\tconst hub = findPluginHub(from);\n\t\t\tthrow new Error(\n\t\t\t\thub\n\t\t\t\t\t? `${from} indexes plugins but is not one itself. Run this from a plugin directory, or pass its path.`\n\t\t\t\t\t: `No plugin.json found in ${from} or any parent directory.`,\n\t\t\t);\n\t\t}\n\t\tconst result = command.stop\n\t\t\t? await dependencies.runAction(\"plugins.manage\", { operation: \"dev-watch-stop\", id: project.pluginId })\n\t\t\t: await dependencies.runAction(\"plugins.manage\", {\n\t\t\t\t\toperation: \"dev-watch\",\n\t\t\t\t\tid: project.pluginId,\n\t\t\t\t\tprojectDir: project.root,\n\t\t\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, result })}\\n`\n\t\t\t\t: command.stop\n\t\t\t\t\t? `Stopped hot reload for ${project.pluginId}.\\n`\n\t\t\t\t\t: `Hot reload on for ${project.pluginId}. Vetta now loads it from ${project.root}.\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : \"PLUGIN_WATCH_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t}\n}\n\n/**\n * 卸载一个插件。省略 id 时按 cwd 推断,语义与 `add .` / `watch` 一致。\n *\n * 刻意不在这里做二次确认:宿主自己会为写操作弹审批,CLI 再问一遍只是噪音。系统插件由\n * 宿主拒绝,这里不重复判断——那份名单不该有第二个真相源。\n */\nasync function runUninstallCommand(\n\tcommand: Extract<PluginCommand, { type: \"uninstall\" }>,\n\tdependencies: PluginCommandDependencies,\n): Promise<number> {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tlet pluginId = command.pluginId;\n\t\tif (!pluginId) {\n\t\t\tconst project = findPluginProject(cwd);\n\t\t\tif (!project) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No plugin.json found in ${cwd} or any parent directory. Pass the id: vetta-plugin-cli uninstall <plugin-id>`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tpluginId = project.pluginId;\n\t\t}\n\t\tconst result = await dependencies.runAction(\"plugins.manage\", { operation: \"uninstall\", id: pluginId });\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json ? `${JSON.stringify({ ok: true, result })}\\n` : `Uninstalled ${pluginId}.\\n`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(\n\t\t\t\t`${JSON.stringify({ ok: false, error: { code: error instanceof ActionRpcError ? error.code : \"PLUGIN_UNINSTALL_FAILED\", message } })}\\n`,\n\t\t\t);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\tif (error instanceof ActionRpcError) return 4;\n\t\treturn isConnectionError(error) ? 3 : 5;\n\t}\n}\n\n/**\n * 对账能力市场索引。定位靠向上找 `.vetta/marketplace.json`,因此在仓库任何位置都能跑。\n *\n * `--check` 只报不写并以非零退出,给 CI 用:索引漂移的三种后果里,两种不在作者机器上复现,\n * 一种压根不报错,光靠人自觉看不住。\n */\nfunction runSyncCommand(\n\tcommand: Extract<PluginCommand, { type: \"sync\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\tconst hub = findPluginHub(cwd);\n\tif (!hub) {\n\t\tconst message = `No .vetta/marketplace.json found in ${cwd} or any parent directory. sync is for marketplace repositories.\\n`;\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"HUB_NOT_FOUND\", message: message.trim() } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(message);\n\t\t}\n\t\treturn 6;\n\t}\n\ttry {\n\t\tconst result = syncMarketplaceIndex({ hubRoot: hub.root, manifestPath: hub.manifestPath, apply: !command.check });\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: result.problems.length === 0, ...result })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStdout(formatSyncReport(result, command.check));\n\t\t}\n\t\tif (result.problems.length > 0) return 7;\n\t\t// --check 的职责就是「有漂移就红」,否则 CI 拦不住任何东西。\n\t\treturn command.check && result.changes.length > 0 ? 7 : 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"SYNC_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\nfunction formatSyncReport(result: ReturnType<typeof syncMarketplaceIndex>, check: boolean): string {\n\tconst lines: string[] = [];\n\tfor (const change of result.changes) {\n\t\tlines.push(` ${change.slug}: ${change.field} ${JSON.stringify(change.from)} -> ${JSON.stringify(change.to)}`);\n\t}\n\tif (lines.length > 0) {\n\t\tlines.unshift(check ? \"Index is out of date:\" : \"Updated the index:\");\n\t}\n\tif (result.problems.length > 0) {\n\t\tlines.push(\"Problems:\");\n\t\tfor (const problem of result.problems) lines.push(` ${problem.slug}: ${problem.message}`);\n\t}\n\tif (result.unlisted.length > 0) {\n\t\tlines.push(\"Ability directories not listed in the index (add them by hand when ready to publish):\");\n\t\tfor (const dir of result.unlisted) lines.push(` ${dir}`);\n\t}\n\tif (lines.length === 0) return \"Index is in sync.\\n\";\n\tif (check && result.changes.length > 0) lines.push(\"Run `vetta-plugin-cli sync` to apply.\");\n\treturn `${lines.join(\"\\n\")}\\n`;\n}\n\n/** 生成一个合规的能力市场仓库骨架,连同仓库级 AGENTS.md 与对账用的 CI。 */\nfunction runInitHubCommand(\n\tcommand: Extract<PluginCommand, { type: \"init-hub\" }>,\n\tdependencies: PluginCommandDependencies,\n): number {\n\tconst cwd = dependencies.cwd?.() ?? process.cwd();\n\ttry {\n\t\tconst result = initHubRepository({\n\t\t\ttargetDir: resolve(cwd, command.targetDir ?? command.name),\n\t\t\tname: command.name,\n\t\t\trepository: command.repository,\n\t\t\tminAppVersion: command.minAppVersion,\n\t\t});\n\t\tdependencies.writeStdout(\n\t\t\tcommand.json\n\t\t\t\t? `${JSON.stringify({ ok: true, ...result })}\\n`\n\t\t\t\t: [\n\t\t\t\t\t\t`Created marketplace ${result.name} at ${result.root}`,\n\t\t\t\t\t\t\"Add an ability: npx @vetta-org/plugin-cli init --id <slug> --name \\\"<Display>\\\" abilities/plugins/<slug>\",\n\t\t\t\t\t\t\"Then list it in .vetta/marketplace.json and run: npx @vetta-org/plugin-cli sync\",\n\t\t\t\t\t\t\"The working agreement for agents is in AGENTS.md.\",\n\t\t\t\t\t].join(\"\\n\") + \"\\n\",\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (command.json) {\n\t\t\tdependencies.writeStdout(`${JSON.stringify({ ok: false, error: { code: \"HUB_INIT_FAILED\", message } })}\\n`);\n\t\t} else {\n\t\t\tdependencies.writeStderr(`${message}\\n`);\n\t\t}\n\t\treturn 5;\n\t}\n}\n\nexport async function runPluginCli(argv: string[]): Promise<number> {\n\tif (argv.length === 0 || argv[0] === \"-h\" || argv[0] === \"--help\") {\n\t\treturn runPluginAddCommand({ type: \"help\" });\n\t}\n\tconst command = parsePluginAddCommand(argv) ?? parsePluginReloadCommand(argv) ?? parsePluginDocsCommand(argv) ?? parsePluginInitCommand(argv) ?? parsePluginWatchCommand(argv) ?? parsePluginUninstallCommand(argv) ?? parsePluginSyncCommand(argv);\n\tif (!command) {\n\t\tprocess.stderr.write(`Unknown command: ${argv[0]}\\n`);\n\t\treturn 2;\n\t}\n\treturn runPluginCommand(command);\n}\n"]}
package/dist/index.js CHANGED
@@ -11904,6 +11904,18 @@ var agentExperimentalSettingsUpdateType = Type.Object({
11904
11904
  promptPrediction: Type.Optional(Type.Boolean()),
11905
11905
  agentSkills: Type.Optional(Type.Boolean())
11906
11906
  }, { additionalProperties: false, minProperties: 1 });
11907
+ var imageGenerationSettingsType = Type.Object({
11908
+ textToImageProviderId: Type.Optional(Type.String({ minLength: 1, maxLength: 129 })),
11909
+ textToImageModelId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
11910
+ imageToImageProviderId: Type.Optional(Type.String({ minLength: 1, maxLength: 129 })),
11911
+ imageToImageModelId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 }))
11912
+ }, { additionalProperties: false });
11913
+ var imageGenerationSettingsUpdateType = Type.Object({
11914
+ textToImageProviderId: Type.Optional(Type.Union([Type.String({ minLength: 1, maxLength: 129 }), Type.Null()])),
11915
+ textToImageModelId: Type.Optional(Type.Union([Type.String({ minLength: 1, maxLength: 256 }), Type.Null()])),
11916
+ imageToImageProviderId: Type.Optional(Type.Union([Type.String({ minLength: 1, maxLength: 129 }), Type.Null()])),
11917
+ imageToImageModelId: Type.Optional(Type.Union([Type.String({ minLength: 1, maxLength: 256 }), Type.Null()]))
11918
+ }, { additionalProperties: false, minProperties: 1 });
11907
11919
  var agentSettingsEmptyInputSchema = defineCapabilityInputSchema(agentSettingsEmptyInputType);
11908
11920
  var agentExperimentalSettingsSchema = defineCapabilityOutputSchema(agentExperimentalSettingsType, { clean: true });
11909
11921
  var agentExperimentalSettingsUpdateSchema = defineCapabilityInputSchema(agentExperimentalSettingsUpdateType);
@@ -11923,6 +11935,22 @@ var DOMAIN_AGENT_SETTINGS_CAPABILITIES = {
11923
11935
  version: 1,
11924
11936
  input: agentExperimentalSettingsUpdateSchema,
11925
11937
  output: agentExperimentalSettingsSchema
11938
+ }),
11939
+ GET_IMAGE_GENERATION: defineCapability({
11940
+ id: "cap.domain.vetta.agent-settings.image-generation.get",
11941
+ kind: "query",
11942
+ layer: CAPABILITY_LAYERS.DOMAIN,
11943
+ version: 1,
11944
+ input: agentSettingsEmptyInputSchema,
11945
+ output: defineCapabilityOutputSchema(imageGenerationSettingsType, { clean: true })
11946
+ }),
11947
+ SET_IMAGE_GENERATION: defineCapability({
11948
+ id: "cap.domain.vetta.agent-settings.image-generation.set",
11949
+ kind: "command",
11950
+ layer: CAPABILITY_LAYERS.DOMAIN,
11951
+ version: 1,
11952
+ input: defineCapabilityInputSchema(imageGenerationSettingsUpdateType),
11953
+ output: defineCapabilityOutputSchema(imageGenerationSettingsType, { clean: true })
11926
11954
  })
11927
11955
  };
11928
11956
  var DOMAIN_AGENT_SETTINGS_CAPABILITY_CATALOG = createCapabilityCatalog(Object.values(DOMAIN_AGENT_SETTINGS_CAPABILITIES));
@@ -13028,7 +13056,7 @@ var FOUNDATION_JOB_CAPABILITIES = {
13028
13056
  var FOUNDATION_JOB_CAPABILITY_CATALOG = createCapabilityCatalog(Object.values(FOUNDATION_JOB_CAPABILITIES));
13029
13057
 
13030
13058
  // ../../capability-sdk/dist/domain/media.js
13031
- var MEDIA_PROTOCOL_VERSION = 4;
13059
+ var MEDIA_PROTOCOL_VERSION = 5;
13032
13060
  var MEDIA_OPERATIONS = {
13033
13061
  GENERATE: "generate",
13034
13062
  COMPOSE: "compose",
@@ -13127,6 +13155,16 @@ var mediaGenerationModeCapabilityType = Type.Object({
13127
13155
  aspectRatioPolicy: Type.Optional(Type.Union([Type.Literal("configurable"), Type.Literal("input-derived")])),
13128
13156
  audioGeneration: Type.Optional(Type.Union([Type.Literal("none"), Type.Literal("always"), Type.Literal("optional")]))
13129
13157
  }, { additionalProperties: false });
13158
+ var mediaGenerationModelDescriptorType = Type.Object({
13159
+ id: requiredStringType2,
13160
+ displayName: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
13161
+ sourceId: Type.Optional(requiredStringType2),
13162
+ sourceDisplayName: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
13163
+ modes: Type.Array(mediaGenerationModeType, { minItems: 1 }),
13164
+ aspectRatios: Type.Optional(Type.Array(requiredStringType2)),
13165
+ resolutions: Type.Optional(Type.Array(requiredStringType2)),
13166
+ defaultResolution: Type.Optional(requiredStringType2)
13167
+ }, { additionalProperties: false });
13130
13168
  var mediaGenerateCapabilityType = Type.Object({
13131
13169
  operation: Type.Literal(MEDIA_OPERATIONS.GENERATE),
13132
13170
  kind: mediaGenerationKindType,
@@ -13134,6 +13172,8 @@ var mediaGenerateCapabilityType = Type.Object({
13134
13172
  aspectRatios: Type.Optional(Type.Array(requiredStringType2)),
13135
13173
  resolutions: Type.Optional(Type.Array(requiredStringType2)),
13136
13174
  defaultResolution: Type.Optional(requiredStringType2),
13175
+ models: Type.Optional(Type.Array(mediaGenerationModelDescriptorType, { minItems: 1 })),
13176
+ defaultModelId: Type.Optional(requiredStringType2),
13137
13177
  durationsSeconds: Type.Optional(Type.Array(Type.Number({ exclusiveMinimum: 0 }))),
13138
13178
  modeCapabilities: Type.Optional(Type.Array(mediaGenerationModeCapabilityType))
13139
13179
  }, { additionalProperties: false });
@@ -13317,6 +13357,8 @@ var modelProviderUpsertModelType = Type.Object({
13317
13357
  name: Type.Optional(Type.String()),
13318
13358
  api: Type.Optional(Type.String()),
13319
13359
  reasoning: Type.Optional(Type.Boolean()),
13360
+ reasoningLevels: modelDefinitionDetailType.properties.reasoningLevels,
13361
+ defaultReasoningLevel: modelDefinitionDetailType.properties.defaultReasoningLevel,
13320
13362
  contextWindow: Type.Optional(Type.Number()),
13321
13363
  maxTokens: Type.Optional(Type.Number())
13322
13364
  }, { additionalProperties: false });
@@ -19432,13 +19474,31 @@ function syncMarketplaceIndex(input) {
19432
19474
  for (const member of Array.isArray(bundleConfig?.members) ? bundleConfig.members : []) {
19433
19475
  if (typeof member !== "object" || member === null || Array.isArray(member))
19434
19476
  continue;
19435
- const memberSource = member.source;
19477
+ const memberEntry = member;
19478
+ const memberSource = memberEntry.source;
19436
19479
  const memberPath = typeof memberSource === "object" && memberSource !== null && !Array.isArray(memberSource) ? memberSource.path : undefined;
19437
19480
  if (typeof memberPath !== "string")
19438
19481
  continue;
19439
19482
  const dir = resolveAbilityDir(input.hubRoot, memberPath);
19440
- if (dir)
19483
+ if (dir) {
19441
19484
  listedDirs.add(dir);
19485
+ if (manifest.schemaVersion === 3 && memberEntry.type === "plugin") {
19486
+ const descriptor2 = readJsonFile2(join4(dir, "ability.json"));
19487
+ const memberChanges = [];
19488
+ reconcilePlugin({
19489
+ entry: { ...memberEntry, version: descriptor2?.version },
19490
+ slug: typeof memberEntry.slug === "string" ? memberEntry.slug : "(unnamed)",
19491
+ abilityDir: dir,
19492
+ schemaVersion: manifest.schemaVersion,
19493
+ minAppVersion: manifest.minAppVersion,
19494
+ changes: memberChanges,
19495
+ problems
19496
+ });
19497
+ for (const change of memberChanges) {
19498
+ problems.push({ slug: change.slug, message: `ability.json version does not match latest release: ${change.to}` });
19499
+ }
19500
+ }
19501
+ }
19442
19502
  }
19443
19503
  continue;
19444
19504
  }
@@ -19455,7 +19515,7 @@ function syncMarketplaceIndex(input) {
19455
19515
  }
19456
19516
  listedDirs.add(abilityDir);
19457
19517
  if (type === "plugin")
19458
- reconcilePlugin({ entry, slug, abilityDir, changes, problems });
19518
+ reconcilePlugin({ entry, slug, abilityDir, schemaVersion: manifest.schemaVersion, minAppVersion: manifest.minAppVersion, changes, problems });
19459
19519
  else if (type === "mcp")
19460
19520
  reconcileIdentityFile({ entry, slug, abilityDir, fileName: "mcp.json", changes, problems });
19461
19521
  }
@@ -19482,7 +19542,77 @@ function syncMarketplaceIndex(input) {
19482
19542
  return { manifestPath: input.manifestPath, changes, problems, unlisted, written };
19483
19543
  }
19484
19544
  function reconcilePlugin(context) {
19485
- const { entry, slug, abilityDir, changes, problems } = context;
19545
+ const { entry, slug, abilityDir, schemaVersion, minAppVersion, changes, problems } = context;
19546
+ if (schemaVersion === 3 && !("releases" in entry)) {
19547
+ problems.push({ slug, message: "schemaVersion 3 plugin requires versioned releases" });
19548
+ return;
19549
+ }
19550
+ if ("releases" in entry) {
19551
+ if (schemaVersion !== 3) {
19552
+ problems.push({ slug, message: "versioned plugin releases require marketplace schemaVersion 3" });
19553
+ return;
19554
+ }
19555
+ const releases = entry.releases;
19556
+ if (!Array.isArray(releases) || releases.length === 0) {
19557
+ problems.push({ slug, message: "plugin releases must be a nonempty array" });
19558
+ return;
19559
+ }
19560
+ let latest;
19561
+ const seen = new Set;
19562
+ for (const raw2 of releases) {
19563
+ if (typeof raw2 !== "object" || raw2 === null || Array.isArray(raw2)) {
19564
+ problems.push({ slug, message: "plugin release must be an object" });
19565
+ continue;
19566
+ }
19567
+ const release = raw2;
19568
+ const match2 = typeof release.version === "string" ? /^(\d+)\.(\d+)\.(\d+)$/.exec(release.version) : null;
19569
+ if (!match2 || seen.has(release.version)) {
19570
+ problems.push({ slug, message: `invalid or duplicate plugin release version: ${String(release.version)}` });
19571
+ continue;
19572
+ }
19573
+ seen.add(release.version);
19574
+ const minimum = typeof release.minAppVersion === "string" ? /^(\d+)\.(\d+)\.(\d+)$/.exec(release.minAppVersion) : null;
19575
+ if (!minimum || typeof minAppVersion !== "string" || !/^(\d+)\.(\d+)\.(\d+)$/.test(minAppVersion)) {
19576
+ problems.push({ slug, message: `release ${release.version} has an invalid minAppVersion` });
19577
+ } else {
19578
+ const marketMinimum = /^(\d+)\.(\d+)\.(\d+)$/.exec(minAppVersion);
19579
+ if (marketMinimum && compareVersionParts(minimum.slice(1).map(Number), marketMinimum.slice(1).map(Number)) < 0) {
19580
+ problems.push({ slug, message: `release ${release.version} requires an app older than the marketplace` });
19581
+ }
19582
+ }
19583
+ if (typeof release.pluginApiVersion !== "string" || !/^\^\d+\.\d+\.\d+$/.test(release.pluginApiVersion)) {
19584
+ problems.push({ slug, message: `release ${release.version} has an invalid pluginApiVersion` });
19585
+ }
19586
+ for (const field of ["permissions", "commands"]) {
19587
+ if (release[field] !== undefined && !stringArray(release[field])) {
19588
+ problems.push({ slug, message: `release ${release.version} has invalid ${field}` });
19589
+ }
19590
+ }
19591
+ const parts = [Number(match2[1]), Number(match2[2]), Number(match2[3])];
19592
+ if (!latest || compareVersionParts(parts, latest.parts) > 0) {
19593
+ latest = { version: release.version, parts };
19594
+ }
19595
+ const artifact = release.artifact;
19596
+ if (typeof artifact !== "object" || artifact === null || Array.isArray(artifact)) {
19597
+ problems.push({ slug, message: `release ${release.version} has no artifact` });
19598
+ continue;
19599
+ }
19600
+ const { url, sha256 } = artifact;
19601
+ let validUrl = false;
19602
+ try {
19603
+ const parsed = new URL(String(url));
19604
+ validUrl = parsed.protocol === "https:" && !parsed.username && !parsed.password && !parsed.hash;
19605
+ } catch {}
19606
+ if (!validUrl || typeof sha256 !== "string" || !/^[a-f0-9]{64}$/.test(sha256)) {
19607
+ problems.push({ slug, message: `release ${release.version} has an invalid HTTPS artifact or SHA-256` });
19608
+ }
19609
+ }
19610
+ if (latest && entry.version !== latest.version) {
19611
+ changes.push({ slug, field: "version", from: entry.version, to: latest.version });
19612
+ entry.version = latest.version;
19613
+ }
19614
+ return;
19615
+ }
19486
19616
  const manifest = readJsonFile2(join4(abilityDir, "plugin.json"));
19487
19617
  if (!manifest) {
19488
19618
  problems.push({ slug, message: "plugin.json is missing or malformed" });
@@ -19522,6 +19652,13 @@ function reconcilePlugin(context) {
19522
19652
  }
19523
19653
  }
19524
19654
  }
19655
+ function compareVersionParts(left, right) {
19656
+ for (let index = 0;index < 3; index += 1) {
19657
+ if (left[index] !== right[index])
19658
+ return (left[index] ?? 0) - (right[index] ?? 0);
19659
+ }
19660
+ return 0;
19661
+ }
19525
19662
  function reconcileIdentityFile(context) {
19526
19663
  const { entry, slug, abilityDir, fileName, changes, problems } = context;
19527
19664
  const identity = readJsonFile2(join4(abilityDir, fileName));
@@ -19653,7 +19790,7 @@ function readManualSdkVersion(manualDir) {
19653
19790
  var HELP_TEXT = `Vetta plugin manager
19654
19791
 
19655
19792
  Usage:
19656
- vetta-plugin-cli add <npm-package|zip-path|http-url> [--json]
19793
+ vetta-plugin-cli add <npm-package|package-path|http-url> [--json]
19657
19794
  vetta-plugin-cli reload <plugin-id> [--json]
19658
19795
  vetta-plugin-cli docs [--check-latest] [--json]
19659
19796
  vetta-plugin-cli init --id <plugin-id> [--name <display>] [dir] [--json]
@@ -19667,7 +19804,7 @@ Examples:
19667
19804
  npx @vetta-org/plugin-cli add @example/vetta-plugin-demo
19668
19805
  npx @vetta-org/plugin-cli add @example/vetta-plugin-demo@1.2.0
19669
19806
  npx @vetta-org/plugin-cli add . # 当前插件工程(先 pack)
19670
- npx @vetta-org/plugin-cli add ./release/demo-1.2.0.zip
19807
+ npx @vetta-org/plugin-cli add ./release/demo-1.2.0.vettapkg
19671
19808
  npx @vetta-org/plugin-cli reload demo
19672
19809
  npx @vetta-org/plugin-cli docs
19673
19810
  npx @vetta-org/plugin-cli init --id my-plugin --name "My Plugin"
@@ -19693,7 +19830,7 @@ function parsePluginAddCommand(argv) {
19693
19830
  }
19694
19831
  const [source, unexpected] = parsed.positionals;
19695
19832
  if (!source)
19696
- return { type: "error", message: "Missing <npm-package|zip-path|http-url>" };
19833
+ return { type: "error", message: "Missing <npm-package|package-path|http-url>" };
19697
19834
  if (unexpected)
19698
19835
  return { type: "error", message: `Unexpected argument: ${unexpected}` };
19699
19836
  return { type: "add", source, json: parsed.values.json === true };
@@ -19924,8 +20061,9 @@ function isHttpUrl(source) {
19924
20061
  return false;
19925
20062
  }
19926
20063
  }
19927
- function isLocalZip(source) {
19928
- if (source.toLowerCase().endsWith(".zip"))
20064
+ function isLocalPackage(source) {
20065
+ const lower = source.toLowerCase();
20066
+ if (lower.endsWith(".vettapkg") || lower.endsWith(".zip"))
19929
20067
  return true;
19930
20068
  const path = resolve5(source);
19931
20069
  return existsSync4(path) && !statSync2(path).isDirectory();
@@ -19944,7 +20082,7 @@ function resolveProjectArchive(source) {
19944
20082
  }
19945
20083
  throw new Error(`No plugin.json found in ${from} or any parent directory.`);
19946
20084
  }
19947
- const archivePath = join6(project.root, "release", `${project.pluginId}-${project.version}.zip`);
20085
+ const archivePath = join6(project.root, "release", `${project.pluginId}-${project.version}.vettapkg`);
19948
20086
  if (!existsSync4(archivePath)) {
19949
20087
  throw new Error(`Packaged archive not found: ${archivePath}
19950
20088
  Build it first: npm run build && npx vetta-plugin pack`);
@@ -19965,6 +20103,7 @@ function indexDriftHint(project) {
19965
20103
  function npmInstallInput(resolved) {
19966
20104
  return {
19967
20105
  operation: "install-from-path",
20106
+ initiator: "plugin-cli",
19968
20107
  path: resolved.archivePath,
19969
20108
  enable: true,
19970
20109
  source: "npm",
@@ -20058,19 +20197,22 @@ async function runPluginCommand(command, dependencies = defaultDependencies) {
20058
20197
  } else if (isHttpUrl(command.source)) {
20059
20198
  result = await dependencies.runAction("plugins.manage", {
20060
20199
  operation: "install-from-url",
20200
+ initiator: "plugin-cli",
20061
20201
  url: command.source
20062
20202
  });
20063
20203
  } else if (isDirectorySource(command.source)) {
20064
20204
  const { archivePath, project } = resolveProjectArchive(command.source);
20065
20205
  result = await dependencies.runAction("plugins.manage", {
20066
20206
  operation: "install-from-path",
20207
+ initiator: "plugin-cli",
20067
20208
  path: archivePath,
20068
20209
  enable: true
20069
20210
  });
20070
20211
  driftHint = indexDriftHint(project);
20071
- } else if (isLocalZip(command.source)) {
20212
+ } else if (isLocalPackage(command.source)) {
20072
20213
  result = await dependencies.runAction("plugins.manage", {
20073
20214
  operation: "install-from-path",
20215
+ initiator: "plugin-cli",
20074
20216
  path: resolve5(command.source),
20075
20217
  enable: true
20076
20218
  });
@@ -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.6",
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",