openpets 1.0.5 → 1.0.6

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.
Files changed (96) hide show
  1. package/dist/data/api.json +3172 -0
  2. package/dist/src/core/ai-client-base/index.d.ts +47 -0
  3. package/dist/src/core/ai-client-base/index.d.ts.map +1 -0
  4. package/dist/src/core/ai-client-base/index.js +168 -0
  5. package/dist/src/core/ai-client-base/index.js.map +1 -0
  6. package/dist/src/core/browser.d.ts +10 -0
  7. package/dist/src/core/browser.d.ts.map +1 -0
  8. package/{browser.ts → dist/src/core/browser.js} +4 -4
  9. package/dist/src/core/browser.js.map +1 -0
  10. package/dist/src/core/build-pet.d.ts +2 -0
  11. package/dist/src/core/build-pet.d.ts.map +1 -0
  12. package/dist/src/core/build-pet.js +364 -0
  13. package/dist/src/core/build-pet.js.map +1 -0
  14. package/dist/src/core/cli.d.ts +3 -0
  15. package/dist/src/core/cli.d.ts.map +1 -0
  16. package/dist/src/core/cli.js +244 -0
  17. package/dist/src/core/cli.js.map +1 -0
  18. package/dist/src/core/config-manager.d.ts +13 -0
  19. package/dist/src/core/config-manager.d.ts.map +1 -0
  20. package/dist/src/core/config-manager.js +59 -0
  21. package/dist/src/core/config-manager.js.map +1 -0
  22. package/dist/src/core/deploy-pet.d.ts +2 -0
  23. package/dist/src/core/deploy-pet.d.ts.map +1 -0
  24. package/dist/src/core/deploy-pet.js +66 -0
  25. package/dist/src/core/deploy-pet.js.map +1 -0
  26. package/dist/src/core/index.d.ts +11 -0
  27. package/dist/src/core/index.d.ts.map +1 -0
  28. package/dist/src/core/index.js +11 -0
  29. package/dist/src/core/index.js.map +1 -0
  30. package/dist/src/core/local-cache.d.ts +69 -0
  31. package/dist/src/core/local-cache.d.ts.map +1 -0
  32. package/dist/src/core/local-cache.js +212 -0
  33. package/dist/src/core/local-cache.js.map +1 -0
  34. package/dist/src/core/logger.d.ts.map +1 -0
  35. package/{logger.js → dist/src/core/logger.js} +8 -9
  36. package/dist/src/core/logger.js.map +1 -0
  37. package/dist/src/core/mcp-factory.d.ts +12 -0
  38. package/dist/src/core/mcp-factory.d.ts.map +1 -0
  39. package/dist/src/core/mcp-factory.js +143 -0
  40. package/dist/src/core/mcp-factory.js.map +1 -0
  41. package/dist/src/core/mcp-server.d.ts +3 -0
  42. package/dist/src/core/mcp-server.d.ts.map +1 -0
  43. package/dist/src/core/mcp-server.js +55 -0
  44. package/dist/src/core/mcp-server.js.map +1 -0
  45. package/dist/src/core/migrate-plugin.d.ts +15 -0
  46. package/dist/src/core/migrate-plugin.d.ts.map +1 -0
  47. package/dist/src/core/migrate-plugin.js +181 -0
  48. package/dist/src/core/migrate-plugin.js.map +1 -0
  49. package/dist/src/core/pets-registry.d.ts +47 -0
  50. package/dist/src/core/pets-registry.d.ts.map +1 -0
  51. package/dist/src/core/pets-registry.js +109 -0
  52. package/dist/src/core/pets-registry.js.map +1 -0
  53. package/dist/src/core/plugin-factory.d.ts +58 -0
  54. package/dist/src/core/plugin-factory.d.ts.map +1 -0
  55. package/dist/src/core/plugin-factory.js +212 -0
  56. package/dist/src/core/plugin-factory.js.map +1 -0
  57. package/dist/src/core/prompt-utils.d.ts +14 -0
  58. package/dist/src/core/prompt-utils.d.ts.map +1 -0
  59. package/dist/src/core/prompt-utils.js +106 -0
  60. package/dist/src/core/prompt-utils.js.map +1 -0
  61. package/dist/src/core/schema-helpers.d.ts +33 -0
  62. package/dist/src/core/schema-helpers.d.ts.map +1 -0
  63. package/dist/src/core/schema-helpers.js +46 -0
  64. package/dist/src/core/schema-helpers.js.map +1 -0
  65. package/dist/src/core/search-pets.d.ts +29 -0
  66. package/dist/src/core/search-pets.d.ts.map +1 -0
  67. package/dist/src/core/search-pets.js +196 -0
  68. package/dist/src/core/search-pets.js.map +1 -0
  69. package/dist/src/core/types.d.ts +63 -0
  70. package/dist/src/core/types.d.ts.map +1 -0
  71. package/dist/src/core/types.js +2 -0
  72. package/dist/src/core/types.js.map +1 -0
  73. package/dist/src/core/validate-pet.d.ts +40 -0
  74. package/dist/src/core/validate-pet.d.ts.map +1 -0
  75. package/dist/src/core/validate-pet.js +650 -0
  76. package/dist/src/core/validate-pet.js.map +1 -0
  77. package/package.json +8 -21
  78. package/ai-client-base/index.ts +0 -229
  79. package/build-pet.ts +0 -429
  80. package/cli.ts +0 -268
  81. package/config-manager.ts +0 -82
  82. package/deploy-pet.ts +0 -91
  83. package/index.ts +0 -10
  84. package/local-cache.ts +0 -280
  85. package/logger.ts +0 -143
  86. package/mcp-factory.ts +0 -180
  87. package/mcp-server.ts +0 -69
  88. package/migrate-plugin.ts +0 -220
  89. package/pets-registry.ts +0 -160
  90. package/plugin-factory.ts +0 -300
  91. package/prompt-utils.ts +0 -130
  92. package/schema-helpers.ts +0 -59
  93. package/search-pets.ts +0 -267
  94. package/types.ts +0 -68
  95. package/validate-pet.ts +0 -749
  96. /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
- }