testblocks 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli/executor.d.ts +32 -0
  4. package/dist/cli/executor.js +517 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +411 -0
  7. package/dist/cli/reporters.d.ts +62 -0
  8. package/dist/cli/reporters.js +451 -0
  9. package/dist/client/assets/index-4hbFPUhP.js +2087 -0
  10. package/dist/client/assets/index-4hbFPUhP.js.map +1 -0
  11. package/dist/client/assets/index-Dnk1ti7l.css +1 -0
  12. package/dist/client/index.html +25 -0
  13. package/dist/core/blocks/api.d.ts +2 -0
  14. package/dist/core/blocks/api.js +610 -0
  15. package/dist/core/blocks/data-driven.d.ts +2 -0
  16. package/dist/core/blocks/data-driven.js +245 -0
  17. package/dist/core/blocks/index.d.ts +15 -0
  18. package/dist/core/blocks/index.js +71 -0
  19. package/dist/core/blocks/lifecycle.d.ts +2 -0
  20. package/dist/core/blocks/lifecycle.js +199 -0
  21. package/dist/core/blocks/logic.d.ts +2 -0
  22. package/dist/core/blocks/logic.js +357 -0
  23. package/dist/core/blocks/playwright.d.ts +2 -0
  24. package/dist/core/blocks/playwright.js +764 -0
  25. package/dist/core/blocks/procedures.d.ts +5 -0
  26. package/dist/core/blocks/procedures.js +321 -0
  27. package/dist/core/index.d.ts +5 -0
  28. package/dist/core/index.js +44 -0
  29. package/dist/core/plugins.d.ts +66 -0
  30. package/dist/core/plugins.js +118 -0
  31. package/dist/core/types.d.ts +153 -0
  32. package/dist/core/types.js +2 -0
  33. package/dist/server/codegenManager.d.ts +54 -0
  34. package/dist/server/codegenManager.js +259 -0
  35. package/dist/server/codegenParser.d.ts +17 -0
  36. package/dist/server/codegenParser.js +598 -0
  37. package/dist/server/executor.d.ts +37 -0
  38. package/dist/server/executor.js +672 -0
  39. package/dist/server/globals.d.ts +85 -0
  40. package/dist/server/globals.js +273 -0
  41. package/dist/server/index.d.ts +2 -0
  42. package/dist/server/index.js +361 -0
  43. package/dist/server/plugins.d.ts +55 -0
  44. package/dist/server/plugins.js +206 -0
  45. package/package.json +103 -0
@@ -0,0 +1,361 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TestExecutor = void 0;
7
+ const express_1 = __importDefault(require("express"));
8
+ const cors_1 = __importDefault(require("cors"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const executor_1 = require("./executor");
11
+ Object.defineProperty(exports, "TestExecutor", { enumerable: true, get: function () { return executor_1.TestExecutor; } });
12
+ const reporters_1 = require("../cli/reporters");
13
+ const plugins_1 = require("./plugins");
14
+ const globals_1 = require("./globals");
15
+ const codegenManager_1 = require("./codegenManager");
16
+ // Set plugins directory (default to examples/plugins or can be overridden via env)
17
+ const pluginsDir = process.env.PLUGINS_DIR || path_1.default.join(process.cwd(), 'examples', 'plugins');
18
+ (0, plugins_1.setPluginsDirectory)(pluginsDir);
19
+ // Set globals directory (default to examples or can be overridden via env)
20
+ const globalsDir = process.env.GLOBALS_DIR || path_1.default.join(process.cwd(), 'examples');
21
+ (0, globals_1.setGlobalsDirectory)(globalsDir);
22
+ // Load all discovered plugins on startup
23
+ (0, plugins_1.loadAllPlugins)().then(() => {
24
+ (0, plugins_1.initializeServerPlugins)();
25
+ // Load globals and snippets after plugins
26
+ (0, globals_1.initializeGlobalsAndSnippets)();
27
+ }).catch(err => {
28
+ console.error('Failed to load plugins:', err);
29
+ });
30
+ const app = (0, express_1.default)();
31
+ const PORT = process.env.PORT || 3001;
32
+ app.use((0, cors_1.default)());
33
+ app.use(express_1.default.json({ limit: '10mb' }));
34
+ // Health check
35
+ app.get('/api/health', (req, res) => {
36
+ res.json({ status: 'ok', version: '1.0.0' });
37
+ });
38
+ // List available plugins
39
+ app.get('/api/plugins', (req, res) => {
40
+ const available = (0, plugins_1.discoverPlugins)();
41
+ const loaded = (0, plugins_1.getServerPlugins)().map(p => ({
42
+ name: p.name,
43
+ version: p.version,
44
+ description: p.description,
45
+ blockCount: p.blocks.length,
46
+ }));
47
+ res.json({
48
+ directory: (0, plugins_1.getPluginsDirectory)(),
49
+ available,
50
+ loaded,
51
+ });
52
+ });
53
+ // Load specific plugins
54
+ app.post('/api/plugins/load', async (req, res) => {
55
+ try {
56
+ const { plugins } = req.body;
57
+ await (0, plugins_1.loadTestFilePlugins)(plugins);
58
+ res.json({ loaded: plugins });
59
+ }
60
+ catch (error) {
61
+ res.status(500).json({ error: error.message });
62
+ }
63
+ });
64
+ // Get globals and snippets
65
+ app.get('/api/globals', (req, res) => {
66
+ const globals = (0, globals_1.getGlobals)();
67
+ const snippets = (0, globals_1.getAllSnippets)().map(s => ({
68
+ name: s.name,
69
+ description: s.description,
70
+ category: s.category,
71
+ params: s.params,
72
+ stepCount: s.steps.length,
73
+ }));
74
+ res.json({
75
+ directory: (0, globals_1.getGlobalsDirectory)(),
76
+ globals,
77
+ snippets,
78
+ testIdAttribute: (0, globals_1.getTestIdAttribute)(),
79
+ });
80
+ });
81
+ // Update test ID attribute
82
+ app.put('/api/globals/test-id-attribute', (req, res) => {
83
+ const { testIdAttribute } = req.body;
84
+ if (!testIdAttribute || typeof testIdAttribute !== 'string') {
85
+ return res.status(400).json({ error: 'testIdAttribute is required and must be a string' });
86
+ }
87
+ (0, globals_1.setTestIdAttribute)(testIdAttribute);
88
+ res.json({ testIdAttribute: (0, globals_1.getTestIdAttribute)() });
89
+ });
90
+ // Run tests
91
+ app.post('/api/run', async (req, res) => {
92
+ try {
93
+ const testFile = req.body;
94
+ if (!testFile || !testFile.tests) {
95
+ return res.status(400).json({ error: 'Invalid test file format' });
96
+ }
97
+ console.log(`Running ${testFile.tests.length} tests from "${testFile.name}"...`);
98
+ // Merge global variables with test file variables
99
+ const globalVars = (0, globals_1.getGlobalVariables)();
100
+ const testIdAttr = (0, globals_1.getTestIdAttribute)();
101
+ const executor = new executor_1.TestExecutor({
102
+ headless: req.query.headless !== 'false',
103
+ timeout: Number(req.query.timeout) || 30000,
104
+ variables: globalVars, // Pass global variables
105
+ testIdAttribute: testIdAttr, // Pass test ID attribute
106
+ baseDir: globalsDir, // Base directory for resolving relative file paths
107
+ });
108
+ const results = await executor.runTestFile(testFile);
109
+ const passed = results.filter(r => r.status === 'passed').length;
110
+ const failed = results.filter(r => r.status === 'failed').length;
111
+ console.log(`Results: ${passed} passed, ${failed} failed`);
112
+ res.json(results);
113
+ }
114
+ catch (error) {
115
+ const err = error;
116
+ console.error('Test execution failed:', err.message);
117
+ console.error('Stack:', err.stack);
118
+ res.status(500).json({
119
+ error: 'Test execution failed',
120
+ message: err.message,
121
+ });
122
+ }
123
+ });
124
+ // Run a single test
125
+ app.post('/api/run/:testId', async (req, res) => {
126
+ try {
127
+ const testFile = req.body;
128
+ const { testId } = req.params;
129
+ const test = testFile.tests.find(t => t.id === testId);
130
+ if (!test) {
131
+ return res.status(404).json({ error: `Test not found: ${testId}` });
132
+ }
133
+ // Merge global variables
134
+ const globalVars = (0, globals_1.getGlobalVariables)();
135
+ const testIdAttr = (0, globals_1.getTestIdAttribute)();
136
+ const executor = new executor_1.TestExecutor({
137
+ headless: req.query.headless !== 'false',
138
+ timeout: Number(req.query.timeout) || 30000,
139
+ variables: globalVars,
140
+ testIdAttribute: testIdAttr,
141
+ baseDir: globalsDir, // Base directory for resolving relative file paths
142
+ });
143
+ // Register custom blocks from procedures before running the test
144
+ if (testFile.procedures) {
145
+ executor.registerProcedures(testFile.procedures);
146
+ }
147
+ await executor.initialize();
148
+ const result = await executor.runTest(test, testFile.variables);
149
+ await executor.cleanup();
150
+ res.json(result);
151
+ }
152
+ catch (error) {
153
+ console.error('Test execution failed:', error);
154
+ res.status(500).json({
155
+ error: 'Test execution failed',
156
+ message: error.message,
157
+ });
158
+ }
159
+ });
160
+ // Validate test file
161
+ app.post('/api/validate', (req, res) => {
162
+ try {
163
+ const testFile = req.body;
164
+ const errors = [];
165
+ if (!testFile.version) {
166
+ errors.push('Missing version field');
167
+ }
168
+ if (!testFile.name) {
169
+ errors.push('Missing name field');
170
+ }
171
+ if (!testFile.tests || !Array.isArray(testFile.tests)) {
172
+ errors.push('Missing or invalid tests array');
173
+ }
174
+ else {
175
+ testFile.tests.forEach((test, index) => {
176
+ if (!test.id) {
177
+ errors.push(`Test at index ${index} is missing an id`);
178
+ }
179
+ if (!test.name) {
180
+ errors.push(`Test at index ${index} is missing a name`);
181
+ }
182
+ });
183
+ }
184
+ if (errors.length > 0) {
185
+ res.status(400).json({ valid: false, errors });
186
+ }
187
+ else {
188
+ res.json({ valid: true });
189
+ }
190
+ }
191
+ catch (error) {
192
+ res.status(400).json({
193
+ valid: false,
194
+ errors: ['Invalid JSON: ' + error.message],
195
+ });
196
+ }
197
+ });
198
+ // ===== Recording Endpoints =====
199
+ // Start a recording session
200
+ app.post('/api/record/start', async (req, res) => {
201
+ try {
202
+ const { url, testIdAttribute } = req.body;
203
+ if (!url) {
204
+ return res.status(400).json({ error: 'URL is required' });
205
+ }
206
+ console.log(`Starting recording session for URL: ${url}`);
207
+ const sessionId = await codegenManager_1.codegenManager.startRecording(url, {
208
+ testIdAttribute: testIdAttribute || undefined,
209
+ });
210
+ res.json({
211
+ sessionId,
212
+ status: 'running',
213
+ });
214
+ }
215
+ catch (error) {
216
+ console.error('Failed to start recording:', error);
217
+ res.status(500).json({
218
+ error: 'Failed to start recording',
219
+ message: error.message,
220
+ });
221
+ }
222
+ });
223
+ // Stop a recording session and get the recorded steps
224
+ app.post('/api/record/stop', async (req, res) => {
225
+ try {
226
+ const { sessionId } = req.body;
227
+ if (!sessionId) {
228
+ return res.status(400).json({ error: 'Session ID is required' });
229
+ }
230
+ console.log(`Stopping recording session: ${sessionId}`);
231
+ const steps = await codegenManager_1.codegenManager.stopRecording(sessionId);
232
+ res.json({
233
+ status: 'completed',
234
+ steps,
235
+ });
236
+ // Clean up the session after returning the steps
237
+ setTimeout(() => {
238
+ codegenManager_1.codegenManager.cleanup(sessionId);
239
+ }, 5000);
240
+ }
241
+ catch (error) {
242
+ console.error('Failed to stop recording:', error);
243
+ res.status(500).json({
244
+ error: 'Failed to stop recording',
245
+ message: error.message,
246
+ });
247
+ }
248
+ });
249
+ // Get the status of a recording session
250
+ app.get('/api/record/status/:sessionId', (req, res) => {
251
+ const { sessionId } = req.params;
252
+ const session = codegenManager_1.codegenManager.getStatus(sessionId);
253
+ if (!session) {
254
+ return res.status(404).json({ error: 'Session not found' });
255
+ }
256
+ res.json({
257
+ sessionId: session.id,
258
+ status: session.status,
259
+ url: session.url,
260
+ error: session.error,
261
+ });
262
+ });
263
+ // ===== Report Generation Endpoints =====
264
+ // Generate HTML report from test results
265
+ app.post('/api/reports/html', (req, res) => {
266
+ try {
267
+ const { testFile, results } = req.body;
268
+ if (!testFile || !results) {
269
+ return res.status(400).json({ error: 'testFile and results are required' });
270
+ }
271
+ const timestamp = new Date().toISOString();
272
+ const reportData = {
273
+ timestamp,
274
+ summary: {
275
+ totalTests: results.length,
276
+ passed: results.filter(r => r.status === 'passed').length,
277
+ failed: results.filter(r => r.status !== 'passed').length,
278
+ duration: results.reduce((sum, r) => sum + r.duration, 0),
279
+ },
280
+ testFiles: [{
281
+ file: testFile.name || 'TestBlocks Test',
282
+ testFile,
283
+ results,
284
+ }],
285
+ };
286
+ const html = (0, reporters_1.generateHTMLReport)(reportData);
287
+ const filename = `report-${(0, reporters_1.getTimestamp)()}.html`;
288
+ res.setHeader('Content-Type', 'text/html');
289
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
290
+ res.send(html);
291
+ }
292
+ catch (error) {
293
+ console.error('Failed to generate HTML report:', error);
294
+ res.status(500).json({
295
+ error: 'Failed to generate report',
296
+ message: error.message,
297
+ });
298
+ }
299
+ });
300
+ // Generate JUnit XML report from test results
301
+ app.post('/api/reports/junit', (req, res) => {
302
+ try {
303
+ const { testFile, results } = req.body;
304
+ if (!testFile || !results) {
305
+ return res.status(400).json({ error: 'testFile and results are required' });
306
+ }
307
+ const timestamp = new Date().toISOString();
308
+ const reportData = {
309
+ timestamp,
310
+ summary: {
311
+ totalTests: results.length,
312
+ passed: results.filter(r => r.status === 'passed').length,
313
+ failed: results.filter(r => r.status !== 'passed').length,
314
+ duration: results.reduce((sum, r) => sum + r.duration, 0),
315
+ },
316
+ testFiles: [{
317
+ file: testFile.name || 'TestBlocks Test',
318
+ testFile,
319
+ results,
320
+ }],
321
+ };
322
+ const xml = (0, reporters_1.generateJUnitXML)(reportData);
323
+ const filename = `junit-${(0, reporters_1.getTimestamp)()}.xml`;
324
+ res.setHeader('Content-Type', 'application/xml');
325
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
326
+ res.send(xml);
327
+ }
328
+ catch (error) {
329
+ console.error('Failed to generate JUnit report:', error);
330
+ res.status(500).json({
331
+ error: 'Failed to generate report',
332
+ message: error.message,
333
+ });
334
+ }
335
+ });
336
+ // Clean up sessions on server shutdown
337
+ process.on('SIGTERM', () => {
338
+ console.log('Received SIGTERM, cleaning up...');
339
+ codegenManager_1.codegenManager.cleanupAll();
340
+ process.exit(0);
341
+ });
342
+ process.on('SIGINT', () => {
343
+ console.log('Received SIGINT, cleaning up...');
344
+ codegenManager_1.codegenManager.cleanupAll();
345
+ process.exit(0);
346
+ });
347
+ app.listen(PORT, () => {
348
+ console.log(`TestBlocks server running on http://localhost:${PORT}`);
349
+ console.log('API endpoints:');
350
+ console.log(' GET /api/health - Health check');
351
+ console.log(' GET /api/plugins - List plugins');
352
+ console.log(' GET /api/globals - Get globals and snippets');
353
+ console.log(' POST /api/run - Run all tests');
354
+ console.log(' POST /api/run/:id - Run single test');
355
+ console.log(' POST /api/validate - Validate test file');
356
+ console.log(' POST /api/record/start - Start recording session');
357
+ console.log(' POST /api/record/stop - Stop recording and get steps');
358
+ console.log(' GET /api/record/status - Get recording session status');
359
+ console.log(' POST /api/reports/html - Generate HTML report');
360
+ console.log(' POST /api/reports/junit - Generate JUnit XML report');
361
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Server-side Plugin Initialization
3
+ *
4
+ * This module provides utilities for loading and registering plugins
5
+ * for the test executor. Plugins can be:
6
+ * 1. Registered manually via registerServerPlugin()
7
+ * 2. Discovered automatically from a plugins folder via discoverPlugins()
8
+ */
9
+ import { Plugin } from '../core';
10
+ /**
11
+ * Set the plugins directory path
12
+ */
13
+ export declare function setPluginsDirectory(dir: string): void;
14
+ /**
15
+ * Get the current plugins directory
16
+ */
17
+ export declare function getPluginsDirectory(): string;
18
+ /**
19
+ * Register a plugin for server-side use
20
+ * This registers the plugin's blocks with the core block registry
21
+ */
22
+ export declare function registerServerPlugin(plugin: Plugin): void;
23
+ /**
24
+ * Register multiple plugins at once
25
+ */
26
+ export declare function registerServerPlugins(plugins: Plugin[]): void;
27
+ /**
28
+ * Discover and list available plugins from the plugins directory
29
+ * Returns plugin file names (without extension)
30
+ */
31
+ export declare function discoverPlugins(): string[];
32
+ /**
33
+ * Load a specific plugin by name from the plugins directory
34
+ */
35
+ export declare function loadPlugin(pluginName: string): Promise<Plugin | null>;
36
+ /**
37
+ * Load plugins specified in a test file
38
+ */
39
+ export declare function loadTestFilePlugins(pluginNames: string[]): Promise<void>;
40
+ /**
41
+ * Load all discovered plugins from the plugins directory
42
+ */
43
+ export declare function loadAllPlugins(): Promise<void>;
44
+ /**
45
+ * Initialize server plugins (call after all plugins are registered)
46
+ */
47
+ export declare function initializeServerPlugins(): void;
48
+ /**
49
+ * Get all registered server plugins
50
+ */
51
+ export declare function getServerPlugins(): Plugin[];
52
+ /**
53
+ * Check if a plugin is registered
54
+ */
55
+ export declare function isPluginLoaded(pluginName: string): boolean;
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ /**
3
+ * Server-side Plugin Initialization
4
+ *
5
+ * This module provides utilities for loading and registering plugins
6
+ * for the test executor. Plugins can be:
7
+ * 1. Registered manually via registerServerPlugin()
8
+ * 2. Discovered automatically from a plugins folder via discoverPlugins()
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.setPluginsDirectory = setPluginsDirectory;
45
+ exports.getPluginsDirectory = getPluginsDirectory;
46
+ exports.registerServerPlugin = registerServerPlugin;
47
+ exports.registerServerPlugins = registerServerPlugins;
48
+ exports.discoverPlugins = discoverPlugins;
49
+ exports.loadPlugin = loadPlugin;
50
+ exports.loadTestFilePlugins = loadTestFilePlugins;
51
+ exports.loadAllPlugins = loadAllPlugins;
52
+ exports.initializeServerPlugins = initializeServerPlugins;
53
+ exports.getServerPlugins = getServerPlugins;
54
+ exports.isPluginLoaded = isPluginLoaded;
55
+ const fs = __importStar(require("fs"));
56
+ const path = __importStar(require("path"));
57
+ const core_1 = require("../core");
58
+ // Track initialization
59
+ let pluginsInitialized = false;
60
+ // List of registered plugins
61
+ const registeredPlugins = new Map();
62
+ // Plugins directory (can be overridden)
63
+ let pluginsDirectory = path.join(process.cwd(), 'plugins');
64
+ /**
65
+ * Set the plugins directory path
66
+ */
67
+ function setPluginsDirectory(dir) {
68
+ pluginsDirectory = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
69
+ console.log(`Plugins directory set to: ${pluginsDirectory}`);
70
+ }
71
+ /**
72
+ * Get the current plugins directory
73
+ */
74
+ function getPluginsDirectory() {
75
+ return pluginsDirectory;
76
+ }
77
+ /**
78
+ * Register a plugin for server-side use
79
+ * This registers the plugin's blocks with the core block registry
80
+ */
81
+ function registerServerPlugin(plugin) {
82
+ if (registeredPlugins.has(plugin.name)) {
83
+ console.log(`Plugin already registered: ${plugin.name}`);
84
+ return;
85
+ }
86
+ try {
87
+ (0, core_1.registerPlugin)(plugin);
88
+ registeredPlugins.set(plugin.name, plugin);
89
+ console.log(`Registered plugin: ${plugin.name}`);
90
+ }
91
+ catch (error) {
92
+ console.error(`Failed to register plugin "${plugin.name}":`, error);
93
+ throw error;
94
+ }
95
+ }
96
+ /**
97
+ * Register multiple plugins at once
98
+ */
99
+ function registerServerPlugins(plugins) {
100
+ for (const plugin of plugins) {
101
+ registerServerPlugin(plugin);
102
+ }
103
+ }
104
+ /**
105
+ * Discover and list available plugins from the plugins directory
106
+ * Returns plugin file names (without extension)
107
+ */
108
+ function discoverPlugins() {
109
+ if (!fs.existsSync(pluginsDirectory)) {
110
+ console.log(`Plugins directory not found: ${pluginsDirectory}`);
111
+ return [];
112
+ }
113
+ const files = fs.readdirSync(pluginsDirectory);
114
+ const plugins = [];
115
+ for (const file of files) {
116
+ // Look for .js or compiled .js files (not .d.ts)
117
+ if ((file.endsWith('.js') || file.endsWith('.ts')) && !file.endsWith('.d.ts')) {
118
+ const pluginName = file.replace(/\.(js|ts)$/, '');
119
+ if (pluginName !== 'index') {
120
+ plugins.push(pluginName);
121
+ }
122
+ }
123
+ }
124
+ console.log(`Discovered ${plugins.length} plugin(s): ${plugins.join(', ')}`);
125
+ return plugins;
126
+ }
127
+ /**
128
+ * Load a specific plugin by name from the plugins directory
129
+ */
130
+ async function loadPlugin(pluginName) {
131
+ // Check if already loaded
132
+ if (registeredPlugins.has(pluginName)) {
133
+ return registeredPlugins.get(pluginName);
134
+ }
135
+ const jsPath = path.join(pluginsDirectory, `${pluginName}.js`);
136
+ const tsPath = path.join(pluginsDirectory, `${pluginName}.ts`);
137
+ let pluginPath = null;
138
+ if (fs.existsSync(jsPath)) {
139
+ pluginPath = jsPath;
140
+ }
141
+ else if (fs.existsSync(tsPath)) {
142
+ // For TypeScript files, require ts-node or esbuild-register
143
+ pluginPath = tsPath;
144
+ }
145
+ if (!pluginPath) {
146
+ console.error(`Plugin not found: ${pluginName} (looked in ${pluginsDirectory})`);
147
+ return null;
148
+ }
149
+ try {
150
+ console.log(`Loading plugin: ${pluginName} from ${pluginPath}`);
151
+ // Dynamic import
152
+ const module = await Promise.resolve(`${pluginPath}`).then(s => __importStar(require(s)));
153
+ // Look for default export or named export matching plugin name
154
+ const plugin = module.default ||
155
+ module[pluginName.replace(/-./g, x => x[1].toUpperCase()) + 'Plugin'] ||
156
+ module[pluginName.replace(/-/g, '') + 'Plugin'] ||
157
+ Object.values(module).find((exp) => typeof exp === 'object' && exp !== null && 'name' in exp && 'blocks' in exp);
158
+ if (!plugin) {
159
+ console.error(`No valid plugin export found in ${pluginPath}`);
160
+ return null;
161
+ }
162
+ registerServerPlugin(plugin);
163
+ return plugin;
164
+ }
165
+ catch (error) {
166
+ console.error(`Failed to load plugin ${pluginName}:`, error);
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * Load plugins specified in a test file
172
+ */
173
+ async function loadTestFilePlugins(pluginNames) {
174
+ for (const name of pluginNames) {
175
+ await loadPlugin(name);
176
+ }
177
+ }
178
+ /**
179
+ * Load all discovered plugins from the plugins directory
180
+ */
181
+ async function loadAllPlugins() {
182
+ const pluginNames = discoverPlugins();
183
+ await loadTestFilePlugins(pluginNames);
184
+ }
185
+ /**
186
+ * Initialize server plugins (call after all plugins are registered)
187
+ */
188
+ function initializeServerPlugins() {
189
+ if (pluginsInitialized) {
190
+ return;
191
+ }
192
+ pluginsInitialized = true;
193
+ console.log(`Server plugins initialized: ${registeredPlugins.size} plugin(s) loaded`);
194
+ }
195
+ /**
196
+ * Get all registered server plugins
197
+ */
198
+ function getServerPlugins() {
199
+ return Array.from(registeredPlugins.values());
200
+ }
201
+ /**
202
+ * Check if a plugin is registered
203
+ */
204
+ function isPluginLoaded(pluginName) {
205
+ return registeredPlugins.has(pluginName);
206
+ }