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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Built for Claude - Ship fast, track progress, stay focused. Developer momentum tool for indie hackers.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {
@@ -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({ onNavigate }: { onNavigate?: () => void }) {
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="flex h-14 items-center justify-between px-3 border-b border-border">
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
- return (
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 gap-3 rounded-md px-3 py-2.5 transition-colors min-h-[44px]',
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 p-3 space-y-1">
59
- <Link
60
- href="/settings"
61
- onClick={onNavigate}
62
- className={cn(
63
- 'flex items-center gap-3 rounded-md px-3 py-2.5 transition-colors min-h-[44px]',
64
- pathname === '/settings'
65
- ? 'bg-accent text-accent-foreground'
66
- : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
67
- )}
68
- >
69
- <Settings className="h-5 w-5 shrink-0" />
70
- <span className="text-sm font-medium">Settings</span>
71
- </Link>
72
- <Link
73
- href="/help"
74
- onClick={onNavigate}
75
- className={cn(
76
- 'flex items-center gap-3 rounded-md px-3 py-2.5 transition-colors min-h-[44px]',
77
- pathname === '/help'
78
- ? 'bg-accent text-accent-foreground'
79
- : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
80
- )}
81
- >
82
- <HelpCircle className="h-5 w-5 shrink-0" />
83
- <span className="text-sm font-medium">Need help?</span>
84
- </Link>
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: Static sidebar
173
+ // Desktop: Collapsible sidebar
124
174
  return (
125
- <aside className="hidden md:flex h-full w-60 flex-col border-r border-border bg-card">
126
- <SidebarContent />
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
- // Try to run the generate-views CLI command
481
- await execAsync(`bun ${join(PRJCT_CLI_PATH, 'bin', 'generate-views.js')} --project=${projectId}`)
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) {
@@ -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
- req.on('data', chunk => { body += chunk })
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 server close
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