tackle-harness 0.0.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/LICENSE +21 -0
- package/README.en.md +259 -0
- package/README.md +261 -0
- package/bin/tackle.js +150 -0
- package/package.json +29 -0
- package/plugins/contracts/plugin-interface.js +244 -0
- package/plugins/core/hook-skill-gate/index.js +437 -0
- package/plugins/core/hook-skill-gate/plugin.json +12 -0
- package/plugins/core/provider-memory-store/index.js +403 -0
- package/plugins/core/provider-memory-store/plugin.json +9 -0
- package/plugins/core/provider-role-registry/index.js +477 -0
- package/plugins/core/provider-role-registry/plugin.json +9 -0
- package/plugins/core/provider-state-store/index.js +244 -0
- package/plugins/core/provider-state-store/plugin.json +9 -0
- package/plugins/core/skill-agent-dispatcher/plugin.json +13 -0
- package/plugins/core/skill-agent-dispatcher/skill.md +912 -0
- package/plugins/core/skill-batch-task-creator/plugin.json +13 -0
- package/plugins/core/skill-batch-task-creator/skill.md +616 -0
- package/plugins/core/skill-checklist/plugin.json +10 -0
- package/plugins/core/skill-checklist/skill.md +115 -0
- package/plugins/core/skill-completion-report/plugin.json +10 -0
- package/plugins/core/skill-completion-report/skill.md +331 -0
- package/plugins/core/skill-experience-logger/plugin.json +10 -0
- package/plugins/core/skill-experience-logger/skill.md +235 -0
- package/plugins/core/skill-human-checkpoint/plugin.json +10 -0
- package/plugins/core/skill-human-checkpoint/skill.md +194 -0
- package/plugins/core/skill-progress-tracker/plugin.json +10 -0
- package/plugins/core/skill-progress-tracker/skill.md +204 -0
- package/plugins/core/skill-role-manager/plugin.json +10 -0
- package/plugins/core/skill-role-manager/skill.md +252 -0
- package/plugins/core/skill-split-work-package/plugin.json +13 -0
- package/plugins/core/skill-split-work-package/skill.md +446 -0
- package/plugins/core/skill-task-creator/plugin.json +13 -0
- package/plugins/core/skill-task-creator/skill.md +744 -0
- package/plugins/core/skill-team-cleanup/plugin.json +10 -0
- package/plugins/core/skill-team-cleanup/skill.md +266 -0
- package/plugins/core/skill-workflow-orchestrator/plugin.json +13 -0
- package/plugins/core/skill-workflow-orchestrator/skill.md +274 -0
- package/plugins/core/validator-doc-sync/index.js +248 -0
- package/plugins/core/validator-doc-sync/plugin.json +9 -0
- package/plugins/core/validator-work-package/index.js +300 -0
- package/plugins/core/validator-work-package/plugin.json +9 -0
- package/plugins/plugin-registry.json +118 -0
- package/plugins/runtime/config-manager.js +306 -0
- package/plugins/runtime/event-bus.js +187 -0
- package/plugins/runtime/harness-build.js +1019 -0
- package/plugins/runtime/logger.js +174 -0
- package/plugins/runtime/plugin-loader.js +339 -0
- package/plugins/runtime/state-store.js +277 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger - Plugin-level logging service for AI Agent Harness
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - debug / info / warn / error log levels
|
|
6
|
+
* - Per-plugin log segregation
|
|
7
|
+
* - History query interface for debugging
|
|
8
|
+
* - No external dependencies
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const LOG_LEVELS = {
|
|
14
|
+
debug: 0,
|
|
15
|
+
info: 1,
|
|
16
|
+
warn: 2,
|
|
17
|
+
error: 3,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
class Logger {
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} [options]
|
|
23
|
+
* @param {string} [options.level='info'] - minimum log level
|
|
24
|
+
* @param {number} [options.maxHistory=500] - max history entries to retain
|
|
25
|
+
*/
|
|
26
|
+
constructor(options) {
|
|
27
|
+
options = options || {};
|
|
28
|
+
this._minLevel = LOG_LEVELS[options.level] !== undefined
|
|
29
|
+
? LOG_LEVELS[options.level]
|
|
30
|
+
: LOG_LEVELS.info;
|
|
31
|
+
this._maxHistory = options.maxHistory || 500;
|
|
32
|
+
this._history = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- public API ---
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Log a debug message.
|
|
39
|
+
* @param {string} plugin - plugin name
|
|
40
|
+
* @param {string} message
|
|
41
|
+
* @param {object} [data]
|
|
42
|
+
*/
|
|
43
|
+
debug(plugin, message, data) {
|
|
44
|
+
this._log('debug', plugin, message, data);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Log an info message.
|
|
49
|
+
* @param {string} plugin
|
|
50
|
+
* @param {string} message
|
|
51
|
+
* @param {object} [data]
|
|
52
|
+
*/
|
|
53
|
+
info(plugin, message, data) {
|
|
54
|
+
this._log('info', plugin, message, data);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log a warning message.
|
|
59
|
+
* @param {string} plugin
|
|
60
|
+
* @param {string} message
|
|
61
|
+
* @param {object} [data]
|
|
62
|
+
*/
|
|
63
|
+
warn(plugin, message, data) {
|
|
64
|
+
this._log('warn', plugin, message, data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Log an error message.
|
|
69
|
+
* @param {string} plugin
|
|
70
|
+
* @param {string} message
|
|
71
|
+
* @param {object} [data]
|
|
72
|
+
*/
|
|
73
|
+
error(plugin, message, data) {
|
|
74
|
+
this._log('error', plugin, message, data);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Query log history.
|
|
79
|
+
* @param {object} [filter]
|
|
80
|
+
* @param {string} [filter.plugin] - filter by plugin name
|
|
81
|
+
* @param {string} [filter.level] - filter by level (debug/info/warn/error)
|
|
82
|
+
* @param {number} [filter.since] - timestamp lower bound (ms)
|
|
83
|
+
* @param {number} [filter.until] - timestamp upper bound (ms)
|
|
84
|
+
* @param {number} [filter.limit] - max entries to return
|
|
85
|
+
* @returns {object[]}
|
|
86
|
+
*/
|
|
87
|
+
query(filter) {
|
|
88
|
+
filter = filter || {};
|
|
89
|
+
let results = this._history;
|
|
90
|
+
|
|
91
|
+
if (filter.plugin) {
|
|
92
|
+
results = results.filter(function (e) { return e.plugin === filter.plugin; });
|
|
93
|
+
}
|
|
94
|
+
if (filter.level) {
|
|
95
|
+
var levelNum = LOG_LEVELS[filter.level];
|
|
96
|
+
if (levelNum !== undefined) {
|
|
97
|
+
results = results.filter(function (e) { return LOG_LEVELS[e.level] >= levelNum; });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (filter.since) {
|
|
101
|
+
results = results.filter(function (e) { return e.timestamp >= filter.since; });
|
|
102
|
+
}
|
|
103
|
+
if (filter.until) {
|
|
104
|
+
results = results.filter(function (e) { return e.timestamp <= filter.until; });
|
|
105
|
+
}
|
|
106
|
+
if (filter.limit) {
|
|
107
|
+
results = results.slice(-filter.limit);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Clear all history entries.
|
|
115
|
+
*/
|
|
116
|
+
clear() {
|
|
117
|
+
this._history = [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a child logger bound to a specific plugin name.
|
|
122
|
+
* The child logger exposes debug/info/warn/error without needing the plugin arg.
|
|
123
|
+
* @param {string} pluginName
|
|
124
|
+
* @returns {object} child logger with debug/info/warn/error methods
|
|
125
|
+
*/
|
|
126
|
+
createChild(pluginName) {
|
|
127
|
+
var self = this;
|
|
128
|
+
return {
|
|
129
|
+
debug: function (message, data) { self.debug(pluginName, message, data); },
|
|
130
|
+
info: function (message, data) { self.info(pluginName, message, data); },
|
|
131
|
+
warn: function (message, data) { self.warn(pluginName, message, data); },
|
|
132
|
+
error: function (message, data) { self.error(pluginName, message, data); },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- internal ---
|
|
137
|
+
|
|
138
|
+
_log(level, plugin, message, data) {
|
|
139
|
+
var levelNum = LOG_LEVELS[level];
|
|
140
|
+
if (levelNum === undefined) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (levelNum < this._minLevel) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
var entry = {
|
|
148
|
+
level: level,
|
|
149
|
+
plugin: plugin || 'system',
|
|
150
|
+
message: message,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
};
|
|
153
|
+
if (data !== undefined) {
|
|
154
|
+
entry.data = data;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this._history.push(entry);
|
|
158
|
+
if (this._history.length > this._maxHistory) {
|
|
159
|
+
this._history.shift();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Also write to console for immediate visibility
|
|
163
|
+
var prefix = '[' + entry.timestamp + '] [' + level.toUpperCase() + '] [' + entry.plugin + ']';
|
|
164
|
+
if (level === 'error') {
|
|
165
|
+
console.error(prefix, message, data !== undefined ? data : '');
|
|
166
|
+
} else if (level === 'warn') {
|
|
167
|
+
console.warn(prefix, message, data !== undefined ? data : '');
|
|
168
|
+
} else {
|
|
169
|
+
console.log(prefix, message, data !== undefined ? data : '');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = Logger;
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginLoader - Plugin discovery, dependency resolution, and lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Load plugins from plugin-registry.json
|
|
6
|
+
* - Topological sort for dependency order
|
|
7
|
+
* - Circular dependency detection
|
|
8
|
+
* - Lifecycle management: load -> activate -> deactivate
|
|
9
|
+
* - Graceful handling of empty registries
|
|
10
|
+
* - Error isolation per plugin
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
var fs = require('fs');
|
|
16
|
+
var path = require('path');
|
|
17
|
+
var { PluginState } = require('../contracts/plugin-interface');
|
|
18
|
+
|
|
19
|
+
class PluginLoader {
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} options
|
|
22
|
+
* @param {string} options.registryPath - path to plugin-registry.json
|
|
23
|
+
* @param {object} options.eventBus - EventBus instance
|
|
24
|
+
* @param {object} options.stateStore - StateStore instance
|
|
25
|
+
* @param {object} options.configManager - ConfigManager instance
|
|
26
|
+
* @param {object} options.logger - Logger instance
|
|
27
|
+
*/
|
|
28
|
+
constructor(options) {
|
|
29
|
+
options = options || {};
|
|
30
|
+
this._registryPath = options.registryPath || path.join(process.cwd(), 'plugins', 'plugin-registry.json');
|
|
31
|
+
this._eventBus = options.eventBus || null;
|
|
32
|
+
this._stateStore = options.stateStore || null;
|
|
33
|
+
this._configManager = options.configManager || null;
|
|
34
|
+
this._logger = options.logger || null;
|
|
35
|
+
|
|
36
|
+
/** @type {Map<string, object>} loaded plugin instances */
|
|
37
|
+
this.loadedPlugins = new Map();
|
|
38
|
+
/** @type {Map<string, object>} plugin configs from registry */
|
|
39
|
+
this._pluginConfigs = new Map();
|
|
40
|
+
/** @type {object} registry data */
|
|
41
|
+
this._registry = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- public API ---
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load and activate all plugins from the registry.
|
|
48
|
+
* Respects dependency order via topological sort.
|
|
49
|
+
* Handles empty registries gracefully.
|
|
50
|
+
*
|
|
51
|
+
* @returns {Promise<string[]>} names of successfully loaded plugins
|
|
52
|
+
*/
|
|
53
|
+
async loadAll() {
|
|
54
|
+
this._log('info', 'Starting plugin loading...');
|
|
55
|
+
|
|
56
|
+
// 1. Read registry
|
|
57
|
+
this._registry = this._readRegistry();
|
|
58
|
+
var pluginNames = this._getPluginNames();
|
|
59
|
+
|
|
60
|
+
if (pluginNames.length === 0) {
|
|
61
|
+
this._log('info', 'Registry is empty, no plugins to load');
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Build dependency graph from registry entries
|
|
66
|
+
var depGraph = this._buildDependencyGraph(pluginNames);
|
|
67
|
+
|
|
68
|
+
// 3. Topological sort (with cycle detection)
|
|
69
|
+
var loadOrder = this._topologicalSort(depGraph);
|
|
70
|
+
|
|
71
|
+
this._log('info', 'Load order resolved: ' + loadOrder.join(', '));
|
|
72
|
+
|
|
73
|
+
// 4. Load and activate in order
|
|
74
|
+
var loaded = [];
|
|
75
|
+
for (var i = 0; i < loadOrder.length; i++) {
|
|
76
|
+
var name = loadOrder[i];
|
|
77
|
+
var config = this._pluginConfigs.get(name);
|
|
78
|
+
|
|
79
|
+
if (config && config.enabled === false) {
|
|
80
|
+
this._log('info', 'Plugin "' + name + '" is disabled, skipping');
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await this._loadPlugin(name, config);
|
|
86
|
+
loaded.push(name);
|
|
87
|
+
if (this._eventBus) {
|
|
88
|
+
this._eventBus.emit('plugin:loaded', { pluginName: name });
|
|
89
|
+
}
|
|
90
|
+
this._log('info', 'Plugin "' + name + '" loaded successfully');
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this._log('error', 'Failed to load plugin "' + name + '": ' + err.message);
|
|
93
|
+
// Non-critical plugins don't block others
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this._log('info', 'Plugin loading complete. ' + loaded.length + ' plugins loaded');
|
|
98
|
+
return loaded;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Activate a single loaded plugin.
|
|
103
|
+
* @param {string} name - plugin name
|
|
104
|
+
* @param {object} context - PluginContext to inject
|
|
105
|
+
*/
|
|
106
|
+
async activate(name, context) {
|
|
107
|
+
var plugin = this.loadedPlugins.get(name);
|
|
108
|
+
if (!plugin) {
|
|
109
|
+
throw new Error('Plugin "' + name + '" is not loaded');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof plugin.onActivate === 'function') {
|
|
113
|
+
await plugin.onActivate(context);
|
|
114
|
+
}
|
|
115
|
+
plugin.state = PluginState.ACTIVATED;
|
|
116
|
+
|
|
117
|
+
if (this._eventBus) {
|
|
118
|
+
this._eventBus.emit('plugin:activated', { pluginName: name });
|
|
119
|
+
}
|
|
120
|
+
this._log('info', 'Plugin "' + name + '" activated');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Deactivate a single plugin.
|
|
125
|
+
* @param {string} name
|
|
126
|
+
*/
|
|
127
|
+
async deactivate(name) {
|
|
128
|
+
var plugin = this.loadedPlugins.get(name);
|
|
129
|
+
if (!plugin) {
|
|
130
|
+
throw new Error('Plugin "' + name + '" is not loaded');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof plugin.onDeactivate === 'function') {
|
|
134
|
+
await plugin.onDeactivate();
|
|
135
|
+
}
|
|
136
|
+
plugin.state = PluginState.DEACTIVATED;
|
|
137
|
+
|
|
138
|
+
if (this._eventBus) {
|
|
139
|
+
this._eventBus.emit('plugin:deactivated', { pluginName: name });
|
|
140
|
+
}
|
|
141
|
+
this._log('info', 'Plugin "' + name + '" deactivated');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Deactivate all loaded plugins in reverse order.
|
|
146
|
+
*/
|
|
147
|
+
async deactivateAll() {
|
|
148
|
+
var names = Array.from(this.loadedPlugins.keys()).reverse();
|
|
149
|
+
for (var i = 0; i < names.length; i++) {
|
|
150
|
+
try {
|
|
151
|
+
await this.deactivate(names[i]);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
this._log('error', 'Error deactivating "' + names[i] + '": ' + err.message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get a loaded plugin by name.
|
|
160
|
+
* @param {string} name
|
|
161
|
+
* @returns {object|undefined}
|
|
162
|
+
*/
|
|
163
|
+
getPlugin(name) {
|
|
164
|
+
return this.loadedPlugins.get(name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if a plugin is loaded.
|
|
169
|
+
* @param {string} name
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
*/
|
|
172
|
+
isLoaded(name) {
|
|
173
|
+
return this.loadedPlugins.has(name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get names of all loaded plugins.
|
|
178
|
+
* @returns {string[]}
|
|
179
|
+
*/
|
|
180
|
+
getLoadedNames() {
|
|
181
|
+
return Array.from(this.loadedPlugins.keys());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- internal ---
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read and parse the registry file.
|
|
188
|
+
* @returns {object}
|
|
189
|
+
*/
|
|
190
|
+
_readRegistry() {
|
|
191
|
+
try {
|
|
192
|
+
var content = fs.readFileSync(this._registryPath, 'utf-8');
|
|
193
|
+
return JSON.parse(content);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// If file doesn't exist or is invalid, return empty registry
|
|
196
|
+
this._log('warn', 'Could not read registry: ' + err.message + '. Using empty registry.');
|
|
197
|
+
return { version: '1.0.0', plugins: [] };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Extract plugin names and configs from registry.
|
|
203
|
+
* Supports both array format and object format.
|
|
204
|
+
* @returns {string[]}
|
|
205
|
+
*/
|
|
206
|
+
_getPluginNames() {
|
|
207
|
+
var plugins = this._registry.plugins;
|
|
208
|
+
if (!plugins) return [];
|
|
209
|
+
|
|
210
|
+
// Array format: [{ name, source, enabled, config }]
|
|
211
|
+
if (Array.isArray(plugins)) {
|
|
212
|
+
var names = [];
|
|
213
|
+
for (var i = 0; i < plugins.length; i++) {
|
|
214
|
+
var entry = plugins[i];
|
|
215
|
+
var name = entry.name || entry;
|
|
216
|
+
this._pluginConfigs.set(name, entry);
|
|
217
|
+
names.push(name);
|
|
218
|
+
}
|
|
219
|
+
return names;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Object format: { name: { source, enabled, config } }
|
|
223
|
+
if (typeof plugins === 'object') {
|
|
224
|
+
var keys = Object.keys(plugins);
|
|
225
|
+
for (var j = 0; j < keys.length; j++) {
|
|
226
|
+
this._pluginConfigs.set(keys[j], plugins[keys[j]]);
|
|
227
|
+
}
|
|
228
|
+
return keys;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build a dependency graph from plugin configs.
|
|
236
|
+
* @param {string[]} pluginNames
|
|
237
|
+
* @returns {Map<string, string[]>} name -> dependency names
|
|
238
|
+
*/
|
|
239
|
+
_buildDependencyGraph(pluginNames) {
|
|
240
|
+
var graph = new Map();
|
|
241
|
+
for (var i = 0; i < pluginNames.length; i++) {
|
|
242
|
+
var name = pluginNames[i];
|
|
243
|
+
var config = this._pluginConfigs.get(name);
|
|
244
|
+
var deps = [];
|
|
245
|
+
|
|
246
|
+
if (config && config.dependencies) {
|
|
247
|
+
deps = config.dependencies.plugins || config.dependencies || [];
|
|
248
|
+
if (!Array.isArray(deps)) deps = [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
graph.set(name, deps);
|
|
252
|
+
}
|
|
253
|
+
return graph;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Topological sort with circular dependency detection.
|
|
258
|
+
* @param {Map<string, string[]>} graph - name -> dependencies
|
|
259
|
+
* @returns {string[]} sorted plugin names
|
|
260
|
+
* @throws {Error} on circular dependency or missing dependency
|
|
261
|
+
*/
|
|
262
|
+
_topologicalSort(graph) {
|
|
263
|
+
var WHITE = 0, GRAY = 1, BLACK = 2;
|
|
264
|
+
var color = new Map();
|
|
265
|
+
var result = [];
|
|
266
|
+
|
|
267
|
+
// Initialize all as white
|
|
268
|
+
var keys = Array.from(graph.keys());
|
|
269
|
+
for (var i = 0; i < keys.length; i++) {
|
|
270
|
+
color.set(keys[i], WHITE);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function visit(node) {
|
|
274
|
+
var c = color.get(node);
|
|
275
|
+
if (c === GRAY) {
|
|
276
|
+
throw new Error('Circular dependency detected involving plugin: ' + node);
|
|
277
|
+
}
|
|
278
|
+
if (c === BLACK) return;
|
|
279
|
+
|
|
280
|
+
color.set(node, GRAY);
|
|
281
|
+
|
|
282
|
+
var deps = graph.get(node) || [];
|
|
283
|
+
for (var j = 0; j < deps.length; j++) {
|
|
284
|
+
var dep = deps[j];
|
|
285
|
+
if (!graph.has(dep)) {
|
|
286
|
+
throw new Error('Plugin "' + node + '" depends on unknown plugin: ' + dep);
|
|
287
|
+
}
|
|
288
|
+
visit(dep);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
color.set(node, BLACK);
|
|
292
|
+
result.push(node);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (var k = 0; k < keys.length; k++) {
|
|
296
|
+
if (color.get(keys[k]) === WHITE) {
|
|
297
|
+
visit(keys[k]);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Load a single plugin.
|
|
306
|
+
* For now, records the plugin in the loaded map.
|
|
307
|
+
* Actual JS module loading will be added when plugins have executable code.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} name
|
|
310
|
+
* @param {object} config
|
|
311
|
+
*/
|
|
312
|
+
async _loadPlugin(name, config) {
|
|
313
|
+
// Store the plugin with metadata
|
|
314
|
+
// In future iterations, this will require() the actual plugin module
|
|
315
|
+
var pluginEntry = {
|
|
316
|
+
name: name,
|
|
317
|
+
config: config || {},
|
|
318
|
+
state: PluginState.LOADED,
|
|
319
|
+
// Placeholder - real plugins will have onActivate/onDeactivate
|
|
320
|
+
onActivate: function () {},
|
|
321
|
+
onDeactivate: function () {},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
this.loadedPlugins.set(name, pluginEntry);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Internal logging helper.
|
|
329
|
+
*/
|
|
330
|
+
_log(level, message) {
|
|
331
|
+
if (this._logger && typeof this._logger[level] === 'function') {
|
|
332
|
+
this._logger[level]('plugin-loader', message);
|
|
333
|
+
} else {
|
|
334
|
+
console.log('[plugin-loader] [' + level + '] ' + message);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = PluginLoader;
|