@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.
- package/README.md +1358 -334
- package/dist/generator/cli.d.ts +3 -0
- package/dist/generator/cli.d.ts.map +1 -0
- package/dist/generator/cli.js +161 -0
- package/dist/generator/generator.d.ts +6 -0
- package/dist/generator/generator.d.ts.map +1 -0
- package/dist/generator/generator.js +389 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/plugin-kit.d.ts +9 -1
- package/dist/plugin-kit.d.ts.map +1 -1
- package/dist/plugin-kit.js +391 -19
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -1
package/dist/plugin-kit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
+
log(`[pathToRegex] Found param: ${k}`);
|
|
268
276
|
return '/([^/]+)';
|
|
269
277
|
});
|
|
270
|
-
|
|
271
|
-
|
|
278
|
+
log(`[pathToRegex] Final pattern: ${pattern}`);
|
|
279
|
+
log(`[pathToRegex] Keys: [${keys.join(', ')}]`);
|
|
272
280
|
const regex = new RegExp(`^${pattern}$`);
|
|
273
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|