prjct-cli 0.11.0 → 0.11.2

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 (81) hide show
  1. package/bin/serve.js +90 -26
  2. package/package.json +11 -1
  3. package/packages/shared/dist/index.d.ts +615 -0
  4. package/packages/shared/dist/index.js +204 -0
  5. package/packages/shared/package.json +29 -0
  6. package/packages/shared/src/index.ts +9 -0
  7. package/packages/shared/src/schemas.ts +124 -0
  8. package/packages/shared/src/types.ts +187 -0
  9. package/packages/shared/src/utils.ts +148 -0
  10. package/packages/shared/tsconfig.json +18 -0
  11. package/packages/web/README.md +36 -0
  12. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  13. package/packages/web/app/api/claude/status/route.ts +34 -0
  14. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  15. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  16. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  17. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  18. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  19. package/packages/web/app/api/projects/route.ts +16 -0
  20. package/packages/web/app/api/sessions/history/route.ts +122 -0
  21. package/packages/web/app/api/stats/route.ts +38 -0
  22. package/packages/web/app/error.tsx +34 -0
  23. package/packages/web/app/favicon.ico +0 -0
  24. package/packages/web/app/globals.css +155 -0
  25. package/packages/web/app/layout.tsx +43 -0
  26. package/packages/web/app/loading.tsx +7 -0
  27. package/packages/web/app/not-found.tsx +25 -0
  28. package/packages/web/app/page.tsx +227 -0
  29. package/packages/web/app/project/[id]/error.tsx +41 -0
  30. package/packages/web/app/project/[id]/loading.tsx +9 -0
  31. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  32. package/packages/web/app/project/[id]/page.tsx +253 -0
  33. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  34. package/packages/web/app/sessions/page.tsx +165 -0
  35. package/packages/web/app/settings/page.tsx +150 -0
  36. package/packages/web/components/AppSidebar.tsx +113 -0
  37. package/packages/web/components/CommandButton.tsx +39 -0
  38. package/packages/web/components/ConnectionStatus.tsx +29 -0
  39. package/packages/web/components/Logo.tsx +65 -0
  40. package/packages/web/components/MarkdownContent.tsx +123 -0
  41. package/packages/web/components/ProjectAvatar.tsx +54 -0
  42. package/packages/web/components/TechStackBadges.tsx +20 -0
  43. package/packages/web/components/TerminalTab.tsx +84 -0
  44. package/packages/web/components/TerminalTabs.tsx +210 -0
  45. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  46. package/packages/web/components/providers.tsx +45 -0
  47. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  48. package/packages/web/components/ui/badge.tsx +46 -0
  49. package/packages/web/components/ui/button.tsx +60 -0
  50. package/packages/web/components/ui/card.tsx +92 -0
  51. package/packages/web/components/ui/chart.tsx +385 -0
  52. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  53. package/packages/web/components/ui/scroll-area.tsx +58 -0
  54. package/packages/web/components/ui/sheet.tsx +139 -0
  55. package/packages/web/components/ui/tabs.tsx +66 -0
  56. package/packages/web/components/ui/tooltip.tsx +61 -0
  57. package/packages/web/components.json +22 -0
  58. package/packages/web/context/TerminalContext.tsx +45 -0
  59. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  60. package/packages/web/eslint.config.mjs +18 -0
  61. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  62. package/packages/web/hooks/useProjectStats.ts +38 -0
  63. package/packages/web/hooks/useProjects.ts +73 -0
  64. package/packages/web/hooks/useStats.ts +28 -0
  65. package/packages/web/lib/format.ts +23 -0
  66. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  67. package/packages/web/lib/projects.ts +452 -0
  68. package/packages/web/lib/pty.ts +101 -0
  69. package/packages/web/lib/query-config.ts +44 -0
  70. package/packages/web/lib/utils.ts +6 -0
  71. package/packages/web/next-env.d.ts +6 -0
  72. package/packages/web/next.config.ts +7 -0
  73. package/packages/web/package.json +53 -0
  74. package/packages/web/postcss.config.mjs +7 -0
  75. package/packages/web/public/file.svg +1 -0
  76. package/packages/web/public/globe.svg +1 -0
  77. package/packages/web/public/next.svg +1 -0
  78. package/packages/web/public/vercel.svg +1 -0
  79. package/packages/web/public/window.svg +1 -0
  80. package/packages/web/server.ts +262 -0
  81. package/packages/web/tsconfig.json +34 -0
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Custom Next.js server with WebSocket support for PTY
3
+ *
4
+ * PTY sessions are managed here (not in API routes) to share memory context
5
+ */
6
+
7
+ import { createServer } from 'http'
8
+ import next from 'next'
9
+ import { WebSocketServer, WebSocket } from 'ws'
10
+ import * as pty from 'node-pty'
11
+ import type { IPty } from 'node-pty'
12
+
13
+ const dev = process.env.NODE_ENV !== 'production'
14
+ const hostname = 'localhost'
15
+ const port = parseInt(process.env.PORT || '9472', 10)
16
+
17
+ // PTY Sessions stored in server memory
18
+ interface Session {
19
+ pty: IPty
20
+ projectDir: string
21
+ createdAt: Date
22
+ hasStartedClaude: boolean // Track if claude command was sent
23
+ }
24
+
25
+ const sessions = new Map<string, Session>()
26
+
27
+ function createSession(sessionId: string, projectDir: string): { pty: IPty; isNew: boolean } {
28
+ const existing = sessions.get(sessionId)
29
+
30
+ // If session exists for this project, reuse it (allows multiple tabs)
31
+ if (existing) {
32
+ console.log(`[PTY] Reusing existing session: ${sessionId}`)
33
+ return { pty: existing.pty, isNew: false }
34
+ }
35
+
36
+ const shell = process.platform === 'win32' ? 'cmd.exe' : 'bash'
37
+ const args = process.platform === 'win32' ? [] : ['-l']
38
+
39
+ const ptyProcess = pty.spawn(shell, args, {
40
+ name: 'xterm-256color',
41
+ cols: 120,
42
+ rows: 30,
43
+ cwd: projectDir,
44
+ env: {
45
+ ...process.env,
46
+ TERM: 'xterm-256color',
47
+ COLORTERM: 'truecolor'
48
+ }
49
+ })
50
+
51
+ sessions.set(sessionId, {
52
+ pty: ptyProcess,
53
+ projectDir,
54
+ createdAt: new Date(),
55
+ hasStartedClaude: false
56
+ })
57
+
58
+ // NOTE: Don't auto-start claude here - let the WebSocket handler do it
59
+ // once the client is connected and ready to receive output
60
+
61
+ return { pty: ptyProcess, isNew: true }
62
+ }
63
+
64
+ function getSession(sessionId: string): IPty | null {
65
+ return sessions.get(sessionId)?.pty || null
66
+ }
67
+
68
+ function killSession(sessionId: string): void {
69
+ const session = sessions.get(sessionId)
70
+ if (session) {
71
+ try { session.pty.kill() } catch {}
72
+ sessions.delete(sessionId)
73
+ }
74
+ }
75
+
76
+ function resizeSession(sessionId: string, cols: number, rows: number): void {
77
+ const session = sessions.get(sessionId)
78
+ if (session) {
79
+ try { session.pty.resize(cols, rows) } catch {}
80
+ }
81
+ }
82
+
83
+ const app = next({ dev, hostname, port })
84
+ const handle = app.getRequestHandler()
85
+
86
+ app.prepare().then(() => {
87
+ const server = createServer(async (req, res) => {
88
+ const url = new URL(req.url || '', `http://${req.headers.host}`)
89
+
90
+ // Handle session creation directly in server (bypasses API route isolation)
91
+ if (url.pathname === '/api/claude/sessions' && req.method === 'POST') {
92
+ let body = ''
93
+ req.on('data', chunk => { body += chunk })
94
+ req.on('end', () => {
95
+ try {
96
+ const { sessionId, projectDir } = JSON.parse(body)
97
+ if (!sessionId || !projectDir) {
98
+ res.statusCode = 400
99
+ res.setHeader('Content-Type', 'application/json')
100
+ res.end(JSON.stringify({ success: false, error: 'sessionId and projectDir required' }))
101
+ return
102
+ }
103
+
104
+ const { isNew } = createSession(sessionId, projectDir)
105
+ console.log(`[PTY] ${isNew ? 'Created' : 'Reusing'} session: ${sessionId} for ${projectDir}`)
106
+
107
+ res.statusCode = 200
108
+ res.setHeader('Content-Type', 'application/json')
109
+ res.end(JSON.stringify({ success: true, data: { sessionId, projectDir, isNew } }))
110
+ } catch (err) {
111
+ console.error('[PTY] Error creating session:', err)
112
+ res.statusCode = 500
113
+ res.setHeader('Content-Type', 'application/json')
114
+ res.end(JSON.stringify({ success: false, error: 'Failed to create session' }))
115
+ }
116
+ })
117
+ return
118
+ }
119
+
120
+ // Handle session list
121
+ if (url.pathname === '/api/claude/sessions' && req.method === 'GET') {
122
+ const list = Array.from(sessions.entries()).map(([id, s]) => ({
123
+ sessionId: id,
124
+ projectDir: s.projectDir,
125
+ createdAt: s.createdAt
126
+ }))
127
+ res.statusCode = 200
128
+ res.setHeader('Content-Type', 'application/json')
129
+ res.end(JSON.stringify({ success: true, data: list }))
130
+ return
131
+ }
132
+
133
+ // All other requests go to Next.js
134
+ try {
135
+ await handle(req, res)
136
+ } catch (err) {
137
+ console.error('Error handling request:', err)
138
+ res.statusCode = 500
139
+ res.end('Internal Server Error')
140
+ }
141
+ })
142
+
143
+ // WebSocket server for PTY communication
144
+ const wss = new WebSocketServer({ noServer: true })
145
+
146
+ // Heartbeat interval to detect dead connections (30 seconds)
147
+ const HEARTBEAT_INTERVAL = 30000
148
+
149
+ const heartbeatInterval = setInterval(() => {
150
+ wss.clients.forEach((ws) => {
151
+ const extWs = ws as WebSocket & { isAlive?: boolean; sessionId?: string }
152
+ if (extWs.isAlive === false) {
153
+ console.log(`[WS] Terminating dead connection: ${extWs.sessionId}`)
154
+ if (extWs.sessionId) {
155
+ killSession(extWs.sessionId)
156
+ }
157
+ return ws.terminate()
158
+ }
159
+ extWs.isAlive = false
160
+ ws.ping()
161
+ })
162
+ }, HEARTBEAT_INTERVAL)
163
+
164
+ // Cleanup on server close
165
+ wss.on('close', () => {
166
+ clearInterval(heartbeatInterval)
167
+ })
168
+
169
+ server.on('upgrade', (request, socket, head) => {
170
+ const url = new URL(request.url || '', `http://${request.headers.host}`)
171
+
172
+ if (url.pathname.startsWith('/ws/claude/')) {
173
+ wss.handleUpgrade(request, socket, head, (ws) => {
174
+ wss.emit('connection', ws, request)
175
+ })
176
+ }
177
+ // Other upgrades (HMR) pass through to Next.js
178
+ })
179
+
180
+ wss.on('connection', (ws: WebSocket, request) => {
181
+ const url = new URL(request.url || '', `http://${request.headers.host}`)
182
+ const sessionId = url.pathname.replace('/ws/claude/', '')
183
+
184
+ console.log(`[WS] New PTY connection for session: ${sessionId}`)
185
+
186
+ // Mark connection as alive and store sessionId for heartbeat
187
+ const extWs = ws as WebSocket & { isAlive?: boolean; sessionId?: string }
188
+ extWs.isAlive = true
189
+ extWs.sessionId = sessionId
190
+
191
+ // Handle pong response
192
+ ws.on('pong', () => {
193
+ extWs.isAlive = true
194
+ })
195
+
196
+ const session = sessions.get(sessionId)
197
+
198
+ if (!session) {
199
+ console.log(`[WS] Session not found: ${sessionId}`)
200
+ ws.send(JSON.stringify({ type: 'error', message: 'Session not found' }))
201
+ ws.close()
202
+ return
203
+ }
204
+
205
+ const ptyProcess = session.pty
206
+
207
+ // Register data handler FIRST before sending any commands
208
+ const dataHandler = ptyProcess.onData((data: string) => {
209
+ if (ws.readyState === WebSocket.OPEN) {
210
+ ws.send(JSON.stringify({ type: 'output', data }))
211
+ }
212
+ })
213
+
214
+ ws.send(JSON.stringify({ type: 'connected', sessionId }))
215
+
216
+ // Auto-start Claude CLI only once, when first client connects
217
+ if (!session.hasStartedClaude) {
218
+ session.hasStartedClaude = true
219
+ console.log(`[WS] Starting Claude CLI for session: ${sessionId}`)
220
+ setTimeout(() => {
221
+ ptyProcess.write('claude\r')
222
+ }, 200) // Small delay to ensure client is ready
223
+ }
224
+
225
+ const exitHandler = ptyProcess.onExit(({ exitCode }) => {
226
+ if (ws.readyState === WebSocket.OPEN) {
227
+ ws.send(JSON.stringify({ type: 'exit', code: exitCode }))
228
+ }
229
+ killSession(sessionId)
230
+ })
231
+
232
+ ws.on('message', (message: Buffer) => {
233
+ try {
234
+ const { type, data, cols, rows } = JSON.parse(message.toString())
235
+ switch (type) {
236
+ case 'input':
237
+ ptyProcess?.write(data)
238
+ break
239
+ case 'resize':
240
+ if (cols && rows) resizeSession(sessionId, cols, rows)
241
+ break
242
+ }
243
+ } catch (err) {
244
+ console.error('[WS] Error:', err)
245
+ }
246
+ })
247
+
248
+ ws.on('close', () => {
249
+ console.log(`[WS] PTY connection closed: ${sessionId}`)
250
+ dataHandler.dispose()
251
+ exitHandler.dispose()
252
+ })
253
+
254
+ ws.on('error', (error) => {
255
+ console.error(`[WS] Error for ${sessionId}:`, error)
256
+ })
257
+ })
258
+
259
+ server.listen(port, () => {
260
+ console.log(`> prjct ready on http://${hostname}:${port}`)
261
+ })
262
+ })
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }