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,1114 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, lstatSync, readFileSync, readdirSync, statSync, } from 'node:fs';
|
|
2
|
+
import { validateHeaderName, validateHeaderValue } from 'node:http';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
|
+
import { buildClaudeHooksConfig, buildClaudeManifest, buildClaudeMcpConfig, buildLspConfig, } from '../adapters/claude.js';
|
|
7
|
+
import { buildCodexManifest } from '../adapters/codex.js';
|
|
8
|
+
import { buildAgentPluginManifest, buildAgentPluginMcpConfig, } from '../adapters/agent-plugins.js';
|
|
9
|
+
import { isStdioMcp } from '../adapters/mcp.js';
|
|
10
|
+
import { AuthorizedPathError, authorizeExistingDirectory, resolveAuthorizedPath, } from '../infrastructure/authorized-path.js';
|
|
11
|
+
import { readMarketplaceMetadata } from './marketplace.js';
|
|
12
|
+
import { parsePluginYamlSource } from './plugin-source.js';
|
|
13
|
+
import { readSkillDocument } from './skill-document.js';
|
|
14
|
+
class HealthCollector {
|
|
15
|
+
issues = [];
|
|
16
|
+
diagnostics = [];
|
|
17
|
+
validationDiagnostics = [];
|
|
18
|
+
add(copy, plugin, options = {}) {
|
|
19
|
+
const diagnosticId = `diagnostic-${String(this.diagnostics.length + 1).padStart(3, '0')}`;
|
|
20
|
+
const issueId = `issue-${String(this.issues.length + 1).padStart(3, '0')}`;
|
|
21
|
+
this.diagnostics.push({
|
|
22
|
+
id: diagnosticId,
|
|
23
|
+
code: copy.code,
|
|
24
|
+
message: copy.diagnosticMessage,
|
|
25
|
+
});
|
|
26
|
+
const issue = {
|
|
27
|
+
id: issueId,
|
|
28
|
+
dimension: copy.dimension,
|
|
29
|
+
severity: copy.severity,
|
|
30
|
+
scope: copy.scope,
|
|
31
|
+
title: copy.title,
|
|
32
|
+
summary: copy.summary,
|
|
33
|
+
impact: copy.impact,
|
|
34
|
+
nextAction: copy.nextAction,
|
|
35
|
+
diagnosticRef: diagnosticId,
|
|
36
|
+
};
|
|
37
|
+
this.issues.push(issue);
|
|
38
|
+
if (plugin !== undefined) {
|
|
39
|
+
plugin.issueIds.push(issueId);
|
|
40
|
+
plugin.diagnosticRefs.push(diagnosticId);
|
|
41
|
+
if (options.includeInValidation !== false) {
|
|
42
|
+
this.addValidationDiagnostic(plugin, copy.code, copy.diagnosticMessage);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return issue;
|
|
46
|
+
}
|
|
47
|
+
addValidationDiagnostic(plugin, code, message) {
|
|
48
|
+
this.validationDiagnostics.push({
|
|
49
|
+
pluginDirectoryName: plugin.directoryName,
|
|
50
|
+
code,
|
|
51
|
+
message,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function mapAuthorizationFailure(error) {
|
|
56
|
+
const code = error.code === 'NOT_A_DIRECTORY'
|
|
57
|
+
? 'NOT_A_DIRECTORY'
|
|
58
|
+
: error.code === 'UNSAFE_SYMLINK'
|
|
59
|
+
? 'UNSAFE_SYMLINK'
|
|
60
|
+
: error.code === 'PATH_UNAVAILABLE'
|
|
61
|
+
? 'PATH_UNAVAILABLE'
|
|
62
|
+
: 'PATH_NOT_FOUND';
|
|
63
|
+
return {
|
|
64
|
+
status: 'unavailable',
|
|
65
|
+
error: {
|
|
66
|
+
code,
|
|
67
|
+
title: code === 'UNSAFE_SYMLINK'
|
|
68
|
+
? '不能检查这个链接目录'
|
|
69
|
+
: code === 'NOT_A_DIRECTORY'
|
|
70
|
+
? '请选择一个文件夹'
|
|
71
|
+
: '无法检查这个位置',
|
|
72
|
+
message: code === 'UNSAFE_SYMLINK'
|
|
73
|
+
? '所选位置使用了符号链接,无法确认它仍在授权范围内。'
|
|
74
|
+
: '系统当前无法只读检查所选文件夹。',
|
|
75
|
+
impact: '检查已停止,没有创建、修复、格式化或生成任何文件。',
|
|
76
|
+
nextAction: '请检查文件夹位置与权限后重试,或重新选择其他文件夹。',
|
|
77
|
+
technicalDetail: error.message,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function unavailableFromUnexpectedFailure(error) {
|
|
82
|
+
return {
|
|
83
|
+
status: 'unavailable',
|
|
84
|
+
error: {
|
|
85
|
+
code: 'PATH_UNAVAILABLE',
|
|
86
|
+
title: '检查过程中无法继续读取',
|
|
87
|
+
message: '文件夹内容在检查期间发生变化,或系统暂时无法读取必要内容。',
|
|
88
|
+
impact: '检查已停止,没有创建、修复、格式化或生成任何文件。',
|
|
89
|
+
nextAction: '请确认文件夹仍可访问,然后重新检查。',
|
|
90
|
+
technicalDetail: error instanceof Error ? error.message : String(error),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function diagnostic(collector, plugin, copy, options = {}) {
|
|
95
|
+
return collector.add({
|
|
96
|
+
...copy,
|
|
97
|
+
scope: {
|
|
98
|
+
...copy.scope,
|
|
99
|
+
pluginId: plugin.id,
|
|
100
|
+
},
|
|
101
|
+
}, plugin, options);
|
|
102
|
+
}
|
|
103
|
+
function unavailableSourceProblem(collector, plugin, code, rawMessage, copy) {
|
|
104
|
+
plugin.source = 'invalid';
|
|
105
|
+
if (plugin.references === 'valid') {
|
|
106
|
+
plugin.references = 'unknown';
|
|
107
|
+
}
|
|
108
|
+
plugin.generated = 'unknown';
|
|
109
|
+
return diagnostic(collector, plugin, {
|
|
110
|
+
dimension: 'source',
|
|
111
|
+
severity: 'blocking',
|
|
112
|
+
code,
|
|
113
|
+
diagnosticMessage: rawMessage,
|
|
114
|
+
scope: {
|
|
115
|
+
kind: copy.field
|
|
116
|
+
? 'field'
|
|
117
|
+
: copy.relativePath
|
|
118
|
+
? 'path'
|
|
119
|
+
: 'plugin',
|
|
120
|
+
label: plugin.displayName,
|
|
121
|
+
...(copy.field === undefined ? {} : { field: copy.field }),
|
|
122
|
+
...(copy.relativePath === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { relativePath: copy.relativePath }),
|
|
125
|
+
},
|
|
126
|
+
title: copy.title,
|
|
127
|
+
summary: copy.summary,
|
|
128
|
+
impact: '客户端无法可靠判断这个插件的组件与生成结果。',
|
|
129
|
+
nextAction: '请在外部编辑器中修正源信息,然后重新检查。',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function semanticSourceProblem(collector, plugin, code, rawMessage, copy, options = {}) {
|
|
133
|
+
plugin.source = 'invalid';
|
|
134
|
+
plugin.generated = 'unknown';
|
|
135
|
+
return diagnostic(collector, plugin, {
|
|
136
|
+
dimension: 'source',
|
|
137
|
+
severity: 'blocking',
|
|
138
|
+
code,
|
|
139
|
+
diagnosticMessage: rawMessage,
|
|
140
|
+
scope: {
|
|
141
|
+
kind: copy.field
|
|
142
|
+
? 'field'
|
|
143
|
+
: copy.relativePath
|
|
144
|
+
? 'path'
|
|
145
|
+
: 'plugin',
|
|
146
|
+
label: plugin.displayName,
|
|
147
|
+
...(copy.field === undefined ? {} : { field: copy.field }),
|
|
148
|
+
...(copy.relativePath === undefined
|
|
149
|
+
? {}
|
|
150
|
+
: { relativePath: copy.relativePath }),
|
|
151
|
+
},
|
|
152
|
+
title: copy.title,
|
|
153
|
+
summary: copy.summary,
|
|
154
|
+
impact: '客户端仍会独立检查本地内容,但不会把平台生成结果判为最新。',
|
|
155
|
+
nextAction: '请在外部编辑器中修正源信息,然后重新检查。',
|
|
156
|
+
}, options);
|
|
157
|
+
}
|
|
158
|
+
function referenceProblem(collector, plugin, code, rawMessage, title, summary, relativePath) {
|
|
159
|
+
plugin.references = 'invalid';
|
|
160
|
+
diagnostic(collector, plugin, {
|
|
161
|
+
dimension: 'reference',
|
|
162
|
+
severity: 'blocking',
|
|
163
|
+
code,
|
|
164
|
+
diagnosticMessage: rawMessage,
|
|
165
|
+
scope: {
|
|
166
|
+
kind: 'path',
|
|
167
|
+
label: plugin.displayName,
|
|
168
|
+
relativePath,
|
|
169
|
+
},
|
|
170
|
+
title,
|
|
171
|
+
summary,
|
|
172
|
+
impact: '这个插件引用的本地内容当前不能安全使用。',
|
|
173
|
+
nextAction: '请在外部编辑器中修正引用位置或补齐对应内容,然后重新检查。',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function generatedProblem(collector, plugin, state, code, rawMessage, displayLabel, relativePath, includeInHealth = true) {
|
|
177
|
+
if (!includeInHealth) {
|
|
178
|
+
collector.addValidationDiagnostic(plugin, code, rawMessage);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (state === 'stale' || plugin.generated !== 'stale') {
|
|
182
|
+
plugin.generated = state;
|
|
183
|
+
}
|
|
184
|
+
diagnostic(collector, plugin, {
|
|
185
|
+
dimension: 'generated',
|
|
186
|
+
severity: 'attention',
|
|
187
|
+
code,
|
|
188
|
+
diagnosticMessage: rawMessage,
|
|
189
|
+
scope: {
|
|
190
|
+
kind: 'path',
|
|
191
|
+
label: plugin.displayName,
|
|
192
|
+
relativePath,
|
|
193
|
+
},
|
|
194
|
+
title: state === 'missing'
|
|
195
|
+
? `${displayLabel}还没有生成`
|
|
196
|
+
: `${displayLabel}需要更新`,
|
|
197
|
+
summary: state === 'missing'
|
|
198
|
+
? `“${plugin.displayName}”缺少这项平台结果。`
|
|
199
|
+
: `“${plugin.displayName}”的这项平台结果与当前源信息不一致。`,
|
|
200
|
+
impact: '源信息仍然有效,但使用这项生成结果可能得到缺失或过期内容。',
|
|
201
|
+
nextAction: '请先查看问题范围;当前只读检查不会自动生成或改写文件。',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function newPlugin(directoryName) {
|
|
205
|
+
return {
|
|
206
|
+
id: `plugin:${directoryName}`,
|
|
207
|
+
directoryName,
|
|
208
|
+
displayName: directoryName,
|
|
209
|
+
componentCount: 0,
|
|
210
|
+
componentKinds: [],
|
|
211
|
+
platforms: [],
|
|
212
|
+
source: 'valid',
|
|
213
|
+
references: 'valid',
|
|
214
|
+
generated: 'fresh',
|
|
215
|
+
issueIds: [],
|
|
216
|
+
diagnosticRefs: [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function listComponentKinds(config) {
|
|
220
|
+
const kinds = [];
|
|
221
|
+
if (config.components.skills?.length)
|
|
222
|
+
kinds.push('Skill');
|
|
223
|
+
if (config.components.mcp?.length)
|
|
224
|
+
kinds.push('MCP');
|
|
225
|
+
if (config.components.hooks?.length)
|
|
226
|
+
kinds.push('Hook');
|
|
227
|
+
if (config.components.lsp?.length)
|
|
228
|
+
kinds.push('LSP');
|
|
229
|
+
return kinds;
|
|
230
|
+
}
|
|
231
|
+
function componentCount(config) {
|
|
232
|
+
return ((config.components.skills?.length ?? 0) +
|
|
233
|
+
(config.components.mcp?.length ?? 0) +
|
|
234
|
+
(config.components.hooks?.length ?? 0) +
|
|
235
|
+
(config.components.lsp?.length ?? 0));
|
|
236
|
+
}
|
|
237
|
+
export function isPluginLocalPathReference(value) {
|
|
238
|
+
return (value.startsWith('./') ||
|
|
239
|
+
value.startsWith('../') ||
|
|
240
|
+
value.startsWith('/') ||
|
|
241
|
+
value.startsWith('hooks/') ||
|
|
242
|
+
value.startsWith('mcp/') ||
|
|
243
|
+
value.startsWith('skills/'));
|
|
244
|
+
}
|
|
245
|
+
function resolvePluginReference(authorization, plugin, collector, configuredPath, label) {
|
|
246
|
+
try {
|
|
247
|
+
return resolveAuthorizedPath(authorization, configuredPath);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (!(error instanceof AuthorizedPathError))
|
|
251
|
+
throw error;
|
|
252
|
+
const rawMessage = error.code === 'ABSOLUTE_PATH'
|
|
253
|
+
? `${label} 不应使用绝对路径: ${configuredPath}`
|
|
254
|
+
: error.code === 'OUTSIDE_AUTHORIZED_ROOT'
|
|
255
|
+
? `${label} 越过插件目录: ${configuredPath}`
|
|
256
|
+
: error.code === 'UNSAFE_SYMLINK'
|
|
257
|
+
? `${label} 不允许使用符号链接: ${configuredPath}`
|
|
258
|
+
: `${label} 无法确认授权范围: ${configuredPath}`;
|
|
259
|
+
referenceProblem(collector, plugin, `REFERENCE_${error.code}`, rawMessage, '组件引用超出安全范围', `“${plugin.displayName}”中的本地引用无法在已授权插件范围内确认。`, configuredPath);
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function validateExecutable(path, displayPath, plugin, collector) {
|
|
264
|
+
try {
|
|
265
|
+
const stat = statSync(path);
|
|
266
|
+
if (!stat.isFile()) {
|
|
267
|
+
referenceProblem(collector, plugin, 'HOOK_NOT_FILE', `Hook 脚本不是文件: ${displayPath}`, 'Hook 内容不是可运行文件', `“${plugin.displayName}”引用的 Hook 位置不是文件。`, displayPath);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
accessSync(path, constants.X_OK);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
referenceProblem(collector, plugin, 'HOOK_NOT_EXECUTABLE', `Hook 脚本不可执行: ${displayPath}`, 'Hook 文件当前不能运行', `“${plugin.displayName}”引用的 Hook 文件没有可执行权限。`, displayPath);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const AGENT_SKILL_NAME_PATTERN = /^(?!.*--)[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
277
|
+
function validatePortableSkills(authorization, config, plugin, collector) {
|
|
278
|
+
const declaredDirectories = new Set();
|
|
279
|
+
for (const skill of config.components.skills ?? []) {
|
|
280
|
+
const portableMatch = /^skills\/([^/]+)$/.exec(skill.path);
|
|
281
|
+
if (portableMatch === null) {
|
|
282
|
+
referenceProblem(collector, plugin, 'SKILL_PATH_NOT_PORTABLE', `Agent Plugins Skill 必须是 skills/ 下的直接子目录: ${skill.path}`, 'Skill 不在可移植发现位置', `“${skill.name}”不会被 Agent Plugins 客户端从固定位置发现。`, skill.path);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const directoryName = portableMatch[1];
|
|
286
|
+
if (declaredDirectories.has(directoryName)) {
|
|
287
|
+
referenceProblem(collector, plugin, 'SKILL_DECLARATION_DUPLICATE', `Skill 目录被重复声明: ${skill.path}`, 'Skill 目录被重复声明', `“${directoryName}”只能映射到一个可移植 Skill。`, skill.path);
|
|
288
|
+
}
|
|
289
|
+
declaredDirectories.add(directoryName);
|
|
290
|
+
if (skill.name !== directoryName) {
|
|
291
|
+
referenceProblem(collector, plugin, 'SKILL_NAME_MISMATCH', `Skill 声明名称 (${skill.name}) 与父目录 (${directoryName}) 不一致`, 'Skill 名称与文件夹不一致', `“${skill.name}”不能以“${directoryName}”作为 Agent Skills 父目录。`, `${skill.path}/SKILL.md`);
|
|
292
|
+
}
|
|
293
|
+
const document = readSkillDocument({
|
|
294
|
+
pluginDirectory: authorization.canonicalPath,
|
|
295
|
+
skillDirectory: skill.path,
|
|
296
|
+
});
|
|
297
|
+
if (document.status === 'unavailable') {
|
|
298
|
+
if (document.problem.code === 'SKILL_UNAVAILABLE') {
|
|
299
|
+
referenceProblem(collector, plugin, 'SKILL_DOCUMENT_UNAVAILABLE', `SKILL.md 无法作为普通文本文件读取: ${skill.path}/SKILL.md: ${document.problem.message}`, 'Skill 正文当前无法读取', `“${skill.name}”的正文尚未完成 Agent Skills 契约检查。`, `${skill.path}/SKILL.md`);
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (document.status === 'uninterpretable') {
|
|
304
|
+
referenceProblem(collector, plugin, document.problem.code === 'INVALID_ENCODING'
|
|
305
|
+
? 'SKILL_ENCODING_INVALID'
|
|
306
|
+
: 'SKILL_FRONTMATTER_INVALID', `SKILL.md 无法解释: ${skill.path}/SKILL.md: ${document.problem.message}`, 'Skill 正文无法按 Agent Skills 读取', `“${skill.name}”的正文编码或 frontmatter 无效。`, `${skill.path}/SKILL.md`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const frontmatter = document.document.draft.frontmatter;
|
|
310
|
+
if (frontmatter.name.length === 0) {
|
|
311
|
+
referenceProblem(collector, plugin, 'SKILL_NAME_REQUIRED', `SKILL.md frontmatter 缺少 name: ${skill.path}/SKILL.md`, 'Skill 缺少名称', `“${skill.name}”的 frontmatter 必须声明 name。`, `${skill.path}/SKILL.md`);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
if (!AGENT_SKILL_NAME_PATTERN.test(frontmatter.name)) {
|
|
315
|
+
referenceProblem(collector, plugin, 'SKILL_NAME_INVALID', `SKILL.md name 不符合 Agent Skills 命名规则: ${frontmatter.name}`, 'Skill 名称不符合可移植格式', `“${frontmatter.name}”必须使用 1–64 位小写字母、数字或单连字符。`, `${skill.path}/SKILL.md`);
|
|
316
|
+
}
|
|
317
|
+
if (frontmatter.name !== directoryName) {
|
|
318
|
+
referenceProblem(collector, plugin, 'SKILL_NAME_MISMATCH', `SKILL.md name (${frontmatter.name}) 与父目录 (${directoryName}) 不一致`, 'Skill 名称与文件夹不一致', `“${frontmatter.name}”必须与父目录“${directoryName}”完全相同。`, `${skill.path}/SKILL.md`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (frontmatter.description.trim().length === 0) {
|
|
322
|
+
referenceProblem(collector, plugin, 'SKILL_DESCRIPTION_REQUIRED', `SKILL.md frontmatter 缺少 description: ${skill.path}/SKILL.md`, 'Skill 缺少说明', `“${skill.name}”的 frontmatter 必须声明非空 description。`, `${skill.path}/SKILL.md`);
|
|
323
|
+
}
|
|
324
|
+
else if (Array.from(frontmatter.description).length > 1024) {
|
|
325
|
+
referenceProblem(collector, plugin, 'SKILL_DESCRIPTION_TOO_LONG', `SKILL.md description 超过 1024 字符: ${skill.path}/SKILL.md`, 'Skill 说明过长', `“${skill.name}”的 description 超出 Agent Skills 上限。`, `${skill.path}/SKILL.md`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
let skillsDirectory;
|
|
329
|
+
try {
|
|
330
|
+
skillsDirectory = resolveAuthorizedPath(authorization, 'skills');
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
referenceProblem(collector, plugin, 'SKILL_DISCOVERY_UNSAFE', error instanceof Error ? error.message : String(error), 'Skill 发现目录不安全', `“${plugin.displayName}”的 skills/ 无法在授权范围内扫描。`, 'skills');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (!existsSync(skillsDirectory))
|
|
337
|
+
return;
|
|
338
|
+
if (!lstatSync(skillsDirectory).isDirectory()) {
|
|
339
|
+
referenceProblem(collector, plugin, 'SKILLS_NOT_DIRECTORY', 'Agent Plugins skills 位置不是目录: skills', 'Skill 发现位置不是文件夹', `“${plugin.displayName}”无法从固定 skills/ 位置发现组件。`, 'skills');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
for (const entry of readdirSync(skillsDirectory, { withFileTypes: true })) {
|
|
343
|
+
if (entry.isSymbolicLink()) {
|
|
344
|
+
referenceProblem(collector, plugin, 'SKILL_DISCOVERY_SYMLINK', `Agent Plugins Skill 目录不允许使用符号链接: skills/${entry.name}`, 'Skill 发现位置使用了链接', `“${entry.name}”无法确认仍在插件包内。`, `skills/${entry.name}`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!entry.isDirectory())
|
|
348
|
+
continue;
|
|
349
|
+
const documentPath = join(skillsDirectory, entry.name, 'SKILL.md');
|
|
350
|
+
if (!existsSync(documentPath))
|
|
351
|
+
continue;
|
|
352
|
+
if (!declaredDirectories.has(entry.name)) {
|
|
353
|
+
referenceProblem(collector, plugin, 'SKILL_UNDECLARED', `发现未在 plugin.yaml 声明的 Agent Plugins Skill: skills/${entry.name}/SKILL.md`, '发现未声明的 Skill', `“${entry.name}”会被协议发现,但不在 canonical 组件声明中。`, `skills/${entry.name}/SKILL.md`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function pathSegmentsRemainContained(path) {
|
|
358
|
+
if (path.includes('\\'))
|
|
359
|
+
return false;
|
|
360
|
+
let depth = 0;
|
|
361
|
+
for (const segment of path.split('/')) {
|
|
362
|
+
if (segment.length === 0 || segment === '.')
|
|
363
|
+
continue;
|
|
364
|
+
if (segment === '..') {
|
|
365
|
+
if (depth === 0)
|
|
366
|
+
return false;
|
|
367
|
+
depth -= 1;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
depth += 1;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
function validPortableCommand(command) {
|
|
376
|
+
if (command.startsWith('./')) {
|
|
377
|
+
return !command.includes('${') && pathSegmentsRemainContained(command.slice(2));
|
|
378
|
+
}
|
|
379
|
+
return (command.length > 0 &&
|
|
380
|
+
command !== '.' &&
|
|
381
|
+
command !== '..' &&
|
|
382
|
+
!/[\s/\\]/u.test(command) &&
|
|
383
|
+
!command.includes('${'));
|
|
384
|
+
}
|
|
385
|
+
function validPortableCwd(cwd) {
|
|
386
|
+
if (cwd.startsWith('./'))
|
|
387
|
+
return pathSegmentsRemainContained(cwd.slice(2));
|
|
388
|
+
for (const prefix of ['${PLUGIN_ROOT}', '${PLUGIN_DATA}']) {
|
|
389
|
+
if (cwd === prefix)
|
|
390
|
+
return true;
|
|
391
|
+
if (cwd.startsWith(`${prefix}/`)) {
|
|
392
|
+
return pathSegmentsRemainContained(cwd.slice(prefix.length + 1));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
function pluginRootCwdReference(cwd) {
|
|
398
|
+
if (cwd.startsWith('./'))
|
|
399
|
+
return cwd;
|
|
400
|
+
if (cwd === '${PLUGIN_ROOT}')
|
|
401
|
+
return '.';
|
|
402
|
+
if (cwd.startsWith('${PLUGIN_ROOT}/')) {
|
|
403
|
+
return `./${cwd.slice('${PLUGIN_ROOT}/'.length)}`;
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
function isLoopbackHostname(hostname) {
|
|
408
|
+
const unwrapped = hostname.startsWith('[') && hostname.endsWith(']')
|
|
409
|
+
? hostname.slice(1, -1)
|
|
410
|
+
: hostname;
|
|
411
|
+
if (unwrapped === 'localhost')
|
|
412
|
+
return true;
|
|
413
|
+
const family = isIP(unwrapped);
|
|
414
|
+
if (family === 4)
|
|
415
|
+
return unwrapped.startsWith('127.');
|
|
416
|
+
return family === 6 && (unwrapped === '::1' || unwrapped === '0:0:0:0:0:0:0:1');
|
|
417
|
+
}
|
|
418
|
+
function validatePortableMcp(authorization, config, plugin, collector) {
|
|
419
|
+
const names = new Set();
|
|
420
|
+
for (const mcp of config.components.mcp ?? []) {
|
|
421
|
+
if (names.has(mcp.name)) {
|
|
422
|
+
semanticSourceProblem(collector, plugin, 'MCP_DUPLICATE_NAME', `MCP server name 重复: ${mcp.name}`, {
|
|
423
|
+
title: 'MCP server 名称重复',
|
|
424
|
+
summary: `“${plugin.displayName}”中有多个 server 使用“${mcp.name}”。`,
|
|
425
|
+
field: 'components.mcp.name',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
names.add(mcp.name);
|
|
429
|
+
if (isStdioMcp(mcp)) {
|
|
430
|
+
if (!validPortableCommand(mcp.command)) {
|
|
431
|
+
semanticSourceProblem(collector, plugin, 'MCP_COMMAND_INVALID', `MCP command 必须是单个 bare executable 或 ./ 相对路径: ${mcp.command}`, {
|
|
432
|
+
title: 'MCP 启动命令不可移植',
|
|
433
|
+
summary: `“${mcp.name}”的 command 不是 Agent Plugins 允许的单个 token。`,
|
|
434
|
+
field: 'components.mcp.command',
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (typeof mcp.cwd === 'string') {
|
|
438
|
+
if (!validPortableCwd(mcp.cwd)) {
|
|
439
|
+
semanticSourceProblem(collector, plugin, 'MCP_CWD_UNSAFE', `MCP cwd 形式无效或越出协议根目录: ${mcp.cwd}`, {
|
|
440
|
+
title: 'MCP 工作目录不安全',
|
|
441
|
+
summary: `“${mcp.name}”的 cwd 不能安全解析到 PLUGIN_ROOT 或 PLUGIN_DATA。`,
|
|
442
|
+
field: 'components.mcp.cwd',
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
const configuredPath = pluginRootCwdReference(mcp.cwd);
|
|
447
|
+
if (configuredPath !== undefined) {
|
|
448
|
+
const cwd = resolvePluginReference(authorization, plugin, collector, configuredPath, 'MCP cwd');
|
|
449
|
+
if (cwd !== undefined && !existsSync(cwd)) {
|
|
450
|
+
referenceProblem(collector, plugin, 'MCP_CWD_MISSING', `MCP cwd 不存在: ${mcp.cwd}`, '找不到 MCP 工作目录', `“${mcp.name}”的插件内工作目录不存在。`, mcp.cwd);
|
|
451
|
+
}
|
|
452
|
+
else if (cwd !== undefined && !lstatSync(cwd).isDirectory()) {
|
|
453
|
+
referenceProblem(collector, plugin, 'MCP_CWD_NOT_DIRECTORY', `MCP cwd 不是目录: ${mcp.cwd}`, 'MCP 工作目录不是文件夹', `“${mcp.name}”的 cwd 不能作为进程工作目录。`, mcp.cwd);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
for (const key of Object.keys(mcp.env ?? {})) {
|
|
459
|
+
if (key === 'PLUGIN_ROOT' || key === 'PLUGIN_DATA') {
|
|
460
|
+
semanticSourceProblem(collector, plugin, 'MCP_ENV_RESERVED', `MCP env 不得覆盖客户端保留变量: ${key}`, {
|
|
461
|
+
title: 'MCP 环境变量占用了保留名称',
|
|
462
|
+
summary: `“${mcp.name}”必须由客户端提供 ${key}。`,
|
|
463
|
+
field: 'components.mcp.env',
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
let url;
|
|
470
|
+
try {
|
|
471
|
+
url = new URL(mcp.url);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
semanticSourceProblem(collector, plugin, 'MCP_REMOTE_URL_INVALID', `远端 MCP URL 不是绝对 URL: ${mcp.url}`, {
|
|
475
|
+
title: '远端 MCP URL 无效',
|
|
476
|
+
summary: `“${mcp.name}”必须使用绝对 HTTP 或 HTTPS URL。`,
|
|
477
|
+
field: 'components.mcp.url',
|
|
478
|
+
});
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const httpProtocol = url.protocol === 'http:' || url.protocol === 'https:';
|
|
482
|
+
if (!httpProtocol ||
|
|
483
|
+
url.username.length > 0 ||
|
|
484
|
+
url.password.length > 0 ||
|
|
485
|
+
url.hash.length > 0) {
|
|
486
|
+
semanticSourceProblem(collector, plugin, 'MCP_REMOTE_URL_INVALID', `远端 MCP URL 必须是无 userinfo/fragment 的绝对 HTTP(S) URL: ${mcp.url}`, {
|
|
487
|
+
title: '远端 MCP URL 无效',
|
|
488
|
+
summary: `“${mcp.name}”的 URL 包含协议不允许的部分。`,
|
|
489
|
+
field: 'components.mcp.url',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
else if (url.protocol === 'http:' && !isLoopbackHostname(url.hostname)) {
|
|
493
|
+
semanticSourceProblem(collector, plugin, 'MCP_REMOTE_URL_INSECURE', `非 loopback 远端 MCP URL 必须使用 HTTPS: ${mcp.url}`, {
|
|
494
|
+
title: '远端 MCP 连接不安全',
|
|
495
|
+
summary: `“${mcp.name}”只能对 localhost 或 loopback IP 使用 HTTP。`,
|
|
496
|
+
field: 'components.mcp.url',
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
const headerNames = new Set();
|
|
500
|
+
for (const [name, value] of Object.entries(mcp.headers ?? {})) {
|
|
501
|
+
const normalized = name.toLowerCase();
|
|
502
|
+
if (headerNames.has(normalized)) {
|
|
503
|
+
semanticSourceProblem(collector, plugin, 'MCP_HEADER_DUPLICATE', `HTTP header 名称按大小写折叠后重复: ${name}`, {
|
|
504
|
+
title: '远端 MCP header 重复',
|
|
505
|
+
summary: `“${mcp.name}”包含大小写不同但语义相同的 header。`,
|
|
506
|
+
field: 'components.mcp.headers',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
headerNames.add(normalized);
|
|
510
|
+
try {
|
|
511
|
+
validateHeaderName(name);
|
|
512
|
+
validateHeaderValue(name, value);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
semanticSourceProblem(collector, plugin, 'MCP_HEADER_INVALID', `HTTP header 字段无效: ${name}`, {
|
|
516
|
+
title: '远端 MCP header 无效',
|
|
517
|
+
summary: `“${mcp.name}”包含不能作为 HTTP header 发送的名称或值。`,
|
|
518
|
+
field: 'components.mcp.headers',
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function validateLocalReferences(authorization, config, plugin, collector) {
|
|
525
|
+
validatePortableSkills(authorization, config, plugin, collector);
|
|
526
|
+
for (const skill of config.components.skills ?? []) {
|
|
527
|
+
const skillDirectory = resolvePluginReference(authorization, plugin, collector, skill.path, 'Skill 路径');
|
|
528
|
+
if (skillDirectory === undefined)
|
|
529
|
+
continue;
|
|
530
|
+
if (!existsSync(skillDirectory)) {
|
|
531
|
+
referenceProblem(collector, plugin, 'SKILL_DIRECTORY_MISSING', `Skill 目录不存在: ${skill.path}`, '找不到 Skill 内容', `“${skill.name}”引用的内容文件夹不存在。`, skill.path);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (!statSync(skillDirectory).isDirectory()) {
|
|
535
|
+
referenceProblem(collector, plugin, 'SKILL_PATH_NOT_DIRECTORY', `Skill 路径不是目录: ${skill.path}`, 'Skill 位置不是文件夹', `“${skill.name}”引用的位置不能作为 Skill 内容文件夹。`, skill.path);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const skillMarkdown = resolvePluginReference(authorization, plugin, collector, `${skill.path}/SKILL.md`, 'Skill 内容路径');
|
|
539
|
+
if (skillMarkdown !== undefined) {
|
|
540
|
+
if (!existsSync(skillMarkdown)) {
|
|
541
|
+
referenceProblem(collector, plugin, 'SKILL_DOCUMENT_MISSING', `SKILL.md 不存在: ${skill.path}/SKILL.md`, '找不到 Skill 正文', `“${skill.name}”的内容文件夹中没有正文。`, `${skill.path}/SKILL.md`);
|
|
542
|
+
}
|
|
543
|
+
else if (!lstatSync(skillMarkdown).isFile()) {
|
|
544
|
+
referenceProblem(collector, plugin, 'SKILL_DOCUMENT_NOT_FILE', `SKILL.md 不是普通文件: ${skill.path}/SKILL.md`, 'Skill 正文不是普通文件', `“${skill.name}”的 SKILL.md 不能作为 Agent Skills 正文读取。`, `${skill.path}/SKILL.md`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
validatePortableMcp(authorization, config, plugin, collector);
|
|
549
|
+
for (const hook of config.components.hooks ?? []) {
|
|
550
|
+
const script = resolvePluginReference(authorization, plugin, collector, hook.command, 'Hook 脚本路径');
|
|
551
|
+
if (script === undefined)
|
|
552
|
+
continue;
|
|
553
|
+
if (!existsSync(script)) {
|
|
554
|
+
referenceProblem(collector, plugin, 'HOOK_MISSING', `Hook 脚本不存在: ${hook.command}`, '找不到 Hook 文件', `“${plugin.displayName}”引用的 Hook 文件不存在。`, hook.command);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
validateExecutable(script, hook.command, plugin, collector);
|
|
558
|
+
}
|
|
559
|
+
for (const mcp of config.components.mcp ?? []) {
|
|
560
|
+
if (!isStdioMcp(mcp))
|
|
561
|
+
continue;
|
|
562
|
+
if (isPluginLocalPathReference(mcp.command)) {
|
|
563
|
+
const command = resolvePluginReference(authorization, plugin, collector, mcp.command, 'MCP command 路径');
|
|
564
|
+
if (command !== undefined && !existsSync(command)) {
|
|
565
|
+
referenceProblem(collector, plugin, 'MCP_COMMAND_MISSING', `MCP command 文件不存在: ${mcp.command}`, '找不到 MCP 启动文件', `“${mcp.name}”引用的本地启动文件不存在。`, mcp.command);
|
|
566
|
+
}
|
|
567
|
+
else if (command !== undefined && !lstatSync(command).isFile()) {
|
|
568
|
+
referenceProblem(collector, plugin, 'MCP_COMMAND_NOT_FILE', `MCP command 不是普通文件: ${mcp.command}`, 'MCP 启动目标不是文件', `“${mcp.name}”的插件内 command 不能作为可执行文件启动。`, mcp.command);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const argument of mcp.args ?? []) {
|
|
572
|
+
if (!isPluginLocalPathReference(argument))
|
|
573
|
+
continue;
|
|
574
|
+
const path = resolvePluginReference(authorization, plugin, collector, argument, 'MCP args 路径');
|
|
575
|
+
if (path !== undefined && !existsSync(path)) {
|
|
576
|
+
referenceProblem(collector, plugin, 'MCP_ARGUMENT_MISSING', `MCP args 引用的文件不存在: ${argument}`, '找不到 MCP 使用的本地文件', `“${mcp.name}”的一项启动输入不存在。`, argument);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const seenLspNames = new Set();
|
|
581
|
+
for (const lsp of config.components.lsp ?? []) {
|
|
582
|
+
if (seenLspNames.has(lsp.name)) {
|
|
583
|
+
semanticSourceProblem(collector, plugin, 'LSP_DUPLICATE_NAME', `LSP server name 重复: ${lsp.name}`, {
|
|
584
|
+
title: '语言服务名称重复',
|
|
585
|
+
summary: `“${plugin.displayName}”中有多个语言服务使用“${lsp.name}”。`,
|
|
586
|
+
field: 'components.lsp.name',
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
seenLspNames.add(lsp.name);
|
|
590
|
+
if (lsp.transport != null &&
|
|
591
|
+
lsp.transport !== 'stdio' &&
|
|
592
|
+
lsp.transport !== 'socket') {
|
|
593
|
+
semanticSourceProblem(collector, plugin, 'LSP_TRANSPORT_INVALID', `LSP transport 仅支持 stdio 或 socket: ${lsp.name}`, {
|
|
594
|
+
title: '语言服务连接方式不可用',
|
|
595
|
+
summary: `“${lsp.name}”使用了当前不支持的连接方式。`,
|
|
596
|
+
field: 'components.lsp.transport',
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
if (lsp.startupTimeout != null && lsp.startupTimeout < 0) {
|
|
600
|
+
semanticSourceProblem(collector, plugin, 'LSP_TIMEOUT_INVALID', `LSP startupTimeout 不能为负数: ${lsp.name}`, {
|
|
601
|
+
title: '语言服务启动时间无效',
|
|
602
|
+
summary: `“${lsp.name}”的启动等待时间不能小于零。`,
|
|
603
|
+
field: 'components.lsp.startupTimeout',
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (lsp.maxRestarts != null && lsp.maxRestarts < 0) {
|
|
607
|
+
semanticSourceProblem(collector, plugin, 'LSP_RESTARTS_INVALID', `LSP maxRestarts 不能为负数: ${lsp.name}`, {
|
|
608
|
+
title: '语言服务重试次数无效',
|
|
609
|
+
summary: `“${lsp.name}”的重试次数不能小于零。`,
|
|
610
|
+
field: 'components.lsp.maxRestarts',
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
for (const [extension, languageId] of Object.entries(lsp.extensionToLanguage)) {
|
|
614
|
+
if (!extension.startsWith('.')) {
|
|
615
|
+
semanticSourceProblem(collector, plugin, 'LSP_EXTENSION_INVALID', `LSP extensionToLanguage 扩展名必须以 \\. 开头: ${extension}`, {
|
|
616
|
+
title: '文件扩展名格式无效',
|
|
617
|
+
summary: `“${lsp.name}”中的“${extension}”不是可识别的文件扩展名。`,
|
|
618
|
+
field: 'components.lsp.extensionToLanguage',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (languageId.trim().length === 0) {
|
|
622
|
+
semanticSourceProblem(collector, plugin, 'LSP_LANGUAGE_ID_EMPTY', `LSP extensionToLanguage language id 不能为空: ${extension}`, {
|
|
623
|
+
title: '语言标识不能为空',
|
|
624
|
+
summary: `“${lsp.name}”中的“${extension}”还没有语言标识。`,
|
|
625
|
+
field: 'components.lsp.extensionToLanguage',
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (isPluginLocalPathReference(lsp.command)) {
|
|
630
|
+
const command = resolvePluginReference(authorization, plugin, collector, lsp.command, 'LSP command 路径');
|
|
631
|
+
if (command !== undefined && !existsSync(command)) {
|
|
632
|
+
referenceProblem(collector, plugin, 'LSP_COMMAND_MISSING', `LSP command 文件不存在: ${lsp.command}`, '找不到语言服务启动文件', `“${lsp.name}”引用的本地启动文件不存在。`, lsp.command);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (const argument of lsp.args ?? []) {
|
|
636
|
+
if (!isPluginLocalPathReference(argument))
|
|
637
|
+
continue;
|
|
638
|
+
const path = resolvePluginReference(authorization, plugin, collector, argument, 'LSP args 路径');
|
|
639
|
+
if (path !== undefined && !existsSync(path)) {
|
|
640
|
+
referenceProblem(collector, plugin, 'LSP_ARGUMENT_MISSING', `LSP args 引用的文件不存在: ${argument}`, '找不到语言服务使用的本地文件', `“${lsp.name}”的一项启动输入不存在。`, argument);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function readGeneratedJson(path, beforeGeneratedRead) {
|
|
646
|
+
beforeGeneratedRead?.(path);
|
|
647
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
648
|
+
}
|
|
649
|
+
function validateGeneratedFile(authorization, plugin, collector, relativePath, expected, displayLabel, diagnosticLabel, includeInHealth, beforeGeneratedRead) {
|
|
650
|
+
let path;
|
|
651
|
+
try {
|
|
652
|
+
path = resolveAuthorizedPath(authorization, relativePath);
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
656
|
+
generatedProblem(collector, plugin, 'stale', 'GENERATED_UNSAFE_PATH', `${diagnosticLabel} 无法确认授权范围: ${relativePath}: ${message}`, displayLabel, relativePath, includeInHealth);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (!existsSync(path)) {
|
|
660
|
+
if (expected !== null) {
|
|
661
|
+
generatedProblem(collector, plugin, 'missing', 'GENERATED_MISSING', `${diagnosticLabel} 缺失,请重新运行 build: ${path}`, displayLabel, relativePath, includeInHealth);
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (expected === null) {
|
|
666
|
+
generatedProblem(collector, plugin, 'stale', 'GENERATED_OBSOLETE', `${diagnosticLabel} 已过期,请删除或重新运行 build: ${path}`, displayLabel, relativePath, includeInHealth);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
let actual;
|
|
670
|
+
try {
|
|
671
|
+
actual = readGeneratedJson(path, beforeGeneratedRead);
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
generatedProblem(collector, plugin, 'stale', 'GENERATED_INVALID_JSON', `生成文件不是有效 JSON: ${path}: ${error instanceof Error ? error.message : String(error)}`, displayLabel, relativePath, includeInHealth);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (!isDeepStrictEqual(actual, expected)) {
|
|
678
|
+
generatedProblem(collector, plugin, 'stale', 'GENERATED_STALE', `${diagnosticLabel} 与 plugin.yaml 派生结果不一致,请重新运行 build: ${path}`, displayLabel, relativePath, includeInHealth);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function validateGeneratedFiles(authorization, config, plugin, collector, includeInHealth, beforeGeneratedRead) {
|
|
682
|
+
validateGeneratedFile(authorization, plugin, collector, 'plugin.json', buildAgentPluginManifest(config), 'Agent Plugins manifest', 'Agent Plugins manifest', includeInHealth, beforeGeneratedRead);
|
|
683
|
+
validateGeneratedFile(authorization, plugin, collector, 'mcp.json', buildAgentPluginMcpConfig(config), 'Agent Plugins MCP 配置', 'Agent Plugins MCP 配置', includeInHealth, beforeGeneratedRead);
|
|
684
|
+
validateGeneratedFile(authorization, plugin, collector, '.claude-plugin/plugin.json', buildClaudeManifest(config), 'Claude Code 平台结果', 'Claude manifest', includeInHealth, beforeGeneratedRead);
|
|
685
|
+
validateGeneratedFile(authorization, plugin, collector, '.codex-plugin/plugin.json', buildCodexManifest(config), 'Codex 平台结果', 'Codex manifest', includeInHealth, beforeGeneratedRead);
|
|
686
|
+
validateGeneratedFile(authorization, plugin, collector, '.mcp.json', buildClaudeMcpConfig(config), 'MCP 配置', 'MCP 配置', includeInHealth, beforeGeneratedRead);
|
|
687
|
+
validateGeneratedFile(authorization, plugin, collector, 'hooks/hooks.json', buildClaudeHooksConfig(config), 'Hooks 配置', 'Hooks 配置', includeInHealth, beforeGeneratedRead);
|
|
688
|
+
validateGeneratedFile(authorization, plugin, collector, '.lsp.json', buildLspConfig(config), 'LSP 配置', 'LSP 配置', includeInHealth, beforeGeneratedRead);
|
|
689
|
+
}
|
|
690
|
+
function scanPlugin(workspaceAuthorization, directoryName, collector, options) {
|
|
691
|
+
const plugin = newPlugin(directoryName);
|
|
692
|
+
if (directoryName.length === 0 ||
|
|
693
|
+
directoryName === '.' ||
|
|
694
|
+
directoryName === '..' ||
|
|
695
|
+
directoryName.includes('/') ||
|
|
696
|
+
directoryName.includes('\\')) {
|
|
697
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_NAME_UNSAFE', `插件目录不存在: plugins/${directoryName}`, {
|
|
698
|
+
title: '插件名称不能作为安全文件夹定位',
|
|
699
|
+
summary: '所请求的插件名称不是 Marketplace 内的单个文件夹名称。',
|
|
700
|
+
relativePath: `plugins/${directoryName}`,
|
|
701
|
+
});
|
|
702
|
+
return plugin;
|
|
703
|
+
}
|
|
704
|
+
let pluginDirectory;
|
|
705
|
+
try {
|
|
706
|
+
pluginDirectory = resolveAuthorizedPath(workspaceAuthorization, `plugins/${directoryName}`);
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_UNSAFE_PATH', `插件目录无法确认授权范围: plugins/${directoryName}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
710
|
+
title: '插件位置不安全',
|
|
711
|
+
summary: `“${directoryName}”不能在当前授权范围内安全读取。`,
|
|
712
|
+
relativePath: `plugins/${directoryName}`,
|
|
713
|
+
});
|
|
714
|
+
return plugin;
|
|
715
|
+
}
|
|
716
|
+
if (!existsSync(pluginDirectory)) {
|
|
717
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_DIRECTORY_MISSING', `插件目录不存在: plugins/${directoryName}`, {
|
|
718
|
+
title: '找不到插件文件夹',
|
|
719
|
+
summary: `“${directoryName}”的文件夹已经移动或不存在。`,
|
|
720
|
+
relativePath: `plugins/${directoryName}`,
|
|
721
|
+
});
|
|
722
|
+
return plugin;
|
|
723
|
+
}
|
|
724
|
+
if (!lstatSync(pluginDirectory).isDirectory()) {
|
|
725
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_NOT_DIRECTORY', `插件目录不存在: plugins/${directoryName}`, {
|
|
726
|
+
title: '插件位置不是文件夹',
|
|
727
|
+
summary: `“${directoryName}”的位置不能作为插件文件夹。`,
|
|
728
|
+
relativePath: `plugins/${directoryName}`,
|
|
729
|
+
});
|
|
730
|
+
return plugin;
|
|
731
|
+
}
|
|
732
|
+
let pluginAuthorization;
|
|
733
|
+
try {
|
|
734
|
+
pluginAuthorization = authorizeExistingDirectory(pluginDirectory);
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_UNAVAILABLE', error instanceof Error ? error.message : String(error), {
|
|
738
|
+
title: '插件暂时无法读取',
|
|
739
|
+
summary: `系统当前无法安全读取“${directoryName}”。`,
|
|
740
|
+
relativePath: `plugins/${directoryName}`,
|
|
741
|
+
});
|
|
742
|
+
return plugin;
|
|
743
|
+
}
|
|
744
|
+
let yamlPath;
|
|
745
|
+
try {
|
|
746
|
+
yamlPath = resolveAuthorizedPath(pluginAuthorization, 'plugin.yaml');
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_SOURCE_UNSAFE', error instanceof Error ? error.message : String(error), {
|
|
750
|
+
title: '插件源信息使用了不安全的链接',
|
|
751
|
+
summary: `“${directoryName}”的基础信息无法在授权范围内读取。`,
|
|
752
|
+
relativePath: 'plugin.yaml',
|
|
753
|
+
});
|
|
754
|
+
return plugin;
|
|
755
|
+
}
|
|
756
|
+
if (!existsSync(yamlPath)) {
|
|
757
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_SOURCE_MISSING', `plugin.yaml not found: ${yamlPath}`, {
|
|
758
|
+
title: '找不到插件基础信息',
|
|
759
|
+
summary: `“${directoryName}”中没有插件声明。`,
|
|
760
|
+
relativePath: 'plugin.yaml',
|
|
761
|
+
});
|
|
762
|
+
return plugin;
|
|
763
|
+
}
|
|
764
|
+
let raw;
|
|
765
|
+
try {
|
|
766
|
+
raw = readFileSync(yamlPath, 'utf-8');
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_SOURCE_UNREADABLE', error instanceof Error ? error.message : String(error), {
|
|
770
|
+
title: '插件基础信息暂时无法读取',
|
|
771
|
+
summary: `系统无法读取“${directoryName}”的插件声明。`,
|
|
772
|
+
relativePath: 'plugin.yaml',
|
|
773
|
+
});
|
|
774
|
+
return plugin;
|
|
775
|
+
}
|
|
776
|
+
const parsed = parsePluginYamlSource(raw);
|
|
777
|
+
if (parsed.status === 'invalid-yaml') {
|
|
778
|
+
unavailableSourceProblem(collector, plugin, 'PLUGIN_YAML_INVALID', parsed.message, {
|
|
779
|
+
title: '插件基础信息无法解析',
|
|
780
|
+
summary: `“${directoryName}”的插件声明不是可识别的 YAML。`,
|
|
781
|
+
relativePath: 'plugin.yaml',
|
|
782
|
+
});
|
|
783
|
+
return plugin;
|
|
784
|
+
}
|
|
785
|
+
if (parsed.status === 'invalid-schema') {
|
|
786
|
+
const diagnosticRef = collector.add({
|
|
787
|
+
dimension: 'source',
|
|
788
|
+
severity: 'blocking',
|
|
789
|
+
scope: {
|
|
790
|
+
kind: 'plugin',
|
|
791
|
+
label: plugin.displayName,
|
|
792
|
+
pluginId: plugin.id,
|
|
793
|
+
},
|
|
794
|
+
code: 'PLUGIN_SCHEMA_INVALID',
|
|
795
|
+
diagnosticMessage: parsed.message,
|
|
796
|
+
title: '插件基础信息需要修正',
|
|
797
|
+
summary: `“${directoryName}”有 ${parsed.issues.length} 项基础信息不符合当前格式。`,
|
|
798
|
+
impact: '客户端无法可靠判断这个插件的组件与生成结果。',
|
|
799
|
+
nextAction: '请在外部编辑器中修正标出的字段,然后重新检查。',
|
|
800
|
+
}, plugin).diagnosticRef;
|
|
801
|
+
plugin.source = 'invalid';
|
|
802
|
+
plugin.references = 'unknown';
|
|
803
|
+
plugin.generated = 'unknown';
|
|
804
|
+
for (const schemaIssue of parsed.issues) {
|
|
805
|
+
const issueId = `issue-${String(collector.issues.length + 1).padStart(3, '0')}`;
|
|
806
|
+
collector.issues.push({
|
|
807
|
+
id: issueId,
|
|
808
|
+
dimension: 'source',
|
|
809
|
+
severity: 'blocking',
|
|
810
|
+
scope: {
|
|
811
|
+
kind: 'field',
|
|
812
|
+
label: plugin.displayName,
|
|
813
|
+
pluginId: plugin.id,
|
|
814
|
+
field: schemaIssue.instancePath,
|
|
815
|
+
relativePath: 'plugin.yaml',
|
|
816
|
+
},
|
|
817
|
+
title: '字段内容不符合当前格式',
|
|
818
|
+
summary: `${schemaIssue.instancePath}:${schemaIssue.message}`,
|
|
819
|
+
impact: '这个字段会阻止客户端可靠理解该插件。',
|
|
820
|
+
nextAction: '请在外部编辑器中修正该字段,然后重新检查。',
|
|
821
|
+
diagnosticRef,
|
|
822
|
+
});
|
|
823
|
+
plugin.issueIds.push(issueId);
|
|
824
|
+
}
|
|
825
|
+
return plugin;
|
|
826
|
+
}
|
|
827
|
+
const config = parsed.value;
|
|
828
|
+
plugin.canonicalName = config.name;
|
|
829
|
+
plugin.displayName =
|
|
830
|
+
config.platform?.codex?.interface?.displayName ?? config.name;
|
|
831
|
+
plugin.version = config.version;
|
|
832
|
+
plugin.componentKinds = listComponentKinds(config);
|
|
833
|
+
plugin.componentCount = componentCount(config);
|
|
834
|
+
plugin.platforms = ['Agent Plugins', 'Claude Code', 'Codex'];
|
|
835
|
+
let deferredNameMismatch;
|
|
836
|
+
if (config.name !== directoryName) {
|
|
837
|
+
const message = `plugin.yaml 中的 name (${config.name}) 与目录名 (${directoryName}) 不一致`;
|
|
838
|
+
semanticSourceProblem(collector, plugin, 'PLUGIN_NAME_MISMATCH', message, {
|
|
839
|
+
title: '插件名称与文件夹不一致',
|
|
840
|
+
summary: `“${plugin.displayName}”的内部名称与所在文件夹不同。`,
|
|
841
|
+
field: 'name',
|
|
842
|
+
}, { includeInValidation: false });
|
|
843
|
+
deferredNameMismatch = {
|
|
844
|
+
code: 'PLUGIN_NAME_MISMATCH',
|
|
845
|
+
message,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
validateLocalReferences(pluginAuthorization, config, plugin, collector);
|
|
849
|
+
if (options.inspectGenerated) {
|
|
850
|
+
validateGeneratedFiles(pluginAuthorization, config, plugin, collector, plugin.source === 'valid', options.beforeGeneratedRead);
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
plugin.generated = 'not-applicable';
|
|
854
|
+
}
|
|
855
|
+
if (deferredNameMismatch !== undefined) {
|
|
856
|
+
collector.addValidationDiagnostic(plugin, deferredNameMismatch.code, deferredNameMismatch.message);
|
|
857
|
+
}
|
|
858
|
+
return plugin;
|
|
859
|
+
}
|
|
860
|
+
function pluginNamesForRequest(authorization, requestedNames, collector, readDirectory) {
|
|
861
|
+
if (requestedNames !== undefined)
|
|
862
|
+
return [...requestedNames];
|
|
863
|
+
let pluginsDirectory;
|
|
864
|
+
try {
|
|
865
|
+
pluginsDirectory = resolveAuthorizedPath(authorization, 'plugins');
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
collector.add({
|
|
869
|
+
dimension: 'source',
|
|
870
|
+
severity: 'blocking',
|
|
871
|
+
scope: {
|
|
872
|
+
kind: 'path',
|
|
873
|
+
label: '插件目录',
|
|
874
|
+
relativePath: 'plugins',
|
|
875
|
+
},
|
|
876
|
+
code: 'PLUGINS_DIRECTORY_UNSAFE',
|
|
877
|
+
diagnosticMessage: error instanceof Error ? error.message : String(error),
|
|
878
|
+
title: '插件目录不安全',
|
|
879
|
+
summary: '插件目录无法在当前授权范围内读取。',
|
|
880
|
+
impact: '客户端无法列出或检查这个 Marketplace 中的插件。',
|
|
881
|
+
nextAction: '请移除链接或修正插件目录后重新检查。',
|
|
882
|
+
});
|
|
883
|
+
return [];
|
|
884
|
+
}
|
|
885
|
+
if (!existsSync(pluginsDirectory))
|
|
886
|
+
return [];
|
|
887
|
+
if (!lstatSync(pluginsDirectory).isDirectory()) {
|
|
888
|
+
collector.add({
|
|
889
|
+
dimension: 'source',
|
|
890
|
+
severity: 'blocking',
|
|
891
|
+
scope: {
|
|
892
|
+
kind: 'path',
|
|
893
|
+
label: '插件目录',
|
|
894
|
+
relativePath: 'plugins',
|
|
895
|
+
},
|
|
896
|
+
code: 'PLUGINS_NOT_DIRECTORY',
|
|
897
|
+
diagnosticMessage: 'plugins 不是目录',
|
|
898
|
+
title: '插件位置不是文件夹',
|
|
899
|
+
summary: 'Marketplace 的插件位置不能作为文件夹读取。',
|
|
900
|
+
impact: '客户端无法列出或检查插件。',
|
|
901
|
+
nextAction: '请在外部修正 plugins 位置后重新检查。',
|
|
902
|
+
});
|
|
903
|
+
return [];
|
|
904
|
+
}
|
|
905
|
+
const names = [];
|
|
906
|
+
for (const entry of readDirectory(pluginsDirectory)) {
|
|
907
|
+
if (entry.isSymbolicLink()) {
|
|
908
|
+
collector.add({
|
|
909
|
+
dimension: 'reference',
|
|
910
|
+
severity: 'blocking',
|
|
911
|
+
scope: {
|
|
912
|
+
kind: 'path',
|
|
913
|
+
label: entry.name,
|
|
914
|
+
relativePath: `plugins/${entry.name}`,
|
|
915
|
+
},
|
|
916
|
+
code: 'PLUGIN_DIRECTORY_SYMLINK',
|
|
917
|
+
diagnosticMessage: `插件目录不允许使用符号链接: plugins/${entry.name}`,
|
|
918
|
+
title: '插件文件夹使用了不安全的链接',
|
|
919
|
+
summary: `“${entry.name}”无法确认仍在授权的 Marketplace 内。`,
|
|
920
|
+
impact: '客户端不会跟随这个链接读取插件内容。',
|
|
921
|
+
nextAction: '请把插件内容放回 Marketplace 内的真实文件夹后重新检查。',
|
|
922
|
+
});
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (!entry.isDirectory())
|
|
926
|
+
continue;
|
|
927
|
+
names.push(entry.name);
|
|
928
|
+
}
|
|
929
|
+
return names.sort();
|
|
930
|
+
}
|
|
931
|
+
function aggregateGenerated(plugins) {
|
|
932
|
+
if (plugins.length === 0)
|
|
933
|
+
return 'not-applicable';
|
|
934
|
+
if (plugins.some((plugin) => plugin.generated === 'stale'))
|
|
935
|
+
return 'stale';
|
|
936
|
+
if (plugins.some((plugin) => plugin.generated === 'missing'))
|
|
937
|
+
return 'missing';
|
|
938
|
+
if (plugins.some((plugin) => plugin.generated === 'unknown'))
|
|
939
|
+
return 'unknown';
|
|
940
|
+
return 'fresh';
|
|
941
|
+
}
|
|
942
|
+
function aggregateReferences(plugins, issues) {
|
|
943
|
+
if (issues.some((issue) => issue.dimension === 'reference'))
|
|
944
|
+
return 'invalid';
|
|
945
|
+
if (plugins.some((plugin) => plugin.references === 'unknown'))
|
|
946
|
+
return 'unknown';
|
|
947
|
+
return 'valid';
|
|
948
|
+
}
|
|
949
|
+
function recommend(issues, pluginCount) {
|
|
950
|
+
const source = issues.find((issue) => issue.dimension === 'source');
|
|
951
|
+
if (source !== undefined) {
|
|
952
|
+
return {
|
|
953
|
+
kind: 'review-source',
|
|
954
|
+
label: '查看首要源信息问题',
|
|
955
|
+
description: '先理解阻止客户端读取插件的第一项问题。',
|
|
956
|
+
issueId: source.id,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const reference = issues.find((issue) => issue.dimension === 'reference');
|
|
960
|
+
if (reference !== undefined) {
|
|
961
|
+
return {
|
|
962
|
+
kind: 'review-reference',
|
|
963
|
+
label: '查看首要本地内容问题',
|
|
964
|
+
description: '先确认缺失或越界的本地内容,再继续维护。',
|
|
965
|
+
issueId: reference.id,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
const generated = issues.find((issue) => issue.dimension === 'generated');
|
|
969
|
+
if (generated !== undefined) {
|
|
970
|
+
return {
|
|
971
|
+
kind: 'review-generated',
|
|
972
|
+
label: '查看需要更新的生成结果',
|
|
973
|
+
description: '源内容仍然有效;先核对受影响的平台结果。',
|
|
974
|
+
issueId: generated.id,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
if (pluginCount === 0) {
|
|
978
|
+
return {
|
|
979
|
+
kind: 'prepare-first-plugin',
|
|
980
|
+
label: '了解第一个插件的维护入口',
|
|
981
|
+
description: '当前 Marketplace 可以使用;本次只读检查只说明后续入口,不会提前创建内容。',
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
kind: 'review-evidence',
|
|
986
|
+
label: '查看本次检查依据',
|
|
987
|
+
description: '确认源信息、本地内容和插件生成结果的只读检查范围。',
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function scanWorkspaceHealthUnsafe(request, dependencies = {}) {
|
|
991
|
+
const authorization = authorizeExistingDirectory(request.directory);
|
|
992
|
+
const collector = new HealthCollector();
|
|
993
|
+
let marketplaceValidity = 'valid';
|
|
994
|
+
let marketplace;
|
|
995
|
+
try {
|
|
996
|
+
marketplace = readMarketplaceMetadata(authorization.canonicalPath);
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
marketplaceValidity = 'invalid';
|
|
1000
|
+
marketplace = { name: basename(authorization.canonicalPath) };
|
|
1001
|
+
collector.add({
|
|
1002
|
+
dimension: 'source',
|
|
1003
|
+
severity: 'blocking',
|
|
1004
|
+
scope: {
|
|
1005
|
+
kind: 'path',
|
|
1006
|
+
label: 'Marketplace 信息',
|
|
1007
|
+
relativePath: 'marketplace.yaml',
|
|
1008
|
+
},
|
|
1009
|
+
code: existsSync(join(authorization.canonicalPath, 'marketplace.yaml'))
|
|
1010
|
+
? 'MARKETPLACE_INVALID'
|
|
1011
|
+
: 'MARKETPLACE_MISSING',
|
|
1012
|
+
diagnosticMessage: error instanceof Error ? error.message : String(error),
|
|
1013
|
+
title: existsSync(join(authorization.canonicalPath, 'marketplace.yaml'))
|
|
1014
|
+
? 'Marketplace 基础信息无法读取'
|
|
1015
|
+
: '这里还不是可识别的 Marketplace',
|
|
1016
|
+
summary: existsSync(join(authorization.canonicalPath, 'marketplace.yaml'))
|
|
1017
|
+
? 'marketplace.yaml 存在,但其中的基础信息不是有效格式。'
|
|
1018
|
+
: '所选文件夹中没有 marketplace.yaml。',
|
|
1019
|
+
impact: '这个位置可以继续只读检查,但不会被标记为有效的活动 Marketplace。',
|
|
1020
|
+
nextAction: '请在外部修正 Marketplace 基础信息,然后重新检查。',
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
const discoveredNames = pluginNamesForRequest(authorization, request.workspaceOnly ? undefined : request.pluginNames, collector, dependencies.readDirectory ??
|
|
1024
|
+
((path) => readdirSync(path, { withFileTypes: true })));
|
|
1025
|
+
const names = request.workspaceOnly ? [] : discoveredNames;
|
|
1026
|
+
const mutablePlugins = names.map((name) => scanPlugin(authorization, name, collector, {
|
|
1027
|
+
inspectGenerated: request.scope !== 'source-and-references',
|
|
1028
|
+
beforeGeneratedRead: dependencies.beforeGeneratedRead,
|
|
1029
|
+
}));
|
|
1030
|
+
const sourceIssueIds = collector.issues
|
|
1031
|
+
.filter((issue) => issue.dimension === 'source')
|
|
1032
|
+
.map((issue) => issue.id);
|
|
1033
|
+
const referenceIssueIds = collector.issues
|
|
1034
|
+
.filter((issue) => issue.dimension === 'reference')
|
|
1035
|
+
.map((issue) => issue.id);
|
|
1036
|
+
const generatedIssueIds = collector.issues
|
|
1037
|
+
.filter((issue) => issue.dimension === 'generated')
|
|
1038
|
+
.map((issue) => issue.id);
|
|
1039
|
+
const source = sourceIssueIds.length > 0 ? 'invalid' : 'valid';
|
|
1040
|
+
const references = aggregateReferences(mutablePlugins, collector.issues);
|
|
1041
|
+
const generated = aggregateGenerated(mutablePlugins);
|
|
1042
|
+
const overall = source === 'invalid' || references === 'invalid'
|
|
1043
|
+
? 'invalid'
|
|
1044
|
+
: generated === 'missing' || generated === 'stale'
|
|
1045
|
+
? 'stale'
|
|
1046
|
+
: mutablePlugins.length === 0
|
|
1047
|
+
? 'empty'
|
|
1048
|
+
: 'ready';
|
|
1049
|
+
const platforms = Array.from(new Set(mutablePlugins.flatMap((plugin) => plugin.platforms)));
|
|
1050
|
+
const snapshot = {
|
|
1051
|
+
overall,
|
|
1052
|
+
workspace: {
|
|
1053
|
+
path: authorization.canonicalPath,
|
|
1054
|
+
name: marketplace.name,
|
|
1055
|
+
...(marketplace.description === undefined
|
|
1056
|
+
? {}
|
|
1057
|
+
: { description: marketplace.description }),
|
|
1058
|
+
...(marketplace.organization === undefined
|
|
1059
|
+
? {}
|
|
1060
|
+
: { organization: marketplace.organization }),
|
|
1061
|
+
marketplaceValidity,
|
|
1062
|
+
},
|
|
1063
|
+
dimensions: {
|
|
1064
|
+
source: { state: source, issueIds: sourceIssueIds },
|
|
1065
|
+
references: { state: references, issueIds: referenceIssueIds },
|
|
1066
|
+
generated: { state: generated, issueIds: generatedIssueIds },
|
|
1067
|
+
},
|
|
1068
|
+
summary: {
|
|
1069
|
+
pluginCount: mutablePlugins.length,
|
|
1070
|
+
componentCount: mutablePlugins.reduce((sum, plugin) => sum + plugin.componentCount, 0),
|
|
1071
|
+
platformCount: platforms.length,
|
|
1072
|
+
platforms,
|
|
1073
|
+
},
|
|
1074
|
+
plugins: mutablePlugins.map((plugin) => ({
|
|
1075
|
+
id: plugin.id,
|
|
1076
|
+
directoryName: plugin.directoryName,
|
|
1077
|
+
displayName: plugin.displayName,
|
|
1078
|
+
...(plugin.canonicalName === undefined
|
|
1079
|
+
? {}
|
|
1080
|
+
: { canonicalName: plugin.canonicalName }),
|
|
1081
|
+
...(plugin.version === undefined ? {} : { version: plugin.version }),
|
|
1082
|
+
componentCount: plugin.componentCount,
|
|
1083
|
+
componentKinds: plugin.componentKinds,
|
|
1084
|
+
platforms: plugin.platforms,
|
|
1085
|
+
source: plugin.source,
|
|
1086
|
+
references: plugin.references,
|
|
1087
|
+
generated: plugin.generated,
|
|
1088
|
+
issueIds: plugin.issueIds,
|
|
1089
|
+
diagnosticRefs: Array.from(new Set(plugin.diagnosticRefs)),
|
|
1090
|
+
})),
|
|
1091
|
+
issues: collector.issues,
|
|
1092
|
+
diagnostics: collector.diagnostics,
|
|
1093
|
+
recommendedAction: recommend(collector.issues, mutablePlugins.length),
|
|
1094
|
+
checkedAt: (dependencies.now ?? (() => new Date()))().toISOString(),
|
|
1095
|
+
access: 'read-only',
|
|
1096
|
+
generatedScope: 'plugin-manifests',
|
|
1097
|
+
};
|
|
1098
|
+
return {
|
|
1099
|
+
status: 'scanned',
|
|
1100
|
+
snapshot,
|
|
1101
|
+
validationDiagnostics: collector.validationDiagnostics,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
export function scanWorkspaceHealth(request, dependencies = {}) {
|
|
1105
|
+
try {
|
|
1106
|
+
return scanWorkspaceHealthUnsafe(request, dependencies);
|
|
1107
|
+
}
|
|
1108
|
+
catch (error) {
|
|
1109
|
+
return error instanceof AuthorizedPathError
|
|
1110
|
+
? mapAuthorizationFailure(error)
|
|
1111
|
+
: unavailableFromUnexpectedFailure(error);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
//# sourceMappingURL=workspace-health.js.map
|