@apiquest/fracture 1.0.4 → 1.0.5

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 (59) hide show
  1. package/README.md +90 -2
  2. package/dist/CollectionRunner.d.ts +2 -0
  3. package/dist/CollectionRunner.d.ts.map +1 -1
  4. package/dist/CollectionRunner.js +20 -2
  5. package/dist/CollectionRunner.js.map +1 -1
  6. package/dist/LibraryLoader.d.ts +49 -0
  7. package/dist/LibraryLoader.d.ts.map +1 -0
  8. package/dist/LibraryLoader.js +198 -0
  9. package/dist/LibraryLoader.js.map +1 -0
  10. package/dist/PluginLoader.d.ts.map +1 -1
  11. package/dist/PluginLoader.js +9 -6
  12. package/dist/PluginLoader.js.map +1 -1
  13. package/dist/PluginResolver.d.ts +1 -1
  14. package/dist/PluginResolver.d.ts.map +1 -1
  15. package/dist/PluginResolver.js +1 -1
  16. package/dist/PluginResolver.js.map +1 -1
  17. package/dist/ScriptEngine.d.ts +2 -1
  18. package/dist/ScriptEngine.d.ts.map +1 -1
  19. package/dist/ScriptEngine.js +15 -8
  20. package/dist/ScriptEngine.js.map +1 -1
  21. package/dist/cli/index.js +35 -3
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/plugin-commands.d.ts.map +1 -1
  24. package/dist/cli/plugin-commands.js +47 -81
  25. package/dist/cli/plugin-commands.js.map +1 -1
  26. package/dist/cli/plugin-installer.d.ts +48 -0
  27. package/dist/cli/plugin-installer.d.ts.map +1 -0
  28. package/dist/cli/plugin-installer.js +136 -0
  29. package/dist/cli/plugin-installer.js.map +1 -0
  30. package/dist/cli/plugin-registry.d.ts +17 -0
  31. package/dist/cli/plugin-registry.d.ts.map +1 -0
  32. package/dist/cli/plugin-registry.js +77 -0
  33. package/dist/cli/plugin-registry.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/CollectionAnalyzer.ts +0 -102
  36. package/src/CollectionRunner.ts +0 -1423
  37. package/src/CollectionRunner.types.ts +0 -9
  38. package/src/CollectionValidator.ts +0 -289
  39. package/src/ConsoleReporter.ts +0 -143
  40. package/src/CookieJar.ts +0 -258
  41. package/src/DagScheduler.ts +0 -439
  42. package/src/Logger.ts +0 -85
  43. package/src/PluginLoader.ts +0 -126
  44. package/src/PluginManager.ts +0 -208
  45. package/src/PluginResolver.ts +0 -154
  46. package/src/QuestAPI.ts +0 -764
  47. package/src/QuestAPI.types.ts +0 -33
  48. package/src/QuestTestAPI.ts +0 -164
  49. package/src/RequestFilter.ts +0 -224
  50. package/src/ScriptEngine.ts +0 -219
  51. package/src/ScriptValidator.ts +0 -428
  52. package/src/TaskGraph.ts +0 -598
  53. package/src/TestCounter.ts +0 -109
  54. package/src/VariableResolver.ts +0 -114
  55. package/src/cli/index.ts +0 -480
  56. package/src/cli/plugin-commands.ts +0 -342
  57. package/src/cli/plugin-discovery.ts +0 -44
  58. package/src/index.ts +0 -24
  59. package/src/utils.ts +0 -52
@@ -1,439 +0,0 @@
1
- import { EventEmitter } from 'events';
2
- import type { TaskNode } from './TaskGraph.js';
3
- import type { ExecutionContext, RequestResult, Request, Folder, ScriptResult } from '@apiquest/types';
4
- import { ScriptType } from '@apiquest/types';
5
- import { isNullOrWhitespace } from './utils.js';
6
- import { Logger } from './Logger.js';
7
-
8
- /**
9
- * Callback interface for DagScheduler to call back into CollectionRunner
10
- */
11
- export interface DagExecutionCallbacks {
12
- // Flags are passed for every node execution.
13
- // When skip is true: no script execution, emit skipped assertions only
14
- // When bail is true: suppress assertions and scripts
15
- // Script execution (always through queue)
16
- executeScript(
17
- script: string,
18
- scriptType: ScriptType,
19
- context: ExecutionContext,
20
- node: TaskNode,
21
- flags: { skip: boolean; bail: boolean }
22
- ): Promise<ScriptResult>;
23
-
24
- // Folder lifecycle (always through queue)
25
- executeFolderEnter(
26
- node: TaskNode,
27
- context: ExecutionContext,
28
- flags: { skip: boolean; bail: boolean }
29
- ): Promise<void>;
30
-
31
- executeFolderExit(
32
- node: TaskNode,
33
- context: ExecutionContext,
34
- flags: { skip: boolean; bail: boolean }
35
- ): Promise<void>;
36
-
37
- // Request execution (I/O only, scripts handled separately)
38
- executeRequestIO(
39
- node: TaskNode,
40
- context: ExecutionContext,
41
- flags: { skip: boolean; bail: boolean }
42
- ): Promise<RequestResult>;
43
-
44
- // Condition evaluation
45
- evaluateCondition(
46
- condition: string,
47
- context: ExecutionContext
48
- ): Promise<boolean>;
49
-
50
- // Abort check
51
- isAborted(): boolean;
52
- }
53
-
54
- /**
55
- * Async queue for workers
56
- */
57
- class AsyncQueue<T> extends EventEmitter {
58
- private items: T[] = [];
59
- private waiting: Array<(value: T | null) => void> = [];
60
- private closed = false;
61
-
62
- enqueue(item: T): void {
63
- if (this.closed) {
64
- throw new Error('Queue is closed');
65
- }
66
-
67
- // If someone is waiting, give it to them immediately
68
- const resolver = this.waiting.shift();
69
- if (resolver !== undefined) {
70
- resolver(item);
71
- return;
72
- }
73
-
74
- // Otherwise, queue it
75
- this.items.push(item);
76
- this.emit('item');
77
- }
78
-
79
- async dequeue(): Promise<T | null> {
80
- // If items available, return immediately
81
- if (this.items.length > 0) {
82
- return this.items.shift()!;
83
- }
84
-
85
- // If closed and empty, return null
86
- if (this.closed) {
87
- return null;
88
- }
89
-
90
- // Wait for item or close
91
- return new Promise<T | null>((resolve) => {
92
- this.waiting.push(resolve);
93
- });
94
- }
95
-
96
- close(): void {
97
- this.closed = true;
98
- // Resolve all waiting promises with null
99
- for (const resolver of this.waiting) {
100
- resolver(null);
101
- }
102
- this.waiting = [];
103
- }
104
-
105
- get length(): number {
106
- return this.items.length;
107
- }
108
- }
109
-
110
- /**
111
- * DagScheduler coordinates parallel execution via DAG
112
- */
113
- export class DagScheduler {
114
- private callbacks: DagExecutionCallbacks;
115
- private maxConcurrency: number;
116
- private logger: Logger;
117
-
118
- // Tracking
119
- private completedNodes = new Set<string>();
120
- private totalNodes = 0;
121
- private aborted = false;
122
- private skippedNodes = new Set<string>();
123
- private graph?: import('./TaskGraph.js').TaskGraph;
124
-
125
- // Queues
126
- private scriptQueue = new AsyncQueue<TaskNode>();
127
- private requestQueue = new AsyncQueue<TaskNode>();
128
-
129
- constructor(callbacks: DagExecutionCallbacks, maxConcurrency: number, baseLogger?: Logger) {
130
- this.callbacks = callbacks;
131
- this.maxConcurrency = maxConcurrency;
132
- this.logger = baseLogger?.createLogger('DagScheduler') ?? new Logger('DagScheduler');
133
- }
134
-
135
- /**
136
- * Execute the DAG
137
- * Returns array of RequestResult for all requests that were executed
138
- */
139
- public async execute(
140
- graph: import('./TaskGraph.js').TaskGraph,
141
- context: ExecutionContext
142
- ): Promise<RequestResult[]> {
143
- const results: RequestResult[] = [];
144
-
145
- // Initialize tracking
146
- this.totalNodes = graph.getNodes().size;
147
- this.completedNodes.clear();
148
- this.skippedNodes.clear();
149
- this.aborted = false;
150
- this.graph = graph;
151
-
152
- this.logger.debug(`Starting DAG execution: ${this.totalNodes} nodes, maxConcurrency=${this.maxConcurrency}`);
153
-
154
- // Get initial ready nodes
155
- const readyNodes = graph.getReadyNodes();
156
- this.logger.debug(`Initial ready nodes: ${readyNodes.length}`);
157
-
158
- if (readyNodes.length === 0) {
159
- // Empty DAG
160
- return results;
161
- }
162
-
163
- // Enqueue initial ready nodes
164
- for (const node of readyNodes) {
165
- this.enqueueNode(node);
166
- }
167
-
168
- // Start workers
169
- const workers: Promise<void>[] = [];
170
-
171
- // Script worker (single threaded)
172
- workers.push(this.runScriptWorker(graph, context, results));
173
-
174
- // Request workers (parallel pool)
175
- for (let i = 0; i < this.maxConcurrency; i++) {
176
- workers.push(this.runRequestWorker(graph, context, results));
177
- }
178
-
179
- // Wait for all workers to complete
180
- await Promise.all(workers);
181
-
182
- return results;
183
- }
184
-
185
- private async runScriptWorker(
186
- graph: import('./TaskGraph.js').TaskGraph,
187
- context: ExecutionContext,
188
- results: RequestResult[]
189
- ): Promise<void> {
190
- while (true) {
191
- const node = await this.scriptQueue.dequeue();
192
- if (node === null) {
193
- // Queue closed, we're done
194
- break;
195
- }
196
-
197
- // Check abort
198
- if (this.aborted || this.callbacks.isAborted()) {
199
- this.markComplete(node.id, graph);
200
- continue;
201
- }
202
-
203
- // Check if node was skipped (e.g., by folder condition)
204
- if (this.skippedNodes.has(node.id)) {
205
- // Node already skipped by skipSubtree(), just mark complete
206
- this.markComplete(node.id, graph);
207
- continue;
208
- }
209
-
210
- // Execute script node
211
- await this.executeScriptNode(node, context, results);
212
-
213
- // Mark complete and enqueue newly-ready nodes
214
- this.markComplete(node.id, graph);
215
- }
216
- }
217
-
218
- private async runRequestWorker(
219
- graph: import('./TaskGraph.js').TaskGraph,
220
- context: ExecutionContext,
221
- results: RequestResult[]
222
- ): Promise<void> {
223
- while (true) {
224
- const node = await this.requestQueue.dequeue();
225
- if (node === null) {
226
- // Queue closed, we're done
227
- break;
228
- }
229
-
230
- // Check abort
231
- if (this.aborted || this.callbacks.isAborted()) {
232
- this.markComplete(node.id, graph);
233
- continue;
234
- }
235
-
236
- // Check if node was skipped (e.g., by folder condition)
237
- if (this.skippedNodes.has(node.id)) {
238
- // Node already skipped by skipSubtree(), just mark complete
239
- this.markComplete(node.id, graph);
240
- continue;
241
- }
242
-
243
- // Execute request node
244
- await this.executeRequestNode(node, context, results);
245
-
246
- // Mark complete and enqueue newly-ready nodes
247
- this.markComplete(node.id, graph);
248
- }
249
- }
250
-
251
- private async executeScriptNode(
252
- node: TaskNode,
253
- context: ExecutionContext,
254
- results: RequestResult[]
255
- ): Promise<void> {
256
- const flags = {
257
- skip: false,
258
- bail: this.aborted || this.callbacks.isAborted()
259
- };
260
- // Handle folder lifecycle nodes
261
- if (node.type === 'folder-enter') {
262
- if (node.condition !== undefined) {
263
- this.logger.debug(`Evaluating folder condition for ${node.name}: ${node.condition}`);
264
- const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
265
- this.logger.debug(`Folder condition result for ${node.name}: ${conditionPassed}`);
266
- if (!conditionPassed) {
267
- this.logger.debug(`Skipping folder subtree for ${node.name} (condition=false)`);
268
- await this.skipSubtree(node.id, context, results, 'condition-false');
269
- return;
270
- }
271
- }
272
-
273
- // Execute folder enter lifecycle (PUSH scope + emit beforeFolder)
274
- await this.callbacks.executeFolderEnter(node, context, flags);
275
- return;
276
- }
277
-
278
- if (node.type === 'folder-exit') {
279
- // Execute folder exit lifecycle (POP scope + emit afterFolder)
280
- await this.callbacks.executeFolderExit(node, context, flags);
281
- return;
282
- }
283
-
284
- // Handle regular script nodes
285
- // Evaluate condition if present
286
- if (node.condition !== undefined) {
287
- const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
288
- if (!conditionPassed) {
289
- // Skip this node
290
- return;
291
- }
292
- }
293
-
294
- // Execute script if present
295
- if (!isNullOrWhitespace(node.script) && node.scriptType !== undefined) {
296
- const scriptResult = await this.callbacks.executeScript(
297
- node.script!,
298
- node.scriptType,
299
- context,
300
- node,
301
- flags
302
- );
303
-
304
- // Handle test failures (trigger bail if enabled)
305
- // Note: callbacks.executeScript handles bail internally
306
- }
307
- }
308
-
309
- private async executeRequestNode(
310
- node: TaskNode,
311
- context: ExecutionContext,
312
- results: RequestResult[]
313
- ): Promise<void> {
314
- const flags = {
315
- skip: false,
316
- bail: this.aborted || this.callbacks.isAborted()
317
- };
318
-
319
- if (flags.bail) {
320
- this.logger.debug(`Bail active, skipping request ${node.id}`);
321
- return;
322
- }
323
- // Evaluate condition if present
324
- if (node.condition !== undefined) {
325
- const conditionPassed = await this.callbacks.evaluateCondition(node.condition, context);
326
- if (!conditionPassed) {
327
- flags.skip = true;
328
- this.logger.debug(`Request ${node.id} skipped by condition`);
329
- // Add skipped result
330
- const request = node.item as Request;
331
- const skippedResult: RequestResult = {
332
- requestId: request.id,
333
- requestName: request.name,
334
- path: node.path,
335
- success: true,
336
- tests: [],
337
- duration: 0,
338
- iteration: context.iterationCurrent,
339
- scriptError: 'Skipped by condition'
340
- };
341
- results.push(skippedResult);
342
- return;
343
- }
344
- }
345
-
346
- // Execute request I/O
347
- const result = await this.callbacks.executeRequestIO(node, context, flags);
348
- results.push(result);
349
- }
350
-
351
- private async skipSubtree(
352
- rootNodeId: string,
353
- context: ExecutionContext,
354
- results: RequestResult[],
355
- reason: string
356
- ): Promise<void> {
357
- const graph = this.graph;
358
- if (graph === undefined) {
359
- return;
360
- }
361
-
362
- const nodes = graph.getNodes();
363
- const childrenByFolderId = graph.getChildrenByFolderId();
364
- const stack: string[] = [rootNodeId];
365
-
366
- this.logger.debug(`Skipping subtree from node ${rootNodeId} (${reason})`);
367
-
368
- while (stack.length > 0) {
369
- const currentId = stack.pop() as string;
370
- if (this.skippedNodes.has(currentId)) {
371
- continue;
372
- }
373
- this.skippedNodes.add(currentId);
374
-
375
- const currentNode = nodes.get(currentId);
376
- if (currentNode?.type === 'request') {
377
- const request = currentNode.item as Request;
378
- const result = await this.callbacks.executeRequestIO(currentNode, context, {
379
- skip: true,
380
- bail: false
381
- });
382
- results.push(result);
383
- }
384
-
385
- const childIds = currentNode?.path !== undefined
386
- ? (childrenByFolderId.get(currentNode.path) ?? [])
387
- : [];
388
-
389
- for (const childId of childIds) {
390
- stack.push(childId);
391
- }
392
-
393
- this.markComplete(currentId, graph);
394
- }
395
- }
396
-
397
- private markComplete(
398
- nodeId: string,
399
- graph: import('./TaskGraph.js').TaskGraph
400
- ): void {
401
- this.completedNodes.add(nodeId);
402
-
403
- // Get newly-ready nodes
404
- const nowReady = graph.completeNode(nodeId);
405
-
406
- // Only enqueue new nodes if not aborted (bail stops scheduling new work)
407
- if (!this.aborted && !this.callbacks.isAborted()) {
408
- for (const readyNode of nowReady) {
409
- this.enqueueNode(readyNode);
410
- }
411
- } else {
412
- // Mark skipped nodes as complete so we can finish
413
- for (const readyNode of nowReady) {
414
- this.completedNodes.add(readyNode.id);
415
- }
416
- }
417
-
418
- // Check if all nodes complete OR if aborted and queues empty
419
- const allComplete = this.completedNodes.size === this.totalNodes;
420
- const abortedAndIdle = (this.aborted || this.callbacks.isAborted()) &&
421
- this.scriptQueue.length === 0 &&
422
- this.requestQueue.length === 0;
423
-
424
- if (allComplete || abortedAndIdle) {
425
- // Close queues to signal workers to exit
426
- this.scriptQueue.close();
427
- this.requestQueue.close();
428
- }
429
- }
430
-
431
- private enqueueNode(node: TaskNode): void {
432
- // folder-enter and folder-exit are lifecycle nodes, must be serialized through script queue
433
- if (node.type === 'folder-enter' || node.type === 'folder-exit' || node.type === 'script') {
434
- this.scriptQueue.enqueue(node);
435
- } else {
436
- this.requestQueue.enqueue(node);
437
- }
438
- }
439
- }
package/src/Logger.ts DELETED
@@ -1,85 +0,0 @@
1
- import { EventEmitter } from 'events';
2
- import { randomUUID } from 'crypto';
3
- import { ILogger, LogLevel } from '@apiquest/types';
4
-
5
- /**
6
- * Logger for fracture runtime and host integrations.
7
- */
8
- export class Logger implements ILogger {
9
- private level: LogLevel;
10
- private component: string;
11
- private emitter?: EventEmitter;
12
-
13
- constructor(component: string, level: LogLevel = LogLevel.INFO, emitter?: EventEmitter) {
14
- this.component = component;
15
- this.level = level;
16
- this.emitter = emitter;
17
- }
18
-
19
- createLogger(component: string): Logger {
20
- return new Logger(component, this.level, this.emitter);
21
- }
22
-
23
- setLevel(level: LogLevel): void {
24
- this.level = level;
25
- }
26
-
27
- error(message: string, ...args: unknown[]): void {
28
- this.log(LogLevel.ERROR, message, ...args);
29
- }
30
-
31
- warn(message: string, ...args: unknown[]): void {
32
- this.log(LogLevel.WARN, message, ...args);
33
- }
34
-
35
- info(message: string, ...args: unknown[]): void {
36
- this.log(LogLevel.INFO, message, ...args);
37
- }
38
-
39
- debug(message: string, ...args: unknown[]): void {
40
- this.log(LogLevel.DEBUG, message, ...args);
41
- }
42
-
43
- trace(message: string, ...args: unknown[]): void {
44
- this.log(LogLevel.TRACE, message, ...args);
45
- }
46
-
47
- private log(level: LogLevel, message: string, ...args: unknown[]): void {
48
- if (level > this.level) return;
49
-
50
- const levelNames = ['error', 'warn', 'info', 'debug', 'trace'];
51
- const levelName = levelNames[level];
52
- const prefix = `[${this.component}]`;
53
- const fullMessage = `${prefix} ${message}`;
54
-
55
- const formattedArgs = args.length > 0
56
- ? ' ' + args.map(a => {
57
- if (a instanceof Error) {
58
- return a.message;
59
- }
60
- if (typeof a === 'object' && a !== null) {
61
- try {
62
- return JSON.stringify(a);
63
- } catch {
64
- return String(a);
65
- }
66
- }
67
- return String(a);
68
- }).join(' ')
69
- : '';
70
-
71
- const finalMessage = fullMessage + formattedArgs;
72
-
73
- if (this.emitter !== null && this.emitter !== undefined) {
74
- this.emitter.emit('console', {
75
- id: randomUUID(),
76
- level,
77
- levelName,
78
- component: this.component,
79
- message: finalMessage,
80
- args,
81
- timestamp: new Date().toISOString()
82
- });
83
- }
84
- }
85
- }
@@ -1,126 +0,0 @@
1
- import type { IProtocolPlugin, IAuthPlugin, IValueProviderPlugin } from '@apiquest/types';
2
- import { Logger } from './Logger.js';
3
- import { PluginManager } from './PluginManager.js';
4
- import type { ResolvedPlugin } from './PluginResolver.js';
5
- import type { PluginRequirements } from './CollectionAnalyzer.js';
6
- import { isNullOrEmpty } from './utils.js';
7
-
8
- export class PluginLoader {
9
- private logger: Logger;
10
- private pluginManager: PluginManager;
11
- private loadedPlugins: Set<string> = new Set();
12
-
13
- constructor(pluginManager: PluginManager, baseLogger?: Logger) {
14
- this.pluginManager = pluginManager;
15
- this.logger = baseLogger?.createLogger('PluginLoader') ?? new Logger('PluginLoader');
16
- }
17
-
18
- /**
19
- * Load only plugins needed by the collection
20
- */
21
- async loadRequiredPlugins(
22
- resolved: ResolvedPlugin[],
23
- requirements: PluginRequirements
24
- ): Promise<void> {
25
- const needed = this.filterNeededPlugins(resolved, requirements);
26
-
27
- this.logger.info(`Loading ${needed.length} required plugins (${resolved.length} available)`);
28
-
29
- const loadPromises = needed.map(plugin =>
30
- this.loadPlugin(plugin).catch(err => {
31
- this.logger.error(`Failed to load ${plugin.name}:`, err);
32
- throw err;
33
- })
34
- );
35
-
36
- await Promise.all(loadPromises);
37
- this.logger.debug('Required plugins loaded');
38
- }
39
-
40
- /**
41
- * Filter resolved plugins to only those needed by collection
42
- */
43
- private filterNeededPlugins(
44
- resolved: ResolvedPlugin[],
45
- requirements: PluginRequirements
46
- ): ResolvedPlugin[] {
47
- const needed: ResolvedPlugin[] = [];
48
-
49
- for (const plugin of resolved) {
50
- let isNeeded = false;
51
-
52
- if (plugin.type === 'protocol') {
53
- // Check if collection uses this protocol
54
- if (plugin.protocols?.some(p => requirements.protocols.has(p)) === true) {
55
- isNeeded = true;
56
- }
57
- } else if (plugin.type === 'auth') {
58
- // Check if collection uses any of these auth types
59
- if (plugin.authTypes?.some(a => requirements.authTypes.has(a)) === true) {
60
- isNeeded = true;
61
- }
62
- } else if (plugin.type === 'value') {
63
- // Check if collection uses this value provider
64
- const provider = plugin.provider;
65
- if (provider !== null && provider !== undefined && provider !== '' && requirements.valueProviders.has(provider)) {
66
- isNeeded = true;
67
- }
68
- }
69
-
70
- if (isNeeded) {
71
- this.logger.debug(`Plugin needed: ${plugin.name} v${plugin.version} (${plugin.type})`);
72
- needed.push(plugin);
73
- }
74
- }
75
-
76
- return needed;
77
- }
78
-
79
- /**
80
- * Dynamically import and register a single plugin
81
- */
82
- private async loadPlugin(plugin: ResolvedPlugin): Promise<void> {
83
- const { pathToFileURL } = await import('url');
84
-
85
- // Skip if already loaded
86
- if (this.loadedPlugins.has(plugin.name)) {
87
- this.logger.debug(`Already loaded: ${plugin.name}`);
88
- return;
89
- }
90
-
91
- this.logger.debug(`Loading ${plugin.name} v${plugin.version} from ${plugin.path}`);
92
-
93
- // Mark as loaded
94
- this.loadedPlugins.add(plugin.name);
95
-
96
- // Convert to file:// URL for Windows compatibility
97
- const moduleUrl = pathToFileURL(plugin.entryPoint).href;
98
- const pluginModule = await import(moduleUrl) as Record<string, unknown>;
99
-
100
- // Handle different export patterns
101
- const defaultExport = pluginModule.default;
102
- const namedExport = pluginModule[Object.keys(pluginModule)[0]];
103
- const exported = defaultExport ?? namedExport;
104
-
105
- if (exported === null || exported === undefined) {
106
- throw new Error(`Plugin ${plugin.name} has no exports`);
107
- }
108
-
109
- // Register based on plugin type
110
- if (plugin.type === 'protocol') {
111
- this.pluginManager.registerPlugin(exported as IProtocolPlugin);
112
- this.logger.debug(`Registered protocol plugin: ${plugin.protocols?.join(', ') ?? ''}`);
113
- } else if (plugin.type === 'auth') {
114
- // Auth plugins might export array or single
115
- const authArray = Array.isArray(exported) ? (exported as IAuthPlugin[]) : [exported as IAuthPlugin];
116
-
117
- for (const authPlugin of authArray) {
118
- this.pluginManager.registerAuthPlugin(authPlugin);
119
- this.logger.debug(`Registered auth plugin: ${authPlugin.authTypes.join(', ')}`);
120
- }
121
- } else if (plugin.type === 'value') {
122
- this.pluginManager.registerVariableProvider(exported as IValueProviderPlugin);
123
- this.logger.debug(`Registered value provider: ${plugin.provider ?? ''}`);
124
- }
125
- }
126
- }