@majkapp/plugin-kit 1.0.17 → 1.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.
@@ -7,6 +7,16 @@ exports.definePlugin = definePlugin;
7
7
  const http_1 = __importDefault(require("http"));
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
+ const registry_1 = require("./registry");
11
+ const transports_1 = require("./transports");
12
+ /**
13
+ * Conditional logging - suppressed in extract mode
14
+ */
15
+ const log = (message) => {
16
+ if (process.env.PLUGIN_KIT_MODE !== 'extract') {
17
+ console.log(message);
18
+ }
19
+ };
10
20
  /**
11
21
  * Build-time errors with clear, actionable messages
12
22
  */
@@ -143,25 +153,132 @@ function validateEntity(entityType, entity) {
143
153
  }
144
154
  return errors;
145
155
  }
156
+ /**
157
+ * Convert JSON Schema to OpenAPI Schema
158
+ */
159
+ function jsonSchemaToOpenApi(schema) {
160
+ // OpenAPI doesn't use the $schema property
161
+ const { ...openApiSchema } = schema;
162
+ return openApiSchema;
163
+ }
164
+ /**
165
+ * Generate OpenAPI specification from routes
166
+ */
167
+ function generateOpenApiSpec(pluginId, pluginName, pluginVersion, routes, baseUrl) {
168
+ const paths = {};
169
+ for (const route of routes) {
170
+ const openApiPath = route.path.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
171
+ if (!paths[openApiPath]) {
172
+ paths[openApiPath] = {};
173
+ }
174
+ const operation = {
175
+ operationId: `${pluginId}_${route.name.replace(/\s+/g, '_')}`,
176
+ summary: route.name,
177
+ description: route.description,
178
+ tags: route.tags || [pluginId],
179
+ deprecated: route.deprecated
180
+ };
181
+ // Add parameters from request schema
182
+ if (route.requestSchema) {
183
+ const parameters = [];
184
+ // Path parameters
185
+ if (route.requestSchema.params) {
186
+ const paramProps = route.requestSchema.params.properties || {};
187
+ for (const [name, schema] of Object.entries(paramProps)) {
188
+ parameters.push({
189
+ name,
190
+ in: 'path',
191
+ required: route.requestSchema.params.required?.includes(name) ?? true,
192
+ schema: jsonSchemaToOpenApi(schema),
193
+ description: schema.description
194
+ });
195
+ }
196
+ }
197
+ // Query parameters
198
+ if (route.requestSchema.query) {
199
+ const queryProps = route.requestSchema.query.properties || {};
200
+ for (const [name, schema] of Object.entries(queryProps)) {
201
+ parameters.push({
202
+ name,
203
+ in: 'query',
204
+ required: route.requestSchema.query.required?.includes(name) ?? false,
205
+ schema: jsonSchemaToOpenApi(schema),
206
+ description: schema.description
207
+ });
208
+ }
209
+ }
210
+ if (parameters.length > 0) {
211
+ operation.parameters = parameters;
212
+ }
213
+ // Request body
214
+ if (route.requestSchema.body) {
215
+ operation.requestBody = {
216
+ required: true,
217
+ content: {
218
+ 'application/json': {
219
+ schema: jsonSchemaToOpenApi(route.requestSchema.body)
220
+ }
221
+ }
222
+ };
223
+ }
224
+ }
225
+ // Response schema
226
+ operation.responses = {
227
+ '200': {
228
+ description: 'Successful response',
229
+ content: route.responseSchema ? {
230
+ 'application/json': {
231
+ schema: jsonSchemaToOpenApi(route.responseSchema)
232
+ }
233
+ } : {}
234
+ },
235
+ '400': {
236
+ description: 'Bad request'
237
+ },
238
+ '500': {
239
+ description: 'Internal server error'
240
+ }
241
+ };
242
+ paths[openApiPath][route.method.toLowerCase()] = operation;
243
+ }
244
+ return {
245
+ openapi: '3.0.0',
246
+ info: {
247
+ title: `${pluginName} API`,
248
+ version: pluginVersion,
249
+ description: `API documentation for the ${pluginName} MAJK plugin`
250
+ },
251
+ servers: [
252
+ {
253
+ url: baseUrl,
254
+ description: 'Plugin API server'
255
+ }
256
+ ],
257
+ paths,
258
+ components: {
259
+ schemas: {}
260
+ }
261
+ };
262
+ }
146
263
  /**
147
264
  * Path parameter regex builder
148
265
  */
149
266
  function pathToRegex(p) {
150
267
  const keys = [];
151
- console.log(`[pathToRegex] Input path: ${p}`);
268
+ log(`[pathToRegex] Input path: ${p}`);
152
269
  // First, escape special regex characters EXCEPT the colon (for path params)
153
270
  const escaped = p.replace(/([.+*?=^!${}()[\]|\\])/g, '\\$1');
154
- console.log(`[pathToRegex] After escaping: ${escaped}`);
271
+ log(`[pathToRegex] After escaping: ${escaped}`);
155
272
  // Then, replace path parameters with capturing groups
156
273
  const pattern = escaped.replace(/\/:([A-Za-z0-9_]+)/g, (_m, k) => {
157
274
  keys.push(k);
158
- console.log(`[pathToRegex] Found param: ${k}`);
275
+ log(`[pathToRegex] Found param: ${k}`);
159
276
  return '/([^/]+)';
160
277
  });
161
- console.log(`[pathToRegex] Final pattern: ${pattern}`);
162
- console.log(`[pathToRegex] Keys: [${keys.join(', ')}]`);
278
+ log(`[pathToRegex] Final pattern: ${pattern}`);
279
+ log(`[pathToRegex] Keys: [${keys.join(', ')}]`);
163
280
  const regex = new RegExp(`^${pattern}$`);
164
- console.log(`[pathToRegex] Final regex: ${regex}`);
281
+ log(`[pathToRegex] Final regex: ${regex}`);
165
282
  return { regex, keys };
166
283
  }
167
284
  /**
@@ -290,7 +407,11 @@ function serveSpa(ui, req, res, ctx) {
290
407
  * Main plugin class built by the fluent API
291
408
  */
292
409
  class BuiltPlugin {
293
- constructor(id, name, version, capabilities, tools, apiRoutes, uiConfig, reactScreens, htmlScreens, wizard, onReadyFn, healthCheckFn, userProvidedPluginRoot) {
410
+ constructor(id, name, version, capabilities, tools, apiRoutes, uiConfig, reactScreens, htmlScreens, wizard, settings, onReadyFn, healthCheckFn, userProvidedPluginRoot,
411
+ // New parameters for function-first architecture
412
+ functionRegistry, transports,
413
+ // New parameter for configurable entities
414
+ configurableEntities) {
294
415
  this.capabilities = capabilities;
295
416
  this.tools = tools;
296
417
  this.apiRoutes = apiRoutes;
@@ -298,19 +419,74 @@ class BuiltPlugin {
298
419
  this.reactScreens = reactScreens;
299
420
  this.htmlScreens = htmlScreens;
300
421
  this.wizard = wizard;
422
+ this.settings = settings;
301
423
  this.onReadyFn = onReadyFn;
302
424
  this.healthCheckFn = healthCheckFn;
303
425
  this.userProvidedPluginRoot = userProvidedPluginRoot;
426
+ this.configurableEntities = configurableEntities;
304
427
  this.cleanups = [];
305
428
  this.router = [];
429
+ this.transports = [];
306
430
  this.id = id;
307
431
  this.name = name;
308
432
  this.version = version;
433
+ this.functionRegistry = functionRegistry;
434
+ this.transports = transports || [];
435
+ this.configurableEntities = configurableEntities || [];
309
436
  }
310
- getCapabilities() {
437
+ async getCapabilities() {
438
+ // Start with static capabilities
439
+ const dynamicCapabilities = [...this.capabilities];
440
+ // Only process configurable entities if context is available (after onLoad has been called)
441
+ if (!this.context) {
442
+ // Context not set yet - return only static capabilities
443
+ // This happens when getCapabilities() is called before onLoad()
444
+ return {
445
+ apiVersion: 'majk.capabilities/v1',
446
+ capabilities: dynamicCapabilities
447
+ };
448
+ }
449
+ // If wizard has schema, check for config and add configurable entities
450
+ if (this.wizard?.schema && this.configurableEntities && this.configurableEntities.length > 0) {
451
+ const storageKey = this.wizard.storageKey || '_plugin_config';
452
+ this.context.logger.info(`[ConfigWizard] getCapabilities: checking for config at key: ${storageKey}`);
453
+ try {
454
+ // Load config from storage
455
+ const config = await this.context.storage.get(storageKey);
456
+ if (config) {
457
+ this.context.logger.info(`[ConfigWizard] ✅ Config found - generating configurable entities`);
458
+ this.context.logger.info(`[ConfigWizard] Config:`, JSON.stringify(config, null, 2));
459
+ // Call each configurable entity factory with the config
460
+ for (const ce of this.configurableEntities) {
461
+ this.context.logger.info(`[ConfigWizard] Calling factory for ${ce.entityType}...`);
462
+ try {
463
+ const entities = ce.factory(config);
464
+ this.context.logger.info(`[ConfigWizard] Factory returned ${entities.length} ${ce.entityType} entities`);
465
+ if (entities.length > 0) {
466
+ dynamicCapabilities.push({
467
+ type: 'entity',
468
+ entityType: ce.entityType,
469
+ entities: entities
470
+ });
471
+ this.context.logger.info(`[ConfigWizard] ✅ Added ${entities.length} ${ce.entityType} entities to capabilities`);
472
+ }
473
+ }
474
+ catch (factoryError) {
475
+ this.context.logger.error(`[ConfigWizard] ❌ Factory for ${ce.entityType} failed:`, factoryError.message);
476
+ }
477
+ }
478
+ }
479
+ else {
480
+ this.context.logger.info(`[ConfigWizard] ⚠️ No config found - configurable entities will not be registered`);
481
+ }
482
+ }
483
+ catch (error) {
484
+ this.context.logger.error(`[ConfigWizard] ❌ Failed to load config:`, error.message);
485
+ }
486
+ }
311
487
  return {
312
488
  apiVersion: 'majk.capabilities/v1',
313
- capabilities: this.capabilities
489
+ capabilities: dynamicCapabilities
314
490
  };
315
491
  }
316
492
  /**
@@ -349,6 +525,7 @@ class BuiltPlugin {
349
525
  };
350
526
  context.logger.info('═══════════════════════════════════════════════════════');
351
527
  context.logger.info(`🚀 Loading Plugin: ${this.name} (${this.id}) v${this.version}`);
528
+ context.logger.info(`🔧 Plugin Kit Version: @majkapp/plugin-kit@1.0.19`);
352
529
  context.logger.info(`📍 Plugin Root: ${effectivePluginRoot}`);
353
530
  if (this.userProvidedPluginRoot) {
354
531
  context.logger.info(` (user-provided via .pluginRoot(__dirname))`);
@@ -379,7 +556,35 @@ class BuiltPlugin {
379
556
  }
380
557
  try {
381
558
  await this.startServer();
382
- // Register API routes
559
+ // Initialize transports for function-first architecture
560
+ if (this.functionRegistry) {
561
+ context.logger.info('');
562
+ context.logger.info('🔌 Initializing Transports...');
563
+ for (const transport of this.transports) {
564
+ await transport.initialize(this.functionRegistry, this.context);
565
+ await transport.start();
566
+ const metadata = transport.getMetadata();
567
+ context.logger.info(` ✅ ${transport.name} transport initialized`);
568
+ context.logger.info(` • Base Path: ${metadata.endpoint}`);
569
+ context.logger.info(` • Discovery: ${metadata.discovery}`);
570
+ }
571
+ // Log registered functions
572
+ const functionNames = this.functionRegistry.getFunctionNames();
573
+ if (functionNames.length > 0) {
574
+ context.logger.info('');
575
+ context.logger.info(`📦 Registered ${functionNames.length} Functions:`);
576
+ for (const fname of functionNames) {
577
+ const func = this.functionRegistry.functions.get(fname);
578
+ if (func) {
579
+ context.logger.info(` • ${fname}: ${func.description}`);
580
+ if (func.deprecated) {
581
+ context.logger.info(` ⚠️ DEPRECATED`);
582
+ }
583
+ }
584
+ }
585
+ }
586
+ }
587
+ // Register API routes (legacy)
383
588
  for (const route of this.apiRoutes) {
384
589
  const { regex, keys } = pathToRegex(route.path);
385
590
  this.router.push({
@@ -388,17 +593,127 @@ class BuiltPlugin {
388
593
  keys,
389
594
  name: route.name,
390
595
  description: route.description,
391
- handler: route.handler
596
+ handler: route.handler,
597
+ requestSchema: route.requestSchema,
598
+ responseSchema: route.responseSchema,
599
+ tags: route.tags,
600
+ deprecated: route.deprecated
392
601
  });
393
602
  context.logger.info(`📝 Registered route: ${route.method} ${route.path} - ${route.name}`);
394
603
  }
604
+ // Register the discovery endpoint at /majk/plugin/api
605
+ const discoveryPath = '/majk/plugin/api';
606
+ const { regex: discoveryRegex, keys: discoveryKeys } = pathToRegex(discoveryPath);
607
+ this.router.push({
608
+ method: 'GET',
609
+ regex: discoveryRegex,
610
+ keys: discoveryKeys,
611
+ name: 'API Discovery',
612
+ description: 'Get OpenAPI specification for all plugin API routes',
613
+ handler: async (_req, _res, _ctx) => {
614
+ // If using function-first architecture, generate spec from function registry
615
+ if (this.functionRegistry && this.functionRegistry.getFunctionNames().length > 0) {
616
+ return this.functionRegistry.toOpenAPISpec();
617
+ }
618
+ // Otherwise fall back to legacy API routes
619
+ const openApiSpec = generateOpenApiSpec(this.id, this.name, this.version, this.apiRoutes, this.context.http.baseUrl);
620
+ return openApiSpec;
621
+ }
622
+ });
623
+ context.logger.info(`📝 Registered discovery route: GET ${discoveryPath}`);
624
+ // Also register a simpler JSON format at /majk/plugin/routes for quick access
625
+ const routesPath = '/majk/plugin/routes';
626
+ const { regex: routesRegex, keys: routesKeys } = pathToRegex(routesPath);
627
+ this.router.push({
628
+ method: 'GET',
629
+ regex: routesRegex,
630
+ keys: routesKeys,
631
+ name: 'Routes List',
632
+ description: 'Get a simple JSON list of all available API routes',
633
+ handler: async (_req, _res, _ctx) => {
634
+ return {
635
+ plugin: {
636
+ id: this.id,
637
+ name: this.name,
638
+ version: this.version
639
+ },
640
+ routes: this.apiRoutes.map(route => ({
641
+ method: route.method,
642
+ path: route.path,
643
+ name: route.name,
644
+ description: route.description,
645
+ deprecated: route.deprecated,
646
+ tags: route.tags,
647
+ requestSchema: route.requestSchema,
648
+ responseSchema: route.responseSchema
649
+ })),
650
+ discoveryUrl: `${this.context.http.baseUrl}/majk/plugin/api`
651
+ };
652
+ }
653
+ });
654
+ context.logger.info(`📝 Registered routes list: GET ${routesPath}`);
395
655
  // Call onReady hook
396
656
  if (this.onReadyFn) {
397
657
  context.logger.info('⚙️ Calling onReady hook...');
398
658
  await this.onReadyFn(this.context, (fn) => this.cleanups.push(fn));
399
659
  context.logger.info('✅ onReady hook completed');
400
660
  }
401
- context.logger.info(`✅ Plugin "${this.name}" loaded successfully`);
661
+ // Print comprehensive summary
662
+ context.logger.info('');
663
+ context.logger.info('╔══════════════════════════════════════════════════════════╗');
664
+ context.logger.info('║ 🎉 PLUGIN LOADED SUCCESSFULLY ║');
665
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
666
+ context.logger.info('║ 🌐 API ENDPOINTS ║');
667
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
668
+ // API Discovery endpoints
669
+ context.logger.info(`║ 🔍 Discovery: ║`);
670
+ context.logger.info(`║ ${context.http.baseUrl}/majk/plugin/api`);
671
+ context.logger.info(`║ ${context.http.baseUrl}/majk/plugin/routes`);
672
+ // Function endpoints
673
+ if (this.functionRegistry && this.functionRegistry.getFunctionNames().length > 0) {
674
+ context.logger.info(`║ ║`);
675
+ context.logger.info(`║ 🚀 Function Endpoints: ║`);
676
+ for (const fname of this.functionRegistry.getFunctionNames()) {
677
+ context.logger.info(`║ POST ${context.http.baseUrl}/api/fn/${fname}`);
678
+ }
679
+ }
680
+ // Legacy API routes
681
+ if (this.apiRoutes.length > 0) {
682
+ context.logger.info(`║ ║`);
683
+ context.logger.info(`║ 📋 Legacy API Routes: ║`);
684
+ for (const route of this.apiRoutes) {
685
+ context.logger.info(`║ ${route.method} ${context.http.baseUrl}${route.path}`);
686
+ }
687
+ }
688
+ // UI Screens
689
+ const allScreens = [...this.reactScreens, ...this.htmlScreens];
690
+ if (allScreens.length > 0 || this.uiConfig) {
691
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
692
+ context.logger.info('║ 🕹 UI SCREENS ║');
693
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
694
+ for (const screen of allScreens) {
695
+ context.logger.info(`║ • ${screen.name || screen.id}`);
696
+ context.logger.info(`║ ${context.http.baseUrl}${screen.route}`);
697
+ }
698
+ }
699
+ // Config Wizard
700
+ if (this.wizard) {
701
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
702
+ context.logger.info('║ 🧙 CONFIG WIZARD ║');
703
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
704
+ context.logger.info(`║ ${this.wizard.title}`);
705
+ context.logger.info(`║ ${context.http.baseUrl}${this.wizard.path}`);
706
+ }
707
+ // Settings
708
+ if (this.settings) {
709
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
710
+ context.logger.info('║ ⚙️ SETTINGS ║');
711
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
712
+ context.logger.info(`║ ${this.settings.title}`);
713
+ context.logger.info(`║ ${context.http.baseUrl}${this.settings.path}`);
714
+ }
715
+ context.logger.info('╚══════════════════════════════════════════════════════════╝');
716
+ context.logger.info('');
402
717
  }
403
718
  catch (error) {
404
719
  context.logger.error(`❌ Failed to load plugin "${this.name}": ${error.message}`);
@@ -526,8 +841,52 @@ class BuiltPlugin {
526
841
  this.context.logger.debug(`← ${method} ${url} ${res.statusCode || 200} (${duration}ms)`);
527
842
  return;
528
843
  }
529
- // API routes
530
- if (req.url?.startsWith('/api/')) {
844
+ // Function calls (new function-first API)
845
+ if (req.method === 'POST' && req.url?.startsWith('/api/fn/')) {
846
+ const functionName = req.url.substring('/api/fn/'.length).split('?')[0];
847
+ if (this.functionRegistry && this.transports.length > 0) {
848
+ // Use the first HTTP transport to handle the function call
849
+ const httpTransport = this.transports.find(t => t.name === 'http');
850
+ if (httpTransport) {
851
+ const body = await readBody(req);
852
+ const request = {
853
+ method: 'POST',
854
+ url: req.url,
855
+ pathname: req.url.split('?')[0],
856
+ query: new URL(req.url, `http://localhost:${port}`).searchParams,
857
+ params: {},
858
+ body,
859
+ headers: req.headers
860
+ };
861
+ const response = makeResponse(res);
862
+ await httpTransport.handleFunctionCall(functionName, request, response, {
863
+ majk: this.context.majk,
864
+ storage: this.context.storage,
865
+ logger: this.context.logger,
866
+ http: this.context.http
867
+ });
868
+ const duration = Date.now() - startTime;
869
+ this.context.logger.debug(`← POST /api/fn/${functionName} ${res.statusCode || 200} (${duration}ms)`);
870
+ return;
871
+ }
872
+ }
873
+ }
874
+ // Function discovery endpoint
875
+ if (req.method === 'GET' && req.url === '/api/fn/discovery') {
876
+ if (this.functionRegistry && this.transports.length > 0) {
877
+ const httpTransport = this.transports.find(t => t.name === 'http');
878
+ if (httpTransport) {
879
+ const discovery = httpTransport.getDiscovery();
880
+ res.writeHead(200, corsHeaders({ 'Content-Type': 'application/json' }));
881
+ res.end(JSON.stringify(discovery));
882
+ const duration = Date.now() - startTime;
883
+ this.context.logger.debug(`← GET /api/fn/discovery 200 (${duration}ms)`);
884
+ return;
885
+ }
886
+ }
887
+ }
888
+ // API routes (including discovery endpoints) - legacy
889
+ if (req.url?.startsWith('/api/') || req.url?.startsWith('/majk/plugin/')) {
531
890
  const url = new URL(req.url, `http://localhost:${port}`);
532
891
  const pathname = url.pathname;
533
892
  const method = req.method;
@@ -635,6 +994,9 @@ function groupByScope(tools) {
635
994
  * Create a plugin with fluent builder API
636
995
  */
637
996
  function definePlugin(id, name, version) {
997
+ log(`🔨 Building plugin: ${name} (${id}) v${version}`);
998
+ log(`📦 Plugin Kit: @majkapp/plugin-kit@1.0.19`);
999
+ log(``);
638
1000
  const _capabilities = [];
639
1001
  const _tools = new Map();
640
1002
  const _toolSpecs = [];
@@ -643,6 +1005,11 @@ function definePlugin(id, name, version) {
643
1005
  const _htmlScreens = [];
644
1006
  const _topbars = [];
645
1007
  const _entities = [];
1008
+ const _configurableEntities = [];
1009
+ // Function-first architecture state
1010
+ const _functionRegistry = new registry_1.FunctionRegistryImpl(id, name, version);
1011
+ const _transports = [];
1012
+ let _clientConfig = null;
646
1013
  let _ui = { appDir: 'ui/dist', base: '/', history: 'browser' };
647
1014
  let _uiConfigured = false;
648
1015
  let _wizard = null;
@@ -696,12 +1063,17 @@ function definePlugin(id, name, version) {
696
1063
  }
697
1064
  const iframeUrl = `http://localhost:{port}${_ui.base}${screen.reactPath}`;
698
1065
  _reactScreens.push(screen);
699
- _capabilities.push({
1066
+ log(` 🕹 Registered React screen: ${screen.name} at ${screen.route}`);
1067
+ const capability = {
700
1068
  type: 'screen',
701
1069
  id: screen.id,
702
1070
  path: screen.route,
703
1071
  iframeUrl
704
- });
1072
+ };
1073
+ if (screen.hash) {
1074
+ capability.hash = screen.hash;
1075
+ }
1076
+ _capabilities.push(capability);
705
1077
  return this;
706
1078
  },
707
1079
  screenHtml(screen) {
@@ -711,6 +1083,7 @@ function definePlugin(id, name, version) {
711
1083
  }
712
1084
  const iframeUrl = `http://localhost:{port}/__html/${encodeURIComponent(screen.id)}`;
713
1085
  _htmlScreens.push(screen);
1086
+ log(` 📋 Registered HTML screen: ${screen.name} at ${screen.route}`);
714
1087
  _capabilities.push({
715
1088
  type: 'screen',
716
1089
  id: screen.id,
@@ -724,9 +1097,70 @@ function definePlugin(id, name, version) {
724
1097
  if (!route.path.startsWith('/api/')) {
725
1098
  throw new PluginBuildError(`API route path must start with "/api/"`, `Change path from "${route.path}" to "/api/your-endpoint"`, { route: route.name });
726
1099
  }
1100
+ // Runtime validation for request schema (TypeScript already enforces at compile-time)
1101
+ // This catches any runtime bypasses or JavaScript usage
1102
+ if (!route.requestSchema) {
1103
+ throw new PluginBuildError(`API route "${route.name}" must provide a requestSchema`, `Add requestSchema to your route definition. For routes with no input, use an empty object: requestSchema: {}`, { route: route.name, path: route.path, method: route.method });
1104
+ }
1105
+ // Validate request schema structure
1106
+ if (route.requestSchema) {
1107
+ const { query, params, body } = route.requestSchema;
1108
+ if (query && query.type !== 'object') {
1109
+ throw new PluginBuildError(`Query schema for route "${route.name}" must be of type "object"`, `Change query.type to "object" in your schema definition`, { route: route.name });
1110
+ }
1111
+ if (params && params.type !== 'object') {
1112
+ throw new PluginBuildError(`Params schema for route "${route.name}" must be of type "object"`, `Change params.type to "object" in your schema definition`, { route: route.name });
1113
+ }
1114
+ // Check for path parameters in the route
1115
+ const pathParamMatches = route.path.match(/\/:([A-Za-z0-9_]+)/g);
1116
+ const pathParams = pathParamMatches ? pathParamMatches.map(p => p.substring(2)) : [];
1117
+ if (pathParams.length > 0 && !params) {
1118
+ throw new PluginBuildError(`Route "${route.name}" has path parameters but no params schema`, `Add params schema defining: ${pathParams.join(', ')}`, { route: route.name, pathParams });
1119
+ }
1120
+ if (params && pathParams.length > 0) {
1121
+ const schemaProps = params.properties || {};
1122
+ const missingParams = pathParams.filter(p => !(p in schemaProps));
1123
+ if (missingParams.length > 0) {
1124
+ throw new PluginBuildError(`Params schema missing definitions for path parameters: ${missingParams.join(', ')}`, `Add properties for: ${missingParams.join(', ')} to params.properties`, { route: route.name, missingParams });
1125
+ }
1126
+ }
1127
+ }
727
1128
  _apiRoutes.push(route);
728
1129
  return this;
729
1130
  },
1131
+ function(name, config) {
1132
+ validateDescription(config.description, `Function "${name}"`);
1133
+ try {
1134
+ _functionRegistry.registerFunction(name, config.description, config.input, config.output, config.handler, {
1135
+ tags: config.tags,
1136
+ deprecated: config.deprecated
1137
+ });
1138
+ // Log function registration
1139
+ log(` 📦 Registered function: ${name} - ${config.description}`);
1140
+ }
1141
+ catch (error) {
1142
+ throw new PluginBuildError(`Failed to register function "${name}"`, error.message, { function: name });
1143
+ }
1144
+ return this;
1145
+ },
1146
+ subscription(name, config) {
1147
+ validateDescription(config.description, `Subscription "${name}"`);
1148
+ try {
1149
+ _functionRegistry.registerSubscription(name, config.description, config.input, config.output, config.handler);
1150
+ }
1151
+ catch (error) {
1152
+ throw new PluginBuildError(`Failed to register subscription "${name}"`, error.message, { subscription: name });
1153
+ }
1154
+ return this;
1155
+ },
1156
+ transport(transport) {
1157
+ _transports.push(transport);
1158
+ return this;
1159
+ },
1160
+ generateClient(config) {
1161
+ _clientConfig = config;
1162
+ return this;
1163
+ },
730
1164
  tool(scope, spec, handler) {
731
1165
  validateDescription(spec.description, `Tool "${spec.name}"`);
732
1166
  if (_tools.has(spec.name)) {
@@ -762,12 +1196,97 @@ function definePlugin(id, name, version) {
762
1196
  // Type-safe wrapper for team member entities
763
1197
  return this.entity('teamMember', members);
764
1198
  },
1199
+ configurableEntity(entityType, factory) {
1200
+ _configurableEntities.push({ entityType, factory });
1201
+ log(` 🔧 Registered configurable entity: ${entityType} (requires config)`);
1202
+ return this;
1203
+ },
1204
+ configurableMcp(factory) {
1205
+ _configurableEntities.push({ entityType: 'mcpServer', factory });
1206
+ log(` 🔧 Registered configurable MCP server (requires config)`);
1207
+ return this;
1208
+ },
1209
+ configurableTeamMember(factory) {
1210
+ _configurableEntities.push({ entityType: 'teamMember', factory });
1211
+ log(` 🔧 Registered configurable team member (requires config)`);
1212
+ return this;
1213
+ },
765
1214
  configWizard(def) {
766
1215
  if (_wizard) {
767
1216
  throw new PluginBuildError('Config wizard already defined', 'You can only have one config wizard per plugin');
768
1217
  }
769
1218
  validateDescription(def.description, 'Config wizard');
770
1219
  _wizard = def;
1220
+ // Auto-generate config management functions if schema is provided
1221
+ if (def.schema) {
1222
+ const storageKey = def.storageKey || '_plugin_config';
1223
+ log(` 🔧 Schema provided - auto-generating config functions`);
1224
+ log(` Storage key: ${storageKey}`);
1225
+ // Auto-generate updateConfig function
1226
+ _functionRegistry.registerFunction('updateConfig', 'Update plugin configuration (auto-generated)', def.schema, {
1227
+ type: 'object',
1228
+ properties: {
1229
+ success: { type: 'boolean' },
1230
+ message: { type: 'string' },
1231
+ data: def.schema,
1232
+ error: { type: 'string' }
1233
+ },
1234
+ required: ['success']
1235
+ }, async (config, ctx) => {
1236
+ ctx.logger.info(`[ConfigWizard] updateConfig called`);
1237
+ ctx.logger.info(`[ConfigWizard] Config to save:`, JSON.stringify(config, null, 2));
1238
+ try {
1239
+ // Save to storage
1240
+ await ctx.storage.set(storageKey, config);
1241
+ ctx.logger.info(`[ConfigWizard] ✅ Config saved to storage key: ${storageKey}`);
1242
+ return {
1243
+ success: true,
1244
+ message: 'Configuration saved successfully',
1245
+ data: config
1246
+ };
1247
+ }
1248
+ catch (error) {
1249
+ ctx.logger.error(`[ConfigWizard] ❌ Failed to save config:`, error.message);
1250
+ return {
1251
+ success: false,
1252
+ error: error.message
1253
+ };
1254
+ }
1255
+ }, { tags: ['config'], deprecated: false });
1256
+ // Auto-generate getConfig function
1257
+ _functionRegistry.registerFunction('getConfig', 'Get current plugin configuration (auto-generated)', {
1258
+ type: 'object',
1259
+ properties: {},
1260
+ additionalProperties: false
1261
+ }, {
1262
+ type: 'object',
1263
+ properties: {
1264
+ success: { type: 'boolean' },
1265
+ data: def.schema,
1266
+ error: { type: 'string' }
1267
+ },
1268
+ required: ['success']
1269
+ }, async (_input, ctx) => {
1270
+ ctx.logger.info(`[ConfigWizard] getConfig called`);
1271
+ try {
1272
+ const config = await ctx.storage.get(storageKey);
1273
+ if (config) {
1274
+ ctx.logger.info(`[ConfigWizard] ✅ Config loaded from storage`);
1275
+ return { success: true, data: config };
1276
+ }
1277
+ else {
1278
+ ctx.logger.info(`[ConfigWizard] ⚠️ No config found in storage`);
1279
+ return { success: false, error: 'No configuration found' };
1280
+ }
1281
+ }
1282
+ catch (error) {
1283
+ ctx.logger.error(`[ConfigWizard] ❌ Failed to load config:`, error.message);
1284
+ return { success: false, error: error.message };
1285
+ }
1286
+ }, { tags: ['config'], deprecated: false });
1287
+ log(` ✅ Auto-generated: updateConfig()`);
1288
+ log(` ✅ Auto-generated: getConfig()`);
1289
+ }
771
1290
  return this;
772
1291
  },
773
1292
  settings(def) {
@@ -780,6 +1299,7 @@ function definePlugin(id, name, version) {
780
1299
  },
781
1300
  onReady(fn) {
782
1301
  _onReady = fn;
1302
+ log(` 🏁 Registered onReady lifecycle hook`);
783
1303
  return this;
784
1304
  },
785
1305
  health(fn) {
@@ -795,6 +1315,27 @@ function definePlugin(id, name, version) {
795
1315
  },
796
1316
  build() {
797
1317
  // ========== Build-Time Validation ==========
1318
+ // Validate configurable entities require configWizard with schema
1319
+ if (_configurableEntities.length > 0) {
1320
+ if (!_wizard) {
1321
+ throw new PluginBuildError('Configurable entities require configWizard', 'You used .configurableTeamMember(), .configurableMcp(), or .configurableEntity() but did not call .configWizard()', {
1322
+ configurableEntities: _configurableEntities.map(ce => ce.entityType),
1323
+ hint: 'Add .configWizard({ schema: YourSchema, ... }) to your plugin'
1324
+ });
1325
+ }
1326
+ if (!_wizard.schema) {
1327
+ throw new PluginBuildError('Configurable entities require configWizard with schema', 'You used configurable entities but .configWizard() does not have a schema property', {
1328
+ configurableEntities: _configurableEntities.map(ce => ce.entityType),
1329
+ hint: 'Add schema property to .configWizard({ schema: YourSchema, ... })'
1330
+ });
1331
+ }
1332
+ log(` ✅ Validated: ${_configurableEntities.length} configurable entities with config schema`);
1333
+ }
1334
+ // Warn if config wizard has schema but no configurable entities
1335
+ if (_wizard?.schema && _configurableEntities.length === 0) {
1336
+ log(` ⚠️ Warning: configWizard has schema but no configurable entities defined`);
1337
+ log(` Consider using .configurableTeamMember() or .configurableMcp() to take advantage of the config`);
1338
+ }
798
1339
  // Validate React UI setup
799
1340
  if (_reactScreens.length > 0 && !_uiConfigured) {
800
1341
  throw new PluginBuildError('UI not configured but React screens were added', 'Call .ui() before adding React screens', { reactScreens: _reactScreens.map(s => s.id) });
@@ -889,6 +1430,10 @@ function definePlugin(id, name, version) {
889
1430
  },
890
1431
  required: true
891
1432
  };
1433
+ // Add hash if provided
1434
+ if (_wizard.hash) {
1435
+ wizardCap.screen.hash = _wizard.hash;
1436
+ }
892
1437
  // Add shouldShow as method name reference if provided
893
1438
  if (_wizard.shouldShow) {
894
1439
  wizardCap.shouldShow = 'shouldShowConfigWizard';
@@ -897,7 +1442,7 @@ function definePlugin(id, name, version) {
897
1442
  }
898
1443
  // Add settings
899
1444
  if (_settings) {
900
- _capabilities.push({
1445
+ const settingsCap = {
901
1446
  type: 'settings-screen',
902
1447
  path: `/plugins/${id}/settings`,
903
1448
  name: _settings.title,
@@ -905,14 +1450,72 @@ function definePlugin(id, name, version) {
905
1450
  path: _settings.path,
906
1451
  title: _settings.title
907
1452
  }
908
- });
1453
+ };
1454
+ // Add hash if provided
1455
+ if (_settings.hash) {
1456
+ settingsCap.screen.hash = _settings.hash;
1457
+ }
1458
+ _capabilities.push(settingsCap);
909
1459
  }
910
1460
  // ========== Build Plugin Class ==========
911
1461
  // MAJK expects a class/constructor, not an instance
912
1462
  // Return a class that instantiates BuiltPlugin
1463
+ // If no transports specified but functions were defined, add default HTTP transport
1464
+ if (_functionRegistry.functions.size > 0 && _transports.length === 0) {
1465
+ _transports.push(new transports_1.HttpTransport());
1466
+ }
1467
+ // TODO: Implement client generation if configured
1468
+ if (_clientConfig) {
1469
+ // This would generate the client files based on the function registry
1470
+ // For now, just log that it was requested
1471
+ log('Client generation requested: ' + JSON.stringify(_clientConfig));
1472
+ }
1473
+ // Check for extract mode - output metadata and exit
1474
+ if (process.env.PLUGIN_KIT_MODE === 'extract') {
1475
+ const metadata = {
1476
+ id,
1477
+ name,
1478
+ version,
1479
+ functions: Array.from(_functionRegistry.functions.entries()).map(([fname, func]) => ({
1480
+ name: fname,
1481
+ description: func.description,
1482
+ input: func.input,
1483
+ output: func.output,
1484
+ tags: func.tags,
1485
+ deprecated: func.deprecated
1486
+ })),
1487
+ screens: [..._reactScreens, ..._htmlScreens].map(s => ({
1488
+ id: s.id,
1489
+ name: s.name,
1490
+ route: s.route,
1491
+ type: 'reactPath' in s ? 'react' : 'html'
1492
+ })),
1493
+ tools: _toolSpecs.map(t => ({
1494
+ name: t.spec.name,
1495
+ description: t.spec.description,
1496
+ scope: t.scope
1497
+ })),
1498
+ openapi: _functionRegistry.toOpenAPISpec()
1499
+ };
1500
+ // Write to file specified by env variable
1501
+ const outputFile = process.env.PLUGIN_KIT_OUTPUT;
1502
+ if (outputFile) {
1503
+ fs_1.default.writeFileSync(outputFile, JSON.stringify(metadata, null, 2), 'utf-8');
1504
+ console.log(`Metadata written to ${outputFile}`);
1505
+ }
1506
+ else {
1507
+ // Fallback to stdout if no file specified
1508
+ console.log(JSON.stringify(metadata, null, 2));
1509
+ }
1510
+ process.exit(0);
1511
+ }
913
1512
  return class extends BuiltPlugin {
914
1513
  constructor() {
915
- super(id, name, version, _capabilities, _tools, _apiRoutes, _uiConfigured ? _ui : null, _reactScreens, _htmlScreens, _wizard, _onReady, _healthCheck, _pluginRoot);
1514
+ super(id, name, version, _capabilities, _tools, _apiRoutes, _uiConfigured ? _ui : null, _reactScreens, _htmlScreens, _wizard, _settings, _onReady, _healthCheck, _pluginRoot,
1515
+ // Pass function registry and transports if functions were defined
1516
+ _functionRegistry.functions.size > 0 ? _functionRegistry : undefined, _transports.length > 0 ? _transports : undefined,
1517
+ // Pass configurable entities
1518
+ _configurableEntities.length > 0 ? _configurableEntities : undefined);
916
1519
  }
917
1520
  };
918
1521
  }