agent-plugkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/adapters/agent-plugins.d.ts +35 -0
- package/dist/adapters/agent-plugins.js +57 -0
- package/dist/adapters/agent-plugins.js.map +1 -0
- package/dist/adapters/claude.d.ts +64 -0
- package/dist/adapters/claude.js +123 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/cli-validation.d.ts +12 -0
- package/dist/adapters/cli-validation.js +31 -0
- package/dist/adapters/cli-validation.js.map +1 -0
- package/dist/adapters/codex.d.ts +62 -0
- package/dist/adapters/codex.js +91 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/marketplace-registration.d.ts +17 -0
- package/dist/adapters/marketplace-registration.js +369 -0
- package/dist/adapters/marketplace-registration.js.map +1 -0
- package/dist/adapters/marketplace.d.ts +11 -0
- package/dist/adapters/marketplace.js +96 -0
- package/dist/adapters/marketplace.js.map +1 -0
- package/dist/adapters/mcp.d.ts +17 -0
- package/dist/adapters/mcp.js +28 -0
- package/dist/adapters/mcp.js.map +1 -0
- package/dist/application/artifact-generation.d.ts +58 -0
- package/dist/application/artifact-generation.js +344 -0
- package/dist/application/artifact-generation.js.map +1 -0
- package/dist/application/local-release.d.ts +122 -0
- package/dist/application/local-release.js +940 -0
- package/dist/application/local-release.js.map +1 -0
- package/dist/application/marketplace-document-contract.d.ts +81 -0
- package/dist/application/marketplace-document-contract.js +2 -0
- package/dist/application/marketplace-document-contract.js.map +1 -0
- package/dist/application/marketplace-document.d.ts +45 -0
- package/dist/application/marketplace-document.js +557 -0
- package/dist/application/marketplace-document.js.map +1 -0
- package/dist/application/marketplace-registration-contract.d.ts +51 -0
- package/dist/application/marketplace-registration-contract.js +11 -0
- package/dist/application/marketplace-registration-contract.js.map +1 -0
- package/dist/application/marketplace-registration.d.ts +20 -0
- package/dist/application/marketplace-registration.js +304 -0
- package/dist/application/marketplace-registration.js.map +1 -0
- package/dist/application/marketplace.d.ts +9 -0
- package/dist/application/marketplace.js +32 -0
- package/dist/application/marketplace.js.map +1 -0
- package/dist/application/plugin-components-contract.d.ts +135 -0
- package/dist/application/plugin-components-contract.js +2 -0
- package/dist/application/plugin-components-contract.js.map +1 -0
- package/dist/application/plugin-components.d.ts +17 -0
- package/dist/application/plugin-components.js +1000 -0
- package/dist/application/plugin-components.js.map +1 -0
- package/dist/application/plugin-document-contract.d.ts +115 -0
- package/dist/application/plugin-document-contract.js +2 -0
- package/dist/application/plugin-document-contract.js.map +1 -0
- package/dist/application/plugin-document.d.ts +69 -0
- package/dist/application/plugin-document.js +1037 -0
- package/dist/application/plugin-document.js.map +1 -0
- package/dist/application/plugin-lifecycle-contract.d.ts +230 -0
- package/dist/application/plugin-lifecycle-contract.js +3 -0
- package/dist/application/plugin-lifecycle-contract.js.map +1 -0
- package/dist/application/plugin-lifecycle.d.ts +8 -0
- package/dist/application/plugin-lifecycle.js +1375 -0
- package/dist/application/plugin-lifecycle.js.map +1 -0
- package/dist/application/plugin-scaffold.d.ts +18 -0
- package/dist/application/plugin-scaffold.js +188 -0
- package/dist/application/plugin-scaffold.js.map +1 -0
- package/dist/application/plugin-source.d.ts +39 -0
- package/dist/application/plugin-source.js +59 -0
- package/dist/application/plugin-source.js.map +1 -0
- package/dist/application/skill-document-contract.d.ts +92 -0
- package/dist/application/skill-document-contract.js +8 -0
- package/dist/application/skill-document-contract.js.map +1 -0
- package/dist/application/skill-document-guided.d.ts +3 -0
- package/dist/application/skill-document-guided.js +53 -0
- package/dist/application/skill-document-guided.js.map +1 -0
- package/dist/application/skill-document.d.ts +19 -0
- package/dist/application/skill-document.js +368 -0
- package/dist/application/skill-document.js.map +1 -0
- package/dist/application/workspace-create-contract.d.ts +50 -0
- package/dist/application/workspace-create-contract.js +16 -0
- package/dist/application/workspace-create-contract.js.map +1 -0
- package/dist/application/workspace-create.d.ts +8 -0
- package/dist/application/workspace-create.js +656 -0
- package/dist/application/workspace-create.js.map +1 -0
- package/dist/application/workspace-health-contract.d.ts +133 -0
- package/dist/application/workspace-health-contract.js +272 -0
- package/dist/application/workspace-health-contract.js.map +1 -0
- package/dist/application/workspace-health.d.ts +10 -0
- package/dist/application/workspace-health.js +1114 -0
- package/dist/application/workspace-health.js.map +1 -0
- package/dist/application/workspace-operation-contract.d.ts +99 -0
- package/dist/application/workspace-operation-contract.js +450 -0
- package/dist/application/workspace-operation-contract.js.map +1 -0
- package/dist/application/workspace-operation.d.ts +42 -0
- package/dist/application/workspace-operation.js +517 -0
- package/dist/application/workspace-operation.js.map +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +136 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add.d.ts +8 -0
- package/dist/commands/add.js +77 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/build.d.ts +7 -0
- package/dist/commands/build.js +29 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/component-scaffold.d.ts +4 -0
- package/dist/commands/component-scaffold.js +14 -0
- package/dist/commands/component-scaffold.js.map +1 -0
- package/dist/commands/import-skill.d.ts +15 -0
- package/dist/commands/import-skill.js +41 -0
- package/dist/commands/import-skill.js.map +1 -0
- package/dist/commands/index-gen.d.ts +12 -0
- package/dist/commands/index-gen.js +33 -0
- package/dist/commands/index-gen.js.map +1 -0
- package/dist/commands/init-repo.d.ts +8 -0
- package/dist/commands/init-repo.js +53 -0
- package/dist/commands/init-repo.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +33 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install-repo.d.ts +20 -0
- package/dist/commands/install-repo.js +200 -0
- package/dist/commands/install-repo.js.map +1 -0
- package/dist/commands/release-local.d.ts +9 -0
- package/dist/commands/release-local.js +27 -0
- package/dist/commands/release-local.js.map +1 -0
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +45 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/core/errors.d.ts +5 -0
- package/dist/core/errors.js +12 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/generated/skill-content.d.ts +3 -0
- package/dist/generated/skill-content.js +9 -0
- package/dist/generated/skill-content.js.map +1 -0
- package/dist/infrastructure/authorized-path.d.ts +20 -0
- package/dist/infrastructure/authorized-path.js +85 -0
- package/dist/infrastructure/authorized-path.js.map +1 -0
- package/dist/infrastructure/document-commit.d.ts +165 -0
- package/dist/infrastructure/document-commit.js +1352 -0
- package/dist/infrastructure/document-commit.js.map +1 -0
- package/dist/infrastructure/process-runner.d.ts +29 -0
- package/dist/infrastructure/process-runner.js +130 -0
- package/dist/infrastructure/process-runner.js.map +1 -0
- package/dist/infrastructure/vscode-user-settings.d.ts +42 -0
- package/dist/infrastructure/vscode-user-settings.js +509 -0
- package/dist/infrastructure/vscode-user-settings.js.map +1 -0
- package/dist/schema/plugin-yaml.d.ts +89 -0
- package/dist/schema/plugin-yaml.js +241 -0
- package/dist/schema/plugin-yaml.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +30 -0
- package/dist/utils/frontmatter.js +60 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/helpers.d.ts +44 -0
- package/dist/utils/helpers.js +94 -0
- package/dist/utils/helpers.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { accessSync, constants as fsConstants, existsSync, lstatSync, } from "node:fs";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { Document, isAlias, isMap, isScalar, isSeq, parseDocument, } from "yaml";
|
|
6
|
+
import { AuthorizedPathError, authorizeExistingDirectory, resolveAuthorizedPath, } from "../infrastructure/authorized-path.js";
|
|
7
|
+
import { commitAuthorizedDocumentAndFiles, documentRevision, } from "../infrastructure/document-commit.js";
|
|
8
|
+
import { loadPluginDocumentState, pluginDocumentRelativePath, } from "./plugin-document.js";
|
|
9
|
+
import { createAddedComponentScaffold, } from "./plugin-scaffold.js";
|
|
10
|
+
import { parsePluginYamlSource, validatePluginYamlValue, } from "./plugin-source.js";
|
|
11
|
+
import { isPluginLocalPathReference } from "./workspace-health.js";
|
|
12
|
+
const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]);
|
|
13
|
+
const componentKey = {
|
|
14
|
+
skill: "skills",
|
|
15
|
+
mcp: "mcp",
|
|
16
|
+
hook: "hooks",
|
|
17
|
+
lsp: "lsp",
|
|
18
|
+
};
|
|
19
|
+
function collectionsFrom(config) {
|
|
20
|
+
return {
|
|
21
|
+
skill: structuredClone(config.components.skills ?? []),
|
|
22
|
+
mcp: structuredClone(config.components.mcp ?? []),
|
|
23
|
+
hook: structuredClone(config.components.hooks ?? []),
|
|
24
|
+
lsp: structuredClone(config.components.lsp ?? []),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function valuesForKind(config, kind) {
|
|
28
|
+
const values = config.components[componentKey[kind]] ?? [];
|
|
29
|
+
return structuredClone(values);
|
|
30
|
+
}
|
|
31
|
+
function loadInternal(request, dependencies = {}) {
|
|
32
|
+
const loaded = loadPluginDocumentState(request, dependencies);
|
|
33
|
+
if (loaded.status !== "loaded") {
|
|
34
|
+
const problem = loaded.problem;
|
|
35
|
+
return {
|
|
36
|
+
status: loaded.status,
|
|
37
|
+
message: problem.message,
|
|
38
|
+
impact: problem.impact,
|
|
39
|
+
nextAction: problem.nextAction,
|
|
40
|
+
...(loaded.status === "invalid" ? { revision: loaded.revision } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const bytes = loaded.sourceBytes;
|
|
44
|
+
const bom = bytes.subarray(0, UTF8_BOM.length).equals(UTF8_BOM);
|
|
45
|
+
const source = bytes.subarray(bom ? UTF8_BOM.length : 0).toString("utf8");
|
|
46
|
+
const parsed = parsePluginYamlSource(source);
|
|
47
|
+
if (parsed.status !== "valid") {
|
|
48
|
+
return {
|
|
49
|
+
status: "invalid",
|
|
50
|
+
message: "当前插件声明还不能安全维护组件。",
|
|
51
|
+
impact: "组件表单不会覆盖无效源文件;已有内容保持不变。",
|
|
52
|
+
nextAction: "请先修正插件基础信息中的阻塞问题,再重新加载。",
|
|
53
|
+
revision: documentRevision(bytes),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const workspace = authorizeExistingDirectory(request.directory);
|
|
58
|
+
const pluginPath = resolveAuthorizedPath(workspace, `plugins/${request.pluginDirectoryName}`);
|
|
59
|
+
const pluginAuthorization = authorizeExistingDirectory(pluginPath);
|
|
60
|
+
return {
|
|
61
|
+
status: "loaded",
|
|
62
|
+
value: {
|
|
63
|
+
workspaceCanonicalPath: workspace.canonicalPath,
|
|
64
|
+
pluginAuthorization,
|
|
65
|
+
sourceBytes: Buffer.from(bytes),
|
|
66
|
+
source,
|
|
67
|
+
bom,
|
|
68
|
+
lineEnding: source.includes("\r\n") ? "crlf" : "lf",
|
|
69
|
+
revision: documentRevision(bytes),
|
|
70
|
+
config: parsed.value,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
status: "unavailable",
|
|
77
|
+
message: "当前插件文件夹无法确认授权。",
|
|
78
|
+
impact: "组件和关联文件保持不变。",
|
|
79
|
+
nextAction: "请重新检查 Marketplace 或文件夹权限。",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function pairForKey(map, key) {
|
|
84
|
+
return map.items.find((pair) => isScalar(pair.key) && pair.key.value === key);
|
|
85
|
+
}
|
|
86
|
+
function lineIndent(source, offset) {
|
|
87
|
+
const start = source.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
|
|
88
|
+
const indent = source.slice(start, offset);
|
|
89
|
+
if (!/^[ \t]*$/.test(indent)) {
|
|
90
|
+
throw new Error("无法确认组件声明的缩进");
|
|
91
|
+
}
|
|
92
|
+
return indent;
|
|
93
|
+
}
|
|
94
|
+
function serializeSequence(values, continuationIndent, eol) {
|
|
95
|
+
if (values.length === 0)
|
|
96
|
+
return "[]";
|
|
97
|
+
const serialized = new Document(values).toString({
|
|
98
|
+
lineWidth: 0,
|
|
99
|
+
blockQuote: false,
|
|
100
|
+
});
|
|
101
|
+
const normalized = serialized.replace(/\n$/, "").replaceAll("\n", eol);
|
|
102
|
+
return normalized.replaceAll(eol, `${eol}${continuationIndent}`);
|
|
103
|
+
}
|
|
104
|
+
function serializeInlineValue(value) {
|
|
105
|
+
const document = new Document();
|
|
106
|
+
document.contents = document.createNode(value, { flow: true });
|
|
107
|
+
return document
|
|
108
|
+
.toString({ lineWidth: 0, blockQuote: false })
|
|
109
|
+
.replace(/\n$/, "");
|
|
110
|
+
}
|
|
111
|
+
function tokenStart(item, fallback) {
|
|
112
|
+
return item?.start?.find((part) => part.offset !== undefined)?.offset ?? fallback;
|
|
113
|
+
}
|
|
114
|
+
function applyTextPatches(source, patches) {
|
|
115
|
+
const ordered = [...patches].sort((left, right) => right.start - left.start || right.end - left.end);
|
|
116
|
+
let previousStart = source.length + 1;
|
|
117
|
+
let next = source;
|
|
118
|
+
for (const patch of ordered) {
|
|
119
|
+
if (patch.start < 0 ||
|
|
120
|
+
patch.end < patch.start ||
|
|
121
|
+
patch.end > source.length ||
|
|
122
|
+
patch.end > previousStart) {
|
|
123
|
+
throw new Error("组件最小补丁范围发生重叠");
|
|
124
|
+
}
|
|
125
|
+
next =
|
|
126
|
+
next.slice(0, patch.start) + patch.replacement + next.slice(patch.end);
|
|
127
|
+
previousStart = patch.start;
|
|
128
|
+
}
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
function editComponentItem(source, item, previousValue, nextValue, eol) {
|
|
132
|
+
if (item.flow || item.range == null) {
|
|
133
|
+
throw new Error("目标组件不是可最小修改的 block mapping");
|
|
134
|
+
}
|
|
135
|
+
const previous = previousValue;
|
|
136
|
+
const next = nextValue;
|
|
137
|
+
const pairTokens = item.srcToken?.items;
|
|
138
|
+
const patches = [];
|
|
139
|
+
const existingKeys = new Set();
|
|
140
|
+
for (const [pairIndex, pair] of item.items.entries()) {
|
|
141
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== "string") {
|
|
142
|
+
throw new Error("目标组件包含无法定位的字段 key");
|
|
143
|
+
}
|
|
144
|
+
const key = pair.key.value;
|
|
145
|
+
existingKeys.add(key);
|
|
146
|
+
const had = Object.hasOwn(previous, key);
|
|
147
|
+
const has = Object.hasOwn(next, key);
|
|
148
|
+
if (!had || (has && isDeepStrictEqual(previous[key], next[key])))
|
|
149
|
+
continue;
|
|
150
|
+
if (!has) {
|
|
151
|
+
const keyRange = pair.key.range;
|
|
152
|
+
if (keyRange == null)
|
|
153
|
+
throw new Error(`无法定位目标组件字段 ${key}`);
|
|
154
|
+
const nextPair = item.items[pairIndex + 1];
|
|
155
|
+
const nextKey = nextPair?.key;
|
|
156
|
+
const start = pairIndex === 0
|
|
157
|
+
? keyRange[0]
|
|
158
|
+
: tokenStart(pairTokens?.[pairIndex], keyRange[0]);
|
|
159
|
+
const end = pairIndex === 0 && nextPair !== undefined
|
|
160
|
+
? tokenStart(pairTokens?.[pairIndex + 1], nextKey?.range?.[0] ?? item.range[1])
|
|
161
|
+
: nextPair === undefined
|
|
162
|
+
? item.range[1]
|
|
163
|
+
: tokenStart(pairTokens?.[pairIndex + 1], nextKey?.range?.[0] ?? item.range[1]);
|
|
164
|
+
patches.push({
|
|
165
|
+
start,
|
|
166
|
+
end,
|
|
167
|
+
replacement: pairIndex === 0 && nextPair !== undefined ? eol : "",
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const value = pair.value;
|
|
172
|
+
if (value == null ||
|
|
173
|
+
value.range == null ||
|
|
174
|
+
isAlias(value) ||
|
|
175
|
+
value.anchor !== undefined) {
|
|
176
|
+
throw new Error(`目标组件字段 ${key} 不能安全最小修改`);
|
|
177
|
+
}
|
|
178
|
+
patches.push({
|
|
179
|
+
start: value.range[0],
|
|
180
|
+
end: value.range[1],
|
|
181
|
+
replacement: `${serializeInlineValue(next[key])}${source.slice(value.range[0], value.range[1]).endsWith("\r\n")
|
|
182
|
+
? "\r\n"
|
|
183
|
+
: source.slice(value.range[0], value.range[1]).endsWith("\n")
|
|
184
|
+
? "\n"
|
|
185
|
+
: ""}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const additions = Object.keys(next).filter((key) => !existingKeys.has(key));
|
|
189
|
+
if (additions.length > 0) {
|
|
190
|
+
const lastPair = item.items.at(-1);
|
|
191
|
+
const lastKey = lastPair?.key;
|
|
192
|
+
if (lastKey?.range == null) {
|
|
193
|
+
throw new Error("无法定位目标组件新增字段的缩进");
|
|
194
|
+
}
|
|
195
|
+
const indent = lineIndent(source, lastKey.range[0]);
|
|
196
|
+
patches.push({
|
|
197
|
+
start: item.range[1],
|
|
198
|
+
end: item.range[1],
|
|
199
|
+
replacement: additions
|
|
200
|
+
.map((key) => `${indent}${key}: ${serializeInlineValue(next[key])}${eol}`)
|
|
201
|
+
.join(""),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return applyTextPatches(source, patches);
|
|
205
|
+
}
|
|
206
|
+
function patchComponentSequence(loaded, mutation) {
|
|
207
|
+
const document = parseDocument(loaded.source, {
|
|
208
|
+
keepSourceTokens: true,
|
|
209
|
+
logLevel: "silent",
|
|
210
|
+
});
|
|
211
|
+
if (document.errors.length > 0 || !isMap(document.contents)) {
|
|
212
|
+
return { status: "unsafe", message: "plugin.yaml 不是可局部修改的 mapping" };
|
|
213
|
+
}
|
|
214
|
+
const componentsPair = pairForKey(document.contents, "components");
|
|
215
|
+
const componentsNode = componentsPair?.value;
|
|
216
|
+
if (componentsPair === undefined ||
|
|
217
|
+
componentsNode == null ||
|
|
218
|
+
isAlias(componentsNode) ||
|
|
219
|
+
!isMap(componentsNode) ||
|
|
220
|
+
componentsNode.range == null) {
|
|
221
|
+
return {
|
|
222
|
+
status: "unsafe",
|
|
223
|
+
message: "components 使用了别名或非 mapping 结构",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const key = componentKey[mutation.kind];
|
|
227
|
+
const pair = pairForKey(componentsNode, key);
|
|
228
|
+
const eol = loaded.lineEnding === "crlf" ? "\r\n" : "\n";
|
|
229
|
+
let nextSource;
|
|
230
|
+
try {
|
|
231
|
+
if (pair !== undefined) {
|
|
232
|
+
const node = pair.value;
|
|
233
|
+
if (node == null ||
|
|
234
|
+
isAlias(node) ||
|
|
235
|
+
!isSeq(node) ||
|
|
236
|
+
node.range == null ||
|
|
237
|
+
node.items.some((item) => {
|
|
238
|
+
const componentNode = item;
|
|
239
|
+
return (componentNode != null &&
|
|
240
|
+
(isAlias(componentNode) || componentNode.anchor !== undefined));
|
|
241
|
+
})) {
|
|
242
|
+
return {
|
|
243
|
+
status: "unsafe",
|
|
244
|
+
message: `${key} 使用了当前不能安全局部维护的 anchor/alias 结构`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (node.flow) {
|
|
248
|
+
if (mutation.operation !== "add" ||
|
|
249
|
+
node.items.length !== 0 ||
|
|
250
|
+
loaded.source.slice(node.range[0], node.range[1]).trim() !== "[]") {
|
|
251
|
+
return {
|
|
252
|
+
status: "unsafe",
|
|
253
|
+
message: `${key} 使用了当前不能安全局部维护的 flow 结构`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const pairKey = pair.key;
|
|
257
|
+
if (pairKey?.range == null) {
|
|
258
|
+
return { status: "unsafe", message: `无法定位 ${key} 字段` };
|
|
259
|
+
}
|
|
260
|
+
const indent = `${lineIndent(loaded.source, pairKey.range[0])} `;
|
|
261
|
+
let replacementStart = node.range[0];
|
|
262
|
+
while (replacementStart > pairKey.range[1] &&
|
|
263
|
+
(loaded.source[replacementStart - 1] === " " ||
|
|
264
|
+
loaded.source[replacementStart - 1] === "\t")) {
|
|
265
|
+
replacementStart -= 1;
|
|
266
|
+
}
|
|
267
|
+
const nodeEnd = node.range[2] ?? node.range[1];
|
|
268
|
+
const trailing = loaded.source.slice(node.range[1], nodeEnd);
|
|
269
|
+
const hadLineEnding = trailing.endsWith(eol);
|
|
270
|
+
const inlineSuffix = hadLineEnding
|
|
271
|
+
? trailing.slice(0, -eol.length)
|
|
272
|
+
: trailing;
|
|
273
|
+
if (inlineSuffix.trim().length > 0) {
|
|
274
|
+
if (node.comment === undefined ||
|
|
275
|
+
!/^[ \t]+#[^\r\n]*$/.test(inlineSuffix)) {
|
|
276
|
+
return {
|
|
277
|
+
status: "unsafe",
|
|
278
|
+
message: `${key} 的空 flow sequence 尾部无法安全保留`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
nextSource = applyTextPatches(loaded.source, [
|
|
282
|
+
{
|
|
283
|
+
start: replacementStart,
|
|
284
|
+
end: nodeEnd,
|
|
285
|
+
replacement: `${inlineSuffix}${eol}${indent}${serializeSequence([mutation.value], indent, eol)}${hadLineEnding ? eol : ""}`,
|
|
286
|
+
},
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
nextSource = applyTextPatches(loaded.source, [
|
|
291
|
+
{
|
|
292
|
+
start: replacementStart,
|
|
293
|
+
end: node.range[1],
|
|
294
|
+
replacement: `${eol}${indent}${serializeSequence([mutation.value], indent, eol)}`,
|
|
295
|
+
},
|
|
296
|
+
]);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (mutation.operation === "add") {
|
|
300
|
+
const indent = lineIndent(loaded.source, node.range[0]);
|
|
301
|
+
const leading = node.range[1] > 0 && loaded.source[node.range[1] - 1] !== "\n"
|
|
302
|
+
? eol
|
|
303
|
+
: "";
|
|
304
|
+
nextSource = applyTextPatches(loaded.source, [
|
|
305
|
+
{
|
|
306
|
+
start: node.range[1],
|
|
307
|
+
end: node.range[1],
|
|
308
|
+
replacement: `${leading}${indent}${serializeSequence([mutation.value], indent, eol)}${eol}`,
|
|
309
|
+
},
|
|
310
|
+
]);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const target = node.items[mutation.index];
|
|
314
|
+
if (target == null || target.range == null || !isMap(target)) {
|
|
315
|
+
return {
|
|
316
|
+
status: "unsafe",
|
|
317
|
+
message: `无法定位 ${key} 中的目标组件`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (mutation.operation === "edit") {
|
|
321
|
+
const previous = valuesForKind(loaded.config, mutation.kind)[mutation.index];
|
|
322
|
+
if (previous === undefined) {
|
|
323
|
+
return { status: "unsafe", message: `无法读取 ${key} 中的目标组件` };
|
|
324
|
+
}
|
|
325
|
+
nextSource = editComponentItem(loaded.source, target, previous, mutation.value, eol);
|
|
326
|
+
}
|
|
327
|
+
else if (node.items.length === 1) {
|
|
328
|
+
const original = loaded.source.slice(node.range[0], node.range[1]);
|
|
329
|
+
const trailing = original.endsWith("\r\n")
|
|
330
|
+
? "\r\n"
|
|
331
|
+
: original.endsWith("\n")
|
|
332
|
+
? "\n"
|
|
333
|
+
: "";
|
|
334
|
+
nextSource = applyTextPatches(loaded.source, [
|
|
335
|
+
{
|
|
336
|
+
start: node.range[0],
|
|
337
|
+
end: node.range[1],
|
|
338
|
+
replacement: `[]${trailing}`,
|
|
339
|
+
},
|
|
340
|
+
]);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const sequenceTokens = node.srcToken?.items;
|
|
344
|
+
const start = tokenStart(sequenceTokens?.[mutation.index], target.range[0]);
|
|
345
|
+
const nextTarget = node.items[mutation.index + 1];
|
|
346
|
+
const end = nextTarget == null
|
|
347
|
+
? node.range[1]
|
|
348
|
+
: tokenStart(sequenceTokens?.[mutation.index + 1], nextTarget.range?.[0] ?? node.range[1]);
|
|
349
|
+
nextSource = applyTextPatches(loaded.source, [
|
|
350
|
+
{ start, end, replacement: "" },
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
if (mutation.operation !== "add") {
|
|
357
|
+
return { status: "unsafe", message: `无法定位 ${key} 组件序列` };
|
|
358
|
+
}
|
|
359
|
+
if (componentsNode.flow) {
|
|
360
|
+
return {
|
|
361
|
+
status: "unsafe",
|
|
362
|
+
message: "flow components mapping 不能安全插入新的组件类型",
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const componentsKey = componentsPair.key;
|
|
366
|
+
if (componentsKey?.range == null) {
|
|
367
|
+
return { status: "unsafe", message: "无法定位 components 字段" };
|
|
368
|
+
}
|
|
369
|
+
const childIndent = `${lineIndent(loaded.source, componentsKey.range[0])} `;
|
|
370
|
+
const sequenceIndent = `${childIndent} `;
|
|
371
|
+
const serialized = serializeSequence([mutation.value], sequenceIndent, eol);
|
|
372
|
+
const insertionPoint = componentsNode.range[1];
|
|
373
|
+
const leading = insertionPoint > 0 && loaded.source[insertionPoint - 1] !== "\n"
|
|
374
|
+
? eol
|
|
375
|
+
: "";
|
|
376
|
+
const insertion = `${leading}${childIndent}${key}:${eol}${sequenceIndent}${serialized}${eol}`;
|
|
377
|
+
nextSource =
|
|
378
|
+
loaded.source.slice(0, insertionPoint) +
|
|
379
|
+
insertion +
|
|
380
|
+
loaded.source.slice(insertionPoint);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
return {
|
|
385
|
+
status: "unsafe",
|
|
386
|
+
message: error instanceof Error ? error.message : String(error),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const encoded = Buffer.from(nextSource, "utf8");
|
|
390
|
+
return {
|
|
391
|
+
status: "patched",
|
|
392
|
+
bytes: loaded.bom ? Buffer.concat([UTF8_BOM, encoded]) : encoded,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function technicalPath(issue) {
|
|
396
|
+
const base = issue.instancePath === "/" ? "" : issue.instancePath;
|
|
397
|
+
if (issue.keyword === "required" && issue.missingProperty !== undefined) {
|
|
398
|
+
return `${base}/${issue.missingProperty}` || "/";
|
|
399
|
+
}
|
|
400
|
+
if (issue.keyword === "additionalProperties" &&
|
|
401
|
+
issue.additionalProperty !== undefined) {
|
|
402
|
+
return `${base}/${issue.additionalProperty}` || "/";
|
|
403
|
+
}
|
|
404
|
+
return issue.instancePath || "/";
|
|
405
|
+
}
|
|
406
|
+
function schemaIssue(issue, index, fallbackKind) {
|
|
407
|
+
const path = technicalPath(issue);
|
|
408
|
+
const match = path.match(/^\/components\/(skills|mcp|hooks|lsp)\/(\d+)(?:\/(.*))?/);
|
|
409
|
+
const kind = match?.[1] === "skills"
|
|
410
|
+
? "skill"
|
|
411
|
+
: match?.[1] === "hooks"
|
|
412
|
+
? "hook"
|
|
413
|
+
: match?.[1] === "mcp"
|
|
414
|
+
? "mcp"
|
|
415
|
+
: match?.[1] === "lsp"
|
|
416
|
+
? "lsp"
|
|
417
|
+
: fallbackKind;
|
|
418
|
+
const field = match?.[3] ?? "component";
|
|
419
|
+
return {
|
|
420
|
+
id: `component-schema-${String(index + 1).padStart(3, "0")}`,
|
|
421
|
+
severity: "blocking",
|
|
422
|
+
kind,
|
|
423
|
+
...(match?.[2] === undefined ? {} : { index: Number(match[2]) }),
|
|
424
|
+
field,
|
|
425
|
+
title: "组件信息还不完整",
|
|
426
|
+
message: "这项组件信息不符合当前插件格式。",
|
|
427
|
+
impact: "保存会停止,磁盘上的插件声明和关联文件保持不变。",
|
|
428
|
+
nextAction: "请修正标出的组件字段后重新预览。",
|
|
429
|
+
technicalPath: path,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function duplicateIssues(collections) {
|
|
433
|
+
const issues = [];
|
|
434
|
+
const check = (kind, values, field, label) => {
|
|
435
|
+
const first = new Map();
|
|
436
|
+
for (const [index, value] of values.entries()) {
|
|
437
|
+
const existing = first.get(value);
|
|
438
|
+
if (existing === undefined) {
|
|
439
|
+
first.set(value, index);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
issues.push({
|
|
443
|
+
id: `component-duplicate-${kind}-${index}`,
|
|
444
|
+
severity: "blocking",
|
|
445
|
+
kind,
|
|
446
|
+
index,
|
|
447
|
+
field,
|
|
448
|
+
title: `${label}重复`,
|
|
449
|
+
message: `第 ${existing + 1} 项与第 ${index + 1} 项使用了相同${label}。`,
|
|
450
|
+
impact: "CLI 与客户端都无法可靠定位要维护的具体组件。",
|
|
451
|
+
nextAction: `请为每项${label}使用不同内容。`,
|
|
452
|
+
technicalPath: `/components/${componentKey[kind]}/${index}/${field}`,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
check("skill", collections.skill.map((item) => item.name), "name", "Skill 名称");
|
|
457
|
+
check("skill", collections.skill.map((item) => item.path), "path", "Skill 路径");
|
|
458
|
+
check("mcp", collections.mcp.map((item) => item.name), "name", "MCP 名称");
|
|
459
|
+
check("lsp", collections.lsp.map((item) => item.name), "name", "语言服务名称");
|
|
460
|
+
check("hook", collections.hook.map((item) => item.command), "command", "Hook 文件");
|
|
461
|
+
return issues;
|
|
462
|
+
}
|
|
463
|
+
function semanticIssues(collections) {
|
|
464
|
+
const issues = duplicateIssues(collections);
|
|
465
|
+
for (const [index, lsp] of collections.lsp.entries()) {
|
|
466
|
+
if (lsp.startupTimeout != null && lsp.startupTimeout < 0) {
|
|
467
|
+
issues.push({
|
|
468
|
+
id: `component-lsp-timeout-${index}`,
|
|
469
|
+
severity: "blocking",
|
|
470
|
+
kind: "lsp",
|
|
471
|
+
index,
|
|
472
|
+
field: "startupTimeout",
|
|
473
|
+
title: "启动等待时间不能小于零",
|
|
474
|
+
message: "语言服务需要使用零或正数的启动等待时间。",
|
|
475
|
+
impact: "保存会停止,现有声明保持不变。",
|
|
476
|
+
nextAction: "请填写零或更大的毫秒数。",
|
|
477
|
+
technicalPath: `/components/lsp/${index}/startupTimeout`,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (lsp.maxRestarts != null && lsp.maxRestarts < 0) {
|
|
481
|
+
issues.push({
|
|
482
|
+
id: `component-lsp-restarts-${index}`,
|
|
483
|
+
severity: "blocking",
|
|
484
|
+
kind: "lsp",
|
|
485
|
+
index,
|
|
486
|
+
field: "maxRestarts",
|
|
487
|
+
title: "自动重试次数不能小于零",
|
|
488
|
+
message: "语言服务需要使用零或正数的重试次数。",
|
|
489
|
+
impact: "保存会停止,现有声明保持不变。",
|
|
490
|
+
nextAction: "请填写零或更大的次数。",
|
|
491
|
+
technicalPath: `/components/lsp/${index}/maxRestarts`,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
for (const [extension, language] of Object.entries(lsp.extensionToLanguage)) {
|
|
495
|
+
if (!extension.startsWith(".")) {
|
|
496
|
+
issues.push({
|
|
497
|
+
id: `component-lsp-extension-${index}-${issues.length}`,
|
|
498
|
+
severity: "blocking",
|
|
499
|
+
kind: "lsp",
|
|
500
|
+
index,
|
|
501
|
+
field: "extensionToLanguage",
|
|
502
|
+
title: "文件扩展名需要以点号开头",
|
|
503
|
+
message: `“${extension}”不是可识别的文件扩展名。`,
|
|
504
|
+
impact: "语言服务无法把文件映射到正确语言。",
|
|
505
|
+
nextAction: "请改为例如 .ts 或 .py 的形式。",
|
|
506
|
+
technicalPath: `/components/lsp/${index}/extensionToLanguage/${extension}`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
if (language.trim().length === 0) {
|
|
510
|
+
issues.push({
|
|
511
|
+
id: `component-lsp-language-${index}-${issues.length}`,
|
|
512
|
+
severity: "blocking",
|
|
513
|
+
kind: "lsp",
|
|
514
|
+
index,
|
|
515
|
+
field: "extensionToLanguage",
|
|
516
|
+
title: "语言标识不能为空",
|
|
517
|
+
message: `“${extension}”还没有对应的语言标识。`,
|
|
518
|
+
impact: "语言服务无法识别这类文件。",
|
|
519
|
+
nextAction: "请填写语言服务支持的 language id。",
|
|
520
|
+
technicalPath: `/components/lsp/${index}/extensionToLanguage/${extension}`,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return issues;
|
|
526
|
+
}
|
|
527
|
+
function permissions(path) {
|
|
528
|
+
const allowed = (mode) => {
|
|
529
|
+
try {
|
|
530
|
+
accessSync(path, mode);
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
return {
|
|
538
|
+
readable: allowed(fsConstants.R_OK),
|
|
539
|
+
writable: allowed(fsConstants.W_OK),
|
|
540
|
+
executable: allowed(fsConstants.X_OK),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function fileFact(authorization, input) {
|
|
544
|
+
const emptyPermissions = {
|
|
545
|
+
readable: false,
|
|
546
|
+
writable: false,
|
|
547
|
+
executable: false,
|
|
548
|
+
};
|
|
549
|
+
try {
|
|
550
|
+
const path = resolveAuthorizedPath(authorization, input.configuredPath);
|
|
551
|
+
if (!existsSync(path)) {
|
|
552
|
+
return {
|
|
553
|
+
...input,
|
|
554
|
+
state: "missing",
|
|
555
|
+
objectType: "unknown",
|
|
556
|
+
permissions: emptyPermissions,
|
|
557
|
+
canReveal: false,
|
|
558
|
+
canOpenExternally: false,
|
|
559
|
+
message: "关联对象不存在。",
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const stat = lstatSync(path);
|
|
563
|
+
const objectType = stat.isFile()
|
|
564
|
+
? "file"
|
|
565
|
+
: stat.isDirectory()
|
|
566
|
+
? "directory"
|
|
567
|
+
: "other";
|
|
568
|
+
const validType = objectType === input.expected;
|
|
569
|
+
const access = permissions(path);
|
|
570
|
+
return {
|
|
571
|
+
...input,
|
|
572
|
+
state: validType ? "present" : "wrong-type",
|
|
573
|
+
objectType,
|
|
574
|
+
permissions: access,
|
|
575
|
+
canReveal: validType,
|
|
576
|
+
canOpenExternally: validType && access.readable,
|
|
577
|
+
message: validType
|
|
578
|
+
? `对象存在;${access.readable ? "可读取" : "不可读取"},${access.writable ? "可写入" : "只读"}${input.expected === "file" ? `,${access.executable ? "可执行" : "不可执行"}` : ""}。`
|
|
579
|
+
: `关联位置不是${input.expected === "file" ? "普通文件" : "文件夹"}。`,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
const unsafe = error instanceof AuthorizedPathError &&
|
|
584
|
+
(error.code === "ABSOLUTE_PATH" ||
|
|
585
|
+
error.code === "OUTSIDE_AUTHORIZED_ROOT" ||
|
|
586
|
+
error.code === "UNSAFE_SYMLINK");
|
|
587
|
+
return {
|
|
588
|
+
...input,
|
|
589
|
+
state: unsafe ? "unsafe" : "unavailable",
|
|
590
|
+
objectType: "unknown",
|
|
591
|
+
permissions: emptyPermissions,
|
|
592
|
+
canReveal: false,
|
|
593
|
+
canOpenExternally: false,
|
|
594
|
+
message: unsafe
|
|
595
|
+
? "关联位置不是当前插件内的安全对象。"
|
|
596
|
+
: "暂时无法确认关联对象状态。",
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function externalCommandFact(kind, componentIndex, command) {
|
|
601
|
+
return {
|
|
602
|
+
kind,
|
|
603
|
+
componentIndex,
|
|
604
|
+
role: "command",
|
|
605
|
+
configuredPath: command,
|
|
606
|
+
state: "external-command",
|
|
607
|
+
objectType: "unknown",
|
|
608
|
+
permissions: { readable: false, writable: false, executable: false },
|
|
609
|
+
canReveal: false,
|
|
610
|
+
canOpenExternally: false,
|
|
611
|
+
message: "这是由系统环境提供的命令,不是插件内文件。",
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function inspectFiles(authorization, collections) {
|
|
615
|
+
const facts = [];
|
|
616
|
+
for (const [index, skill] of collections.skill.entries()) {
|
|
617
|
+
facts.push(fileFact(authorization, {
|
|
618
|
+
kind: "skill",
|
|
619
|
+
componentIndex: index,
|
|
620
|
+
role: "skill-directory",
|
|
621
|
+
configuredPath: skill.path,
|
|
622
|
+
expected: "directory",
|
|
623
|
+
}), fileFact(authorization, {
|
|
624
|
+
kind: "skill",
|
|
625
|
+
componentIndex: index,
|
|
626
|
+
role: "skill-document",
|
|
627
|
+
configuredPath: `${skill.path}/SKILL.md`,
|
|
628
|
+
expected: "file",
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
for (const [index, hook] of collections.hook.entries()) {
|
|
632
|
+
facts.push(fileFact(authorization, {
|
|
633
|
+
kind: "hook",
|
|
634
|
+
componentIndex: index,
|
|
635
|
+
role: "hook-script",
|
|
636
|
+
configuredPath: hook.command,
|
|
637
|
+
expected: "file",
|
|
638
|
+
}));
|
|
639
|
+
}
|
|
640
|
+
const commandFacts = (kind, values) => {
|
|
641
|
+
for (const [index, value] of values.entries()) {
|
|
642
|
+
if (!("command" in value))
|
|
643
|
+
continue;
|
|
644
|
+
facts.push(isPluginLocalPathReference(value.command)
|
|
645
|
+
? fileFact(authorization, {
|
|
646
|
+
kind,
|
|
647
|
+
componentIndex: index,
|
|
648
|
+
role: "command",
|
|
649
|
+
configuredPath: value.command,
|
|
650
|
+
expected: "file",
|
|
651
|
+
})
|
|
652
|
+
: externalCommandFact(kind, index, value.command));
|
|
653
|
+
for (const argument of value.args ?? []) {
|
|
654
|
+
if (!isPluginLocalPathReference(argument))
|
|
655
|
+
continue;
|
|
656
|
+
facts.push(fileFact(authorization, {
|
|
657
|
+
kind,
|
|
658
|
+
componentIndex: index,
|
|
659
|
+
role: "argument",
|
|
660
|
+
configuredPath: argument,
|
|
661
|
+
expected: "file",
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
if (kind === "lsp" &&
|
|
665
|
+
"workspaceFolder" in value &&
|
|
666
|
+
typeof value.workspaceFolder === "string" &&
|
|
667
|
+
value.workspaceFolder.length > 0) {
|
|
668
|
+
facts.push(fileFact(authorization, {
|
|
669
|
+
kind,
|
|
670
|
+
componentIndex: index,
|
|
671
|
+
role: "workspace-folder",
|
|
672
|
+
configuredPath: value.workspaceFolder,
|
|
673
|
+
expected: "directory",
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
commandFacts("mcp", collections.mcp);
|
|
679
|
+
commandFacts("lsp", collections.lsp);
|
|
680
|
+
return facts;
|
|
681
|
+
}
|
|
682
|
+
function localPathIssues(facts) {
|
|
683
|
+
return facts
|
|
684
|
+
.filter((fact) => fact.state === "unsafe")
|
|
685
|
+
.map((fact, index) => ({
|
|
686
|
+
id: `component-path-${String(index + 1).padStart(3, "0")}`,
|
|
687
|
+
severity: "blocking",
|
|
688
|
+
kind: fact.kind,
|
|
689
|
+
index: fact.componentIndex,
|
|
690
|
+
field: fact.role === "skill-directory"
|
|
691
|
+
? "path"
|
|
692
|
+
: fact.role === "workspace-folder"
|
|
693
|
+
? "workspaceFolder"
|
|
694
|
+
: fact.role === "command"
|
|
695
|
+
? "command"
|
|
696
|
+
: "args",
|
|
697
|
+
title: "关联位置不在当前插件内",
|
|
698
|
+
message: "绝对路径、越界路径和符号链接不能作为受控组件文件。",
|
|
699
|
+
impact: "保存和系统文件动作都会停止,避免访问未授权对象。",
|
|
700
|
+
nextAction: "请改用当前插件内的相对路径。",
|
|
701
|
+
technicalPath: `/components/${componentKey[fact.kind]}/${fact.componentIndex}`,
|
|
702
|
+
}));
|
|
703
|
+
}
|
|
704
|
+
function issuesFor(config, authorization) {
|
|
705
|
+
const collections = collectionsFrom(config);
|
|
706
|
+
const files = inspectFiles(authorization, collections);
|
|
707
|
+
return {
|
|
708
|
+
issues: [...semanticIssues(collections), ...localPathIssues(files)],
|
|
709
|
+
files,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
export function readPluginComponents(request, dependencies = {}) {
|
|
713
|
+
const loaded = loadInternal(request, dependencies);
|
|
714
|
+
if (loaded.status !== "loaded")
|
|
715
|
+
return loaded;
|
|
716
|
+
const inspected = issuesFor(loaded.value.config, loaded.value.pluginAuthorization);
|
|
717
|
+
return {
|
|
718
|
+
status: "loaded",
|
|
719
|
+
canonicalName: loaded.value.config.name,
|
|
720
|
+
revision: loaded.value.revision,
|
|
721
|
+
collections: collectionsFrom(loaded.value.config),
|
|
722
|
+
issues: inspected.issues,
|
|
723
|
+
files: inspected.files,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function mutationIndexIsValid(values, mutation) {
|
|
727
|
+
return (mutation.operation === "add" ||
|
|
728
|
+
(Number.isSafeInteger(mutation.index) &&
|
|
729
|
+
mutation.index >= 0 &&
|
|
730
|
+
mutation.index < values.length));
|
|
731
|
+
}
|
|
732
|
+
function candidateFor(loaded, mutation) {
|
|
733
|
+
const values = valuesForKind(loaded.config, mutation.kind);
|
|
734
|
+
if (!mutationIndexIsValid(values, mutation)) {
|
|
735
|
+
return {
|
|
736
|
+
status: "invalid",
|
|
737
|
+
message: "要维护的组件已经变化,请重新加载后再试。",
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (mutation.operation === "add")
|
|
741
|
+
values.push(structuredClone(mutation.value));
|
|
742
|
+
if (mutation.operation === "edit") {
|
|
743
|
+
values[mutation.index] = structuredClone(mutation.value);
|
|
744
|
+
}
|
|
745
|
+
if (mutation.operation === "remove")
|
|
746
|
+
values.splice(mutation.index, 1);
|
|
747
|
+
const candidate = structuredClone(loaded.config);
|
|
748
|
+
candidate.components[componentKey[mutation.kind]] = values;
|
|
749
|
+
return { status: "ready", config: candidate, values };
|
|
750
|
+
}
|
|
751
|
+
function scaffoldFor(loaded, mutation) {
|
|
752
|
+
if (mutation.operation !== "add" || !mutation.createScaffold)
|
|
753
|
+
return [];
|
|
754
|
+
if (mutation.kind !== "skill" && mutation.kind !== "hook")
|
|
755
|
+
return [];
|
|
756
|
+
const value = mutation.value;
|
|
757
|
+
if (mutation.kind === "skill") {
|
|
758
|
+
const skill = value;
|
|
759
|
+
const template = createAddedComponentScaffold("skill", skill.name)[0];
|
|
760
|
+
return template === undefined
|
|
761
|
+
? []
|
|
762
|
+
: [
|
|
763
|
+
{
|
|
764
|
+
...template,
|
|
765
|
+
relativePath: `${skill.path}/SKILL.md`,
|
|
766
|
+
},
|
|
767
|
+
];
|
|
768
|
+
}
|
|
769
|
+
const hook = value;
|
|
770
|
+
const stem = basename(hook.command).replace(/\.sh$/i, "") || "hook";
|
|
771
|
+
const template = createAddedComponentScaffold("hook", stem)[0];
|
|
772
|
+
return template === undefined
|
|
773
|
+
? []
|
|
774
|
+
: [{ ...template, relativePath: hook.command.replace(/^\.\//, "") }];
|
|
775
|
+
}
|
|
776
|
+
function generatedForKind(kind) {
|
|
777
|
+
return [
|
|
778
|
+
"plugin.json",
|
|
779
|
+
".claude-plugin/plugin.json",
|
|
780
|
+
".codex-plugin/plugin.json",
|
|
781
|
+
...(kind === "mcp" ? ["mcp.json", ".mcp.json"] : []),
|
|
782
|
+
...(kind === "hook" ? ["hooks/hooks.json"] : []),
|
|
783
|
+
...(kind === "lsp" ? [".lsp.json"] : []),
|
|
784
|
+
];
|
|
785
|
+
}
|
|
786
|
+
function componentLabel(kind) {
|
|
787
|
+
return { skill: "Skill", mcp: "MCP", hook: "Hook", lsp: "语言服务" }[kind];
|
|
788
|
+
}
|
|
789
|
+
function retainedFilesForRemoval(config, mutation) {
|
|
790
|
+
if (mutation.operation !== "remove")
|
|
791
|
+
return [];
|
|
792
|
+
const values = valuesForKind(config, mutation.kind);
|
|
793
|
+
const value = values[mutation.index];
|
|
794
|
+
if (value === undefined)
|
|
795
|
+
return [];
|
|
796
|
+
if (mutation.kind === "skill")
|
|
797
|
+
return [value.path];
|
|
798
|
+
if (mutation.kind === "hook")
|
|
799
|
+
return [value.command];
|
|
800
|
+
const commandValue = value;
|
|
801
|
+
if (!("command" in commandValue))
|
|
802
|
+
return [];
|
|
803
|
+
const command = commandValue.command;
|
|
804
|
+
return isPluginLocalPathReference(command) ? [command] : [];
|
|
805
|
+
}
|
|
806
|
+
function impactFor(loaded, pluginDirectoryName, mutation, scaffolds) {
|
|
807
|
+
const label = componentLabel(mutation.kind);
|
|
808
|
+
const verb = mutation.operation === "add"
|
|
809
|
+
? "添加"
|
|
810
|
+
: mutation.operation === "edit"
|
|
811
|
+
? "更新"
|
|
812
|
+
: "移除";
|
|
813
|
+
const retained = retainedFilesForRemoval(loaded.config, mutation);
|
|
814
|
+
return {
|
|
815
|
+
operation: mutation.operation,
|
|
816
|
+
kind: mutation.kind,
|
|
817
|
+
title: `${verb}${label}声明`,
|
|
818
|
+
summary: mutation.operation === "remove"
|
|
819
|
+
? `只从插件声明中移除这项${label};关联用户文件默认保留。`
|
|
820
|
+
: `${verb}这项${label}的源声明${scaffolds.length > 0 ? "及必要初始文件" : ""}。`,
|
|
821
|
+
canonicalChanges: [
|
|
822
|
+
`plugins/${pluginDirectoryName}/plugin.yaml 中的 ${label} 声明`,
|
|
823
|
+
],
|
|
824
|
+
generatedResults: generatedForKind(mutation.kind),
|
|
825
|
+
retainedUserFiles: retained,
|
|
826
|
+
scaffoldFiles: scaffolds.map((file) => file.relativePath),
|
|
827
|
+
generatedAction: "none",
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function planFingerprint(plan) {
|
|
831
|
+
const hash = createHash("sha256");
|
|
832
|
+
hash.update(plan.workspaceCanonicalPath);
|
|
833
|
+
hash.update("\0");
|
|
834
|
+
hash.update(plan.pluginDirectoryName);
|
|
835
|
+
hash.update("\0");
|
|
836
|
+
hash.update(plan.expectedRevision);
|
|
837
|
+
hash.update("\0");
|
|
838
|
+
hash.update(plan.nextBytes);
|
|
839
|
+
for (const file of plan.createFiles) {
|
|
840
|
+
hash.update("\0");
|
|
841
|
+
hash.update(file.relativePath);
|
|
842
|
+
hash.update("\0");
|
|
843
|
+
hash.update(String(file.mode));
|
|
844
|
+
hash.update("\0");
|
|
845
|
+
hash.update(file.bytes);
|
|
846
|
+
}
|
|
847
|
+
return hash.digest("hex");
|
|
848
|
+
}
|
|
849
|
+
export function planPluginComponentMutation(request, dependencies = {}) {
|
|
850
|
+
const loaded = loadInternal(request, dependencies);
|
|
851
|
+
if (loaded.status !== "loaded") {
|
|
852
|
+
return { ...loaded, issues: [] };
|
|
853
|
+
}
|
|
854
|
+
if (loaded.value.revision !== request.expectedRevision) {
|
|
855
|
+
return {
|
|
856
|
+
status: "conflict",
|
|
857
|
+
currentRevision: loaded.value.revision,
|
|
858
|
+
message: "插件声明已在外部变化;没有创建或修改任何文件。",
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const candidate = candidateFor(loaded.value, request.mutation);
|
|
862
|
+
if (candidate.status === "invalid") {
|
|
863
|
+
return {
|
|
864
|
+
status: "invalid",
|
|
865
|
+
message: candidate.message,
|
|
866
|
+
impact: "当前组件草稿和磁盘内容保持不变。",
|
|
867
|
+
nextAction: "请重新加载组件列表。",
|
|
868
|
+
issues: [],
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
const schema = validatePluginYamlValue(candidate.config);
|
|
872
|
+
const schemaProblems = schema.status === "valid"
|
|
873
|
+
? []
|
|
874
|
+
: schema.issues.map((issue, index) => schemaIssue(issue, index, request.mutation.kind));
|
|
875
|
+
const semantic = semanticIssues(collectionsFrom(candidate.config));
|
|
876
|
+
const candidateFiles = inspectFiles(loaded.value.pluginAuthorization, collectionsFrom(candidate.config));
|
|
877
|
+
const pathProblems = localPathIssues(candidateFiles);
|
|
878
|
+
const issues = [...schemaProblems, ...semantic, ...pathProblems];
|
|
879
|
+
if (issues.some((issue) => issue.severity === "blocking")) {
|
|
880
|
+
return {
|
|
881
|
+
status: "invalid",
|
|
882
|
+
message: "组件信息还有阻塞问题。",
|
|
883
|
+
impact: "没有修改 plugin.yaml 或关联文件。",
|
|
884
|
+
nextAction: issues[0]?.nextAction ?? "请修正组件信息后重试。",
|
|
885
|
+
issues,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const patched = patchComponentSequence(loaded.value, request.mutation);
|
|
889
|
+
if (patched.status === "unsafe") {
|
|
890
|
+
return {
|
|
891
|
+
status: "invalid",
|
|
892
|
+
message: "当前组件结构无法安全局部改写。",
|
|
893
|
+
impact: "客户端保留原文件,不会整份重写 plugin.yaml。",
|
|
894
|
+
nextAction: "请在外部编辑器中整理该组件数组后重新加载。",
|
|
895
|
+
issues: [
|
|
896
|
+
{
|
|
897
|
+
id: "component-unsafe-structure",
|
|
898
|
+
severity: "blocking",
|
|
899
|
+
kind: request.mutation.kind,
|
|
900
|
+
field: "component",
|
|
901
|
+
title: "组件结构需要外部处理",
|
|
902
|
+
message: patched.message,
|
|
903
|
+
impact: "结构化保存已停止。",
|
|
904
|
+
nextAction: "请在外部编辑器中处理后重新加载。",
|
|
905
|
+
technicalPath: `/components/${componentKey[request.mutation.kind]}`,
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
const reparsed = parsePluginYamlSource(patched.bytes
|
|
911
|
+
.subarray(patched.bytes.subarray(0, 3).equals(UTF8_BOM) ? 3 : 0)
|
|
912
|
+
.toString("utf8"));
|
|
913
|
+
if (reparsed.status !== "valid") {
|
|
914
|
+
return {
|
|
915
|
+
status: "invalid",
|
|
916
|
+
message: "组件局部修改后的文档无法重新验证。",
|
|
917
|
+
impact: "没有修改磁盘文件。",
|
|
918
|
+
nextAction: "请检查组件字段或在外部修正源文件。",
|
|
919
|
+
issues,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
const scaffolds = scaffoldFor(loaded.value, request.mutation);
|
|
923
|
+
for (const file of scaffolds) {
|
|
924
|
+
try {
|
|
925
|
+
const path = resolveAuthorizedPath(loaded.value.pluginAuthorization, file.relativePath);
|
|
926
|
+
if (existsSync(path)) {
|
|
927
|
+
return {
|
|
928
|
+
status: "invalid",
|
|
929
|
+
message: "需要创建的初始文件已经存在。",
|
|
930
|
+
impact: "客户端不会覆盖已有用户文件;plugin.yaml 保持不变。",
|
|
931
|
+
nextAction: "请保留现有文件并关闭“创建初始文件”,或选择其他组件位置。",
|
|
932
|
+
issues: [],
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return {
|
|
938
|
+
status: "invalid",
|
|
939
|
+
message: "初始文件位置不在当前插件的安全范围内。",
|
|
940
|
+
impact: "没有修改任何文件。",
|
|
941
|
+
nextAction: "请改用当前插件内的相对路径。",
|
|
942
|
+
issues: [],
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const createFiles = scaffolds.map((file) => ({
|
|
947
|
+
relativePath: `plugins/${request.pluginDirectoryName}/${file.relativePath}`,
|
|
948
|
+
bytes: Buffer.from(file.bytes),
|
|
949
|
+
mode: file.mode,
|
|
950
|
+
}));
|
|
951
|
+
const withoutFingerprint = {
|
|
952
|
+
workspaceCanonicalPath: loaded.value.workspaceCanonicalPath,
|
|
953
|
+
pluginDirectoryName: request.pluginDirectoryName,
|
|
954
|
+
expectedRevision: request.expectedRevision,
|
|
955
|
+
nextBytes: Buffer.from(patched.bytes),
|
|
956
|
+
createFiles,
|
|
957
|
+
impact: impactFor(loaded.value, request.pluginDirectoryName, request.mutation, scaffolds),
|
|
958
|
+
};
|
|
959
|
+
const plan = {
|
|
960
|
+
...withoutFingerprint,
|
|
961
|
+
fingerprint: planFingerprint(withoutFingerprint),
|
|
962
|
+
};
|
|
963
|
+
return { status: "planned", plan, issues };
|
|
964
|
+
}
|
|
965
|
+
export function executePluginComponentPlan(plan, dependencies = {}) {
|
|
966
|
+
const { fingerprint: _fingerprint, ...withoutFingerprint } = plan;
|
|
967
|
+
if (planFingerprint(withoutFingerprint) !== plan.fingerprint) {
|
|
968
|
+
return {
|
|
969
|
+
status: "failed",
|
|
970
|
+
message: "组件变更计划已失效;没有写入文件。",
|
|
971
|
+
diskChanged: false,
|
|
972
|
+
changedPaths: [],
|
|
973
|
+
cleanupComplete: true,
|
|
974
|
+
transaction: {
|
|
975
|
+
canonical: "unchanged",
|
|
976
|
+
scaffolds: "none",
|
|
977
|
+
cleanupComplete: true,
|
|
978
|
+
},
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
const result = commitAuthorizedDocumentAndFiles({
|
|
982
|
+
directory: plan.workspaceCanonicalPath,
|
|
983
|
+
relativePath: pluginDocumentRelativePath(plan.pluginDirectoryName),
|
|
984
|
+
expectedRevision: plan.expectedRevision,
|
|
985
|
+
nextBytes: plan.nextBytes,
|
|
986
|
+
createFiles: plan.createFiles,
|
|
987
|
+
}, dependencies.commitDependencies);
|
|
988
|
+
if (result.status === "committed" || result.status === "verified") {
|
|
989
|
+
return {
|
|
990
|
+
status: "saved",
|
|
991
|
+
revision: result.revision,
|
|
992
|
+
changedPaths: result.changedPaths,
|
|
993
|
+
diskChanged: result.diskChanged,
|
|
994
|
+
cleanupComplete: true,
|
|
995
|
+
transaction: result.transaction,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
return result;
|
|
999
|
+
}
|
|
1000
|
+
//# sourceMappingURL=plugin-components.js.map
|