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.
- package/CHANGELOG.md +58 -0
- package/CLAUDE.md +47 -2
- package/bin/dev.js +217 -0
- package/bin/prjct +10 -0
- package/bin/serve.js +78 -0
- package/core/agentic/command-executor.js +38 -112
- package/core/agentic/prompt-builder.js +72 -0
- package/core/bus/index.js +322 -0
- package/core/command-registry.js +65 -0
- package/core/domain/snapshot-manager.js +375 -0
- package/core/plugin/hooks.js +313 -0
- package/core/plugin/index.js +52 -0
- package/core/plugin/loader.js +331 -0
- package/core/plugin/registry.js +325 -0
- package/core/plugins/webhook.js +143 -0
- package/core/session/index.js +449 -0
- package/core/session/metrics.js +293 -0
- package/package.json +18 -4
- package/templates/agentic/agent-routing.md +42 -9
- package/templates/agentic/checklist-routing.md +98 -0
- package/templates/checklists/accessibility.md +33 -0
- package/templates/checklists/architecture.md +28 -0
- package/templates/checklists/code-quality.md +28 -0
- package/templates/checklists/data.md +33 -0
- package/templates/checklists/documentation.md +33 -0
- package/templates/checklists/infrastructure.md +33 -0
- package/templates/checklists/performance.md +33 -0
- package/templates/checklists/security.md +33 -0
- package/templates/checklists/testing.md +33 -0
- package/templates/checklists/ux-ui.md +37 -0
- package/templates/commands/bug.md +27 -1
- package/templates/commands/done.md +176 -54
- package/templates/commands/feature.md +38 -1
- package/templates/commands/history.md +176 -0
- package/templates/commands/init.md +28 -1
- package/templates/commands/now.md +191 -9
- package/templates/commands/pause.md +176 -12
- package/templates/commands/redo.md +142 -0
- package/templates/commands/resume.md +166 -62
- package/templates/commands/serve.md +121 -0
- package/templates/commands/ship.md +45 -1
- package/templates/commands/sync.md +34 -1
- package/templates/commands/task.md +27 -1
- 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
|
+
}
|