@majkapp/plugin-kit 1.0.18 → 1.2.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.
@@ -9,6 +9,14 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const registry_1 = require("./registry");
11
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
+ };
12
20
  /**
13
21
  * Build-time errors with clear, actionable messages
14
22
  */
@@ -257,20 +265,20 @@ function generateOpenApiSpec(pluginId, pluginName, pluginVersion, routes, baseUr
257
265
  */
258
266
  function pathToRegex(p) {
259
267
  const keys = [];
260
- console.log(`[pathToRegex] Input path: ${p}`);
268
+ log(`[pathToRegex] Input path: ${p}`);
261
269
  // First, escape special regex characters EXCEPT the colon (for path params)
262
270
  const escaped = p.replace(/([.+*?=^!${}()[\]|\\])/g, '\\$1');
263
- console.log(`[pathToRegex] After escaping: ${escaped}`);
271
+ log(`[pathToRegex] After escaping: ${escaped}`);
264
272
  // Then, replace path parameters with capturing groups
265
273
  const pattern = escaped.replace(/\/:([A-Za-z0-9_]+)/g, (_m, k) => {
266
274
  keys.push(k);
267
- console.log(`[pathToRegex] Found param: ${k}`);
275
+ log(`[pathToRegex] Found param: ${k}`);
268
276
  return '/([^/]+)';
269
277
  });
270
- console.log(`[pathToRegex] Final pattern: ${pattern}`);
271
- console.log(`[pathToRegex] Keys: [${keys.join(', ')}]`);
278
+ log(`[pathToRegex] Final pattern: ${pattern}`);
279
+ log(`[pathToRegex] Keys: [${keys.join(', ')}]`);
272
280
  const regex = new RegExp(`^${pattern}$`);
273
- console.log(`[pathToRegex] Final regex: ${regex}`);
281
+ log(`[pathToRegex] Final regex: ${regex}`);
274
282
  return { regex, keys };
275
283
  }
276
284
  /**
@@ -399,9 +407,13 @@ function serveSpa(ui, req, res, ctx) {
399
407
  * Main plugin class built by the fluent API
400
408
  */
401
409
  class BuiltPlugin {
402
- 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,
403
411
  // New parameters for function-first architecture
404
- functionRegistry, transports) {
412
+ functionRegistry, transports,
413
+ // New parameter for configurable entities
414
+ configurableEntities,
415
+ // New parameter for secret providers
416
+ secretProviders) {
405
417
  this.capabilities = capabilities;
406
418
  this.tools = tools;
407
419
  this.apiRoutes = apiRoutes;
@@ -409,9 +421,12 @@ class BuiltPlugin {
409
421
  this.reactScreens = reactScreens;
410
422
  this.htmlScreens = htmlScreens;
411
423
  this.wizard = wizard;
424
+ this.settings = settings;
412
425
  this.onReadyFn = onReadyFn;
413
426
  this.healthCheckFn = healthCheckFn;
414
427
  this.userProvidedPluginRoot = userProvidedPluginRoot;
428
+ this.configurableEntities = configurableEntities;
429
+ this.secretProviders = secretProviders;
415
430
  this.cleanups = [];
416
431
  this.router = [];
417
432
  this.transports = [];
@@ -420,11 +435,62 @@ class BuiltPlugin {
420
435
  this.version = version;
421
436
  this.functionRegistry = functionRegistry;
422
437
  this.transports = transports || [];
438
+ this.configurableEntities = configurableEntities || [];
439
+ this.secretProviders = secretProviders || [];
423
440
  }
424
- getCapabilities() {
441
+ async getCapabilities() {
442
+ // Start with static capabilities
443
+ const dynamicCapabilities = [...this.capabilities];
444
+ // Only process configurable entities if context is available (after onLoad has been called)
445
+ if (!this.context) {
446
+ // Context not set yet - return only static capabilities
447
+ // This happens when getCapabilities() is called before onLoad()
448
+ return {
449
+ apiVersion: 'majk.capabilities/v1',
450
+ capabilities: dynamicCapabilities
451
+ };
452
+ }
453
+ // If wizard has schema, check for config and add configurable entities
454
+ if (this.wizard?.schema && this.configurableEntities && this.configurableEntities.length > 0) {
455
+ const storageKey = this.wizard.storageKey || '_plugin_config';
456
+ this.context.logger.info(`[ConfigWizard] getCapabilities: checking for config at key: ${storageKey}`);
457
+ try {
458
+ // Load config from storage
459
+ const config = await this.context.storage.get(storageKey);
460
+ if (config) {
461
+ this.context.logger.info(`[ConfigWizard] ✅ Config found - generating configurable entities`);
462
+ this.context.logger.info(`[ConfigWizard] Config:`, JSON.stringify(config, null, 2));
463
+ // Call each configurable entity factory with the config
464
+ for (const ce of this.configurableEntities) {
465
+ this.context.logger.info(`[ConfigWizard] Calling factory for ${ce.entityType}...`);
466
+ try {
467
+ const entities = ce.factory(config);
468
+ this.context.logger.info(`[ConfigWizard] Factory returned ${entities.length} ${ce.entityType} entities`);
469
+ if (entities.length > 0) {
470
+ dynamicCapabilities.push({
471
+ type: 'entity',
472
+ entityType: ce.entityType,
473
+ entities: entities
474
+ });
475
+ this.context.logger.info(`[ConfigWizard] ✅ Added ${entities.length} ${ce.entityType} entities to capabilities`);
476
+ }
477
+ }
478
+ catch (factoryError) {
479
+ this.context.logger.error(`[ConfigWizard] ❌ Factory for ${ce.entityType} failed:`, factoryError.message);
480
+ }
481
+ }
482
+ }
483
+ else {
484
+ this.context.logger.info(`[ConfigWizard] ⚠️ No config found - configurable entities will not be registered`);
485
+ }
486
+ }
487
+ catch (error) {
488
+ this.context.logger.error(`[ConfigWizard] ❌ Failed to load config:`, error.message);
489
+ }
490
+ }
425
491
  return {
426
492
  apiVersion: 'majk.capabilities/v1',
427
- capabilities: this.capabilities
493
+ capabilities: dynamicCapabilities
428
494
  };
429
495
  }
430
496
  /**
@@ -452,6 +518,26 @@ class BuiltPlugin {
452
518
  }
453
519
  };
454
520
  }
521
+ async createSecretProvider(capability) {
522
+ const providerName = capability.name;
523
+ const providerDef = this.secretProviders?.find(sp => sp.name === providerName);
524
+ if (!providerDef) {
525
+ throw new PluginRuntimeError(`Secret provider "${providerName}" not found`, 'createSecretProvider', {
526
+ providerName,
527
+ availableProviders: this.secretProviders?.map(sp => sp.name) || []
528
+ });
529
+ }
530
+ this.context.logger.info(`[SecretProvider] Creating provider: ${providerName}`);
531
+ try {
532
+ const instance = await providerDef.factory(this.context);
533
+ this.context.logger.info(`[SecretProvider] ✅ Provider created: ${providerName}`);
534
+ return instance;
535
+ }
536
+ catch (error) {
537
+ this.context.logger.error(`[SecretProvider] ❌ Failed to create provider: ${providerName}`, error);
538
+ throw new PluginRuntimeError(`Failed to create secret provider "${providerName}"`, 'createSecretProvider', { providerName, error: error instanceof Error ? error.message : String(error) });
539
+ }
540
+ }
455
541
  async onLoad(context) {
456
542
  // Use user-provided pluginRoot (from __dirname) if available, otherwise fall back to context.pluginRoot
457
543
  // The user-provided path comes from Node's __dirname which automatically resolves to the real directory
@@ -463,6 +549,7 @@ class BuiltPlugin {
463
549
  };
464
550
  context.logger.info('═══════════════════════════════════════════════════════');
465
551
  context.logger.info(`🚀 Loading Plugin: ${this.name} (${this.id}) v${this.version}`);
552
+ context.logger.info(`🔧 Plugin Kit Version: @majkapp/plugin-kit@1.0.19`);
466
553
  context.logger.info(`📍 Plugin Root: ${effectivePluginRoot}`);
467
554
  if (this.userProvidedPluginRoot) {
468
555
  context.logger.info(` (user-provided via .pluginRoot(__dirname))`);
@@ -495,10 +582,30 @@ class BuiltPlugin {
495
582
  await this.startServer();
496
583
  // Initialize transports for function-first architecture
497
584
  if (this.functionRegistry) {
585
+ context.logger.info('');
586
+ context.logger.info('🔌 Initializing Transports...');
498
587
  for (const transport of this.transports) {
499
588
  await transport.initialize(this.functionRegistry, this.context);
500
589
  await transport.start();
501
- context.logger.info(`✅ ${transport.name} transport initialized`);
590
+ const metadata = transport.getMetadata();
591
+ context.logger.info(` ✅ ${transport.name} transport initialized`);
592
+ context.logger.info(` • Base Path: ${metadata.endpoint}`);
593
+ context.logger.info(` • Discovery: ${metadata.discovery}`);
594
+ }
595
+ // Log registered functions
596
+ const functionNames = this.functionRegistry.getFunctionNames();
597
+ if (functionNames.length > 0) {
598
+ context.logger.info('');
599
+ context.logger.info(`📦 Registered ${functionNames.length} Functions:`);
600
+ for (const fname of functionNames) {
601
+ const func = this.functionRegistry.functions.get(fname);
602
+ if (func) {
603
+ context.logger.info(` • ${fname}: ${func.description}`);
604
+ if (func.deprecated) {
605
+ context.logger.info(` ⚠️ DEPRECATED`);
606
+ }
607
+ }
608
+ }
502
609
  }
503
610
  }
504
611
  // Register API routes (legacy)
@@ -528,6 +635,11 @@ class BuiltPlugin {
528
635
  name: 'API Discovery',
529
636
  description: 'Get OpenAPI specification for all plugin API routes',
530
637
  handler: async (_req, _res, _ctx) => {
638
+ // If using function-first architecture, generate spec from function registry
639
+ if (this.functionRegistry && this.functionRegistry.getFunctionNames().length > 0) {
640
+ return this.functionRegistry.toOpenAPISpec();
641
+ }
642
+ // Otherwise fall back to legacy API routes
531
643
  const openApiSpec = generateOpenApiSpec(this.id, this.name, this.version, this.apiRoutes, this.context.http.baseUrl);
532
644
  return openApiSpec;
533
645
  }
@@ -570,7 +682,62 @@ class BuiltPlugin {
570
682
  await this.onReadyFn(this.context, (fn) => this.cleanups.push(fn));
571
683
  context.logger.info('✅ onReady hook completed');
572
684
  }
573
- context.logger.info(`✅ Plugin "${this.name}" loaded successfully`);
685
+ // Print comprehensive summary
686
+ context.logger.info('');
687
+ context.logger.info('╔══════════════════════════════════════════════════════════╗');
688
+ context.logger.info('║ 🎉 PLUGIN LOADED SUCCESSFULLY ║');
689
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
690
+ context.logger.info('║ 🌐 API ENDPOINTS ║');
691
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
692
+ // API Discovery endpoints
693
+ context.logger.info(`║ 🔍 Discovery: ║`);
694
+ context.logger.info(`║ ${context.http.baseUrl}/majk/plugin/api`);
695
+ context.logger.info(`║ ${context.http.baseUrl}/majk/plugin/routes`);
696
+ // Function endpoints
697
+ if (this.functionRegistry && this.functionRegistry.getFunctionNames().length > 0) {
698
+ context.logger.info(`║ ║`);
699
+ context.logger.info(`║ 🚀 Function Endpoints: ║`);
700
+ for (const fname of this.functionRegistry.getFunctionNames()) {
701
+ context.logger.info(`║ POST ${context.http.baseUrl}/api/fn/${fname}`);
702
+ }
703
+ }
704
+ // Legacy API routes
705
+ if (this.apiRoutes.length > 0) {
706
+ context.logger.info(`║ ║`);
707
+ context.logger.info(`║ 📋 Legacy API Routes: ║`);
708
+ for (const route of this.apiRoutes) {
709
+ context.logger.info(`║ ${route.method} ${context.http.baseUrl}${route.path}`);
710
+ }
711
+ }
712
+ // UI Screens
713
+ const allScreens = [...this.reactScreens, ...this.htmlScreens];
714
+ if (allScreens.length > 0 || this.uiConfig) {
715
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
716
+ context.logger.info('║ 🕹 UI SCREENS ║');
717
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
718
+ for (const screen of allScreens) {
719
+ context.logger.info(`║ • ${screen.name || screen.id}`);
720
+ context.logger.info(`║ ${context.http.baseUrl}${screen.route}`);
721
+ }
722
+ }
723
+ // Config Wizard
724
+ if (this.wizard) {
725
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
726
+ context.logger.info('║ 🧙 CONFIG WIZARD ║');
727
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
728
+ context.logger.info(`║ ${this.wizard.title}`);
729
+ context.logger.info(`║ ${context.http.baseUrl}${this.wizard.path}`);
730
+ }
731
+ // Settings
732
+ if (this.settings) {
733
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
734
+ context.logger.info('║ ⚙️ SETTINGS ║');
735
+ context.logger.info('╟──────────────────────────────────────────────────────────╢');
736
+ context.logger.info(`║ ${this.settings.title}`);
737
+ context.logger.info(`║ ${context.http.baseUrl}${this.settings.path}`);
738
+ }
739
+ context.logger.info('╚══════════════════════════════════════════════════════════╝');
740
+ context.logger.info('');
574
741
  }
575
742
  catch (error) {
576
743
  context.logger.error(`❌ Failed to load plugin "${this.name}": ${error.message}`);
@@ -851,6 +1018,9 @@ function groupByScope(tools) {
851
1018
  * Create a plugin with fluent builder API
852
1019
  */
853
1020
  function definePlugin(id, name, version) {
1021
+ log(`🔨 Building plugin: ${name} (${id}) v${version}`);
1022
+ log(`📦 Plugin Kit: @majkapp/plugin-kit@1.0.19`);
1023
+ log(``);
854
1024
  const _capabilities = [];
855
1025
  const _tools = new Map();
856
1026
  const _toolSpecs = [];
@@ -859,6 +1029,8 @@ function definePlugin(id, name, version) {
859
1029
  const _htmlScreens = [];
860
1030
  const _topbars = [];
861
1031
  const _entities = [];
1032
+ const _configurableEntities = [];
1033
+ const _secretProviders = [];
862
1034
  // Function-first architecture state
863
1035
  const _functionRegistry = new registry_1.FunctionRegistryImpl(id, name, version);
864
1036
  const _transports = [];
@@ -916,12 +1088,17 @@ function definePlugin(id, name, version) {
916
1088
  }
917
1089
  const iframeUrl = `http://localhost:{port}${_ui.base}${screen.reactPath}`;
918
1090
  _reactScreens.push(screen);
919
- _capabilities.push({
1091
+ log(` 🕹 Registered React screen: ${screen.name} at ${screen.route}`);
1092
+ const capability = {
920
1093
  type: 'screen',
921
1094
  id: screen.id,
922
1095
  path: screen.route,
923
1096
  iframeUrl
924
- });
1097
+ };
1098
+ if (screen.hash) {
1099
+ capability.hash = screen.hash;
1100
+ }
1101
+ _capabilities.push(capability);
925
1102
  return this;
926
1103
  },
927
1104
  screenHtml(screen) {
@@ -931,6 +1108,7 @@ function definePlugin(id, name, version) {
931
1108
  }
932
1109
  const iframeUrl = `http://localhost:{port}/__html/${encodeURIComponent(screen.id)}`;
933
1110
  _htmlScreens.push(screen);
1111
+ log(` 📋 Registered HTML screen: ${screen.name} at ${screen.route}`);
934
1112
  _capabilities.push({
935
1113
  type: 'screen',
936
1114
  id: screen.id,
@@ -982,6 +1160,8 @@ function definePlugin(id, name, version) {
982
1160
  tags: config.tags,
983
1161
  deprecated: config.deprecated
984
1162
  });
1163
+ // Log function registration
1164
+ log(` 📦 Registered function: ${name} - ${config.description}`);
985
1165
  }
986
1166
  catch (error) {
987
1167
  throw new PluginBuildError(`Failed to register function "${name}"`, error.message, { function: name });
@@ -1041,12 +1221,108 @@ function definePlugin(id, name, version) {
1041
1221
  // Type-safe wrapper for team member entities
1042
1222
  return this.entity('teamMember', members);
1043
1223
  },
1224
+ configurableEntity(entityType, factory) {
1225
+ _configurableEntities.push({ entityType, factory });
1226
+ log(` 🔧 Registered configurable entity: ${entityType} (requires config)`);
1227
+ return this;
1228
+ },
1229
+ configurableMcp(factory) {
1230
+ _configurableEntities.push({ entityType: 'mcpServer', factory });
1231
+ log(` 🔧 Registered configurable MCP server (requires config)`);
1232
+ return this;
1233
+ },
1234
+ configurableTeamMember(factory) {
1235
+ _configurableEntities.push({ entityType: 'teamMember', factory });
1236
+ log(` 🔧 Registered configurable team member (requires config)`);
1237
+ return this;
1238
+ },
1239
+ secretProvider(def) {
1240
+ if (!def.name) {
1241
+ throw new PluginBuildError('Secret provider requires a name', 'Provide a name in the SecretProviderDef', { provided: def });
1242
+ }
1243
+ if (!def.factory) {
1244
+ throw new PluginBuildError('Secret provider requires a factory function', 'Provide a factory function that returns a SecretProviderInstance', { name: def.name });
1245
+ }
1246
+ _secretProviders.push(def);
1247
+ log(` 🔐 Registered secret provider: ${def.name} (priority: ${def.priority || 100})`);
1248
+ return this;
1249
+ },
1044
1250
  configWizard(def) {
1045
1251
  if (_wizard) {
1046
1252
  throw new PluginBuildError('Config wizard already defined', 'You can only have one config wizard per plugin');
1047
1253
  }
1048
1254
  validateDescription(def.description, 'Config wizard');
1049
1255
  _wizard = def;
1256
+ // Auto-generate config management functions if schema is provided
1257
+ if (def.schema) {
1258
+ const storageKey = def.storageKey || '_plugin_config';
1259
+ log(` 🔧 Schema provided - auto-generating config functions`);
1260
+ log(` Storage key: ${storageKey}`);
1261
+ // Auto-generate updateConfig function
1262
+ _functionRegistry.registerFunction('updateConfig', 'Update plugin configuration (auto-generated)', def.schema, {
1263
+ type: 'object',
1264
+ properties: {
1265
+ success: { type: 'boolean' },
1266
+ message: { type: 'string' },
1267
+ data: def.schema,
1268
+ error: { type: 'string' }
1269
+ },
1270
+ required: ['success']
1271
+ }, async (config, ctx) => {
1272
+ ctx.logger.info(`[ConfigWizard] updateConfig called`);
1273
+ ctx.logger.info(`[ConfigWizard] Config to save:`, JSON.stringify(config, null, 2));
1274
+ try {
1275
+ // Save to storage
1276
+ await ctx.storage.set(storageKey, config);
1277
+ ctx.logger.info(`[ConfigWizard] ✅ Config saved to storage key: ${storageKey}`);
1278
+ return {
1279
+ success: true,
1280
+ message: 'Configuration saved successfully',
1281
+ data: config
1282
+ };
1283
+ }
1284
+ catch (error) {
1285
+ ctx.logger.error(`[ConfigWizard] ❌ Failed to save config:`, error.message);
1286
+ return {
1287
+ success: false,
1288
+ error: error.message
1289
+ };
1290
+ }
1291
+ }, { tags: ['config'], deprecated: false });
1292
+ // Auto-generate getConfig function
1293
+ _functionRegistry.registerFunction('getConfig', 'Get current plugin configuration (auto-generated)', {
1294
+ type: 'object',
1295
+ properties: {},
1296
+ additionalProperties: false
1297
+ }, {
1298
+ type: 'object',
1299
+ properties: {
1300
+ success: { type: 'boolean' },
1301
+ data: def.schema,
1302
+ error: { type: 'string' }
1303
+ },
1304
+ required: ['success']
1305
+ }, async (_input, ctx) => {
1306
+ ctx.logger.info(`[ConfigWizard] getConfig called`);
1307
+ try {
1308
+ const config = await ctx.storage.get(storageKey);
1309
+ if (config) {
1310
+ ctx.logger.info(`[ConfigWizard] ✅ Config loaded from storage`);
1311
+ return { success: true, data: config };
1312
+ }
1313
+ else {
1314
+ ctx.logger.info(`[ConfigWizard] ⚠️ No config found in storage`);
1315
+ return { success: false, error: 'No configuration found' };
1316
+ }
1317
+ }
1318
+ catch (error) {
1319
+ ctx.logger.error(`[ConfigWizard] ❌ Failed to load config:`, error.message);
1320
+ return { success: false, error: error.message };
1321
+ }
1322
+ }, { tags: ['config'], deprecated: false });
1323
+ log(` ✅ Auto-generated: updateConfig()`);
1324
+ log(` ✅ Auto-generated: getConfig()`);
1325
+ }
1050
1326
  return this;
1051
1327
  },
1052
1328
  settings(def) {
@@ -1059,6 +1335,7 @@ function definePlugin(id, name, version) {
1059
1335
  },
1060
1336
  onReady(fn) {
1061
1337
  _onReady = fn;
1338
+ log(` 🏁 Registered onReady lifecycle hook`);
1062
1339
  return this;
1063
1340
  },
1064
1341
  health(fn) {
@@ -1074,6 +1351,27 @@ function definePlugin(id, name, version) {
1074
1351
  },
1075
1352
  build() {
1076
1353
  // ========== Build-Time Validation ==========
1354
+ // Validate configurable entities require configWizard with schema
1355
+ if (_configurableEntities.length > 0) {
1356
+ if (!_wizard) {
1357
+ throw new PluginBuildError('Configurable entities require configWizard', 'You used .configurableTeamMember(), .configurableMcp(), or .configurableEntity() but did not call .configWizard()', {
1358
+ configurableEntities: _configurableEntities.map(ce => ce.entityType),
1359
+ hint: 'Add .configWizard({ schema: YourSchema, ... }) to your plugin'
1360
+ });
1361
+ }
1362
+ if (!_wizard.schema) {
1363
+ throw new PluginBuildError('Configurable entities require configWizard with schema', 'You used configurable entities but .configWizard() does not have a schema property', {
1364
+ configurableEntities: _configurableEntities.map(ce => ce.entityType),
1365
+ hint: 'Add schema property to .configWizard({ schema: YourSchema, ... })'
1366
+ });
1367
+ }
1368
+ log(` ✅ Validated: ${_configurableEntities.length} configurable entities with config schema`);
1369
+ }
1370
+ // Warn if config wizard has schema but no configurable entities
1371
+ if (_wizard?.schema && _configurableEntities.length === 0) {
1372
+ log(` ⚠️ Warning: configWizard has schema but no configurable entities defined`);
1373
+ log(` Consider using .configurableTeamMember() or .configurableMcp() to take advantage of the config`);
1374
+ }
1077
1375
  // Validate React UI setup
1078
1376
  if (_reactScreens.length > 0 && !_uiConfigured) {
1079
1377
  throw new PluginBuildError('UI not configured but React screens were added', 'Call .ui() before adding React screens', { reactScreens: _reactScreens.map(s => s.id) });
@@ -1156,6 +1454,28 @@ function definePlugin(id, name, version) {
1156
1454
  entities: entity.entities
1157
1455
  });
1158
1456
  }
1457
+ // Add secret providers
1458
+ for (const provider of _secretProviders) {
1459
+ const providerCap = {
1460
+ type: 'secret-provider',
1461
+ name: provider.name,
1462
+ priority: provider.priority || 100,
1463
+ scopes: provider.scopes || ['global', 'project', 'integration']
1464
+ };
1465
+ if (provider.description) {
1466
+ providerCap.metadata = providerCap.metadata || {};
1467
+ providerCap.metadata.description = provider.description;
1468
+ }
1469
+ if (provider.icon) {
1470
+ providerCap.metadata = providerCap.metadata || {};
1471
+ providerCap.metadata.icon = provider.icon;
1472
+ }
1473
+ if (provider.tags) {
1474
+ providerCap.metadata = providerCap.metadata || {};
1475
+ providerCap.metadata.tags = provider.tags;
1476
+ }
1477
+ _capabilities.push(providerCap);
1478
+ }
1159
1479
  // Add wizard
1160
1480
  if (_wizard) {
1161
1481
  const wizardCap = {
@@ -1168,6 +1488,10 @@ function definePlugin(id, name, version) {
1168
1488
  },
1169
1489
  required: true
1170
1490
  };
1491
+ // Add hash if provided
1492
+ if (_wizard.hash) {
1493
+ wizardCap.screen.hash = _wizard.hash;
1494
+ }
1171
1495
  // Add shouldShow as method name reference if provided
1172
1496
  if (_wizard.shouldShow) {
1173
1497
  wizardCap.shouldShow = 'shouldShowConfigWizard';
@@ -1176,7 +1500,7 @@ function definePlugin(id, name, version) {
1176
1500
  }
1177
1501
  // Add settings
1178
1502
  if (_settings) {
1179
- _capabilities.push({
1503
+ const settingsCap = {
1180
1504
  type: 'settings-screen',
1181
1505
  path: `/plugins/${id}/settings`,
1182
1506
  name: _settings.title,
@@ -1184,7 +1508,12 @@ function definePlugin(id, name, version) {
1184
1508
  path: _settings.path,
1185
1509
  title: _settings.title
1186
1510
  }
1187
- });
1511
+ };
1512
+ // Add hash if provided
1513
+ if (_settings.hash) {
1514
+ settingsCap.screen.hash = _settings.hash;
1515
+ }
1516
+ _capabilities.push(settingsCap);
1188
1517
  }
1189
1518
  // ========== Build Plugin Class ==========
1190
1519
  // MAJK expects a class/constructor, not an instance
@@ -1197,13 +1526,56 @@ function definePlugin(id, name, version) {
1197
1526
  if (_clientConfig) {
1198
1527
  // This would generate the client files based on the function registry
1199
1528
  // For now, just log that it was requested
1200
- console.log('Client generation requested:', _clientConfig);
1529
+ log('Client generation requested: ' + JSON.stringify(_clientConfig));
1530
+ }
1531
+ // Check for extract mode - output metadata and exit
1532
+ if (process.env.PLUGIN_KIT_MODE === 'extract') {
1533
+ const metadata = {
1534
+ id,
1535
+ name,
1536
+ version,
1537
+ functions: Array.from(_functionRegistry.functions.entries()).map(([fname, func]) => ({
1538
+ name: fname,
1539
+ description: func.description,
1540
+ input: func.input,
1541
+ output: func.output,
1542
+ tags: func.tags,
1543
+ deprecated: func.deprecated
1544
+ })),
1545
+ screens: [..._reactScreens, ..._htmlScreens].map(s => ({
1546
+ id: s.id,
1547
+ name: s.name,
1548
+ route: s.route,
1549
+ type: 'reactPath' in s ? 'react' : 'html'
1550
+ })),
1551
+ tools: _toolSpecs.map(t => ({
1552
+ name: t.spec.name,
1553
+ description: t.spec.description,
1554
+ scope: t.scope
1555
+ })),
1556
+ openapi: _functionRegistry.toOpenAPISpec()
1557
+ };
1558
+ // Write to file specified by env variable
1559
+ const outputFile = process.env.PLUGIN_KIT_OUTPUT;
1560
+ if (outputFile) {
1561
+ fs_1.default.writeFileSync(outputFile, JSON.stringify(metadata, null, 2), 'utf-8');
1562
+ console.log(`Metadata written to ${outputFile}`);
1563
+ }
1564
+ else {
1565
+ // Fallback to stdout if no file specified
1566
+ console.log(JSON.stringify(metadata, null, 2));
1567
+ }
1568
+ process.exit(0);
1201
1569
  }
1202
1570
  return class extends BuiltPlugin {
1203
1571
  constructor() {
1204
- super(id, name, version, _capabilities, _tools, _apiRoutes, _uiConfigured ? _ui : null, _reactScreens, _htmlScreens, _wizard, _onReady, _healthCheck, _pluginRoot,
1572
+ super(id, name, version, _capabilities, _tools, _apiRoutes, _uiConfigured ? _ui : null, _reactScreens, _htmlScreens, _wizard, _settings, _onReady, _healthCheck, _pluginRoot,
1205
1573
  // Pass function registry and transports if functions were defined
1206
- _functionRegistry.functions.size > 0 ? _functionRegistry : undefined, _transports.length > 0 ? _transports : undefined);
1574
+ _functionRegistry.functions.size > 0 ? _functionRegistry : undefined, _transports.length > 0 ? _transports : undefined,
1575
+ // Pass configurable entities
1576
+ _configurableEntities.length > 0 ? _configurableEntities : undefined,
1577
+ // Pass secret providers
1578
+ _secretProviders.length > 0 ? _secretProviders : undefined);
1207
1579
  }
1208
1580
  };
1209
1581
  }