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.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/discovery.d.ts +146 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +262 -0
- package/dist/discovery.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +193 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +466 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +208 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +53 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +204 -0
- package/dist/validation.js.map +1 -0
- package/package.json +45 -0
- package/src/discovery.ts +315 -0
- package/src/index.ts +50 -0
- package/src/registry.ts +557 -0
- package/src/types.ts +275 -0
- package/src/validation.ts +240 -0
package/src/registry.ts
ADDED
|
@@ -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();
|