prjct-cli 0.11.0 → 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/package.json +11 -1
- 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
|
@@ -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
|
+
}
|