prjct-cli 0.10.14 → 0.11.1

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 (105) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/dev.js +217 -0
  3. package/bin/prjct +10 -0
  4. package/bin/serve.js +78 -0
  5. package/core/bus/index.js +322 -0
  6. package/core/command-registry.js +65 -0
  7. package/core/domain/snapshot-manager.js +375 -0
  8. package/core/plugin/hooks.js +313 -0
  9. package/core/plugin/index.js +52 -0
  10. package/core/plugin/loader.js +331 -0
  11. package/core/plugin/registry.js +325 -0
  12. package/core/plugins/webhook.js +143 -0
  13. package/core/session/index.js +449 -0
  14. package/core/session/metrics.js +293 -0
  15. package/package.json +28 -4
  16. package/packages/shared/dist/index.d.ts +615 -0
  17. package/packages/shared/dist/index.js +204 -0
  18. package/packages/shared/package.json +29 -0
  19. package/packages/shared/src/index.ts +9 -0
  20. package/packages/shared/src/schemas.ts +124 -0
  21. package/packages/shared/src/types.ts +187 -0
  22. package/packages/shared/src/utils.ts +148 -0
  23. package/packages/shared/tsconfig.json +18 -0
  24. package/packages/web/README.md +36 -0
  25. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  26. package/packages/web/app/api/claude/status/route.ts +34 -0
  27. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  29. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  30. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  31. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  32. package/packages/web/app/api/projects/route.ts +16 -0
  33. package/packages/web/app/api/sessions/history/route.ts +122 -0
  34. package/packages/web/app/api/stats/route.ts +38 -0
  35. package/packages/web/app/error.tsx +34 -0
  36. package/packages/web/app/favicon.ico +0 -0
  37. package/packages/web/app/globals.css +155 -0
  38. package/packages/web/app/layout.tsx +43 -0
  39. package/packages/web/app/loading.tsx +7 -0
  40. package/packages/web/app/not-found.tsx +25 -0
  41. package/packages/web/app/page.tsx +227 -0
  42. package/packages/web/app/project/[id]/error.tsx +41 -0
  43. package/packages/web/app/project/[id]/loading.tsx +9 -0
  44. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  45. package/packages/web/app/project/[id]/page.tsx +253 -0
  46. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  47. package/packages/web/app/sessions/page.tsx +165 -0
  48. package/packages/web/app/settings/page.tsx +150 -0
  49. package/packages/web/components/AppSidebar.tsx +113 -0
  50. package/packages/web/components/CommandButton.tsx +39 -0
  51. package/packages/web/components/ConnectionStatus.tsx +29 -0
  52. package/packages/web/components/Logo.tsx +65 -0
  53. package/packages/web/components/MarkdownContent.tsx +123 -0
  54. package/packages/web/components/ProjectAvatar.tsx +54 -0
  55. package/packages/web/components/TechStackBadges.tsx +20 -0
  56. package/packages/web/components/TerminalTab.tsx +84 -0
  57. package/packages/web/components/TerminalTabs.tsx +210 -0
  58. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  59. package/packages/web/components/providers.tsx +45 -0
  60. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  61. package/packages/web/components/ui/badge.tsx +46 -0
  62. package/packages/web/components/ui/button.tsx +60 -0
  63. package/packages/web/components/ui/card.tsx +92 -0
  64. package/packages/web/components/ui/chart.tsx +385 -0
  65. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  66. package/packages/web/components/ui/scroll-area.tsx +58 -0
  67. package/packages/web/components/ui/sheet.tsx +139 -0
  68. package/packages/web/components/ui/tabs.tsx +66 -0
  69. package/packages/web/components/ui/tooltip.tsx +61 -0
  70. package/packages/web/components.json +22 -0
  71. package/packages/web/context/TerminalContext.tsx +45 -0
  72. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  73. package/packages/web/eslint.config.mjs +18 -0
  74. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  75. package/packages/web/hooks/useProjectStats.ts +38 -0
  76. package/packages/web/hooks/useProjects.ts +73 -0
  77. package/packages/web/hooks/useStats.ts +28 -0
  78. package/packages/web/lib/format.ts +23 -0
  79. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  80. package/packages/web/lib/projects.ts +452 -0
  81. package/packages/web/lib/pty.ts +101 -0
  82. package/packages/web/lib/query-config.ts +44 -0
  83. package/packages/web/lib/utils.ts +6 -0
  84. package/packages/web/next-env.d.ts +6 -0
  85. package/packages/web/next.config.ts +7 -0
  86. package/packages/web/package.json +53 -0
  87. package/packages/web/postcss.config.mjs +7 -0
  88. package/packages/web/public/file.svg +1 -0
  89. package/packages/web/public/globe.svg +1 -0
  90. package/packages/web/public/next.svg +1 -0
  91. package/packages/web/public/vercel.svg +1 -0
  92. package/packages/web/public/window.svg +1 -0
  93. package/packages/web/server.ts +262 -0
  94. package/packages/web/tsconfig.json +34 -0
  95. package/templates/commands/done.md +176 -54
  96. package/templates/commands/history.md +176 -0
  97. package/templates/commands/init.md +28 -1
  98. package/templates/commands/now.md +191 -9
  99. package/templates/commands/pause.md +176 -12
  100. package/templates/commands/redo.md +142 -0
  101. package/templates/commands/resume.md +166 -62
  102. package/templates/commands/serve.md +121 -0
  103. package/templates/commands/ship.md +45 -1
  104. package/templates/commands/sync.md +34 -1
  105. package/templates/commands/undo.md +152 -0
@@ -0,0 +1,313 @@
1
+ /**
2
+ * HookSystem - Plugin Lifecycle Hooks for prjct-cli
3
+ *
4
+ * Provides hook points for plugins to extend prjct functionality.
5
+ * Hooks can modify data, add side effects, or integrate with external services.
6
+ *
7
+ * @version 1.0.0
8
+ */
9
+
10
+ const { eventBus, EventTypes } = require('../bus')
11
+
12
+ /**
13
+ * Hook Points - Where plugins can intercept
14
+ */
15
+ const HookPoints = {
16
+ // Before hooks (can modify data)
17
+ BEFORE_SESSION_START: 'before:session.start',
18
+ BEFORE_SESSION_COMPLETE: 'before:session.complete',
19
+ BEFORE_TASK_CREATE: 'before:task.create',
20
+ BEFORE_FEATURE_SHIP: 'before:feature.ship',
21
+ BEFORE_SNAPSHOT_CREATE: 'before:snapshot.create',
22
+ BEFORE_COMMIT: 'before:git.commit',
23
+
24
+ // After hooks (for side effects)
25
+ AFTER_SESSION_START: 'after:session.start',
26
+ AFTER_SESSION_COMPLETE: 'after:session.complete',
27
+ AFTER_TASK_CREATE: 'after:task.create',
28
+ AFTER_TASK_COMPLETE: 'after:task.complete',
29
+ AFTER_FEATURE_SHIP: 'after:feature.ship',
30
+ AFTER_IDEA_CAPTURE: 'after:idea.capture',
31
+ AFTER_SNAPSHOT_CREATE: 'after:snapshot.create',
32
+ AFTER_SNAPSHOT_RESTORE: 'after:snapshot.restore',
33
+ AFTER_COMMIT: 'after:git.commit',
34
+ AFTER_PUSH: 'after:git.push',
35
+ AFTER_SYNC: 'after:project.sync',
36
+
37
+ // Transform hooks (must return modified data)
38
+ TRANSFORM_COMMIT_MESSAGE: 'transform:commit.message',
39
+ TRANSFORM_TASK_DATA: 'transform:task.data',
40
+ TRANSFORM_METRICS: 'transform:metrics'
41
+ }
42
+
43
+ class HookSystem {
44
+ constructor() {
45
+ this.hooks = new Map()
46
+ this.pluginHooks = new Map() // Track hooks by plugin
47
+ }
48
+
49
+ /**
50
+ * Register a hook handler
51
+ * @param {string} hookPoint - Hook point from HookPoints
52
+ * @param {Function} handler - Handler function
53
+ * @param {Object} options
54
+ * @param {string} options.pluginName - Name of the plugin
55
+ * @param {number} options.priority - Execution order (lower = first)
56
+ * @returns {Function} Unregister function
57
+ */
58
+ register(hookPoint, handler, options = {}) {
59
+ const { pluginName = 'anonymous', priority = 10 } = options
60
+
61
+ if (!this.hooks.has(hookPoint)) {
62
+ this.hooks.set(hookPoint, [])
63
+ }
64
+
65
+ const hookEntry = {
66
+ handler,
67
+ pluginName,
68
+ priority,
69
+ id: `${pluginName}:${Date.now()}`
70
+ }
71
+
72
+ this.hooks.get(hookPoint).push(hookEntry)
73
+
74
+ // Sort by priority
75
+ this.hooks.get(hookPoint).sort((a, b) => a.priority - b.priority)
76
+
77
+ // Track by plugin
78
+ if (!this.pluginHooks.has(pluginName)) {
79
+ this.pluginHooks.set(pluginName, [])
80
+ }
81
+ this.pluginHooks.get(pluginName).push({ hookPoint, id: hookEntry.id })
82
+
83
+ // Return unregister function
84
+ return () => this.unregister(hookPoint, hookEntry.id)
85
+ }
86
+
87
+ /**
88
+ * Unregister a hook handler
89
+ * @param {string} hookPoint
90
+ * @param {string} id
91
+ */
92
+ unregister(hookPoint, id) {
93
+ const hooks = this.hooks.get(hookPoint)
94
+ if (hooks) {
95
+ const index = hooks.findIndex(h => h.id === id)
96
+ if (index !== -1) {
97
+ hooks.splice(index, 1)
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Unregister all hooks from a plugin
104
+ * @param {string} pluginName
105
+ */
106
+ unregisterPlugin(pluginName) {
107
+ const pluginEntries = this.pluginHooks.get(pluginName)
108
+ if (pluginEntries) {
109
+ for (const entry of pluginEntries) {
110
+ this.unregister(entry.hookPoint, entry.id)
111
+ }
112
+ this.pluginHooks.delete(pluginName)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Execute a "before" hook (can modify data)
118
+ * @param {string} hookPoint
119
+ * @param {Object} data - Data that can be modified
120
+ * @returns {Promise<Object>} Modified data
121
+ */
122
+ async executeBefore(hookPoint, data) {
123
+ const hooks = this.hooks.get(hookPoint) || []
124
+ let result = { ...data }
125
+
126
+ for (const hook of hooks) {
127
+ try {
128
+ const modified = await hook.handler(result)
129
+ if (modified !== undefined) {
130
+ result = { ...result, ...modified }
131
+ }
132
+ } catch (error) {
133
+ console.error(`Hook error [${hook.pluginName}] at ${hookPoint}:`, error.message)
134
+ // Continue with other hooks
135
+ }
136
+ }
137
+
138
+ return result
139
+ }
140
+
141
+ /**
142
+ * Execute an "after" hook (side effects only)
143
+ * @param {string} hookPoint
144
+ * @param {Object} data - Read-only data
145
+ * @returns {Promise<void>}
146
+ */
147
+ async executeAfter(hookPoint, data) {
148
+ const hooks = this.hooks.get(hookPoint) || []
149
+
150
+ // Execute all hooks in parallel for after hooks
151
+ await Promise.allSettled(
152
+ hooks.map(async hook => {
153
+ try {
154
+ await hook.handler(data)
155
+ } catch (error) {
156
+ console.error(`Hook error [${hook.pluginName}] at ${hookPoint}:`, error.message)
157
+ }
158
+ })
159
+ )
160
+
161
+ // Emit corresponding event for plugins listening via EventBus
162
+ const eventType = this.hookToEvent(hookPoint)
163
+ if (eventType) {
164
+ await eventBus.emit(eventType, data)
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Execute a "transform" hook (must return modified value)
170
+ * @param {string} hookPoint
171
+ * @param {*} value - Value to transform
172
+ * @param {Object} context - Additional context
173
+ * @returns {Promise<*>} Transformed value
174
+ */
175
+ async executeTransform(hookPoint, value, context = {}) {
176
+ const hooks = this.hooks.get(hookPoint) || []
177
+ let result = value
178
+
179
+ for (const hook of hooks) {
180
+ try {
181
+ const transformed = await hook.handler(result, context)
182
+ if (transformed !== undefined) {
183
+ result = transformed
184
+ }
185
+ } catch (error) {
186
+ console.error(`Transform hook error [${hook.pluginName}] at ${hookPoint}:`, error.message)
187
+ // Keep previous value on error
188
+ }
189
+ }
190
+
191
+ return result
192
+ }
193
+
194
+ /**
195
+ * Map hook point to event type
196
+ * @param {string} hookPoint
197
+ * @returns {string|null}
198
+ */
199
+ hookToEvent(hookPoint) {
200
+ const mapping = {
201
+ [HookPoints.AFTER_SESSION_START]: EventTypes.SESSION_STARTED,
202
+ [HookPoints.AFTER_SESSION_COMPLETE]: EventTypes.SESSION_COMPLETED,
203
+ [HookPoints.AFTER_TASK_CREATE]: EventTypes.TASK_CREATED,
204
+ [HookPoints.AFTER_TASK_COMPLETE]: EventTypes.TASK_COMPLETED,
205
+ [HookPoints.AFTER_FEATURE_SHIP]: EventTypes.FEATURE_SHIPPED,
206
+ [HookPoints.AFTER_IDEA_CAPTURE]: EventTypes.IDEA_CAPTURED,
207
+ [HookPoints.AFTER_SNAPSHOT_CREATE]: EventTypes.SNAPSHOT_CREATED,
208
+ [HookPoints.AFTER_SNAPSHOT_RESTORE]: EventTypes.SNAPSHOT_RESTORED,
209
+ [HookPoints.AFTER_COMMIT]: EventTypes.COMMIT_CREATED,
210
+ [HookPoints.AFTER_PUSH]: EventTypes.PUSH_COMPLETED,
211
+ [HookPoints.AFTER_SYNC]: EventTypes.PROJECT_SYNCED
212
+ }
213
+ return mapping[hookPoint] || null
214
+ }
215
+
216
+ /**
217
+ * Get all registered hooks for a point
218
+ * @param {string} hookPoint
219
+ * @returns {Object[]}
220
+ */
221
+ getHooks(hookPoint) {
222
+ return (this.hooks.get(hookPoint) || []).map(h => ({
223
+ pluginName: h.pluginName,
224
+ priority: h.priority
225
+ }))
226
+ }
227
+
228
+ /**
229
+ * Get all hooks registered by a plugin
230
+ * @param {string} pluginName
231
+ * @returns {string[]} Hook points
232
+ */
233
+ getPluginHooks(pluginName) {
234
+ const entries = this.pluginHooks.get(pluginName) || []
235
+ return entries.map(e => e.hookPoint)
236
+ }
237
+
238
+ /**
239
+ * Check if a hook point has any handlers
240
+ * @param {string} hookPoint
241
+ * @returns {boolean}
242
+ */
243
+ hasHooks(hookPoint) {
244
+ const hooks = this.hooks.get(hookPoint)
245
+ return hooks && hooks.length > 0
246
+ }
247
+
248
+ /**
249
+ * Clear all hooks
250
+ */
251
+ clear() {
252
+ this.hooks.clear()
253
+ this.pluginHooks.clear()
254
+ }
255
+ }
256
+
257
+ // Singleton instance
258
+ const hookSystem = new HookSystem()
259
+
260
+ // Convenience wrapper for common hook patterns
261
+ const hooks = {
262
+ /**
263
+ * Register a before hook
264
+ */
265
+ before: (point, handler, options) => {
266
+ const hookPoint = `before:${point}`
267
+ return hookSystem.register(hookPoint, handler, options)
268
+ },
269
+
270
+ /**
271
+ * Register an after hook
272
+ */
273
+ after: (point, handler, options) => {
274
+ const hookPoint = `after:${point}`
275
+ return hookSystem.register(hookPoint, handler, options)
276
+ },
277
+
278
+ /**
279
+ * Register a transform hook
280
+ */
281
+ transform: (point, handler, options) => {
282
+ const hookPoint = `transform:${point}`
283
+ return hookSystem.register(hookPoint, handler, options)
284
+ },
285
+
286
+ /**
287
+ * Execute before hooks
288
+ */
289
+ runBefore: (point, data) => {
290
+ return hookSystem.executeBefore(`before:${point}`, data)
291
+ },
292
+
293
+ /**
294
+ * Execute after hooks
295
+ */
296
+ runAfter: (point, data) => {
297
+ return hookSystem.executeAfter(`after:${point}`, data)
298
+ },
299
+
300
+ /**
301
+ * Execute transform hooks
302
+ */
303
+ runTransform: (point, value, context) => {
304
+ return hookSystem.executeTransform(`transform:${point}`, value, context)
305
+ }
306
+ }
307
+
308
+ module.exports = {
309
+ HookSystem,
310
+ HookPoints,
311
+ hookSystem,
312
+ hooks
313
+ }
@@ -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
+ }