@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 +10 -0
- package/README.md +11 -4
- package/dist/cli.js +154 -12
- package/dist/command.d.ts.map +1 -1
- package/dist/index.js +154 -12
- package/dist/sync.d.ts.map +1 -1
- package/package.json +1 -1
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.
|
|
105
|
-
npx @vetta-org/plugin-cli add https://example.com/demo-1.0.0.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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|
|
|
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.
|
|
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|
|
|
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
|
|
19929
|
-
|
|
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}.
|
|
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 (
|
|
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
|
});
|
package/dist/command.d.ts.map
CHANGED
|
@@ -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 =
|
|
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
|
|
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|
|
|
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.
|
|
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|
|
|
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
|
|
19928
|
-
|
|
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}.
|
|
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 (
|
|
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
|
});
|
package/dist/sync.d.ts.map
CHANGED
|
@@ -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"]}
|