openpets 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/api.json +6658 -0
- package/dist/src/core/ai-client-base/index.d.ts +47 -0
- package/dist/src/core/ai-client-base/index.d.ts.map +1 -0
- package/dist/src/core/ai-client-base/index.js +168 -0
- package/dist/src/core/ai-client-base/index.js.map +1 -0
- package/dist/src/core/browser.d.ts +10 -0
- package/dist/src/core/browser.d.ts.map +1 -0
- package/{browser.ts → dist/src/core/browser.js} +4 -4
- package/dist/src/core/browser.js.map +1 -0
- package/dist/src/core/build-pet.d.ts +2 -0
- package/dist/src/core/build-pet.d.ts.map +1 -0
- package/dist/src/core/build-pet.js +392 -0
- package/dist/src/core/build-pet.js.map +1 -0
- package/dist/src/core/cli.d.ts +3 -0
- package/dist/src/core/cli.d.ts.map +1 -0
- package/dist/src/core/cli.js +244 -0
- package/dist/src/core/cli.js.map +1 -0
- package/dist/src/core/config-manager.d.ts +13 -0
- package/dist/src/core/config-manager.d.ts.map +1 -0
- package/dist/src/core/config-manager.js +59 -0
- package/dist/src/core/config-manager.js.map +1 -0
- package/dist/src/core/deploy-pet.d.ts +2 -0
- package/dist/src/core/deploy-pet.d.ts.map +1 -0
- package/dist/src/core/deploy-pet.js +66 -0
- package/dist/src/core/deploy-pet.js.map +1 -0
- package/dist/src/core/index.d.ts +12 -0
- package/dist/src/core/index.d.ts.map +1 -0
- package/dist/src/core/index.js +12 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/local-cache.d.ts +69 -0
- package/dist/src/core/local-cache.d.ts.map +1 -0
- package/dist/src/core/local-cache.js +212 -0
- package/dist/src/core/local-cache.js.map +1 -0
- package/dist/src/core/logger.d.ts.map +1 -0
- package/{logger.js → dist/src/core/logger.js} +8 -9
- package/dist/src/core/logger.js.map +1 -0
- package/dist/src/core/mautrix-bridge.d.ts +93 -0
- package/dist/src/core/mautrix-bridge.d.ts.map +1 -0
- package/dist/src/core/mautrix-bridge.js +1046 -0
- package/dist/src/core/mautrix-bridge.js.map +1 -0
- package/dist/src/core/mcp-factory.d.ts +12 -0
- package/dist/src/core/mcp-factory.d.ts.map +1 -0
- package/dist/src/core/mcp-factory.js +143 -0
- package/dist/src/core/mcp-factory.js.map +1 -0
- package/dist/src/core/mcp-server.d.ts +3 -0
- package/dist/src/core/mcp-server.d.ts.map +1 -0
- package/dist/src/core/mcp-server.js +55 -0
- package/dist/src/core/mcp-server.js.map +1 -0
- package/dist/src/core/migrate-plugin.d.ts +15 -0
- package/dist/src/core/migrate-plugin.d.ts.map +1 -0
- package/dist/src/core/migrate-plugin.js +181 -0
- package/dist/src/core/migrate-plugin.js.map +1 -0
- package/dist/src/core/pets-registry.d.ts +47 -0
- package/dist/src/core/pets-registry.d.ts.map +1 -0
- package/dist/src/core/pets-registry.js +109 -0
- package/dist/src/core/pets-registry.js.map +1 -0
- package/dist/src/core/plugin-factory.d.ts +58 -0
- package/dist/src/core/plugin-factory.d.ts.map +1 -0
- package/dist/src/core/plugin-factory.js +212 -0
- package/dist/src/core/plugin-factory.js.map +1 -0
- package/dist/src/core/prompt-utils.d.ts +14 -0
- package/dist/src/core/prompt-utils.d.ts.map +1 -0
- package/dist/src/core/prompt-utils.js +106 -0
- package/dist/src/core/prompt-utils.js.map +1 -0
- package/dist/src/core/schema-helpers.d.ts +30 -0
- package/dist/src/core/schema-helpers.d.ts.map +1 -0
- package/dist/src/core/schema-helpers.js +46 -0
- package/dist/src/core/schema-helpers.js.map +1 -0
- package/dist/src/core/search-pets.d.ts +29 -0
- package/dist/src/core/search-pets.d.ts.map +1 -0
- package/dist/src/core/search-pets.js +196 -0
- package/dist/src/core/search-pets.js.map +1 -0
- package/dist/src/core/types.d.ts +63 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/core/types.js +2 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/core/validate-pet.d.ts +40 -0
- package/dist/src/core/validate-pet.d.ts.map +1 -0
- package/dist/src/core/validate-pet.js +650 -0
- package/dist/src/core/validate-pet.js.map +1 -0
- package/package.json +15 -28
- package/ai-client-base/index.ts +0 -229
- package/build-pet.ts +0 -429
- package/cli.ts +0 -268
- package/config-manager.ts +0 -82
- package/deploy-pet.ts +0 -91
- package/index.ts +0 -10
- package/local-cache.ts +0 -280
- package/logger.ts +0 -143
- package/mcp-factory.ts +0 -180
- package/mcp-server.ts +0 -69
- package/migrate-plugin.ts +0 -220
- package/pets-registry.ts +0 -160
- package/plugin-factory.ts +0 -300
- package/prompt-utils.ts +0 -130
- package/schema-helpers.ts +0 -59
- package/search-pets.ts +0 -267
- package/types.ts +0 -68
- package/validate-pet.ts +0 -749
- /package/{logger.d.ts → dist/src/core/logger.d.ts} +0 -0
package/validate-pet.ts
DELETED
|
@@ -1,749 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs'
|
|
2
|
-
import { join } from 'path'
|
|
3
|
-
import { execSync } from 'child_process'
|
|
4
|
-
|
|
5
|
-
export interface ValidationResult {
|
|
6
|
-
valid: boolean
|
|
7
|
-
errors: string[]
|
|
8
|
-
warnings: string[]
|
|
9
|
-
codePatternWarnings?: string[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface PackageValidation {
|
|
13
|
-
name: string
|
|
14
|
-
path: string
|
|
15
|
-
result: ValidationResult
|
|
16
|
-
usesModernPattern?: boolean
|
|
17
|
-
usesPluginFactory?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ValidationReport {
|
|
21
|
-
total: number
|
|
22
|
-
passed: number
|
|
23
|
-
failed: number
|
|
24
|
-
details: PackageValidation[]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class PluginValidator {
|
|
28
|
-
private requiredFields = [
|
|
29
|
-
'name',
|
|
30
|
-
'version',
|
|
31
|
-
'title',
|
|
32
|
-
'subtitle',
|
|
33
|
-
'description',
|
|
34
|
-
'main',
|
|
35
|
-
'types',
|
|
36
|
-
'keywords',
|
|
37
|
-
'dependencies',
|
|
38
|
-
'queries',
|
|
39
|
-
'scenarios'
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
private recommendedFields = [
|
|
43
|
-
'providers'
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
private requiredScripts = [
|
|
47
|
-
'test:all'
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
validatePlugin(pluginPath: string): PackageValidation {
|
|
51
|
-
const packageJsonPath = join(pluginPath, 'package.json')
|
|
52
|
-
|
|
53
|
-
if (!existsSync(packageJsonPath)) {
|
|
54
|
-
return {
|
|
55
|
-
name: this.getFolderName(pluginPath),
|
|
56
|
-
path: pluginPath,
|
|
57
|
-
result: {
|
|
58
|
-
valid: false,
|
|
59
|
-
errors: ['package.json not found'],
|
|
60
|
-
warnings: []
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
67
|
-
const result = this.validatePackageJson(packageJson, pluginPath)
|
|
68
|
-
|
|
69
|
-
const codePatternAnalysis = this.analyzeCodePatterns(pluginPath)
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
name: packageJson.name || this.getFolderName(pluginPath),
|
|
73
|
-
path: pluginPath,
|
|
74
|
-
result: {
|
|
75
|
-
...result,
|
|
76
|
-
codePatternWarnings: codePatternAnalysis.warnings
|
|
77
|
-
},
|
|
78
|
-
usesModernPattern: codePatternAnalysis.usesModernPattern,
|
|
79
|
-
usesPluginFactory: codePatternAnalysis.usesPluginFactory
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
return {
|
|
83
|
-
name: this.getFolderName(pluginPath),
|
|
84
|
-
path: pluginPath,
|
|
85
|
-
result: {
|
|
86
|
-
valid: false,
|
|
87
|
-
errors: [`Invalid JSON in package.json: ${error instanceof Error ? error.message : String(error)}`],
|
|
88
|
-
warnings: []
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private validatePackageJson(pkg: any, pluginPath: string): ValidationResult {
|
|
95
|
-
const errors: string[] = []
|
|
96
|
-
const warnings: string[] = []
|
|
97
|
-
|
|
98
|
-
// Check schema reference
|
|
99
|
-
const validSchemas = [
|
|
100
|
-
'../../src/shared-utils/opencode-plugin.schema.json',
|
|
101
|
-
'https://pets.studio/config.json'
|
|
102
|
-
]
|
|
103
|
-
if (!pkg.$schema || !validSchemas.includes(pkg.$schema)) {
|
|
104
|
-
errors.push('Missing or incorrect schema reference. Expected: "../../src/shared-utils/opencode-plugin.schema.json" or "https://pets.studio/config.json"')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Check required fields
|
|
108
|
-
for (const field of this.requiredFields) {
|
|
109
|
-
if (!pkg[field]) {
|
|
110
|
-
errors.push(`Missing required field: ${field}`)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Validate name format
|
|
115
|
-
if (pkg.name && !this.isValidPluginName(pkg.name)) {
|
|
116
|
-
errors.push(`Invalid plugin name format. Should be 'openpets/plugin-name'`)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Validate keywords
|
|
120
|
-
if (pkg.keywords) {
|
|
121
|
-
if (!Array.isArray(pkg.keywords)) {
|
|
122
|
-
errors.push('keywords must be an array')
|
|
123
|
-
} else {
|
|
124
|
-
const requiredKeywords = ['opencode', 'plugin']
|
|
125
|
-
for (const keyword of requiredKeywords) {
|
|
126
|
-
if (!pkg.keywords.includes(keyword)) {
|
|
127
|
-
warnings.push(`Missing recommended keyword: ${keyword}`)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Validate repository
|
|
134
|
-
if (pkg.repository) {
|
|
135
|
-
if (!pkg.repository.type || !pkg.repository.url) {
|
|
136
|
-
errors.push('repository must have type and url fields')
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Validate dependencies - @opencode-ai/plugin should only be in shared-utils
|
|
141
|
-
if (pkg.dependencies && pkg.dependencies['@opencode-ai/plugin']) {
|
|
142
|
-
errors.push('@opencode-ai/plugin should only be in shared-utils, not in individual pets')
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (pkg.peerDependencies && pkg.peerDependencies['@opencode-ai/plugin']) {
|
|
146
|
-
errors.push('@opencode-ai/plugin should only be in shared-utils, not in individual pets')
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Check for required @opencode/shared-utils dependency
|
|
150
|
-
if (!pkg.dependencies || !pkg.dependencies['@opencode/shared-utils']) {
|
|
151
|
-
errors.push('Missing required dependency "@opencode/shared-utils"')
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Validate scripts
|
|
155
|
-
if (pkg.scripts) {
|
|
156
|
-
for (const script of this.requiredScripts) {
|
|
157
|
-
if (!pkg.scripts[script]) {
|
|
158
|
-
warnings.push(`Missing recommended script: ${script}`)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
warnings.push('No scripts defined - consider adding test scripts')
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Validate providers structure if present
|
|
166
|
-
const validProviderIds = new Set<string>()
|
|
167
|
-
if (pkg.providers) {
|
|
168
|
-
const providersValidation = this.validateProviders(pkg.providers)
|
|
169
|
-
errors.push(...providersValidation.errors)
|
|
170
|
-
warnings.push(...providersValidation.warnings)
|
|
171
|
-
|
|
172
|
-
if (Array.isArray(pkg.providers)) {
|
|
173
|
-
pkg.providers.forEach((provider: any) => {
|
|
174
|
-
if (provider.id) {
|
|
175
|
-
validProviderIds.add(provider.id)
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
} else if (pkg.envVariables) {
|
|
180
|
-
warnings.push('Missing recommended field "providers" - consider defining providers with credentialsUrl for better documentation')
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Validate envVariables structure if present
|
|
184
|
-
if (pkg.envVariables) {
|
|
185
|
-
const envValidation = this.validateEnvVariables(pkg.envVariables, validProviderIds)
|
|
186
|
-
errors.push(...envValidation.errors)
|
|
187
|
-
warnings.push(...envValidation.warnings)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Validate queries (required)
|
|
191
|
-
if (!pkg.queries) {
|
|
192
|
-
errors.push('Missing required field: queries')
|
|
193
|
-
} else {
|
|
194
|
-
if (!Array.isArray(pkg.queries)) {
|
|
195
|
-
errors.push('queries must be an array')
|
|
196
|
-
} else if (pkg.queries.length === 0) {
|
|
197
|
-
errors.push('queries array cannot be empty - add at least one example query')
|
|
198
|
-
} else {
|
|
199
|
-
// Validate each query is a non-empty string
|
|
200
|
-
pkg.queries.forEach((query: any, index: number) => {
|
|
201
|
-
if (typeof query !== 'string' || query.trim().length === 0) {
|
|
202
|
-
errors.push(`queries[${index}] must be a non-empty string`)
|
|
203
|
-
}
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Validate scenarios (required)
|
|
209
|
-
if (!pkg.scenarios) {
|
|
210
|
-
errors.push('Missing required field: scenarios')
|
|
211
|
-
} else {
|
|
212
|
-
const scenarioValidation = this.validateScenarios(pkg.scenarios)
|
|
213
|
-
if (Object.keys(pkg.scenarios).length === 0) {
|
|
214
|
-
errors.push('scenarios object cannot be empty - add at least one test scenario')
|
|
215
|
-
}
|
|
216
|
-
errors.push(...scenarioValidation.errors)
|
|
217
|
-
warnings.push(...scenarioValidation.warnings)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check for essential files
|
|
221
|
-
const essentialFiles = ['index.ts', 'opencode.json']
|
|
222
|
-
for (const file of essentialFiles) {
|
|
223
|
-
if (!existsSync(join(pluginPath, file))) {
|
|
224
|
-
errors.push(`Missing essential file: ${file}`)
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Check for .env.example
|
|
229
|
-
if (pkg.envVariables && !existsSync(join(pluginPath, '.env.example'))) {
|
|
230
|
-
errors.push('envVariables defined but .env.example file missing - create this file to document required environment variables')
|
|
231
|
-
} else if (pkg.envVariables && existsSync(join(pluginPath, '.env.example'))) {
|
|
232
|
-
const envExampleValidation = this.validateEnvExampleFile(join(pluginPath, '.env.example'), pkg.envVariables, pkg.providers)
|
|
233
|
-
warnings.push(...envExampleValidation.warnings)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Check for README
|
|
237
|
-
if (!existsSync(join(pluginPath, 'README.md'))) {
|
|
238
|
-
warnings.push('README.md file missing')
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Validate optional but recommended metadata fields
|
|
242
|
-
this.validateOptionalMetadata(pkg, pluginPath, warnings, errors)
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
valid: errors.length === 0,
|
|
246
|
-
errors,
|
|
247
|
-
warnings
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private validateProviders(providers: any): ValidationResult {
|
|
252
|
-
const errors: string[] = []
|
|
253
|
-
const warnings: string[] = []
|
|
254
|
-
|
|
255
|
-
if (!Array.isArray(providers)) {
|
|
256
|
-
errors.push('providers must be an array')
|
|
257
|
-
return { valid: false, errors, warnings }
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (providers.length === 0) {
|
|
261
|
-
warnings.push('providers array is empty - add at least one provider definition')
|
|
262
|
-
return { valid: true, errors, warnings }
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const seenIds = new Set<string>()
|
|
266
|
-
|
|
267
|
-
providers.forEach((provider: any, index: number) => {
|
|
268
|
-
if (typeof provider !== 'object' || provider === null) {
|
|
269
|
-
errors.push(`providers[${index}] must be an object`)
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (!provider.id) {
|
|
274
|
-
errors.push(`providers[${index}] missing required field "id"`)
|
|
275
|
-
} else if (typeof provider.id !== 'string') {
|
|
276
|
-
errors.push(`providers[${index}].id must be a string`)
|
|
277
|
-
} else {
|
|
278
|
-
if (seenIds.has(provider.id)) {
|
|
279
|
-
errors.push(`providers[${index}] duplicate provider id "${provider.id}"`)
|
|
280
|
-
}
|
|
281
|
-
seenIds.add(provider.id)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (!provider.name) {
|
|
285
|
-
warnings.push(`providers[${index}] missing recommended field "name"`)
|
|
286
|
-
} else if (typeof provider.name !== 'string') {
|
|
287
|
-
errors.push(`providers[${index}].name must be a string`)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (!provider.credentialsUrl) {
|
|
291
|
-
warnings.push(`providers[${index}] missing recommended field "credentialsUrl" for API key generation link`)
|
|
292
|
-
} else if (typeof provider.credentialsUrl !== 'string') {
|
|
293
|
-
errors.push(`providers[${index}].credentialsUrl must be a string`)
|
|
294
|
-
} else {
|
|
295
|
-
try {
|
|
296
|
-
new URL(provider.credentialsUrl.replace(/\{[^}]+\}/g, 'placeholder'))
|
|
297
|
-
} catch {
|
|
298
|
-
warnings.push(`providers[${index}].credentialsUrl should be a valid URL pattern`)
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (provider.credentialsDocUrl && typeof provider.credentialsDocUrl !== 'string') {
|
|
303
|
-
errors.push(`providers[${index}].credentialsDocUrl must be a string`)
|
|
304
|
-
} else if (provider.credentialsDocUrl) {
|
|
305
|
-
try {
|
|
306
|
-
new URL(provider.credentialsDocUrl)
|
|
307
|
-
} catch {
|
|
308
|
-
warnings.push(`providers[${index}].credentialsDocUrl should be a valid URL`)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
return { valid: errors.length === 0, errors, warnings }
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private validateEnvVariables(envVars: any, validProviderIds: Set<string>): ValidationResult {
|
|
317
|
-
const errors: string[] = []
|
|
318
|
-
const warnings: string[] = []
|
|
319
|
-
|
|
320
|
-
if (!envVars.required && !envVars.optional) {
|
|
321
|
-
warnings.push('envVariables defined but no required or optional sections found')
|
|
322
|
-
return { valid: true, errors, warnings }
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const validateVarArray = (vars: any[], type: string) => {
|
|
326
|
-
if (!Array.isArray(vars)) {
|
|
327
|
-
errors.push(`envVariables.${type} must be an array`)
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
vars.forEach((envVar, index) => {
|
|
332
|
-
if (!envVar.name) {
|
|
333
|
-
errors.push(`envVariables.${type}[${index}] missing name field`)
|
|
334
|
-
}
|
|
335
|
-
if (!envVar.description) {
|
|
336
|
-
warnings.push(`envVariables.${type}[${index}] missing description field`)
|
|
337
|
-
}
|
|
338
|
-
if (!envVar.providerId) {
|
|
339
|
-
warnings.push(`envVariables.${type}[${index}] missing providerId field - should reference a provider in the providers array`)
|
|
340
|
-
} else if (typeof envVar.providerId === 'string') {
|
|
341
|
-
if (validProviderIds.size > 0 && !validProviderIds.has(envVar.providerId)) {
|
|
342
|
-
errors.push(`envVariables.${type}[${index}].providerId "${envVar.providerId}" does not match any provider id in the providers array`)
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
errors.push(`envVariables.${type}[${index}].providerId must be a string`)
|
|
346
|
-
}
|
|
347
|
-
if (typeof envVar.priority !== 'number') {
|
|
348
|
-
warnings.push(`envVariables.${type}[${index}] missing or invalid priority field`)
|
|
349
|
-
}
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (envVars.required) {
|
|
354
|
-
validateVarArray(envVars.required, 'required')
|
|
355
|
-
}
|
|
356
|
-
if (envVars.optional) {
|
|
357
|
-
validateVarArray(envVars.optional, 'optional')
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return { valid: errors.length === 0, errors, warnings }
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private validateEnvExampleFile(envExamplePath: string, envVars: any, providers: any[]): { warnings: string[] } {
|
|
364
|
-
const warnings: string[] = []
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
const envExampleContent = readFileSync(envExamplePath, 'utf-8')
|
|
368
|
-
const envExampleLines = envExampleContent.split('\n')
|
|
369
|
-
|
|
370
|
-
const documentedVars = new Set<string>()
|
|
371
|
-
const providerComments = new Map<string, boolean>()
|
|
372
|
-
|
|
373
|
-
for (let i = 0; i < envExampleLines.length; i++) {
|
|
374
|
-
const line = envExampleLines[i].trim()
|
|
375
|
-
|
|
376
|
-
if (line.startsWith('#') && (line.includes('http://') || line.includes('https://'))) {
|
|
377
|
-
const nextLine = envExampleLines[i + 1]?.trim()
|
|
378
|
-
if (nextLine && nextLine.includes('=')) {
|
|
379
|
-
const varName = nextLine.split('=')[0].trim()
|
|
380
|
-
providerComments.set(varName, true)
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (line && !line.startsWith('#') && line.includes('=')) {
|
|
385
|
-
const varName = line.split('=')[0].trim()
|
|
386
|
-
documentedVars.add(varName)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const allEnvVars: any[] = [
|
|
391
|
-
...(envVars.required || []),
|
|
392
|
-
...(envVars.optional || [])
|
|
393
|
-
]
|
|
394
|
-
|
|
395
|
-
const providerMap = new Map<string, any>()
|
|
396
|
-
if (providers && Array.isArray(providers)) {
|
|
397
|
-
providers.forEach(provider => {
|
|
398
|
-
if (provider.id) {
|
|
399
|
-
providerMap.set(provider.id, provider)
|
|
400
|
-
}
|
|
401
|
-
})
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
for (const envVar of allEnvVars) {
|
|
405
|
-
if (!documentedVars.has(envVar.name)) {
|
|
406
|
-
warnings.push(`Environment variable "${envVar.name}" defined in package.json but not found in .env.example`)
|
|
407
|
-
} else if (!providerComments.has(envVar.name) && envVar.providerId) {
|
|
408
|
-
const provider = providerMap.get(envVar.providerId)
|
|
409
|
-
if (provider && provider.credentialsUrl) {
|
|
410
|
-
warnings.push(`Environment variable "${envVar.name}" in .env.example missing comment with provider credentialsUrl (${provider.credentialsUrl})`)
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
} catch (error) {
|
|
416
|
-
warnings.push(`Could not validate .env.example file: ${error instanceof Error ? error.message : String(error)}`)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return { warnings }
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
private validateScenarios(scenarios: any): ValidationResult {
|
|
423
|
-
const errors: string[] = []
|
|
424
|
-
const warnings: string[] = []
|
|
425
|
-
|
|
426
|
-
if (typeof scenarios !== 'object') {
|
|
427
|
-
errors.push('scenarios must be an object')
|
|
428
|
-
return { valid: false, errors, warnings }
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
Object.entries(scenarios).forEach(([key, value]) => {
|
|
432
|
-
if (Array.isArray(value)) {
|
|
433
|
-
// Array of commands - validate each is a non-empty string
|
|
434
|
-
value.forEach((cmd, index) => {
|
|
435
|
-
if (typeof cmd !== 'string' || cmd.trim().length === 0) {
|
|
436
|
-
errors.push(`scenarios.${key}[${index}] must be a non-empty string`)
|
|
437
|
-
}
|
|
438
|
-
})
|
|
439
|
-
} else if (typeof value === 'string') {
|
|
440
|
-
// Single command string
|
|
441
|
-
if (value.trim().length === 0) {
|
|
442
|
-
errors.push(`scenarios.${key} must be a non-empty string`)
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
445
|
-
errors.push(`scenarios.${key} must be a string or array of strings`)
|
|
446
|
-
}
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
if (Object.keys(scenarios).length === 0) {
|
|
450
|
-
warnings.push('scenarios object is empty - consider adding test scenarios')
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return { valid: errors.length === 0, errors, warnings }
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
private validateOptionalMetadata(pkg: any, pluginPath: string, warnings: string[], errors: string[]): void {
|
|
457
|
-
const hasIcon = pkg.icon && typeof pkg.icon === 'string'
|
|
458
|
-
const hasHomeWebsite = pkg.homepage && typeof pkg.homepage === 'string'
|
|
459
|
-
|
|
460
|
-
if (!hasIcon && !hasHomeWebsite) {
|
|
461
|
-
warnings.push('Missing both "icon" and "homepage" fields - provide at least one for visual representation (icon: "assets/icon.png" or homepage for favicon fallback)')
|
|
462
|
-
} else if (hasIcon && pkg.icon.includes('assets/')) {
|
|
463
|
-
const iconPath = join(pluginPath, pkg.icon)
|
|
464
|
-
if (!existsSync(iconPath)) {
|
|
465
|
-
warnings.push(`icon file not found at path: ${pkg.icon}`)
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (!pkg.categories) {
|
|
470
|
-
warnings.push('Missing optional field "categories" - should be an array of category strings (e.g., ["productivity", "developer-tools"])')
|
|
471
|
-
} else if (!Array.isArray(pkg.categories)) {
|
|
472
|
-
errors.push('categories must be an array')
|
|
473
|
-
} else if (pkg.categories.length === 0) {
|
|
474
|
-
warnings.push('categories array is empty - add at least one category')
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const hasProviders = pkg.providers && Array.isArray(pkg.providers) && pkg.providers.length > 0
|
|
478
|
-
|
|
479
|
-
if (!pkg.homepage) {
|
|
480
|
-
if (!hasProviders) {
|
|
481
|
-
warnings.push('Missing optional field "homepage" - should be a URL to the original website/service this plugin integrates with')
|
|
482
|
-
}
|
|
483
|
-
} else if (typeof pkg.homepage === 'string') {
|
|
484
|
-
try {
|
|
485
|
-
new URL(pkg.homepage)
|
|
486
|
-
} catch {
|
|
487
|
-
errors.push('homepage must be a valid URL')
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (!pkg.faq) {
|
|
492
|
-
warnings.push('Missing optional field "faq" - should be an array of FAQ items with "question" and "answer" attributes')
|
|
493
|
-
} else if (!Array.isArray(pkg.faq)) {
|
|
494
|
-
errors.push('faq must be an array')
|
|
495
|
-
} else {
|
|
496
|
-
pkg.faq.forEach((item: any, index: number) => {
|
|
497
|
-
if (typeof item !== 'object' || item === null) {
|
|
498
|
-
errors.push(`faq[${index}] must be an object with "question" and "answer" fields`)
|
|
499
|
-
} else {
|
|
500
|
-
if (!item.question || typeof item.question !== 'string') {
|
|
501
|
-
errors.push(`faq[${index}] missing or invalid "question" field`)
|
|
502
|
-
}
|
|
503
|
-
if (!item.answer || typeof item.answer !== 'string') {
|
|
504
|
-
errors.push(`faq[${index}] missing or invalid "answer" field`)
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
})
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (pkg.commands) {
|
|
511
|
-
if (!Array.isArray(pkg.commands)) {
|
|
512
|
-
errors.push('commands must be an array')
|
|
513
|
-
} else {
|
|
514
|
-
pkg.commands.forEach((command: any, index: number) => {
|
|
515
|
-
if (typeof command !== 'object' || command === null) {
|
|
516
|
-
errors.push(`commands[${index}] must be an object`)
|
|
517
|
-
return
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (!command.name || typeof command.name !== 'string') {
|
|
521
|
-
errors.push(`commands[${index}] missing or invalid "name" field`)
|
|
522
|
-
}
|
|
523
|
-
if (!command.title || typeof command.title !== 'string') {
|
|
524
|
-
errors.push(`commands[${index}] missing or invalid "title" field`)
|
|
525
|
-
}
|
|
526
|
-
if (!command.description || typeof command.description !== 'string') {
|
|
527
|
-
errors.push(`commands[${index}] missing or invalid "description" field`)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (command.arguments !== undefined) {
|
|
531
|
-
if (!Array.isArray(command.arguments)) {
|
|
532
|
-
errors.push(`commands[${index}].arguments must be an array`)
|
|
533
|
-
} else {
|
|
534
|
-
command.arguments.forEach((arg: any, argIndex: number) => {
|
|
535
|
-
if (typeof arg !== 'object' || arg === null) {
|
|
536
|
-
errors.push(`commands[${index}].arguments[${argIndex}] must be an object`)
|
|
537
|
-
return
|
|
538
|
-
}
|
|
539
|
-
if (!arg.name || typeof arg.name !== 'string') {
|
|
540
|
-
errors.push(`commands[${index}].arguments[${argIndex}] missing or invalid "name" field`)
|
|
541
|
-
}
|
|
542
|
-
if (!arg.placeholder || typeof arg.placeholder !== 'string') {
|
|
543
|
-
errors.push(`commands[${index}].arguments[${argIndex}] missing or invalid "placeholder" field`)
|
|
544
|
-
}
|
|
545
|
-
if (!arg.type || typeof arg.type !== 'string') {
|
|
546
|
-
errors.push(`commands[${index}].arguments[${argIndex}] missing or invalid "type" field`)
|
|
547
|
-
}
|
|
548
|
-
})
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
})
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (!pkg.envVariables) {
|
|
556
|
-
warnings.push('Missing optional field "envVariables" - consider documenting required environment variables with "required" and "optional" arrays')
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
private isValidPluginName(name: string): boolean {
|
|
561
|
-
return /^openpets\/[a-z0-9-]+$/.test(name)
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
private getFolderName(path: string): string {
|
|
565
|
-
return path.split('/').pop() || 'unknown'
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
validatePluginByName(pluginName: string, petsDirectory: string): PackageValidation {
|
|
569
|
-
const pluginPath = join(petsDirectory, pluginName)
|
|
570
|
-
return this.validatePlugin(pluginPath)
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
validateAllPlugins(petsDirectory: string): ValidationReport {
|
|
574
|
-
const plugins = this.getPluginDirectories(petsDirectory)
|
|
575
|
-
const details: PackageValidation[] = []
|
|
576
|
-
|
|
577
|
-
for (const pluginPath of plugins) {
|
|
578
|
-
const validation = this.validatePlugin(pluginPath)
|
|
579
|
-
details.push(validation)
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const passed = details.filter(d => d.result.valid).length
|
|
583
|
-
const failed = details.filter(d => !d.result.valid).length
|
|
584
|
-
|
|
585
|
-
return {
|
|
586
|
-
total: details.length,
|
|
587
|
-
passed,
|
|
588
|
-
failed,
|
|
589
|
-
details
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
private getPluginDirectories(petsDirectory: string): string[] {
|
|
594
|
-
try {
|
|
595
|
-
const result = execSync(`find "${petsDirectory}" -maxdepth 1 -type d -not -name ".*" | sort`, {
|
|
596
|
-
encoding: 'utf-8'
|
|
597
|
-
})
|
|
598
|
-
.split('\n')
|
|
599
|
-
.filter(dir => dir.trim() && dir !== petsDirectory)
|
|
600
|
-
|
|
601
|
-
return result
|
|
602
|
-
} catch (error) {
|
|
603
|
-
console.error(`Error reading pets directory: ${error}`)
|
|
604
|
-
return []
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
private analyzeCodePatterns(pluginPath: string): {
|
|
609
|
-
usesModernPattern: boolean
|
|
610
|
-
usesPluginFactory: boolean
|
|
611
|
-
warnings: string[]
|
|
612
|
-
} {
|
|
613
|
-
const indexPath = join(pluginPath, 'index.ts')
|
|
614
|
-
const warnings: string[] = []
|
|
615
|
-
|
|
616
|
-
if (!existsSync(indexPath)) {
|
|
617
|
-
return { usesModernPattern: false, usesPluginFactory: false, warnings }
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
try {
|
|
621
|
-
const code = readFileSync(indexPath, 'utf-8')
|
|
622
|
-
|
|
623
|
-
const hasToolImport = /import\s+(?:type\s+)?{\s*[^}]*tool[^}]*}\s+from\s+['"]@opencode-ai\/plugin['"]/.test(code)
|
|
624
|
-
const hasZodImport = /import.*zod/.test(code)
|
|
625
|
-
const usesToolBuilder = /tool\s*\(\s*{/.test(code)
|
|
626
|
-
const usesInputSchema = /inputSchema\s*:\s*{/.test(code)
|
|
627
|
-
const usesPluginFactory = /createPlugin|createSingleTool/.test(code) ||
|
|
628
|
-
/from\s+['"].*plugin-factory['"]/.test(code)
|
|
629
|
-
|
|
630
|
-
const usesModernPattern = hasZodImport && usesPluginFactory && !usesInputSchema
|
|
631
|
-
|
|
632
|
-
if (usesInputSchema && !usesToolBuilder) {
|
|
633
|
-
warnings.push('⚠️ LEGACY PATTERN: Uses inputSchema instead of Zod schema')
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (!hasZodImport && usesInputSchema) {
|
|
637
|
-
warnings.push('⚠️ LEGACY PATTERN: Missing Zod import for type-safe schemas')
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (!usesPluginFactory) {
|
|
641
|
-
warnings.push('❌ Must use plugin-factory (createPlugin/createSingleTool) - direct tool() usage is not supported')
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Check for problematic nested schema patterns
|
|
645
|
-
const nestedSchemaPatterns = this.detectNestedSchemas(code)
|
|
646
|
-
if (nestedSchemaPatterns.length > 0) {
|
|
647
|
-
warnings.push('❌ SCHEMA ERROR: Detected unsupported nested schema patterns:')
|
|
648
|
-
nestedSchemaPatterns.forEach(pattern => {
|
|
649
|
-
warnings.push(` ${pattern}`)
|
|
650
|
-
})
|
|
651
|
-
warnings.push(' Use JSON strings instead. See src/core/plugin-factory.ts for details.')
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
return {
|
|
655
|
-
usesModernPattern,
|
|
656
|
-
usesPluginFactory,
|
|
657
|
-
warnings
|
|
658
|
-
}
|
|
659
|
-
} catch (error) {
|
|
660
|
-
warnings.push(`Error analyzing code patterns: ${error instanceof Error ? error.message : String(error)}`)
|
|
661
|
-
return { usesModernPattern: false, usesPluginFactory: false, warnings }
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
private detectNestedSchemas(code: string): string[] {
|
|
666
|
-
const problems: string[] = []
|
|
667
|
-
|
|
668
|
-
// Check for z.array(z.object({...}))
|
|
669
|
-
if (/z\.array\s*\(\s*z\.object\s*\(/.test(code)) {
|
|
670
|
-
problems.push('z.array(z.object({...})) - nested object in array')
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Check for z.object({ field: z.object({...}) })
|
|
674
|
-
if (/z\.object\s*\(\s*\{[^}]*:\s*z\.object\s*\(/.test(code)) {
|
|
675
|
-
problems.push('z.object({ nested: z.object({...}) }) - nested object in object')
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Check for z.record(...)
|
|
679
|
-
if (/z\.record\s*\(/.test(code)) {
|
|
680
|
-
problems.push('z.record(...) - record types are not supported')
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Check for z.union with objects
|
|
684
|
-
if (/z\.union\s*\(\s*\[[^\]]*z\.object/.test(code)) {
|
|
685
|
-
problems.push('z.union([z.object({...}), ...]) - union with complex types')
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Check for z.discriminatedUnion
|
|
689
|
-
if (/z\.discriminatedUnion\s*\(/.test(code)) {
|
|
690
|
-
problems.push('z.discriminatedUnion(...) - discriminated unions not supported')
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
return problems
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
printReport(report: ValidationReport): void {
|
|
697
|
-
console.log('\n🔍 Pets Plugin Validation Report')
|
|
698
|
-
console.log('==================================')
|
|
699
|
-
console.log(`Total plugins: ${report.total}`)
|
|
700
|
-
console.log(`✅ Passed: ${report.passed}`)
|
|
701
|
-
console.log(`❌ Failed: ${report.failed}`)
|
|
702
|
-
console.log('')
|
|
703
|
-
|
|
704
|
-
const modernCount = report.details.filter(d => d.usesModernPattern).length
|
|
705
|
-
const legacyCount = report.details.filter(d => !d.usesModernPattern).length
|
|
706
|
-
|
|
707
|
-
console.log('📊 Code Pattern Analysis:')
|
|
708
|
-
console.log(` ✅ Modern pattern (plugin-factory + Zod): ${modernCount}`)
|
|
709
|
-
console.log(` ⚠️ Legacy pattern (inputSchema): ${legacyCount}`)
|
|
710
|
-
console.log('')
|
|
711
|
-
|
|
712
|
-
for (const detail of report.details) {
|
|
713
|
-
const status = detail.result.valid ? '✅' : '❌'
|
|
714
|
-
const patternBadge = detail.usesModernPattern ? '🆕' : '⏳'
|
|
715
|
-
console.log(`${status} ${patternBadge} ${detail.name}`)
|
|
716
|
-
|
|
717
|
-
if (detail.result.errors.length > 0) {
|
|
718
|
-
detail.result.errors.forEach(error => {
|
|
719
|
-
console.log(` ❌ ${error}`)
|
|
720
|
-
})
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (detail.result.warnings.length > 0) {
|
|
724
|
-
detail.result.warnings.forEach(warning => {
|
|
725
|
-
console.log(` ⚠️ ${warning}`)
|
|
726
|
-
})
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (detail.result.codePatternWarnings && detail.result.codePatternWarnings.length > 0) {
|
|
730
|
-
detail.result.codePatternWarnings.forEach(warning => {
|
|
731
|
-
console.log(` ${warning}`)
|
|
732
|
-
})
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
console.log('')
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
console.log('==================================')
|
|
739
|
-
if (report.failed === 0) {
|
|
740
|
-
console.log('🎉 All plugins passed validation!')
|
|
741
|
-
} else {
|
|
742
|
-
console.log(`💥 ${report.failed} plugin(s) failed validation. Please fix the errors above.`)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
if (legacyCount > 0) {
|
|
746
|
-
console.log(`\n💡 Tip: ${legacyCount} plugin(s) use legacy patterns. Consider migrating to plugin-factory + Zod.`)
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|