prjct-cli 0.10.14 → 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 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
+ }