prjct-cli 0.12.0 → 0.12.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
CHANGED
|
@@ -7,81 +7,130 @@ import {
|
|
|
7
7
|
Settings,
|
|
8
8
|
HelpCircle,
|
|
9
9
|
Menu,
|
|
10
|
+
PanelLeft,
|
|
10
11
|
} from 'lucide-react'
|
|
11
12
|
import { Logo } from '@/components/Logo'
|
|
12
13
|
import { cn } from '@/lib/utils'
|
|
13
14
|
import { useState, useEffect } from 'react'
|
|
14
15
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
|
|
16
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
|
15
17
|
|
|
16
18
|
const navItems = [
|
|
17
19
|
{ href: '/', icon: LayoutDashboard, label: 'Dashboard' },
|
|
18
20
|
]
|
|
19
21
|
|
|
20
|
-
function SidebarContent({
|
|
22
|
+
function SidebarContent({
|
|
23
|
+
onNavigate,
|
|
24
|
+
isCollapsed = false,
|
|
25
|
+
onToggleCollapse
|
|
26
|
+
}: {
|
|
27
|
+
onNavigate?: () => void
|
|
28
|
+
isCollapsed?: boolean
|
|
29
|
+
onToggleCollapse?: () => void
|
|
30
|
+
}) {
|
|
21
31
|
const pathname = usePathname()
|
|
22
32
|
|
|
23
33
|
return (
|
|
24
34
|
<>
|
|
25
35
|
{/* Header */}
|
|
26
|
-
<div className=
|
|
36
|
+
<div className={cn(
|
|
37
|
+
"flex h-14 items-center border-b border-border",
|
|
38
|
+
isCollapsed ? "justify-center px-2" : "justify-between px-3"
|
|
39
|
+
)}>
|
|
27
40
|
<Link href="/" onClick={onNavigate}>
|
|
28
|
-
<Logo size="xs" showText rounded />
|
|
41
|
+
<Logo size="xs" showText={!isCollapsed} rounded />
|
|
29
42
|
</Link>
|
|
43
|
+
{onToggleCollapse && (
|
|
44
|
+
<Tooltip>
|
|
45
|
+
<TooltipTrigger asChild>
|
|
46
|
+
<button
|
|
47
|
+
onClick={onToggleCollapse}
|
|
48
|
+
className="h-8 w-8 rounded-md flex items-center justify-center text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
49
|
+
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
50
|
+
>
|
|
51
|
+
<PanelLeft className="h-4 w-4" />
|
|
52
|
+
</button>
|
|
53
|
+
</TooltipTrigger>
|
|
54
|
+
<TooltipContent side="right">
|
|
55
|
+
{isCollapsed ? "Expand" : "Collapse"}
|
|
56
|
+
</TooltipContent>
|
|
57
|
+
</Tooltip>
|
|
58
|
+
)}
|
|
30
59
|
</div>
|
|
31
60
|
|
|
32
61
|
{/* Nav */}
|
|
33
|
-
<nav className="flex-1 overflow-y-auto py-4 px-3">
|
|
62
|
+
<nav className={cn("flex-1 overflow-y-auto py-4", isCollapsed ? "px-2" : "px-3")}>
|
|
34
63
|
<div className="space-y-1">
|
|
35
64
|
{navItems.map(({ href, icon: Icon, label }) => {
|
|
36
65
|
const isActive = pathname === href || (href !== '/' && pathname.startsWith(href))
|
|
37
|
-
|
|
66
|
+
const linkContent = (
|
|
38
67
|
<Link
|
|
39
68
|
key={href}
|
|
40
69
|
href={href}
|
|
41
70
|
onClick={onNavigate}
|
|
42
71
|
className={cn(
|
|
43
|
-
'flex items-center
|
|
72
|
+
'flex items-center rounded-md transition-colors min-h-[44px]',
|
|
73
|
+
isCollapsed ? 'justify-center px-2' : 'gap-3 px-3',
|
|
74
|
+
'py-2.5',
|
|
44
75
|
isActive
|
|
45
76
|
? 'bg-accent text-accent-foreground'
|
|
46
77
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
|
47
78
|
)}
|
|
48
79
|
>
|
|
49
80
|
<Icon className="h-5 w-5 shrink-0" />
|
|
50
|
-
<span className="text-sm font-medium">{label}</span>
|
|
81
|
+
{!isCollapsed && <span className="text-sm font-medium">{label}</span>}
|
|
51
82
|
</Link>
|
|
52
83
|
)
|
|
84
|
+
|
|
85
|
+
if (isCollapsed) {
|
|
86
|
+
return (
|
|
87
|
+
<Tooltip key={href}>
|
|
88
|
+
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
|
|
89
|
+
<TooltipContent side="right">{label}</TooltipContent>
|
|
90
|
+
</Tooltip>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
return linkContent
|
|
53
94
|
})}
|
|
54
95
|
</div>
|
|
55
96
|
</nav>
|
|
56
97
|
|
|
57
98
|
{/* Footer */}
|
|
58
|
-
<div className="border-t border-border
|
|
59
|
-
|
|
60
|
-
href
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
<div className={cn("border-t border-border space-y-1", isCollapsed ? "p-2" : "p-3")}>
|
|
100
|
+
{[
|
|
101
|
+
{ href: '/settings', icon: Settings, label: 'Settings' },
|
|
102
|
+
{ href: '/help', icon: HelpCircle, label: 'Need help?' }
|
|
103
|
+
].map(({ href, icon: Icon, label }) => {
|
|
104
|
+
const isActive = pathname === href
|
|
105
|
+
const linkContent = (
|
|
106
|
+
<Link
|
|
107
|
+
key={href}
|
|
108
|
+
href={href}
|
|
109
|
+
onClick={onNavigate}
|
|
110
|
+
className={cn(
|
|
111
|
+
'flex items-center rounded-md transition-colors min-h-[44px]',
|
|
112
|
+
isCollapsed ? 'justify-center px-2' : 'gap-3 px-3',
|
|
113
|
+
'py-2.5',
|
|
114
|
+
isActive
|
|
115
|
+
? 'bg-accent text-accent-foreground'
|
|
116
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
<Icon className="h-5 w-5 shrink-0" />
|
|
120
|
+
{!isCollapsed && <span className="text-sm font-medium">{label}</span>}
|
|
121
|
+
</Link>
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (isCollapsed) {
|
|
125
|
+
return (
|
|
126
|
+
<Tooltip key={href}>
|
|
127
|
+
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
|
|
128
|
+
<TooltipContent side="right">{label}</TooltipContent>
|
|
129
|
+
</Tooltip>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
return linkContent
|
|
133
|
+
})}
|
|
85
134
|
</div>
|
|
86
135
|
</>
|
|
87
136
|
)
|
|
@@ -90,6 +139,7 @@ function SidebarContent({ onNavigate }: { onNavigate?: () => void }) {
|
|
|
90
139
|
export function AppSidebar() {
|
|
91
140
|
const [isOpen, setIsOpen] = useState(false)
|
|
92
141
|
const [isMobile, setIsMobile] = useState(false)
|
|
142
|
+
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
93
143
|
|
|
94
144
|
// Detect mobile on mount and resize
|
|
95
145
|
useEffect(() => {
|
|
@@ -120,10 +170,16 @@ export function AppSidebar() {
|
|
|
120
170
|
)
|
|
121
171
|
}
|
|
122
172
|
|
|
123
|
-
// Desktop:
|
|
173
|
+
// Desktop: Collapsible sidebar
|
|
124
174
|
return (
|
|
125
|
-
<aside className=
|
|
126
|
-
|
|
175
|
+
<aside className={cn(
|
|
176
|
+
"hidden md:flex h-full flex-col border-r border-border bg-card transition-all duration-200",
|
|
177
|
+
isCollapsed ? "w-14" : "w-60"
|
|
178
|
+
)}>
|
|
179
|
+
<SidebarContent
|
|
180
|
+
isCollapsed={isCollapsed}
|
|
181
|
+
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
|
|
182
|
+
/>
|
|
127
183
|
</aside>
|
|
128
184
|
)
|
|
129
185
|
}
|
|
@@ -474,11 +474,13 @@ DESCRIPTION EXTRACTION:
|
|
|
474
474
|
}
|
|
475
475
|
|
|
476
476
|
// Generate views from new JSON files
|
|
477
|
+
// Fire and forget - don't block request to prevent OOM from subprocess spawning
|
|
477
478
|
let viewsGenerated = false
|
|
478
479
|
if (allSuccess) {
|
|
479
480
|
try {
|
|
480
|
-
|
|
481
|
-
|
|
481
|
+
const child = exec(`bun ${join(PRJCT_CLI_PATH, 'bin', 'generate-views.js')} --project=${projectId}`)
|
|
482
|
+
child.on('error', (err) => console.error('[Views] Generation error:', err))
|
|
483
|
+
child.unref() // Allow parent to exit independently
|
|
482
484
|
viewsGenerated = true
|
|
483
485
|
results.push({ file: 'views', success: true })
|
|
484
486
|
} catch (viewError) {
|
package/packages/web/server.ts
CHANGED
|
@@ -100,7 +100,20 @@ app.prepare().then(() => {
|
|
|
100
100
|
// Handle session creation directly in server (bypasses API route isolation)
|
|
101
101
|
if (url.pathname === '/api/claude/sessions' && req.method === 'POST') {
|
|
102
102
|
let body = ''
|
|
103
|
-
|
|
103
|
+
let bodySize = 0
|
|
104
|
+
const MAX_BODY_SIZE = 1024 * 1024 // 1MB limit
|
|
105
|
+
|
|
106
|
+
req.on('data', chunk => {
|
|
107
|
+
bodySize += chunk.length
|
|
108
|
+
if (bodySize > MAX_BODY_SIZE) {
|
|
109
|
+
res.statusCode = 413
|
|
110
|
+
res.setHeader('Content-Type', 'application/json')
|
|
111
|
+
res.end(JSON.stringify({ success: false, error: 'Request body too large' }))
|
|
112
|
+
req.destroy()
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
body += chunk
|
|
116
|
+
})
|
|
104
117
|
req.on('end', () => {
|
|
105
118
|
try {
|
|
106
119
|
const { sessionId, projectDir } = JSON.parse(body)
|
|
@@ -171,11 +184,18 @@ app.prepare().then(() => {
|
|
|
171
184
|
})
|
|
172
185
|
}, HEARTBEAT_INTERVAL)
|
|
173
186
|
|
|
174
|
-
// Cleanup on
|
|
187
|
+
// Cleanup on WSS close
|
|
175
188
|
wss.on('close', () => {
|
|
176
189
|
clearInterval(heartbeatInterval)
|
|
177
190
|
})
|
|
178
191
|
|
|
192
|
+
// Cleanup on server close
|
|
193
|
+
server.on('close', () => {
|
|
194
|
+
clearInterval(heartbeatInterval)
|
|
195
|
+
// Kill all active sessions
|
|
196
|
+
sessions.forEach((_, sessionId) => killSession(sessionId))
|
|
197
|
+
})
|
|
198
|
+
|
|
179
199
|
server.on('upgrade', (request, socket, head) => {
|
|
180
200
|
const url = new URL(request.url || '', `http://${request.headers.host}`)
|
|
181
201
|
|