glost-registry 0.5.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.
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Enhanced Plugin Registry
3
+ *
4
+ * Plugin registry with discovery, metadata, and validation capabilities.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import type { GLOSTExtension } from "glost-extensions";
10
+ import type {
11
+ PluginMetadata,
12
+ PluginQuery,
13
+ PluginCategory,
14
+ ConflictReport,
15
+ PluginConflict,
16
+ ValidationResult,
17
+ ValidationError,
18
+ ValidationWarning,
19
+ RegistryStatistics,
20
+ } from "./types.js";
21
+
22
+ /**
23
+ * Enhanced Plugin Registry
24
+ *
25
+ * Manages plugin metadata, discovery, validation, and conflict detection.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { PluginRegistry } from "glost-registry";
30
+ *
31
+ * const registry = new PluginRegistry();
32
+ *
33
+ * registry.register(myPlugin, {
34
+ * version: "1.0.0",
35
+ * category: "enhancer",
36
+ * tags: ["transcription", "ipa"]
37
+ * });
38
+ *
39
+ * const plugins = registry.search({ language: "th" });
40
+ * ```
41
+ */
42
+ export class PluginRegistry {
43
+ private plugins = new Map<string, PluginMetadata>();
44
+ private extensions = new Map<string, GLOSTExtension>();
45
+
46
+ /**
47
+ * Register a plugin with metadata
48
+ *
49
+ * @param extension - The plugin extension
50
+ * @param metadata - Plugin metadata
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * registry.register(myExtension, {
55
+ * version: "1.0.0",
56
+ * description: "Adds transcription support",
57
+ * category: "enhancer",
58
+ * tags: ["transcription"],
59
+ * supports: {
60
+ * languages: ["th", "ja"],
61
+ * async: true
62
+ * }
63
+ * });
64
+ * ```
65
+ */
66
+ register(
67
+ extension: GLOSTExtension,
68
+ metadata: Omit<PluginMetadata, "id" | "name">
69
+ ): void {
70
+ const pluginMetadata: PluginMetadata = {
71
+ ...metadata,
72
+ id: extension.id,
73
+ name: extension.name,
74
+ description: extension.description || metadata.description,
75
+ registeredAt: new Date(),
76
+ };
77
+
78
+ if (this.plugins.has(extension.id)) {
79
+ console.warn(
80
+ `Plugin "${extension.id}" is already registered. Overwriting.`
81
+ );
82
+ }
83
+
84
+ this.plugins.set(extension.id, pluginMetadata);
85
+ this.extensions.set(extension.id, extension);
86
+ }
87
+
88
+ /**
89
+ * Get plugin metadata
90
+ *
91
+ * @param pluginId - Plugin ID
92
+ * @returns Plugin metadata or undefined
93
+ */
94
+ getMetadata(pluginId: string): PluginMetadata | undefined {
95
+ return this.plugins.get(pluginId);
96
+ }
97
+
98
+ /**
99
+ * Get plugin extension
100
+ *
101
+ * @param pluginId - Plugin ID
102
+ * @returns Plugin extension or undefined
103
+ */
104
+ getExtension(pluginId: string): GLOSTExtension | undefined {
105
+ return this.extensions.get(pluginId);
106
+ }
107
+
108
+ /**
109
+ * List all plugins
110
+ *
111
+ * @returns Array of all plugin metadata
112
+ */
113
+ list(): PluginMetadata[] {
114
+ return Array.from(this.plugins.values());
115
+ }
116
+
117
+ /**
118
+ * Search for plugins
119
+ *
120
+ * @param query - Search query
121
+ * @returns Array of matching plugin metadata
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * // Search by keyword
126
+ * const results = registry.search({ keyword: "transcription" });
127
+ *
128
+ * // Filter by category
129
+ * const enhancers = registry.search({ category: "enhancer" });
130
+ *
131
+ * // Filter by language
132
+ * const thaiPlugins = registry.search({ language: "th" });
133
+ *
134
+ * // Multiple filters
135
+ * const results = registry.search({
136
+ * category: "enhancer",
137
+ * language: "th",
138
+ * tags: ["transcription"]
139
+ * });
140
+ * ```
141
+ */
142
+ search(query: PluginQuery): PluginMetadata[] {
143
+ let results = this.list();
144
+
145
+ // Filter by keyword
146
+ if (query.keyword) {
147
+ const keyword = query.keyword.toLowerCase();
148
+ results = results.filter((plugin) => {
149
+ return (
150
+ plugin.name.toLowerCase().includes(keyword) ||
151
+ plugin.description.toLowerCase().includes(keyword) ||
152
+ plugin.tags.some((tag) => tag.toLowerCase().includes(keyword))
153
+ );
154
+ });
155
+ }
156
+
157
+ // Filter by category
158
+ if (query.category) {
159
+ results = results.filter((plugin) => plugin.category === query.category);
160
+ }
161
+
162
+ // Filter by language
163
+ if (query.language) {
164
+ results = results.filter((plugin) => {
165
+ return (
166
+ !plugin.supports.languages ||
167
+ plugin.supports.languages.includes(query.language!)
168
+ );
169
+ });
170
+ }
171
+
172
+ // Filter by tags
173
+ if (query.tags && query.tags.length > 0) {
174
+ results = results.filter((plugin) => {
175
+ return query.tags!.some((tag) => plugin.tags.includes(tag));
176
+ });
177
+ }
178
+
179
+ // Filter by author
180
+ if (query.author) {
181
+ results = results.filter((plugin) => plugin.author === query.author);
182
+ }
183
+
184
+ // Filter by capability
185
+ if (query.capability) {
186
+ results = results.filter((plugin) => {
187
+ return plugin.supports.custom?.[query.capability!] === true;
188
+ });
189
+ }
190
+
191
+ return results;
192
+ }
193
+
194
+ /**
195
+ * Get plugins by category
196
+ *
197
+ * @param category - Plugin category
198
+ * @returns Array of plugin metadata
199
+ */
200
+ getByCategory(category: PluginCategory): PluginMetadata[] {
201
+ return this.list().filter((plugin) => plugin.category === category);
202
+ }
203
+
204
+ /**
205
+ * Get plugins that support a language
206
+ *
207
+ * @param language - Language code
208
+ * @returns Array of plugin metadata
209
+ */
210
+ getLanguageSupport(language: string): PluginMetadata[] {
211
+ return this.list().filter((plugin) => {
212
+ return (
213
+ !plugin.supports.languages ||
214
+ plugin.supports.languages.includes(language)
215
+ );
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Check if a plugin supports a language
221
+ *
222
+ * @param pluginId - Plugin ID
223
+ * @param language - Language code
224
+ * @returns True if supported
225
+ */
226
+ isLanguageSupported(pluginId: string, language: string): boolean {
227
+ const metadata = this.getMetadata(pluginId);
228
+ if (!metadata) {
229
+ return false;
230
+ }
231
+ return (
232
+ !metadata.supports.languages ||
233
+ metadata.supports.languages.includes(language)
234
+ );
235
+ }
236
+
237
+ /**
238
+ * Check plugin compatibility
239
+ *
240
+ * @param pluginId - Plugin ID
241
+ * @param version - Version to check (semver)
242
+ * @returns True if compatible
243
+ */
244
+ isCompatible(pluginId: string, version: string): boolean {
245
+ const metadata = this.getMetadata(pluginId);
246
+ if (!metadata) {
247
+ return false;
248
+ }
249
+ // Simple version check (could use semver library for proper range checking)
250
+ return metadata.version === version || metadata.version >= version;
251
+ }
252
+
253
+ /**
254
+ * Check for conflicts between plugins
255
+ *
256
+ * @param pluginIds - Plugin IDs to check
257
+ * @returns Conflict report
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * const report = registry.checkConflicts(["plugin1", "plugin2", "plugin3"]);
262
+ * if (report.hasConflicts) {
263
+ * for (const conflict of report.conflicts) {
264
+ * console.error(`Conflict: ${conflict.plugin1} <-> ${conflict.plugin2}`);
265
+ * console.error(`Reason: ${conflict.reason}`);
266
+ * }
267
+ * }
268
+ * ```
269
+ */
270
+ checkConflicts(pluginIds: string[]): ConflictReport {
271
+ const conflicts: PluginConflict[] = [];
272
+
273
+ for (let i = 0; i < pluginIds.length; i++) {
274
+ const pluginId1 = pluginIds[i]!;
275
+ const metadata1 = this.getMetadata(pluginId1);
276
+
277
+ if (!metadata1) {
278
+ continue;
279
+ }
280
+
281
+ for (let j = i + 1; j < pluginIds.length; j++) {
282
+ const pluginId2 = pluginIds[j]!;
283
+ const metadata2 = this.getMetadata(pluginId2);
284
+
285
+ if (!metadata2) {
286
+ continue;
287
+ }
288
+
289
+ // Check explicit conflicts
290
+ if (metadata1.conflicts?.includes(pluginId2)) {
291
+ conflicts.push({
292
+ plugin1: pluginId1,
293
+ plugin2: pluginId2,
294
+ reason: `${pluginId1} declares conflict with ${pluginId2}`,
295
+ severity: "error",
296
+ });
297
+ }
298
+
299
+ if (metadata2.conflicts?.includes(pluginId1)) {
300
+ conflicts.push({
301
+ plugin1: pluginId2,
302
+ plugin2: pluginId1,
303
+ reason: `${pluginId2} declares conflict with ${pluginId1}`,
304
+ severity: "error",
305
+ });
306
+ }
307
+
308
+ // Check for field ownership conflicts
309
+ const ext1 = this.getExtension(pluginId1);
310
+ const ext2 = this.getExtension(pluginId2);
311
+
312
+ if (ext1?.provides?.extras && ext2?.provides?.extras) {
313
+ const sharedFields = ext1.provides.extras.filter((field) =>
314
+ ext2.provides!.extras!.includes(field)
315
+ );
316
+
317
+ if (sharedFields.length > 0) {
318
+ conflicts.push({
319
+ plugin1: pluginId1,
320
+ plugin2: pluginId2,
321
+ reason: `Both plugins provide the same fields: ${sharedFields.join(", ")}`,
322
+ severity: "warning",
323
+ });
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ return {
330
+ hasConflicts: conflicts.length > 0,
331
+ conflicts,
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Resolve dependencies for plugins
337
+ *
338
+ * @param pluginIds - Plugin IDs to resolve
339
+ * @returns Array of plugin IDs in dependency order
340
+ * @throws {Error} If circular dependencies detected or plugin not found
341
+ */
342
+ resolveDependencies(pluginIds: string[]): string[] {
343
+ const resolved: string[] = [];
344
+ const visited = new Set<string>();
345
+ const visiting = new Set<string>();
346
+
347
+ const visit = (id: string): void => {
348
+ if (visiting.has(id)) {
349
+ throw new Error(
350
+ `Circular dependency detected involving plugin: ${id}`
351
+ );
352
+ }
353
+
354
+ if (visited.has(id)) {
355
+ return;
356
+ }
357
+
358
+ const extension = this.getExtension(id);
359
+ if (!extension) {
360
+ throw new Error(`Plugin "${id}" not found in registry`);
361
+ }
362
+
363
+ visiting.add(id);
364
+
365
+ // Visit dependencies first
366
+ if (extension.dependencies) {
367
+ for (const depId of extension.dependencies) {
368
+ if (!pluginIds.includes(depId)) {
369
+ // Dependency not in the list, skip
370
+ continue;
371
+ }
372
+ visit(depId);
373
+ }
374
+ }
375
+
376
+ visiting.delete(id);
377
+ visited.add(id);
378
+ resolved.push(id);
379
+ };
380
+
381
+ for (const id of pluginIds) {
382
+ visit(id);
383
+ }
384
+
385
+ return resolved;
386
+ }
387
+
388
+ /**
389
+ * Validate a plugin configuration
390
+ *
391
+ * @param pluginId - Plugin ID
392
+ * @param options - Plugin options to validate
393
+ * @returns Validation result
394
+ */
395
+ validate(pluginId: string, options?: any): ValidationResult {
396
+ const metadata = this.getMetadata(pluginId);
397
+ const errors: ValidationError[] = [];
398
+ const warnings: ValidationWarning[] = [];
399
+
400
+ if (!metadata) {
401
+ errors.push({
402
+ plugin: pluginId,
403
+ message: `Plugin "${pluginId}" not found in registry`,
404
+ code: "PLUGIN_NOT_FOUND",
405
+ });
406
+ return { valid: false, errors, warnings };
407
+ }
408
+
409
+ // Validate options against schema
410
+ if (metadata.options && options) {
411
+ const schema = metadata.options;
412
+
413
+ // Check required properties
414
+ if (schema.required) {
415
+ for (const required of schema.required) {
416
+ if (!(required in options)) {
417
+ errors.push({
418
+ plugin: pluginId,
419
+ message: `Required option "${required}" is missing`,
420
+ code: "MISSING_REQUIRED_OPTION",
421
+ });
422
+ }
423
+ }
424
+ }
425
+
426
+ // Check property types (basic validation)
427
+ if (schema.properties) {
428
+ for (const [key, value] of Object.entries(options)) {
429
+ const propSchema = schema.properties[key];
430
+ if (!propSchema) {
431
+ if (!schema.additionalProperties) {
432
+ warnings.push({
433
+ plugin: pluginId,
434
+ message: `Unknown option "${key}"`,
435
+ code: "UNKNOWN_OPTION",
436
+ });
437
+ }
438
+ continue;
439
+ }
440
+
441
+ const actualType = typeof value;
442
+ const expectedType = propSchema.type;
443
+
444
+ if (
445
+ actualType !== expectedType &&
446
+ !(expectedType === "array" && Array.isArray(value))
447
+ ) {
448
+ errors.push({
449
+ plugin: pluginId,
450
+ message: `Option "${key}" should be ${expectedType}, got ${actualType}`,
451
+ code: "INVALID_OPTION_TYPE",
452
+ });
453
+ }
454
+ }
455
+ }
456
+ }
457
+
458
+ // Check requirements
459
+ if (metadata.requires?.glostVersion) {
460
+ // Could check against actual glost version
461
+ // For now, just emit a warning
462
+ warnings.push({
463
+ plugin: pluginId,
464
+ message: `Plugin requires GLOST version ${metadata.requires.glostVersion}`,
465
+ code: "VERSION_REQUIREMENT",
466
+ });
467
+ }
468
+
469
+ return {
470
+ valid: errors.length === 0,
471
+ errors,
472
+ warnings,
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Get registry statistics
478
+ *
479
+ * @returns Registry statistics
480
+ */
481
+ getStatistics(): RegistryStatistics {
482
+ const plugins = this.list();
483
+
484
+ const byCategory: Record<PluginCategory, number> = {
485
+ transformer: 0,
486
+ enhancer: 0,
487
+ generator: 0,
488
+ analyzer: 0,
489
+ utility: 0,
490
+ };
491
+
492
+ const byLanguage: Record<string, number> = {};
493
+ const tagCounts: Record<string, number> = {};
494
+
495
+ for (const plugin of plugins) {
496
+ byCategory[plugin.category]++;
497
+
498
+ if (plugin.supports.languages) {
499
+ for (const lang of plugin.supports.languages) {
500
+ byLanguage[lang] = (byLanguage[lang] || 0) + 1;
501
+ }
502
+ }
503
+
504
+ for (const tag of plugin.tags) {
505
+ tagCounts[tag] = (tagCounts[tag] || 0) + 1;
506
+ }
507
+ }
508
+
509
+ const topTags = Object.entries(tagCounts)
510
+ .sort((a, b) => b[1] - a[1])
511
+ .slice(0, 10)
512
+ .map(([tag, count]) => ({ tag, count }));
513
+
514
+ return {
515
+ total: plugins.length,
516
+ byCategory,
517
+ byLanguage,
518
+ topTags,
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Check if a plugin is registered
524
+ *
525
+ * @param pluginId - Plugin ID
526
+ * @returns True if registered
527
+ */
528
+ has(pluginId: string): boolean {
529
+ return this.plugins.has(pluginId);
530
+ }
531
+
532
+ /**
533
+ * Unregister a plugin
534
+ *
535
+ * @param pluginId - Plugin ID
536
+ * @returns True if unregistered
537
+ */
538
+ unregister(pluginId: string): boolean {
539
+ const hasPlugin = this.plugins.has(pluginId);
540
+ this.plugins.delete(pluginId);
541
+ this.extensions.delete(pluginId);
542
+ return hasPlugin;
543
+ }
544
+
545
+ /**
546
+ * Clear all plugins
547
+ */
548
+ clear(): void {
549
+ this.plugins.clear();
550
+ this.extensions.clear();
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Global plugin registry instance
556
+ */
557
+ export const pluginRegistry = new PluginRegistry();