@unrdf/kgc-runtime 26.4.2
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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Plugin Manager - Lifecycle management for KGC plugins
|
|
3
|
+
* @module @unrdf/kgc-runtime/plugin-manager
|
|
4
|
+
* @description Manages plugin lifecycle with state machine: REGISTERED → LOADED → EXECUTING → UNLOADED
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { PluginManifestSchema, PluginStateSchema } from './schemas.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Plugin lifecycle states
|
|
12
|
+
*/
|
|
13
|
+
export const PLUGIN_STATES = {
|
|
14
|
+
REGISTERED: 'registered',
|
|
15
|
+
LOADED: 'loaded',
|
|
16
|
+
EXECUTING: 'executing',
|
|
17
|
+
UNLOADED: 'unloaded',
|
|
18
|
+
FAILED: 'failed',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Valid state transitions for plugin lifecycle
|
|
23
|
+
*/
|
|
24
|
+
const VALID_TRANSITIONS = {
|
|
25
|
+
[PLUGIN_STATES.REGISTERED]: [PLUGIN_STATES.LOADED, PLUGIN_STATES.FAILED],
|
|
26
|
+
[PLUGIN_STATES.LOADED]: [PLUGIN_STATES.EXECUTING, PLUGIN_STATES.UNLOADED, PLUGIN_STATES.FAILED],
|
|
27
|
+
[PLUGIN_STATES.EXECUTING]: [PLUGIN_STATES.LOADED, PLUGIN_STATES.UNLOADED, PLUGIN_STATES.FAILED],
|
|
28
|
+
[PLUGIN_STATES.UNLOADED]: [PLUGIN_STATES.LOADED],
|
|
29
|
+
[PLUGIN_STATES.FAILED]: [PLUGIN_STATES.UNLOADED],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Plugin Manager - Manages plugin lifecycle with deterministic state machine
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* import { PluginManager } from '@unrdf/kgc-runtime/plugin-manager';
|
|
37
|
+
* const manager = new PluginManager();
|
|
38
|
+
* await manager.registerPlugin({ name: 'my-plugin', version: '1.0.0', ... });
|
|
39
|
+
* await manager.loadPlugin('my-plugin');
|
|
40
|
+
* await manager.activatePlugin('my-plugin');
|
|
41
|
+
*/
|
|
42
|
+
export class PluginManager {
|
|
43
|
+
/**
|
|
44
|
+
* Create new plugin manager
|
|
45
|
+
* @param {Object} options - Configuration options
|
|
46
|
+
* @param {Object} options.isolation - Isolation settings
|
|
47
|
+
* @param {boolean} options.parallelLoading - Enable parallel loading (default: true)
|
|
48
|
+
*/
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
/** @type {Map<string, Object>} */
|
|
51
|
+
this.plugins = new Map();
|
|
52
|
+
|
|
53
|
+
/** @type {Map<string, string>} Plugin state map */
|
|
54
|
+
this.states = new Map();
|
|
55
|
+
|
|
56
|
+
/** @type {Array<Object>} Audit log for all operations */
|
|
57
|
+
this.auditLog = [];
|
|
58
|
+
|
|
59
|
+
/** @type {number} State transition counter */
|
|
60
|
+
this.transitionCount = 0;
|
|
61
|
+
|
|
62
|
+
this.options = {
|
|
63
|
+
parallelLoading: options.parallelLoading ?? true,
|
|
64
|
+
isolation: options.isolation || {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Register a plugin
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} manifest - Plugin manifest
|
|
72
|
+
* @returns {Promise<string>} Plugin ID
|
|
73
|
+
* @throws {Error} If manifest is invalid
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const id = await manager.registerPlugin({
|
|
77
|
+
* name: 'custom-receipt',
|
|
78
|
+
* version: '1.0.0',
|
|
79
|
+
* description: 'Custom receipt types',
|
|
80
|
+
* entryPoint: './plugin.mjs',
|
|
81
|
+
* capabilities: ['custom-receipt'],
|
|
82
|
+
* api_version: '5.0.1'
|
|
83
|
+
* });
|
|
84
|
+
*/
|
|
85
|
+
async registerPlugin(manifest) {
|
|
86
|
+
// Validate manifest
|
|
87
|
+
const validated = PluginManifestSchema.parse(manifest);
|
|
88
|
+
|
|
89
|
+
const pluginId = `${validated.name}@${validated.version}`;
|
|
90
|
+
|
|
91
|
+
// Check if already registered
|
|
92
|
+
if (this.plugins.has(pluginId)) {
|
|
93
|
+
throw new Error(`Plugin already registered: ${pluginId}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Store plugin
|
|
97
|
+
this.plugins.set(pluginId, {
|
|
98
|
+
...validated,
|
|
99
|
+
id: pluginId,
|
|
100
|
+
registeredAt: Date.now(),
|
|
101
|
+
loadedAt: null,
|
|
102
|
+
instance: null,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Set initial state
|
|
106
|
+
await this._transitionState(pluginId, PLUGIN_STATES.REGISTERED);
|
|
107
|
+
|
|
108
|
+
// Log registration
|
|
109
|
+
await this._auditLog('register', pluginId, { manifest: validated });
|
|
110
|
+
|
|
111
|
+
return pluginId;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Load a plugin (initialize but don't activate)
|
|
116
|
+
*
|
|
117
|
+
* @param {string} pluginId - Plugin ID (name@version)
|
|
118
|
+
* @returns {Promise<void>}
|
|
119
|
+
* @throws {Error} If plugin not found or load fails
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* await manager.loadPlugin('custom-receipt@1.0.0');
|
|
123
|
+
*/
|
|
124
|
+
async loadPlugin(pluginId) {
|
|
125
|
+
const plugin = this.plugins.get(pluginId);
|
|
126
|
+
|
|
127
|
+
if (!plugin) {
|
|
128
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Verify state allows loading
|
|
132
|
+
const currentState = this.states.get(pluginId);
|
|
133
|
+
if (!this._canTransition(currentState, PLUGIN_STATES.LOADED)) {
|
|
134
|
+
throw new Error(`Cannot load plugin in state: ${currentState}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Transition to loaded state
|
|
139
|
+
await this._transitionState(pluginId, PLUGIN_STATES.LOADED);
|
|
140
|
+
|
|
141
|
+
// Update metadata
|
|
142
|
+
plugin.loadedAt = Date.now();
|
|
143
|
+
|
|
144
|
+
// Log load
|
|
145
|
+
await this._auditLog('load', pluginId, { success: true });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
await this._transitionState(pluginId, PLUGIN_STATES.FAILED);
|
|
148
|
+
await this._auditLog('load', pluginId, { success: false, error: error.message });
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Activate a plugin (start execution)
|
|
155
|
+
*
|
|
156
|
+
* @param {string} pluginId - Plugin ID
|
|
157
|
+
* @returns {Promise<void>}
|
|
158
|
+
* @throws {Error} If plugin not loaded or activation fails
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* await manager.activatePlugin('custom-receipt@1.0.0');
|
|
162
|
+
*/
|
|
163
|
+
async activatePlugin(pluginId) {
|
|
164
|
+
const plugin = this.plugins.get(pluginId);
|
|
165
|
+
|
|
166
|
+
if (!plugin) {
|
|
167
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const currentState = this.states.get(pluginId);
|
|
171
|
+
if (!this._canTransition(currentState, PLUGIN_STATES.EXECUTING)) {
|
|
172
|
+
throw new Error(`Cannot activate plugin in state: ${currentState}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Transition to executing state
|
|
177
|
+
await this._transitionState(pluginId, PLUGIN_STATES.EXECUTING);
|
|
178
|
+
|
|
179
|
+
// Log activation
|
|
180
|
+
await this._auditLog('activate', pluginId, { success: true });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
await this._transitionState(pluginId, PLUGIN_STATES.FAILED);
|
|
183
|
+
await this._auditLog('activate', pluginId, { success: false, error: error.message });
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Deactivate a plugin (stop execution but keep loaded)
|
|
190
|
+
*
|
|
191
|
+
* @param {string} pluginId - Plugin ID
|
|
192
|
+
* @returns {Promise<void>}
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* await manager.deactivatePlugin('custom-receipt@1.0.0');
|
|
196
|
+
*/
|
|
197
|
+
async deactivatePlugin(pluginId) {
|
|
198
|
+
const plugin = this.plugins.get(pluginId);
|
|
199
|
+
|
|
200
|
+
if (!plugin) {
|
|
201
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const currentState = this.states.get(pluginId);
|
|
205
|
+
if (currentState !== PLUGIN_STATES.EXECUTING) {
|
|
206
|
+
throw new Error(`Plugin not executing: ${currentState}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Transition back to loaded
|
|
210
|
+
await this._transitionState(pluginId, PLUGIN_STATES.LOADED);
|
|
211
|
+
|
|
212
|
+
// Log deactivation
|
|
213
|
+
await this._auditLog('deactivate', pluginId, { success: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Unload a plugin (cleanup and remove from memory)
|
|
218
|
+
*
|
|
219
|
+
* @param {string} pluginId - Plugin ID
|
|
220
|
+
* @returns {Promise<void>}
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* await manager.unloadPlugin('custom-receipt@1.0.0');
|
|
224
|
+
*/
|
|
225
|
+
async unloadPlugin(pluginId) {
|
|
226
|
+
const plugin = this.plugins.get(pluginId);
|
|
227
|
+
|
|
228
|
+
if (!plugin) {
|
|
229
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const currentState = this.states.get(pluginId);
|
|
233
|
+
if (!this._canTransition(currentState, PLUGIN_STATES.UNLOADED)) {
|
|
234
|
+
throw new Error(`Cannot unload plugin in state: ${currentState}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Transition to unloaded
|
|
238
|
+
await this._transitionState(pluginId, PLUGIN_STATES.UNLOADED);
|
|
239
|
+
|
|
240
|
+
// Cleanup
|
|
241
|
+
plugin.instance = null;
|
|
242
|
+
plugin.loadedAt = null;
|
|
243
|
+
|
|
244
|
+
// Log unload
|
|
245
|
+
await this._auditLog('unload', pluginId, { success: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Uninstall a plugin (complete removal)
|
|
250
|
+
*
|
|
251
|
+
* @param {string} pluginId - Plugin ID
|
|
252
|
+
* @returns {Promise<void>}
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* await manager.uninstallPlugin('custom-receipt@1.0.0');
|
|
256
|
+
*/
|
|
257
|
+
async uninstallPlugin(pluginId) {
|
|
258
|
+
const plugin = this.plugins.get(pluginId);
|
|
259
|
+
|
|
260
|
+
if (!plugin) {
|
|
261
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Must be unloaded first
|
|
265
|
+
const currentState = this.states.get(pluginId);
|
|
266
|
+
if (currentState !== PLUGIN_STATES.UNLOADED && currentState !== PLUGIN_STATES.REGISTERED) {
|
|
267
|
+
throw new Error(`Must unload plugin before uninstall: ${currentState}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Remove from registry
|
|
271
|
+
this.plugins.delete(pluginId);
|
|
272
|
+
this.states.delete(pluginId);
|
|
273
|
+
|
|
274
|
+
// Log uninstall
|
|
275
|
+
await this._auditLog('uninstall', pluginId, { success: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get plugin state
|
|
280
|
+
*
|
|
281
|
+
* @param {string} pluginId - Plugin ID
|
|
282
|
+
* @returns {string|null} Current state or null if not found
|
|
283
|
+
*/
|
|
284
|
+
getPluginState(pluginId) {
|
|
285
|
+
return this.states.get(pluginId) || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get plugin manifest
|
|
290
|
+
*
|
|
291
|
+
* @param {string} pluginId - Plugin ID
|
|
292
|
+
* @returns {Object|null} Plugin manifest or null if not found
|
|
293
|
+
*/
|
|
294
|
+
getPlugin(pluginId) {
|
|
295
|
+
const plugin = this.plugins.get(pluginId);
|
|
296
|
+
if (!plugin) return null;
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
...plugin,
|
|
300
|
+
state: this.states.get(pluginId),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* List all plugins
|
|
306
|
+
*
|
|
307
|
+
* @returns {Array<Object>} Array of plugin manifests with states
|
|
308
|
+
*/
|
|
309
|
+
listPlugins() {
|
|
310
|
+
return Array.from(this.plugins.entries()).map(([id, plugin]) => ({
|
|
311
|
+
...plugin,
|
|
312
|
+
state: this.states.get(id),
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* List plugins by state
|
|
318
|
+
*
|
|
319
|
+
* @param {string} state - Plugin state
|
|
320
|
+
* @returns {Array<Object>} Plugins in specified state
|
|
321
|
+
*/
|
|
322
|
+
listPluginsByState(state) {
|
|
323
|
+
return this.listPlugins().filter(p => p.state === state);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get audit log
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} filters - Optional filters
|
|
330
|
+
* @param {string} filters.pluginId - Filter by plugin ID
|
|
331
|
+
* @param {string} filters.action - Filter by action
|
|
332
|
+
* @returns {Array<Object>} Filtered audit log entries
|
|
333
|
+
*/
|
|
334
|
+
getAuditLog(filters = {}) {
|
|
335
|
+
let log = this.auditLog;
|
|
336
|
+
|
|
337
|
+
if (filters.pluginId) {
|
|
338
|
+
log = log.filter(entry => entry.pluginId === filters.pluginId);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (filters.action) {
|
|
342
|
+
log = log.filter(entry => entry.action === filters.action);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return log;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get transition count
|
|
350
|
+
*
|
|
351
|
+
* @returns {number} Total number of state transitions
|
|
352
|
+
*/
|
|
353
|
+
getTransitionCount() {
|
|
354
|
+
return this.transitionCount;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Load multiple plugins in parallel
|
|
359
|
+
*
|
|
360
|
+
* @param {string[]} pluginIds - Array of plugin IDs
|
|
361
|
+
* @returns {Promise<Object>} Results { success: [], failed: [] }
|
|
362
|
+
*/
|
|
363
|
+
async loadPluginsParallel(pluginIds) {
|
|
364
|
+
if (!this.options.parallelLoading) {
|
|
365
|
+
throw new Error('Parallel loading not enabled');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const results = await Promise.allSettled(
|
|
369
|
+
pluginIds.map(id => this.loadPlugin(id))
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const success = [];
|
|
373
|
+
const failed = [];
|
|
374
|
+
|
|
375
|
+
results.forEach((result, index) => {
|
|
376
|
+
if (result.status === 'fulfilled') {
|
|
377
|
+
success.push(pluginIds[index]);
|
|
378
|
+
} else {
|
|
379
|
+
failed.push({
|
|
380
|
+
pluginId: pluginIds[index],
|
|
381
|
+
error: result.reason?.message || 'Unknown error',
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return { success, failed };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ===== Private Methods =====
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Transition plugin to new state
|
|
393
|
+
* @private
|
|
394
|
+
*/
|
|
395
|
+
async _transitionState(pluginId, newState) {
|
|
396
|
+
const currentState = this.states.get(pluginId);
|
|
397
|
+
|
|
398
|
+
// Validate transition
|
|
399
|
+
if (currentState && !this._canTransition(currentState, newState)) {
|
|
400
|
+
throw new Error(`Invalid transition: ${currentState} -> ${newState}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Update state
|
|
404
|
+
this.states.set(pluginId, newState);
|
|
405
|
+
this.transitionCount++;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Check if state transition is valid
|
|
410
|
+
* @private
|
|
411
|
+
*/
|
|
412
|
+
_canTransition(fromState, toState) {
|
|
413
|
+
if (!fromState) return true; // Initial registration
|
|
414
|
+
return VALID_TRANSITIONS[fromState]?.includes(toState) || false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Add entry to audit log
|
|
419
|
+
* @private
|
|
420
|
+
*/
|
|
421
|
+
async _auditLog(action, pluginId, details) {
|
|
422
|
+
const entry = {
|
|
423
|
+
timestamp: Date.now(),
|
|
424
|
+
action,
|
|
425
|
+
pluginId,
|
|
426
|
+
details,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
this.auditLog.push(entry);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Create a new plugin manager instance
|
|
435
|
+
*
|
|
436
|
+
* @param {Object} options - Configuration options
|
|
437
|
+
* @returns {PluginManager} New plugin manager
|
|
438
|
+
*/
|
|
439
|
+
export function createPluginManager(options = {}) {
|
|
440
|
+
return new PluginManager(options);
|
|
441
|
+
}
|