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.
- package/CHANGELOG.md +19 -0
- package/bin/dev.js +217 -0
- package/bin/prjct +10 -0
- package/bin/serve.js +78 -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 +28 -4
- package/packages/shared/dist/index.d.ts +615 -0
- package/packages/shared/dist/index.js +204 -0
- package/packages/shared/package.json +29 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/schemas.ts +124 -0
- package/packages/shared/src/types.ts +187 -0
- package/packages/shared/src/utils.ts +148 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/web/README.md +36 -0
- package/packages/web/app/api/claude/sessions/route.ts +44 -0
- package/packages/web/app/api/claude/status/route.ts +34 -0
- package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
- package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
- package/packages/web/app/api/projects/[id]/route.ts +29 -0
- package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
- package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
- package/packages/web/app/api/projects/route.ts +16 -0
- package/packages/web/app/api/sessions/history/route.ts +122 -0
- package/packages/web/app/api/stats/route.ts +38 -0
- package/packages/web/app/error.tsx +34 -0
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +155 -0
- package/packages/web/app/layout.tsx +43 -0
- package/packages/web/app/loading.tsx +7 -0
- package/packages/web/app/not-found.tsx +25 -0
- package/packages/web/app/page.tsx +227 -0
- package/packages/web/app/project/[id]/error.tsx +41 -0
- package/packages/web/app/project/[id]/loading.tsx +9 -0
- package/packages/web/app/project/[id]/not-found.tsx +27 -0
- package/packages/web/app/project/[id]/page.tsx +253 -0
- package/packages/web/app/project/[id]/stats/page.tsx +447 -0
- package/packages/web/app/sessions/page.tsx +165 -0
- package/packages/web/app/settings/page.tsx +150 -0
- package/packages/web/components/AppSidebar.tsx +113 -0
- package/packages/web/components/CommandButton.tsx +39 -0
- package/packages/web/components/ConnectionStatus.tsx +29 -0
- package/packages/web/components/Logo.tsx +65 -0
- package/packages/web/components/MarkdownContent.tsx +123 -0
- package/packages/web/components/ProjectAvatar.tsx +54 -0
- package/packages/web/components/TechStackBadges.tsx +20 -0
- package/packages/web/components/TerminalTab.tsx +84 -0
- package/packages/web/components/TerminalTabs.tsx +210 -0
- package/packages/web/components/charts/SessionsChart.tsx +172 -0
- package/packages/web/components/providers.tsx +45 -0
- package/packages/web/components/ui/alert-dialog.tsx +157 -0
- package/packages/web/components/ui/badge.tsx +46 -0
- package/packages/web/components/ui/button.tsx +60 -0
- package/packages/web/components/ui/card.tsx +92 -0
- package/packages/web/components/ui/chart.tsx +385 -0
- package/packages/web/components/ui/dropdown-menu.tsx +257 -0
- package/packages/web/components/ui/scroll-area.tsx +58 -0
- package/packages/web/components/ui/sheet.tsx +139 -0
- package/packages/web/components/ui/tabs.tsx +66 -0
- package/packages/web/components/ui/tooltip.tsx +61 -0
- package/packages/web/components.json +22 -0
- package/packages/web/context/TerminalContext.tsx +45 -0
- package/packages/web/context/TerminalTabsContext.tsx +136 -0
- package/packages/web/eslint.config.mjs +18 -0
- package/packages/web/hooks/useClaudeTerminal.ts +375 -0
- package/packages/web/hooks/useProjectStats.ts +38 -0
- package/packages/web/hooks/useProjects.ts +73 -0
- package/packages/web/hooks/useStats.ts +28 -0
- package/packages/web/lib/format.ts +23 -0
- package/packages/web/lib/parse-prjct-files.ts +1122 -0
- package/packages/web/lib/projects.ts +452 -0
- package/packages/web/lib/pty.ts +101 -0
- package/packages/web/lib/query-config.ts +44 -0
- package/packages/web/lib/utils.ts +6 -0
- package/packages/web/next-env.d.ts +6 -0
- package/packages/web/next.config.ts +7 -0
- package/packages/web/package.json +53 -0
- package/packages/web/postcss.config.mjs +7 -0
- package/packages/web/public/file.svg +1 -0
- package/packages/web/public/globe.svg +1 -0
- package/packages/web/public/next.svg +1 -0
- package/packages/web/public/vercel.svg +1 -0
- package/packages/web/public/window.svg +1 -0
- package/packages/web/server.ts +262 -0
- package/packages/web/tsconfig.json +34 -0
- package/templates/commands/done.md +176 -54
- 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/undo.md +152 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0] - 2025-12-08
|
|
4
|
+
|
|
5
|
+
### Added - Web Application & Server Components
|
|
6
|
+
|
|
7
|
+
Major release introducing the prjct web application with Next.js.
|
|
8
|
+
|
|
9
|
+
- **Web Application** - Full Next.js web interface for prjct
|
|
10
|
+
- Project stats API implementation
|
|
11
|
+
- Enhanced UI components
|
|
12
|
+
- Terminal functionality in browser
|
|
13
|
+
|
|
14
|
+
- **Server Components** - New server infrastructure
|
|
15
|
+
- Project management endpoints
|
|
16
|
+
- Stats API for metrics and analytics
|
|
17
|
+
|
|
18
|
+
- **UI Enhancements**
|
|
19
|
+
- Improved project management interface
|
|
20
|
+
- Enhanced terminal integration
|
|
21
|
+
|
|
3
22
|
## [0.10.14] - 2025-11-29
|
|
4
23
|
|
|
5
24
|
### Refactored - 100% Agentic Subagent Delegation via Task Tool
|
package/bin/dev.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* prjct dev - Start prjct web development environment
|
|
5
|
+
*
|
|
6
|
+
* Launches Next.js fullstack app on port 9472
|
|
7
|
+
* - Frontend + API routes + WebSocket for PTY
|
|
8
|
+
*
|
|
9
|
+
* Usage: prjct dev [--no-open]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { spawn, exec } = require('child_process')
|
|
13
|
+
const path = require('path')
|
|
14
|
+
const os = require('os')
|
|
15
|
+
|
|
16
|
+
// Configuration
|
|
17
|
+
const PORT = process.env.PRJCT_PORT || 9472
|
|
18
|
+
const WEB_URL = `http://localhost:${PORT}`
|
|
19
|
+
|
|
20
|
+
// Colors for terminal output
|
|
21
|
+
const colors = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
bright: '\x1b[1m',
|
|
24
|
+
dim: '\x1b[2m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
green: '\x1b[32m',
|
|
27
|
+
yellow: '\x1b[33m',
|
|
28
|
+
red: '\x1b[31m',
|
|
29
|
+
magenta: '\x1b[35m'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Find prjct-cli root (where packages/ lives)
|
|
33
|
+
function findPrjctRoot() {
|
|
34
|
+
const locations = [
|
|
35
|
+
path.join(__dirname, '..'),
|
|
36
|
+
path.join(os.homedir(), 'Apps', 'prjct', 'prjct-cli'),
|
|
37
|
+
path.join(os.homedir(), '.prjct-cli', 'source'),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
for (const loc of locations) {
|
|
41
|
+
const pkgPath = path.join(loc, 'packages')
|
|
42
|
+
try {
|
|
43
|
+
require('fs').accessSync(pkgPath)
|
|
44
|
+
return loc
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return locations[0]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PRJCT_ROOT = findPrjctRoot()
|
|
52
|
+
const WEB_PATH = path.join(PRJCT_ROOT, 'packages', 'web')
|
|
53
|
+
|
|
54
|
+
// Print banner
|
|
55
|
+
function printBanner() {
|
|
56
|
+
console.log(`
|
|
57
|
+
${colors.cyan}${colors.bright}╔═══════════════════════════════════════════════╗
|
|
58
|
+
║ ║
|
|
59
|
+
║ ⚡ prjct dev ║
|
|
60
|
+
║ ║
|
|
61
|
+
║ App: ${colors.green}http://localhost:${PORT}${colors.cyan} ║
|
|
62
|
+
║ ║
|
|
63
|
+
║ ${colors.dim}Press Ctrl+C to stop${colors.cyan}${colors.bright} ║
|
|
64
|
+
║ ║
|
|
65
|
+
╚═══════════════════════════════════════════════╝${colors.reset}
|
|
66
|
+
`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Open browser based on OS
|
|
70
|
+
function openBrowser(url) {
|
|
71
|
+
const platform = os.platform()
|
|
72
|
+
let command
|
|
73
|
+
|
|
74
|
+
switch (platform) {
|
|
75
|
+
case 'darwin':
|
|
76
|
+
command = `open "${url}"`
|
|
77
|
+
break
|
|
78
|
+
case 'win32':
|
|
79
|
+
command = `start "" "${url}"`
|
|
80
|
+
break
|
|
81
|
+
default:
|
|
82
|
+
command = `xdg-open "${url}"`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
exec(command, (err) => {
|
|
86
|
+
if (err) {
|
|
87
|
+
console.log(`${colors.yellow}Could not open browser automatically. Visit: ${url}${colors.reset}`)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if port is available
|
|
93
|
+
function checkPort(port) {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
const net = require('net')
|
|
96
|
+
const server = net.createServer()
|
|
97
|
+
|
|
98
|
+
server.once('error', () => resolve(false))
|
|
99
|
+
server.once('listening', () => {
|
|
100
|
+
server.close()
|
|
101
|
+
resolve(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
server.listen(port)
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Wait for server to be ready
|
|
109
|
+
function waitForServer(port, maxAttempts = 60) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
let attempts = 0
|
|
112
|
+
|
|
113
|
+
const check = () => {
|
|
114
|
+
const http = require('http')
|
|
115
|
+
const req = http.get(`http://localhost:${port}`, (res) => {
|
|
116
|
+
resolve(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
req.on('error', () => {
|
|
120
|
+
attempts++
|
|
121
|
+
if (attempts >= maxAttempts) {
|
|
122
|
+
reject(new Error(`Server on port ${port} did not start`))
|
|
123
|
+
} else {
|
|
124
|
+
setTimeout(check, 500)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
req.end()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
check()
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Main function
|
|
136
|
+
async function main() {
|
|
137
|
+
const args = process.argv.slice(2)
|
|
138
|
+
const noOpen = args.includes('--no-open')
|
|
139
|
+
|
|
140
|
+
// Check port
|
|
141
|
+
const portAvailable = await checkPort(PORT)
|
|
142
|
+
|
|
143
|
+
if (!portAvailable) {
|
|
144
|
+
console.log(`${colors.red}Port ${PORT} is already in use. Stop other services or set PRJCT_PORT.${colors.reset}`)
|
|
145
|
+
process.exit(1)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
printBanner()
|
|
149
|
+
|
|
150
|
+
// Start Next.js with custom server
|
|
151
|
+
console.log(`${colors.cyan}Starting prjct...${colors.reset}`)
|
|
152
|
+
const webProc = spawn('npm', ['run', 'dev'], {
|
|
153
|
+
cwd: WEB_PATH,
|
|
154
|
+
env: { ...process.env, PORT: PORT.toString() },
|
|
155
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
156
|
+
shell: true
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
webProc.stdout.on('data', (data) => {
|
|
160
|
+
const lines = data.toString().split('\n').filter(Boolean)
|
|
161
|
+
lines.forEach(line => {
|
|
162
|
+
// Show relevant output
|
|
163
|
+
if (line.includes('ready') || line.includes('Ready') || line.includes('[WS]')) {
|
|
164
|
+
console.log(`${colors.green}${line}${colors.reset}`)
|
|
165
|
+
} else if (!line.includes('╔') && !line.includes('║') && !line.includes('╚')) {
|
|
166
|
+
console.log(`${colors.dim}${line}${colors.reset}`)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
webProc.stderr.on('data', (data) => {
|
|
172
|
+
const msg = data.toString().trim()
|
|
173
|
+
// Filter out common non-error messages
|
|
174
|
+
if (!msg.includes('ExperimentalWarning') && !msg.includes('punycode')) {
|
|
175
|
+
console.log(`${colors.red}${msg}${colors.reset}`)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Wait for server and open browser
|
|
180
|
+
try {
|
|
181
|
+
console.log(`${colors.dim}Waiting for server to start...${colors.reset}`)
|
|
182
|
+
await waitForServer(PORT)
|
|
183
|
+
|
|
184
|
+
console.log(`${colors.green}${colors.bright}Ready!${colors.reset}\n`)
|
|
185
|
+
|
|
186
|
+
if (!noOpen) {
|
|
187
|
+
setTimeout(() => openBrowser(WEB_URL), 500)
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.log(`${colors.red}${err.message}${colors.reset}`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Handle shutdown
|
|
194
|
+
const cleanup = () => {
|
|
195
|
+
console.log(`\n${colors.yellow}Shutting down...${colors.reset}`)
|
|
196
|
+
webProc.kill()
|
|
197
|
+
process.exit(0)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
process.on('SIGINT', cleanup)
|
|
201
|
+
process.on('SIGTERM', cleanup)
|
|
202
|
+
|
|
203
|
+
webProc.on('error', (err) => {
|
|
204
|
+
console.log(`${colors.red}Error: ${err.message}${colors.reset}`)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
webProc.on('exit', (code) => {
|
|
208
|
+
if (code !== 0 && code !== null) {
|
|
209
|
+
console.log(`${colors.red}Exited with code ${code}${colors.reset}`)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
main().catch((err) => {
|
|
215
|
+
console.error(`${colors.red}Error: ${err.message}${colors.reset}`)
|
|
216
|
+
process.exit(1)
|
|
217
|
+
})
|
package/bin/prjct
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
const { VERSION } = require('../core/utils/version')
|
|
10
10
|
const editorsConfig = require('../core/infrastructure/editors-config')
|
|
11
11
|
|
|
12
|
+
// Check for special subcommands that bypass normal CLI
|
|
13
|
+
const args = process.argv.slice(2)
|
|
14
|
+
if (args[0] === 'dev') {
|
|
15
|
+
// Launch prjct dev environment
|
|
16
|
+
require('./dev.js')
|
|
17
|
+
process.exitCode = 0
|
|
18
|
+
} else {
|
|
19
|
+
|
|
12
20
|
// Ensure setup has run for this version
|
|
13
21
|
;(async function ensureSetup() {
|
|
14
22
|
try {
|
|
@@ -32,3 +40,5 @@ const editorsConfig = require('../core/infrastructure/editors-config')
|
|
|
32
40
|
// Continue to main CLI logic
|
|
33
41
|
require('../core/index')
|
|
34
42
|
})()
|
|
43
|
+
|
|
44
|
+
} // end else
|
package/bin/serve.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* prjct serve - Start the web server
|
|
5
|
+
*
|
|
6
|
+
* Launches the prjct web interface with Claude Code CLI integration.
|
|
7
|
+
* Uses your existing Claude subscription via PTY - no API costs!
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('child_process')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
|
|
14
|
+
const serverDir = path.join(__dirname, '..', 'packages', 'server')
|
|
15
|
+
const webDir = path.join(__dirname, '..', 'packages', 'web')
|
|
16
|
+
|
|
17
|
+
// Parse arguments
|
|
18
|
+
const args = process.argv.slice(2)
|
|
19
|
+
const portArg = args.find(a => a.startsWith('--port='))
|
|
20
|
+
const port = portArg ? portArg.split('=')[1] : '3333'
|
|
21
|
+
const webPort = '3000'
|
|
22
|
+
|
|
23
|
+
// Check if packages exist
|
|
24
|
+
if (!fs.existsSync(serverDir) || !fs.existsSync(webDir)) {
|
|
25
|
+
console.error('❌ Web packages not found. Run from prjct-cli directory.')
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`
|
|
30
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
31
|
+
║ ║
|
|
32
|
+
║ ⚡ prjct - Developer Momentum ║
|
|
33
|
+
║ ║
|
|
34
|
+
║ Starting web server... ║
|
|
35
|
+
║ ║
|
|
36
|
+
║ API: http://localhost:${port} ║
|
|
37
|
+
║ Web: http://localhost:${webPort} ║
|
|
38
|
+
║ Claude: ws://localhost:${port}/ws/claude ║
|
|
39
|
+
║ ║
|
|
40
|
+
║ Using your Claude subscription - $0 API costs ║
|
|
41
|
+
║ ║
|
|
42
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
43
|
+
`)
|
|
44
|
+
|
|
45
|
+
// Start server
|
|
46
|
+
const server = spawn('npm', ['run', 'dev'], {
|
|
47
|
+
cwd: serverDir,
|
|
48
|
+
stdio: 'inherit',
|
|
49
|
+
shell: true,
|
|
50
|
+
env: { ...process.env, PORT: port }
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Start web dev server
|
|
54
|
+
const web = spawn('npm', ['run', 'dev'], {
|
|
55
|
+
cwd: webDir,
|
|
56
|
+
stdio: 'inherit',
|
|
57
|
+
shell: true
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Handle shutdown
|
|
61
|
+
const cleanup = () => {
|
|
62
|
+
console.log('\n👋 Shutting down prjct server...')
|
|
63
|
+
server.kill()
|
|
64
|
+
web.kill()
|
|
65
|
+
process.exit(0)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.on('SIGINT', cleanup)
|
|
69
|
+
process.on('SIGTERM', cleanup)
|
|
70
|
+
|
|
71
|
+
// Handle errors
|
|
72
|
+
server.on('error', (err) => {
|
|
73
|
+
console.error('Server error:', err.message)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
web.on('error', (err) => {
|
|
77
|
+
console.error('Web error:', err.message)
|
|
78
|
+
})
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBus - Lightweight Pub/Sub System for prjct-cli
|
|
3
|
+
*
|
|
4
|
+
* Simple event bus for decoupled communication between components.
|
|
5
|
+
* Supports sync/async listeners, wildcards, and one-time subscriptions.
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs').promises
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const pathManager = require('../infrastructure/path-manager')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Event Types - All events that can be emitted
|
|
16
|
+
*/
|
|
17
|
+
const EventTypes = {
|
|
18
|
+
// Session events
|
|
19
|
+
SESSION_STARTED: 'session.started',
|
|
20
|
+
SESSION_PAUSED: 'session.paused',
|
|
21
|
+
SESSION_RESUMED: 'session.resumed',
|
|
22
|
+
SESSION_COMPLETED: 'session.completed',
|
|
23
|
+
|
|
24
|
+
// Task events
|
|
25
|
+
TASK_CREATED: 'task.created',
|
|
26
|
+
TASK_COMPLETED: 'task.completed',
|
|
27
|
+
TASK_UPDATED: 'task.updated',
|
|
28
|
+
|
|
29
|
+
// Feature events
|
|
30
|
+
FEATURE_ADDED: 'feature.added',
|
|
31
|
+
FEATURE_SHIPPED: 'feature.shipped',
|
|
32
|
+
FEATURE_UPDATED: 'feature.updated',
|
|
33
|
+
|
|
34
|
+
// Idea events
|
|
35
|
+
IDEA_CAPTURED: 'idea.captured',
|
|
36
|
+
IDEA_PROMOTED: 'idea.promoted',
|
|
37
|
+
|
|
38
|
+
// Snapshot events
|
|
39
|
+
SNAPSHOT_CREATED: 'snapshot.created',
|
|
40
|
+
SNAPSHOT_RESTORED: 'snapshot.restored',
|
|
41
|
+
|
|
42
|
+
// Git events
|
|
43
|
+
COMMIT_CREATED: 'git.commit',
|
|
44
|
+
PUSH_COMPLETED: 'git.push',
|
|
45
|
+
|
|
46
|
+
// System events
|
|
47
|
+
PROJECT_INITIALIZED: 'project.init',
|
|
48
|
+
PROJECT_SYNCED: 'project.sync',
|
|
49
|
+
ANALYSIS_COMPLETED: 'analysis.completed',
|
|
50
|
+
|
|
51
|
+
// Wildcard
|
|
52
|
+
ALL: '*'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class EventBus {
|
|
56
|
+
constructor() {
|
|
57
|
+
this.listeners = new Map()
|
|
58
|
+
this.onceListeners = new Map()
|
|
59
|
+
this.history = []
|
|
60
|
+
this.historyLimit = 100
|
|
61
|
+
this.projectId = null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initialize event bus for a project
|
|
66
|
+
* @param {string} projectId
|
|
67
|
+
*/
|
|
68
|
+
async initialize(projectId) {
|
|
69
|
+
this.projectId = projectId
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe to an event
|
|
74
|
+
* @param {string} event - Event type or wildcard pattern
|
|
75
|
+
* @param {Function} callback - Handler function
|
|
76
|
+
* @returns {Function} Unsubscribe function
|
|
77
|
+
*/
|
|
78
|
+
on(event, callback) {
|
|
79
|
+
if (!this.listeners.has(event)) {
|
|
80
|
+
this.listeners.set(event, new Set())
|
|
81
|
+
}
|
|
82
|
+
this.listeners.get(event).add(callback)
|
|
83
|
+
|
|
84
|
+
// Return unsubscribe function
|
|
85
|
+
return () => this.off(event, callback)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Subscribe to an event once
|
|
90
|
+
* @param {string} event
|
|
91
|
+
* @param {Function} callback
|
|
92
|
+
* @returns {Function} Unsubscribe function
|
|
93
|
+
*/
|
|
94
|
+
once(event, callback) {
|
|
95
|
+
if (!this.onceListeners.has(event)) {
|
|
96
|
+
this.onceListeners.set(event, new Set())
|
|
97
|
+
}
|
|
98
|
+
this.onceListeners.get(event).add(callback)
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
const listeners = this.onceListeners.get(event)
|
|
102
|
+
if (listeners) {
|
|
103
|
+
listeners.delete(callback)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Unsubscribe from an event
|
|
110
|
+
* @param {string} event
|
|
111
|
+
* @param {Function} callback
|
|
112
|
+
*/
|
|
113
|
+
off(event, callback) {
|
|
114
|
+
const listeners = this.listeners.get(event)
|
|
115
|
+
if (listeners) {
|
|
116
|
+
listeners.delete(callback)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Emit an event
|
|
122
|
+
* @param {string} event - Event type
|
|
123
|
+
* @param {Object} data - Event payload
|
|
124
|
+
* @returns {Promise<void>}
|
|
125
|
+
*/
|
|
126
|
+
async emit(event, data = {}) {
|
|
127
|
+
const timestamp = new Date().toISOString()
|
|
128
|
+
const eventData = {
|
|
129
|
+
type: event,
|
|
130
|
+
timestamp,
|
|
131
|
+
projectId: this.projectId,
|
|
132
|
+
...data
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Store in history
|
|
136
|
+
this.history.push(eventData)
|
|
137
|
+
if (this.history.length > this.historyLimit) {
|
|
138
|
+
this.history.shift()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Log event if project initialized
|
|
142
|
+
if (this.projectId) {
|
|
143
|
+
await this.logEvent(eventData)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get all matching listeners
|
|
147
|
+
const callbacks = this.getMatchingListeners(event)
|
|
148
|
+
|
|
149
|
+
// Execute all callbacks
|
|
150
|
+
const results = await Promise.allSettled(
|
|
151
|
+
callbacks.map(cb => this.executeCallback(cb, eventData))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Log any errors
|
|
155
|
+
results.forEach((result, index) => {
|
|
156
|
+
if (result.status === 'rejected') {
|
|
157
|
+
console.error(`Event listener error for ${event}:`, result.reason)
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Handle once listeners
|
|
162
|
+
const onceCallbacks = this.onceListeners.get(event)
|
|
163
|
+
if (onceCallbacks) {
|
|
164
|
+
for (const cb of onceCallbacks) {
|
|
165
|
+
await this.executeCallback(cb, eventData)
|
|
166
|
+
}
|
|
167
|
+
this.onceListeners.delete(event)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Also trigger wildcard once listeners
|
|
171
|
+
const wildcardOnce = this.onceListeners.get(EventTypes.ALL)
|
|
172
|
+
if (wildcardOnce) {
|
|
173
|
+
for (const cb of wildcardOnce) {
|
|
174
|
+
await this.executeCallback(cb, eventData)
|
|
175
|
+
}
|
|
176
|
+
// Don't delete wildcard once - only for specific events
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get all listeners that match an event (including wildcards)
|
|
182
|
+
* @param {string} event
|
|
183
|
+
* @returns {Function[]}
|
|
184
|
+
*/
|
|
185
|
+
getMatchingListeners(event) {
|
|
186
|
+
const callbacks = []
|
|
187
|
+
|
|
188
|
+
// Exact match
|
|
189
|
+
const exact = this.listeners.get(event)
|
|
190
|
+
if (exact) {
|
|
191
|
+
callbacks.push(...exact)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Wildcard match (*)
|
|
195
|
+
const wildcard = this.listeners.get(EventTypes.ALL)
|
|
196
|
+
if (wildcard) {
|
|
197
|
+
callbacks.push(...wildcard)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Namespace wildcard (e.g., 'session.*' matches 'session.started')
|
|
201
|
+
const namespace = event.split('.')[0]
|
|
202
|
+
const namespaceWildcard = this.listeners.get(`${namespace}.*`)
|
|
203
|
+
if (namespaceWildcard) {
|
|
204
|
+
callbacks.push(...namespaceWildcard)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return callbacks
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Execute a callback safely (handles sync and async)
|
|
212
|
+
* @param {Function} callback
|
|
213
|
+
* @param {Object} data
|
|
214
|
+
*/
|
|
215
|
+
async executeCallback(callback, data) {
|
|
216
|
+
try {
|
|
217
|
+
const result = callback(data)
|
|
218
|
+
if (result instanceof Promise) {
|
|
219
|
+
await result
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('Event callback error:', error)
|
|
223
|
+
throw error
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Log event to persistent storage
|
|
229
|
+
* @param {Object} eventData
|
|
230
|
+
*/
|
|
231
|
+
async logEvent(eventData) {
|
|
232
|
+
try {
|
|
233
|
+
const globalPath = pathManager.getGlobalProjectPath(this.projectId)
|
|
234
|
+
const eventsPath = path.join(globalPath, 'memory', 'events.jsonl')
|
|
235
|
+
|
|
236
|
+
// Ensure directory exists
|
|
237
|
+
await fs.mkdir(path.dirname(eventsPath), { recursive: true })
|
|
238
|
+
|
|
239
|
+
// Append event
|
|
240
|
+
const line = JSON.stringify(eventData) + '\n'
|
|
241
|
+
await fs.appendFile(eventsPath, line)
|
|
242
|
+
} catch {
|
|
243
|
+
// Silently fail - logging should not break functionality
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get recent events from history
|
|
249
|
+
* @param {number} limit
|
|
250
|
+
* @param {string} [type] - Optional filter by event type
|
|
251
|
+
* @returns {Object[]}
|
|
252
|
+
*/
|
|
253
|
+
getHistory(limit = 10, type = null) {
|
|
254
|
+
let events = this.history
|
|
255
|
+
if (type) {
|
|
256
|
+
events = events.filter(e => e.type === type || e.type.startsWith(type))
|
|
257
|
+
}
|
|
258
|
+
return events.slice(-limit)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Clear all listeners
|
|
263
|
+
*/
|
|
264
|
+
clear() {
|
|
265
|
+
this.listeners.clear()
|
|
266
|
+
this.onceListeners.clear()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get count of listeners for an event
|
|
271
|
+
* @param {string} event
|
|
272
|
+
* @returns {number}
|
|
273
|
+
*/
|
|
274
|
+
listenerCount(event) {
|
|
275
|
+
const listeners = this.listeners.get(event)
|
|
276
|
+
return listeners ? listeners.size : 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get all registered event types
|
|
281
|
+
* @returns {string[]}
|
|
282
|
+
*/
|
|
283
|
+
getRegisteredEvents() {
|
|
284
|
+
return Array.from(this.listeners.keys())
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Singleton instance
|
|
289
|
+
const eventBus = new EventBus()
|
|
290
|
+
|
|
291
|
+
// Convenience methods for common events
|
|
292
|
+
const emit = {
|
|
293
|
+
sessionStarted: (data) => eventBus.emit(EventTypes.SESSION_STARTED, data),
|
|
294
|
+
sessionPaused: (data) => eventBus.emit(EventTypes.SESSION_PAUSED, data),
|
|
295
|
+
sessionResumed: (data) => eventBus.emit(EventTypes.SESSION_RESUMED, data),
|
|
296
|
+
sessionCompleted: (data) => eventBus.emit(EventTypes.SESSION_COMPLETED, data),
|
|
297
|
+
|
|
298
|
+
taskCreated: (data) => eventBus.emit(EventTypes.TASK_CREATED, data),
|
|
299
|
+
taskCompleted: (data) => eventBus.emit(EventTypes.TASK_COMPLETED, data),
|
|
300
|
+
|
|
301
|
+
featureAdded: (data) => eventBus.emit(EventTypes.FEATURE_ADDED, data),
|
|
302
|
+
featureShipped: (data) => eventBus.emit(EventTypes.FEATURE_SHIPPED, data),
|
|
303
|
+
|
|
304
|
+
ideaCaptured: (data) => eventBus.emit(EventTypes.IDEA_CAPTURED, data),
|
|
305
|
+
|
|
306
|
+
snapshotCreated: (data) => eventBus.emit(EventTypes.SNAPSHOT_CREATED, data),
|
|
307
|
+
snapshotRestored: (data) => eventBus.emit(EventTypes.SNAPSHOT_RESTORED, data),
|
|
308
|
+
|
|
309
|
+
commitCreated: (data) => eventBus.emit(EventTypes.COMMIT_CREATED, data),
|
|
310
|
+
pushCompleted: (data) => eventBus.emit(EventTypes.PUSH_COMPLETED, data),
|
|
311
|
+
|
|
312
|
+
projectInitialized: (data) => eventBus.emit(EventTypes.PROJECT_INITIALIZED, data),
|
|
313
|
+
projectSynced: (data) => eventBus.emit(EventTypes.PROJECT_SYNCED, data),
|
|
314
|
+
analysisCompleted: (data) => eventBus.emit(EventTypes.ANALYSIS_COMPLETED, data)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
EventBus,
|
|
319
|
+
EventTypes,
|
|
320
|
+
eventBus,
|
|
321
|
+
emit
|
|
322
|
+
}
|