prjct-cli 0.10.13 → 0.11.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/CLAUDE.md +47 -2
  3. package/bin/dev.js +217 -0
  4. package/bin/prjct +10 -0
  5. package/bin/serve.js +78 -0
  6. package/core/agentic/command-executor.js +38 -112
  7. package/core/agentic/prompt-builder.js +72 -0
  8. package/core/bus/index.js +322 -0
  9. package/core/command-registry.js +65 -0
  10. package/core/domain/snapshot-manager.js +375 -0
  11. package/core/plugin/hooks.js +313 -0
  12. package/core/plugin/index.js +52 -0
  13. package/core/plugin/loader.js +331 -0
  14. package/core/plugin/registry.js +325 -0
  15. package/core/plugins/webhook.js +143 -0
  16. package/core/session/index.js +449 -0
  17. package/core/session/metrics.js +293 -0
  18. package/package.json +18 -4
  19. package/templates/agentic/agent-routing.md +42 -9
  20. package/templates/agentic/checklist-routing.md +98 -0
  21. package/templates/checklists/accessibility.md +33 -0
  22. package/templates/checklists/architecture.md +28 -0
  23. package/templates/checklists/code-quality.md +28 -0
  24. package/templates/checklists/data.md +33 -0
  25. package/templates/checklists/documentation.md +33 -0
  26. package/templates/checklists/infrastructure.md +33 -0
  27. package/templates/checklists/performance.md +33 -0
  28. package/templates/checklists/security.md +33 -0
  29. package/templates/checklists/testing.md +33 -0
  30. package/templates/checklists/ux-ui.md +37 -0
  31. package/templates/commands/bug.md +27 -1
  32. package/templates/commands/done.md +176 -54
  33. package/templates/commands/feature.md +38 -1
  34. package/templates/commands/history.md +176 -0
  35. package/templates/commands/init.md +28 -1
  36. package/templates/commands/now.md +191 -9
  37. package/templates/commands/pause.md +176 -12
  38. package/templates/commands/redo.md +142 -0
  39. package/templates/commands/resume.md +166 -62
  40. package/templates/commands/serve.md +121 -0
  41. package/templates/commands/ship.md +45 -1
  42. package/templates/commands/sync.md +34 -1
  43. package/templates/commands/task.md +27 -1
  44. package/templates/commands/undo.md +152 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Plugin System - Main Entry Point
3
+ *
4
+ * Exports all plugin-related functionality.
5
+ *
6
+ * @version 1.0.0
7
+ */
8
+
9
+ const { HookSystem, HookPoints, hookSystem, hooks } = require('./hooks')
10
+ const { PluginLoader, pluginLoader } = require('./loader')
11
+ const { PluginRegistry, pluginRegistry } = require('./registry')
12
+
13
+ /**
14
+ * Initialize the complete plugin system
15
+ * @param {string} projectPath
16
+ * @param {Object} config
17
+ */
18
+ async function initializePlugins(projectPath, config = {}) {
19
+ await pluginRegistry.initialize()
20
+ await pluginLoader.initialize(projectPath, config)
21
+ }
22
+
23
+ /**
24
+ * Shutdown the plugin system
25
+ */
26
+ async function shutdownPlugins() {
27
+ const plugins = pluginLoader.getAllPlugins()
28
+
29
+ for (const plugin of plugins) {
30
+ await pluginLoader.unloadPlugin(plugin.name)
31
+ }
32
+ }
33
+
34
+ module.exports = {
35
+ // Hook system
36
+ HookSystem,
37
+ HookPoints,
38
+ hookSystem,
39
+ hooks,
40
+
41
+ // Plugin loader
42
+ PluginLoader,
43
+ pluginLoader,
44
+
45
+ // Plugin registry
46
+ PluginRegistry,
47
+ pluginRegistry,
48
+
49
+ // Convenience functions
50
+ initializePlugins,
51
+ shutdownPlugins
52
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * PluginLoader - Dynamic Plugin Loading for prjct-cli
3
+ *
4
+ * Loads plugins from:
5
+ * 1. Built-in plugins (core/plugins/*)
6
+ * 2. Global plugins (~/.prjct-cli/plugins/*)
7
+ * 3. Project plugins (configured in prjct.config.json)
8
+ *
9
+ * @version 1.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises
13
+ const path = require('path')
14
+ const { hookSystem } = require('./hooks')
15
+ const { eventBus } = require('../bus')
16
+ const pathManager = require('../infrastructure/path-manager')
17
+
18
+ /**
19
+ * Plugin Interface
20
+ * @typedef {Object} Plugin
21
+ * @property {string} name - Unique plugin name
22
+ * @property {string} version - Semver version
23
+ * @property {string} [description] - Plugin description
24
+ * @property {Object} [hooks] - Hook handlers
25
+ * @property {Object} [commands] - Custom commands
26
+ * @property {Function} [activate] - Called when plugin loads
27
+ * @property {Function} [deactivate] - Called when plugin unloads
28
+ */
29
+
30
+ class PluginLoader {
31
+ constructor() {
32
+ this.plugins = new Map()
33
+ this.pluginPaths = new Map()
34
+ this.initialized = false
35
+ }
36
+
37
+ /**
38
+ * Initialize plugin system
39
+ * @param {string} projectPath - Project root path
40
+ * @param {Object} config - Project config
41
+ */
42
+ async initialize(projectPath, config = {}) {
43
+ if (this.initialized) return
44
+
45
+ this.projectPath = projectPath
46
+ this.config = config
47
+
48
+ // Load plugins in order
49
+ await this.loadBuiltinPlugins()
50
+ await this.loadGlobalPlugins()
51
+ await this.loadProjectPlugins(config.plugins || [])
52
+
53
+ this.initialized = true
54
+ }
55
+
56
+ /**
57
+ * Load built-in plugins from core/plugins
58
+ */
59
+ async loadBuiltinPlugins() {
60
+ const builtinPath = path.join(__dirname, '..', 'plugins')
61
+
62
+ try {
63
+ const files = await fs.readdir(builtinPath)
64
+
65
+ for (const file of files) {
66
+ if (file.endsWith('.js')) {
67
+ const pluginPath = path.join(builtinPath, file)
68
+ await this.loadPlugin(pluginPath, 'builtin')
69
+ }
70
+ }
71
+ } catch {
72
+ // No built-in plugins directory yet
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Load global plugins from ~/.prjct-cli/plugins
78
+ */
79
+ async loadGlobalPlugins() {
80
+ const globalPath = path.join(pathManager.getGlobalStoragePath(), 'plugins')
81
+
82
+ try {
83
+ const files = await fs.readdir(globalPath)
84
+
85
+ for (const file of files) {
86
+ if (file.endsWith('.js') || (await this.isDirectory(path.join(globalPath, file)))) {
87
+ const pluginPath = file.endsWith('.js')
88
+ ? path.join(globalPath, file)
89
+ : path.join(globalPath, file, 'index.js')
90
+ await this.loadPlugin(pluginPath, 'global')
91
+ }
92
+ }
93
+ } catch {
94
+ // No global plugins directory
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Load project-specific plugins from config
100
+ * @param {Array<string|Object>} plugins - Plugin specs from config
101
+ */
102
+ async loadProjectPlugins(plugins) {
103
+ for (const spec of plugins) {
104
+ try {
105
+ if (typeof spec === 'string') {
106
+ // Simple name or path
107
+ if (spec.startsWith('file://')) {
108
+ // Local file path
109
+ const filePath = spec.replace('file://', '')
110
+ const fullPath = path.resolve(this.projectPath, filePath)
111
+ await this.loadPlugin(fullPath, 'project')
112
+ } else if (spec.startsWith('./') || spec.startsWith('../')) {
113
+ // Relative path
114
+ const fullPath = path.resolve(this.projectPath, spec)
115
+ await this.loadPlugin(fullPath, 'project')
116
+ } else {
117
+ // Plugin name - check if already loaded
118
+ if (!this.plugins.has(spec)) {
119
+ console.warn(`Plugin not found: ${spec}`)
120
+ }
121
+ }
122
+ } else if (typeof spec === 'object' && spec.path) {
123
+ // Object with path and config
124
+ const fullPath = path.resolve(this.projectPath, spec.path)
125
+ await this.loadPlugin(fullPath, 'project', spec.config)
126
+ }
127
+ } catch (error) {
128
+ console.error(`Failed to load plugin: ${spec}`, error.message)
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Load a single plugin
135
+ * @param {string} pluginPath - Path to plugin file
136
+ * @param {string} source - 'builtin', 'global', or 'project'
137
+ * @param {Object} config - Plugin-specific config
138
+ */
139
+ async loadPlugin(pluginPath, source, config = {}) {
140
+ try {
141
+ // Check if file exists
142
+ await fs.access(pluginPath)
143
+
144
+ // Require the plugin
145
+ const plugin = require(pluginPath)
146
+
147
+ // Validate plugin structure
148
+ if (!plugin.name) {
149
+ throw new Error('Plugin must have a name property')
150
+ }
151
+
152
+ if (this.plugins.has(plugin.name)) {
153
+ console.warn(`Plugin already loaded: ${plugin.name}`)
154
+ return
155
+ }
156
+
157
+ // Store plugin
158
+ this.plugins.set(plugin.name, {
159
+ ...plugin,
160
+ source,
161
+ config: { ...config, ...this.config[plugin.name] }
162
+ })
163
+ this.pluginPaths.set(plugin.name, pluginPath)
164
+
165
+ // Register hooks
166
+ if (plugin.hooks) {
167
+ this.registerPluginHooks(plugin)
168
+ }
169
+
170
+ // Register event listeners
171
+ if (plugin.events) {
172
+ this.registerPluginEvents(plugin)
173
+ }
174
+
175
+ // Call activate if exists
176
+ if (typeof plugin.activate === 'function') {
177
+ await plugin.activate({
178
+ config: this.plugins.get(plugin.name).config,
179
+ eventBus,
180
+ hookSystem,
181
+ projectPath: this.projectPath
182
+ })
183
+ }
184
+
185
+ } catch (error) {
186
+ if (error.code === 'ENOENT') {
187
+ // File not found - skip silently
188
+ } else if (error.code === 'MODULE_NOT_FOUND') {
189
+ console.error(`Plugin module not found: ${pluginPath}`)
190
+ } else {
191
+ console.error(`Failed to load plugin from ${pluginPath}:`, error.message)
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Register plugin hooks
198
+ * @param {Plugin} plugin
199
+ */
200
+ registerPluginHooks(plugin) {
201
+ for (const [hookPoint, handler] of Object.entries(plugin.hooks)) {
202
+ hookSystem.register(hookPoint, handler, {
203
+ pluginName: plugin.name,
204
+ priority: plugin.priority || 10
205
+ })
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Register plugin event listeners
211
+ * @param {Plugin} plugin
212
+ */
213
+ registerPluginEvents(plugin) {
214
+ for (const [eventType, handler] of Object.entries(plugin.events)) {
215
+ eventBus.on(eventType, handler)
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Unload a plugin
221
+ * @param {string} name - Plugin name
222
+ */
223
+ async unloadPlugin(name) {
224
+ const plugin = this.plugins.get(name)
225
+ if (!plugin) return
226
+
227
+ // Call deactivate if exists
228
+ if (typeof plugin.deactivate === 'function') {
229
+ await plugin.deactivate()
230
+ }
231
+
232
+ // Unregister hooks
233
+ hookSystem.unregisterPlugin(name)
234
+
235
+ // Remove from loaded plugins
236
+ this.plugins.delete(name)
237
+ this.pluginPaths.delete(name)
238
+
239
+ // Clear require cache
240
+ const pluginPath = this.pluginPaths.get(name)
241
+ if (pluginPath) {
242
+ delete require.cache[require.resolve(pluginPath)]
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Reload a plugin
248
+ * @param {string} name - Plugin name
249
+ */
250
+ async reloadPlugin(name) {
251
+ const pluginPath = this.pluginPaths.get(name)
252
+ const plugin = this.plugins.get(name)
253
+
254
+ if (!pluginPath || !plugin) {
255
+ throw new Error(`Plugin not found: ${name}`)
256
+ }
257
+
258
+ await this.unloadPlugin(name)
259
+ await this.loadPlugin(pluginPath, plugin.source, plugin.config)
260
+ }
261
+
262
+ /**
263
+ * Get a loaded plugin
264
+ * @param {string} name
265
+ * @returns {Plugin|null}
266
+ */
267
+ getPlugin(name) {
268
+ return this.plugins.get(name) || null
269
+ }
270
+
271
+ /**
272
+ * Get all loaded plugins
273
+ * @returns {Plugin[]}
274
+ */
275
+ getAllPlugins() {
276
+ return Array.from(this.plugins.values())
277
+ }
278
+
279
+ /**
280
+ * Get plugins by source
281
+ * @param {string} source - 'builtin', 'global', or 'project'
282
+ * @returns {Plugin[]}
283
+ */
284
+ getPluginsBySource(source) {
285
+ return this.getAllPlugins().filter(p => p.source === source)
286
+ }
287
+
288
+ /**
289
+ * Get custom commands from all plugins
290
+ * @returns {Object} Map of command name to handler
291
+ */
292
+ getPluginCommands() {
293
+ const commands = {}
294
+
295
+ for (const plugin of this.plugins.values()) {
296
+ if (plugin.commands) {
297
+ for (const [name, handler] of Object.entries(plugin.commands)) {
298
+ commands[name] = {
299
+ handler,
300
+ plugin: plugin.name,
301
+ description: handler.description || `Command from ${plugin.name}`
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ return commands
308
+ }
309
+
310
+ /**
311
+ * Check if path is a directory
312
+ * @param {string} p
313
+ * @returns {Promise<boolean>}
314
+ */
315
+ async isDirectory(p) {
316
+ try {
317
+ const stat = await fs.stat(p)
318
+ return stat.isDirectory()
319
+ } catch {
320
+ return false
321
+ }
322
+ }
323
+ }
324
+
325
+ // Singleton instance
326
+ const pluginLoader = new PluginLoader()
327
+
328
+ module.exports = {
329
+ PluginLoader,
330
+ pluginLoader
331
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * PluginRegistry - Plugin Discovery and Management
3
+ *
4
+ * Central registry for plugin information, discovery, and management.
5
+ *
6
+ * @version 1.0.0
7
+ */
8
+
9
+ const fs = require('fs').promises
10
+ const path = require('path')
11
+ const pathManager = require('../infrastructure/path-manager')
12
+ const { pluginLoader } = require('./loader')
13
+
14
+ class PluginRegistry {
15
+ constructor() {
16
+ this.availablePlugins = new Map()
17
+ this.initialized = false
18
+ }
19
+
20
+ /**
21
+ * Initialize registry and discover available plugins
22
+ */
23
+ async initialize() {
24
+ if (this.initialized) return
25
+
26
+ await this.discoverPlugins()
27
+ this.initialized = true
28
+ }
29
+
30
+ /**
31
+ * Discover all available plugins (not necessarily loaded)
32
+ */
33
+ async discoverPlugins() {
34
+ // Built-in plugins
35
+ await this.discoverFromPath(
36
+ path.join(__dirname, '..', 'plugins'),
37
+ 'builtin'
38
+ )
39
+
40
+ // Global plugins
41
+ await this.discoverFromPath(
42
+ path.join(pathManager.getGlobalStoragePath(), 'plugins'),
43
+ 'global'
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Discover plugins from a path
49
+ * @param {string} dir
50
+ * @param {string} source
51
+ */
52
+ async discoverFromPath(dir, source) {
53
+ try {
54
+ const entries = await fs.readdir(dir, { withFileTypes: true })
55
+
56
+ for (const entry of entries) {
57
+ try {
58
+ let pluginPath
59
+
60
+ if (entry.isFile() && entry.name.endsWith('.js')) {
61
+ pluginPath = path.join(dir, entry.name)
62
+ } else if (entry.isDirectory()) {
63
+ pluginPath = path.join(dir, entry.name, 'index.js')
64
+ } else {
65
+ continue
66
+ }
67
+
68
+ // Check if file exists
69
+ await fs.access(pluginPath)
70
+
71
+ // Read plugin metadata without loading
72
+ const metadata = await this.readPluginMetadata(pluginPath)
73
+ if (metadata) {
74
+ this.availablePlugins.set(metadata.name, {
75
+ ...metadata,
76
+ path: pluginPath,
77
+ source
78
+ })
79
+ }
80
+ } catch {
81
+ // Skip invalid plugins
82
+ }
83
+ }
84
+ } catch {
85
+ // Directory doesn't exist
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Read plugin metadata without fully loading
91
+ * @param {string} pluginPath
92
+ * @returns {Object|null}
93
+ */
94
+ async readPluginMetadata(pluginPath) {
95
+ try {
96
+ const content = await fs.readFile(pluginPath, 'utf-8')
97
+
98
+ // Extract basic metadata from comments or exports
99
+ const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/)
100
+ const versionMatch = content.match(/version:\s*['"]([^'"]+)['"]/)
101
+ const descMatch = content.match(/description:\s*['"]([^'"]+)['"]/)
102
+
103
+ if (nameMatch) {
104
+ return {
105
+ name: nameMatch[1],
106
+ version: versionMatch ? versionMatch[1] : '1.0.0',
107
+ description: descMatch ? descMatch[1] : ''
108
+ }
109
+ }
110
+
111
+ // Fallback: try to require (might have side effects)
112
+ const plugin = require(pluginPath)
113
+ delete require.cache[require.resolve(pluginPath)]
114
+
115
+ if (plugin.name) {
116
+ return {
117
+ name: plugin.name,
118
+ version: plugin.version || '1.0.0',
119
+ description: plugin.description || ''
120
+ }
121
+ }
122
+ } catch {
123
+ // Can't read metadata
124
+ }
125
+
126
+ return null
127
+ }
128
+
129
+ /**
130
+ * Get all available plugins
131
+ * @returns {Object[]}
132
+ */
133
+ getAvailable() {
134
+ return Array.from(this.availablePlugins.values())
135
+ }
136
+
137
+ /**
138
+ * Get available plugins by source
139
+ * @param {string} source
140
+ * @returns {Object[]}
141
+ */
142
+ getAvailableBySource(source) {
143
+ return this.getAvailable().filter(p => p.source === source)
144
+ }
145
+
146
+ /**
147
+ * Check if a plugin is available
148
+ * @param {string} name
149
+ * @returns {boolean}
150
+ */
151
+ isAvailable(name) {
152
+ return this.availablePlugins.has(name)
153
+ }
154
+
155
+ /**
156
+ * Check if a plugin is loaded
157
+ * @param {string} name
158
+ * @returns {boolean}
159
+ */
160
+ isLoaded(name) {
161
+ return pluginLoader.getPlugin(name) !== null
162
+ }
163
+
164
+ /**
165
+ * Get plugin info
166
+ * @param {string} name
167
+ * @returns {Object|null}
168
+ */
169
+ getPluginInfo(name) {
170
+ const available = this.availablePlugins.get(name)
171
+ const loaded = pluginLoader.getPlugin(name)
172
+
173
+ if (!available && !loaded) return null
174
+
175
+ return {
176
+ ...(available || {}),
177
+ loaded: !!loaded,
178
+ active: loaded ? true : false
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Install a plugin (copy to global plugins)
184
+ * @param {string} sourcePath - Path to plugin file/directory
185
+ * @param {string} [name] - Optional name override
186
+ */
187
+ async install(sourcePath, name = null) {
188
+ const globalPluginsPath = path.join(pathManager.getGlobalStoragePath(), 'plugins')
189
+ await fs.mkdir(globalPluginsPath, { recursive: true })
190
+
191
+ const stat = await fs.stat(sourcePath)
192
+
193
+ if (stat.isFile()) {
194
+ // Single file plugin
195
+ const pluginName = name || path.basename(sourcePath)
196
+ const destPath = path.join(globalPluginsPath, pluginName)
197
+ await fs.copyFile(sourcePath, destPath)
198
+ } else if (stat.isDirectory()) {
199
+ // Directory plugin
200
+ const pluginName = name || path.basename(sourcePath)
201
+ const destPath = path.join(globalPluginsPath, pluginName)
202
+ await this.copyDirectory(sourcePath, destPath)
203
+ }
204
+
205
+ // Refresh available plugins
206
+ await this.discoverPlugins()
207
+ }
208
+
209
+ /**
210
+ * Uninstall a plugin
211
+ * @param {string} name
212
+ */
213
+ async uninstall(name) {
214
+ const plugin = this.availablePlugins.get(name)
215
+
216
+ if (!plugin) {
217
+ throw new Error(`Plugin not found: ${name}`)
218
+ }
219
+
220
+ if (plugin.source === 'builtin') {
221
+ throw new Error('Cannot uninstall built-in plugins')
222
+ }
223
+
224
+ // Unload if loaded
225
+ if (this.isLoaded(name)) {
226
+ await pluginLoader.unloadPlugin(name)
227
+ }
228
+
229
+ // Delete plugin file/directory
230
+ const pluginPath = plugin.path
231
+ const stat = await fs.stat(path.dirname(pluginPath))
232
+
233
+ if (path.basename(pluginPath) === 'index.js') {
234
+ // Directory plugin
235
+ await fs.rm(path.dirname(pluginPath), { recursive: true })
236
+ } else {
237
+ // Single file plugin
238
+ await fs.unlink(pluginPath)
239
+ }
240
+
241
+ // Remove from registry
242
+ this.availablePlugins.delete(name)
243
+ }
244
+
245
+ /**
246
+ * Copy directory recursively
247
+ * @param {string} src
248
+ * @param {string} dest
249
+ */
250
+ async copyDirectory(src, dest) {
251
+ await fs.mkdir(dest, { recursive: true })
252
+ const entries = await fs.readdir(src, { withFileTypes: true })
253
+
254
+ for (const entry of entries) {
255
+ const srcPath = path.join(src, entry.name)
256
+ const destPath = path.join(dest, entry.name)
257
+
258
+ if (entry.isDirectory()) {
259
+ await this.copyDirectory(srcPath, destPath)
260
+ } else {
261
+ await fs.copyFile(srcPath, destPath)
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Generate plugin list for display
268
+ * @returns {string}
269
+ */
270
+ generatePluginList() {
271
+ const available = this.getAvailable()
272
+
273
+ if (available.length === 0) {
274
+ return 'No plugins installed.'
275
+ }
276
+
277
+ let output = '## Installed Plugins\n\n'
278
+
279
+ const bySource = {
280
+ builtin: [],
281
+ global: [],
282
+ project: []
283
+ }
284
+
285
+ for (const plugin of available) {
286
+ bySource[plugin.source].push(plugin)
287
+ }
288
+
289
+ if (bySource.builtin.length > 0) {
290
+ output += '### Built-in\n'
291
+ for (const p of bySource.builtin) {
292
+ const status = this.isLoaded(p.name) ? '●' : '○'
293
+ output += `- ${status} **${p.name}** v${p.version}\n`
294
+ if (p.description) {
295
+ output += ` ${p.description}\n`
296
+ }
297
+ }
298
+ output += '\n'
299
+ }
300
+
301
+ if (bySource.global.length > 0) {
302
+ output += '### Global\n'
303
+ for (const p of bySource.global) {
304
+ const status = this.isLoaded(p.name) ? '●' : '○'
305
+ output += `- ${status} **${p.name}** v${p.version}\n`
306
+ if (p.description) {
307
+ output += ` ${p.description}\n`
308
+ }
309
+ }
310
+ output += '\n'
311
+ }
312
+
313
+ output += '\n● = loaded, ○ = available'
314
+
315
+ return output
316
+ }
317
+ }
318
+
319
+ // Singleton instance
320
+ const pluginRegistry = new PluginRegistry()
321
+
322
+ module.exports = {
323
+ PluginRegistry,
324
+ pluginRegistry
325
+ }