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,375 @@
1
+ /**
2
+ * SnapshotManager - Git-based Undo/Redo System
3
+ *
4
+ * Uses Git internally to track file changes and enable undo/redo.
5
+ * Inspired by OpenCode's snapshot system.
6
+ *
7
+ * Storage: ~/.prjct-cli/projects/{projectId}/snapshots/
8
+ *
9
+ * @version 1.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises
13
+ const path = require('path')
14
+ const { exec } = require('child_process')
15
+ const { promisify } = require('util')
16
+ const pathManager = require('../infrastructure/path-manager')
17
+ const configManager = require('../infrastructure/config-manager')
18
+ const { emit } = require('../bus')
19
+
20
+ const execAsync = promisify(exec)
21
+
22
+ class SnapshotManager {
23
+ constructor(projectPath) {
24
+ this.projectPath = projectPath
25
+ this.projectId = null
26
+ this.snapshotDir = null
27
+ this.initialized = false
28
+ }
29
+
30
+ /**
31
+ * Initialize snapshot system for project
32
+ */
33
+ async initialize() {
34
+ this.projectId = await configManager.getProjectId(this.projectPath)
35
+ if (!this.projectId) {
36
+ throw new Error('No prjct project found. Run /p:init first.')
37
+ }
38
+
39
+ // Snapshots live in global storage
40
+ const globalPath = pathManager.getGlobalProjectPath(this.projectId)
41
+ this.snapshotDir = path.join(globalPath, 'snapshots')
42
+
43
+ // Ensure directory exists
44
+ await fs.mkdir(this.snapshotDir, { recursive: true })
45
+
46
+ // Initialize bare git repo if not exists
47
+ const gitDir = path.join(this.snapshotDir, '.git')
48
+ try {
49
+ await fs.access(gitDir)
50
+ } catch {
51
+ await this.initGitRepo()
52
+ }
53
+
54
+ this.initialized = true
55
+ }
56
+
57
+ /**
58
+ * Initialize internal Git repository
59
+ */
60
+ async initGitRepo() {
61
+ const gitDir = path.join(this.snapshotDir, '.git')
62
+
63
+ // Create bare-ish repo structure
64
+ await execAsync(`git init "${this.snapshotDir}"`, { cwd: this.projectPath })
65
+
66
+ // Configure for snapshot use
67
+ await execAsync(`git config user.email "prjct@local"`, { cwd: this.snapshotDir })
68
+ await execAsync(`git config user.name "prjct-snapshots"`, { cwd: this.snapshotDir })
69
+
70
+ // Create initial empty commit
71
+ await execAsync(`git commit --allow-empty -m "init: snapshot system"`, { cwd: this.snapshotDir })
72
+ }
73
+
74
+ /**
75
+ * Create a snapshot of current project state
76
+ *
77
+ * @param {string} message - Snapshot description
78
+ * @param {string[]} files - Specific files to track (optional, defaults to all changed)
79
+ * @returns {Promise<Object>} Snapshot info {hash, message, timestamp, files}
80
+ */
81
+ async create(message, files = null) {
82
+ if (!this.initialized) await this.initialize()
83
+
84
+ const timestamp = new Date().toISOString()
85
+
86
+ // Copy changed files to snapshot directory
87
+ const changedFiles = files || await this.getChangedFiles()
88
+
89
+ if (changedFiles.length === 0) {
90
+ return {
91
+ hash: null,
92
+ message: 'No changes to snapshot',
93
+ timestamp,
94
+ files: []
95
+ }
96
+ }
97
+
98
+ // Copy files to snapshot dir maintaining structure
99
+ for (const file of changedFiles) {
100
+ const srcPath = path.join(this.projectPath, file)
101
+ const destPath = path.join(this.snapshotDir, file)
102
+
103
+ try {
104
+ const content = await fs.readFile(srcPath, 'utf-8')
105
+ await fs.mkdir(path.dirname(destPath), { recursive: true })
106
+ await fs.writeFile(destPath, content)
107
+ } catch (err) {
108
+ // File might be deleted, mark for removal
109
+ try {
110
+ await fs.unlink(destPath)
111
+ } catch {}
112
+ }
113
+ }
114
+
115
+ // Stage and commit in snapshot repo
116
+ await execAsync(`git add -A`, { cwd: this.snapshotDir })
117
+
118
+ const commitMsg = `${message}\n\nFiles: ${changedFiles.length}\nTimestamp: ${timestamp}`
119
+ await execAsync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { cwd: this.snapshotDir })
120
+
121
+ // Get commit hash
122
+ const { stdout } = await execAsync(`git rev-parse HEAD`, { cwd: this.snapshotDir })
123
+ const hash = stdout.trim()
124
+
125
+ // Log to manifest
126
+ await this.logSnapshot({
127
+ hash,
128
+ message,
129
+ timestamp,
130
+ files: changedFiles
131
+ })
132
+
133
+ // Emit event for plugins
134
+ await emit.snapshotCreated({
135
+ hash,
136
+ message,
137
+ timestamp,
138
+ filesCount: changedFiles.length,
139
+ projectId: this.projectId
140
+ })
141
+
142
+ return { hash, message, timestamp, files: changedFiles }
143
+ }
144
+
145
+ /**
146
+ * Get list of changed files in project
147
+ */
148
+ async getChangedFiles() {
149
+ try {
150
+ const { stdout } = await execAsync(
151
+ `git status --porcelain`,
152
+ { cwd: this.projectPath }
153
+ )
154
+
155
+ return stdout
156
+ .split('\n')
157
+ .filter(Boolean)
158
+ .map(line => line.slice(3).trim())
159
+ .filter(file => !file.startsWith('.prjct/'))
160
+ } catch {
161
+ return []
162
+ }
163
+ }
164
+
165
+ /**
166
+ * List all snapshots
167
+ *
168
+ * @param {number} limit - Max snapshots to return
169
+ * @returns {Promise<Array>} List of snapshots
170
+ */
171
+ async list(limit = 10) {
172
+ if (!this.initialized) await this.initialize()
173
+
174
+ try {
175
+ const { stdout } = await execAsync(
176
+ `git log --pretty=format:'{"hash":"%H","short":"%h","message":"%s","date":"%ai"}' -n ${limit}`,
177
+ { cwd: this.snapshotDir }
178
+ )
179
+
180
+ return stdout
181
+ .split('\n')
182
+ .filter(Boolean)
183
+ .map(line => {
184
+ try {
185
+ return JSON.parse(line)
186
+ } catch {
187
+ return null
188
+ }
189
+ })
190
+ .filter(Boolean)
191
+ } catch {
192
+ return []
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Restore project to a specific snapshot
198
+ *
199
+ * @param {string} hash - Commit hash to restore
200
+ * @returns {Promise<Object>} Restore result
201
+ */
202
+ async restore(hash) {
203
+ if (!this.initialized) await this.initialize()
204
+
205
+ // Get files changed in that commit
206
+ const { stdout: filesOutput } = await execAsync(
207
+ `git diff-tree --no-commit-id --name-only -r ${hash}`,
208
+ { cwd: this.snapshotDir }
209
+ )
210
+
211
+ const files = filesOutput.split('\n').filter(Boolean)
212
+
213
+ // Checkout files from that snapshot
214
+ await execAsync(`git checkout ${hash} -- .`, { cwd: this.snapshotDir })
215
+
216
+ // Copy files back to project
217
+ for (const file of files) {
218
+ const srcPath = path.join(this.snapshotDir, file)
219
+ const destPath = path.join(this.projectPath, file)
220
+
221
+ try {
222
+ const content = await fs.readFile(srcPath, 'utf-8')
223
+ await fs.mkdir(path.dirname(destPath), { recursive: true })
224
+ await fs.writeFile(destPath, content)
225
+ } catch (err) {
226
+ // File doesn't exist in snapshot, might need to delete from project
227
+ try {
228
+ await fs.unlink(destPath)
229
+ } catch {}
230
+ }
231
+ }
232
+
233
+ // Log restoration
234
+ await this.logRestore(hash, files)
235
+
236
+ const timestamp = new Date().toISOString()
237
+
238
+ // Emit event for plugins
239
+ await emit.snapshotRestored({
240
+ hash,
241
+ filesCount: files.length,
242
+ timestamp,
243
+ projectId: this.projectId
244
+ })
245
+
246
+ return { hash, files, timestamp }
247
+ }
248
+
249
+ /**
250
+ * Get diff between current state and a snapshot
251
+ *
252
+ * @param {string} hash - Snapshot hash to compare
253
+ * @returns {Promise<string>} Diff output
254
+ */
255
+ async diff(hash) {
256
+ if (!this.initialized) await this.initialize()
257
+
258
+ try {
259
+ const { stdout } = await execAsync(
260
+ `git diff ${hash} --stat`,
261
+ { cwd: this.snapshotDir }
262
+ )
263
+ return stdout
264
+ } catch {
265
+ return ''
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Get the most recent snapshot hash
271
+ */
272
+ async getLatestHash() {
273
+ if (!this.initialized) await this.initialize()
274
+
275
+ try {
276
+ const { stdout } = await execAsync(
277
+ `git rev-parse HEAD`,
278
+ { cwd: this.snapshotDir }
279
+ )
280
+ return stdout.trim()
281
+ } catch {
282
+ return null
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Get the hash before the current one (for undo)
288
+ */
289
+ async getPreviousHash() {
290
+ if (!this.initialized) await this.initialize()
291
+
292
+ try {
293
+ const { stdout } = await execAsync(
294
+ `git rev-parse HEAD~1`,
295
+ { cwd: this.snapshotDir }
296
+ )
297
+ return stdout.trim()
298
+ } catch {
299
+ return null
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Log snapshot to manifest
305
+ */
306
+ async logSnapshot(snapshot) {
307
+ const manifestPath = path.join(this.snapshotDir, 'manifest.jsonl')
308
+ const entry = JSON.stringify({
309
+ type: 'snapshot',
310
+ ...snapshot
311
+ }) + '\n'
312
+
313
+ await fs.appendFile(manifestPath, entry)
314
+ }
315
+
316
+ /**
317
+ * Log restoration to manifest
318
+ */
319
+ async logRestore(hash, files) {
320
+ const manifestPath = path.join(this.snapshotDir, 'manifest.jsonl')
321
+ const entry = JSON.stringify({
322
+ type: 'restore',
323
+ hash,
324
+ files,
325
+ timestamp: new Date().toISOString()
326
+ }) + '\n'
327
+
328
+ await fs.appendFile(manifestPath, entry)
329
+ }
330
+
331
+ /**
332
+ * Get redo stack (snapshots after current position)
333
+ * This tracks undone snapshots that can be redone
334
+ */
335
+ async getRedoStack() {
336
+ const stackPath = path.join(this.snapshotDir, 'redo-stack.json')
337
+ try {
338
+ const content = await fs.readFile(stackPath, 'utf-8')
339
+ return JSON.parse(content)
340
+ } catch {
341
+ return []
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Push to redo stack (when undoing)
347
+ */
348
+ async pushToRedoStack(snapshot) {
349
+ const stack = await this.getRedoStack()
350
+ stack.push(snapshot)
351
+ const stackPath = path.join(this.snapshotDir, 'redo-stack.json')
352
+ await fs.writeFile(stackPath, JSON.stringify(stack, null, 2))
353
+ }
354
+
355
+ /**
356
+ * Pop from redo stack (when redoing)
357
+ */
358
+ async popFromRedoStack() {
359
+ const stack = await this.getRedoStack()
360
+ const snapshot = stack.pop()
361
+ const stackPath = path.join(this.snapshotDir, 'redo-stack.json')
362
+ await fs.writeFile(stackPath, JSON.stringify(stack, null, 2))
363
+ return snapshot
364
+ }
365
+
366
+ /**
367
+ * Clear redo stack (when creating new snapshot after undo)
368
+ */
369
+ async clearRedoStack() {
370
+ const stackPath = path.join(this.snapshotDir, 'redo-stack.json')
371
+ await fs.writeFile(stackPath, '[]')
372
+ }
373
+ }
374
+
375
+ module.exports = SnapshotManager
@@ -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
+ }